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 端) - 实际部署配置
# Tailscale: NAS=100.70.234.39, Oracle=100.74.137.126
# 注: Tailscale 防火墙待修复,当前 edge_url 使用 Oracle 公网 IP
# Ollama 未对外暴露chat_handler 通过 FAM-Edge 代理
server:
host: "0.0.0.0"
@@ -22,7 +24,7 @@ scheduler:
dispatcher:
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"
max_retries: 3
@@ -32,6 +34,6 @@ video_server:
video_dir: "/volume1/surveillance"
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"
timeout: 120

View File

@@ -4,6 +4,7 @@ API-Gateway - Flask 蓝图,接收任务
同时只允许 1 个任务在处理;新任务到达时若当前有任务处理中,返回 429
"""
import threading
import requests
from flask import Blueprint, request, jsonify
from ..logger import setup_logger
@@ -87,3 +88,22 @@ def health():
"healthy_models": [a.provider_name for a in healthy],
"processing": _currently_processing
}), 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
mysql-connector-python>=8.3.0
PyMySQL>=1.1.0
pandas>=2.1.0
requests>=2.31.0
PyYAML>=6.0

View File

@@ -8,7 +8,8 @@ import os
import sys
import requests
import streamlit as st
import mysql.connector
import pymysql
import pymysql.cursors
import pandas as pd
import json
from datetime import datetime, date
@@ -26,13 +27,14 @@ _db_cfg = _cfg.get('database', {})
def get_db_conn():
"""获取数据库连接"""
return mysql.connector.connect(
return pymysql.connect(
host=_db_cfg.get('host', '127.0.0.1'),
port=_db_cfg.get('port', 3306),
user=_db_cfg.get('user', 'root'),
password=_db_cfg.get('password', ''),
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()
try:
cursor = conn.cursor(dictionary=True)
cursor = conn.cursor()
# 查询条件
date_str = date_filter.isoformat() if date_filter else None
@@ -185,7 +187,7 @@ elif page == "💬 AI 对话":
# 快捷人物按钮
conn = get_db_conn()
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")
named = [row['real_name'] for row in cursor.fetchall()]
finally:
@@ -256,7 +258,7 @@ elif page == "📝 对话历史":
conn = get_db_conn()
try:
cursor = conn.cursor(dictionary=True)
cursor = conn.cursor()
cursor.execute(
"""SELECT chat_id, user_question, ai_answer, context_summary,
queried_date, queried_person, created_at
@@ -311,7 +313,7 @@ elif page == "👤 成员命名":
st.subheader("未命名人物")
conn = get_db_conn()
try:
cursor = conn.cursor(dictionary=True)
cursor = conn.cursor()
cursor.execute(
"""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
@@ -399,7 +401,7 @@ elif page == "📈 统计图表":
conn = get_db_conn()
try:
cursor = conn.cursor(dictionary=True)
cursor = conn.cursor()
# compute_provider 分布
st.subheader("模型来源分布")