全景
┌─────────────── Android 平板(与企微同机)───────────────┐
│ 企业微信 App ←无障碍事件/点击→ SevFishComVBot App │
│ ├ Service 循环:读会话/读消息/执行指令 │
│ ├ Observer 监听:无障碍窗口变化 │
│ └ WeworkRoomUtil / WeworkOperationImpl: 定位与操作 │
└───────────────────────────┬──────────────────────────────────┘
│ WebSocket (JSON) ws://host:8765/webserver/wework/{client_id}
┌───────────────────────────┴──────────────────────────────────┐
│ FastAPI 服务端 (uvicorn, 8765) │
│ │
│ routes/ websocket 接入 · admin 后台 · api · ext 客户控制台 │
│ workflows / tasks / memory / knowledge / tables … │
│ services/ aivbot_service 连接注册表 + 事件分发 + 自动回复 v2 │
│ agent_service LLM 调用 + 工具构建/执行/授权/审计 │
│ task_service 定时 + 条件触发 │
│ memory_service 长期记忆 │
│ workflow_engine 节点执行 │
│ knowledge/table/group/robot/skill/ext_user/provision│
│ models/ agent workflow task robot group message table │
└───────┬───────────────────────────────┬────────────────────────┘
│ │
MySQL 8(唯一持久层) LLM / Embedding(OpenAI 兼容 HTTP)
图里以平板为例:企微的平板端和手机端能同时在线,机器人跑平板不会挤掉本人手机;用手机当执行端也能跑,代价是那台手机的企微长期占用登录(见 01-项目介绍)。
服务端:14 个 service,各管一段
| service | 职责 | 什么时候碰它 |
|---|---|---|
aivbot_service | ★ 与客户端通信的唯一核心:维护 active_connections 注册表、send_command 下发指令、事件订阅、自动回复 v2 引擎(合并窗口、设备级并发限额) | 协议、收发、回复触发 |
agent_service | ★ LLM 调用、system prompt 拼装、工具定义与执行、逐项授权校验、tool_call_audit 落库 | 智能体行为、工具、权限 |
workflow_engine / workflow_service | 节点执行器 / 工作流 CRUD 与运行日志 | 工作流 |
task_service | cron/interval/once/offset 调度 + 条件触发(事件流 → 轻量信号 → 共享轮询三层) | 任务 |
memory_service | 长期记忆:客户、别名自愈、事实去重、注入拼装、护栏 | 记忆 |
memory_cleanup_service | 定时让模型压缩合并智能体写入的记忆(只动 source='agent') | 记忆清理 |
knowledge_service | 知识库:切块、向量化(本地哈希或 embedding 接口)、相似度检索 | 知识库 |
table_service | 数据表格:建表、CRUD、筛选、CSV 导出、按表授权 | 表格 |
group_service | 组织与成员、成员类型(设备/表格/智能体/知识库/记忆集) | 组织 |
robot_service | 设备注册、绑定智能体、记忆集绑定 | 设备 |
skill_service | 技能:按群类型/关键词/正则命中后作为软约束注入 | 技能 |
ext_user_service | 外网客户账号、会话、可见范围 | /ext |
provision_service | 「AI 配置组织」的 10 个 provision_* 工具与默认提示词 | 自动建组织 |
main.py 钩子 | 启动时连库、ensure_table()、在线状态清零、自增值抬到 1000 | 启动 |
服务端:路由与页面
websocket.py— 三条连接路径:/webserver/wework/{client_id}(正式)、/ws/{client_id}、/wsadmin.py— 管理后台(HTML 直出,无前端构建):首页/成员/设备/回复策略/智能体/知识库/长期记忆/表格/工作流/任务/技能/安全日志api.py/aivbot_api.py— 内部 HTTP API(状态、设备、指令下发)ext.py— 客户控制台/ext(只读为主,唯一可写的是长期记忆)provision.py— 配置智能体的对话台- 其余
agents/workflows/tasks/memory/knowledge/tables/skills/groups/test— 对应资源
数据模型(MySQL,启动时自动建表)
| 表 | 说明 |
|---|---|
orgs / org_group_members | 组织 / 成员(成员类型决定资源归属) |
robots | 设备:设备号、名称、绑定的智能体、在线状态(在线判定不读这列,见下) |
agents / agent_conversations | 智能体(LLM 配置、system prompt、工具授权、指令授权)/ 会话记录 |
reply_strategies | 回复策略:config = {room_type: {mode, merge_window, track_duration}} |
workflows / workflow_runs / workflow_node_logs | 工作流定义 / 每次运行 / 每个节点 |
tasks | 任务:触发器 × 动作 |
memory_sets / memory_customers / memory_items / robot_memory_sets / memory_cleanup_log | 长期记忆五表 |
knowledge_bases / knowledge_chunks | 知识库 / 切块与向量 |
data_tables / agent_table_grants | 数据表格 / 按表授权 |
skills | 技能 |
tool_call_audit | 每次工具调用(含失败) |
messages | 会话消息(用于上下文与审计) |
两条容易踩的实现约定
- 在线状态唯一事实来源是内存里的
active_connections,不是robots.status列。列只是"最后一次连接事件",多条路径不会回写它。改状态相关逻辑时按注册表判。 - 表自增值在启动时被抬到 1000,新 id 从 1000 起。测试别断言
id=1。
客户端:三个 Gradle 模块
| 模块 | 内容 |
|---|---|
app | 全部业务:activity(设置/引导/日志)、service(无障碍服务、循环、操作实现)、observer、utils、model |
baselibrary | 基础库 |
floatwindow | 悬浮窗(EasyFloat) |
关键文件:
service/WeworkLoopImpl.kt— 主循环:切会话列表、判断未读、进房间、读消息、上报service/WeworkOperationImpl.kt— 具体操作:发消息、@、拉群、加好友、转发……(对应服务端 203~234 号指令)utils/WeworkRoomUtil.kt— 房间标题/类型的判定(搜索页、无群名外部群都在这处理)Constant.kt— 服务器地址与本地自动化开关(SP 持久化)
一条消息的旅程
客户在群里发一句
→ 客户端无障碍读到「外部群·某群·张三: 在吗」
→ WS: {"type":101, ...} aivbot_service 入队
→ 命中 auto-reply 判定(room_type × mode) 回复策略
→ 合并窗口(默认 30s) 内同一房间同一人的消息攒批
→ agent_service.generate_reply 拼 system + 记忆 + 知识库 + 技能
→ LLM 决定「调用 device_command_203 发送」
→ tool 授权校验 → send_command 下发 203 WS
→ 客户端进房间、发送、回执
→ WS: {"type":203 回执 success_list/fail_list} → 审计 + 会话记录
部署拓扑(最小可用)
一台 Linux(2C4G 起步)
├─ MySQL 8(本机)+ venv + systemd 托管的 fastapi(端口 8765)
├─ 可选:Docker 一条命令起(compose 里自带 MySQL,未生产验证)
└─ (可选)Caddy/Nginx 反代 + HTTPS,仅当你需要给客户开 /ext
一台 Android 平板(推荐,也可以用手机;常开、充电、锁屏常亮免打扰按需)
└─ 与企业微信同机,客户端填服务端内网地址
(可选)frp:让外网客户控制台访问内网服务端,见 06