[deploy] PyMySQL 替换 + Tailscale 组网 + config.yaml + 路径修复

- db_layer.py: mysql-connector-python(19MB) → PyMySQL(45KB), 去掉连接池
- config_loader.py: 修复路径解析 (2级→3级 dirname), FAM-Core + FAM-Edge 均修复
- Tailscale 组网完成: Oracle=100.74.137.126, NAS=100.70.234.39, 互通验证通过
- NAS Python 依赖: flask/gunicorn/pymysql/PyYAML 全部安装成功
- NAS 代码部署: /volume1/web/sentinel-home-ai/, FAM-Core 7 模块导入 PASS
- NAS DB 连接验证: PyMySQL → MariaDB 10.11.11, 6 张表可见, PASS
- config.yaml 创建: FAM-Core + FAM-Edge + FAM-UI (Tailscale IP + 密码)
- requirements.txt: mysql-connector-python → PyMySQL
This commit is contained in:
ericwyuan
2026-08-19 23:27:28 +08:00
parent e5ac667586
commit 47658fa068
8 changed files with 149 additions and 53 deletions

View File

@@ -72,26 +72,27 @@
| 5 | Storage-Cleaner 补全 | `5e4587b` | 2026-08-19 | | 5 | Storage-Cleaner 补全 | `5e4587b` | 2026-08-19 |
| 6 | 服务器凭证写入 README | `ba905d2` | 2026-08-19 | | 6 | 服务器凭证写入 README | `ba905d2` | 2026-08-19 |
| 7 | requirements.txt 兼容 Python 3.8 | `c1bfac5` | 2026-08-19 | | 7 | requirements.txt 兼容 Python 3.8 | `c1bfac5` | 2026-08-19 |
| 8 | Oracle 部署: venv + 依赖安装 + 导入验证 | `50d8353` | 2026-08-19 | | 8 | Oracle 部署: venv + 依赖 + 导入验证 | `50d8353` | 2026-08-19 |
| 9 | 模型可行性基准测试 (llava-phi3 ≤8s PASS) | `50d8353` | 2026-08-19 | | 9 | 模型基准测试 (llava-phi3 ≤8s PASS) | `50d8353` | 2026-08-19 |
| 10 | OllamaAdapter 优化 (num_predict 参数) | `50d8353` | 2026-08-19 | | 10 | OllamaAdapter 优化 (num_predict) | `50d8353` | 2026-08-19 |
| 11 | Oracle Tailscale 安装 (1.102.2) | `50d8353` | 2026-08-19 | | 11 | Tailscale 安装 (Oracle + NAS) + 认证 | `e5ac667` | 2026-08-19 |
| 12 | NAS 环境探查: MariaDB10 运行中, Tailscale 已连接 | 进行中 | 2026-08-19 | | 12 | NAS MariaDB DDL 执行 (6 张表) | `e5ac667` | 2026-08-19 |
| 13 | NAS DDL 执行: sentinel_home_ai 库 + 6 张表创建 | 进行中 | 2026-08-19 | | 13 | db_layer.py 切换 PyMySQL (45KB 替代 19MB) | 进行中 | 2026-08-19 |
| 14 | README 更新: MariaDB 密码 + Tailscale IP | 进行中 | 2026-08-19 | | 14 | config_loader.py 路径修复 (3 级 dirname) | 进行中 | 2026-08-19 |
| 15 | NAS Python 依赖安装 (flask/gunicorn/pymysql/PyYAML) | 进行中 | 2026-08-19 |
| 16 | NAS 代码部署 + FAM-Core 导入验证 PASS | 进行中 | 2026-08-19 |
| 17 | NAS DB 连接验证 PASS (PyMySQL → MariaDB 10.11.11) | 进行中 | 2026-08-19 |
| 18 | config.yaml 实际配置 (FAM-Core + FAM-Edge + FAM-UI) | 进行中 | 2026-08-19 |
### 待完成 ### 待完成
| # | 任务 | 依赖 | 优先级 | | # | 任务 | 依赖 | 优先级 |
|---|------|------|--------| |---|------|------|--------|
| 15 | Oracle Tailscale 认证 (用户访问 URL) | - | **阻塞** | | 19 | NAS 启动 FAM-Core (gunicorn) | 依赖安装 ✅ | 高 |
| 16 | NAS 安装 pip3 + Python 依赖 (flask, mysql-connector, streamlit) | - | 高 | | 20 | Oracle 启动 FAM-Edge (gunicorn) | 依赖安装 ✅ | 高 |
| 17 | NAS 部署 FAM-Core (gunicorn + 配置) | pip3, MariaDB ✅ | | | 21 | NAS 安装 Streamlit + 部署 FAM-UI | pip3 ✅ | |
| 18 | NAS 部署 FAM-UI (streamlit) | pip3 | | | 22 | 端到端集成测试 | FAM-Core + FAM-Edge 启动 | |
| 19 | Oracle 部署 FAM-Edge (gunicorn + 配置 + systemd) | Tailscale 认证 | | | 23 | 单元测试 (JSON parser, circuit breaker, schema) | - | |
| 20 | 更新配置文件中的 Tailscale IP | Tailscale 认证 | 高 |
| 21 | 端到端集成测试 | 全部部署完成 | 高 |
| 22 | 单元测试 (JSON parser, circuit breaker, schema) | - | 中 |
## 技术决策记录 ## 技术决策记录

