https://github.com/Erfan817/personal-bot-demo 这是我写的 bot,锐评一下

回复 Erfan: 看完了。这版和上次那份基本不是一个东西——能力归属标清了、测试补上了、SSRF 挡了、告警有阈值了、HANDOFF 写得比不少生产项目都清楚。所以这次不重复上次的话,只挑现在还在的洞。 一、事件不带 chatId,串行队列是唯一让上下文正确的理由。feishu.mjs 里 currentChatId 和 toolLog 是模块级可变量,brain 发出来的 answer / tool_call 事件本身不带会话标识。今天能对,靠的是两个前提:只有一个渠道在跑、入站严格串行。你待办第 4 条正好要加 QQ 渠道——两个渠道同时进消息,tool_call 就会串到另一个会话里去。这不是概率问题,是必然。修法很轻:事件里带上 chatId 由渠道层路由,或者用 AsyncLocalStorage 把每轮的上下文绑住。 二、调度触发绕过了串行队列。scheduler 的 fire 直接 await runAgent,没走 feishu 里那条 queue。于是 8:00 早报跑着的时候你发消息,就是两条 agent 循环在 2 vCPU / 1 GiB 上同时跑,各自还可能再调 web_search、web_fetch。这一条同时打破了「消息串行处理」的隐含假设和内存预算。fire 也入同一条队列,或者加一个全局并发闸门,排队时回一句正在跑定时任务就够了。 三、错过的那一分钟没有补救,而且提醒任务一起吃亏。scheduler-state.json 的注释自己承认「重启恰好跨过触发分钟,那一轮就错过了」。早报这么判没问题,但 remind_me 建的提醒走同一套 jobs.json、同一套 tick——「8:00 提醒我吃药」跨过去就真的不响,不补发、不告警,你也不知道它响没响。任务是两类:内容型过期无意义,时间型过期更该补。给 job 加个 misfire 字段按类型区分,比统一跳过靠得住。 四、部署还是没有版本。HANDOFF §4 承认得很诚实:服务器目录不是 git 仓库,改坏了没有撤销手段,跑的是哪个 commit 无从查证。文档把风险写清楚了,但风险没降。最低成本是服务器上 git init 加一个 remote,部署改成 fetch + reset --hard 到某个 tag,回滚一条命令;顺带的收益是「真相分两半」那张表能少一格。 五、备份有三个问题。一是 env-* 快照是明文密钥,HANDOFF 自己把它列为泄露路径——标注泄露路径和消除它是两件事,至少加密或挪出项目目录加 chmod 600,别依赖默认权限。二是文件名用 toISOString,那是 UTC:04:00 CST 存出来的名字是前一天 20:00。项目在 cron 那节专门强调过按北京时间算,这里自己破了纪律,跨日排障会误导。三是没有恢复脚本、没演练过——备份唯一的验收标准是「照文档能不能在空机器上把记忆库恢复出来」,而 HANDOFF §5 那七步验证链条里恰好缺这一步。 六、「77/77 通过」目前只在能 ssh 上那台机器的时候成立。memory.test.mjs 依赖原生 better-sqlite3,本地装不上,于是这个数字没法在 CI 复现。仓库是公开的,GitHub Actions 免费,至少让不依赖 sqlite 的那几个测试文件跑在每次 push 上,数字才有凭证,而不是靠人记得去服务器敲一遍。 七、可观测性还是裸 journald。HANDOFF 那七步能回答「服务活着吗」,回答不了「刚才那条消息为什么慢、为什么没回」。而 1 GiB + 8 GB swap 抖动起来的表现恰恰是「心跳正常但每轮很慢」——心跳只证明事件循环还在转,不证明它还有余力。每轮打一行结构化日志:会话标识、turn 数、工具调用次数、总耗时、token 用量。这一行同时也是待办里那个成本看板的地基。 八、成本闸门还是排在用量看板后面。一个会自己定时触发、还会联网的 agent,风险不是看不见花了多少,是没人拦着它花。先加日额度硬上限,超了停推并写告警,再看板。 九、prompt injection 只做了一半。net.mjs 那条 SSRF 链挡得很干净——内网段、云元数据、DNS 解析后复查、十进制 IP 这种绕过写法都想到了。但「网页内容指使 agent」那一半没有可见防御,web_fetch 的返回是原文直灌上下文。方向是给它一个显式的数据边界:system prompt 里声明工具输出是资料不是指令,返回体加标记,或者对抓取内容做一次注入特征过滤。不必一次做完,但别让它以裸文本的身份进上下文。 十、一个小硬伤,偏偏这类仓库最容易被人抓:README 一处说「依赖了 5 个第三方包」,技术栈那节结尾写「依赖总数:4 个」,package.json 里 dependencies 正好 5 条。 如果只挑两条,选一和二。它们都在「再加一个渠道、再加一个用户」的时候立刻现形,而且都是设计层的问题,改起来的代码量很小。