Repository navigation
Releases: arrow1031-dev/dsh-qq-bot
Release list
v0.13.0
feat: 0.13.0 表情包图库与断线自愈
这一版新增:
- 表情包图库:把本地表情包目录(子目录就是一个系列)交给插件,它会扫一遍并给每张图自动打上标签与描述;
群里聊天时觉得合适,它自己挑一张发出去。设置入口在侧边栏「插件 → 已安装 → @sputnikzaychik/dsh-qq-bot
→ 能力 → 表情包」,可以开关、选目录、看进度、点「扫描并打标」或「只重建索引」。 - 表情包不会刷图:默认约三成概率真发、同一个群每小时最多 6 张、同一张图 24 小时内不重复;
你明确要表情包时可以跳过概率与去重,每小时上限仍然生效。 - 主动发言更像本人:可以直接用在 DSH 里配好的人格(比如 dsh-soul-md),也能带表情包;
挑不到合适的图就照常只发文字,不会因为配图把发言搞丢。
修复:
- QQ / NapCat 重启后自动重连,不再出现「程序还开着、机器人却不再说话」的静默离线。
- 发高清大图不再显示发送失败(其实图已经发出去了)。
- 刚发完大图马上让它读聊天记录,不再回答「没有记录」;真出错时会把原因说出来。
- 主动发言不再每轮报「取不到会话模型」并退回降级发言。
- 设置页「发言阶段模型」和「发言阶段 provider」对不上、provider 留空会把配置写空的问题。
其它:
- 设置入口统一到插件详情页,旧的「设置 → QQ 机器人」提示已清掉。
- README 精简,连接方式、权限与指令、表情包、排错等细节移到 docs/USAGE.md。
v0.12.9
chore(release): 0.12.9
0.12.9:适配器静默掉线重连与写调度、大图背压、表情包图库、主动发言人格与设置页两段式修复。
- 适配器静默掉线:握手超时/失败都重连,父进程主动探测
- 大图发送背压不再判失败,工具输出递归清理 undefined
- 动作帧不再排在大图后面:小帧在帧边界插队,qq_history 不再读不到历史
- 表情包图库:目录即库 + 内容哈希索引 + AI 自动打标,qq_sticker 按标签挑图
- 主动发言:会话模型依赖键名修复、宿主人格复用、可配表情包与隐式引导
- 设置页:两段式发言字段各找各的 provider/model,未知 provider 保留
要求 DSH >= 0.2.0-rc.2(0.2.1 系列实测通过)。
v0.12.8
chore(release): 0.12.8
0.12.8:P3 四项结构重构落地(群管理动作表收归、二维码/默认值一致性测试、gateway/plugin.js 拆到 812 行),
外加归档会话自动召回、删除会话后重建、适配器 spawn 修复与设置页排序修正。
v0.12.7
0.12.7 —— 稳定性与体验优化(2026-10-28)
原装抓取工具 moli_fetch 进默认 gatedTools(一道验证墙,不叠第二道)
moli_fetch是 dsh-moli 注册的原生工具(能跟随跳转、写文件、eval 页面 JS),我们不改它的
实现,也不再在入口套一层公网校验/守卫代理——那是我们自己的渲染分支(qq_webfetch的 moli
路径)负责的事,访客继续用它、安全由内网拒绝 + 守卫代理保证。- 原装工具只设一道墙:默认
flowAuth.gatedTools加上moli_fetch,开关打开后拥有者私聊
调它要过一次 6 位动态码;没有第二层地址校验,内网地址也由拥有者用自己的码决定要不要抓。 - 名单仍与接管名单同一份(
flowGate.names()),所以不需要新增任何遮蔽代码:flowAuth 的接管
块本来就会按名单把moli_fetch包一层。 - 测试:
test-flow-wiring加 3 条断言(没码被拦且不调原工具 / 内网目标同样被拦 / 拿码后原样转交)。
守卫代理:判定即钉住地址(DNS rebinding TOCTOU)
resolveGuardTarget把判定阶段解析出的公网地址一并返回;CONNECT 直连与明文 HTTP 直连都用
这个 IP 去连,不再让net.connect/http.request按主机名重新解析一次(明文请求保留原
Host头做虚拟主机路由;上游代理路径不动,目标主机名本来就交给代理侧解析)。- 起因:0.12.6 的守卫代理是「先
dns.lookup判公网、再按主机名连接」——两次解析之间的空窗里,
TTL=0 的域名可以先答公网骗过判定、再答127.0.0.1打进内网,与 0.12.4 在publicFetch
里封掉的是同一类 TOCTOU。 - 测试:
test-guard-proxy加 4 条断言(判定返回钉住的地址 / 一个目标只解析一次 / 解析出内网
拒绝 / 解析不了保留旧语义)。该套件行数记 38 → 47;实测改动前就是 43(既有漂移),
多出的 5 与新增的 4 都在 PR 正文里说明,没有在本次一并重算所有套件。
设置页:preset 三个字段标注「如果你不知道你在做什么,最好别动这个」
agentPresetCustom/auth.ownerPreset/auth.guestPreset的 hint 统一加前缀「如果你不知道你在做什么,最好别动这个」,并写清正确用法:加装了不影响安全的第三方扩展插件、却没进任何 preset 的,在自建 preset
的plugins里加插件行、再把 id 填到这里,而不是放宽访客白名单。- 只改文案:字段位置(高级 → 会话与排错)、默认值、分流优先级都没变。
设置页新增「闸门区」:名单改动要一次动态码(填码解锁 → 保存即消费)
- 设置页「高级」拆出第三个二级标签「闸门区」:
flowAuth.gatedTools(哪些工具必须过动态码)与
flowAuth.gateEditCode(改名单用的 6 位码)放一起,不再和 TOTP 绑定/窗口参数混在流程授权里。 - 行为:动态码开着时名单默认锁定;填满 6 位码即解锁编辑,点「保存」才把码一起提交(服务端
用decideGateEdit验一次 TOTP,±1 窗口);保存成功即重新上锁并清空输入框,服务端也会把
gateEditCode写回空串——码不落盘。不保存就不写入;离开设置页,解锁状态与未保存的改动
一并丢弃(都是组件内 state,没有持久化)。 - 开关本身也要码:把「流程授权」从开切到关(或反过来)和改名单一样需要一次码——否则拿到浏览器
的人可以「先关、改名单、再开」,名单保护就白设了;关掉闸门比改名单更狠,不能更容易。 - 服务端兜底:名单或开关变了但码不对(含没码、没配 secret),插件会把两者一起回滚到上一次接受的
值并清空码字段,日志里写明原因——设置页刷新一下就能看到改动静默回退;不会静默接受。 - 测试:新增
test-gate-guard.mjs(18 项:名单比较、关着不需要码、没码/错码/无 secret 拒绝、
对码接受、±1 窗口容差、开关变更要码、名单没变时顺手清码);test-client断言高级页三个二级标签并覆盖锁定文案。 - 套件 30 → 31,总项 1204 → 1224。只防「绕闸门的人」:填错码只会让改动弹回去,不额外加确认步骤。
回复分条:Markdown 感知切分
- 以前长回答只按换行硬切,会把代码块和 GFM 表格拦腰截断;现在
lib/chunk.js把代码块与表格当原子块搬,
优先在空行处断开,单行超长才按字符硬切;被强切的代码块在本段补收尾围栏、下一段补回语言标记。 - 实现按本仓库风格重写(纯函数、无第三方依赖、逐条断言),判据是「不把 Markdown 结构切坏」;
sendReply换用它。 - 测试:新增
test-chunk.mjs(15 项:短文本/长文本/代码块围栏平衡/表格不切行/超长行硬切/未闭合围栏)。 - 套件 31 → 32,总项 1224 → 1239。
入站去重(消息幂等)
- 新增
lib/msgid-cache.js:按message_id记最近见过的消息,有界(512 条)+ TTL(10 分钟),
重复命中不延长窗口;acceptMessage最前面判重,重投事件只计入skipped/duplicates,不再起第二个回合。 - 触发场景:官方 WS 的 resume/重连会重放少量事件、webhook 可能重投;以前会对同一条消息回两次。
官方 Bot 扫码绑定(服务端 + 设置页卡片)
- 新增
lib/qq-bind.js+lib/bind-routes.js:@tencent-connect/qqbot-connector动态 import(装了就可用,
没装退回手动填 AppID/AppSecret),走 SDK 的startQrConnect回调拿二维码 URL 与凭据;凭据经设置服务落盘,
appSecret只在回调里经过一次、不进任何 HTTP 响应。 - 路由挂在 DSH
webServer上(同源):GET /dsh-qq-bot/bind、POST /dsh-qq-bot/bind/refresh;
设置页「连接」里新增「扫码绑定(手机 QQ)」卡片:显示二维码、2 秒轮询状态、可刷新/重新扫码。 - 既有的 qq-official 适配器 webhook / ws 两条路径都保留,扫码只是把 AppID/AppSecret 填进去。
官方 API v2 富消息:markdown / keyboard / 输入中 / 按钮回传
qq-official适配器的send增加三个可选字段:markdown(走 msg_type=2,可带keyboard按钮)、
keyboard(原样透传;只给 keyboard 时正文包成 markdown)、typing(msg_type=6 输入中,只尽力而为)。
被动回复的msg_id/ 递增msg_seq对 markdown 与输入中同样生效。INTERACTION_CREATE现在同时产出notice(原有行为)与一条message帧(sub_type: 'interaction',
text取按钮button_data)——以前用户点按钮只有一条 notice,模型看不见,像石沉大海。- 测试:
test-adapter-qq-official95 → 102 项(markdown/keyboard/msg_seq、msg_type=6、按钮点击回传与身份保留)。 - 套件 34 / 1271 → 1278 项。
官方 v2 私聊流式(特有能力,适配层就绪)
- 开放平台只对 C2C 私聊开放
stream_messages,所以按特有能力做:qq-official适配器新增
stream命令(input_mode=replace+ 同一msg_seq+ 递增index,结束帧input_state=10;
首帧后复用平台返回的stream_msg_id)。 - 新增
lib/stream-bridge.js:把增量合并成「当前全文」并按 600ms 节流推送,结束时必发 done;
发送失败只记一行日志并停用该会话,不刷错误。计时器可注入,纯逻辑单测覆盖。 - 接线:
runTurn里按shouldStream()(官方 v2 + 开关 + 私聊 + 有msg_id)决定是否走流式;
中间发言改为喂给桥(不再单独发消息),回合末补足剩余文本并发 done 帧;桥没发出任何帧时
退化为普通sendReply,保证用户至少拿到一条。开关在设置页「连接 → 私聊流式回复」,默认关。
模型选择:自动检测,优先用宿主自己的能力
- 新增
lib/model-router.js:按顺序探测 —— ① DSH 原生agentDefaultModel(currentSelection/saveSelection,可写,
改的是默认模型);② DSHllm(listProviders/listModels,只读能列);③ 都没有就落到本地
per-chat 偏好文件(QQ_msg/model-prefs.json),并且如实说明「只记录、不会自动应用」。 - 新增拥有者指令
/model:查看当前模型/接口来源/provider 列表;/model set <provider> <model>;
/model models <provider>。写不进去时不假装成功:返回 applied=false 并说明原因。 - 端到端模拟:
test-plugin新增两条场景——同一message_id重投只回一次;群里@机器人 /model
被截获、不进模型回合。 - 解析器
parseModelCommand统一剥掉群里 @ 前缀与前后空格——第一版在群里被 @ 时文本是
"@123456 /model",startsWith('/model')直接漏判、指令进了模型回合(已修)。 /help里补上/model一行。测试:test-model-router.mjs17 项;套件 35 → 36,总项 1296 → 1313。
测试卫生:失败重试一次并显式标注
test/run-all.mjs:任何套件第一次失败会自动重试一次;重试通过标注「(重试通过)」,
连续两次失败才算失败。会起子进程/真实定时器的套件(adapter/plugin/admin/proactive-round)
不再因为一次机器抖动把整条 CI 判红,也不会静默吞掉失败。- 顺手把 runner 头部过时的「30 个套件」改成不写死数量。
完整变更见 CHANGELOG.md。
v0.12.6
截至 d9d5317(0.12.5)以来的变更:38 个文件,+2156 / −400,新增 lib/guard-proxy.js。
新增
渲染抓取
qq_webfetch接入 Moli 无头浏览器:可抓取需要 JS 渲染的页面,输出 markdown。引擎未启用或不可用时自动退回普通抓取。- 新增
lib/guard-proxy.js:渲染期间 moli 的全部流量(主文档、3xx 重定向、页面子资源、页面 JS 发起的请求)经本地校验代理出站,回环与私网地址一律拒绝。判定语义是「确定是内网才拒」,本机解析不了的域名放行、交给上游代理解析。 - 渲染对 QQ 会话内的访客(群聊与别人的私聊)开放,由
moli.allowGuest与moli.guardProxy两个开关共同控制。
moli 配置段(原来只有 enabled / proxy / exePath)
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
false |
总开关 |
exePath |
'' |
留空 = 自动探测标准位置与 PATH |
proxy |
'' |
留空 = 透传 webSearch.proxy |
defaultTimeoutMs |
30000 |
渲染超时(-t) |
maxChars |
200000 |
单次输出字符上限 |
insecureTls |
true |
跳过 TLS 证书校验 |
guardProxy |
true |
本地校验代理 |
allowGuest |
true |
访客可否触发渲染 |
rateLimitPerMin |
5 |
每会话每分钟渲染上限(0 = 不限) |
maxConcurrentRenders |
2 |
跨会话全局并发上限(0 = 不限) |
设置页
- 新增 Moli 卡片:引擎检测、引擎路径、代理、超时、输出上限、TLS 校验、访客开关、频率限制。
- 卡片内「高级设置」的参数现在可以保存(此前改动不生效)。
变更
auth.guestToolAllow默认值移除moli_fetch,访客的渲染需求由qq_webfetch的 moli 分支承担。- 流程授权的 grant 必须携带工具名,空的
toolName一律拒绝。 - 适配器凭据改从环境变量读取,命令行参数仍然优先。
flowStore移除save与verify:流程脚本由使用者自行放入目录,插件既不写入也不执行。- 换连接配置时等旧适配器真正退出后再启动新的。
- 非 QQ 会话(Web UI、任务板、子代理)仍然不可触发渲染。
修复
- 渲染失败不再返回成功:被防护拦下、上游代理不可达、目标 4xx/5xx 均明确报错。
- 守卫代理区分 403(策略拒绝)与 502(连接类失败)。
dechunkBuffer对负数 chunk-size 的同步死循环。- SSRF 判定补齐 6to4(
2002::/16)与 Teredo(2001::/32);修正 Teredo 取错分组的问题。 - 代理隧道:直连分支的 30 秒空闲超时、CONNECT 目标归一化、响应头 64KB 上限、请求头 CR/LF 校验。
- 守卫代理:
start()并发返回端口 0、ensureGuardProxy并发重复创建、server 未unref()、socks5 上游静默降级。 - OneBot 11 正向 WS 增加半开检测(3 个心跳周期无数据即断开重连);Satori 连续 3 次 PING 无 PONG 改为断开重连。
writeToAdapter返回真实写入结果;顺序锁release幂等并加 120 秒超时。- 图片缓存的三条泄漏路径,新增每 6 小时按 mtime 清理。
- 产物打包从
execFileSync改为异步spawn;适配器 stdout 加 1MB 上限;cleanupStaleConfig补异常处理。 - 会话池加上限 200,按最久未用淘汰。
qq_draw:增加拥有者校验、支持取消、响应体 4MB 上限。qq_imgsearch:本地文件白名单增加当前会话维度。/grant禁止自我授权,授出的权限必须自己已经拥有;/revoke需要grant权限。- 拥有者设置的确认码改用
crypto.randomBytes;恢复码不再受动态码锁定影响。 - 「拥有者私聊」判定改用回合快照,修复
perChatSession=false时高危工具全部不可用。 - 直连抓取增加绝对 deadline。
升级
补丁级更新,配置无需改动;新增的 moli 配置项均有默认值。测试 30 个套件 / 1197 项通过。
v0.12.5
0.12.5 —— 非 QQ 会话越权收口 + 代理隧道统一 + 限流计费修正
一次审查批(#43):
- 非 QQ 会话越权口:
qqbot的send/interrupt与qq_draw在 Web UI 会话 / 任务板 / 子代理里以前没有身份判断,能给任意群发消息、掐断任意会话;现在与qqgroup同一判据(默认拒绝,admin.allowConsole才放行)。 - 代理隧道统一:三条 CONNECT 实现(抓取 / 搜索 / 多引擎)收成一个
proxyRequest()——CONNECT 用resolvePublicAddresses钉住的 IP(SNI 保持原名)、建连与读体共用 deadline、响应体按上限截断、跳转逐跳重算。 - 限流按回合计费:入站限流从消息入口挪到「确定会驱动回合之后」,群里被限流也回一句(按会话 60s 去重)。
- 授权名单合一:
flowAuth.gatedTools同时决定「接管哪些工具」和「谁要动态码」。 - QQ 官方
botSecret/selfId传参、反向 WS 分片按累计字节、safeSessionId拒点号、imgSearch.maxResults生效;外加一批测试「假绿」清理(跳过单列、dsh-tools 链接、mock 修正)。
全套 29 个套件 / 1142 项通过(跳过项单列,不计入通过数)。
v0.12.4
0.12.4 —— 反向 WS 半开连接回收
0.12.3 发完后,独立验收在清单外又抓到一条可用性问题,这版只修这一条:反向 WS 对端只发 FIN(不做 WebSocket close 握手)时连接不回收,4 次半关就能占满 4 条并发名额,之后正常客户端全拿 503,而适配器仍自报 connected:true、links:4。
- 半开连接立即回收:
upgrade的 socket 是allowHalfOpen,对端 FIN 只触发end,而回收逻辑挂在close上;补socket.on('end')→destroy()后正常释放名额并把link.ready置 false。 - 回归断言:
test-adapter-reverse.mjs第 4 段——4 条裸 TCP 握手 + 只发 FIN,断言links归零、新连接仍能 101(未修复时实测links=4、新连接被拒)。
全套 29 个套件 / 1094 项通过。完整清单见 CHANGELOG。
v0.12.3
0.12.3 —— 安全审计修复:SSRF、越权、资源耗尽、默认开放
一轮攻防审计 + 两轮独立验收复测,修完 issue #13–#27 与 #33 共 16 条。
- 出站只允许公网:地址先按 URL 归一(堵
[::ffff:7f00:1]这类写法)再判段,解析结果钉进连接(防 DNS rebinding),8 MB 上限、逐跳校验。 - 入站:默认准入 fail-closed(没填
auth.owners不驱动任何会话)、每人每分钟限流、单条 4000 字、队列上限 5。 - 反向 WS / WebHook:无 token 只监听回环、帧与体上限、并发上限、token 恒时比较。
- 授权与提示词:grant 绑定工具、不可见字符与控制符剥净、身份头清洗。
- 回调重放:QQ 官方 webhook 时间戳 ±5 分钟 + 事件去重。
完整清单与证据见 CHANGELOG;0.12.3 打完后验收又发现一条反向 WS 半开连接不回收的可用性问题,已在 0.12.4 修掉。
v0.12.2
0.12.2 —— 修好「网页端加载失败」与「设置面板空白」
0.12.1 把包名改成 @sputnikzaychik/dsh-qq-bot 之后,插件里还有两处身份写死着旧名字,
装到新版就是网页端不可用:
- 打开网页直接报 Failed to load plugins … could not load "@sputnikzaychik/dsh-qq-bot";
- 就算能打开,插件详情页的配置区也是空白,设置项一个都看不到。
原因是 DSH 有两套身份:客户端模块行、配置区插槽都按 npm 包名派发,
而设置命名空间按 profile 条目 id 派发。插件把前两者也写成了条目 id,改名后自然对不上。
这一版改成:
- 客户端模块 id 用包名(ModuleLoader.load 的 id);
- 配置区插槽 key 用包名(插件管理页按 pkg.name 渲染配置区);
- 设置命名空间仍是 dsh-qq-bot,所以升级不用重填配置——白名单、密钥、绑定都在;
- 清理废弃配置字段时同时认新包名与条目 id,精确匹配,不会误伤 my-qq-bot 这类别的插件;
- 测试里的包名改成从 package.json 读(原先写死,改名后那条断言假通过)。
装过 0.12.0 / 0.12.1 的直接升到 0.12.2 即可,功能内容与 0.12.1 相同,这一版只修客户端加载。
v0.12.1
0.12.1 —— 私聊动态码授权(流程授权)+ 产物投递 + 中间发言
- 流程授权(默认关):
subagent/subagent_fork/spawn_teammate/workflow四个编排工具被同名接管;打开开关后,只有拥有者私聊里回一条 6 位动态码才放行一次调用,关着时与 0.11 完全一致。 - 产物投递:授权跑完这一轮,把会话目录里新增/改动的文件打包送回(默认私聊,可指定群,或只回文字)。
- 中间发言:干活途中先说一句,结论才引用回复。
- 一批安全与稳定性修复:一次调用不再吃掉两个动态码、群里不能白吃私聊的授权、设置页不再覆盖线上密钥。
关于版本号:这批内容原定 0.12.0,但那个号在清理 registry 残留时被 unpublish 过(npm:用过的版本号永不复用),故改发 0.12.1,代码与 0.12.0 一致。发布流程顺带补了 setup-node——runner 自带的 npm 10.9.9 不认 Trusted Publishing 的 OIDC。
逐条的发布链路与文档改动见 CHANGELOG 的 0.12.1 一节。