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:
ericwyuan
2026-08-19 23:59:28 +08:00
parent c8cd2f2665
commit 94a8805045
4 changed files with 35 additions and 11 deletions

View File

@@ -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

View File

@@ -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/generateOllama 未对外暴露)"""
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

View File

@@ -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

View File

@@ -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("模型来源分布")