From 94a8805045c95e0b2667ebbf1c38e6c13446be07 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Wed, 19 Aug 2026 23:59:28 +0800 Subject: [PATCH] feat: FAM-Edge chat proxy + PyMySQL migration for FAM-UI + config update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- fam-core/config/config.yaml | 6 ++++-- .../src/fam_edge/api_gateway/api_gateway.py | 20 +++++++++++++++++++ fam-ui/requirements.txt | 2 +- fam-ui/src/app.py | 18 +++++++++-------- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/fam-core/config/config.yaml b/fam-core/config/config.yaml index e466d6f..bdd6bdc 100644 --- a/fam-core/config/config.yaml +++ b/fam-core/config/config.yaml @@ -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 diff --git a/fam-edge/src/fam_edge/api_gateway/api_gateway.py b/fam-edge/src/fam_edge/api_gateway/api_gateway.py index 4a0794f..ef613f6 100644 --- a/fam-edge/src/fam_edge/api_gateway/api_gateway.py +++ b/fam-edge/src/fam_edge/api_gateway/api_gateway.py @@ -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/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 diff --git a/fam-ui/requirements.txt b/fam-ui/requirements.txt index 58c127b..35222d2 100644 --- a/fam-ui/requirements.txt +++ b/fam-ui/requirements.txt @@ -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 diff --git a/fam-ui/src/app.py b/fam-ui/src/app.py index 56703b6..64095d6 100644 --- a/fam-ui/src/app.py +++ b/fam-ui/src/app.py @@ -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("模型来源分布")