@zxh126/pi-qqbot 2.4.0 → 2.4.1
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 +40 -2
- package/README.md +13 -0
- package/docs/README.zh-CN.md +11 -0
- package/index.ts +17 -3
- package/package.json +2 -2
- package/slash.ts +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -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
|
+
## [2.4.1] - 2026-11-01
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`!` / `!!` 在 QQ 侧明确回执,不再静默丢给模型**:这两个前缀(含全角 `!` / `!!`)只在 pi 主机
|
|
11
|
+
TUI / RPC 前端被当成 shell 命令(`interactive-mode` 的 `text.startsWith("!")`、RPC 的 `bash` 控制命令),
|
|
12
|
+
扩展 API 没有等价能力(`pi.sendUserMessage()` 不分派它们,`pi.exec()` 不过审批、不写会话历史)。现在
|
|
13
|
+
`routeSlash()` 的第一步就拦下来,回一条说明:为什么不能执行,以及该怎么做(用中文说需求,走 `bash`
|
|
14
|
+
工具并在执行前审批)。既不执行,也不当聊天送给模型。
|
|
15
|
+
- **`/help` 增补一行**:说明 `!` / `!!` 不支持。
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **判定刻意收紧**:只有「前缀 + 非空命令」才算 bang 命令(单独 `!`、`!!`、`!!!`、`/xxx`、句末感叹号
|
|
20
|
+
`你好!` 都照旧走普通对话),避免把情绪化感叹号吞掉。
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`/qq reload` 不再报 `This extension ctx is stale after session replacement or reload`**(`/new`、
|
|
25
|
+
`/clone`、`/resume`、`/fork`、`/tree` 的编号选择同理):`runBridge()` 之后处理器还要 `syncModelFromCtx()`
|
|
26
|
+
读一次 `ctx.model`,而 `reload()` / `newSession()` / `fork()` / `switchSession()` / `navigateTree()`
|
|
27
|
+
一返回,pi 就把旧 `ExtensionCommandContext` 的**每个 guard getter 都变成抛错**(`runner.js` 的
|
|
28
|
+
`createCommandContext` 刻意保留 lazy descriptors)。现在:新增 `REPLACES_RUNTIME`
|
|
29
|
+
(`reload` / `update` / `new` / `quit` / `clone`),换过运行时就直接跳过 ctx 收尾;`syncModelFromCtx()`
|
|
30
|
+
自身也容错(`pick` 选 tree/fork/resume 这类运行时才知道的分支靠它兜底);桥接抛错后也不再继续碰 ctx。
|
|
31
|
+
回归用例 `test/bridge-stale-ctx.test.mjs` 用「替换后任何 getter 都抛 stale」的假 ctx 复现原报错栈
|
|
32
|
+
(`syncModelFromCtx` ← `handler`):修前 3 例失败、修后 5 例全绿。
|
|
33
|
+
|
|
34
|
+
### Compatibility
|
|
35
|
+
|
|
36
|
+
- 纯新增分支:斜杠命令路由、审批、队列、进度、凭据接手都不受影响。
|
|
37
|
+
- 行为变化仅一处:`!xxx` / `!!xxx` 以前是「当作普通正文送给模型」,现在是「回执不执行」。如果之前靠发
|
|
38
|
+
`!xxx` 让模型自己去调 `bash`,请改成正常描述需求。
|
|
39
|
+
- 带附件的消息仍绕过所有指令路由(含 bang 判定),与原来一致。
|
|
40
|
+
- `/reload`、`/update`、`/new`、`/clone` 的**功能**还是原来的(会话确实被替换/重载),只是不再多报一条错误。
|
|
41
|
+
- 测试:新增 `test/bang-notice.test.mjs`(6 例)、`test/bridge-stale-ctx.test.mjs`(5 例),
|
|
42
|
+
`npm test` 19 个用例文件全绿。
|
|
43
|
+
- 升级无痛:配置格式、命令名、审批与队列语义均未变;`!` / `!!` 之前就是「不执行」,只是从「静默丢给模型」
|
|
44
|
+
改成「明确回执」。
|
|
45
|
+
|
|
6
46
|
## [2.4.0] - 2026-11-01
|
|
7
47
|
|
|
8
48
|
### Added
|
|
@@ -96,8 +136,6 @@
|
|
|
96
136
|
`source=extension` 的 input 不算接管。
|
|
97
137
|
- `test/remote-notice.test.mjs` 加 `qq-local-handoff` 的 tag 与文案断言。
|
|
98
138
|
|
|
99
|
-
## [Unreleased]
|
|
100
|
-
|
|
101
139
|
## [2.2.1] - 2026-02-11
|
|
102
140
|
|
|
103
141
|
### Fixed
|
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
| **图片与文件** | QQ 里发来的附件会先下载到临时文件,再以标记行交给 pi——图片是 `图片路径 <path>`,其余按类型是 `语音路径` / `视频路径` / `文档路径` / `压缩包路径` / `文件路径`;QQ 侧收到一条回执(数量、类型、路径)。pi 的 `read` 工具能直接看图片,`bash` 能处理文件。 |
|
|
25
25
|
| **把本机文件发到 QQ** | 让 agent 把机器上的文件发给你(「把 D:\docs\报告.pdf 发给我」)或发 `/sendfile <路径>`;机器人按 5MB 分片上传(QQ 富媒体),图片(png/jpg) / 视频(mp4) / 语音(silk) 直接展示,其余变成可下载的文件卡片。**外发必须先在 QQ 里审批。** |
|
|
26
26
|
| **内置指令** | `/status`、`/queue`、`/progress`、`/reset`、`/cancel`、`/sendfile`、`/update`、`/help`。 |
|
|
27
|
+
| **`!` / `!!` 只回执、不执行** | 这两个 shell 快捷前缀只在 pi 的 TUI / RPC 前端生效,扩展 API 没有等价能力,所以 QQ 侧只回一条说明,既不执行、也不把 `!ls` 当聊天丢给模型。要跑命令直接用中文说需求,agent 会调 `bash` 工具并在执行前向你申请审批。 |
|
|
27
28
|
|
|
28
29
|
## 安装
|
|
29
30
|
|
|
@@ -250,6 +251,12 @@ QQ run 还在跑时插话不处理(那时不会开新的 run)。`/status`
|
|
|
250
251
|
只能在主机执行(回执,不转发给模型):`/settings`、`/hotkeys`、`/changelog`、`/trust`、`/login`、
|
|
251
252
|
`/logout`、`/scoped-models`、`/llama`、`/import`、`/export`、`/share`、`/bug`、`/copy`、`/privacy`。
|
|
252
253
|
|
|
254
|
+
以 `!` 或 `!!` 开头的消息(全角 `!` / `!!` 也认)**不会被执行**。这两个前缀只在主机 pi 的 TUI / RPC 前端
|
|
255
|
+
被当成 shell 命令(`!` 跑完把输出交给模型,`!!` 跑完把输出排除出上下文),而扩展 API 没有等价入口:
|
|
256
|
+
`pi.sendUserMessage()` 不做这个分派,`pi.exec()` 又不过审批、不写会话历史。所以 `!ls` 只会收到一条
|
|
257
|
+
「QQ 侧不支持」的回执,也不会被当成正文送给模型。想跑命令直接用中文说需求(例如「列一下当前目录里的仓库」),
|
|
258
|
+
agent 会调 `bash` 工具,执行前在 QQ 里向你申请审批。单独一个 `!`、`!!!`、以及句末感叹号仍按普通对话处理。
|
|
259
|
+
|
|
253
260
|
### 只往 TUI 打字的命令,现在会回到 QQ
|
|
254
261
|
|
|
255
262
|
有些扩展命令从不往对话里输出,而是调 `ctx.ui.notify()`(配合 `pi.appendEntry()`)——只渲染在主机
|
|
@@ -278,6 +285,10 @@ custom entry 类型名,以及它写在 `~/.pi/agent/` 下的两个配置文件
|
|
|
278
285
|
`reload()` / `newSession()` 又只能在命令上下文里调用。于是本扩展注册了 `/qq`,把 `/reload`
|
|
279
286
|
改派成 `/qq reload`。内置命令名是硬编码的(pi 没有把内置清单导出给扩展);任何不认识的
|
|
280
287
|
`/xxx` 都会退回当普通文字。
|
|
288
|
+
- 桥接命令不会在「换掉运行时」之后再去碰旧 ctx:`reload` / `update` / `new` / `clone` / `quit`
|
|
289
|
+
(以及 `tree` / `fork` / `resume` 的编号选择)会让旧 `ExtensionCommandContext` 立刻失效,之后**读任何
|
|
290
|
+
属性** pi 都会抛 `This extension ctx is stale after session replacement or reload`。这些情况下模型缓存同步
|
|
291
|
+
直接跳过(`REPLACES_RUNTIME` + 防御式读取),不再多报一条错误。
|
|
281
292
|
|
|
282
293
|
### 忙碌时的队列
|
|
283
294
|
|
package/index.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { AuthManager } from "./qq/auth";
|
|
|
36
36
|
import { QQApi } from "./qq/api";
|
|
37
37
|
import { createGatewayUrlFetcher, QQGateway, QQMessage } from "./qq/gateway";
|
|
38
38
|
import { Approver } from "./approver";
|
|
39
|
-
import { parsePickIndex, pickerExpired, routeSlash, runBridge, type PickerState } from "./slash";
|
|
39
|
+
import { parsePickIndex, pickerExpired, REPLACES_RUNTIME, routeSlash, runBridge, type PickerState } from "./slash";
|
|
40
40
|
import { agoLabel, MessageQueue, previewText, type QueuedJob } from "./queue";
|
|
41
41
|
import { ProgressTracker } from "./progress";
|
|
42
42
|
import {
|
|
@@ -725,8 +725,12 @@ export default function (
|
|
|
725
725
|
} catch (e) {
|
|
726
726
|
log("[/qq] 桥接失败:", String(e));
|
|
727
727
|
reply(`/${sub} 执行失败:${String(e).slice(0, 200)}`);
|
|
728
|
+
return;
|
|
728
729
|
}
|
|
729
|
-
// 命令可能换了模型(/model、编号选择):同步一份给「欠费后发新 key
|
|
730
|
+
// 命令可能换了模型(/model、编号选择):同步一份给「欠费后发新 key」用。
|
|
731
|
+
// 但 reload / update / new / quit / clone 会把运行时换掉,pi 立刻把旧 ctx 判为 stale,
|
|
732
|
+
// 这时再读 ctx.model 就是错的(新实例会自己同步)——直接跳过,不发多余的错误。
|
|
733
|
+
if (REPLACES_RUNTIME.has(sub)) return;
|
|
730
734
|
syncModelFromCtx(state, ctx);
|
|
731
735
|
},
|
|
732
736
|
});
|
|
@@ -936,9 +940,18 @@ function onDispatchError(pi: ExtensionAPI, state: State, job: QueuedJob, error:
|
|
|
936
940
|
* 同步一份「当前真正生效的模型」到 state:
|
|
937
941
|
* 1. 欠费后用户发新 key 时,要知道落到哪个 provider;
|
|
938
942
|
* 2. 探测新 key 需要 baseUrl,只有 ctx.model 里有。
|
|
943
|
+
*
|
|
944
|
+
* ctx 可能是 **stale** 的:`reload()` / `newSession()` / `fork()` / `switchSession()` / `navigateTree()`
|
|
945
|
+
* 之后 pi 会把旧 ctx 的每个 guard getter 都变成抛错("This extension ctx is stale after session
|
|
946
|
+
* replacement or reload")。这只是个顺带的缓存,读不到就算了,不能让错误冒到命令处理器外面。
|
|
939
947
|
*/
|
|
940
948
|
function syncModelFromCtx(state: State, ctx: any): void {
|
|
941
|
-
|
|
949
|
+
let model: any;
|
|
950
|
+
try {
|
|
951
|
+
model = ctx?.model;
|
|
952
|
+
} catch {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
942
955
|
if (!model?.provider || !model?.id) return;
|
|
943
956
|
state.lastModel = {
|
|
944
957
|
provider: String(model.provider),
|
|
@@ -1465,6 +1478,7 @@ export async function handleMessage(
|
|
|
1465
1478
|
"/sendfile <路径> — 把本机文件发到你的 QQ(png/jpg/mp4/silk 直显,其余为文件卡片;外发需审批)",
|
|
1466
1479
|
"只关进度推送:/progress off(或「别汇报了」「闭嘴」);「停」是中断任务,不是静音",
|
|
1467
1480
|
"/help — 帮助",
|
|
1481
|
+
"不支持 ! / !! 命令:那是主机 pi 的 shell 快捷方式,QQ 侧只回执不执行;要跑命令直接用中文说需求。",
|
|
1468
1482
|
"pi 的命令优先:/reload /new /compact /session /name /model /thinking,以及扩展命令、模板与 /skill:xxx。",
|
|
1469
1483
|
"忙碌时发来的普通消息(含模板/skill 命令)会排队,当前任务结束后自动执行。",
|
|
1470
1484
|
"多 bot:配置了多个 qqbot 时,多个 pi 进程会按列表顺序各自接入一个,抢不到的待命重扫。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zxh126/pi-qqbot",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
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,7 +27,7 @@
|
|
|
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/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"
|
|
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"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"*.ts",
|
package/slash.ts
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* 3. **built-ins we cannot reach** (`HOST_ONLY`) — reply "run it on the host".
|
|
18
18
|
* 4. **anything else** — treated as ordinary chat text (the caller forwards it).
|
|
19
19
|
*
|
|
20
|
+
* Messages starting with `!` / `!!` are not slash commands but they are handled here too,
|
|
21
|
+
* for the same reason: pi only turns them into shell commands in the **TUI / RPC front end**
|
|
22
|
+
* (`interactive-mode` checks `text.startsWith("!")`, RPC has a `bash` control command), and the
|
|
23
|
+
* extension API exposes no equivalent (`pi.sendUserMessage()` does not dispatch them,
|
|
24
|
+
* `pi.exec()` bypasses approval and session history). Forwarding `!ls` to the model would just
|
|
25
|
+
* confuse it, so the bridge answers with an explicit "not supported in QQ" receipt.
|
|
26
|
+
*
|
|
20
27
|
* Selection-style commands (`/tree`, `/fork`, `/resume`) answer with a numbered list
|
|
21
28
|
* and accept the number as the next message; the pending list lives in `BridgeStore`.
|
|
22
29
|
*
|
|
@@ -45,6 +52,39 @@ export function parseSlash(text: string): SlashCommandText | null {
|
|
|
45
52
|
return { name: match[1]!.toLowerCase(), args: (match[2] ?? "").trim(), raw: text.trim() };
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
export interface BangCommand {
|
|
56
|
+
/** `!` (run + show output to the model) or `!!` (run, keep output out of the context) */
|
|
57
|
+
prefix: "!" | "!!";
|
|
58
|
+
/** whatever followed the prefix */
|
|
59
|
+
command: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `!` / `!!`,全角 `!` / `!!` 也认(中文输入法很容易打出全角) */
|
|
63
|
+
const BANG = /^([!!]{1,2})\s*([\s\S]*\S)\s*$/;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 识别 pi 的 shell 快捷前缀。只有「前缀 + 非空命令」才算,单独一个 `!`、`!!!`、`你好!` 都退回普通文本,
|
|
67
|
+
* 免得把情绪化感叹号当成命令吞掉。
|
|
68
|
+
*/
|
|
69
|
+
export function parseBangCommand(text: string): BangCommand | null {
|
|
70
|
+
const match = BANG.exec(text.trim());
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
const rest = match[2]!.trim();
|
|
73
|
+
// `!!!ls` 这类:第三个别当成命令内容,整体退回普通文本
|
|
74
|
+
if (/^[!!]/.test(rest)) return null;
|
|
75
|
+
return { prefix: match[1]!.replace(/!/g, "!") as "!" | "!!", command: rest };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `!` / `!!` 的 QQ 侧回执:说清为什么不能执行、以及该怎么做 */
|
|
79
|
+
export function bangNotice(cmd: BangCommand): string {
|
|
80
|
+
return [
|
|
81
|
+
`QQ 侧不支持 ${cmd.prefix} 命令,这一条没有执行。`,
|
|
82
|
+
`${cmd.prefix} 是主机 pi 里的 shell 快捷方式${cmd.prefix === "!!" ? "(`!!` 还会把输出排除出模型上下文)" : ""},` +
|
|
83
|
+
"只有 TUI / RPC 前端认识;扩展 API 没有等价能力,桥接不会假装执行一遍(也不会把这条丢给模型去猜)。",
|
|
84
|
+
"想在本机跑命令:直接用中文说需求(例如「列一下当前目录里的仓库」),我会调 bash 工具执行,执行前在 QQ 里向你申请审批。",
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
export type BridgeSub =
|
|
49
89
|
| "reload"
|
|
50
90
|
| "new"
|
|
@@ -92,6 +132,18 @@ update: { sub: "update", needsIdle: true, hint: "在线更新 pi-qqbot 并重载
|
|
|
92
132
|
sendfile: { sub: "sendfile", needsArgs: true, hint: "把本机文件发送到你的 QQ" },
|
|
93
133
|
};
|
|
94
134
|
|
|
135
|
+
/**
|
|
136
|
+
* `/qq <sub>` 里会**换掉运行时**的动作。
|
|
137
|
+
*
|
|
138
|
+
* pi 在它们返回后立刻把旧 `ExtensionCommandContext` 判为 stale:每个 guard getter /
|
|
139
|
+
* 方法都会抛 `This extension ctx is stale after session replacement or reload`。
|
|
140
|
+
* 所以调用方在这几个动作之后不能再碰 ctx(读 `ctx.model` 也算)。
|
|
141
|
+
*
|
|
142
|
+
* `/qq pick` 选 tree / fork / resume 时同样会换运行时,但运行时才知道选的是哪一类,
|
|
143
|
+
* 调用方对它靠 `syncModelFromCtx()` 内部的 try/catch 兜底。
|
|
144
|
+
*/
|
|
145
|
+
export const REPLACES_RUNTIME: ReadonlySet<string> = new Set(["reload", "update", "new", "quit", "clone"]);
|
|
146
|
+
|
|
95
147
|
/** pi built-ins that the extension API cannot reach at all. */
|
|
96
148
|
export const HOST_ONLY: ReadonlySet<string> = new Set([
|
|
97
149
|
"settings",
|
|
@@ -184,6 +236,14 @@ const allowed = (cfg: CommandConfig, name: string): boolean => {
|
|
|
184
236
|
|
|
185
237
|
/** Route one slash message. Returns "text" when it should be treated as chat. */
|
|
186
238
|
export async function routeSlash(deps: RouteDeps, text: string): Promise<RouteResult> {
|
|
239
|
+
// 0. `!` / `!!`:主机前端的 shell 快捷方式,扩展 API 够不到 → 明确回执,别当聊天丢给模型
|
|
240
|
+
const bang = parseBangCommand(text);
|
|
241
|
+
if (bang) {
|
|
242
|
+
deps.log("[cmd] 收到不支持的 shell 前缀:", bang.prefix, bang.command.slice(0, 60));
|
|
243
|
+
await deps.reply(bangNotice(bang));
|
|
244
|
+
return "handled";
|
|
245
|
+
}
|
|
246
|
+
|
|
187
247
|
const parsed = parseSlash(text);
|
|
188
248
|
if (!parsed) return "text";
|
|
189
249
|
const { name } = parsed;
|