@zxh126/pi-qqbot 2.6.0 → 2.6.2
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.en.md +611 -0
- package/{CHANGELOG.md → CHANGELOG.zh-CN.md} +40 -0
- package/README.md +10 -1
- package/docs/README.zh-CN.md +8 -1
- package/package.json +4 -3
- package/slash.ts +65 -4
package/CHANGELOG.en.md
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pi-qqbot are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
+
and versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
> Chinese changelog: [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md)
|
|
7
|
+
|
|
8
|
+
## [2.6.2] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`/qq …` bypassed the command gates**: `/qq` is itself an extension command, so sending `/qq reload`,
|
|
13
|
+
`/qq quit` or `/qq new` landed in the passthrough branch of `routeSlash`, which checks neither
|
|
14
|
+
`commands.allowReload` / `allowQuit` / `allowUpdate` / `allowSession` nor the busy state (`needsIdle`).
|
|
15
|
+
With the defaults (`allowQuit: false`) `/qq quit` from QQ could shut the host pi down. `/qq` is now routed
|
|
16
|
+
*before* passthrough: `bridgeGate()` maps the sub-command back to its built-in name (multi-word sub-commands
|
|
17
|
+
`list tree` / `list fork` / `list resume` are matched as a whole first) and reuses the same
|
|
18
|
+
`allowed()` / `needsIdle` checks.
|
|
19
|
+
- **Dispatch receipts swallowed the argument**: `/qq reload` used to be answered with
|
|
20
|
+
`已派发 /qq 给 pi。` — `parseSlash` only takes the first word as the command name, so it looked like `reload`
|
|
21
|
+
had been ignored. Receipts and logs now echo the whole command (truncated at 80 characters with `…`).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Destructive bridge sub-commands (`reload` / `new` / `quit` / `clone`) now answer **before** they are
|
|
26
|
+
dispatched, so the receipt cannot lose the race against `ctx.reload()` / `newSession()`.
|
|
27
|
+
- `/qq sendfile …`, `/qq usage …`, `/qq pick …` and unknown sub-commands no longer get an extra generic
|
|
28
|
+
receipt (the bridge answers for itself), which removes one useless message.
|
|
29
|
+
- Cleaned up a leftover `+` line in `slash.ts` from an older hand-patched edit
|
|
30
|
+
(`+reply(formatPicker(picker))` — harmless but dirty).
|
|
31
|
+
|
|
32
|
+
### Compatibility
|
|
33
|
+
|
|
34
|
+
- `/reload`, `/new`, `/quit`, `/compact` and friends behave exactly as before; only `/qq <sub>` is now
|
|
35
|
+
held to the same gates.
|
|
36
|
+
- No config change required: deployments with `allowReload: true` keep working, and `/qq quit` is answered
|
|
37
|
+
with a refusal when `allowQuit` is false (the default).
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
- New `test/qq-bridge-gate.test.mjs` (21 assertions): sub-command mapping, three gate refusals, busy
|
|
42
|
+
refusal, receipt-before-dispatch ordering, multi-word sub-commands, receipt truncation, `/reload` regression.
|
|
43
|
+
- Full suite: 274 assertions across 22 test files pass; `tsc --noEmit --strict` is clean for the changed
|
|
44
|
+
`slash.ts`.
|
|
45
|
+
|
|
46
|
+
## [2.6.1] - 2026-11-01
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Per-language changelogs**: the Chinese changelog is renamed to `CHANGELOG.zh-CN.md` and this English
|
|
51
|
+
`CHANGELOG.en.md` is added. `README.md` links to `CHANGELOG.en.md`, `docs/README.zh-CN.md` links to
|
|
52
|
+
`CHANGELOG.zh-CN.md`.
|
|
53
|
+
|
|
54
|
+
## [2.6.0] - 2026-11-01
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **Inline buttons for approvals and single-choice lists**: approval cards carry ✅ 允许一次 / ❌ 拒绝 /
|
|
59
|
+
🚀 以后都同意 buttons (strict tools show only the first two); the `/tree`, `/fork`, `/resume` and
|
|
60
|
+
model-selection lists carry option buttons (2 columns, at most 5 rows, labels truncated at 10 characters
|
|
61
|
+
while the full list stays in the message body). Buttons are attached only to markdown replies
|
|
62
|
+
(`markdownReply=true`).
|
|
63
|
+
- **`INTERACTION_CREATE` event pipeline**: the gateway subscribes to intent `1<<26` and dispatches
|
|
64
|
+
interaction events; `QQApi.acknowledgeInteraction` replies with `PUT /interactions/{id}` inside the
|
|
65
|
+
5-second window (matched → ACK 0; expired / unauthorized / out-of-range → ACK 3, duplicate operation).
|
|
66
|
+
- **`qq/keyboard.ts`**: keyboard construction plus `button_data` encode/decode, routed by the top-level
|
|
67
|
+
`t` discriminator (`approval` / `pick`).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `QQApi.sendC2C`'s `SendOptions.keyboard` is only sent with markdown messages; when a markdown send
|
|
72
|
+
fails and falls back to plain text, the keyboard is dropped automatically.
|
|
73
|
+
- `replyTo` / `bridgeReply` gain an optional `keyboard` passthrough. Picker-button clicks reuse the
|
|
74
|
+
"reply with a number" `/qq pick` dispatch path; approval-button clicks reuse `Approver.confirm` /
|
|
75
|
+
`allowAll`. Text replies (同意/拒绝 N, numbers) all remain as fallbacks.
|
|
76
|
+
|
|
77
|
+
### Compatibility
|
|
78
|
+
|
|
79
|
+
- With `markdownReply=false` the behaviour is exactly as before (no keyboard, plain-text replies).
|
|
80
|
+
- Custom buttons require the "invited" enablement on the QQ Open Platform; if it is not granted, the
|
|
81
|
+
buttons do not render and numbered/text replies are unaffected.
|
|
82
|
+
|
|
83
|
+
## [2.5.0] - 2026-11-01
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- **`markdownReply` takes effect**: set `true` and all replies go through QQ markdown messages
|
|
88
|
+
(`msg_type=2`); GFM tables and code blocks are rendered by the QQ client. Previously this config was
|
|
89
|
+
parsed but never consumed. On send failure (e.g. no markdown permission) `qq/api.ts` automatically
|
|
90
|
+
falls back to plain text — safe by design.
|
|
91
|
+
- **`util/chunk.ts` splitter keeps tables intact**: `splitText` preserves GFM tables as whole blocks
|
|
92
|
+
(table rows are buffered and flushed as a block when a non-table line appears) and never splits at
|
|
93
|
+
code-fence boundaries; long replies no longer show tables cut in half.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- The reply format is driven by `state.cfg.markdownReply`: `replyTo`'s `format` defaults to `auto` and
|
|
98
|
+
applies to final answers, error notices, approval prompts and every receipt; an explicit `"text"`
|
|
99
|
+
still forces plain text.
|
|
100
|
+
|
|
101
|
+
### Compatibility
|
|
102
|
+
|
|
103
|
+
- `markdownReply` still defaults to `false`; behaviour is unchanged when it is disabled.
|
|
104
|
+
|
|
105
|
+
## [2.4.1] - 2026-11-01
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- **`!` / `!!` get an explicit receipt instead of being silently dropped**: these prefixes (full-width
|
|
110
|
+
`!` / `!!` included) are shell shortcuts only in pi's host TUI / RPC front ends (`interactive-mode`'s
|
|
111
|
+
`text.startsWith("!")`, the RPC `bash` control command); the extension API has no equivalent
|
|
112
|
+
(`pi.sendUserMessage()` does not dispatch them, `pi.exec()` bypasses approval and session history).
|
|
113
|
+
`routeSlash()` now intercepts them first and replies explaining why they cannot run and what to do
|
|
114
|
+
instead (describe the task in words; it then goes through the `bash` tool with a QQ approval first).
|
|
115
|
+
Neither executed nor forwarded to the model as chat.
|
|
116
|
+
- **`/help` gains a line**: `!` / `!!` are not supported.
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
|
|
120
|
+
- **Detection deliberately tightened**: a bang command requires "prefix + non-empty command" (a bare
|
|
121
|
+
`!`, `!!`, `!!!`, `/xxx`, or a sentence-final exclamation such as `你好!` stays ordinary chat), so
|
|
122
|
+
emotional exclamation marks are not swallowed.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- **`/qq reload` no longer errors with `This extension ctx is stale after session replacement or reload`**
|
|
127
|
+
(same for the numbered picks of `/new`, `/clone`, `/resume`, `/fork`, `/tree`): after `runBridge()`
|
|
128
|
+
the handler still called `syncModelFromCtx()` to read `ctx.model`, while `reload()` / `newSession()` /
|
|
129
|
+
`fork()` / `switchSession()` / `navigateTree()` make pi turn every guard getter of the old
|
|
130
|
+
`ExtensionCommandContext` into a throw (`runner.js`'s `createCommandContext` keeps lazy descriptors on
|
|
131
|
+
purpose). Now a `REPLACES_RUNTIME` set (`reload` / `update` / `new` / `quit` / `clone`) skips the ctx
|
|
132
|
+
teardown entirely when the runtime was replaced; `syncModelFromCtx()` itself is fault-tolerant (it
|
|
133
|
+
backs the picks of tree/fork/resume, whose branches only the runtime knows); and after a bridge error
|
|
134
|
+
the code stops touching ctx. Regression test `test/bridge-stale-ctx.test.mjs` reproduces the original
|
|
135
|
+
stack (`syncModelFromCtx` ← `handler`) with a fake ctx whose every getter throws: 3 cases failed
|
|
136
|
+
before the fix, all 5 pass after.
|
|
137
|
+
|
|
138
|
+
### Compatibility
|
|
139
|
+
|
|
140
|
+
- Pure additions: slash-command routing, approval, queue, progress and credential intake are unaffected.
|
|
141
|
+
- One behaviour change: `!xxx` / `!!xxx` used to be forwarded to the model as ordinary text; they are
|
|
142
|
+
now answered without execution. If you relied on sending `!xxx` to make the model call `bash` itself,
|
|
143
|
+
describe the task normally instead.
|
|
144
|
+
- Messages with attachments still bypass all command routing (including bang detection), as before.
|
|
145
|
+
- `/reload`, `/update`, `/new`, `/clone` still do what they did (the session is really replaced or
|
|
146
|
+
reloaded); they just no longer report an extra error.
|
|
147
|
+
- Tests: new `test/bang-notice.test.mjs` (6 cases) and `test/bridge-stale-ctx.test.mjs` (5 cases); all
|
|
148
|
+
19 test files green under `npm test`.
|
|
149
|
+
- Painless upgrade: config format, command names, approval and queue semantics unchanged; `!` / `!!`
|
|
150
|
+
were already "not executed" — only the silence became an explicit receipt.
|
|
151
|
+
|
|
152
|
+
## [2.4.0] - 2026-11-01
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
|
|
156
|
+
- **No more silence when the model runs out of credit or its key is invalid**: on a provider error pi
|
|
157
|
+
only leaves `stopReason: "error"` + `errorMessage` on the assistant message and produces **no body
|
|
158
|
+
text**, so QQ used to see just "no reaction". Now `message_end` classifies the error (`billing` /
|
|
159
|
+
`auth` / `permission` / `rate` / `other`) and `agent_settled` replies with the **current model**
|
|
160
|
+
(`provider/model`), the server's original message and three recovery paths. 401 / 402 / `1311 plan
|
|
161
|
+
not open` all take the "recoverable" branch; errors with body text, user-cancelled runs and 500-class
|
|
162
|
+
errors behave as before.
|
|
163
|
+
- **Remote key replacement**: after running out of credit, sending a bare key (`sk-…` / `ark-…` / `AIza…`
|
|
164
|
+
/ hex / base64 / local-service placeholders like `ollama`, quoted or backticked also accepted) writes
|
|
165
|
+
it into the failing provider in `~/.pi/agent/auth.json` and really probes it once with
|
|
166
|
+
`GET <baseUrl>/models`, replying with a masked fragment and the probe result. **No `sk-` prefix is
|
|
167
|
+
assumed**: the only criteria are "non-empty + printable ASCII + no whitespace"; a bare string (the
|
|
168
|
+
whole message is the key) additionally requires length ≥ 12 with digits / uppercase / separators, so
|
|
169
|
+
ordinary text like `configuration.ts` is not swallowed.
|
|
170
|
+
- **Onboarding a new endpoint from QQ**: send `https://host/v1 <key>` or `endpoint <url> <key> models a,b`
|
|
171
|
+
→ probes `/models` (falls back to `/v1` without a version suffix; 401/403 immediately marks the key
|
|
172
|
+
invalid) → filters out non-chat ids (`embedding` / `rerank` / speech / image generation) → writes
|
|
173
|
+
`models.json` + `pi.registerProvider()` (usable in the running process immediately) → sends a
|
|
174
|
+
**numbered list with context sizes**; replying with a number calls `pi.setModel()` (reuses the
|
|
175
|
+
existing picker, valid 10 minutes). The provider id is auto-derived as `qq-<host>`, numbered on
|
|
176
|
+
collision.
|
|
177
|
+
- **Model parameters align with the built-in catalog**: `/models` usually returns only ids, so
|
|
178
|
+
`borrowModelMeta()` borrows `contextWindow` / `maxTokens` / `reasoning` / `input` / `thinkingLevelMap` /
|
|
179
|
+
`cost` / `compat` / `inputLimits` from `modelRegistry.getAll()` (built-in catalog + configured
|
|
180
|
+
providers) by model id, ordered "same host > same-vendor domain family > `openai-completions`".
|
|
181
|
+
Conservative defaults (128k / 16k) apply only when nothing can be borrowed, and the receipt says how
|
|
182
|
+
many were filled in and how many are defaults.
|
|
183
|
+
- **Self-update from QQ**: `/update` (or 「更新自己」「升级一下插件」「自更新」) → `npm view` for the
|
|
184
|
+
latest version → **`pi update npm:@zxh126/pi-qqbot`** → **version-verification gate** (the on-disk
|
|
185
|
+
`package.json` must equal the registry version, otherwise it only reports and does not reload) →
|
|
186
|
+
`/qq reload` (`ctx.reload()`). Natural-language triggers are deliberately narrow — a bare 「更新」
|
|
187
|
+
is not recognised.
|
|
188
|
+
|
|
189
|
+
### Changed
|
|
190
|
+
|
|
191
|
+
- **Config `commands.allowUpdate`** (default `false`, in the style of `allowReload` / `allowQuit`):
|
|
192
|
+
gates `/update` and 「更新自己」; a top-level default plus per-bot override.
|
|
193
|
+
- **`PickerKind` gains `model`**: numbered selection supports "models of a newly onboarded service";
|
|
194
|
+
picking one calls `pi.setModel()` directly; when the model is no longer in the catalog it says so
|
|
195
|
+
instead of silently proceeding, prompting a fresh `/model`.
|
|
196
|
+
- **Error notices and logs**: `errorMessage` is uniformly run through `redactSecrets()` + whitespace
|
|
197
|
+
collapse + 600-char clip; the error class and current model are written to `pi-qqbot.log`
|
|
198
|
+
(`[reply] 本轮模型接口报错: <class> <provider/model> <original>`), inspectable afterwards.
|
|
199
|
+
|
|
200
|
+
### Compatibility
|
|
201
|
+
|
|
202
|
+
- Pure additions: without `commands.allowUpdate` nothing changes; outside the billing/auth-failure state
|
|
203
|
+
a bare key is not treated as a credential (the explicit `key …` form excepted).
|
|
204
|
+
- Requires pi to provide `message_end`'s `stopReason` / `errorMessage`, `pi.registerProvider()` and
|
|
205
|
+
`pi.setModel()` (all present in pi 1.4.x).
|
|
206
|
+
- Disk writes touch only pi's own `auth.json` / `models.json`, using pi's own `proper-lockfile` +
|
|
207
|
+
atomic rename; `models.json` only adds or edits its own provider, never touching others.
|
|
208
|
+
- The install directory is shared: `/update` only updates the disk; other pi processes still running
|
|
209
|
+
old code must reload or restart on their own.
|
|
210
|
+
|
|
211
|
+
### Tests
|
|
212
|
+
|
|
213
|
+
- **14 suites / 191 cases** all green (`npm test`).
|
|
214
|
+
- New `test/billing.test.mjs` (26): error classification, Chinese copy, `parseCredentialMessage` forms,
|
|
215
|
+
`looksLikeSecret` (including non-`sk-` prefixes, explicit/bare tightness), `stripWrapping`,
|
|
216
|
+
`normalizeBaseUrl` / `extractModelIds`, `probeEndpoint` (success / `/v1` fallback / immediate 401 /
|
|
217
|
+
network error), `providerIdForEndpoint`, `borrowModelMeta`, `filterChatModelIds`, `modelLabel`,
|
|
218
|
+
`updateProviderKey` / `upsertEndpointProvider` persistence.
|
|
219
|
+
- New `test/credential-intake.test.mjs` (11): drives the real `handleMessage` — bare key while broke,
|
|
220
|
+
no swallow when not broke, explicit prefix, endpoint registration + numbered list, metadata
|
|
221
|
+
borrowing, probe 401, resend with `models`, numbered pick `setModel`, non-`sk` key, quoted key.
|
|
222
|
+
- New `test/selfupdate.test.mjs` (13): version comparison (incl. prerelease), npm output parsing, five
|
|
223
|
+
flow outcomes (latest / updated / check failure / update failure / version mismatch),
|
|
224
|
+
`isUpdateSelfIntent`, `/update` gating.
|
|
225
|
+
- New `test/error-notice.test.mjs` (8): 402 / 401 / 500 / with body / cancelled / cleared after a good
|
|
226
|
+
run / credential gate / redaction.
|
|
227
|
+
|
|
228
|
+
## [2.3.0] - 2026-10-31
|
|
229
|
+
|
|
230
|
+
### Added
|
|
231
|
+
|
|
232
|
+
- **Remote-context notice**: in `before_agent_start` it first checks "was this turn dispatched by this
|
|
233
|
+
extension as a QQ message", and only then writes a notice into the system prompt's
|
|
234
|
+
`qq-remote-context` section (before the user message, invisible on both QQ and the TUI): the
|
|
235
|
+
conversation comes from a remote QQ user who cannot see the local screen or dialogs; desktop
|
|
236
|
+
screenshots / screen OCR / UI automation / keyboard-mouse simulation / headed browsers are **off by
|
|
237
|
+
default** (`mcp__screenshot`, `mcp__winui` named explicitly) — render images off-screen to PNG and
|
|
238
|
+
send them back with `qq_send_file`; capturing the desktop once is allowed only to check whether the
|
|
239
|
+
machine is locked. Merely loading the extension or chatting in the local TUI injects nothing — it
|
|
240
|
+
must wait for the first QQ message to actually start a run.
|
|
241
|
+
- **System-prompt rules**: every run **from QQ** appends three short rules (remote user / locked screen
|
|
242
|
+
→ off-screen rendering / say so plainly when human help is needed) so they survive compaction of a
|
|
243
|
+
long session; no stacking within one turn, and local TUI runs get nothing.
|
|
244
|
+
- **Re-injection after compaction**: `session_compact` clears the "noticed" flag so the next message
|
|
245
|
+
injects the notice again.
|
|
246
|
+
- **Config `remote`**: `notice` (default `true`, once per session), `guidelines` (default `true`),
|
|
247
|
+
`extra` (appended to the notice, empty by default); a top-level default plus per-bot override.
|
|
248
|
+
- **Local hand-off correction**: when, within one session, the previous turn came from QQ and the next
|
|
249
|
+
is typed directly in the local TUI, that local turn injects a one-time `qq-local-handoff` section
|
|
250
|
+
("remote QQ user" premise void: screenshots are fine, no `qq_send_file` needed); consecutive local
|
|
251
|
+
turns do not repeat it. Detected via the `input` event's `source=interactive` (this extension's
|
|
252
|
+
`sendUserMessage` is `extension`, so it never misfires); interjecting mid-run of a QQ turn does not
|
|
253
|
+
count as a hand-off (no new run). `/status` gains `来源: QQ(远程)/ 本机(TUI)`.
|
|
254
|
+
|
|
255
|
+
### Fixed
|
|
256
|
+
|
|
257
|
+
- **No automatic progress while an approval is pending**: when a tool approval waits for the user,
|
|
258
|
+
automatic progress pushes are held back (otherwise one line every 15 s would push the approval
|
|
259
|
+
prompt off-screen); they resume on the next interval after the approval is handled (or times out).
|
|
260
|
+
Asking `/progress` yourself still works; `/status` shows 「等审批期间暂缓」. The in-session manual
|
|
261
|
+
pause (`/progress off`) and quota-silence semantics are unchanged.
|
|
262
|
+
|
|
263
|
+
### Tests
|
|
264
|
+
|
|
265
|
+
- New `test/remote-notice.test.mjs` (defaults / copy elements / `extra` append / system-rule length).
|
|
266
|
+
- `test/config.test.mjs` gains `remote` global-default + per-account override cases.
|
|
267
|
+
- `test/accounts.test.mjs` gains 3 cases: **local TUI chat injects nothing**, only the first QQ
|
|
268
|
+
message's run injects and once per session, re-injection after compaction; `remote.notice=false`
|
|
269
|
+
disables only the notice and `remote.extra` appends; `remote.guidelines=false` disables only the
|
|
270
|
+
rules, and repeated triggers within one turn do not stack.
|
|
271
|
+
- `test/progress.test.mjs` gains cases: no push while an approval is pending, `/status` shows the hold,
|
|
272
|
+
asking for progress still answers once, pushes resume after the approval is handled.
|
|
273
|
+
- `test/accounts.test.mjs` gains 2 more cases: the local hand-off corrects exactly once (`/status`
|
|
274
|
+
shows the origin); the pending hand-off is overwritten by a new QQ turn, and an `extension`-source
|
|
275
|
+
input is not a hand-off.
|
|
276
|
+
- `test/remote-notice.test.mjs` gains `qq-local-handoff` tag and copy assertions.
|
|
277
|
+
|
|
278
|
+
## [2.2.1] - 2026-02-11
|
|
279
|
+
|
|
280
|
+
### Fixed
|
|
281
|
+
|
|
282
|
+
- **Copy text was treated as a command**: when a user quoted the whole approval prompt back (or
|
|
283
|
+
discussed it in a sentence like 「你这句话里说『取消自动同意』」), the old **unanchored** regex
|
|
284
|
+
matched 「取消自动同意」 / 「以后都同意」 inside the sentence and acted on it (the log even showed
|
|
285
|
+
a reply of 「当前没有开启自动同意」). `parseReply` now strips whitespace and punctuation and does
|
|
286
|
+
**whole-sentence matching** with a length cap (≤ 12 chars); quoted prompts, long sentences and
|
|
287
|
+
exclamations are never parsed — they go to pi as ordinary chat.
|
|
288
|
+
- **The approval prompt was too wordy**: compressed from 3 lines (with 「停止」/「以后都同意」/
|
|
289
|
+
「取消自动同意」) to 2 lines: `[审批 #N] <summary>` + `回「同意 N」或「拒绝 N」;想全部放行回
|
|
290
|
+
「以后都同意」。`; tools that must be confirmed every time (file sending) use the strict copy
|
|
291
|
+
(no 「以后都同意」).
|
|
292
|
+
|
|
293
|
+
### Tests
|
|
294
|
+
|
|
295
|
+
- New `test/approver.test.mjs` (6 cases: approve/reject variants, stop / always-allow / revoke-always,
|
|
296
|
+
quoted prompt not parsed, mid-sentence keywords not parsed, 「停止汇报」 not a command, overlong
|
|
297
|
+
sentence not parsed, prompt ≤ 2 lines).
|
|
298
|
+
- `test/queue.test.mjs` syncs the strict-copy assertions (19 cases).
|
|
299
|
+
|
|
300
|
+
## [2.2.0] - 2026-02-11
|
|
301
|
+
|
|
302
|
+
### Added
|
|
303
|
+
|
|
304
|
+
- **Sending local files to QQ**: new tool `qq_send_file(path, name?)` and command `/sendfile <path>`.
|
|
305
|
+
Uploads through the official rich-media API in chunks (`upload_prepare` → chunked `PUT` →
|
|
306
|
+
`upload_part_finish` → merge for `file_info`), then sends with `msg_type=7` + `media.file_info` as a
|
|
307
|
+
**passive reply** (`srv_send_msg:false`, no active-quota burn). Local files have no public URL and
|
|
308
|
+
the one-shot binary upload caps at 5 MB, so everything goes through chunking, no relay dependency.
|
|
309
|
+
- **`file_type` mapping**: png/jpg/jpeg → image (1), mp4 → video (2), silk → voice (3), everything
|
|
310
|
+
else → file (4); otherwise QQ answers `850019`. Images/videos/voice display inline; other formats
|
|
311
|
+
arrive as a downloadable file card.
|
|
312
|
+
- **Outbound files are force-gated (asked every time)**: `qq_send_file` is added to
|
|
313
|
+
`FORCE_APPROVAL_TOOLS` — a QQ approval is mandatory no matter what `approve.tools` says, and it
|
|
314
|
+
**skips the session-wide 「以后都同意」 and remembered decisions**; each file is confirmed on its
|
|
315
|
+
own and is never written into remember. The prompt shows the file path and display name, with
|
|
316
|
+
「需单独确认」.
|
|
317
|
+
- **Config `files`**: `enabled` (default true), `maxBytes` (default 30 MB; QQ hard cap 200 MB),
|
|
318
|
+
overridable per bot.
|
|
319
|
+
- **Error-code translation**: `850031` too large, `850019` format unsupported, `40093002` daily
|
|
320
|
+
capacity reached, `850027` timeout, `850018` muted/rate-limited, `40093001` transfer failed — all
|
|
321
|
+
turned into Chinese receipts.
|
|
322
|
+
|
|
323
|
+
### Changed
|
|
324
|
+
|
|
325
|
+
- `qq/api.ts`: new `sendC2CMedia(openid, fileInfo, { msgId })`, reusing the existing send queue and
|
|
326
|
+
`msg_seq` counting.
|
|
327
|
+
- `slash.ts`: new takable command `sendfile` (not gated by `commands.allowSession`; guarded by
|
|
328
|
+
`files.enabled` + approval).
|
|
329
|
+
- `index.ts`: `State` records `restBase`; `/help` gains the `/sendfile` entry.
|
|
330
|
+
- `package.json`: new peer dependency `typebox` (tool-parameter schema; aliased by the pi runtime).
|
|
331
|
+
|
|
332
|
+
### Tests
|
|
333
|
+
|
|
334
|
+
- New `test/upload.test.mjs` (9 cases: `file_type` mapping, md5/sha1/md5_10m, the full chunked
|
|
335
|
+
pipeline with byte reassembly, image type, chunk retry, local validation (missing / directory /
|
|
336
|
+
empty / over limit), merge without `file_info`, error-code translation, `sendC2CMedia`'s
|
|
337
|
+
`msg_type=7` body).
|
|
338
|
+
- Added to `npm test`; the whole suite went 130 → 140 cases green (queue 18 → 19, with the new
|
|
339
|
+
"file sends are always approved" case).
|
|
340
|
+
|
|
341
|
+
## [2.1.0] - 2026-02-11
|
|
342
|
+
|
|
343
|
+
### Added
|
|
344
|
+
|
|
345
|
+
- **Receiving QQ attachments (images / voice / video / documents / archives / plain files)**: incoming
|
|
346
|
+
attachments are downloaded to temp files immediately and their paths are handed to pi as marker
|
|
347
|
+
lines — images as `图片路径 <path>`, the rest by type as `语音路径` / `视频路径` / `文档路径` /
|
|
348
|
+
`压缩包路径` / `文件路径`. The original text stays on the first line, in the same turn as the
|
|
349
|
+
paths; pi's `read` tool can open the image directly and `bash` can process the files. (Before
|
|
350
|
+
2.1.0 only images were kept and never saved to disk; a pure-image message delivered an empty
|
|
351
|
+
string to pi.)
|
|
352
|
+
- **QQ receipt**: after saving, the bot replies 「已收到 N 个附件(图片 ×1、文档 ×1),已存为临时文件
|
|
353
|
+
并交给 pi:」 followed by each path; failed downloads are listed with the reason, and the text part
|
|
354
|
+
still runs.
|
|
355
|
+
- **Automatic temp-file cleanup**: the save directory is split by date
|
|
356
|
+
(`<system temp>/pi-qqbot/<bot>/<YYYY-MM-DD>/`) and swept at session start according to
|
|
357
|
+
`attachments.retentionHours` (default 24 h) and `attachments.maxFiles` (default 200); `<date>`
|
|
358
|
+
subdirectories not created by this extension are never touched.
|
|
359
|
+
- **Config `attachments`**: `enabled` / `dir` / `maxBytes` / `maxPerMessage` / `retentionHours` /
|
|
360
|
+
`maxFiles`, overridable per bot in `qqbots[]` like `queue` / `progress`. With `enabled: false` it
|
|
361
|
+
replies with an ignore notice and processes text only.
|
|
362
|
+
- **New `attachment.ts`**: six-type detection, filename sanitising (only `[0-9a-zA-Z._-]` kept),
|
|
363
|
+
concurrent downloads (default 3), 25 MB per-attachment cap, 30 s download timeout, one retry with
|
|
364
|
+
credentials on 401/403, a failed attachment never blocks the others.
|
|
365
|
+
|
|
366
|
+
### Changed
|
|
367
|
+
|
|
368
|
+
- `qq/gateway.ts`: `QQMessage.attachments` becomes `{ url, contentType, filename?, size? }[]` and
|
|
369
|
+
**no longer drops non-image attachments**; when a voice message body only has `voice_wav_url`, that
|
|
370
|
+
is used as the download URL.
|
|
371
|
+
- The gateway log no longer prints the whole JSON packet (it carries the short-lived `rkey`
|
|
372
|
+
credential); it logs openid / content summary / attachment count / source host instead.
|
|
373
|
+
- `handleMessage()`: messages with attachments **bypass** slash commands, approval parsing, numbered
|
|
374
|
+
selection and built-in commands — always ordinary content (still queued when busy).
|
|
375
|
+
|
|
376
|
+
### Tests
|
|
377
|
+
|
|
378
|
+
- New `test/attachment.test.mjs` (12 cases: six-type detection, extension rules, saved content, over
|
|
379
|
+
limit, 401 retry, partial failure, per-message cap, receipt copy, directory cleanup).
|
|
380
|
+
- `test/queue.test.mjs` 12 → 18 cases (pure-image dispatch, text+document line joining, busy queueing,
|
|
381
|
+
`enabled=false` ignore notice, `/status` bypass, failed download still delivers the text).
|
|
382
|
+
|
|
383
|
+
## [2.0.1] - 2026-10-25
|
|
384
|
+
|
|
385
|
+
### Fixed
|
|
386
|
+
|
|
387
|
+
- **Word order: 「暂停汇报进度」 was not recognised** and leaked to the model as ordinary chat. Both
|
|
388
|
+
orders — 「暂停**进度汇报**」 and 「暂停**汇报进度**」 — are now recognised; trailing particles
|
|
389
|
+
like 「别汇报了**吧**」 are also caught.
|
|
390
|
+
|
|
391
|
+
### Changed
|
|
392
|
+
|
|
393
|
+
- **Colloquial mute words expanded**: 「别汇报了」「别说了」「别讲了」「别推了」「别发了」「别报了」
|
|
394
|
+
「闭嘴」「安静」「打住」「甭」 etc. are equivalent to `/progress off` (resume with 「恢复汇报」 /
|
|
395
|
+
「接着说」).
|
|
396
|
+
- **「停」 now interrupts the task** (per user feedback): `停`/`停下`/`停一下`/`停停`/`停掉`/`别跑了`
|
|
397
|
+
join `RE_CANCEL` and are fully equivalent to `/cancel`; only noun-carrying phrases like 「停止汇报」
|
|
398
|
+
or pure shut-up words mute the reports.
|
|
399
|
+
- **Asking for progress is a command**: 「汇报一下当前进度」「看一下进度」「说说进度」「进度如何」
|
|
400
|
+
「进度怎么样」 are no longer forwarded to pi as chat; while busy they answer once with a summary
|
|
401
|
+
without queueing, while idle they answer 「当前没有正在运行的任务」.
|
|
402
|
+
- `/help` distinguishes the two meanings: to stop only the pushes use `/progress off` / 「别汇报了」 /
|
|
403
|
+
「闭嘴」; 「停」 interrupts the task.
|
|
404
|
+
- Both READMEs gain the colloquial-shortcut table (intent → things to say).
|
|
405
|
+
|
|
406
|
+
### Tests
|
|
407
|
+
|
|
408
|
+
- `test/progress.test.mjs` 16 → 22 cases (new/rewritten: both word orders, colloquial mute words,
|
|
409
|
+
「停/停下/停一下/停止/取消」 all interrupt, 「停止汇报」 still mutes, three poses of asking for
|
|
410
|
+
progress, no false positives).
|
|
411
|
+
|
|
412
|
+
## [2.0.0] - 2026-10-25
|
|
413
|
+
|
|
414
|
+
### Added
|
|
415
|
+
|
|
416
|
+
- **Multiple QQ bots (one config, one machine, several pi processes)**: the config takes
|
|
417
|
+
`qqbots: [...]` with multiple `appId` / `appSecret` pairs, supporting `name` and per-account
|
|
418
|
+
overrides of `sandbox` / `allowlist` / `queue` / `progress` / `approve` / `commands` (unwritten keys
|
|
419
|
+
inherit the global defaults, merged per key).
|
|
420
|
+
- **Automatic claiming**: every pi process, on start (`session_start`), claims the first **offline**
|
|
421
|
+
bot in config order; processes that cannot claim stay off QQ but rescan every 5 seconds and take
|
|
422
|
+
over when someone exits. One lock per process — never concurrent double-connection.
|
|
423
|
+
- **"Never steal a bot temporarily released by a reload"**: on session rebuild (`/reload`, `/new`,
|
|
424
|
+
`/fork`, `/resume`) a 20-second reservation is written **before** releasing the lock, so other
|
|
425
|
+
processes skip that bot; after claiming, the reservation record is re-read and yielded on collision
|
|
426
|
+
(closing the race window). The new runtime reclaims its own bot with the same pid, silently.
|
|
427
|
+
- **Visible identity**: the claim log carries bot name / appId / cwd / pid; the first QQ receipt is
|
|
428
|
+
prefixed with 「(已接入 qqbot「work」)」; `/status`'s first line shows `bot: work (…0143) · cwd: …`.
|
|
429
|
+
- **Takeover notices**: `~/.pi/agent/pi-qqbot-<appId>.owner.json` records the previous holder's pid and
|
|
430
|
+
working directory; on takeover the log writes the old and new pid/cwd and the first QQ receipt
|
|
431
|
+
carries 「(qqbot「work」已由新实例接管 · 当前工作目录 …)」.
|
|
432
|
+
- `PI_QQBOT_ACCOUNT=<name|appId>` pins an account (for debugging or a fixed directory); an unknown
|
|
433
|
+
name means no connection, with a log line.
|
|
434
|
+
- **Credentials are verified right after claiming**: the claim immediately exchanges an
|
|
435
|
+
`access_token`; on failure it **releases the lock** and skips that bot (trying the next one), so a
|
|
436
|
+
bot with a wrong secret can no longer hold the slot while never connecting; when nothing works it
|
|
437
|
+
stops rescanning pointlessly.
|
|
438
|
+
- **Fix** `AuthManager`: the QQ platform answers invalid credentials with HTTP 200 + `code 100016`;
|
|
439
|
+
the old code went on to connect the gateway with an empty token. It now throws immediately (with
|
|
440
|
+
code/message) and is caught during claiming.
|
|
441
|
+
- New doc [docs/accounts.md](docs/accounts.md); new tests `test/config.test.mjs` (9),
|
|
442
|
+
`test/lock.test.mjs` (8, real subprocesses + real lockfile, including "two processes in the same
|
|
443
|
+
working directory each get one bot"), `test/accounts.test.mjs` (9, end-to-end claiming / notices /
|
|
444
|
+
invalid-credential degradation), `test/auth.test.mjs` (3).
|
|
445
|
+
|
|
446
|
+
### Changed
|
|
447
|
+
|
|
448
|
+
- Logs are per account: `~/.pi/agent/pi-qqbot-<name>.log` (several processes on one machine no longer
|
|
449
|
+
interleave); the single-bot v1 format still writes `pi-qqbot.log`.
|
|
450
|
+
- `/status` gains the bot identity and working-directory lines; `/help` gains the multi-bot note.
|
|
451
|
+
- Config loading becomes `loadAccounts()` (returns the account list in order); `loadConfig()` stays as
|
|
452
|
+
a compatibility wrapper.
|
|
453
|
+
|
|
454
|
+
### Compatibility
|
|
455
|
+
|
|
456
|
+
- **Old single-bot configs work unchanged** (equivalent to an account named `default`, behaving like
|
|
457
|
+
1.5.0); `QQ_BOT_APPID` / `QQ_BOT_APPSECRET` still apply to the single-bot scenario only.
|
|
458
|
+
- Writing both the top-level `appId` and `qqbots` → error, no connection (ambiguity is not guessed);
|
|
459
|
+
an account with missing credentials / duplicate `appId` / duplicate `name` is skipped with a log
|
|
460
|
+
line, the rest work normally.
|
|
461
|
+
- **A bot is not bound to a working directory**: allocation depends only on config order and online
|
|
462
|
+
state, not on cwd — several pi processes in the same directory each get their own bot. cwd is only
|
|
463
|
+
used for logs, takeover notices and `/status`.
|
|
464
|
+
- Recommended: restart (or `/reload`) once after the upgrade — per-account log files only appear on a
|
|
465
|
+
fresh start.
|
|
466
|
+
|
|
467
|
+
## [1.5.0] - 2026-10-25
|
|
468
|
+
|
|
469
|
+
### Added
|
|
470
|
+
|
|
471
|
+
- **Pause / resume automatic progress reports (per session)**: `/progress off` (aliases `/p pause`,
|
|
472
|
+
`暂停`, `stop`, 「暂停汇报」「关闭自动汇报」) stops the timer; `/progress on` (aliases `resume`,
|
|
473
|
+
`start`, `恢复`, 「恢复汇报」「继续汇报」) turns it back on.
|
|
474
|
+
- The manual pause survives runs (`agent_start` no longer resets it) and only resets to enabled on
|
|
475
|
+
`session_start` (`/new`, `/reload`, restart).
|
|
476
|
+
- While manually paused, `/progress`, 「进度」, 「当前进度」, 「现在进度」, 「进展」 **answer once
|
|
477
|
+
with the current summary** without secretly re-opening the 15 s timer, and the reply ends with
|
|
478
|
+
「(自动汇报已暂停,发 /progress on 恢复)」.
|
|
479
|
+
- Strictly separate from the existing "quota silence": quota silence is the automatic pause after a
|
|
480
|
+
failed send, cleared by asking once; the manual pause is cleared only by `on` / 「恢复汇报」.
|
|
481
|
+
- `/progress <gibberish>` answers with usage instead of being silently ignored.
|
|
482
|
+
- `/status` gains an `自动汇报:` line distinguishing four modes: `开启(每 Ns)` / `已暂停(本会话…)`
|
|
483
|
+
/ `推送失败已静默` / `未启用(配置 progress.enabled=false)`.
|
|
484
|
+
- Bare-text queries 「当前进度」「现在进度」「目前进展」 (sub-commands allowed, e.g. 「进度 暂停」)
|
|
485
|
+
also route to the progress query.
|
|
486
|
+
- Tests: `test/progress.test.mjs` gains 7 cases (pause/resume immediate effect, survives runs, asking
|
|
487
|
+
while paused does not unpause, Chinese commands without false positives, idle 「当前进度」,
|
|
488
|
+
`/status` copy, unknown subcommand).
|
|
489
|
+
|
|
490
|
+
### Changed
|
|
491
|
+
|
|
492
|
+
- `/progress` (no argument) no longer clears the pause while manually paused (previously any query
|
|
493
|
+
wiped the pause flag).
|
|
494
|
+
- "Failed-send silence" and "user pause" are two separate states (`progressPaused` / `progressMuted`)
|
|
495
|
+
that no longer overwrite each other.
|
|
496
|
+
- README (English) and `docs/README.zh-CN.md` command table, progress section and `progress.enabled`
|
|
497
|
+
notes updated in sync.
|
|
498
|
+
|
|
499
|
+
## [1.4.0] - 2026-10-24
|
|
500
|
+
|
|
501
|
+
### Added
|
|
502
|
+
|
|
503
|
+
- While a task runs, a progress summary is pushed every `progress.intervalMs` (default 15 s): turns,
|
|
504
|
+
tool-call count with the top three tools, failure count, the most recently finished tool (with
|
|
505
|
+
duration), the tool currently running, and token usage. Built purely from local events — no model
|
|
506
|
+
call, no tokens spent.
|
|
507
|
+
- End-to-end secret redaction (`redact.ts`): progress summaries, approval prompts, queue previews and
|
|
508
|
+
log writes all pass through one scrubber covering `Authorization` / QQ bot tokens / `sk-…` /
|
|
509
|
+
`ghp_…` / `npm_…` / `AKIA…` / JWT / PEM / `token=` / `password=` / `--api-key` / URL `user:pass@` /
|
|
510
|
+
high-entropy strings.
|
|
511
|
+
- `/progress` (aliases `/p`, 「进度」) and the busy-time queries 「到哪了」 / 「怎么样了」.
|
|
512
|
+
- A final answer that cannot be delivered (quota) is cached and re-sent with
|
|
513
|
+
「(上一条回复发送失败,补发如下)」 on the next message.
|
|
514
|
+
- Config `progress.enabled` / `progress.intervalMs` (env override `PI_QQBOT_PROGRESS_MS`).
|
|
515
|
+
|
|
516
|
+
## [1.3.3] - 2026-10-24
|
|
517
|
+
|
|
518
|
+
### Fixed
|
|
519
|
+
|
|
520
|
+
- The `/usage` report read-back now polls: `appendEntry` lands after the command's `await` returns,
|
|
521
|
+
which occasionally missed the freshly written report.
|
|
522
|
+
|
|
523
|
+
## [1.3.2] - 2026-10-24
|
|
524
|
+
|
|
525
|
+
### Fixed
|
|
526
|
+
|
|
527
|
+
- `/usage` read-back fallback: when `getEntries` has not refreshed yet, read the tail of the session
|
|
528
|
+
file directly; `generatedAt` tolerates millisecond timestamps.
|
|
529
|
+
|
|
530
|
+
## [1.3.1] - 2026-10-24
|
|
531
|
+
|
|
532
|
+
### Fixed
|
|
533
|
+
|
|
534
|
+
- `/usage rate` now really executes and reads the config file back to confirm, instead of claiming
|
|
535
|
+
success in words only.
|
|
536
|
+
|
|
537
|
+
## [1.3.0] - 2026-10-24
|
|
538
|
+
|
|
539
|
+
### Added
|
|
540
|
+
|
|
541
|
+
- `/usage`, `/cost` (extension commands that only print to `ctx.ui.*`) are bridged back to QQ instead
|
|
542
|
+
of being silent.
|
|
543
|
+
- `/mcp`, `/llama` — commands that need host-TUI selection dialogs or login — answer with an explicit
|
|
544
|
+
explanation instead.
|
|
545
|
+
|
|
546
|
+
## [1.2.1] - 2026-10-24
|
|
547
|
+
|
|
548
|
+
### Changed
|
|
549
|
+
|
|
550
|
+
- Log timestamps use local time with a UTC offset (e.g. `[2026-10-24T07:39:09.762+08:00]`), aligning
|
|
551
|
+
directly with file mtimes and process start times — no timezone arithmetic needed.
|
|
552
|
+
|
|
553
|
+
## [1.2.0] - 2026-10-24
|
|
554
|
+
|
|
555
|
+
### Added
|
|
556
|
+
|
|
557
|
+
- Messages arriving while busy go into a queue and resume automatically after the current task
|
|
558
|
+
settles; `/cancel` is never queued and takes effect immediately.
|
|
559
|
+
- `/queue` (alias `/q`) to view the queue; `/queue clear`, `/queue drop <编号>` to manage it.
|
|
560
|
+
- Config `queue.max` (0 = queueing off) and `queue.ack`.
|
|
561
|
+
|
|
562
|
+
## [1.1.0] - 2026-10-24
|
|
563
|
+
|
|
564
|
+
### Added
|
|
565
|
+
|
|
566
|
+
- Numbered-selection takeover for `/tree`, `/fork`, `/resume`: replies with a list of at most 10
|
|
567
|
+
entries, valid only for the asking user, expiring in 10 minutes.
|
|
568
|
+
- `/clone` support.
|
|
569
|
+
|
|
570
|
+
## [1.0.0] - 2026-10-24
|
|
571
|
+
|
|
572
|
+
### Added
|
|
573
|
+
|
|
574
|
+
- Takes over pi's slash commands (allowlist `commands.passthrough` / `allowReload` / `allowQuit` /
|
|
575
|
+
`allowSession`): `/reload`, `/new`, `/compact`, `/session`, `/name`, `/model`, `/thinking`,
|
|
576
|
+
`/quit`, plus passthrough of extension commands, prompt templates and `/skill:name`.
|
|
577
|
+
- While a task is running, `/reload`, `/new`, `/compact`, `/quit`, `/tree`, `/fork`, `/clone`,
|
|
578
|
+
`/resume` refuse to execute.
|
|
579
|
+
|
|
580
|
+
## [0.3.0] - 2026-10-24
|
|
581
|
+
|
|
582
|
+
### Added
|
|
583
|
+
|
|
584
|
+
- `/cancel` (aliases `/stop`, `/abort`, 「取消」「停止」「中止」「终止」「算了」) interrupts the
|
|
585
|
+
running task: first terminates pending approvals and empties the queue, then `ctx.abort()` and
|
|
586
|
+
drops the half-written reply; an 8-second watchdog releases the busy flag as a fallback.
|
|
587
|
+
|
|
588
|
+
## [0.2.1] - 2026-10-24
|
|
589
|
+
|
|
590
|
+
### Changed
|
|
591
|
+
|
|
592
|
+
- The Chinese README moves into `docs/README.zh-CN.md`, fixing the npm page defaulting to the Chinese
|
|
593
|
+
readme.
|
|
594
|
+
|
|
595
|
+
## [0.2.0] - 2026-10-24
|
|
596
|
+
|
|
597
|
+
### Added
|
|
598
|
+
|
|
599
|
+
- Session-level "always allow" (`approve.remember`), including the request already waiting;
|
|
600
|
+
phrases that only look like cancelling — 「取消自动同意」「恢复审批」 — still go through the
|
|
601
|
+
approval branch.
|
|
602
|
+
- Bilingual README (Chinese and English).
|
|
603
|
+
|
|
604
|
+
## [0.1.0] - 2026-10-16
|
|
605
|
+
|
|
606
|
+
### Added
|
|
607
|
+
|
|
608
|
+
- Initial release: bridges the Pi coding agent into QQ private chats (C2C), single session +
|
|
609
|
+
`proper-lockfile` single-instance lock (host takeover), remote approval before dangerous tools
|
|
610
|
+
(`bash` / `powershell` / `write` / `edit` and `destructiveHint` tools) run, automatic segmentation
|
|
611
|
+
of long replies.
|
|
@@ -3,6 +3,46 @@
|
|
|
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
|
+
> 英文版 changelog:[CHANGELOG.en.md](CHANGELOG.en.md)
|
|
7
|
+
|
|
8
|
+
## [2.6.2] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`/qq …` 绕过命令门禁**:`/qq` 本身就是扩展命令,所以直接发 `/qq reload`、`/qq quit`、`/qq new`
|
|
13
|
+
会落进 `routeSlash` 的 passthrough 分支,既不查 `commands.allowReload` / `allowQuit` / `allowUpdate` /
|
|
14
|
+
`allowSession`,也不查忙碌状态(`needsIdle`)——默认配置(`allowQuit: false`)下 QQ 侧发 `/qq quit`
|
|
15
|
+
就能把主机 pi 关掉。现在 `/qq` 在 passthrough **之前**分流:`bridgeGate()` 把子命令翻回内置命令名
|
|
16
|
+
(多词 `list tree` / `list fork` / `list resume` 先整体匹配),复用同一套 `allowed()` / `needsIdle` 判定。
|
|
17
|
+
- **派发回执吞掉参数**:`/qq reload` 过去回「已派发 /qq 给 pi。」——`parseSlash` 只取首段当命令名,
|
|
18
|
+
看起来像 `reload` 没被受理。现在回执与日志都显示完整命令(超 80 字截断加 `…`)。
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- 自毁类桥接子命令(`reload` / `new` / `quit` / `clone`)改为**先回执再派发**,避免回执和
|
|
23
|
+
`ctx.reload()` / `newSession()` 抢时序。
|
|
24
|
+
- `/qq sendfile …`、`/qq usage …`、`/qq pick …` 以及未知子命令不再额外发一条通用回执(子命令自己会回),
|
|
25
|
+
减少一次无意义的消息。
|
|
26
|
+
- 顺手清掉 `slash.ts` 里一处历史补丁残留的空行 `+`(`+reply(formatPicker(picker))`,语义上无害但脏)。
|
|
27
|
+
|
|
28
|
+
### Compatibility
|
|
29
|
+
|
|
30
|
+
- 发 `/reload`、`/new`、`/quit`、`/compact` 等原命令的行为与门禁不变;只是 `/qq <sub>` 现在受同样约束。
|
|
31
|
+
- 无需改配置:原本 `allowReload: true` 的部署照旧可用;`allowQuit: false`(默认)下 `/qq quit` 变成回执拒绝。
|
|
32
|
+
|
|
33
|
+
### Tests
|
|
34
|
+
|
|
35
|
+
- 新增 `test/qq-bridge-gate.test.mjs`(21 条断言):子命令映射、三种门禁拒绝、忙碌拒绝、
|
|
36
|
+
先回执后派发顺序、多词 sub、回执截断、`/reload` 未回归。
|
|
37
|
+
- 全套 274 条断言 / 22 个测试文件通过;`tsc --noEmit --strict` 对改动文件 `slash.ts` 无报错。
|
|
38
|
+
|
|
39
|
+
## [2.6.1] - 2026-11-01
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **changelog 分语种**:中文版更名 `CHANGELOG.zh-CN.md`,新增英文版 `CHANGELOG.en.md`(含全部历史版本);
|
|
44
|
+
README.md(英文)引用 `CHANGELOG.en.md`,docs/README.zh-CN.md 引用 `CHANGELOG.zh-CN.md`。
|
|
45
|
+
|
|
6
46
|
## [2.6.0] - 2026-11-01
|
|
7
47
|
|
|
8
48
|
### Added
|
package/README.md
CHANGED
|
@@ -302,6 +302,15 @@ Guards:
|
|
|
302
302
|
else (or 「取消」) abandons it. A number without a pending list is ordinary chat.
|
|
303
303
|
- `/reload` is off by default: it restarts the bot, and if the new code fails to load the bot stays
|
|
304
304
|
down until someone fixes it on the host. Turn it on with `commands.allowReload: true`.
|
|
305
|
+
- Sending the bridge command yourself (`/qq reload`, `/qq quit`, `/qq new`, `/qq list fork`, …) hits the
|
|
306
|
+
**same** gates: the sub-command is mapped back to its built-in name first, so `commands.allowReload` /
|
|
307
|
+
`allowQuit` / `allowUpdate` / `allowSession` and the busy check all apply. Routing it as a plain
|
|
308
|
+
extension command would have bypassed every gate — with `allowQuit: false`, `/qq quit` could shut the
|
|
309
|
+
host pi down. Destructive sub-commands (`reload`, `new`, `quit`, `clone`) answer *before* they are
|
|
310
|
+
dispatched, because they replace the runtime they would answer through.
|
|
311
|
+
- Dispatch receipts echo the **whole** command (`已派发 /deploy prod 给 pi。`) instead of only the first
|
|
312
|
+
word, truncated at 80 characters with `…`. Previously `/qq reload` was answered with
|
|
313
|
+
`已派发 /qq 给 pi。`, which looked like the argument had been dropped.
|
|
305
314
|
- Why the indirection: pi only dispatches **extension** commands from a message
|
|
306
315
|
(`prompt()` → `_tryExecuteExtensionCommand`), and `reload()` / `newSession()` are command-context
|
|
307
316
|
APIs. So this extension registers `/qq` and forwards `/reload` → `/qq reload`. Built-in names are
|
|
@@ -525,7 +534,7 @@ Debug logs are written to `~/.pi/agent/pi-qqbot-<name>.log` (one file per bot, s
|
|
|
525
534
|
|
|
526
535
|
## Changelog
|
|
527
536
|
|
|
528
|
-
See [CHANGELOG.md](./CHANGELOG.md) — every release with what changed and why. The progress-report
|
|
537
|
+
See [CHANGELOG.en.md](./CHANGELOG.en.md) — every release with what changed and why. (中文:[CHANGELOG.zh-CN.md](./CHANGELOG.zh-CN.md)) The progress-report
|
|
529
538
|
pause/resume (`/progress off` / `on`) landed in **1.5.0**; multi-bot auto-claiming in **2.0.0**;
|
|
530
539
|
receiving QQ images/files (saved to temp files and handed to pi as `图片路径 <path>`) in **2.1.0**;
|
|
531
540
|
sending local files to QQ (`qq_send_file` / `/sendfile`, chunked rich-media upload) in **2.2.0**.
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -281,6 +281,13 @@ custom entry 类型名,以及它写在 `~/.pi/agent/` 下的两个配置文件
|
|
|
281
281
|
该用户下一条「纯数字」消息即执行选择,其它内容(或「取消」)则放弃。没有待选列表时,数字就是普通对话。
|
|
282
282
|
- `/reload` 默认关闭:它会重启 bot,如果新代码加载失败,bot 会一直哑着,只能在主机上救。
|
|
283
283
|
置 `commands.allowReload: true` 才启用。
|
|
284
|
+
- 直接发桥接命令(`/qq reload`、`/qq quit`、`/qq new`、`/qq list fork` …)走**同一套门禁**:子命令会先
|
|
285
|
+
翻回对应的内置命令名,所以 `commands.allowReload` / `allowQuit` / `allowUpdate` / `allowSession`
|
|
286
|
+
与忙碌检查全部生效。若按普通扩展命令派发(2.6.1 之前的行为),这些门禁会全被绕过 ——
|
|
287
|
+
`allowQuit: false` 时发 `/qq quit` 也能关掉主机 pi。自毁类子命令(`reload`、`new`、`quit`、`clone`)
|
|
288
|
+
会**先回执再派发**,因为动作会换掉它自己用来回执的运行时。
|
|
289
|
+
- 派发回执显示**完整命令**(`已派发 /deploy prod 给 pi。`),超 80 字截断加 `…`;不再只显示首段,
|
|
290
|
+
把参数吞掉(旧回执是 `已派发 /qq 给 pi。`,看起来像 `/qq reload` 的 `reload` 没生效)。
|
|
284
291
|
- 为什么要绕一层:pi 只对**扩展命令**执行派发(`prompt()` → `_tryExecuteExtensionCommand`),而
|
|
285
292
|
`reload()` / `newSession()` 又只能在命令上下文里调用。于是本扩展注册了 `/qq`,把 `/reload`
|
|
286
293
|
改派成 `/qq reload`。内置命令名是硬编码的(pi 没有把内置清单导出给扩展);任何不认识的
|
|
@@ -484,7 +491,7 @@ QQ 网关对同一 bot 只保留一个活跃连接。插件用 `proper-lockfile`
|
|
|
484
491
|
|
|
485
492
|
## 变更日志
|
|
486
493
|
|
|
487
|
-
见 [CHANGELOG.md](../CHANGELOG.md)
|
|
494
|
+
见 [CHANGELOG.zh-CN.md](../CHANGELOG.zh-CN.md):每个版本改了什么、为什么改。(English: [CHANGELOG.en.md](../CHANGELOG.en.md))进度汇报的暂停/恢复(`/progress off` / `on`)在 **1.5.0** 加入;多 bot 自动抢占在 **2.0.0**;接收 QQ 图片/文件(落盘后以 `图片路径 <path>` 这类标记行交给 pi)在 **2.1.0**;把本机文件发到 QQ(`qq_send_file` / `/sendfile`,富媒体分片上传)在 **2.2.0**。
|
|
488
495
|
|
|
489
496
|
## License
|
|
490
497
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zxh126/pi-qqbot",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bridge the Pi coding agent into QQ private chats, with remote tool-call approval",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node test/queue.test.mjs && node test/approver.test.mjs && node test/logger.test.mjs && node test/usage-bridge.test.mjs && node test/bang-notice.test.mjs && node test/bridge-stale-ctx.test.mjs && node test/progress.test.mjs && node test/redact.test.mjs && node test/config.test.mjs && node test/remote-notice.test.mjs && node test/lock.test.mjs && node test/accounts.test.mjs && node test/auth.test.mjs && node test/attachment.test.mjs && node test/upload.test.mjs && node test/error-notice.test.mjs && node test/billing.test.mjs && node test/credential-intake.test.mjs && node test/selfupdate.test.mjs && node test/chunk.test.mjs && node test/interactions.test.mjs"
|
|
30
|
+
"test": "node test/queue.test.mjs && node test/approver.test.mjs && node test/logger.test.mjs && node test/usage-bridge.test.mjs && node test/bang-notice.test.mjs && node test/bridge-stale-ctx.test.mjs && node test/qq-bridge-gate.test.mjs && node test/progress.test.mjs && node test/redact.test.mjs && node test/config.test.mjs && node test/remote-notice.test.mjs && node test/lock.test.mjs && node test/accounts.test.mjs && node test/auth.test.mjs && node test/attachment.test.mjs && node test/upload.test.mjs && node test/error-notice.test.mjs && node test/billing.test.mjs && node test/credential-intake.test.mjs && node test/selfupdate.test.mjs && node test/chunk.test.mjs && node test/interactions.test.mjs"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"*.ts",
|
|
34
34
|
"qq",
|
|
35
35
|
"util",
|
|
36
36
|
"README.md",
|
|
37
|
-
"CHANGELOG.md",
|
|
37
|
+
"CHANGELOG.en.md",
|
|
38
|
+
"CHANGELOG.zh-CN.md",
|
|
38
39
|
"docs",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
package/slash.ts
CHANGED
|
@@ -133,6 +133,33 @@ update: { sub: "update", needsIdle: true, hint: "在线更新 pi-qqbot 并重载
|
|
|
133
133
|
sendfile: { sub: "sendfile", needsArgs: true, hint: "把本机文件发送到你的 QQ" },
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* `/qq <sub> [args]` → 对应的 TAKEOVER 条目。
|
|
138
|
+
*
|
|
139
|
+
* `/qq` 自己也是**扩展命令**,所以直接发 `/qq reload` 会先命中 passthrough 分支:既不查
|
|
140
|
+
* `commands.allow*`,也不查 `needsIdle`(等于绕过 `/reload`、`/quit` 的门禁),回执还会
|
|
141
|
+
* 显示成「已派发 /qq 给 pi。」把参数吞掉。路由前先用这张表把子命令翻回内置命令名,
|
|
142
|
+
* 复用同一套判定。多词 sub(`list tree` / `list fork` / `list resume`)先整体匹配,再退回首词。
|
|
143
|
+
*/
|
|
144
|
+
export function bridgeGate(args: string): { name: string; takeover: Takeover } | undefined {
|
|
145
|
+
const lower = args.trim().toLowerCase();
|
|
146
|
+
if (!lower) return undefined;
|
|
147
|
+
const entries = Object.entries(TAKEOVER) as [string, Takeover][];
|
|
148
|
+
const exact = entries.find(([, item]) => item.sub.toLowerCase() === lower);
|
|
149
|
+
if (exact) return { name: exact[0], takeover: exact[1] };
|
|
150
|
+
const first = lower.split(/\s+/)[0]!;
|
|
151
|
+
const byFirst = entries.find(([, item]) => item.sub.split(/\s+/)[0]!.toLowerCase() === first);
|
|
152
|
+
return byFirst ? { name: byFirst[0], takeover: byFirst[1] } : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 命令被禁用时该提示改哪个配置项 */
|
|
156
|
+
export function gateConfigKey(name: string): string {
|
|
157
|
+
if (name === "reload") return "allowReload";
|
|
158
|
+
if (name === "quit") return "allowQuit";
|
|
159
|
+
if (name === "update") return "allowUpdate";
|
|
160
|
+
return "allowSession";
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
/**
|
|
137
164
|
* `/qq <sub>` 里会**换掉运行时**的动作。
|
|
138
165
|
*
|
|
@@ -235,6 +262,11 @@ const allowed = (cfg: CommandConfig, name: string): boolean => {
|
|
|
235
262
|
return cfg.allowSession;
|
|
236
263
|
};
|
|
237
264
|
|
|
265
|
+
/** 回执/日志里展示命令原文:太长就截断,别把 QQ 消息撑爆,也别只显示首段把参数吞掉 */
|
|
266
|
+
function previewCommand(raw: string, max = 80): string {
|
|
267
|
+
return raw.length > max ? `${raw.slice(0, max)}…` : raw;
|
|
268
|
+
}
|
|
269
|
+
|
|
238
270
|
/** Route one slash message. Returns "text" when it should be treated as chat. */
|
|
239
271
|
export async function routeSlash(deps: RouteDeps, text: string): Promise<RouteResult> {
|
|
240
272
|
// 0. `!` / `!!`:主机前端的 shell 快捷方式,扩展 API 够不到 → 明确回执,别当聊天丢给模型
|
|
@@ -249,6 +281,36 @@ export async function routeSlash(deps: RouteDeps, text: string): Promise<RouteRe
|
|
|
249
281
|
if (!parsed) return "text";
|
|
250
282
|
const { name } = parsed;
|
|
251
283
|
|
|
284
|
+
// 0.5 `/qq …`:桥接命令自身是扩展命令,落到下面的 passthrough 会**绕开** `commands.allow*`
|
|
285
|
+
// 与 `needsIdle`(默认配置下 QQ 侧发 `/qq quit` 就能关掉主机),且通用回执只显示 `/qq`。
|
|
286
|
+
// 这里提前分流:按内置命令的门禁判定,自毁类**先回执再派发**(动作会换掉运行时)。
|
|
287
|
+
if (name === "qq") {
|
|
288
|
+
const shown = previewCommand(parsed.raw);
|
|
289
|
+
const gate = bridgeGate(parsed.args);
|
|
290
|
+
if (!gate) {
|
|
291
|
+
deps.log("[cmd] 派发 /qq 桥接命令:", shown);
|
|
292
|
+
deps.dispatch(parsed.raw);
|
|
293
|
+
return "dispatched";
|
|
294
|
+
}
|
|
295
|
+
const builtin = gate.name;
|
|
296
|
+
const takeover = gate.takeover;
|
|
297
|
+
if (!allowed(deps.cfg, builtin)) {
|
|
298
|
+
deps.log("[cmd] 拒绝 /qq 子命令:", shown, "配置 commands." + gateConfigKey(builtin));
|
|
299
|
+
await deps.reply(`/${builtin} 在 QQ 侧被禁用(配置 commands.${gateConfigKey(builtin)})。`);
|
|
300
|
+
return "handled";
|
|
301
|
+
}
|
|
302
|
+
if (takeover.needsIdle && deps.busy) {
|
|
303
|
+
await deps.reply(`当前有任务在跑,先发 /cancel 取消,再执行 /${builtin}。`);
|
|
304
|
+
return "handled";
|
|
305
|
+
}
|
|
306
|
+
deps.log("[cmd] 派发 /qq 桥接命令:", shown, "gate=", builtin);
|
|
307
|
+
if (takeover.selfDestroying) {
|
|
308
|
+
await deps.reply(`已请求 /${builtin}(${takeover.hint})…`);
|
|
309
|
+
}
|
|
310
|
+
deps.dispatch(parsed.raw);
|
|
311
|
+
return "dispatched";
|
|
312
|
+
}
|
|
313
|
+
|
|
252
314
|
// 0. 输出只走 TUI 通道的扩展命令:桥接或明确回执(必须早于 passthrough)
|
|
253
315
|
const uiOnly = UI_ONLY[name];
|
|
254
316
|
if (uiOnly && deps.listPiCommands().some((c) => c.name.toLowerCase() === name)) {
|
|
@@ -275,7 +337,7 @@ export async function routeSlash(deps: RouteDeps, text: string): Promise<RouteRe
|
|
|
275
337
|
} else if (result === "rejected") {
|
|
276
338
|
await deps.reply(`队列已满,/${name} 没有排上。发 /queue 查看队列,或 /cancel 清空后重发。`);
|
|
277
339
|
} else {
|
|
278
|
-
await deps.reply(`已派发
|
|
340
|
+
await deps.reply(`已派发 ${previewCommand(parsed.raw)} 给 pi。`);
|
|
279
341
|
}
|
|
280
342
|
return "dispatched";
|
|
281
343
|
}
|
|
@@ -285,8 +347,7 @@ export async function routeSlash(deps: RouteDeps, text: string): Promise<RouteRe
|
|
|
285
347
|
const takeover = TAKEOVER[name];
|
|
286
348
|
if (takeover) {
|
|
287
349
|
if (!allowed(deps.cfg, name)) {
|
|
288
|
-
|
|
289
|
-
await deps.reply(`/${name} 在 QQ 侧被禁用(配置 commands.${gate})。`);
|
|
350
|
+
await deps.reply(`/${name} 在 QQ 侧被禁用(配置 commands.${gateConfigKey(name)})。`);
|
|
290
351
|
return "handled";
|
|
291
352
|
}
|
|
292
353
|
if (takeover.needsIdle && deps.busy) {
|
|
@@ -719,7 +780,7 @@ export async function runBridge(
|
|
|
719
780
|
return;
|
|
720
781
|
}
|
|
721
782
|
const picker: PickerState = { kind, items, openid: store?.pickerOwner ?? null, createdAt: Date.now() };
|
|
722
|
-
|
|
783
|
+
|
|
723
784
|
reply(formatPicker(picker));
|
|
724
785
|
return;
|
|
725
786
|
}
|