View File

@@ -0,0 +1,37 @@
# FAM-Core 配置文件 (NAS 端) - 实际部署配置
# Tailscale: NAS=100.70.234.39, Oracle=100.74.137.126
server:
host: "0.0.0.0"
port: 8000
database:
host: "127.0.0.1"
port: 3306
user: "root"
password: "iLoveJava5!"
database: "sentinel_home_ai"
unix_socket: "/run/mysqld/mysqld10.sock"
scheduler:
scan_interval: 60
video_dir: "/volume1/surveillance"
video_extensions: [".mp4", ".mkv", ".avi"]
file_stable_seconds: 60
camera_name: "客厅"
dispatcher:
poll_interval: 30
edge_url: "http://100.74.137.126:5000/api/edge/video/analyze"
webhook_url: "http://100.70.234.39:8000/api/core/callback/event"
max_retries: 3
video_server:
base_url: "http://100.70.234.39:8000/media"
token: "sentinel-media-2026"
video_dir: "/volume1/surveillance"
chat_handler:
ollama_url: "http://100.74.137.126:11434/api/generate"
model_name: "llava-phi3"
timeout: 120

View File

@@ -1,5 +1,5 @@
flask>=3.0.0 flask>=3.0.0
gunicorn>=21.2.0 gunicorn>=21.2.0
mysql-connector-python>=8.3.0 PyMySQL>=1.1.0
PyYAML>=6.0 PyYAML>=6.0
requests>=2.31.0 requests>=2.31.0

View File

@@ -24,7 +24,7 @@ def load_config(config_path=None):
"""加载 YAML 配置文件,自动解析 ${ENV_VAR} 引用""" """加载 YAML 配置文件,自动解析 ${ENV_VAR} 引用"""
if config_path is None: if config_path is None:
config_path = os.path.join( config_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
'config', 'config.yaml' 'config', 'config.yaml'
) )
with open(config_path, 'r', encoding='utf-8') as f: with open(config_path, 'r', encoding='utf-8') as f:

View File

