feat: 交互Agent + VLM优化 + 企微演示模式

- 新增交互Agent调度器(意图识别 + 工单/查询/报表/闲聊4个Handler)
- 新增工单服务、Excel报表生成器、企微消息加解密模块
- VLM提示词优化(角色设定、≤25字描述、布尔值优先输出)
- VLM降级策略(入侵默认放行、离岗默认拦截)
- 企微演示模式(WECHAT_TEST_UIDS兜底 + SERVICE_BASE_URL修复)
- 新增Agent回调路由和测试接口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 10:42:32 +08:00
parent 1d84456c0f
commit 7cc4f604d0
13 changed files with 827 additions and 54 deletions

View File

@@ -16,32 +16,22 @@ logger = logging.getLogger(__name__)
# 算法类型 → VLM Prompt 模板
VLM_PROMPTS = {
"leave_post": """分析这张岗位监控截图
摄像头位置:{camera_name},监控区域:{roi_name}
边缘AI检测到该区域无人在岗请你复核该区域内是否确实没有工作人员在岗
"leave_post": """你是安防监控AI复核员。判断{roi_name}岗位区域内是否有人在岗
confirmed=true表示确实无人在岗告警成立false表示有人误报
description用≤25字描述画面。
仅输出JSON{{"confirmed":true,"description":"..."}}""",
输出严格的JSON格式不要输出其他内容
{{"confirmed": true, "description": "一句话描述当前画面"}}
说明confirmed=true 表示确实无人在岗告警成立confirmed=false 表示有人在岗(误报)。""",
"intrusion": """分析这张周界监控截图。
摄像头位置:{camera_name},监控区域:{roi_name}
边缘AI检测到该区域有人员入侵请你复核该区域内是否确实有人员出现
输出严格的JSON格式不要输出其他内容
{{"confirmed": true, "description": "一句话描述当前画面"}}
说明confirmed=true 表示确实有人入侵告警成立confirmed=false 表示无人(误报)。""",
"intrusion": """你是安防监控AI复核员。判断{roi_name}周界区域内是否有人员入侵。
confirmed=true表示确实有人入侵告警成立false表示无人误报
description用≤25字描述画面。
仅输出JSON{{"confirmed":true,"description":"..."}}""",
}
# 通用降级 prompt未知算法类型时使用
DEFAULT_PROMPT = """分析这张监控截图
摄像头位置:{camera_name},监控区域:{roi_name}
边缘AI触发了 {alarm_type} 告警,请判断告警是否属实
输出严格的JSON格式不要输出其他内容
{{"confirmed": true, "description": "一句话描述当前画面"}}"""
DEFAULT_PROMPT = """你是安防监控AI复核员。边缘AI触发了{alarm_type}告警,判断告警是否属实
confirmed=true表示告警成立false表示误报
description用≤25字描述画面
仅输出JSON{{"confirmed":true,"description":"..."}}"""
class VLMService:
@@ -74,6 +64,16 @@ class VLMService:
def enabled(self) -> bool:
return self._enabled
@staticmethod
def _fallback_result(alarm_type: str, camera_name: str, reason: str) -> Dict:
"""降级结果入侵默认放行宁可多报离岗默认拦截避免VLM不可用时误推"""
confirmed = alarm_type != "leave_post"
return {
"confirmed": confirmed,
"description": f"{camera_name or '未知位置'} 触发 {alarm_type} 告警({reason}",
"skipped": True,
}
async def verify_alarm(
self,
snapshot_url: str,
@@ -95,19 +95,11 @@ class VLMService:
- skipped=True 表示 VLM 未调用(降级处理)
"""
if not self._enabled or not self._client:
return {
"confirmed": True,
"description": f"{camera_name or '未知位置'} 触发 {alarm_type} 告警",
"skipped": True,
}
return self._fallback_result(alarm_type, camera_name, "VLM未启用")
if not snapshot_url:
logger.warning("告警无截图URL跳过 VLM 复核")
return {
"confirmed": True,
"description": f"{camera_name or '未知位置'} 触发 {alarm_type} 告警(无截图)",
"skipped": True,
}
return self._fallback_result(alarm_type, camera_name, "无截图")
# 选择 prompt 模板
template = VLM_PROMPTS.get(alarm_type, DEFAULT_PROMPT)
@@ -144,7 +136,7 @@ class VLMService:
result = json.loads(content)
logger.info(
f"VLM 复核完成: confirmed={result.get('confirmed')}, "
f"desc={result.get('description', '')[:50]}"
f"desc={result.get('description', '')[:30]}"
)
return {
"confirmed": result.get("confirmed", True),
@@ -154,25 +146,13 @@ class VLMService:
except asyncio.TimeoutError:
logger.warning(f"VLM 复核超时 ({self._timeout}s),降级处理")
return {
"confirmed": True,
"description": f"{camera_name or '未知位置'} 触发 {alarm_type} 告警VLM超时",
"skipped": True,
}
return self._fallback_result(alarm_type, camera_name, "VLM超时")
except json.JSONDecodeError as e:
logger.warning(f"VLM 返回内容解析失败: {e}, 原始内容: {content[:200]}")
return {
"confirmed": True,
"description": content[:100] if content else "VLM返回异常",
"skipped": True,
}
return self._fallback_result(alarm_type, camera_name, "解析失败")
except Exception as e:
logger.error(f"VLM 调用异常: {e}")
return {
"confirmed": True,
"description": f"{camera_name or '未知位置'} 触发 {alarm_type} 告警VLM异常",
"skipped": True,
}
return self._fallback_result(alarm_type, camera_name, "VLM异常")
# 全局单例