Files
sentinel-home-ai/fam-edge/src/fam_edge/api_gateway/api_gateway.py

178 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
API-Gateway - Flask 蓝图(新架构 v3
端点:
GET /api/oracle/sync NAS 每 30 分钟拉取增量since + token 校验)
POST /api/oracle/people/correct NAS 推送手动命名校正label -> canonical_name
POST /api/edge/chat/ask 智能问答编排Gemini -> NVIDIA -> Ollama
GET /api/oracle/activity 实时服务状态 + 最近活动流
GET /health 健康检查
已移除v3 去除帧图/avatar 依赖,改用大模型特征值):
/api/oracle/video/<id>/thumb, /api/oracle/event/<id>/thumb,
/api/oracle/person/avatar —— 不再生成 jpgUI 读 sync_people.features_json
已移除(旧推送/分块/队列模式): /video/push, /enqueue, /chunk, /assemble,
/results, /queue/stats, /mark_frames
"""
import os
from flask import Blueprint, request, jsonify
from ..logger import setup_logger
from .. import state
from ..qa import QAOrchestrator
logger = setup_logger('fam-edge.api_gateway')
api_bp = Blueprint('api_gateway', __name__)
_qa = None
def get_qa():
global _qa
if _qa is None:
_qa = QAOrchestrator()
return _qa
def _check_token() -> bool:
expected = _sync_token()
token = request.args.get('token') or request.form.get('token') or \
(request.get_json(silent=True) or {}).get('token', '')
return bool(expected) and token == expected
_SYNC_TOK = None
def _sync_token():
global _SYNC_TOK
if _SYNC_TOK is None:
from ..config_loader import load_config
_SYNC_TOK = load_config().get('sync_api', {}).get('token', '${ORACLE_SYNC_TOKEN}')
if _SYNC_TOK.startswith('${') and _SYNC_TOK.endswith('}'):
_SYNC_TOK = os.environ.get(_SYNC_TOK[2:-1], '')
return _SYNC_TOK or ''
def _now_iso_str() -> str:
from datetime import datetime, timezone, timedelta
return datetime.now(timezone(timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S')
@api_bp.route('/api/oracle/sync', methods=['GET'])
def sync_pull():
"""NAS 拉取增量数据。since=ISO 时间字符串(默认 '' 拉全量)。
返回: {videos:[...], events:[...], people:[...], server_time}
"""
if not _check_token():
return jsonify({"error": "unauthorized"}), 401
since = request.args.get('since', '')
try:
delta = state.get_db().get_sync_delta(since)
except Exception as e:
logger.error(f"sync_pull 异常: {e}")
return jsonify({"error": str(e)}), 500
return jsonify(delta), 200
@api_bp.route('/api/oracle/people/correct', methods=['POST'])
def people_correct():
"""NAS 手动命名校正推送。
请求: {"label": "人物A", "canonical_name": "张三", "token": "..."}
更新 people 表manual 优先,不被 LLM 覆盖),立即重算 known_members_context。
"""
if not _check_token():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Invalid JSON"}), 400
label = (data.get('label') or '').strip()
canonical = (data.get('canonical_name') or '').strip()
if not label or not canonical:
return jsonify({"error": "缺少 label / canonical_name"}), 400
try:
state.get_db().set_canonical(label, canonical, source='manual')
except Exception as e:
logger.error(f"people_correct 异常: {e}")
return jsonify({"error": str(e)}), 500
return jsonify({"status": "ok", "label": label, "canonical_name": canonical}), 200
@api_bp.route('/api/edge/chat/ask', methods=['POST'])
def chat_ask():
"""智能问答编排Gemini → NVIDIA → 本地 Ollama两云端都失败才用本地兜底
请求: {"prompt": "...", "max_tokens": 512}
响应: {"answer": "...", "provider": "gemini"|"nvidia"|"ollama"}
"""
data = request.get_json(silent=True)
if not data or 'prompt' not in data:
return jsonify({"error": "缺少必填字段: prompt"}), 400
prompt = data['prompt']
max_tokens = int(data.get('max_tokens', 1024))
answer, provider = get_qa().run_qa(prompt, max_tokens=max_tokens)
if answer is None:
return jsonify({
"error": "所有模型均不可用Gemini / NVIDIA / Ollama 全部失败)"
}), 503
return jsonify({"answer": answer, "provider": provider}), 200
@api_bp.route('/api/oracle/activity', methods=['GET'])
def activity():
"""实时服务状态 + 最近活动流token 校验)。
返回各服务当前状态(队列/当前处理视频/rclone 最近同步/人物合并/模型调用)
与最近 50 条活动service_activity只保留 7 天)。
"""
if not _check_token():
return jsonify({"error": "unauthorized"}), 401
db = state.get_db()
queue = state.get_queue()
# 队列实时状态
q_status = None
if queue is not None:
try:
q_status = queue.status()
except Exception as e:
logger.warning(f"queue.status 异常: {e}")
# 各服务最近活动rclone / person / model 切换)
def _last_activity(service):
row = db._conn.execute(
"SELECT service, action, detail, ts FROM service_activity "
"WHERE service=? ORDER BY id DESC LIMIT 1", (service,)).fetchone()
return dict(row) if row else None
# 最近模型调用(实时模型卡)
model_calls = db._conn.execute(
"SELECT id, provider, model, filename, started_at, duration_sec, "
"success, error FROM model_calls ORDER BY id DESC LIMIT 5").fetchall()
return jsonify({
"queue": q_status,
"db": db.get_queue_status(),
"rclone": _last_activity('rclone'),
"person": _last_activity('person'),
"model_calls": [dict(m) for m in model_calls],
"activities": db.get_recent_activities(50),
"ts": _now_iso_str(),
}), 200
@api_bp.route('/health', methods=['GET'])
def health():
"""健康检查"""
try:
db = state.get_db()
vids = db._conn.execute(
"SELECT COUNT(*) c FROM videos WHERE status='done'").fetchone()['c']
return jsonify({"status": "ok", "processed_videos": vids}), 200
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 500