消息工作台 API 全量盘点(P0-6.1)
调研对象:xijian-legacy/message/(Flask + SQLite,独立部署,115 机 /opt/message/,systemd 服务名 message,用户定案不动不管)。
视角:犀见新 server 作为消费方,通过 HTTP 集成;不重写已验证的 QiWe 接入层。
方法:本地代码通读(app.py 1530 行 + api_ext.py 2091 行为主,13 个辅助模块),未 SSH,部署事实以代码/配置文件为准,标记处已注明「未核实」。
1. 概览
部署事实(从代码/配置推断,未 ssh 核实)
| 项 |
值 |
出处 |
| 技术栈 |
Python 3 + Flask + gunicorn(-w 1 --threads 4,单进程)+ requests + pymysql |
message.service、qiwe.py、lynoxi.py |
| 部署目录 |
/opt/message/(systemd WorkingDirectory) |
message.service |
| systemd 服务名 |
message(不是 README 里写的 xijianxiaoxi —— README 明显滞后于实际重命名,代码目录名、message.service、XIJIAN_DB=/opt/message/xijian.db 三处互相印证) |
message.service vs README.md |
| 监听地址 |
127.0.0.1:5040 与 172.17.0.1:5040(docker 网桥地址,供跑在容器里的 nginx-ui 反代访问,见 app.py _gzip() 注释) |
message.service ExecStart |
| 数据库 |
SQLite,XIJIAN_DB 环境变量指定路径,实际 /opt/message/xijian.db(store.py 里的默认值 /opt/xijianxiaoxi/xijian.db 是旧路径,被 systemd 的 EnvironmentFile/Environment= 覆盖) |
message.service、store.py:16 |
| 对外域名(当前,推断) |
message.lynoxi.com(独立域名,根路径服务)——nginx-lynoxi-api.conf 明确写着「工作台界面已迁到独立域名」,/message 与 /message/* 在 api.lynoxi.com 上做 301 跳转过去 |
nginx-lynoxi-api.conf:39-45、nginx-message-domain.conf |
| 对外域名(旧,推断已废弃) |
y1.wcc.cn/jingyinghuan/xijianxiaoxi/(README 首行仍写这个,nginx-message.conf 的路径前缀 + auth_request /_fa_verify 是这一代的产物)——与当前 feishu.py 登录闸、message.lynoxi.com 域名配置不一致,判断是历史遗留,未核实是否已下线 |
README.md:4、nginx-message.conf |
公网免鉴权端点固定在 api.lynoxi.com 上,不能迁 |
/qiwe/callback(企微回调→/ingest)、/qiwe/wcid/redeem(小程序回传→/api/wcid/redeem)、/qiwe/share.png(小程序卡片封面图)、/qiwe/up/*(待发送媒体公开目录,7 天清理) |
nginx-lynoxi-api.conf:26-76 |
| 登录鉴权 |
飞书 OAuth(feishu.py,复用犀见 admin 端同一飞书应用)签发 HttpOnly Cookie;FEISHU_APP_ID 未配置时鉴权整体旁路(见 §6 认证缺口);独立域名版靠 nginx Basic Auth(.htpasswd_message)兜底并透传 X-Auth-User |
app.py:54-72、nginx-message-domain.conf:61-67 |
| 当前接入企微号数量 |
5 个(devices.json,2 个仅有 guid 靠自动发现补身份,3 个手工填了显示名) |
devices.json |
| 前端 |
原生 JS 单页 web/index.html,服务端直接 serve,无独立构建 |
app.py:1233,1490 |
数据源拓扑(对理解「犀见该拿什么」很关键)
消息工作台自己接了 4 个外部数据源,犀见规划契约时必须分清楚"数据在谁的库里":
- lynoxi 生产库(MySQL,只读账号
msgboard_ro,7 张表)——lynoxi.py。〔口径更正 2026-08-17:犀见已定案独立新库 + 跨库只读,达人域数据一次性映射导入新表;对旧库仅跨库只读(xijian_app 只读授权),不经消息工作台转一手这一点不变〕。
- 消息工作台自己的 SQLite(
xijian.db)——会话、消息、绑定关系(peer_binding/wcid_token)、定价、群发/加人任务、指令记录……这些数据只存在这里,MySQL 里没有,犀见必须走 HTTP 才能拿到。
- 大千中台 open API(
zhongtai.py,https://ai.iphome.cn/api/open/projects,Bearer token)——"项目"清单,priority>0 判定"正在执行"。
- 经营环本地文件(
jyh.py,读 /opt/jingyinghuan/cards/*.json 与 /opt/jingyinghuan/web/daren-exec-gen-*.html)——项目卡/达人执行计划,代码注释明确写着这台机器上 /opt/jingyinghuan/cards 不存在,PROJECTS 硬编码只有 ["shzoo","sz-xiaomeisha"] 两个项目,这条数据链路目前处于半失效状态(详见 §7)。
- (历史遗留、当前主流程已不用)大千数仓
dy_influencer——daqian.py,README 记录的"单位混用"坑就在这个源,app.py 里 _lynoxi_profile() 的注释说明已经改读 lynoxi 而非数仓,但 daqian.py/cmdbus.py 仍 import 着它,未确认是否还有路径在用。
结论:项目/需求这个概念在系统里同时有 lynoxi.task(project_id 全库恒为 1,靠标题关键词 zhongtai.match_title 归项目)、中台 project(zhongtai.py,另一套 id)、经营环 project card(jyh.py,第三套、半失效)三份,互不是同一个主键体系。犀见如果要复用"项目"概念,必须先决定以哪个为准,不能假设消息工作台传回来的 projectId 能直接跟 lynoxi 的什么字段对上。
2. HTTP API 全量清单
统计口径:app.py 34 个业务路由 + 2 个静态兜底(/、/<path:fn>,不计入业务 API);api_ext.py(Blueprint ext)77 个;feishu.py(Blueprint feishu)4 个认证路由。业务 API 合计 111 个,另有 4 个认证路由、2 个静态路由。
2.1 消息收发
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| POST |
/api/conv/<cid>/send |
发文本 |
body {text} |
{ok,message,err,rateLimited?} |
走进程内限流闸(§3);app.py:890 |
| POST |
/api/conv/<cid>/reply |
引用回复 |
body {text, quoteUid} |
{ok,degraded,note} |
拼不出 reply 结构会降级成普通发送(degraded:true);api_ext.py:1181 |
| POST |
/api/msg/<uid>/recall |
撤回一条自己发的消息 |
- |
{ok,err} |
认的是 msgServerId 不是 uid,历史/补录消息可能撤不了;auto.py 实现 |
| POST |
/api/msg/<uid>/forward |
转发到多个会话(≤20) |
body {targets:[convId]} |
{ok,sent,failed} |
每个目标各过一次限流闸;api_ext.py:393 |
| POST |
/api/conv/<cid>/send-card |
发联系人名片 |
body {sharedId} |
{ok,name} |
api_ext.py:334 |
| POST |
/api/conv/<cid>/send-location |
发定位 |
body {title,address,lat,lng} |
{ok} |
探店场景用;api_ext.py:362 |
| POST |
/api/conv/<cid>/send-media |
发图片/视频/文件/语音 |
body {kind,url,name} |
{ok} |
先 /api/upload 拿公网 URL 再调它,QiWe 上传端点只吃 JSON 不吃 multipart;api_ext.py:493 |
| POST |
/api/upload |
前端文件上传中转 |
multipart file |
{ok,url,name,size} |
落盘到 nginx 公开只读目录 /var/www/qiwe-up,7 天清理 |
| POST |
/api/conv/<cid>/send-miniprogram |
发小程序卡片(wcid 关联机制) |
body {pageKey,title,desc,taskId} |
{ok,wcid,path} |
犀见入驻绑定的核心接口,详见 §4;api_ext.py:683 |
| POST |
/api/miniprogram/cover/init |
初始化小程序卡片封面(企微 CDN fileId) |
query guid,url |
{ok,cover} |
一次性/低频操作 |
| GET |
/api/miniprogram/pages |
可发小程序页面清单 |
- |
{appId,pages} |
appId 默认 wx120205414255fb5b,与犀见 miniapp 同一个小程序 |
2.2 会话
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/state |
左栏全量:账号+会话列表+项目+usage+AI模式 |
- |
大对象 |
面向整页刷新设计,非增量,几百会话时响应体较大(有 gzip) |
| GET |
/api/conv/<cid> |
会话详情(消息+项目面板+对方情况+草稿) |
- |
大对象 |
打开即标已读 |
| POST |
/api/conv/<cid>/read |
标已读 |
- |
{ok} |
顺带同步企微已读状态 |
| POST |
/api/conv/<cid>/handling |
显式改会话四态 auto/escalated/taken/done |
body {state} |
{ok,handling,was,closed} |
唯一允许改回 auto(交还)的入口,铁律见 §3 |
| POST |
/api/conv/<cid>/takeover |
人工接管(taken) |
- |
{ok,handling} |
|
| POST |
/api/conv/<cid>/done |
标处理完但不交还 |
- |
{ok,handling} |
|
| POST |
/api/conv/<cid>/handback |
交还给 AI |
- |
{ok,handling,resolved} |
顺带关闭该会话未处理的转人工事项 |
| POST |
/api/conv/<cid>/bind |
绑定所属经营环项目(注意与 §4 的达人绑定是两回事) |
body {projectId} |
{ok} |
projectId 必须在 jyh.PROJECTS(当前只有 2 个硬编码项目) |
| POST |
/api/project/<pid>/brief |
项目简介人工覆盖版 |
body {text} |
{ok} |
|
| GET |
/api/conv/<cid>/history |
往上翻历史(游标分页) |
query before,limit≤200 |
{ok,messages,hasMore} |
|
| GET |
/api/search |
全局搜索会话名+聊天内容 |
query kw |
{ok,convs,messages} |
|
| GET |
/api/conv-of |
按 guid+userId 查会话 id |
query guid,userId |
{ok,convId} |
通讯录"单发"用 |
| POST |
/api/conv/new |
主动发起会话(还没聊过的联系人) |
body {userId,guid} |
{ok,convId,name} |
|
| GET |
/api/conv/<cid>/alt-accounts |
同一个人在别的企微号下的会话 |
- |
{ok,current,alternatives} |
一个 peer 可能挂在多台设备下 |
| GET/POST |
/api/conv-flags、/api/conv/<cid>/flag |
置顶/免打扰/标记 |
body {pin,mute,mark} |
{ok,flags} |
存 kv,不动会话表 |
| GET/POST |
/api/conv/<cid>/tags |
会话(联系人)标签,走企微标签体系 |
body {tagIds,add} |
{ok,tags} |
|
| GET/POST |
/api/conv/<cid>/price |
会话定价(人工标记,AI 不参与) |
body {priceYuan\|priceFen,evidenceUid,note} |
{ok,ts}/{ok,latest,history} |
不选证据不能提交;金额以分入库、追加不覆盖;operator=unknown 时 403 |
| GET |
/api/send-quota |
各账号剩余发送额度 |
- |
{ok,quota} |
给前端"先看到余量再决定"用 |
2.3 AI 自动回复
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| POST |
/api/aimode |
三态开关 off/shadow/live |
body {mode} |
{ok,mode} |
切换即清空防抖队列 |
| GET |
/api/aimode/queue |
待转人工队列 |
- |
{items} |
|
| GET |
/api/aimode/activity |
自动回复活动流水 |
query limit≤100 |
{items} |
|
| GET |
/api/aimode/watch |
观察窗:AI 正在看的会话 |
query limit≤200 |
{items,stale,truncated,limit} |
|
| POST |
/api/conv/<cid>/suggest |
生成/取回 AI 起草建议 |
body {trigger,model} |
建议对象 |
有缓存命中即返回缓存 |
| POST |
/api/escalation/<eid>/resolve |
处理一条转人工事项 |
body {note} |
{ok} |
|
| GET |
/api/usage |
今日 AI token/调用量 + 最近 20 条 |
- |
{todayTokens,todayCalls,recent} |
不报分母(无日额度) |
2.4 联系人 / 达人档案 / 绑定
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/projects |
中台"正在执行"项目清单 |
- |
{ok,projects} |
数据源见 §1 拓扑第 3 条 |
| GET |
/api/requirements |
达人需求(lynoxi.task)清单 |
query projectId |
{ok,total,requirements} |
直读 lynoxi,犀见可跳过此接口直连库 |
| GET |
/api/requirement/<tid> |
需求详情 |
- |
{ok,requirement} |
同上 |
| GET |
/api/influencers/search |
达人搜索(昵称/手机/抖音号) |
query kw |
{ok,influencers} |
直读 lynoxi,犀见可跳过 |
| GET |
/api/influencer/<iid> |
达人档案大视图 |
- |
{ok,influencer} |
同上 |
| GET/POST/DELETE |
/api/conv/<cid>/binding |
企微联系人↔犀见达人绑定的 CRUD |
body {influencerId,taskId,note} |
{ok,...,pendingCards} |
只存在消息工作台 SQLite,犀见必须走这里拿;api_ext.py:131 |
| GET |
/api/binding/stats |
绑定统计(总数+来源分布) |
- |
{ok,bound,bySource} |
|
| POST |
/api/wcid/redeem |
小程序回传兑现绑定(无鉴权,HMAC 签名防伪) |
body {wcid,openId,unionId,influencerId?} |
{ok,bound,influencerId} |
wcid 机制核心落点,详见 §4 |
| GET |
/api/contacts |
通讯录检索(群发选人用) |
query kw,guid,tagId,bound,limit≤5000 |
{ok,total,contacts} |
|
| GET |
/api/contacts/enrich |
批量补达人属性(等级/地区/类目) |
query influencerIds(逗号分隔,≤200) |
{ok,profiles} |
|
| GET |
/api/roster |
名册(会话+通讯录合并视图) |
query scope=active\|all |
{ok,total,activeTotal,rows} |
单接口全量返回,非分页 |
| POST |
/api/roster/tag |
批量/单行改标签 |
body {convIds,tag}或{convIds:[1],tags:[]} |
{ok,changed}/{ok,tags} |
|
| POST |
/api/contact/<uid>/tags |
不经会话直接给联系人打标签 |
body {guid,tagIds,add} |
{ok,changed,tags} |
|
| GET |
/api/tags |
企微标签目录 |
query guid,force |
{ok,groups,tags} |
|
| POST |
/api/backfill/sender-names | /api/backfill/sender-avatars |
回刷历史消息发送者昵称/头像 |
- |
{ok,fixed,...} |
运维工具类接口 |
2.5 好友申请(addfriend,批量加人)
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/addfriend |
任务列表+各号今日余量 |
- |
{ok,items,quota,config} |
|
| POST |
/api/addfriend/parse |
只解析手机号不落库 |
body {text} |
{ok,phones,count} |
|
| POST |
/api/addfriend/upload |
Excel/CSV 解析手机号 |
multipart file |
{ok,phones,count} |
纯标准库解析 xlsx,不依赖 openpyxl |
| POST |
/api/addfriend |
建草稿任务 |
body {phones\|text,guids,mode,verifyText,note} |
{ok,id,job} |
mode: single\|balance |
| GET |
/api/addfriend/<jid> |
任务详情(含逐条明细) |
- |
{ok,job} |
|
| POST |
/api/addfriend/<jid>/confirm |
二次确认(数量原样输入) |
body {count} |
{ok} |
|
| POST |
/api/addfriend/<jid>/start | /cancel |
启动/取消 |
- |
{ok,job} |
后台线程异步跑 |
| POST |
/api/addfriend/unpause |
人工解除某号的风控挂起 |
body {guid} |
{ok,quota} |
正常不该用 |
| GET |
/api/addfriend/stats |
实测速率/错误分布 |
query days |
{ok,byDay,errors,config} |
上限没有官方文档,靠这个接口"测出真实上限"(§7 坑清单) |
2.6 群发助手(broadcast)
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/broadcast |
草稿/任务列表 |
- |
{ok,items} |
|
| GET |
/api/broadcast/rule |
查企微自身群发规则 |
query guid |
{ok,rule} |
查不到不等于不允许 |
| POST |
/api/broadcast |
建草稿(不直接发) |
body {guid,targets,msgs,sendType,note} |
{ok,id,draft} |
≤500 人/次 |
| GET |
/api/broadcast/<bid> |
草稿详情 |
- |
{ok,draft} |
|
| POST |
/api/broadcast/<bid>/confirm |
二次确认(收件人数量原样输入) |
body {count} |
{ok} |
|
| POST |
/api/broadcast/<bid>/send |
真发(必须已 confirmed) |
- |
{ok,msg} |
异步分批(20人/批,间隔3秒),走企微 /msg/sendGroupMsg,不经过 §3 的单聊限流闸,是独立节流 |
| POST |
/api/broadcast/<bid>/cancel |
取消 |
- |
{ok,draft} |
|
2.7 群管理(rooms)
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/rooms |
所有在线设备的群合并列表 |
query guid? |
{ok,rooms,note} |
只问在线设备 |
| GET |
/api/room/<rid> |
群详情(含成员) |
query guid |
{ok,room} |
|
| GET |
/api/room/<rid>/members |
群成员(挨个试在线设备) |
query guid? |
{ok,members,guid,notice} |
|
| POST |
/api/room/<rid>/<action> |
群操作:rename/notice/invite(安全)、kick/transfer/dismiss(危险,必须 confirm=群名原文) |
body 依 action |
{ok} |
全程写 room_audit 审计表 |
| GET |
/api/room/<rid>/qrcode |
群二维码 |
query guid |
{ok,qrcode} |
|
| GET |
/api/room/<rid>/tops | POST /api/msg/<uid>/top |
群置顶消息 |
- |
- |
|
| GET |
/api/rooms/audit |
群操作审计日志 |
- |
{ok,log} |
|
2.8 一键拉群(挂在需求单上)
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/requirement/<tid>/room-plan |
建群前计划卡:谁能拉/谁连不上 |
- |
{ok,pullable,unreachable,guid,...} |
按抖音号匹配企微身份,命中率取决于 peer_identity |
| POST |
/api/requirement/<tid>/room-create |
确认后真建群 |
body {roomName,senderIds} |
{ok,roomId,pulled,missed,dropped,unreachable} |
确认名单是冻结的:只在"确认集合∩此刻仍可拉"里动手;回写后台走 DEMAND_ROOM_WRITEBACK 环境变量,默认关(无接口契约) |
2.9 指挥台(cmdbus,自然语言→计划→确认→执行)
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| POST |
/api/cmd/plan |
一句话解析成执行计划 |
body {text,convId} |
{ok,plan,err,guesses} |
AI 解析(走 cligw),read 档直接把结果一起带回来 |
| POST |
/api/cmd/run |
执行一条计划 |
body {planId,targetIds,draft,confirmWord} |
{ok,result,jobId,err} |
write 档需确认,mass 档异步返回 jobId |
| GET |
/api/cmd/job/<jobId> | POST /stop |
群发类异步任务查询/中止 |
- |
- |
|
| GET |
/api/cmd/history |
指令历史(留 30 天) |
query limit |
{items} |
|
| GET |
/api/cmd/examples |
示例话术分组 |
- |
{groups} |
|
支持的动作(ACTIONS 注册表,cmdbus.py:62525附近):读档 contact.search / daren.profile / conv.history / room.members / stat.ask;写档 contact.add / contact.accept / msg.send / room.create / room.invite / contact.remark / contact.tag;批量档仅 msg.mass。删好友/踢群/解散群/发朋友圈这类不可逆动作,代码里根本没有实现这条路(不是开关关着)。支持 XIJIAN_CMD_DRYRUN=1 环境变量整体 dry-run(写类动作断在调企微接口前一步)。
2.10 设备 / 账号
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/api/devices/refresh |
强制刷新设备状态 |
- |
{ok,accounts,stats} |
平时 10 分钟缓存 |
| POST |
/api/device/<guid>/remove |
移出设备清单(不删历史) |
body {confirm=设备名} |
{ok,name,keptMessages,accounts} |
|
| POST |
/api/devices/add |
手动并入新设备 |
body {guid} |
{ok,name,state,accounts} |
必须过企微核验 |
| POST |
/api/device/<guid>/login/start | GET /status | POST /verify |
页面内扫码登录三部曲 |
- |
- |
掉线设备必须人工扫码,manualLogin 免扫码接口实测无效 |
2.11 其他
| 方法 |
路径 |
用途 |
关键参数 |
返回形状 |
备注/坑 |
| GET |
/health |
探活 |
- |
{ok} |
免鉴权 |
| POST |
/ingest |
企微回调入口(QiWe → 这里) |
企微 webhook payload |
{ok:true} |
免鉴权,犀见不需要调它 |
| GET |
/api/media/<uid>/raw | /voice | /urls | /download |
媒体取用四件套 |
- |
- |
企微 CDN 直链不能裸访问,需服务端代理 |
| POST |
/api/media/<uid>/voice-to-text |
语音转文字 |
- |
{ok,text,cached} |
企微不给语音下载直链,转文字是唯一读法 |
| GET/POST/DELETE |
/api/requirement/<tid>/docs*、/api/doc/<...> |
挂在需求单上的参考文档 |
- |
- |
同需求下所有会话共用 |
| GET/POST |
/api/phrases |
快捷回复短语库 |
- |
{ok,phrases} |
|
2.12 认证(feishu.py,独立 Blueprint)
| 方法 |
路径 |
用途 |
| GET |
/auth/feishu/login |
跳转飞书授权 |
| GET |
/auth/feishu/callback |
换 token、签 Cookie |
| GET |
/auth/me |
当前登录人 |
| GET |
/auth/logout |
登出 |
3. 发送安全机制
三态门(AI 自动回复,auto.py)
off(默认)/ shadow(影子:三层闸+起草+入队全跑,只是不真调 /msg/sendText,记入 shadow_sends 表,绝不进 messages 表)/ live(真发)。
- 存储:kv 表
ai_mode 键,库里没这个键即视为 off(新装服务不会自己开始发消息)。
- 切换即清空防抖队列(
cancel_all()),5 秒防抖窗口(DEBOUNCE_SEC=5.0,对方连发合并成一次判断)。
- 三层闸(
auto.py:1-31 文档注释):① 确定性预检(代码,群/非文本/陌生人/模式关闭直接拦)② AI 自判(模型自己说 escalate 或解析失败)③ 产出后置检查(报价敏感词/档期关键词/比率表述/候选为空)。任一层说转人工就转人工,只有三层全过才发。
- 兜底:会话只要还有未处理的转人工事项就不再自动回;异常一律变成队列里的一条,禁止
except: pass 静默吞掉。
发送限流(单聊路径)
- 实现:
api_ext.py:1493-1521,进程内存字典 _send_log = {guid: [ts,...]},非持久化、非跨进程共享。
- 数值:
RATE_MIN, RATE_HOUR = 20, 300(20条/分、300条/时,单账号)——硬编码在 api_ext.py:1500,非来自官方文档,是工程保守取值(README 同样定案)。
- 覆盖范围:
/api/conv/<cid>/send、/reply、/forward(每个目标各算一次)、/send-card、/send-location。
- 不覆盖:
/api/broadcast/<bid>/send(群发走独立的批次节流:20人/批、批间隔3秒,broadcast.py:26-28,走企微 /msg/sendGroupMsg 而非 /msg/sendText);/api/addfriend 走独立的加人节流(见下)。
- 推论对犀见的约束:因为限流状态只活在这一个 gunicorn worker 进程的内存里,犀见若要保证不触发企微风控,必须让所有主动发消息动作都经过这个服务的 API,不能绕过它自建发送通道——否则两边各自限流各自的,加起来可能突破企微侧真实上限。gunicorn 是
-w 1 单进程,进程内状态在该服务生命周期内是一致的(无多 worker 竞态问题),但服务重启会清空限流计数(不落库)。
批量加人限流(addfriend.py)
DAILY_PER_GUID=20/号/天、间隔随机 45~90 秒、仅 9:00–21:00 动手、单任务硬上限 2000 个号——同样是保守工程取值,官方文档没有频控说明(addfriend.py:1-51 有详细调研记录)。
- 风控自动熔断:命中
RISK_WORDS(频繁/限制/风控/受限/异常/封…)立即把该账号挂起到当天零点,并通过 notify.py(飞书机器人 + 企微在线号双通道)告警。
stats() 接口把真实执行数据(哪天第几个开始出错)沉淀出来,作为调整默认值的唯一依据——没有拍脑袋的数字,靠实测。
危险操作二次确认模式(跨模块统一套路,可直接复用到犀见的确认机制设计)
统一套路:前端先出计划/预览 → 要求把关键信息原样输入一遍才放行 → 全程留痕。四处独立实现,模式一致:
| 场景 |
确认要素 |
出处 |
| 群发 |
原样输入收件人数量 |
api_ext.py:869-878 |
| 批量加人 |
原样输入手机号数量 |
api_ext.py:979-988 |
| 移除设备 |
原样输入设备显示名 |
app.py:1122-1131 |
| 群踢人/转让/解散 |
原样输入群名 |
rooms.py(DANGEROUS = {"kick","dismiss","transfer"}) |
| 指挥台写类动作 |
confirmWord + XIJIAN_CMD_DRYRUN 环境变量整体断路 |
cmdbus.py |
幂等 / 去重
- 消息表主键
(guid, uid),INSERT OR IGNORE 天然去重同一条企微消息的重复推送。
set_handling() 写库前判"旧值==新值"直接返回不写(store.py:1943-1944),与犀见 CLAUDE.md 铁律 2 是同一套纪律,这个仓库本身就是这条铁律的先例。
- 掉线告警去重:同一 guid 6 小时(
XIJIAN_OFFLINE_ALERT_GAP)内只喊一次,写 kv 不写内存(重启不会重复告警)。
- 存量未读一次性清零(
kv 标记跑过不再跑),避免历史误标未读永久污染统计。
4. 绑定与回流
企微不给外部联系人手机号/微信号/unionid(contactType 2057 实测全空;企微 unionid 与犀见小程序不在同一开放平台,884 个 unionid 对 6474 个达人 0 命中)——这是整个绑定机制存在的前提。现有两条路,全部存在消息工作台自己的 SQLite(peer_binding/wcid_token 表),MySQL 里没有这份数据:
路径 A:wcid 回流(自动、管增量)
- 运营(或系统)触发
POST /api/conv/<cid>/send-miniprogram(或新好友自动欢迎卡片,见 _welcome_card() / XIJIAN_WELCOME_CARD=1 开关)。
- 服务端
bind.make_wcid(guid, peer_id, conv_id, task_id, page) 生成一次性 token(<随机串>.<HMAC-SHA256前16位>,密钥 WCID_SECRET),写入 wcid_token 表,拼进小程序 pagePath 的 query(如 pages/onboarding/onboarding.html?wcid=xxx.yyy)。
- 达人在小程序里打开卡片 → 小程序拿到
wcid + 微信 openId/unionId → 回传 POST /api/wcid/redeem(无鉴权,靠 wcid 自身 HMAC 签名 + 30 天 TTL 防伪造,走 api.lynoxi.com/qiwe/wcid/redeem 固定域名)。
- 服务端校验签名后,优先用微信身份(
openId/unionId)反查 lynoxi 库拿 influencer_id(lynoxi.by_open_id/by_union_id),拿不到才信小程序传上来的 influencerId 兜底——这是一条修复过的坑:曾经反过来信任小程序传的 id,导致小程序端 localStorage 残留的陈旧账号把聊天记录错绑到别人档案上。
- 写入/更新
peer_binding(已有人工绑定的不覆盖,人工优先级更高)。
MP_APPID 默认值 wx120205414255fb5b,与 CLAUDE.md 记录的犀见 miniapp AppID 完全一致——说明消息工作台的 wcid 机制本来就是绑定犀见小程序设计的,这条链路是犀见新 miniapp 大概率需要直接对接(或复刻)的现成协议。
路径 B:人工绑定(管存量)
GET/POST/DELETE /api/conv/<cid>/binding,运营在界面上手动搜索/选择达人,source=manual。
犀见入驻绑定要对接的点
- 若犀见新 miniapp 继续复用这个 wcid 协议:犀见后端需要能生成符合同一 HMAC 规则的 wcid(若沿用消息工作台签发,则由消息工作台的
/api/conv/<cid>/send-miniprogram 触发;若犀见自己签发,需要共享 WCID_SECRET 或改造成双方都认的机制——这是 P0-6.2 要定的点,不属于本次调研范围)。
- 若犀见要展示"这个会话绑的是哪个达人",走
GET /api/conv/<cid>/binding;要反向查"这个达人在企微那边绑了谁",现有接口没有按 influencer_id 反查的能力(只有按 peer_id 单个/批量查,bind.get/bind.get_many),这是一个缺口(详见 §6)。
pendingCards(bind.pending_for_peer):这个人被发过卡片但还没点开,是给运营提示用的,犀见若做"入驻进度"页面可以用上。
5. 可复用资产
| 资产 |
位置 |
说明 |
| QiWe 最小客户端 |
qiwe.py 全文件(404行) |
统一 call(method, params) 入口、login_info 三态判定(在线/离线/未登录,坑很细,见文件头注释)、掉线自动告警去重、设备自动发现/注销(内存+devices.json双写,原子替换)。若犀见将来需要直连 QiWe(而不是永远经消息工作台转发),这个文件是最省事的移植起点,但当前铁律是"不重造",优先级低。 |
| 进程内限流器模式 |
api_ext.py:1494-1521 |
简单有效的滑动窗口限流实现(无需 Redis),犀见若有类似"防外呼被封"场景可参考同一模式,但注意它是单进程内存态,多进程/多实例部署需要换成集中式存储。 |
| 三档危险操作确认模式 |
见 §3 表格 |
"计划预览 → 原样输入关键信息确认 → 执行 → 留痕" 是这几处独立实现但高度一致的模式,与 CLAUDE.md 铁律 4(危险外呼先 dry-run/确认机制)直接对应,犀见新 server 做危险外呼时可直接照搬这个交互契约。 |
XIJIAN_CMD_DRYRUN 环境变量整体断路模式 |
cmdbus.py:_call() |
一个环境变量统一拦截所有写类企微调用,写类动作断在"调用前一步"而非事后补偿,是 dry-run 实现的一个干净范式。 |
| 批量加人的"风控自动熔断+双通道告警"模式 |
addfriend.py:_halt()、notify.py |
命中风控关键词立即挂起当天配额 + 飞书/企微双通道告警,犀见做 RPA/自动化外呼时的安全闸可参照。 |
| "写库前判值是否真变" 先例 |
store.py:set_handling() (line ~1943) |
犀见 CLAUDE.md 铁律 2 在这个仓库已有实践先例,不是新发明。 |
| wcid HMAC 签名 token 生成/校验 |
bind.py:make_wcid/verify_wcid |
短小(约30行),若犀见需要自己签发同类一次性追踪串,可直接照抄逻辑(token 格式、签名截断长度、TTL 判定)。 |
| Excel/CSV 免第三方库解析手机号 |
api_ext.py:api_add_upload()(921-954行) |
用标准库 zipfile 读 xlsx 的 sharedStrings.xml/sheet*.xml,不依赖 openpyxl,犀见若有类似批量导入场景可以直接借用思路。 |
| Feishu OAuth 登录 + HttpOnly Cookie 自签会话 |
feishu.py 全文件(143行) |
与犀见现有测试 token 登录、未来飞书 OAuth 整合(CLAUDE.md 技术选型记录里提到"飞书 OAuth 后补")思路一致,可作为实现参考(自签 token 格式:hex(payload).hmac前32位)。 |
6. 给 P0-6.2 契约的候选接口
按任务描述的四个场景对应:
一键定向邀约
- 批量好友申请:
GET/POST /api/addfriend* 全套(parse→create→confirm→start→job 轮询)已经是完整的"草稿-确认-执行"流程,现成可用,犀见不需要重造。
- 邀约话术/入驻卡片:
POST /api/conv/<cid>/send-miniprogram(发入驻页小程序卡片)——但这个接口的前提是已经有一个 cid(会话),也就是已经是好友。批量加人和批量发卡片是两个独立动作,犀见如果要做"加上好友后自动发入驻卡片",现有代码已经有一个模板(app.py:_welcome_card(),靠 webhook 里的"你已添加了xxx"系统提示触发),但这是消息工作台内部逻辑,没有对外暴露成犀见可订阅的事件(见下方缺口)。
催发通知
- 单发:
POST /api/conv/<cid>/send 或 /reply(现成)。
- 批量:
POST /api/broadcast(草稿)→/confirm→/send(现成,含二次确认与分批节流)。
- 缺口:没有"催发对象=犀见业务系统里某批任务超时未完成的达人"这种按业务条件圈人再群发的组合接口——犀见需要自己先从 lynoxi/自身域算出目标名单(userId 或手机号),再调
/api/contacts(按手机号查 userId)或直接用已知的 guid+userId 建群发草稿。这一步"业务条件→企微身份"的翻译,现有 API 只提供了零件(/api/contacts、/api/roster),组合逻辑要犀见自己写。
入驻卡片 / 绑定回流
- 现成:
/api/conv/<cid>/send-miniprogram(发卡)+ /api/wcid/redeem(回传,miniapp 侧调用,非犀见 server 直接消费)+ /api/conv/<cid>/binding(查绑定状态)。
- 这条链路的关键决策点在 P0-6.2:犀见新 miniapp 是直接调消息工作台的
/api/wcid/redeem(沿用现有协议),还是犀见自己的 server 做一层转发/影子记录? 本次调研只确认了协议本身,决策留给契约设计。
会话查看(嵌入运营台)
GET /api/state、/api/conv/<cid>、/api/conv/<cid>/history、/api/search 现成可用,但这几个接口都是为整页人工界面设计的(返回大而全的对象,/api/state 尤其重),如果犀见只是想在运营台侧栏嵌入"这个达人最近聊了什么"这种轻量小组件,直接用会拿到远超需要的数据量——建议 P0-6.2 评估是否需要一个裁剪版只读接口,或者接受现状按需截取字段。
认证缺口(贯穿所有场景的头号问题)
现有鉴权只有两条路,都不是为程序化服务间调用设计的:
1. 飞书 OAuth Cookie(浏览器会话,犀见 server 后端进程无法参与);
2. nginx Basic Auth 透传 X-Auth-User(app.py:_operator() 读 X-Auth-Oid/X-Auth-User 请求头,这两个头是直接信任、不做校验的——nginx-message-domain.conf:66-67 特意清空客户端自带的 X-Auth-Oid 防伪造,说明这套机制假设"过了 Basic Auth 就是可信内网调用方")。
若 FEISHU_APP_ID 环境变量未配置,app.py:_guard() 的登录守卫直接整体旁路(if not feishu.APP_ID: return None),这种情况下所有 /api/* 无需任何凭证即可调用——这是本次调研中最需要在 P0-6.2 明确核实的一点:当前生产环境是否配置了 FEISHU_APP_ID(本地 .env 不在仓库里,未 ssh 核实)。
建议 P0-6.2 讨论:是否需要给犀见 server 单独开一个 API Key / 内网 mTLS 之类的服务间凭证,或者约定犀见固定用一个 Basic Auth 账号 + 固定 X-Auth-Oid 值代表"系统自动化"这个操作人身份(定价、绑定这类接口会把 operator 落库,需要一个稳定可辨识的系统身份,不能是 unknown)。
缺口清单(一句话各是什么)
- 无服务间认证机制——只有浏览器 Cookie 和 nginx Basic Auth 透传头,犀见 server 后端进程没有干净的凭证路径。
- 无按 influencer_id 反查绑定的接口——只能按 peer_id(企微侧 userId)查,犀见若要"这个达人绑了哪个企微身份"需要自己在本地缓存
/api/roster 或 /api/contacts 的全量结果做反查。
- 无事件推送/webhook——犀见若想知道"某达人刚绑定成功""某会话进入某聊天阶段""收到新消息",只能轮询现有只读接口,且这些接口是为人工 UI 刷新设计的(全量而非增量),不天然支持"自上次轮询以来变化了什么"这种增量查询,容易撞上 CLAUDE.md 铁律 2 提到的轮询去重/binlog 教训。
- 发送侧无客户端幂等键——
POST /api/conv/<cid>/send 等接口不接受调用方传入的请求 ID 做去重,犀见如果因为超时重试而重复调用,会真的重复发消息;需要犀见自己在业务层做"这条消息今天有没有发过"的判断,或推动消息工作台加一个幂等参数。
- "项目"概念三源不一致(见 §1 拓扑),P0-6.2 若涉及按项目圈人/催发,要先跟 product-owner/口径基线确认犀见自己的项目主键体系,不能假设消息工作台传回的
projectId 直接可用。
7. 坑清单(照录,标出处)
- 企微不给外部联系人手机号/微信号(
contactType 2057 实测全空),只能靠 wcid 回流或人工绑定认人。——BACKLOG.md:40-41
- 企微 unionid 与犀见小程序不在同一开放平台(884 个 unionid 对犀见 6474 个达人 0 命中),企微侧绑定要求主体一致、只能绑一个,这条路走不通,别再试。——
BACKLOG.md:42-44
- 设备掉线是企微侧策略,非代码问题;
cmd=11016 webhook 会给出原因(登录态已过期/新设备需扫码安全验证);云端实例闲置会被回收,有真实消息往来的号反而稳。——README.md:64
/login/checkLogin 官方警告不要频繁调用,已降频到 10 分钟缓存 + 有流量就跳过(store.py STATUS_TTL=600、TRAFFIC_WINDOW=900)。——README.md:65
/login/manualLogin(免扫码登录)实测无效(错误码 -2003),掉线必须人工扫码;/client/restoreClient 有效,能把"客户端实例不存在"的死实例救回成可扫码状态。——README.md:66、qiwe.py:324-330
- 大千数仓
dy_influencer 同表混用单位:daren_gmv_30d 是分、item_gmv_total_30d 是元,差 100 倍;其余 GMV 字段单位存疑,代码只放行已交叉验证过的字段。——README.md:67、daqian.py:1-9(注:主档案读取路径已改走 lynoxi,此数仓可能已非主路径,未确认是否完全弃用)
- 比率类表述全站禁止上墙(经营环口径
gc_ratio_no_target),三层防护:提示词禁令 + 喂料清洗 + 输出过滤。——README.md:68、jyh.py:22-25
- 加人/发消息频控没有官方文档数字,QiWe 116 条接口条目全查过,唯一沾边的是"创建设备时 areaCode 需与登录省份一致""3分钟内完成扫码",都不是频控——现有的 20/分、300/时(发消息)与 20/号/天(加人)都是保守工程取值,靠
stats() 接口用真实执行数据校准,不是查文档查出来的。——addfriend.py:7-38、api_ext.py:1497-1500
checkLogin 对未登录设备返回 userId="0",历史 bug 曾把 "0" 当真身份写库,导致该设备整条消息流方向判反(自己发的被当成收到的);修复方式是 _valid_uid() 统一过滤,且启动时清洗存量脏数据。——store.py:_valid_uid() 及 _migrate() 注释
- 企微引用消息(reply)结构官方文档建议原样取 webhook 原始
msgData,不要自己拼,字段结构随消息类型变,且 reply.type 与 webhook msgType 是两套不同枚举(文本:webhook=2 / reply=0)。——app.py:392-400、api_ext.py:1119-1170
- nginx 面板默认
gzip on 但没有 gzip_types,只压 text/html,导致 /api/* JSON 全程裸传(实测 674KB→压后55KB,12倍),因此在 Flask 应用层自己做了 gzip 中间件,不依赖前置 nginx 配置。——app.py:75-116
/opt/jingyinghuan/cards(经营环项目卡数据源)在当前部署机器上不存在,jyh.project_name() 取不到名字会回落显示 pid(如 shzoo),左栏一度显示黑话代码而不是项目名,已改用中台 zhongtai.py 实时拉取修复主路径,但 jyh.py 这条数据链路本身仍处半失效状态(硬编码只有 2 个项目)。——app.py:224-226、jyh.py:16-20
- SQLite 连接不关闭导致 fd 打满、整站 500:标准
sqlite3.Connection.__exit__ 只提交/回滚不关闭连接,原实现对每条会话(400+条)各开一个连接查联系人,高峰冲破 1024 fd 上限;修复:自定义 _Conn.__exit__ 强制 close() + 批量查询取代循环内单条查询。——store.py _Conn 类注释
- 历史回捞消息与实时消息同一条入库路径,631 条早已在企微里读过的老消息被计成未读,全站合计 896;做了一次性清零(kv 标记跑过不再跑),因为"哪几条真没读过"无从判断,谎报比清零更糟。——
store.py:_reset_unread_once()