@zxh126/pi-qqbot 2.4.0 → 2.6.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/CHANGELOG.md +81 -2
- package/LICENSE +21 -21
- package/README.md +25 -8
- package/approver.ts +2 -2
- package/billing.ts +579 -579
- package/constants.ts +2 -0
- package/docs/README.zh-CN.md +22 -8
- package/docs/accounts.md +88 -88
- package/index.ts +148 -23
- package/lock.ts +237 -237
- package/logger.ts +58 -58
- package/package.json +2 -2
- package/progress.ts +176 -176
- package/qq/api.ts +17 -0
- package/qq/auth.ts +35 -35
- package/qq/gateway.ts +20 -3
- package/qq/keyboard.ts +141 -0
- package/queue.ts +120 -120
- package/redact.ts +136 -136
- package/selfupdate.ts +212 -212
- package/slash.ts +63 -2
- package/stream-buffer.ts +22 -22
- package/util/chunk.ts +107 -42
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,87 @@
|
|
|
3
3
|
本文件记录 pi-qqbot 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
4
4
|
版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
|
|
5
5
|
|
|
6
|
+
## [2.6.0] - 2026-11-01
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **审批与单选列表支持内联按钮**:审批卡片附带「✅ 允许一次 / ❌ 拒绝 / 🚀 以后都同意」按钮
|
|
11
|
+
(强制审批工具只有前两个);/tree、/fork、/resume 与模型选择列表附带选项按钮(2 列、最多 5 行,
|
|
12
|
+
label 超 10 字符截断,正文仍保留完整列表)。按钮只在 markdown 回复(`markdownReply=true`)时附带。
|
|
13
|
+
- **`INTERACTION_CREATE` 事件链路**:网关新增 `1<<26` 意图订阅与互动事件分发;`QQApi.acknowledgeInteraction`
|
|
14
|
+
在 5 秒窗口内 `PUT /interactions/{id}` 回应(命中 ACK 0,过期/越权/越界 ACK 3 重复操作)。
|
|
15
|
+
- **`qq/keyboard.ts`**:键盘构建与 `button_data` 编解码(顶层 `t` 字段路由 approval / pick)。
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `QQApi.sendC2C` 的 `SendOptions.keyboard` 只随 markdown 消息发送;markdown 失败回退纯文本时自动丢弃键盘。
|
|
20
|
+
- `replyTo` / `bridgeReply` 增加可选 `keyboard` 透传;选择按钮点击复用「回复编号」的 `/qq pick` 派发路径,
|
|
21
|
+
审批按钮点击复用 `Approver.confirm` / `allowAll`,文本回复(同意/拒绝 N、编号)全部保留为兜底。
|
|
22
|
+
|
|
23
|
+
### Compatibility
|
|
24
|
+
|
|
25
|
+
- `markdownReply=false` 时行为与之前完全一致(键盘不附带、纯文本回复)。
|
|
26
|
+
- 自定义按钮需 QQ 开放平台「内邀开通」;未开通时按钮不渲染,编号/文本回复不受影响。
|
|
27
|
+
|
|
28
|
+
## [2.5.0] - 2026-11-01
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`markdownReply` 配置生效**:置 `true` 后回复统一走 QQ markdown 消息(`msg_type=2`),GFM 表格、
|
|
33
|
+
代码块等由 QQ 客户端渲染。此前该配置只被解析、从未被消费。发送失败(如无 markdown 权限)由
|
|
34
|
+
`qq/api.ts` 自动回退纯文本,行为安全。
|
|
35
|
+
- **`util/chunk.ts` 切分器防拆表格**:`splitText` 把 GFM 表格整块保留(表格行先入缓冲、遇到非表格行
|
|
36
|
+
再整块提交),代码块边界不作切分点;长回复不再出现被拦腰截断的破表。
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- 回复格式由 `state.cfg.markdownReply` 决定:`replyTo` 的 `format` 默认值改为 `auto`,最终答案、
|
|
41
|
+
错误通知、审批提示与各回执统一生效;仍可显式传 `"text"` 强制纯文本。
|
|
42
|
+
|
|
43
|
+
### Compatibility
|
|
44
|
+
|
|
45
|
+
- `markdownReply` 默认仍为 `false`,未开启时行为与之前完全一致。
|
|
46
|
+
|
|
47
|
+
## [2.4.1] - 2026-11-01
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`!` / `!!` 在 QQ 侧明确回执,不再静默丢给模型**:这两个前缀(含全角 `!` / `!!`)只在 pi 主机
|
|
52
|
+
TUI / RPC 前端被当成 shell 命令(`interactive-mode` 的 `text.startsWith("!")`、RPC 的 `bash` 控制命令),
|
|
53
|
+
扩展 API 没有等价能力(`pi.sendUserMessage()` 不分派它们,`pi.exec()` 不过审批、不写会话历史)。现在
|
|
54
|
+
`routeSlash()` 的第一步就拦下来,回一条说明:为什么不能执行,以及该怎么做(用中文说需求,走 `bash`
|
|
55
|
+
工具并在执行前审批)。既不执行,也不当聊天送给模型。
|
|
56
|
+
- **`/help` 增补一行**:说明 `!` / `!!` 不支持。
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **判定刻意收紧**:只有「前缀 + 非空命令」才算 bang 命令(单独 `!`、`!!`、`!!!`、`/xxx`、句末感叹号
|
|
61
|
+
`你好!` 都照旧走普通对话),避免把情绪化感叹号吞掉。
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **`/qq reload` 不再报 `This extension ctx is stale after session replacement or reload`**(`/new`、
|
|
66
|
+
`/clone`、`/resume`、`/fork`、`/tree` 的编号选择同理):`runBridge()` 之后处理器还要 `syncModelFromCtx()`
|
|
67
|
+
读一次 `ctx.model`,而 `reload()` / `newSession()` / `fork()` / `switchSession()` / `navigateTree()`
|
|
68
|
+
一返回,pi 就把旧 `ExtensionCommandContext` 的**每个 guard getter 都变成抛错**(`runner.js` 的
|
|
69
|
+
`createCommandContext` 刻意保留 lazy descriptors)。现在:新增 `REPLACES_RUNTIME`
|
|
70
|
+
(`reload` / `update` / `new` / `quit` / `clone`),换过运行时就直接跳过 ctx 收尾;`syncModelFromCtx()`
|
|
71
|
+
自身也容错(`pick` 选 tree/fork/resume 这类运行时才知道的分支靠它兜底);桥接抛错后也不再继续碰 ctx。
|
|
72
|
+
回归用例 `test/bridge-stale-ctx.test.mjs` 用「替换后任何 getter 都抛 stale」的假 ctx 复现原报错栈
|
|
73
|
+
(`syncModelFromCtx` ← `handler`):修前 3 例失败、修后 5 例全绿。
|
|
74
|
+
|
|
75
|
+
### Compatibility
|
|
76
|
+
|
|
77
|
+
- 纯新增分支:斜杠命令路由、审批、队列、进度、凭据接手都不受影响。
|
|
78
|
+
- 行为变化仅一处:`!xxx` / `!!xxx` 以前是「当作普通正文送给模型」,现在是「回执不执行」。如果之前靠发
|
|
79
|
+
`!xxx` 让模型自己去调 `bash`,请改成正常描述需求。
|
|
80
|
+
- 带附件的消息仍绕过所有指令路由(含 bang 判定),与原来一致。
|
|
81
|
+
- `/reload`、`/update`、`/new`、`/clone` 的**功能**还是原来的(会话确实被替换/重载),只是不再多报一条错误。
|
|
82
|
+
- 测试:新增 `test/bang-notice.test.mjs`(6 例)、`test/bridge-stale-ctx.test.mjs`(5 例),
|
|
83
|
+
`npm test` 19 个用例文件全绿。
|
|
84
|
+
- 升级无痛:配置格式、命令名、审批与队列语义均未变;`!` / `!!` 之前就是「不执行」,只是从「静默丢给模型」
|
|
85
|
+
改成「明确回执」。
|
|
86
|
+
|
|
6
87
|
## [2.4.0] - 2026-11-01
|
|
7
88
|
|
|
8
89
|
### Added
|
|
@@ -96,8 +177,6 @@
|
|
|
96
177
|
`source=extension` 的 input 不算接管。
|
|
97
178
|
- `test/remote-notice.test.mjs` 加 `qq-local-handoff` 的 tag 与文案断言。
|
|
98
179
|
|
|
99
|
-
## [Unreleased]
|
|
100
|
-
|
|
101
180
|
## [2.2.1] - 2026-02-11
|
|
102
181
|
|
|
103
182
|
### Fixed
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 pi-qqbot contributors
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pi-qqbot contributors
|
|
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
|
@@ -12,9 +12,9 @@ Bridge the **[Pi](https://pi.dev) coding agent** into QQ private (C2C) chats: se
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| **Chat-driven agent** | QQ private messages are fed into the current pi session; the assistant's answer is sent back to QQ (long replies are split automatically). |
|
|
14
14
|
| **Message queue** | While a task is running, further QQ messages are **queued** instead of rejected; each one starts automatically as soon as the current run settles. `/cancel` is the exception — it acts immediately and clears the queue. |
|
|
15
|
-
| **Remote approval** | Before pi runs `bash` / `powershell` / `write` / `edit` — or any tool annotated `destructiveHint` — the bot asks in QQ first. |
|
|
15
|
+
| **Remote approval** | Before pi runs `bash` / `powershell` / `write` / `edit` — or any tool annotated `destructiveHint` — the bot asks in QQ first. With `markdownReply: true` the card carries inline buttons; text replies work either way. |
|
|
16
16
|
| **Remote-context notice** | The first QQ message of a session is really handed to pi (loading the extension or chatting locally in the TUI adds nothing), then the bot tells it the conversation comes from a remote QQ user and that the machine may be locked: desktop screenshots / screen OCR / UI automation / headed browsers are off by default, images must be rendered off-screen and sent with `qq_send_file`. Re-injected after context compaction. |
|
|
17
|
-
| **Session-scoped "always allow"** |
|
|
17
|
+
| **Session-scoped "always allow"** | Tap 🚀 以后都同意 on the approval card (or reply 「以后都同意」) and every approval request in the **current session** is auto-approved, including the one already waiting. |
|
|
18
18
|
| **Single-instance lock** | Only one pi process hosts the QQ gateway; the others stand by and take over automatically when the host exits. |
|
|
19
19
|
| **Cancel a running task** | Send `/cancel` (or 「取消」/「停止」) and the current run is aborted immediately — the QQ reply, the pending tool calls, the half-written answer and the pending queue are all cleaned up. |
|
|
20
20
|
| **Slash-command routing** | QQ messages starting with `/` are routed: pi extension commands, prompt templates and `/skill:name` are dispatched to pi; a set of pi built-ins (`/reload`, `/new`, `/compact`, `/session`, `/name`, `/model`, `/thinking`, `/quit`) is taken over; host-only commands are answered instead of being fed to the model. |
|
|
@@ -27,6 +27,7 @@ Bridge the **[Pi](https://pi.dev) coding agent** into QQ private (C2C) chats: se
|
|
|
27
27
|
| **Images & files** | Anything you attach in QQ is downloaded to a temp file and handed to pi as a marked line — `图片路径 <path>` for images, `语音路径` / `视频路径` / `文档路径` / `压缩包路径` / `文件路径` for the rest — and QQ gets a receipt with the counts and paths. pi's `read` tool can open the image, `bash` can process the file. |
|
|
28
28
|
| **Send files to your QQ** | Ask the agent to send a file from the machine (`把 D:\docs\report.pdf 发给我`) or run `/sendfile <path>`; the bot uploads it in 5 MB chunks (QQ rich media) and sends it as an image / video / voice / file card. Images (png/jpg), videos (mp4) and voice (silk) render natively; everything else arrives as a downloadable file card. **Always approved in QQ first.** |
|
|
29
29
|
| **Built-in commands** | `/status`, `/queue`, `/progress` (+ `/progress off` / `on`), `/reset`, `/cancel`, `/sendfile`, `/update`, `/help`. |
|
|
30
|
+
| **`!` / `!!` are answered, not executed** | pi's shell shortcuts only exist inside its TUI / RPC front end, and the extension API exposes no equivalent — so the bot replies with an explanation instead of forwarding `!ls` to the model. Ask in plain language and the agent runs it through the `bash` tool, with a QQ approval first. |
|
|
30
31
|
|
|
31
32
|
## Install
|
|
32
33
|
|
|
@@ -89,7 +90,7 @@ Then create `~/.pi/agent/pi-qqbot.json`:
|
|
|
89
90
|
| `appId` / `appSecret` | required | Credentials of your QQ Open Platform bot |
|
|
90
91
|
| `sandbox` | `false` | Set `true` while the bot is not yet published (sandbox environment) |
|
|
91
92
|
| `allowlist` | `[]` | openid allowlist; empty means no restriction |
|
|
92
|
-
| `markdownReply` | `false` | QQ
|
|
93
|
+
| `markdownReply` | `false` | Reply with QQ markdown messages (msg_type=2) so GFM tables render; approval and selection lists then carry inline buttons (custom keyboards need platform enablement). Needs the bot's markdown permission; falls back to plain text on send failure |
|
|
93
94
|
| `streaming` | `false` | Reserved, streaming is not enabled in v1 |
|
|
94
95
|
| `approve.tools` | `["bash","powershell","write","edit"]` | Tools that require approval; tools annotated `destructiveHint` are always gated too |
|
|
95
96
|
| `approve.remember` | `true` | Remember the decision per tool inside the session (no more prompts for that tool) |
|
|
@@ -254,10 +255,10 @@ Any message that starts with `/` is routed before it can become a chat prompt:
|
|
|
254
255
|
| `/name [name]` | get or set the session name | `commands.allowSession` |
|
|
255
256
|
| `/model [provider/model]` | list available models or switch | `commands.allowSession` |
|
|
256
257
|
| `/thinking [level]` | get or set the thinking level | `commands.allowSession` |
|
|
257
|
-
| `/tree` | list branch points (numbered), answer with a number to jump there | `commands.allowSession` |
|
|
258
|
-
| `/fork` | list earlier user messages (numbered), answer with a number to fork from it | `commands.allowSession` |
|
|
258
|
+
| `/tree` | list branch points (numbered), answer with a number or tap the button to jump there | `commands.allowSession` |
|
|
259
|
+
| `/fork` | list earlier user messages (numbered), answer with a number or tap the button to fork from it | `commands.allowSession` |
|
|
259
260
|
| `/clone` | duplicate the current session at its current leaf | `commands.allowSession` |
|
|
260
|
-
| `/resume` | list the project's other sessions (numbered), answer with a number to switch | `commands.allowSession` |
|
|
261
|
+
| `/resume` | list the project's other sessions (numbered), answer with a number or tap the button to switch | `commands.allowSession` |
|
|
261
262
|
| `/quit` | shuts the host pi down | `commands.allowQuit` (default **false**) |
|
|
262
263
|
| `/update` | updates pi-qqbot from npm (`pi update`) and reloads itself — see [Self-update](#self-update-from-qq) | `commands.allowUpdate` (default **false**) |
|
|
263
264
|
|
|
@@ -265,6 +266,14 @@ Host-only (answered, never forwarded to the model): `/settings`, `/hotkeys`, `/c
|
|
|
265
266
|
`/trust`, `/login`, `/logout`, `/scoped-models`, `/llama`, `/import`, `/export`, `/share`, `/bug`,
|
|
266
267
|
`/copy`, `/privacy`.
|
|
267
268
|
|
|
269
|
+
Messages that start with `!` or `!!` (full-width `!` / `!!` included) are **not** shell commands here.
|
|
270
|
+
Those prefixes are expanded only by pi's TUI / RPC front end (`!` runs the command and lets the model see
|
|
271
|
+
the output, `!!` keeps it out of the context), and the extension API has no equivalent — `pi.sendUserMessage()`
|
|
272
|
+
does not dispatch them, and `pi.exec()` bypasses approval and session history. So `!ls` gets a receipt saying it
|
|
273
|
+
was **not executed**; it is never forwarded to the model as prose either. To run something, just ask in words
|
|
274
|
+
— the agent then calls the `bash` tool and asks in QQ before it runs. A lone `!`, `!!!`, or an exclamation
|
|
275
|
+
mark at the end of a sentence is still ordinary chat.
|
|
276
|
+
|
|
268
277
|
### TUI-only commands come back to QQ
|
|
269
278
|
|
|
270
279
|
Some extension commands never print into the conversation — they call `ctx.ui.notify()` (and
|
|
@@ -288,7 +297,7 @@ Guards:
|
|
|
288
297
|
- Prompt templates and `/skill:name` start an agent run, so while a task is in progress they are
|
|
289
298
|
**queued** like ordinary messages (see [Queueing while busy](#queueing-while-busy)). Extension commands
|
|
290
299
|
that manage their own LLM interaction still run immediately.
|
|
291
|
-
- Selection commands (`/tree`, `/fork`, `/resume`) answer with a numbered list of at most 10 entries, scoped to
|
|
300
|
+
- Selection commands (`/tree`, `/fork`, `/resume`) answer with a numbered list of at most 10 entries (with tap-to-select buttons when `markdownReply` is on), scoped to
|
|
292
301
|
the user who asked and valid for 10 minutes. The next bare number from that user executes the choice; anything
|
|
293
302
|
else (or 「取消」) abandons it. A number without a pending list is ordinary chat.
|
|
294
303
|
- `/reload` is off by default: it restarts the bot, and if the new code fails to load the bot stays
|
|
@@ -298,6 +307,10 @@ Guards:
|
|
|
298
307
|
APIs. So this extension registers `/qq` and forwards `/reload` → `/qq reload`. Built-in names are
|
|
299
308
|
hard-coded here because pi does not export its built-in list; any unknown `/word` falls back to
|
|
300
309
|
plain text.
|
|
310
|
+
- The bridged command never touches the command ctx after a session replacement: `reload` / `update` / `new` /
|
|
311
|
+
`clone` / `quit` (and the `tree` / `fork` / `resume` picks) invalidate it, and pi then throws
|
|
312
|
+
`This extension ctx is stale after session replacement or reload` on **any** property access. The model
|
|
313
|
+
cache sync is skipped in those cases instead (`REPLACES_RUNTIME` + a guarded read).
|
|
301
314
|
|
|
302
315
|
### Queueing while busy
|
|
303
316
|
|
|
@@ -468,7 +481,11 @@ This is pi's own update path, not a re-implementation: `npm view @zxh126/pi-qqbo
|
|
|
468
481
|
要本会话内全部放行,回复「以后都同意」(可用「取消自动同意」关闭)。
|
|
469
482
|
```
|
|
470
483
|
|
|
471
|
-
|
|
484
|
+
With `markdownReply: true` the card carries ✅ 允许一次 / ❌ 拒绝 / 🚀 以后都同意 buttons — tapping one
|
|
485
|
+
is the same as replying. Strict tools (`qq_send_file`) show only the first two buttons. Text replies keep
|
|
486
|
+
working either way.
|
|
487
|
+
|
|
488
|
+
Reply vocabulary (approval keywords are Chinese, with English aliases; the card buttons are the faster path when they are shown):
|
|
472
489
|
|
|
473
490
|
| Intent | Reply |
|
|
474
491
|
|---|---|
|
package/approver.ts
CHANGED
|
@@ -99,8 +99,8 @@ export class Approver {
|
|
|
99
99
|
render(seq: number, opts: { strict?: boolean } = {}): string {
|
|
100
100
|
const item = this.pending.get(seq);
|
|
101
101
|
const hint = opts.strict
|
|
102
|
-
?
|
|
103
|
-
:
|
|
102
|
+
? `点下方按钮,或回「同意 ${seq}」/「拒绝 ${seq}」,需单独确认。`
|
|
103
|
+
: `点下方按钮,或回「同意 ${seq}」/「拒绝 ${seq}」;想全部放行回「以后都同意」。`;
|
|
104
104
|
return [`[审批 #${seq}] ${item?.summary ?? ""}`, hint].join("\n");
|
|
105
105
|
}
|
|
106
106
|
|