feat: FAM-Edge chat proxy + PyMySQL migration for FAM-UI + config update
- FAM-Edge: add /api/edge/chat proxy endpoint forwarding to local Ollama (Ollama port 11434 not exposed externally, FAM-Edge acts as reverse proxy) - FAM-Core config: edge_url and ollama_url switched from Tailscale IP to Oracle public IP (Tailscale firewall blocking between NAS and Oracle) - FAM-UI: migrate mysql.connector to PyMySQL (same as FAM-Core) - FAM-UI: cursor(dictionary=True) replaced with cursorclass=DictCursor - End-to-end chat verified: FAM-Core -> FAM-Edge proxy -> Ollama -> response Answer: 今天没有观察到张三 (no events in DB yet, expected)
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
# FAM-Core 配置文件 (NAS 端) - 实际部署配置
|
# FAM-Core 配置文件 (NAS 端) - 实际部署配置
|
||||||
# Tailscale: NAS=100.70.234.39, Oracle=100.74.137.126
|
# Tailscale: NAS=100.70.234.39, Oracle=100.74.137.126
|
||||||
|
# 注: Tailscale 防火墙待修复,当前 edge_url 使用 Oracle 公网 IP
|
||||||
|
# Ollama 未对外暴露,chat_handler 通过 FAM-Edge 代理
|
||||||
|
|
||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
@@ -22,7 +24,7 @@ scheduler:
|
|||||||
|
|
||||||
dispatcher:
|
dispatcher:
|
||||||
poll_interval: 30
|
poll_interval: 30
|
||||||
edge_url: "http://100.74.137.126:5000/api/edge/video/analyze"
|
edge_url: "http://129.146.203.203:5000/api/edge/video/analyze"
|
||||||
webhook_url: "http://100.70.234.39:8000/api/core/callback/event"
|
webhook_url: "http://100.70.234.39:8000/api/core/callback/event"
|
||||||
max_retries: 3
|
max_retries: 3
|
||||||
|
|
||||||
@@ -32,6 +34,6 @@ video_server:
|
|||||||
video_dir: "/volume1/surveillance"
|
video_dir: "/volume1/surveillance"
|
||||||
|
|
||||||
chat_handler:
|
chat_handler:
|
||||||
ollama_url: "http://100.74.137.126:11434/api/generate"
|
ollama_url: "http://129.146.203.203:5000/api/edge/chat"
|
||||||
model_name: "llava-phi3"
|
model_name: "llava-phi3"
|
||||||
timeout: 120
|
timeout: 120
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ API-Gateway - Flask 蓝图,接收任务
|
|||||||
同时只允许 1 个任务在处理;新任务到达时若当前有任务处理中,返回 429
|
同时只允许 1 个任务在处理;新任务到达时若当前有任务处理中,返回 429
|
||||||
"""
|
"""
|
||||||
import threading
|
import threading
|
||||||
|
import requests
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
|
|
||||||
from ..logger import setup_logger
|
from ..logger import setup_logger
|
||||||
@@ -87,3 +88,22 @@ def health():
|
|||||||
"healthy_models": [a.provider_name for a in healthy],
|
"healthy_models": [a.provider_name for a in healthy],
|
||||||
"processing": _currently_processing
|
"processing": _currently_processing
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/api/edge/chat', methods=['POST'])
|
||||||
|
def chat_proxy():
|
||||||
|
"""代理转发至本地 Ollama /api/generate(Ollama 未对外暴露)"""
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "Invalid JSON"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
'http://127.0.0.1:11434/api/generate',
|
||||||
|
json=data,
|
||||||
|
timeout=data.get('options', {}).get('timeout', 120)
|
||||||
|
)
|
||||||
|
return jsonify(resp.json()), resp.status_code
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"Chat proxy error: {e}")
|
||||||
|
return jsonify({"error": f"Ollama unreachable: {e}"}), 502
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
streamlit>=1.30.0
|
streamlit>=1.30.0
|
||||||
mysql-connector-python>=8.3.0
|
PyMySQL>=1.1.0
|
||||||
pandas>=2.1.0
|
pandas>=2.1.0
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import mysql.connector
|
import pymysql
|
||||||
|
import pymysql.cursors
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
@@ -26,13 +27,14 @@ _db_cfg = _cfg.get('database', {})
|
|||||||
|
|
||||||
def get_db_conn():
|
def get_db_conn():
|
||||||
"""获取数据库连接"""
|
"""获取数据库连接"""
|
||||||
return mysql.connector.connect(
|
return pymysql.connect(
|
||||||
host=_db_cfg.get('host', '127.0.0.1'),
|
host=_db_cfg.get('host', '127.0.0.1'),
|
||||||
port=_db_cfg.get('port', 3306),
|
port=_db_cfg.get('port', 3306),
|
||||||
user=_db_cfg.get('user', 'root'),
|
user=_db_cfg.get('user', 'root'),
|
||||||
password=_db_cfg.get('password', ''),
|
password=_db_cfg.get('password', ''),
|
||||||
database=_db_cfg.get('database', 'sentinel_home_ai'),
|
database=_db_cfg.get('database', 'sentinel_home_ai'),
|
||||||
charset='utf8mb4'
|
charset='utf8mb4',
|
||||||
|
cursorclass=pymysql.cursors.DictCursor
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ if page == "📊 事件列表":
|
|||||||
|
|
||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 查询条件
|
# 查询条件
|
||||||
date_str = date_filter.isoformat() if date_filter else None
|
date_str = date_filter.isoformat() if date_filter else None
|
||||||
@@ -185,7 +187,7 @@ elif page == "💬 AI 对话":
|
|||||||
# 快捷人物按钮
|
# 快捷人物按钮
|
||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT DISTINCT real_name FROM family_members WHERE real_name IS NOT NULL AND is_active = TRUE")
|
cursor.execute("SELECT DISTINCT real_name FROM family_members WHERE real_name IS NOT NULL AND is_active = TRUE")
|
||||||
named = [row['real_name'] for row in cursor.fetchall()]
|
named = [row['real_name'] for row in cursor.fetchall()]
|
||||||
finally:
|
finally:
|
||||||
@@ -256,7 +258,7 @@ elif page == "📝 对话历史":
|
|||||||
|
|
||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""SELECT chat_id, user_question, ai_answer, context_summary,
|
"""SELECT chat_id, user_question, ai_answer, context_summary,
|
||||||
queried_date, queried_person, created_at
|
queried_date, queried_person, created_at
|
||||||
@@ -311,7 +313,7 @@ elif page == "👤 成员命名":
|
|||||||
st.subheader("未命名人物")
|
st.subheader("未命名人物")
|
||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""SELECT fm.abstract_label, fm.feature_description, fm.first_seen_at,
|
"""SELECT fm.abstract_label, fm.feature_description, fm.first_seen_at,
|
||||||
(SELECT COUNT(*) FROM event_details ed WHERE ed.person = fm.abstract_label) AS event_count
|
(SELECT COUNT(*) FROM event_details ed WHERE ed.person = fm.abstract_label) AS event_count
|
||||||
@@ -399,7 +401,7 @@ elif page == "📈 统计图表":
|
|||||||
|
|
||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# compute_provider 分布
|
# compute_provider 分布
|
||||||
st.subheader("模型来源分布")
|
st.subheader("模型来源分布")
|
||||||
|
|||||||
Reference in New Issue
Block a user