feat(监控): 实时服务状态界面 - Oracle 新增 service_activity 活动表(只保留7天,写入时清理)与 /api/oracle/activity 接口(队列实时状态/当前处理视频/模型调用/rclone/人物合并/最近50条活动);VideoQueue 打点+当前处理跟踪+status();PersonService 打点;rclone_sync.sh 同步结果写活动表;fam-ui 新增'🖥 服务状态'页(状态卡+活动时间流,直连 Oracle 实时拉取)

This commit is contained in:
ericwyuan
2026-08-21 15:52:36 +08:00
parent da7631976d
commit 0d0a7f6ef8
7 changed files with 311 additions and 9 deletions

View File

@@ -58,6 +58,9 @@ class VideoQueue:
self._producer = None
self._consumers: List[threading.Thread] = []
self._stats = {"produced": 0, "consumed_ok": 0, "consumed_fail": 0}
# 实时状态:当前正在处理的视频(服务状态界面用)
self._current = None
self._current_lock = threading.Lock()
# ------------------------------------------------------------------
# 生产者
@@ -97,16 +100,19 @@ class VideoQueue:
vid = self.db.ensure_video(fn, path, camera_name=self.camera_name)
self.db.set_video_file_status(vid, True, '', vmeta)
logger.info(f"登记新视频并入队: {fn} (id={vid}, meta={vmeta})")
self.db.record_activity('queue', 'register', f"{fn} (id={vid})")
self._enqueue(vid)
continue
vid = self.db.ensure_video(fn, path, camera_name=self.camera_name)
logger.info(f"登记新视频并入队: {fn} (id={vid})")
self.db.record_activity('queue', 'register', f"{fn} (id={vid})")
self._enqueue(vid)
elif row['status'] in ('pending', 'failed') and self._retry_allowed(row):
self._enqueue(row['id'])
elif row['status'] == 'done' and self._file_changed(row, path):
# 文件被覆盖rclone 重新同步/更新):重置 pending 重新分析
logger.info(f"文件内容变更,重置重新分析: {fn} (id={row['id']})")
self.db.record_activity('queue', 'reanalyze', f"{fn} (id={row['id']})")
self.db._conn.execute(
"UPDATE videos SET status='pending', retry_count=0, summary_json=NULL, "
"events_json=NULL, people_json=NULL, compute_provider=NULL, "
@@ -209,13 +215,38 @@ class VideoQueue:
if not self._retry_allowed(row):
logger.warning(f"[video_id={video_id}] 已达重试上限({row['retry_count']}),放弃")
return
ok = processor.process_video(
video_id, row['filename'], row['local_path'],
timeout_multiplier=self.timeout_multiplier)
if ok:
self._stats["consumed_ok"] += 1
else:
self._stats["consumed_fail"] += 1
fn = row['filename'] or ''
with self._current_lock:
self._current = {"video_id": video_id, "filename": fn,
"started_at": None}
self.db.record_activity('queue', 'process_start', f"video {video_id} {fn}")
try:
ok = processor.process_video(
video_id, fn, row['local_path'],
timeout_multiplier=self.timeout_multiplier)
if ok:
self._stats["consumed_ok"] += 1
self.db.record_activity('queue', 'process_done', f"video {video_id} {fn}")
else:
self._stats["consumed_fail"] += 1
self.db.record_activity('queue', 'process_fail', f"video {video_id} {fn}")
finally:
with self._current_lock:
self._current = None
# ------------------------------------------------------------------
# 实时状态(服务状态界面用)
# ------------------------------------------------------------------
def status(self) -> Dict:
with self._current_lock:
cur = dict(self._current) if self._current else None
return {
"queued": self._queue.qsize(),
"current": cur,
"stats": dict(self._stats),
"max_concurrent": self.max_concurrent,
"running": (self._producer is not None and self._producer.is_alive()),
}
# ------------------------------------------------------------------
# 生命周期