Repository navigation
Releases
v0.3.25 — 补审批漏接防护(0.3.24 收尾)
v0.3.25 — 补上「审批漏接」防护(0.3.24 的收尾)
背景
0.3.24 按市场审核意见把 _umo_is_bound() 从「同步阻塞 HTTP」改成「纯缓存读 + 异步刷新」,
审阅点已修。但 TTL 缓存带来一个自己引入的漏接风险:
/dsh <任务> 刚在网关侧建立绑定、本地缓存还是 False 时,紧跟的审批回复
(纯数字 / 批准 / 取消)会被判成「没绑定」而不捕获 → 落到人设手里。
而这条链路很常见:任务要跑工具 → 弹审批 → 用户回 1。
改法(三处强制刷新,全部异步)
| 时机 | 原因 |
|---|---|
| 审批类消息捕获之前 | 接不接取决于此刻绑定状态,等 TTL 会判错 |
| 一轮任务/命令结束后 | /dsh <任务>、 |
| ew、use、end 都走 _forward,它们会改绑定 | |
| /dsh follow 结束后 | follow 在未绑定时可能自动绑定 |
验证(容器内探针,import 实机那份 main.py)
| 断言 | 实测 |
|---|---|
| 过期缓存(False) + 审批文本 → 捕获 | False(复现漏接) |
| force 刷新后 → 绑定 True → 再捕获 | True ✅ |
| _should_capture 耗时(空/命中) | 0.117 ms / 0.014 ms(仍无网络 I/O) |
| _refresh_binding 真请求 → 再命中 | 212.8 ms → 0.0133 ms |
安装
- AstrBot 侧:装 0.3.25 后重载插件(市场审核请以本版重新提交)。
- DSH 侧(ingress):本版未改 lib/,无需重载。
v0.3.24 — 修并发模型问题(事件循环内不再有阻塞 HTTP)
v0.3.24 — 修 AstrBot 市场审核打回项:事件循环里的阻塞 HTTP
审核意见(原文摘要)
main.py:597 的 _umo_is_bound() 在同步函数中直接调用 httpx.Client(timeout=3.0)发起阻塞式
网络请求,而它被同步的 _should_capture() 调用,后者运行在异步事件处理函数 on_message() 内。
属实,而且是 3 秒超时 × 4 个调用分支。
改法(I/O 移出事件循环,捕获语义不变)
| 函数 | 改动 |
|---|---|
| _umo_is_bound() | 改成纯内存缓存读,函数体内零网络调用 |
| _refresh_binding()(新增,�sync) | httpx.AsyncClient 请求 GET /binding;命中缓存直接返回;成功缓存 30s / 失败缓存 5s |
| _dsh_side_cwd() | 同样改为纯缓存读(cwd 由刷新时顺带写入) |
| on_message() | 开头 �wait self._refresh_binding(...) 刷一次,随后同步的 _should_capture() 只读内存 |
验证(容器内探针,import 实机那份 main.py)
| 断言 | 实测 |
|---|---|
| _refresh_binding 是协程 | ✅ |
| _umo_is_bound(空缓存/命中) | 0.0067 ms / 0.0012 ms |
| _should_capture(空缓存/命中) | 0.108 ms / 0.0133 ms(全程无 I/O) |
| 真请求(异步)后再命中 | 121.7 ms → 0.0172 ms |
| 缓存语义 | 空=False、命中=True、"1" 正确走批准分支 |
唯一行为差异
绑定状态探测最多 30 秒延迟(探测失败 5 秒内重试)。所有改绑定的命令本身走 HTTP,不受影响。
安装
- AstrBot 侧:装 0.3.24 后重载插件。
- DSH 侧(ingress):本版未改 lib/,无需重载;npm 版本号同步到 0.3.24。
v0.3.23 — 主动消息拉黑不再跨群连累
v0.3.23 — 主动消息「拉黑」不再跨群连累
现象
/dsh follow 看起来不优先尝试主动发送 —— 一直在被动回复,5 条一到就发不出去了。
真因(日志铁证)
{'message': '主动消息失败, 无权限', 'code': 40034105}
→ 10 次,全部来自群 23552507E8E4D0A0428643533EF44CB3
→ 用户的群 C121A06D… 在整个日志窗口里出现 0 次
_proactive_denied 原是个全局布尔:任何一个会话撞一次 40034105,就把所有会话一起
拉黑成被动。而主动消息权限是按群授予的(要群主在开放平台开)—— 那个没权限的群把用户的群
也钉死在被动,passive-first 的「首条之后转主动」永远走不到。
修复
改成按会话记(set[str],键用 _convo_key):只有真正被拒的会话才退避;
其它群若已开权限仍会尝试主动。
验证
容器内探针(import 实机那份 main.py,两个真实群号):
群A 撞过 40034105 之后:群A 敢走主动=False(对) 群B 敢走主动=True(对,不再连累)
回滚成全局布尔后立刻复现「群B 也被拉黑」。
安装
- AstrBot 侧:装 0.3.23 后重载插件 —— 本次改动全在插件侧,DSH 侧不用重启。
- 之后每个群第一次走主动时各撞一次
40034105是正常的(权限逐群授予),
撞过就只对该群退避。
v0.3.22 — 修 runningSessions 用了不存在的会话字段
v0.3.22 — 修「解绑后网页在跑,follow 却说没有在跑的会话」
原因(我 0.3.21 写的 bug)
0.3.21 的
unningSessions()\ 里我用了不存在的字段:
\\js
for (const s of ctx.sessions.list())
if (s.agent?.status === 'running') … // ← Session 根本没有 agent 字段
\\
\Session\ 只有 \id / cwd / header\ 那些,没有 \�gent* —— 这个循环一个都匹配不上,
函数实际只靠 \state.turns\ 兜底,而解绑后它是空的*,于是「明明在跑却说没有在跑的会话」。
正确的权威来源是 **\ctx.agents.list()**(活 agent 注册表),会话 id 取 \�gent.session.id\。
回滚验证:改回旧写法,harness 立刻复现该现象(\400 当前没有绑定会话,也没有正在跑的会话\)。
测试基建也修了
\ollow_adopt_harness.mjs\ 的桩把 \Session\ 桩成了带 \�gent\ 字段、并给 \ctx.agents
漏了 \list()\ —— 这正是那个错法能混过测试的原因。现在桩按真实形状来:
Session 只有 \id/\header\,判断在跑必须走 \�gents.list()\。
安装
- DSH 侧:\dsh plugin --profile web add dsh-astrbot-ingress@0.3.22\ 然后重启。
- AstrBot 侧:装 0.3.22 后重载插件。
v0.3.21 — 未绑定也能 follow(唯一的在跑会话自动接上)
v0.3.21 — 未绑定时 /dsh follow 会自动接上正在跑的会话
新增
以前群里没绑定会话时 /dsh follow\ 直接报「当前没有绑定会话」。现在分三种情况:
| 情况 | 行为 |
|---|---|
| 未绑定 + 恰好一个会话在跑 | 用它,并顺手把这个 QQ 会话绑上(等同 /dsh use\ 过了) |
| 未绑定 + 多个在跑 | 不猜,列出候选(标题 + 短 id),先 /dsh use <序号>\ |
| 未绑定 + 没有在跑的 | 说清「先 /dsh <任务> 起一个」 |
为什么多个时坚决不猜:可能同时跑着好几个会话,而「你要的是哪个」桥这边判断不了
(网页当前打开的那个只是浏览器内存状态,读不到)。猜错会把别人的输出接到这个群,比报错更糟。
顺带
- 插件侧不再写死 400/409 的文案,改为照读 ingress 给的 \error\。
- 修掉一个自己写出来的判据错误:把「有回合记录」当成「在跑」会让候选数虚高
(表现为自动绑定成功一次后,下一次被误判成「有 3 个在跑」)。现收紧为
有活着的 sink,或 agent 状态确实是 running。
验证
\ollow_adopt_harness.mjs\ 三分支断言全过;连同此前四个 harness 共 5 个全绿。
安装
- DSH 侧:\dsh plugin --profile web add dsh-astrbot-ingress@0.3.21\ 然后重启(ingress 有改动)。
- AstrBot 侧:装 0.3.21 后重载插件。
v0.3.20 — 紧急修复 dsh web 崩溃
v0.3.20 — 紧急修复:0.3.19 引入的 dsh web 崩溃
崩溃
\
dsh: fatal uncaught exception: TypeError: Cannot read properties of null (reading 'write')
at sseWrite (.../dsh-astrbot-ingress/lib/index.js:255:7)
at Timeout._onTimeout (.../lib/index.js:509:9)
\\
0.3.19 让「流被掐断的回合」降级成无源流记录(\ urn.res = null\),但只要有跟随者,
\ urnHasSink()\ 就为 true —— 于是 digest 定时器走到 \sseWrite(live.res, …)\ 去读
ull.write\,异常从 \setInterval\ 逃出去、直接杀掉宿主进程。这是我的 bug。
修复
- 那个写出点改用 \ urnWrite(live, …)\(写向所有 sink,null 安全);minimal 档的保活同理。
- \sseWrite()\ 加兜底守卫:
es\ 为空 / 不可写 / 已结束 时直接返回。
宿主插件里「少发一条状态」和「进程崩掉」不对等,这类地方一律 fail-soft。
验证
\ollow_crash_harness.mjs\:复刻崩溃前状态并把 \progress_interval_sec\ 压到 10 秒,
等 digest 定时器真的触发 —— 带修复发给跟随者且进程存活;回滚修复则逐字复现崩溃。
安装
- DSH 侧:更新 profile 并重启(本版 ingress 有改动):
\dsh plugin --profile web add dsh-astrbot-ingress@0.3.20\ - AstrBot 侧:装 0.3.20 后重载插件。
v0.3.19 — 修「5 条额度用完后就 follow 不到」
v0.3.19 — 修「QQ 回合 5 条额度用完后就 follow 不到」
现象
QQ 里发起的回合,被动回复额度用尽(每条消息 5 次)之后,群里再 /dsh follow\ 会说
「这个会话当前没有在跑的回合」—— 可那个回合在 DSH 那边还在正常跑。
真因(有日志证据)
\
botpy.errors.ServerError: 回复消息失败,被动回复时间或者次数超过限制
在调用插件 astrbot_plugin_dsh 的处理函数 on_message 时出现异常:…
\\
额度用尽 → 插件 \event.send()\ 抛异常 → 从 \on_message\ 冒出去 →
SSE 读取循环中断、那条流关闭 → ingress 的
es.on('close')\ 把整条回合记录删掉
→ 群里再 follow 就找不到回合了。
两处修
- 插件侧:发送失败不再抛(_try_deliver\),额度用尽时安静收场并记日志
(额度用完就是发不出去,再发还是撞墙)。回合照旧跑完,发一条新消息 + /dsh follow\ 接着看。 - ingress 侧:
es.on('close')\ 不再删记录,而是降级成无源流记录,follow 随时能接上。
验证
\ollow_detach_strict_harness.mjs\:掐断 QQ 那条流、并故意让 agent 状态保持 idle
(不给「follow 现场建记录」兜底)→ 带修复 200 并收到断开后的正文;
改回旧行为则 409「没有在跑的回合」,与服务端日志现象一致。
安装
- DSH 侧:需要重载 \dsh-astrbot-ingress\(或重启 \dsh web\)—— 本版两侧都有改动。
- AstrBot 侧:装 0.3.19 后重载插件。
v0.3.18 — /dsh follow 也能接住网页发起的回合
v0.3.18 — /dsh follow 现在也能接住「网页发起的回合」
修的是什么
0.3.17 的 /dsh follow 只对从 QQ 发起的回合有效。网页上跑起来的回合去 follow 会得到
「这个会话当前没有在跑的回合」—— 因为 state.turns 只由 /inbound\ 创建,
网页发起的回合在桥里根本不存在,而它恰恰是这个功能最主要的目标场景。
现在新增无源头流的回合记录:来自已绑定会话、但桥里没有记录的事件,会为它建一条记录
(没有「发起者那条流」),于是跟随者可以随时挂上来,事件照常认领、回合结束照常收尾;
没人跟随时它什么都不写,只是一条内存记录。
顺带修掉一个 0.3.18 自己引入的回归
无源头流的记录会让审批/提问有机会命中网页发起的回合 —— 那会把审批卡/提问框从网页抢走、
然后在群里没人应答。现在只接管「QQ 那边真的能答」的回合:有源流,或此刻有人跟随。
用法没变
\
/dsh follow # 或 /dsh 跟随 / /dsh 续上
\\
网页那边跑起来 → 群里发 /dsh follow\ → 输出接到这条新消息上,回合结束自动断开。
被动回复 5 条用完时,再发一条新消息 + /dsh follow\ 就换到新额度。
安装
- DSH 侧:需要重载 \dsh-astrbot-ingress\(或重启 \dsh web\)——本版 ingress 有改动。
- AstrBot 侧:装 0.3.18 后重载插件。
v0.3.17 — /dsh follow:把正在跑的回合接到群里
v0.3.17 — /dsh follow:把正在跑的回合接到群里
解决什么
以前只有在群里发起的会话才会持续输出到群里 —— 那条 QQ 消息自己开着一条 SSE。
网页(或其它地方)发起的回合没有这条流,群里永远看不到结果。
现在:群里发一次 /dsh follow\,就能把正在跑的那个回合的正文、工具行、结束标记
实时接到这条新消息上,回合结束自动断开(临时跟随,不是长期订阅)。
顺带解决官方 QQ 被动回复 5 条上限
官方规则:被动回复的额度是按「被回复的那条消息」算的(每条消息 5 分钟 / 5 次)。
所以每发一条新消息就换来一份新的 5 条额度 —— 5 条用完时,在群里再发一条消息 +
/dsh follow\,就接着输出。
日志里 \40034128\(被动回复时间或次数超限)现在会被单独识别并提示换新额度,
不再与 \40034105\(没有主动消息权限)混为一谈。
用法
\
/dsh follow # 或 /dsh 跟随 / /dsh 续上
\\
- 当前没有在跑的回合 → 明确告诉你「跟随只在回合进行中有意义」
- 会话还没绑定 → 提示先 /dsh <任务>\ 或 /dsh use\
安装
- DSH 侧(ingress):需要重载 \dsh-astrbot-ingress\,或重启 \dsh web\。
- AstrBot 侧:装 0.3.17 后重载插件。
v0.3.16 — @机器人+直接发图不再被接管
v0.3.16 — 「@机器人 + 自己直接发一张图」不再被当成续聊接管
现象
群里只要 @机器人 + 带一张图(自己发的图,不是引用),消息就会被交给 DSH —— 哪怕
�ound_media_passthrough 和 official_at_continue 都关着。
根因
官方通道有一条特例:「引用一张图/文件 + @机器人」也要接住(AstrBot 不建 Reply,
被引附件只能从原始 payload 的 msg_elements[].attachments[] 里捡)。但那条判断用的是
_has_inbound_media() —— 它把本条消息自带的附件和被引消息里的附件混在一起,
于是「自己发的那张图」也满足了条件。而且那条分支完全不看 �ound_media_passthrough,
所以关掉开关也没用。
修法
新增 _has_quoted_media(),只看被引消息(aiocqhttp 看 Reply 段里的组件,官方通道看
msg_elements[].attachments[]),那条特例改用它:
| 场景 | 之前 | 现在 |
|---|---|---|
| @机器人 + 本条直接带一张图 | 接住 ❌ | 不接住 ✅(�ound_media_passthrough 打开时才会接住) |
| @机器人 + 引用一条带图的消息 | 接住 | 接住(特例保留) |
| @机器人 + 只有文字 | 不接住 | 不接住 |
容器内探针验证(D:\dswk\.dsh-inbox\capture_media_probe.py),上表四行(含开关打开那行)全部符合预期。
安装
全在 AstrBot 插件侧,装完重载「DSH 桥」;DSH 侧不用动。