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 端) - 实际部署配置
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("模型来源分布")
|
||||
|
||||
Reference in New Issue
Block a user