Skip to content

v0.12.7

Choose a tag to compare

@github-actions github-actions released this 28 Oct 04:16
· 44 commits to main since this release
d2ce4b9

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-official 95 → 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,可写,
    改的是默认模型);② DSH llm(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.mjs 17 项;套件 35 → 36,总项 1296 → 1313。

测试卫生:失败重试一次并显式标注

  • test/run-all.mjs:任何套件第一次失败会自动重试一次;重试通过标注「(重试通过)」,
    连续两次失败才算失败。会起子进程/真实定时器的套件(adapter/plugin/admin/proactive-round)
    不再因为一次机器抖动把整条 CI 判红,也不会静默吞掉失败。
  • 顺手把 runner 头部过时的「30 个套件」改成不写死数量。

完整变更见 CHANGELOG.md。