[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:
31
PROGRESS.md
31
PROGRESS.md
@@ -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) | - | 中 |
|
|
||||||
|
|
||||||
## 技术决策记录
|
## 技术决策记录
|
||||||
|
|
||||||
|
|||||||
37
fam-core/config/config.yaml
Normal file
37
fam-core/config/config.yaml
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
54
fam-edge/config/config.yaml
Normal file
54
fam-edge/config/config.yaml
Normal 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
|
||||||
@@ -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
12
fam-ui/config/config.yaml
Normal 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"
|
||||||
Reference in New Issue
Block a user