dsh-qq-digest 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 huangpeiyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,217 @@
1
- # Temporary Holding Version
1
+ # dsh-qq-digest
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 一个**只读**的 QQ 摘要插件:读群聊,产出「重点活动 / 任务待办 / 决定结论」。
4
+ **群聊默认全开、私聊默认全关**;私聊只由你在设置页一条一条打开,不允许一键全开。
5
+
6
+ > 这是插件,不是机器人:它没有、也不会有任何发送/回复/转发能力。
7
+ > 工具清单里不存在发送类工具,代码里也不存在发送路径。
8
+
9
+ ## 三个开关(设置 → QQ 群摘要)
10
+
11
+ | 开关 | 默认 | 关掉后 |
12
+ |---|---|---|
13
+ | **群聊读取总开关** | 开 | 所有群一律不读;单群开关同时失效;定时任务空转 |
14
+ | 每个群的开关 | 全开 | 只跳过那一个群 |
15
+ | 每条私聊的开关 | **全关** | 默认就是关;勾上之后这条私聊才进入摘要流程 |
16
+
17
+ 「一键放开所有私聊」在策略层被**直接判非法**(`private.defaultEnabled=true` → 策略作废 → 全关),
18
+ 所以「私聊被读」只可能是你逐条点出来的结果。
19
+
20
+ ## 与微信版的区别(别照抄微信那套)
21
+
22
+ | | dsh-wechat-digest | dsh-qq-digest(本插件) |
23
+ |---|---|---|
24
+ | 群聊 | 默认开,可关 | 默认开,可关,另有总开关 |
25
+ | 私聊 | 默认关,可开 | 默认关,**只能逐条打开**;一键全开会被判非法 |
26
+ | 句柄密钥之外的标识 | 原始 chat id 止于闸门 | 同,且**规范 id 也不外传**(水位线改用句柄做键) |
27
+
28
+ ## 工具(四个,全部只读)
29
+
30
+ | 工具 | 作用 |
31
+ |---|---|
32
+ | `qq_digest_list_rooms` | 列出策略允许的群(只给不透明句柄 + 群名,不给群号) |
33
+ | `qq_digest_run_summary` | 立即跑一次增量总结。**没有入参**——触发不携带范围 |
34
+ | `qq_digest_get_digest` | 按句柄读摘要;不传句柄返回每个群最新一篇 |
35
+ | `qq_digest_status` | 数据源、策略是否降级、可用群数、上次运行结果 |
36
+
37
+ ## 数据源(三选一,`QQ_DIGEST_SOURCE`)
38
+
39
+ | 源 | 默认 | 说明 |
40
+ |---|---|---|
41
+ | `bridge` | ✅ | 读本机 `qq-bridge` 的只读 HTTP 接口(`127.0.0.1:3100`)。**只覆盖机器人见过的会话** |
42
+ | `snowluma` | | 直连 SnowLuma OneBot HTTP(`127.0.0.1:3000`)。能枚举机器人所在的**所有群**,但要求网关在线 |
43
+ | `fixture` | | 内置假数据,含群 + 私聊 + 频道哨兵,用于验证隔离 |
44
+
45
+ **如实说明**:`bridge` 源只能看到「有消息进过桥接」的群。想覆盖全部群,要么开 `snowluma` 源(需网关在线),
46
+ 要么让机器人在那些群里被 @ 过。这不是插件偷懒,是 QQ 侧取数的客观限制。
47
+
48
+ ## 摘要器(`config.json` 的 `summarizer`)
49
+
50
+ - `llm`(默认):OpenAI 兼容接口,结构化 JSON。密钥取 `QQ_DIGEST_LLM_API_KEY` 或 `~/.dsh/.credentials.yaml` 的 `DEEPSEEK_API_KEY`。
51
+ 失败**不静默降级**,直接抛 `LLM_*` 错误码。
52
+ - `heuristic`:本地关键词规则,零网络零成本,质量有上限(会漏也会误抓)。
53
+
54
+ ## 配置
55
+
56
+ `<DSH_HOME>/qq-digest/config.json`(默认 `<DSH_HOME>/qq-digest/config.json`)。
57
+ **每次工具调用都重读**,改完立即生效,不用重启。
58
+
59
+ ```json
60
+ {
61
+ "version": 1,
62
+ "groups": {
63
+ "group:70001111": { "enabled": true, "name": "客户对接群" }
64
+ },
65
+ "summarizer": "llm",
66
+ "schedule": { "enabled": true, "kind": "weekly", "weekdays": ["MO","TU","WE","TH","FR"], "time": "09:00", "timeZone": "Asia/Shanghai" }
67
+ }
68
+ ```
69
+
70
+ - 不写 `groups` = 所有群都可读。
71
+ - `groups` 里只用于**关掉**某个群或给它起名。
72
+ - 写出 `private` / `friend` / `send` / `scope` 一类键 → 整份配置作废 → **全关**(fail-closed)。
73
+ - 配置损坏同理:全关,不回退到「默认允许」。
74
+
75
+ ## 落盘
76
+
77
+ `<DSH_HOME>/qq-digest/digests.jsonl`(每行一篇摘要)+ `watermarks.json`(增量水位线)。
78
+
79
+ 落盘字段白名单:`roomHandle / roomTitle / digest / provenance / trigger / createdAt`。
80
+ 写入代码里**不存在**「消息原文」和「原始会话 id」这两条路——不是过滤,是结构上不可能(有测试断言)。
81
+
82
+ ## 验证
83
+
84
+ ```bash
85
+ npm run verify # core 纯净性检查 + 51 个用例
86
+ node scripts/smoke.mjs # fixture 端到端(真实子进程走 MCP 协议)
87
+ node scripts/smoke.mjs bridge # 真桥接只读冒烟
88
+ ```
89
+
90
+ core 纯净性检查锁定:`src/core/` 不许 import `node:fs` / 子进程 / 网络,不许出现 `fetch(`、`send_*`、`/api/send`、写文件符号。
91
+
92
+ ## 设置页
93
+
94
+ 插件装好后,在 **设置 → QQ 群摘要** 里:
95
+
96
+ | 区块 | 能改什么 |
97
+ |---|---|
98
+ | 群聊读取总开关 | 关掉后所有群一律不读,单群开关同时失效 |
99
+ | 参与总结的群 | 每个群一个开关 + 一个「群名」输入框。默认全开 |
100
+ | 私聊 | 每条一个开关 + 一个名字输入框,**默认全关**,逐条打开;勾上才读 |
101
+ | 摘要器 | 模型摘要 / 本地规则 |
102
+ | 定时执行 | 开关、每天或每周指定、星期几、时间、时区(默认 Asia/Shanghai) |
103
+ | 立即总结 | **顶部和底部各一个按钮,任何时候都可点**;有未保存改动会「先保存再跑」 |
104
+
105
+ 保存走宿主同一套校验:非法时间、未知字段、任何私聊 id 一律 400 拒绝,不会落一份半合法配置。
106
+
107
+ **名字不能重复**:群和私聊**合并判定**(`trim` + 忽略大小写),空名字不参与。
108
+ 面板会实时把重复的两个输入框标红并提示「重名」;真正拦下来的是宿主,
109
+ 返回 `DUPLICATE_NAME:「X」同时被群 100000 和私聊 88880001 使用`,整份配置不落盘。
110
+
111
+ ### 定时是怎么跑的
112
+
113
+ 定时器在 DSH 宿主进程里,每 30 秒看一次表:
114
+
115
+ - 到点(`HH:MM` 精确命中)+ 今天还没跑过 → 抢跨进程锁 → 跑一轮 → 记 `schedule-state.json`
116
+ - **DSH 没在运行时不会跑**,也不会事后补跑(只在启动后 5 秒内补同一天同一分钟那一次)
117
+ - 和 MCP 工具的手动触发互斥:两边用同一个 `run.lock`,同时来只会跑一轮
118
+
119
+ ### 为什么之前「看不见」
120
+
121
+ 设置页这一项曾经注册失败,原因是 `slots` 服务**晚于** `apply` 发布:
122
+ 读不到就 `return` 等于静默放弃 —— 插件显示「已安装」、接口一切正常,
123
+ 但设置里永远没有这一项,且没有任何报错。现在改成和 `dsh-instruction-memory`
124
+ 一样的做法:`ctx.slots ?? ctx.get('slots')`,读不到就每 50ms 重试,最多 20 次
125
+ (约 1 秒),仍然没有才 `console.error` 并上报。
126
+
127
+ 配套加了**客户端心跳**,让这类问题不必再猜:
128
+
129
+ | 现象 | 含义 |
130
+ |---|---|
131
+ | `factoryAt` 有值 | 客户端 bundle 真的被执行了 |
132
+ | `mountedAt` 有值 | 设置页注册成功 |
133
+ | `failedAt` + `lastError` | 加载了但注册失败,错误原文就在里面 |
134
+ | 三个都没值 | 客户端 bundle 根本没加载(是列出问题,不是注册问题) |
135
+
136
+ `GET /qq-digest-api/state` 的 `clientBoot` 字段就是这份记录。
137
+
138
+ ### 设置页背后的接口
139
+
140
+ `/qq-digest-api`(只接受回环地址):
141
+
142
+ | 方法 | 路径 | 作用 |
143
+ |---|---|---|
144
+ | GET | `/state` | 配置 + 群列表 + 上次执行情况 |
145
+ | POST | `/config` | 保存群开关/群名/摘要器/定时 |
146
+ | POST | `/run` | 立即跑一次 |
147
+ | GET | `/digest?group=<群号>` | 某个群的最新摘要 |
148
+
149
+ ## 安装进 DSH
150
+
151
+ 这一步分两块,互不冲突(row id 不同):
152
+
153
+ ```bash
154
+ # 1) MCP 行:给模型用的四个只读工具
155
+ node scripts/install.mjs --apply
156
+
157
+ # 2) bundle 行:设置页 + 定时器(宿主半 + 客户端半)
158
+ node scripts/link-profile.mjs --apply
159
+ ```
160
+
161
+ 第 2 步**不走 pnpm**:本机 profile 的 `node_modules` 里有文件被占用
162
+ (`@img/sharp-win32-x64` 会在 pnpm 重装时报 `ERR_PNPM_EPERM`),所以这里用
163
+ junction + 手改 profile `package.json`(会自动备份),和本机 `dsh-instruction-memory` 的装法一致。
164
+
165
+ 两块都要**重启 DSH** 才生效(Desktop 应用完全退出再打开)。
166
+
167
+ ## 客户端半的加载契约(踩过的坑,别再踩)
168
+
169
+ ```js
170
+ window.__ModuleLoader__.load({
171
+ id: 'dsh-qq-digest', // 必须等于包名 / loader entry 名
172
+ factory: (require) => { // require 是**参数**,不是全局
173
+ const React = require('react');
174
+ ...
175
+ return module.exports;
176
+ },
177
+ });
178
+ ```
179
+
180
+ 写成无参 IIFE `load((() => { ... })())` 会在浏览器里直接
181
+ `ReferenceError: require is not defined`,启动日志记成
182
+ `web boot: 1 entry did not activate / dsh-qq-digest: import failed`,
183
+ 而 **宿主半完全正常**(接口照常响应)—— 只表现在「设置里没有这一项」,极易误判成没装上。
184
+ `test/client.test.js` 会在无浏览器环境里真的执行这个 bundle,把这条锁住。
185
+
186
+ 主题色一律用 `--dsw-alias-*`(`--dsw-alias-border-l1` / `--dsw-alias-bg-layer-1` /
187
+ `--dsw-alias-label-secondary` / `--dsw-alias-brand-primary` / `--dsw-alias-state-*`),
188
+ 不是 `--color-*`;写错不会报错,只会失去明暗主题适配。
189
+
190
+ ## 自检
191
+
192
+ ```bash
193
+ node scripts/verify-dsh.mjs # 宿主半在线?客户端半上次启动有没有失败?落盘状态?
194
+ ```
195
+
196
+ 检查项与判定口径:
197
+
198
+ - 宿主半:API 200 + 群列表非空 + 策略未降级 + 数据源无报错
199
+ - 客户端半:最近一次 `crash-*-web-boot.log` 是否提到本插件;**如果那次 boot 早于 client.js 的修改时间,报 SKIP 而不是通过**(避免用旧日志证明新代码)
200
+ - 落盘:`config.json` 是否存在、`digests.jsonl` 有几条摘要
201
+
202
+ ## 已知限制(不美化)
203
+
204
+ - **设置页的开关不是安全边界**:`/qq-digest-api` 只允许回环地址访问,但本机任何进程
205
+ (包括模型通过 bash 起一个 `curl`)都能 POST 改配置。开关管的是「默认不读」,
206
+ 不是「别人改不了」。真要防,得给 API 加一次性的会话令牌,本期没做。
207
+ - 私聊只在 `bridge` 数据源下可用;`snowluma` 适配器只调群历史,没有私聊取数路径。
208
+ - `bridge` 源的群名是空的(桥接不存群名);可在 `config.json` 的 `groups.<id>.name` 里人工命名。
209
+ - `bridge` 源每轮最多回看 **100 条**(桥接 `/api/messages` 的硬上限)。两次运行之间新增超过 100 条时,
210
+ 更早的那些会被跳过——水位线只记得住它实际读到的最后一条。别指望用它补历史。
211
+ - `bridge` 源看不到「机器人没收到过消息」的群。要覆盖全部群,得开 `snowluma` 源(需网关在线)。
212
+ - `snowluma` 源是 `stateless`:每轮全量重算固定窗口(默认 60 条)。原因是 OneBot 的 `message_id`
213
+ 不保证单调,拿它当水位线会静默漏读新消息。
214
+ - 句子级启发式摘要对「谁负责」判断保守:猜不准就回退成发消息的人,不做过度推断。
215
+ - `config.json` 里的 `schedule` 字段目前只是**描述性配置**:插件本身不注册定时任务。
216
+ 要每天自动跑,需要另外用 DSH 的 automation 建一条定时任务去调用 `qq_digest_run_summary`。
217
+ - 未评估 QQ 侧 ToS 与账号风险;本插件只读本地桥接/网关接口,不额外登录。
@@ -0,0 +1,11 @@
1
+ # dsh-qq-digest bundle patch。
2
+ #
3
+ # 只挂**宿主半**:设置页要的 HTTP API(/qq-digest-api)和定时器都在里面。
4
+ # 客户端半不需要在这里登记 —— package.json 的 dsh.client 会被自动加载。
5
+ #
6
+ # MCP 那一行(qq-digest 的四个工具)在 profile 的 cordis.patch.yml 里由
7
+ # `node scripts/install.mjs` 维护,两者互不冲突(row id 不同)。
8
+
9
+ - insert:
10
+ - id: qq-digest
11
+ name: dsh-qq-digest
@@ -0,0 +1,13 @@
1
+ # === qq-digest MCP BEGIN ===
2
+ # 由 dsh-qq-digest 的 scripts/install.mjs 维护;整段会被替换,请勿手工编辑内部条目。
3
+ - insert:
4
+ - id: mcp-qq-digest
5
+ name: '@deepseek-ai/dsh-mcp-client'
6
+ config:
7
+ serverName: qq-digest
8
+ transport: stdio
9
+ command: 'D:\Program Files\nodejs\node.exe'
10
+ args:
11
+ - '__PACKAGE_ROOT__/src/run-mcp.mjs'
12
+ toolCallTimeoutMs: 180000
13
+ # === qq-digest MCP END ===