@fengye_nobug/dsh-qq-bot 0.0.0-stage → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,303 @@
1
+ # 更新日志
2
+
3
+ ## v0.2.5 — 2026-10-30
4
+
5
+ - **设置卡片可以一键更新**:有新正式版时出现「更新到 vX.Y.Z」。点下去才安装,并重启面板。优先执行 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@版本`,走本机 npm / pnpm 已经配置的 registry。插件不写死镜像地址。registry 里找不到这个版本时,改从 GitHub 下载官方源码包(先 github.com,连不上再试同一标签的 codeload.github.com),不走第三方镜像。v0.2.3 及更早标签里的 `package.json` 仍是无作用域名,这种源码包仍然能装。装完提示刷新页面,登录口令可能会变。现有设置还在,不改 NapCat。已经是最新时没有这个按钮。不会安装更旧的版本,也不装草稿和预发布。连不上、安装失败、没能重启,都用白话说明。
6
+ - **「检查更新」和「启动时检查一次」仍然只询问**:不下载、不安装。先问 npm 上的 `@fengye_nobug/dsh-qq-bot`。有比当前新的正式版时不再访问 GitHub。npm 上没有更新,或包还连不上时,才看 GitHub Releases。
7
+ - **npm 包名改为 `@fengye_nobug/dsh-qq-bot`**:无作用域的 `dsh-qq-bot` 发不出去,npm 认为它和已有的 `dsh-qqbot` 太像。npm 上已经发出的是 `@fengye_nobug/dsh-qq-bot@0.2.4`。源码 0.2.5 还没发到 npm,GitHub Release 仍是 v0.2.3。界面显示名和消息来源 `plugin:dsh-qq-bot` 不变,Cordis id 仍是 `qq-bridge`。不要再用 `dsh plugin add dsh-qq-bot@版本`。
8
+
9
+ ## v0.2.4 — 2026-10-30
10
+
11
+ - **设置页可以检查 GitHub 上有没有新版本**:卡片显示当前版本。点「检查更新」才请求 `yunfengnobug/dsh-qq-bot` 的 Releases,按 semver 对比。有新版时写出最新版本号、说明摘要,以及 `github:yunfengnobug/dsh-qq-bot#vX.Y.Z` 的安装命令。没有新版、连不上、被限流,都用白话说明,不把英文报错摆到界面上。不会自动下载或安装。
12
+ - **新设置「启动时检查一次」**(`checkUpdateOnStart`,默认 `false`):打开并保存后,下次启动 dsh-web 向 GitHub 问一次,只写日志。默认关。
13
+ - **安装说明**:README 写清推荐用 Release tag(已发布的是 `v0.2.3`)、Watch Releases、最低环境、白名单先填再 @、三条升级路径(面板升级 / `#vX.Y.Z` / 重装后重启 dsh-web)、与官方 `sj244/dsh-qq-bridge` 不能同时挂同一个 OneBot,以及以后的发版流程(改版本号 → CHANGELOG → tag `vX.Y.Z` → GitHub Release)。
14
+ - **npm 与插件市场**:`package.json` 补了 `publishConfig.access`(`public`)和 `engines.node`(`>=22`),`files` 白名单含更新检查模块和 CHANGELOG。维护者用 `npm login` 后自己 `npm publish`,本仓库不代发。当时写的安装命令是无作用域的 `dsh-qq-bot@<版本>`。这个名字发不出去,实际包名和命令见 v0.2.5。本 fork 还没进 awesome-dsh-plugin,申请步骤和 Issue 模板(连不上 / 不回 / 发不出)写在 README。
15
+
16
+ ## v0.2.3 — 2026-10-30
17
+
18
+ - **商城表情按原生表情包发出去,普通图片仍是图片**:入站 `mface`,以及带着 `emoji_id` / `emoji_package_id` / `key` / `summary` 的 `image` 段,会把这四项跟图片哈希留在一起。图片落盘时旁边写 `<hash>.mface.json`。`qq_bridge_sticker` 的 `save` 把它们抄进登记表。`send` 时四项都在就发 OneBot `mface`(`emoji_package_id` 是数字);缺任何一项仍用 `base64://` 发图片。模型看到的占位符也分开:商城表情是 `[表情包#短哈希]`,普通图片是 `[图片#短哈希]`。没有可下载图片的 `mface` 只有文字占位符,登记不了。没有这些 id 的图(本地图、收藏表情、普通图片)发出去仍是图片。
19
+
20
+ ## v0.2.2 — 2026-10-30
21
+
22
+ - **出站把 `[笑哭]` 这类表情名变成系统表情**:`sendToQQ` 在去掉 Markdown 之后,把正文拆成 OneBot 消息段。`[笑哭]` 是 face 182,`[挥手]` 是 face 129,NapCat 显示系统表情。也认入站占位符 `[表情182]` 和 `[CQ:face,id=182]`。对不上的方括号留在文字段里。标记块和 `qq_bridge_send` 都走这条路。`qq_bridge_sticker` 继续按登记表发送图片。字数上限按拆表情之前的正文计算。
23
+
24
+ ## v0.2.1 — 2026-10-30
25
+
26
+ - **设置接口不再因为没注入 webServer 而变成空的 HTTP 400**:已登录的面板请求会问监听器是不是 HTTPS。以前读 `ctx.webServer`,插件并没有 inject 这项服务,Cordis 直接抛错,宿主收成没有正文的 400,卡片诊断显示本机接口不可用。现在只通过 `ctx.get('webServer')` 读协议。读不到这项服务、或读取失败时,按 HTTP 继续做登录判定。
27
+ - **按群新建的会话带工作目录和标题**:以前新建会话不带 `meta.cwd`,会话落在 `_no-cwd`,面板打开报 `session/not-found`,轨迹是空的。标题只写在日志里,列表显示「未命名」。现在工作目录优先用宿主 `workingDirectory.defaultDirectory`,没有这项或它不是绝对路径时,用进程当前工作目录。标题写成「QQ群 <群名或群号>」或「QQ私聊 <昵称或 QQ 号>」:先走宿主的会话改名,改名服务不可用或失败时,直接写入 `session/title`。升级前已经建好、当时没有工作目录的旧会话不会自动改。删掉状态文件里对应的 `sessionByDest`,再在那个群或私聊里 @ 一次,才会按新规则新建。
28
+
29
+ ## v0.2.0 — 2026-10-30
30
+
31
+ Fork 功能版本。Cordis 插件 id 仍是 `qq-bridge`。
32
+
33
+ - **设置卡片不等宿主设置服务**:DSH 0.2.x 上 `settingsScope` / `webUiSettings` 可能不存在,以前会一直等、卡片不出现。现在加载时就挂到设置页和插件页(含插件详情,只挂在本包上)。宿主设置域还在就用它,否则走 `/api/qq-bridge/settings`。请求带上面板登录 cookie。卡片默认展开。
34
+ - **NapCat 网页地址和缺失目录**:新增设置 `napcatWebuiUrl`(「NapCat 网页管理地址」)。留空仍读 `webui.json`;地址是 `127.0.0.1` 或 `localhost` 时,「打开 WebUI」保持可点,并说明只有跑着 NapCat 的机器能打开。安装目录不存在时「打开文件夹」不会去猜路径,提示自己部署或 Docker 要另填目录。NapCat 已经自己跑着时,可以跳过下载那一段,但 OneBot 端口仍要对上。
35
+ - **设置卡片改成白话**:标签、说明和选项都是中文,例如「群里发出去的是哪一段」「忙的时候怎么插话」。配置键仍是英文。
36
+ - **设置和 NapCat 按钮不再只限本机回环**:本机请求照旧可用。经反向代理访问时,认 DSH 面板已经通过的登录会话,不必再配 `trustedHosts`。没有会话的外网请求仍然拒绝。打到 `127.0.0.1`、但来源是别的网站的请求也仍然拒绝。
37
+ - **出站目的地钉在唤醒那一轮**:标记块、发消息工具、表情包和会话跟随都用这一轮被叫醒时的群或私聊。别的群里只被记录的消息仍会更新“最后一次入站”,但不会把正在回答的这一轮带跑。工具里显式写了群号或 QQ 号时发到那里,并且不引用别的会话的原消息。
38
+ - **按目的地分会话,并限制每个会话保留的轮数**:新设置 `sessionScope`(`single` 默认,沿用一个 `targetSessionId`;`perGroup` 为每个群、每个私聊自动创建或恢复一条 DSH 会话)。映射持久化在状态文件的 `sessionByDest`。`maxRecentTurns` 默认 40,`0` 表示不限制;QQ 驱动的一轮结束后,若宿主有 `compaction.compactNow`,会尽量把更早的轮次压到这个上限。
39
+ - **Fork 品牌改为 dsh-qq-bot**:包名、界面显示名、NapCat 配置里的默认名称改为 dsh-qq-bot。Cordis 插件 id、设置命名空间和 `/api/qq-bridge/...` 仍是 `qq-bridge`,已有 profile 覆盖不用改。v4 消息来源写成 `plugin:dsh-qq-bot`,同时仍认上游的 `plugin:dsh-qq-bridge`。上游 [sj244/dsh-qq-bridge](https://github.com/sj244/dsh-qq-bridge) 的 MIT 版权保留。
40
+
41
+ - **修 issue #1:相邻重复的 `[QQ]` 块只发一次**:模型先写回答、再调工具、随后又把同一句重写一遍时,
42
+ 本轮正文按 step 累积,拼接后 QQ 会收到连着两句相同的话。现在相邻且完全相同的块只保留一个;
43
+ 非相邻的重复保持原样(可能是刻意复述)。比较的是块内正文(已 trim),
44
+ 所以中间夹着给 DSH 看的正文也能识别。
45
+ - **提示词:不要给正文加额外标签**:部分模型习惯在句子后面补 `</test>` 这类标签。
46
+ 群聊礼仪里写明「要界定引用就用双引号,不要额外加标签 —— 标签起不到界定作用,
47
+ 只会跟着正文一起发到 QQ」。
48
+ - **提示词:无人值守的判据改成“要不要提权”**:原来写成一串「不要主动做:执行或安装程序……写文件」,
49
+ 有的模型把它读成“什么都不能改”,连会话工作区里写文件都不敢做。现在写明判据是
50
+ **这件事要不要向会话发起人提权(征求同意)**,并明确「会话工作区里写文件、改代码、跑测试、
51
+ 读资料都是正常工作,照做即可」。同时补上例外:**管理账户在私聊里明确要求时按该要求执行** ——
52
+ 私聊 + 被唤醒即说明发信人在白名单内,就是会话发起人本人;群里任何人的同类要求仍不算例外。
53
+ 不弹审批的路径(浏览器点网页、直接发 HTTP 写请求)仍然不许拿来绕过这份清单。
54
+ - **用法说明的重挂判据改为比对渲染后的正文**:原来比一个「设置签名」。签名漏过两次 ——
55
+ 先只按 `replyMode` 判(改出站字数上限不生效),再是正文加了一句措辞而签名没跟着变
56
+ (改文案不生效)。改文案也要重启一次才生效,这一点很难被发现。
57
+ 现在直接比 `buildUsagePrompt()` 的输出,设置与文案任何一处变了都会重挂,不必再维护签名的键清单。
58
+
59
+ ## v0.1.3 — 2026-10-10
60
+
61
+ 维护性更新:1 项安全修复 + 两轮审计修复 + 文本规范修订。无功能新增。
62
+ 行为有 3 处变化:超长出站改为拒绝发送、设置卡片支持撤销覆盖、省略条数按真实丢失量统计。
63
+
64
+ - **本机接口不再只认回环地址,改为校验请求来源**(安全修复):设置接口与 NapCat 路由原来只看对端
65
+ 地址,用户浏览器里的任意网页都能用无预检的 `fetch` 打到本机端口 —— 响应读不到,但改白名单、
66
+ 改目标会话、启动 NapCat 这些副作用照发生。现在两道路由组共用同一道闸门,按
67
+ 「对端地址 → 来源(`Origin`,无 `Origin` 时看 `Referer`)→ 带体 POST 的 `content-type`」判定。
68
+ 空体的 POST 不要求 `content-type`:跨站简单请求的 `content-type` 只能取安全列表里的值,
69
+ 带体的 POST 必须声明 `application/json`;空体改不了任何东西,且来源判定对所有请求都生效。
70
+ 卡片发出的 POST 已补上 JSON 头。原来直接显示在卡片上的英文错误码一并改成中文。
71
+ - **用法说明的重挂判据改为设置签名**:原来只看 `replyMode`。改「出站字数上限」后常驻提示仍写旧值,
72
+ 模型按旧上限写字,出站一律按 `too-long` 丢弃,QQ 里没有消息且模型不知道。
73
+ 现在判据是提示词所依赖设置(`replyMode` 与 `replyMaxChars`)的签名。
74
+ - **目标会话被卸载后不再复用死句柄**:命中缓存句柄时按对象同一性验活,失效就删条目并走 `resume`。
75
+ - **出站被拒时新增独立丢弃台账并回注会话**:标记块被拒(超长、未连接、无目的地)原来只写日志,
76
+ 模型以为已经回复。现在记入 `state.dropped`(环形上限 20 条),并在 `qq_bridge_status` 的
77
+ `recentDropped` 暴露;同时用 `agent.inject()` 把原因回注会话 —— 只进上下文、不唤醒,
78
+ 避免模型再写一条同样超长的消息。被拒的出站仍然不写 `outbox`:那份台账的语义是「真的发到 QQ 了」。
79
+ 两条出站路径(标记块与 `qq_bridge_send` 工具)都记进同一份丢弃台账;
80
+ 工具路径不回注会话,因为原因已经随工具结果回给模型。
81
+ - **图片占位符与描述一一对应**:原来占位符按消息里全部图片段生成,而只有落盘成功的图才进描述数组,
82
+ 第 1 张落盘失败时第 2 张的描述会落到第 1 个占位符上。现在落盘失败的位置留空位,
83
+ 输出 `[图片](未描述)`,后面的图不顶位。
84
+ - **设置卡片支持撤销覆盖**:设置文件的值优先于插件配置页,而卡片原来只写不删,
85
+ 用户从插件配置页改了不生效,也无处重置。现在被覆盖的字段旁有「撤销覆盖」按钮,
86
+ 撤销后回落到插件配置页的值。
87
+ - **群聊判据改用 `groupId`**:原来用群名判断,`get_group_info` 失败时群消息会带
88
+ `[QQ · 私聊]` 前缀进入上下文。现在取不到群名时写「群 `<群号>`」。
89
+ - **视觉模型列举失败不再静默**:单个 provider 的 `listModels` 抛错原来被当成「没有视觉模型」。
90
+ 现在记 warn,带 provider id 与失败原因。
91
+ - **STC 文本修订**:文档里 10 类与事实不符的说法已修正(版本号、`recentChatLimit` 默认 8、
92
+ 超长由分段改为拒发、白名单已改空、M3 已完成、文件清单尺寸等)。
93
+ 对外文档与代码注释里的直角引号与半角引号统一为全角双引号,
94
+ 行内代码与围栏代码块内的字面值保留。
95
+ - **审计第 2 轮其余条目记录在案**:出站闸门的多标志重构留待单独一轮;
96
+ `wakeProbability` 填 `5` 被当成 100% 判为假问题(卡片已写明该字段是 0 至 1 的比例,
97
+ 并给出 0.05 = 5% 的换算与 0.01 的步进)。
98
+ - **测试总数**:四个套件 498 条断言全绿
99
+ (`test-smoke` 372 / `test-onebot` 27 / `test-napcat` 59 / `test-stickers` 40)。
100
+
101
+ - **公开仓库脱敏:真实群号、QQ 号、私人昵称与本机路径换成占位值**:只换字面值,不改逻辑、
102
+ 不改断言结构、不删用例。
103
+ 真实群号换成 `12345678`,涉及 `test-smoke.mjs` 与 `test-stickers.mjs`。
104
+ 机器人自身 QQ 号换成 `10000001`,测试号换成 `10000002`,都在 `test-smoke.mjs`。
105
+ 私人昵称换成 `小助手`,涉及 `cordis.patch.yml`、`index.js`、`client/client.js`、
106
+ `README.md`、`HANDOVER.md`、`test-onebot.mjs`、`test-smoke.mjs`。
107
+ 本机绝对路径换成 `<工作区>`、`<QQ 安装目录>`、`<DSH 安装目录>`,涉及
108
+ `docs/dsh-0.2.0-migration.md` 与 `HANDOVER.md`。
109
+ `README.md` 的断言总数由 360 更正为 417;四个套件的断言总数仍为 417。
110
+ - **未唤醒摘要的省略条数改为真实丢失量,超过 99 条写“大量消息”**:`renderDigest()` 的省略数
111
+ 原来是 `picked.length - lines.length`,只数窗口内没装下的条目。窗口外(比 `recentChatLimit`
112
+ 更早)的条目同样随快照一起删除,却没有计入,所以对模型报的丢失量偏小。
113
+ 现在省略数取 `list.length - lines.length`:`list` 是传入的缓冲快照,`lines` 是真正写进摘要的行数。
114
+ `lines.length` 含被截断后仍然保留的那一条,这一点未变。
115
+ 省略数大于 99 时只写“更早的大量消息已省略”,不写数字,避免巨大的数字进入上下文。
116
+ 省略数为 0 时不加省略说明。`handleInbound()` 里按快照删除缓冲的行为未变。
117
+ README 的缓冲说明与上下文上限表同步。测试新增 9 条断言。测试总数 399 → 408。
118
+ - **接住投递 Promise 的异步失败**:`handleInbound()` 的 `deliver()` 丢弃了
119
+ `agent.followup()` / `agent.steer()` 的返回值。该返回值是 rejected Promise 时,
120
+ 既没有人接住它,也没有把台账改成 `wake-failed`,进程还会报 unhandled rejection。
121
+ 现在 `deliver()` 把返回值传出来:`withInitiator` 的返回值是 thenable 时用它的返回值;
122
+ `withInitiator` 抛错后改调 `send()`,那一次的返回值同样传出来。
123
+ 返回值是 thenable 时挂 `.catch()`:把同一条台账条目的 `decision` 改成 `wake-failed`,
124
+ `error` 写成“投递后这一轮失败:<原因>”,并调用 `saveState()` 与 `logger.warn`。
125
+ 这一路不回滚 `pendingFromQQ` / `currentTurnFromQQ` / `claimedFromQQ`:投递已经真的把这一轮
126
+ 开起来,三面标志已被这一轮正常消费,事后回滚会破坏下一轮的归属。
127
+ 同步失败路径保持原样,两者都不向调用方抛。测试新增 9 条断言。测试总数 408 → 417。
128
+ - **唤醒失败不再记账为成功**(严重):`handleInbound()` 先前在 `d.action === 'wake'` 时就写了
129
+ `decision: 'wake'` 的台账,之后 `ensureTargetAgent()` 抛错(未配置 `targetSessionId`、resume 失败)
130
+ 或投递抛错(`followup` / `steer` 抛错,含 `withInitiator` 失败后那次 `send()` 再抛)都会异常退出,
131
+ 台账里留下的仍是 `wake` —— 事后查 `status.recentLog` 看不出失败。
132
+ 现在 `pushLog()` 返回推入的条目对象,唤醒路径保留引用:投递失败时把 `decision` 就地改成
133
+ `wake-failed` 并加上 `error`;`handleInbound()` 返回
134
+ `{ ...d, delivered: false, wakeFailed: true, error }`,不向上抛,`action` 仍为 `wake`。
135
+ 失败时同时回滚 `pendingFromQQ` / `currentTurnFromQQ` / `claimedFromQQ` 到置位前的值
136
+ (失败的唤醒不再污染下一轮的归属判定)。投递失败时不进入清缓冲那一步,
137
+ 失败的消息留在缓冲里,下次唤醒还能带上。
138
+ 测试新增 18 条断言覆盖这 3 个失败点、回滚与缓冲。测试总数 366 → 384。
139
+ - **视觉模型的解析结果改为按设置签名缓存**(严重):`visionModelCache` 原来只存一个值,
140
+ 一个进程内只解析一次。用户在设置卡片里填 `visionModel` 不生效;部署当时没有可用视觉模型时,
141
+ 之后补上模型仍然返回“没找到支持图片输入的模型”。现在缓存是 `{ signature, model }`,
142
+ 签名取 `visionModel` 的值。签名不同就重新解析,解析失败的 `null` 也带签名。
143
+ `applySettingChanges()` 丢掉“解析失败”的缓存,所以之后补上视觉模型时,
144
+ 改任何设置都会重试一次;成功的解析照旧按签名缓存。
145
+ 测试新增 15 条断言覆盖缓存命中、`null` 缓存、改值后重新枚举、失败结果的重试与描述路径用到新模型。
146
+ 测试总数 384 → 399。
147
+ - **未唤醒摘要的清缓冲说明改为与真实行为一致**:只改注释与文档,行为未变。
148
+ 投递成功后按快照删除缓冲里的**全部**条目,没进摘要的旧条目也一起删掉,不会留到下一次唤醒。
149
+ `index.js` 的注释、README 与 CHANGELOG 里的描述同步修正。
150
+ - **默认值修正**:`cordis.patch.yml` 里的 `targetSessionId` 与 `whitelist` 不再带占位值
151
+ (原来的 `'<TARGET_SESSION_ID>'` 与 `['test-user']`),两项都改为空。
152
+ 空 `targetSessionId` = 不驱动任何会话;空 `whitelist` = fail-closed,任何来源都不能唤醒会话。
153
+ README 的设置项表同步了这一默认值。
154
+ - **超长出站改为拒绝发送**:出站文本超过 `replyMaxChars` 时**一条都不发**,不再分段。
155
+ `sendToQQ()` 返回 `{ sent: 0, dropped: true, reason: 'too-long', limit, length }`。
156
+ 删除了分段函数 `chunkText()` 与分段发送路径。拒绝原因会经 `qq_bridge_send` 的返回、
157
+ 标记块模式的日志、用法说明与设置项说明传到模型侧,文案统一为“超过上限的回复不会发出,请缩短后重发”。
158
+ - **表情包措辞更新**:用法说明里加一条:发了表情包不要解释它是什么意思(除非有人问)。
159
+ 人自己看得懂表情包,解释反而奇怪。
160
+ - **出站台账只记真话**:`outbox` 只记录真正发出去的消息。
161
+ 被拒绝的发送(超长、未连接、无目的地)不再落台账,只写 warn 日志。
162
+ 原来的写法会让台账里出现 QQ 里根本没有的消息,事后对不上账。
163
+ - **测试修复:出站用例改为看真实发送帧**(严重):主 ctx 从未连上传输,
164
+ 12 条出站用例靠假台账被误判为通过,实际每条都是 `reason: 'not-connected'`。
165
+ 现在为主 ctx 装上能连上的假 WebSocket,判据改为 `send_group_msg` /
166
+ `send_private_msg` 帧里的文本。负面用例同时检查真实帧与台账 2 个口径。
167
+ 新增 2 条断言钉住传输处于 `open`。测试总数 361 → 366。
168
+
169
+ ## v0.1.2 — 2026-10-10
170
+
171
+ - **设置卡片的“启动 NapCat”按钮反映运行状态**:NapCat 运行中时,按钮显示“已在运行”并禁用,
172
+ 不再重复启动。宿主半新增 `napcatRunning()`,判据有 2 条:本插件启动的进程还没结束,
173
+ 或 OneBot WS 已连接(`transport.isOpen()`)。第 2 条用于识别外部启动的 NapCat,例如手动跑过 bat。
174
+ `GET /api/qq-bridge/napcat` 的返回值新增 `running` 与 `startedByPlugin` 两个布尔字段。
175
+ `POST /api/qq-bridge/napcat/launch` 在 NapCat 已运行时不再启动,
176
+ 返回 `{ ok: false, error: 'NapCat 已在运行,未重复启动' }`。
177
+ 卡片展开期间每 5 秒轮询一次该接口,用于发现外部启动。
178
+ 卡片收起或组件卸载时清掉定时器。
179
+ 启动请求结束后,按钮重拉一次状态,成功与失败都重拉。
180
+ - **NapCat 启动改为按布局探测,并优先调用官方 launcher**(真机修复):以前把路径写死成
181
+ `<安装目录>/bootmain/NapCatWinBootMain.exe`。用户把 `napcatInstallDir` 设成 `NapCat.Shell` 子目录时,
182
+ 这条路径必然不存在,点“启动 NapCat”只会报“找不到 …,先跑 action:download”。
183
+ 现在 `napcatPaths()` 按候选顺序探测,**OneKey 根与 `NapCat.Shell` 子目录两种布局都能解析**:
184
+ `shellDir` 取存在的 `<安装目录>/NapCat.Shell`,不存在时取 `<安装目录>`;
185
+ `bootMain` 依次取 `<shellDir>/bootmain/…`、`<安装目录>/bootmain/…`、`<shellDir>/NapCatWinBootMain.exe`;
186
+ 新增 `shellDir`、`hookDll`、`launcherBat` 三个字段。
187
+ 启动时优先执行官方 `launcher-win10.bat`(工作目录设为 `shellDir`):该脚本自己读注册表找 QQ 路径,
188
+ 非管理员时用 UAC 自我提权,所以**可能弹出 UAC 窗口**。
189
+ 没有 launcher 脚本时才直接调用引导器,参数改为官方顺序
190
+ `<bootMain> <QQ.exe 路径> <hook.dll> [-q <QQ号>]`;以前只传了 `<bootMain> <QQ号>`,
191
+ 并且工作目录是 `bootmain`,路径即使对也起不来。
192
+ 错误信息现在写明**期望路径**与**下一步动作**,且不含 token 值。
193
+ - **设置卡片新增 3 个操作按钮**:展开卡片后可以**打开 WebUI**、**启动 NapCat**、**打开文件夹**。
194
+ 打开 WebUI 的地址由 `<安装目录>/config/webui.json` 里的 host / port / token 拼成(没读到就禁用并写明原因);
195
+ 启动复用 `qq_bridge_napcat` 的 launch;打开文件夹按系统调用 `explorer.exe` / `open` / `xdg-open`。
196
+ 宿主半新挂 `GET /api/qq-bridge/napcat` 与 `POST /api/qq-bridge/napcat/launch|reveal` 三条
197
+ **不受 legacy 门控**的路由(两代宿主都有),只服务本机:非 loopback 一律 403。
198
+ token 只拼进 URL,不写日志。
199
+ - **出站更新(二次确认)**:工具模式下,不是 QQ 唤醒的那一轮不再“硬拒绝”。第一次调用只回一句确认、
200
+ **不会发出去**;同一轮里**再调一次**才真的发(确认状态按轮清零,不会跨轮泄漏;非 QQ 轮的发送在
201
+ outbox 里标 `nonQQTurn: true`)。
202
+ - **图片描述改为就地标注**:以前描述另起一段附在消息末尾,模型会读成“这张图没解析出来、后面又来了一张”;
203
+ 现在直接把描述填回图片原来的位置,形如 `[图片]:<描述>`(2026-10-02 起改成 `[图片#<短哈希>]:<描述>`,见本节末条)。
204
+ - **戳一戳更新**:戳一戳走的是 **notice 事件**(不是消息段),以前会被静默丢掉;现在“戳我”和 @ 我同级
205
+ **必唤醒**,戳别人只记录不唤醒,其它 notice 会留一条日志方便后续适配。
206
+ - **兼容 DSH 0.2.0 的设置页**:0.2.0 换了**两处**名字 —— 客户端设置服务从 `settingsScope` 变成
207
+ `webUiSettings`,插件配置页的挂载点从 `settings.plugin.item` 变成 `settings.plugins.tab`(插件自己是一个 tab)。
208
+ 插件现在**两个服务门、两个 slot 各注册一次**,哪个存在哪个生效,0.1.5-rc.3 与 0.2.0 都能显示设置卡片。
209
+ - **兼容 DSH 0.2.0 的宿主设置模型**(真机修复):0.2.0 删掉了 `settings.register/get`(DSH #677),
210
+ 设置表单改为从插件自己的 `Config` 派生。老代码在 `apply()` 里无条件 `register` 会抛 TypeError →
211
+ **整条 fiber failed**(客户端半照样显示 tab,看起来就像“设置项全没了”)。现在按方法探测走两条路:
212
+ 老宿主维持命名空间,新宿主直接读 entry 配置并 `configure({ auto: true })` 生成页面。
213
+ 顺带修掉 `readSettings()` 里若干字段没回落 `config` 的隐患(新模型下会悄悄丢用户配置)。
214
+ 客户端卡片也**两个可写设置 key 都绑**(0.1.5 的 `qq-bridge` vs 0.2.0 的 `include:qq-bridge`),谁 ready 用谁。
215
+ - **兼容 DSH 0.2.0 的消息格式 v4**:v4 废弃了 `source.kind === 'plugin'`(`assertV4MessageSources` 直接抛
216
+ “requires a producer-owned source kind”),导致 QQ 唤醒的轮次整个失败。注入消息的 source 改为按宿主分叉:
217
+ 0.1.5 用老包装、0.2.0 用 producer-owned 的 `{ kind: 'plugin:dsh-qq-bridge' }`(`injectedMessageSource()`,导出可测)。
218
+ - **0.2.0 上设置卡片可用了(自建设置通道)**:0.2.0 的宿主设置桥对第三方插件不给可写域(#677 迁移未完成),
219
+ 于是照抄生态里真正能用的做法(皮肤中心):宿主半**可选**注入 `webServer`,挂一条**只服务本机**的
220
+ `GET/POST /api/qq-bridge/settings`;值存 `$DSH_HOME/qq-bridge-settings.json`
221
+ (优先级:设置文件 > composition config);浏览器半在拿不到可写域时自动改走这个接口。
222
+ 非 loopback 请求一律 403,只写 schema 白名单里的键。0.1.5 与 0.2.0 上都能改设置了。
223
+ - **表情包(新功能)**:群友发的图可以登记成表情包,之后按情绪或哈希再发出去。新模块 `stickers.js`
224
+ (纯函数 + 文件读写,导出以便测试):`hashOfBuffer()` 取图片字节 sha256 的前 16 位十六进制,
225
+ `loadStickers()` / `saveStickers()`(原子写,读不到或坏了返回空表)、`upsertSticker()` /
226
+ `findSticker()` / `listStickers()`、`importSticker()`(算 hash、按需复制图片本体、登记)。
227
+ 登记表存 `<statePath 目录>/qq-bridge-stickers.json`,图片本体存同目录的 `qq-bridge-stickers/<hash>.<ext>`。
228
+ 新工具 `qq_bridge_sticker`(**不受 `debugTools` 限制**)有 3 个 action:`list` 列已登记的
229
+ (短哈希 / mood / 截断描述 / 大小)、`save` 登记一张图(给 `hash`,或 `fromLast: true` 取缓冲区里最近一张;
230
+ `mood` 与 `description` 必填)、`send` 发一张(给 `hash` 精确发,或只给 `mood` 在该情绪里随机挑一张)。
231
+ 发送与 `qq_bridge_send` 共用同一道来源闸门(抽出为 `outboundGate()`);图片本体用 `base64://` 发送,
232
+ 不用本地路径(NapCat 的 `enableLocalFile2Url` 是 false);成功记 outbox(`via: 'tool'`、`action: 'sticker'`)
233
+ 并更新该条的 `sentCount` / `lastSentAt`。
234
+ - **图片占位符带短哈希 + 描述缓存命中**:入站占位符从 `[图片]:<描述>` 改成 `[图片#<短哈希>]:<描述>`
235
+ (描述失败写成 `[图片#<短哈希>]:(描述失败:原因)`),模型据此能指着一张具体的图交给 `qq_bridge_sticker`;
236
+ 注入的系统提示同步改了格式说明。图片缓存的文件名改成内容哈希(`<hash>.<ext>`):同一张图不重复落盘,
237
+ 命中已有文件时也拿得到哈希。`describeImage()` 先查登记表,命中且有 `description` 时**直接返回登记的描述、
238
+ 不调用多模态模型**(反复出现的图只描述一次),日志只记短哈希。新增测试套件 `test-stickers.mjs`(40 条);
239
+ `test-smoke.mjs` 增加描述缓存命中、入站短哈希渲染与表情包工具的断言。
240
+
241
+
242
+
243
+
244
+
245
+ ## v0.1.1 — 2026-10-01
246
+
247
+ 生活更新(补丁版,无破坏性改动)。
248
+
249
+ - 识图:改成让模型**逐字抄下图中所有可见文字**再说明画面,token 上限 300 → 600。
250
+ 说明:描述偏短的原因是提示词要求 1 至 2 句,不是代码截断。
251
+ - 设置卡片:改成折叠块,点标题展开;界面里可以改的设置项补齐(含 visionModel、agentPreset)。
252
+ - OneBot 地址/token 栏加形状校验,避免把整段配置粘进单栏。
253
+ - 目标会话下拉不再列出已归档的会话(当前值仍保留,不会把你的选择吞掉)。
254
+
255
+ 本项目遵循语义化版本。**v0.1.0 是第一次公开发布。**
256
+
257
+ ## [0.1.0] — 2026-09-30
258
+
259
+ 把**一个已存在的、固定的 DSH 会话**接到 QQ(OneBot / NapCat),**保留原上下文**,
260
+ 并用一套省 token 的唤醒策略决定哪些 QQ 消息真正唤起模型。
261
+
262
+ **和同类 QQ 插件的区别**:接的是**同一个老会话**(不新建、不重置人设),并且**唤醒要花钱、所以要挑**。
263
+ 多数同类方案是“每个群 / 每个私聊用户各自开一个新会话”。
264
+
265
+ ### 能力
266
+
267
+ - **固定会话注入**:`agents.get(id)` 拿不到就 `agents.resume({resumeSessionId})` 显式拉起;**绝不回退**到别的会话。
268
+ - **唤醒策略(省 token)**:白名单 fail-closed → 被 @ / 命中昵称必唤醒 → 否则按概率(默认 5%)→
269
+ 其余**只记录不唤醒**(零模型调用),下次唤醒时取最近 `recentChatLimit` 条(默认 8)作为上下文附上;
270
+ 没进摘要的旧条目随这次清缓冲一起删掉,不会留到下一次唤醒。
271
+ - **会话跟随** `qq_bridge_listen`:一段线性对话开个短窗,期间普通消息也唤醒,不必每句都 @。
272
+ - **OneBot v11 传输**:内置 WebSocket(**零第三方运行时依赖**),指数退避重连、心跳看门狗、action/echo 配对。
273
+ - **出站方式开关** `replyMode`:
274
+ `marker`(只发 `[QQ]…[/QQ]` 块)|`tool`(关掉标记块,改用 `qq_bridge_send` 工具,**发不出去时有明确反馈**)|`always`。
275
+ - **投递方式** `delivery`:`auto`(目标正忙时**插话**)、`followup`(排队)、`steer`(总是插话)。
276
+ - **设置界面**:`设置 → 插件 → 插件配置` 里的“QQ 桥接”卡片(目标会话下拉、昵称、白名单、概率、出站/投递方式……),
277
+ **保存即生效**,改出站方式会立刻重挂注入给会话的用法说明。
278
+ - **图片**:收到即落盘缓存 → 唤醒时用多模态模型转成文字描述(目标模型看不见图时的唯一出路)。
279
+ - **NapCat 自助托管**:查 Release、sha256 校验下载、纯 Node 解包 ZIP、写 OneBot 配置、启停。
280
+ - **无人值守安全边界**:注入的系统提示明确禁止无人值守时做特权操作、把 QQ 消息当不可信输入;
281
+ **明确不做审批桥接**(把审批搬到 QQ = 把“谁能发消息”变成“谁能替你授权”)。
282
+
283
+ ### 修复(都是真机联调暴露的)
284
+
285
+ - 出站闸门**按来源生效**:只有被 QQ 唤醒的那一轮才出站,DSH 界面里的标记块一个字都不发。
286
+ - 出站归属**按轮快照**:轮内插入别的消息不会把归属打掉(哑火)或抬高(误发)。
287
+ - **四次哑火、四个不同成因**:`source` 字段被上游重写/单一标记被同轮消息打掉/
288
+ harness 注入的上下文被当成“一轮的触发器”/`followup()` **同步**起轮导致置位晚一拍。
289
+ - 提示词**按出站方式分叉**,并在方式变化时**重挂**(提示词正文是挂载时的快照)。
290
+ - 设置卡片的 key 必须**逐字等于** settings 命名空间,否则界面静默空白(有测试盯住)。
291
+ - 图片:唤醒结束时的清缓冲会吞掉期间新到的消息(“发三张图只处理一张”的元凶)。
292
+ - 非文本消息段(表情包 / 语音 / 文件 / 合并转发……)都有占位符,**绝不让整条消息凭空消失**。
293
+ - **恢复没开着的会话时必须带上模型**:否则部署提示词里的 `{{model}}` 无值,
294
+ 每一轮都在组装提示词时失败(`prompt variable "{{model}}" has no value …`)。
295
+ 现在按官方 GUI 的优先级取值:会话的模型选择投影 → 日志里最后一次 `request/header` → 部署默认模型。
296
+ (目标会话在 GUI 里开着时走的是“活的 agent”路径,所以这个坑只在**冷恢复**时出现 —— 真机验证过。)
297
+
298
+ ### 已知限制
299
+
300
+ - **NapCat 自助托管未在真机跑通全流程**:安装器与 QQ 登录是交互式的,需要人在场(工具本身已离线验证)。
301
+ - 目标会话的模型如果不支持图片输入,必须配一个多模态模型(`visionModel`);否则只看到“图片省略”。
302
+ - 白名单为空 = **谁都不能唤醒**(fail-closed),第一次用必须先把群号 / QQ 号填进去。
303
+ - 回复方式选 `tool` 时,每轮多一次模型调用(工具结果回来还要再出一轮)。
package/DESIGN.md ADDED
@@ -0,0 +1,184 @@
1
+ # dsh-qq-bot 设计草案
2
+
3
+ > 本文记录上游 [sj244/dsh-qq-bridge](https://github.com/sj244/dsh-qq-bridge) 的设计。本 fork 的 npm 包名是 `@fengye_nobug/dsh-qq-bot`(无作用域的 `dsh-qq-bot` 发不出去,它和已有的 `dsh-qqbot` 太像)。界面显示名仍是 dsh-qq-bot。Cordis 插件 id 仍是 `qq-bridge`。文中的 `qq-bridge` 是插件 id,不是包名。上游许可证为 MIT,版权归 sj244。
4
+
5
+ > 目标:把 **一个指定的、已存在的 DSH 会话**(当前目标:
6
+ > `<TARGET_SESSION_ID>`,<TARGET_WORKSPACE> 工作区)
7
+ > 接到 QQ,**保留原有上下文**(同一个 sessionId,不新建会话)。
8
+
9
+ > Fork 现状以 README 为准。`sessionScope` 默认仍是 `single`(一个 `targetSessionId`)。设成 `perGroup` 时,每个群、每个私聊各有一条会话。出站在一轮里钉住唤醒时的目的地,`lastDestination` 只在没有这根钉时作回落。设置接口在本机可直接用;经反向代理时要已登录的 DSH 面板会话,且来源主机名和请求的 `Host` 一致。空的 `requestRejection` 不算已登录。
10
+
11
+ ## 1. 与现有 QQ 插件的根本区别
12
+
13
+ | | 现有 QQ 插件(官方 / community / qqchat) | dsh-qq-bot(id 仍是 `qq-bridge`) |
14
+ |---|---|---|
15
+ | 会话模型 | 每个 QQ 私聊/群 → **新建**一个独立会话 | **固定指向一个已存在的 sessionId** |
16
+ | 上下文 | 从零开始 | **原样继承**(会话日志即上下文) |
17
+ | 唤醒 | 每条消息都跑模型 | **昵称必醒 + 5% 概率醒 + 其余只记录** |
18
+
19
+ ## 2. 唤醒策略(核心逻辑)
20
+
21
+ ```
22
+ 收到 QQ 消息
23
+ ├─ 不在白名单? → 丢弃 + 记日志(fail-closed)
24
+ ├─ 群里且 atOnlyInGroup 且没被 @? → 丢弃 + 记日志
25
+ ├─ 文本命中昵称 / 被 @ ? → 唤醒(followup)
26
+ └─ 否则 → random() < p ? 唤醒 : 只记录
27
+ (p 默认 0.05,可在设置界面调)
28
+ ```
29
+
30
+ - **昵称匹配**:归一化后子串匹配(小写、去空格与常见标点),另支持 QQ @机器人
31
+ (array 形态看 `{type:'at'}` 段,string 形态看 `[CQ:at,qq=…]` CQ 码)。
32
+ - **只记录**:消息只进 `state.buffer`(上限 200 条),**期间零模型调用**。
33
+ 在下一次真正唤醒时,把最近 `recentChatLimit`(默认 8)条拼成
34
+ `[QQ 未唤醒期间聊天记录 · 最近 N 条] … [记录结束]` 作为前缀附在那条唤醒消息上,然后清空缓冲。
35
+ → 既不丢上下文,也**不会**给每条闲聊都写一次会话日志(省 token + 不污染上下文)。
36
+ > ⚠️ 早期草案这里写的是 `agent.inject()`(每条都进模型上下文,但**也每条都写会话日志**);
37
+ > 实现改成了“缓冲 + 摘要前缀”,长聊时不会膨胀。**以代码为准。**
38
+ - **唤醒(followup)**:`agent.followup(msg)` —— 排队一轮普通对话(agent 忙时自动排队,不并发重叠)。
39
+ 调用时包一层 `ctx.agents.withInitiator(agent, …)`:OneBot 的 WS 回调本身**没有发起者**,
40
+ 而某些插件会调 `agents.requireInitiator()` 并在无发起者时抛错。
41
+
42
+ ## 3. 固定会话的加载(不重蹈 dsh-cron 的覆辙)
43
+
44
+ dsh-cron 的已知缺陷:`ctx.agents.roots()` 只查**当前已加载**的会话,绑定的会话没开着就
45
+ **回退到活动会话**(这就是 244 巡逻任务投到别人会话的根因)。
46
+
47
+ 本插件的做法:
48
+ ```
49
+ ensureTargetAgent():
50
+ a = ctx.agents.get(targetSessionId)
51
+ if a exists → return a
52
+ else → agentLoop.resume(...)/agents.resume(...) 显式加载该会话
53
+ if 仍未成功 → 报错 + 记日志,并【绝不】回退到别的会话
54
+ ```
55
+
56
+ ## 4. 关键接口(已核实)
57
+
58
+ | 用途 | 接口 |
59
+ |---|---|
60
+ | 构造消息 | `createUserMessage({ content:[{type:'text',text}], source:{kind:'plugin',plugin:'qq-bridge'} })`(`@deepseek-ai/dsh-llm`) |
61
+ | 唤醒一轮 | `agent.followup(message)`,包在 `ctx.agents.withInitiator(agent, …)` 里 |
62
+ | 只喂上下文不唤醒 | `agent.inject(message)`(**当前未使用**,见 §2 的说明) |
63
+ | 出站回收 | `ctx.on('session/event', (session, event) => …)`;`event.type ∈ {user/message, assistant/message, turn/end}` |
64
+ | 找/加载会话 | `ctx.agents.get(id)` / `ctx.agents.roots()` / `agents.resume({resumeSessionId, setup?})` → `AgentHandle{agent, dispose()}` |
65
+ | 设置持久化 | `settings.register('qq-bridge', Schema, {base, applies:'live'})`;`get/describe/update/mutate` |
66
+ | 设置界面 | client 半注册 `settings.section`(list,`{id, order, label}`) |
67
+ | **M2 传输** | 内置全局 `WebSocket`(Node ≥ 22,零依赖):连 NapCat 正向 WS;`access_token` 走**查询串**(WHATWG WebSocket 不支持自定义请求头) |
68
+ | **M2 凭据** | `ctx.get('credentials').resolve(ref)`,`ref` 是环境变量名;每次建连**重新解析**(不得缓存) |
69
+ | **M2 出站** | `{action:'send_private_msg', params:{user_id, message:[{type:'text',data:{text}}]}}`(群则 `send_group_msg` + `group_id`);`echo` 配对回包 |
70
+
71
+ ## 5. 设置项(settings 命名空间 `qq-bridge`)
72
+
73
+ | 键 | 类型 | 默认 | 说明 |
74
+ |---|---|---|---|
75
+ | `targetSessionId` | string | — | 固定目标会话(<TARGET_WORKSPACE> 那个) |
76
+ | `nicknames` | string[] | `['244']` | 命中即唤醒的昵称/别名 |
77
+ | `wakeProbability` | number | `0.05` | 非昵称消息的唤醒概率 |
78
+ | `whitelist` | string[] | `[]` | 私聊 openid 白名单(空=全拒,fail-closed) |
79
+ | `groupWhitelist` | string[] | `[]` | 群 openid 白名单 |
80
+ | `attachRecentChat` / `recentChatLimit` | boolean / number | `true` / `8` | 未唤醒聊天是否作为摘要附给下次唤醒、条数 |
81
+ | `replyMaxChars` | number | `1500` | 出站字数上限(下限 100);**超过就不发送** |
82
+ | `replyMode` | string | `'marker'` | 出站闸门:只发 `[QQ]…[/QQ]` 块内的内容;`'always'` = 整轮都发 |
83
+ | `onebotUrl` | string | `''` | NapCat 正向 WS 地址;**空 = 不启用传输** |
84
+ | `accessTokenEnv` | string(`role('credential-ref')`) | `''` | access_token 的凭据引用名,**不放明文** |
85
+ | `selfId` | string | `''` | 自己的 QQ 号;空 = 连上后 `get_login_info` 自动取 |
86
+ | `replyWithQuote` | boolean | `false` | 首段带 `[CQ:reply]` 引用触发消息 |
87
+ | `atOnlyInGroup` | boolean | `false` | 群里只有被 @ 才处理 |
88
+ | `stripMarkdown` | boolean | `true` | 出站去掉 Markdown 标记 |
89
+
90
+ Config(`cordis.patch.yml`)另有:`agentPreset`、`statePath`、`debugTools`、`heartbeatTimeoutMs`。
91
+
92
+ ## 6. 设置界面(client 半)
93
+
94
+ 一页 `settings.section`“QQ 桥接”:
95
+ - **昵称**:标签式增删(chips)
96
+ - **白名单**:私聊/群两组列表增删(可一键填入当前会话/最近发送者 openid)
97
+ - **唤醒概率**:0–100% 滑块(默认 5%),旁边显示“约每 N 条醒一次”
98
+ - **目标会话**:从会话列表选一个(显示工作区与标题),并展示“已加载/需 resume”
99
+ - **连接状态**:QQ 网关在线/断线、最近 20 条唤醒日志(醒 / 只记录 / 丢弃原因)
100
+
101
+ ## 7. QQ 接入方式:**B —— OneBot(NapCat,个人 QQ 号)· M2 已实现**
102
+
103
+ - ✅ **采用 B. OneBot 协议**(NapCat / go-cqhttp 等):**可用个人 QQ 号**,私聊/群都能收。
104
+ - ~~A. QQ 官方机器人(q.qq.com)~~:**已否决**(只能用官方 Bot 身份,不能当个人号)。
105
+ - **实现形态**:插件是 **WS 客户端**,主动连 NapCat 的“正向 WebSocket”。
106
+ - 用 Node 内置全局 `WebSocket`(Node ≥ 22),**不依赖 `ws`**——插件目录只挂了
107
+ `@deepseek-ai` 一个 junction,`ws` 从该目录解析不到(`ERR_MODULE_NOT_FOUND`)。
108
+ - 内置 WebSocket 是 WHATWG 接口、**不支持自定义请求头**,所以 `access_token` 走
109
+ **查询串** `?access_token=…`(OneBot 服务端支持这种写法)。
110
+ - 连接管理:指数退避重连(1s→30s,带抖动)+ 心跳看门狗(`heartbeatTimeoutMs` 无流量即重连)。
111
+ - 防回环:忽略 `post_type:'message_sent'` 与 `user_id === self_id`。
112
+ - 入站映射**同时兼容** `message` 为 **array** 与 **string** 两种形态
113
+ (array 看 `{type:'at'}` 段判 @;string 则解析 `[CQ:at,qq=…]`)。
114
+ - 现场线索(已在真机核实,见 `HANDOVER.md` §12):正向 WebSocket 端口 **3001**、WebUI 端口 **6099**;
115
+ 消息格式为 **`array`**(非 `string`);**`reportSelfMessage: false` 保持 false**(防回复死循环)。
116
+ - 现场线索(未再核实):NapCat 端口 **7998** 是否被旧 MaiBot 占用;群号 **<GROUP_ID>**。
117
+ - 出站:`send_private_msg` / `send_group_msg`,按 `replyMaxChars` 判定,超长一条都不发,
118
+ 目的地在这一轮里钉住唤醒时的群或私聊;没有钉时才回落到 `state.lastDestination`(最后一次入站来源)。
119
+
120
+ ### 7.1 NapCat 自助托管(M5,“一键包”体验但不真打包)
121
+
122
+ 思路:**不把 NapCat 打进仓库**,而是在需要时由插件从官方 Releases 现取并托管。
123
+
124
+ ```
125
+ qq_bridge_napcat
126
+ status → QQ 是否装了 / NapCat 在不在 / 代理配没配 / 端口对不对
127
+ download → 取发行包 → sha256 校验 → 解包到 $DSH_HOME/napcat
128
+ configure → 写 config/onebot11.json(正向 WS 端口 + token),免点 WebUI
129
+ launch → ctx.subprocess 拉起 NapCatWinBootMain.exe
130
+ stop → 结束该进程
131
+ ```
132
+
133
+ - **安全模型**:所有动作都由**显式工具调用**触发;插件加载时不下载、不执行任何内容。
134
+ - **下载实现**:优先 `ctx.subprocess` + 系统自带 `curl.exe`(能带 `-x` 代理;本机直连 GitHub 不通),
135
+ 无 subprocess 时退回内置 `fetch`。
136
+ - **校验**:按 GitHub Release 的 `asset.digest` 做 sha256,不符即删文件报错。
137
+ - **解包**:纯 Node(`node:zlib` 的 `inflateRawSync`),不依赖 7z / Expand-Archive;
138
+ 校验 CRC32,并拒绝 `..` / 绝对路径(zip-slip)。
139
+ - **配置落点不确定**:NapCat 的 `config/` 取决于安装方式,所以 `status` 会列出候选目录
140
+ (OneKey 根目录下的 `config/`、QQ 安装目录里的 NapCat 数据目录),`configure` 也可显式指定。
141
+ - **做不到的(已在文档与工具输出里明确写出)**:
142
+ 1. `NapCatInstaller.exe` 是交互式的,而 `SubprocessStdio.stdin` 没有 `inherit` → 必须用户手动跑;
143
+ 2. QQ 首次登录必须扫码/密码 → 无法自动化;
144
+ 3. 启动的进程由 DSH subprocess 服务托管,插件卸载 / DSH 退出时一并结束。
145
+
146
+ ## 8. 里程碑(含当前状态)
147
+
148
+ - **M1** ✅ **已完成并实测**:host 半 —— 固定会话 `ensureTargetAgent`(显式 resume)+
149
+ `followup` + 唤醒策略引擎 + 出站采集 + 调试工具;曾在运行中的 host 内验证三条策略分支。
150
+ - **M2** ✅ **已完成,并已在真实 NapCat + 真实 QQ 上端到端跑通**(收/发/三种策略分支全部实测)。
151
+ 传输层 `onebot.js`;离线测试 `test-onebot.mjs`;真机实录见 `HANDOVER.md` §12。
152
+ - **M3** ✅ **已完成并真机验证(读 / 写 / 生效全通)**:设置界面 —— `设置 → 插件 → 插件配置`
153
+ 里的“QQ 桥接”卡片。
154
+ 关键发现:那一页**按 settings 命名空间派发卡片**,只有宿主半 = 交集为空 = 界面上什么都不出现,
155
+ 所以补了浏览器半 `client/client.js`(手写 `__ModuleLoader__` bundle,不依赖构建步骤)
156
+ 与 `package.json` 的 `dsh.client`。详见 `HANDOVER.md` §15。
157
+ - **M4** ✅ **已完成**:打成 DSH bundle,`dsh plugin --profile web add <path>` 安装、重启、实测通过。
158
+ (当前插件已被用户从 profile 卸载,源码与文档保留在本目录,可随时重装。)
159
+ - **M5** ✅ **已完成(离线验证)**:NapCat 自助托管 —— 下载 / sha256 校验 / 纯 Node 解包 /
160
+ 写 OneBot 配置 / 启停,见 §7.1。
161
+ ✅ 真实数据验证过:下载 OneKey 包,sha256 与 Release digest 一致,纯 Node 解包出全部 9 个文件。
162
+ ⚠️ **未在真实 NapCat 上跑通全流程**(安装器与 QQ 登录是交互式的,需要人在场)。
163
+ - **M6** ✅ **已完成**:真机联调补齐 —— 出站标记闸门(`replyMode`,只发 `[QQ]…[/QQ]`)、上下文上限、
164
+ 非文本消息段占位符、图片缓存 + 多模态描述、会话跟随、用法注入系统提示。
165
+ 见 `HANDOVER.md` §12.4–12.6 与 §13。
166
+ - **M7** ✅ **已完成**:安全边界 —— 用法说明重写为分节契约(含**无人值守不碰特权操作**);
167
+ **明确不做审批桥接**。见 §9 与 `HANDOVER.md` §14。
168
+ - **M8** ✅ **已完成**:分得清“在 QQ 还是 DSH 里” —— 出站闸门按来源生效、出站方式做成开关
169
+ (`replyMode` 含 `'tool'`)、改模式立刻重挂提示词。见 `HANDOVER.md` §17、§17.5。
170
+ - **M9** 🚧 **进行中**:戳一戳 —— 入站已通(戳我必唤醒),出站受 NapCat PacketBackend 限制。
171
+ 见 `HANDOVER.md` §13.2b。
172
+
173
+ ## 9. 安全与注意
174
+
175
+ - 白名单 **fail-closed**:默认谁都不能唤醒(QQ 那头能驱动 <TARGET_WORKSPACE> 工作区的工具)。
176
+ - **`accessTokenEnv` 只存引用名**:token 走 `ctx.credentials`,不落进 `cordis.patch.yml`、
177
+ 不进日志(日志里的地址会脱敏 `access_token=***`)、不进状态文件。
178
+ - **`onebotUrl` 默认为空 = 传输完全不启用**,不会在没配置时就去连。
179
+ - 目标会话同时在 Web 与 QQ 被驱动时**共享同一上下文**、且一次只能跑一轮(消息排队)。
180
+ - 工具审批:**决定不桥接 `approval/request` 到 QQ**(2026-09-30)。无人值守时没有应答者 →
181
+ 危险操作**直接失败**(fail-closed),不会静默执行;同时注入的系统提示禁止无人值守时主动做特权操作,
182
+ 并把 QQ 消息当**不可信输入**(群友可注入),需要动手就停下等人。理由见 `HANDOVER.md` §14。
183
+ - 形态:**host 平面 profile bundle**(像 dsh-cron 一样 `dsh plugin add` 安装)+ **一个浏览器半**
184
+ (`dsh.client`,只为了那张设置卡片),不是 agent preset。
package/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sj244
4
+ Fork copyright (c) 2026 yunfengnobug
5
+
6
+ This fork is based on sj244/dsh-qq-bridge (https://github.com/sj244/dsh-qq-bridge),
7
+ which remains under the MIT license above. The copyright notice and this
8
+ permission notice must stay with the software.
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.