@@ -1,9 +1,9 @@
""" """
数据库访问层 - MariaDB 连接管理与 CRUD 操作 数据库访问层 - MariaDB 连接管理与 CRUD 操作
使用 PyMySQL (纯 Python, ~45KB) 连接 MariaDB 服务器
""" """
import json import json
import mysql.connector import pymysql
from mysql.connector import pooling
from datetime import datetime from datetime import datetime
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@@ -13,7 +13,6 @@ from .logger import setup_logger
logger = setup_logger('fam-core.db') logger = setup_logger('fam-core.db')
_config = None _config = None
_pool = None
def get_config(): def get_config():
@@ -23,29 +22,22 @@ def get_config():
return _config return _config
def get_pool(): def get_conn():
"""获取数据库连接(单""" """获取数据库连接(单 worker gunicorn无需连接池"""
global _pool
if _pool is None:
cfg = get_config().get('database', {}) cfg = get_config().get('database', {})
_pool = pooling.MySQLConnectionPool( kwargs = dict(
host=cfg.get('host', '127.0.0.1'), host=cfg.get('host', '127.0.0.1'),
port=cfg.get('port', 3306), port=cfg.get('port', 3306),
user=cfg.get('user', 'root'), user=cfg.get('user', 'root'),
password=cfg.get('password', ''), password=cfg.get('password', ''),
database=cfg.get('database', 'sentinel_home_ai'), database=cfg.get('database', 'sentinel_home_ai'),
charset='utf8mb4', charset='utf8mb4',
collation='utf8mb4_unicode_ci',
pool_name='fam_core_pool',
pool_size=5,
autocommit=False autocommit=False
) )
return _pool unix_socket = cfg.get('unix_socket')
if unix_socket:
kwargs['unix_socket'] = unix_socket
def get_conn(): return pymysql.connect(**kwargs)
"""从连接池获取一个连接"""
return get_pool().get_connection()
# ============================================================ # ============================================================
@@ -73,7 +65,7 @@ def get_pending_tasks(limit=10) -> List[Dict]:
"""获取待处理任务""" """获取待处理任务"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute( cursor.execute(
"SELECT * FROM process_tasks WHERE status = 'PENDING' ORDER BY created_at ASC LIMIT %s", "SELECT * FROM process_tasks WHERE status = 'PENDING' ORDER BY created_at ASC LIMIT %s",
(limit,) (limit,)
@@ -87,7 +79,7 @@ def get_tasks_by_status(status: str, limit=10) -> List[Dict]:
"""按状态获取任务""" """按状态获取任务"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute( cursor.execute(
"SELECT * FROM process_tasks WHERE status = %s ORDER BY created_at ASC LIMIT %s", "SELECT * FROM process_tasks WHERE status = %s ORDER BY created_at ASC LIMIT %s",
(status, limit) (status, limit)
@@ -130,7 +122,7 @@ def get_task(task_id: int) -> Optional[Dict]:
"""获取单个任务""" """获取单个任务"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM process_tasks WHERE task_id = %s", (task_id,)) cursor.execute("SELECT * FROM process_tasks WHERE task_id = %s", (task_id,))
return cursor.fetchone() return cursor.fetchone()
finally: finally:
@@ -207,7 +199,7 @@ def query_event_details(person: str, queried_date: str) -> List[Dict]:
"""查询某人在某天的事件明细""" """查询某人在某天的事件明细"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute( cursor.execute(
"""SELECT frame_timestamp, camera_name, person, action, clothing, """SELECT frame_timestamp, camera_name, person, action, clothing,
is_attention_event is_attention_event
@@ -225,7 +217,7 @@ def get_recent_events(limit=20, offset=0, date_filter=None) -> List[Dict]:
"""获取事件列表(分页 + 日期筛选)""" """获取事件列表(分页 + 日期筛选)"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
if date_filter: if date_filter:
cursor.execute( cursor.execute(
"""SELECT me.event_id, me.task_id, me.event_start_time, me.event_end_time, """SELECT me.event_id, me.task_id, me.event_start_time, me.event_end_time,
@@ -281,7 +273,7 @@ def get_chat_history(limit=20, date_filter=None, person_filter=None) -> List[Dic
"""获取对话历史""" """获取对话历史"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
conditions = [] conditions = []
params = [] params = []
if date_filter: if date_filter:
@@ -326,7 +318,7 @@ def get_unnamed_members() -> List[Dict]:
"""获取未命名成员列表""" """获取未命名成员列表"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
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
@@ -343,7 +335,7 @@ def get_all_members(include_named=True, include_unnamed=True) -> List[Dict]:
"""获取所有成员""" """获取所有成员"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
conditions = [] conditions = []
if include_named and include_unnamed: if include_named and include_unnamed:
pass # 全部 pass # 全部
@@ -420,7 +412,7 @@ def get_known_members_context() -> str:
"""获取已命名+未命名成员清单,用于注入 VLM Prompt""" """获取已命名+未命名成员清单,用于注入 VLM Prompt"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute( cursor.execute(
"SELECT abstract_label, real_name, feature_description FROM family_members WHERE is_active = TRUE" "SELECT abstract_label, real_name, feature_description FROM family_members WHERE is_active = TRUE"
) )
@@ -446,7 +438,7 @@ def get_compute_provider_stats() -> List[Dict]:
"""获取 compute_provider 分布统计""" """获取 compute_provider 分布统计"""
conn = get_conn() conn = get_conn()
try: try:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute( cursor.execute(
"""SELECT """SELECT
JSON_UNQUOTE(JSON_EXTRACT(item, '$')) AS provider, JSON_UNQUOTE(JSON_EXTRACT(item, '$')) AS provider,

View File

@@ -0,0 +1,54 @@
# FAM-Edge 配置文件 (Oracle 端) - 实际部署配置
# Tailscale: Oracle=100.74.137.126, NAS=100.70.234.39
# NAS 端回调地址
nas:
webhook_url: "http://100.70.234.39:8000/api/core/callback/event"
media_base_url: "http://100.70.234.39:8000/media"
media_token: "sentinel-media-2026"
# Oracle 端服务
server:
host: "0.0.0.0"
port: 5000
max_concurrent_tasks: 1
# 关键帧筛选参数
video:
candidate_frames: 30
min_key_frames: 5
max_key_frames: 8
mse_threshold: 500
jpeg_quality: 80
max_long_edge: 1024
# 超时(秒)
timeout:
download: 60
vlm_visual: 240
vlm_fusion: 120
callback: 30
overall: 600
# 模型清单
models:
- provider: "ollama"
enabled: true
model_name: "llava-phi3"
base_url: "http://localhost:11434"
timeout: 240
num_predict: 500
circuit_breaker:
enabled: false
threshold: 5
cooldown: 900
- provider: "gemini"
enabled: false
model_name: "gemini-1.5-flash"
api_key: ""
timeout: 8
circuit_breaker:
enabled: true
threshold: 5
cooldown: 900

View File

@@ -22,7 +22,7 @@ def _resolve_env_vars(value):
def load_config(config_path=None): def load_config(config_path=None):
if config_path is None: if config_path is None:
config_path = os.path.join( config_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
'config', 'config.yaml' 'config', 'config.yaml'
) )
with open(config_path, 'r', encoding='utf-8') as f: with open(config_path, 'r', encoding='utf-8') as f:

12
fam-ui/config/config.yaml Normal file
View File

@@ -0,0 +1,12 @@
# FAM-UI 配置文件 (NAS 端) - 实际部署配置
# Tailscale: NAS=100.70.234.39
core_url: "http://127.0.0.1:8000"
database:
host: "127.0.0.1"
port: 3306
user: "root"
password: "iLoveJava5!"
database: "sentinel_home_ai"
unix_socket: "/run/mysqld/mysqld10.sock"