@zxh126/pi-qqbot 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,26 +1,38 @@
1
1
  # pi-qqbot
2
2
 
3
- 把 [Pi](https://pi.dev) coding agent 接入 QQ 单聊的桥接插件:在 QQ 里和 AI 对话,驱动 pi 干活。
3
+ Bridge the **[Pi](https://pi.dev) coding agent** into QQ private (C2C) chats: send a message from QQ and let the agent work on the machine where pi runs — with remote approval before it executes dangerous tools.
4
4
 
5
- > 单会话版:所有 QQ 用户共用同一个 pi 会话(v1 设计,多用户多会话后续用 SDK 方案实现)。
5
+ **English** | [简体中文](README.zh-CN.md)
6
6
 
7
- ## 功能
7
+ > **Single-session design (v1):** every QQ user shares one pi session. Multi-user / multi-session support is planned on top of the pi SDK.
8
8
 
9
- - **对话驱动 pi**:QQ 单聊消息送入当前 pi 会话,AI 回复发回 QQ。
10
- - **QQ 远程审批**:pi 执行 `bash` / `powershell` 等命令类工具时,先在 QQ 里发「同意 N / 拒绝 N」请求,用户回复后放行/拦截。
11
- - **单实例锁**:多个 pi 实例只会有「宿主」连接 QQ,其余待命,宿主退出后自动接管。
12
- - **内置指令**:`/new` 重置、`/status` 状态、`/help` 帮助。
9
+ ## Features
13
10
 
14
- ## 安装
11
+ | Feature | Description |
12
+ |---|---|
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
+ | **Remote approval** | Before pi runs `bash` / `powershell` / `write` / `edit` — or any tool annotated `destructiveHint` — the bot asks in QQ first. |
15
+ | **Session-scoped "always allow"** | Reply 「以后都同意」 and every approval request in the **current session** is auto-approved, including the one already waiting. |
16
+ | **Single-instance lock** | Only one pi process hosts the QQ gateway; the others stand by and take over automatically when the host exits. |
17
+ | **Built-in commands** | `/new`, `/status`, `/help`. |
15
18
 
16
- 1. 把本目录放到 `~/.pi/agent/extensions/pi-qqbot/`(或作为 Pi Package:`pi install ./pi-qqbot`)。
17
- 2. 安装依赖:`npm install`。
18
- 3. 创建配置 `~/.pi/agent/pi-qqbot.json`:
19
+ ## Install
20
+
21
+ ```bash
22
+ # from npm
23
+ pi install npm:@zxh126/pi-qqbot
24
+
25
+ # or from a local checkout (dependencies are needed here)
26
+ npm install
27
+ pi install ./pi-qqbot
28
+ ```
29
+
30
+ Then create `~/.pi/agent/pi-qqbot.json`:
19
31
 
20
32
  ```json
21
33
  {
22
- "appId": "你的AppID",
23
- "appSecret": "你的AppSecret",
34
+ "appId": "YOUR_APP_ID",
35
+ "appSecret": "YOUR_APP_SECRET",
24
36
  "sandbox": true,
25
37
  "allowlist": [],
26
38
  "markdownReply": false,
@@ -32,57 +44,73 @@
32
44
  }
33
45
  ```
34
46
 
35
- | 字段 | 默认 | 说明 |
36
- |------|------|------|
37
- | `appId` / `appSecret` | 必填 | QQ 开放平台机器人凭据 |
38
- | `sandbox` | `false` | 未提审上线的 bot 用 `true`(沙箱环境) |
39
- | `allowlist` | `[]` | openid 白名单,空 = 不限制 |
40
- | `markdownReply` | `false` | QQ 单聊不支持 markdown,保持 `false` |
41
- | `streaming` | `false` | 预留,v1 未启用流式 |
42
- | `approve.tools` | `["bash","powershell","write","edit"]` | 需要 QQ 审批的工具名;另有 `destructiveHint` 注解的工具也会拦截 |
43
- | `approve.remember` | `true` | 会话内记住选择:同意/拒绝后同类工具不再询问 |
44
-
45
- 也可用环境变量 `QQ_BOT_APPID` / `QQ_BOT_APPSECRET` 代替文件凭据。
46
-
47
- ## 使用
48
-
49
- 启动 pi 后,在 QQ 里给机器人发消息,pi 会处理并回复。内置指令:
47
+ | Field | Default | Description |
48
+ |---|---|---|
49
+ | `appId` / `appSecret` | required | Credentials of your QQ Open Platform bot |
50
+ | `sandbox` | `false` | Set `true` while the bot is not yet published (sandbox environment) |
51
+ | `allowlist` | `[]` | openid allowlist; empty means no restriction |
52
+ | `markdownReply` | `false` | QQ private chat does not support markdown, keep `false` |
53
+ | `streaming` | `false` | Reserved, streaming is not enabled in v1 |
54
+ | `approve.tools` | `["bash","powershell","write","edit"]` | Tools that require approval; tools annotated `destructiveHint` are always gated too |
55
+ | `approve.remember` | `true` | Remember the decision per tool inside the session (no more prompts for that tool) |
50
56
 
51
- - `/new` — 重置会话(清空当前回复缓冲)
52
- - `/status` — 查看是否空闲
53
- - `/help` — 帮助
57
+ Credentials can also come from the `QQ_BOT_APPID` / `QQ_BOT_APPSECRET` environment variables.
54
58
 
55
- ## 单实例锁
59
+ ## Usage
56
60
 
57
- QQ 网关对同一 bot 只保留一个活跃连接。插件用 `proper-lockfile` 文件锁(`~/.pi/agent/pi-qqbot-<appId>.lock`):
61
+ Start pi, then message the bot in QQ; pi handles it and replies.
58
62
 
59
- - 抢到锁的实例 = 宿主,连接 QQ;
60
- - 其余实例待命,每 5 秒重试;
61
- - 宿主退出或崩溃(30 秒心跳超时)后,待命实例自动接管。
63
+ | Command | Description |
64
+ |---|---|
65
+ | `/new` | Reset: clear the reply buffer, approval memory and session-wide auto-approve |
66
+ | `/status` | Show busy/idle state, auto-approve state and pending approval count |
67
+ | `/help` | Show help |
62
68
 
63
- ## 远程审批
69
+ Anything else is forwarded to pi as a user message.
64
70
 
65
- pi 执行 `bash` / `powershell` / `write` / `edit` 等工具时(或带 `destructiveHint` 注解的工具),会先在 QQ 里发审批请求:
71
+ ## Remote approval
66
72
 
67
- ```
73
+ ```text
68
74
  [审批 #1] bash: rm -rf node_modules
69
75
  回复「同意 1」批准,「拒绝 1」拒绝,「停止」终止本轮。
76
+ 要本会话内全部放行,回复「以后都同意」(可用「取消自动同意」关闭)。
70
77
  ```
71
78
 
72
- - 回复「同意 N」/「拒绝 N」:放行/拦截对应工具调用;
73
- - 回复「停止」:终止所有待审批请求;
74
- - `approve.remember: true` 时,本次会话内同类工具记住选择,不再重复询问(`/new` 重置记忆);
75
- - 超时(10 分钟)自动拒绝并通知。
79
+ Reply vocabulary (approval keywords are Chinese, with English aliases):
80
+
81
+ | Intent | Reply |
82
+ |---|---|
83
+ | Approve one request | `同意 1` / `allow 1` — a bare `同意` / `allow` applies to the oldest pending request |
84
+ | Reject one request | `拒绝 1` / `deny 1` — a bare `拒绝` / `deny` applies to the oldest pending request |
85
+ | Abort the pending requests | `停止` |
86
+ | Allow everything in this session | `以后都同意`, `一直同意`, `全部放行`, `别再问了`, `always allow` |
87
+ | Back to per-request approval | `取消自动同意`, `恢复审批`, `以后都不同意` |
88
+
89
+ Semantics:
90
+
91
+ - **Session scope only.** The auto-approve switch lives in memory and lasts until `/new`, a new pi session, or one of the "back to per-request approval" replies. Nothing is persisted, nothing survives a restart.
92
+ - The switch is an **explicit user instruction**, so it is **not** affected by `approve.remember`.
93
+ - `approve.remember: true` additionally remembers the decision **per tool** for the session: after approving `bash` once, later `bash` calls are not asked again (`/new` clears it).
94
+ - Requests time out after **10 minutes** and are rejected with a notification. A timeout is **not** remembered as a rejection — the tool is asked again next time.
95
+ - Approval text that arrives while no request is pending is forwarded to pi as a normal message.
96
+
97
+ ## Single-instance lock
98
+
99
+ The QQ gateway keeps only one active connection per bot. The extension uses a `proper-lockfile` lock at `~/.pi/agent/pi-qqbot-<appId>.lock`:
100
+
101
+ - the process that acquires the lock becomes the host and connects to QQ;
102
+ - other processes stand by and retry every 5 seconds;
103
+ - when the host exits or crashes (30 second heartbeat timeout), a standby process takes over.
76
104
 
77
- ## 前置条件
105
+ ## Prerequisites
78
106
 
79
- 1. 在 [QQ 开放平台](https://q.qq.com/) 创建机器人,拿到 **AppID** 与 **AppSecret**。
80
- 2. 未上线阶段配置**沙箱单聊账号**,并把 `sandbox` 设为 `true`。
81
- 3. 正式环境填写服务器公网出口 IP 白名单。
107
+ 1. Create a bot on the [QQ Open Platform](https://q.qq.com/) and copy its **AppID** / **AppSecret**.
108
+ 2. Before the bot is published, configure a **sandbox C2C account** and set `sandbox` to `true`.
109
+ 3. In production, add the server's public egress IP to the bot's allowlist.
82
110
 
83
- ## 日志
111
+ ## Logs
84
112
 
85
- 调试日志写入 `~/.pi/agent/pi-qqbot.log`。
113
+ Debug logs are written to `~/.pi/agent/pi-qqbot.log`.
86
114
 
87
115
  ## License
88
116
 
@@ -0,0 +1,117 @@
1
+ # pi-qqbot
2
+
3
+ 把 **[Pi](https://pi.dev) coding agent** 接入 QQ 单聊的桥接插件:在 QQ 里发一句话,让 AI 在运行 pi 的那台机器上干活 —— 执行危险工具前会先在 QQ 里向你申请审批。
4
+
5
+ [English](README.md) | **简体中文**
6
+
7
+ > **单会话设计(v1)**:所有 QQ 用户共用同一个 pi 会话。多用户/多会话后续基于 pi SDK 实现。
8
+
9
+ ## 功能
10
+
11
+ | 功能 | 说明 |
12
+ |---|---|
13
+ | **对话驱动 agent** | QQ 单聊消息送入当前 pi 会话,AI 的回复发回 QQ(长文本自动分片)。 |
14
+ | **远程审批** | pi 执行 `bash` / `powershell` / `write` / `edit`,或任何带 `destructiveHint` 注解的工具前,先在 QQ 里申请审批。 |
15
+ | **会话级「以后都同意」** | 回复「以后都同意」,**当前会话**内所有审批请求直接放行,连已经挂起的那条也一起放行。 |
16
+ | **单实例锁** | 只有一个 pi 进程作为宿主连接 QQ,其余待命,宿主退出后自动接管。 |
17
+ | **内置指令** | `/new`、`/status`、`/help`。 |
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ # 从 npm 安装
23
+ pi install npm:@zxh126/pi-qqbot
24
+
25
+ # 或从本地目录安装(需要先装依赖)
26
+ npm install
27
+ pi install ./pi-qqbot
28
+ ```
29
+
30
+ 然后创建 `~/.pi/agent/pi-qqbot.json`:
31
+
32
+ ```json
33
+ {
34
+ "appId": "你的AppID",
35
+ "appSecret": "你的AppSecret",
36
+ "sandbox": true,
37
+ "allowlist": [],
38
+ "markdownReply": false,
39
+ "streaming": false,
40
+ "approve": {
41
+ "tools": ["bash", "powershell", "write", "edit"],
42
+ "remember": true
43
+ }
44
+ }
45
+ ```
46
+
47
+ | 字段 | 默认 | 说明 |
48
+ |---|---|---|
49
+ | `appId` / `appSecret` | 必填 | QQ 开放平台机器人凭据 |
50
+ | `sandbox` | `false` | 未提审上线的 bot 用 `true`(沙箱环境) |
51
+ | `allowlist` | `[]` | openid 白名单,空 = 不限制 |
52
+ | `markdownReply` | `false` | QQ 单聊不支持 markdown,保持 `false` |
53
+ | `streaming` | `false` | 预留,v1 未启用流式 |
54
+ | `approve.tools` | `["bash","powershell","write","edit"]` | 需要审批的工具名;带 `destructiveHint` 注解的工具同样会拦截 |
55
+ | `approve.remember` | `true` | 会话内按工具记住决策(同类工具不再重复询问) |
56
+
57
+ 也可用环境变量 `QQ_BOT_APPID` / `QQ_BOT_APPSECRET` 代替文件里的凭据。
58
+
59
+ ## 使用
60
+
61
+ 启动 pi 后,在 QQ 里给机器人发消息,pi 会处理并回复。
62
+
63
+ | 指令 | 说明 |
64
+ |---|---|
65
+ | `/new` | 重置:清空回复缓冲、审批记忆与自动同意开关 |
66
+ | `/status` | 查看忙闲状态、自动同意状态、待审批数量 |
67
+ | `/help` | 帮助 |
68
+
69
+ 其余文本都作为用户消息交给 pi。
70
+
71
+ ## 远程审批
72
+
73
+ ```text
74
+ [审批 #1] bash: rm -rf node_modules
75
+ 回复「同意 1」批准,「拒绝 1」拒绝,「停止」终止本轮。
76
+ 要本会话内全部放行,回复「以后都同意」(可用「取消自动同意」关闭)。
77
+ ```
78
+
79
+ 回复用语(审批关键词以中文为主,也支持英文别名):
80
+
81
+ | 意图 | 回复 |
82
+ |---|---|
83
+ | 批准某条 | `同意 1` / `allow 1`;裸 `同意` / `allow` 作用于最早的一条待审批 |
84
+ | 拒绝某条 | `拒绝 1` / `deny 1`;裸 `拒绝` / `deny` 作用于最早的一条待审批 |
85
+ | 终止当前待审批 | `停止` |
86
+ | 本会话全部放行 | `以后都同意`、`一直同意`、`全部放行`、`别再问了`、`always allow` |
87
+ | 恢复逐条审批 | `取消自动同意`、`恢复审批`、`以后都不同意` |
88
+
89
+ 语义说明:
90
+
91
+ - **仅当前会话有效**:自动同意开关只存在于内存,`/new`、pi 新会话,或回复「取消自动同意」类用语后立即失效;不写盘、不跨进程保留。
92
+ - 该开关是**用户的显式指令**,**不受 `approve.remember` 配置限制**。
93
+ - `approve.remember: true` 时会额外**按工具名**记住决策:同意过一次 `bash`,本会话后续 `bash` 不再询问(`/new` 清除)。
94
+ - 审批请求 **10 分钟**超时后自动拒绝并通知;超时**不会**被记成“拒绝”,下次仍会询问。
95
+ - 没有待审批请求时,这类文本会作为普通消息转交给 pi。
96
+
97
+ ## 单实例锁
98
+
99
+ QQ 网关对同一 bot 只保留一个活跃连接。插件用 `proper-lockfile` 文件锁(`~/.pi/agent/pi-qqbot-<appId>.lock`):
100
+
101
+ - 抢到锁的进程 = 宿主,连接 QQ;
102
+ - 其余进程待命,每 5 秒重试;
103
+ - 宿主退出或崩溃(30 秒心跳超时)后,待命进程自动接管。
104
+
105
+ ## 前置条件
106
+
107
+ 1. 在 [QQ 开放平台](https://q.qq.com/) 创建机器人,拿到 **AppID** 与 **AppSecret**。
108
+ 2. 未上线阶段配置**沙箱单聊账号**,并把 `sandbox` 设为 `true`。
109
+ 3. 正式环境填写服务器公网出口 IP 白名单。
110
+
111
+ ## 日志
112
+
113
+ 调试日志写入 `~/.pi/agent/pi-qqbot.log`。
114
+
115
+ ## License
116
+
117
+ MIT
package/approver.ts CHANGED
@@ -3,8 +3,19 @@
3
3
  * - 每个待审批请求生成编号,发到 QQ,等用户回复「同意/拒绝 N」或「停止」。
4
4
  * - register 返回一个 Promise,QQ 回复后 resolve 决策。
5
5
  * - remember 记忆:会话内同意/拒绝某工具后,同类工具不再询问。
6
+ * - 自动同意(「以后都同意」):会话级开关,开启后本会话内所有需审批的工具直接放行,
7
+ * 直到 /new、新会话或回复「取消自动同意」为止(不跨会话、不写盘)。
6
8
  */
7
- export type ApproveDecision = "allow" | "block" | "terminate";
9
+ export type ApproveDecision = "allow" | "block" | "terminate" | "timeout";
10
+
11
+ /** parseReply 的解析结果:前四种对应具体动作,always/revoke 操作会话级自动同意 */
12
+ export type ParsedDecision = "allow" | "block" | "terminate" | "always" | "revoke";
13
+
14
+ export interface ParsedReply {
15
+ decision: ParsedDecision;
16
+ /** 缺省表示「作用于最早的一条待审批」,没有待审批时按普通消息处理 */
17
+ seq?: number;
18
+ }
8
19
 
9
20
  interface PendingApproval {
10
21
  summary: string;
@@ -12,12 +23,37 @@ interface PendingApproval {
12
23
  resolve: (decision: ApproveDecision) => void;
13
24
  }
14
25
 
26
+ /** 「以后都同意」类表达 */
27
+ const RE_ALWAYS =
28
+ /(以后|之后|今后|一直|全都|全部|都)\s*(同意|允许|批准|放行)|别再(问|烦|提示)|always\s*(allow|yes)/i;
29
+ /** 「取消自动同意」类表达(含「以后都不同意」) */
30
+ const RE_REVOKE =
31
+ /(取消|关闭|结束|停止)\s*自动同意|^恢复审批$|(以后|之后|今后|都|全部)\s*不(同意|允许|批准|放行)/;
32
+ /** 「停止」终止本轮 */
33
+ const RE_TERMINATE = /^停止$/;
34
+ /** 带或不带编号的批准/拒绝 */
35
+ const RE_DECIDE = /^(同意|批准|放行|允许|allow|拒绝|驳回|deny)\s*(\d+)?$/i;
36
+
15
37
  export class Approver {
16
38
  private pending = new Map<number, PendingApproval>();
17
39
  private rememberMap = new Map<string, ApproveDecision>();
18
40
  private nextSeq = 1;
41
+ private autoAllow = false;
42
+ private timeoutMs: number;
43
+
44
+ constructor(timeoutMs: number) {
45
+ this.timeoutMs = timeoutMs;
46
+ }
47
+
48
+ /** 会话级自动同意是否开启 */
49
+ get autoAllowed(): boolean {
50
+ return this.autoAllow;
51
+ }
19
52
 
20
- constructor(private timeoutMs: number) {}
53
+ /** 待审批请求数量 */
54
+ get pendingCount(): number {
55
+ return this.pending.size;
56
+ }
21
57
 
22
58
  /** 会话内是否已记住某工具的决策 */
23
59
  isRemembered(toolName: string): ApproveDecision | undefined {
@@ -32,7 +68,7 @@ export class Approver {
32
68
  const done = new Promise<ApproveDecision>((resolve) => {
33
69
  const timer = setTimeout(() => {
34
70
  this.pending.delete(seq);
35
- resolve("block");
71
+ resolve("timeout");
36
72
  onTimeout?.();
37
73
  }, this.timeoutMs);
38
74
  this.pending.set(seq, { summary, timer, resolve });
@@ -45,15 +81,29 @@ export class Approver {
45
81
  return [
46
82
  `[审批 #${seq}] ${item?.summary ?? ""}`,
47
83
  `回复「同意 ${seq}」批准,「拒绝 ${seq}」拒绝,「停止」终止本轮。`,
84
+ `要本会话内全部放行,回复「以后都同意」(可用「取消自动同意」关闭)。`,
48
85
  ].join("\n");
49
86
  }
50
87
 
51
- parseReply(text: string): { decision: ApproveDecision; seq?: number } | null {
88
+ /** 最早(编号最小)的待审批请求 */
89
+ oldestSeq(): number | undefined {
90
+ let oldest: number | undefined;
91
+ for (const seq of this.pending.keys()) {
92
+ if (oldest === undefined || seq < oldest) oldest = seq;
93
+ }
94
+ return oldest;
95
+ }
96
+
97
+ parseReply(text: string): ParsedReply | null {
52
98
  const t = text.trim();
53
- if (/^停止$/.test(t)) return { decision: "terminate" };
54
- const m = /^(同意|拒绝)\s*(\d+)$/.exec(t);
99
+ if (!t) return null;
100
+ if (RE_REVOKE.test(t)) return { decision: "revoke" };
101
+ if (RE_ALWAYS.test(t)) return { decision: "always" };
102
+ if (RE_TERMINATE.test(t)) return { decision: "terminate" };
103
+ const m = RE_DECIDE.exec(t);
55
104
  if (!m) return null;
56
- return { decision: m[1] === "同意" ? "allow" : "block", seq: Number(m[2]) };
105
+ const allow = /^(同意|批准|放行|允许|allow)$/i.test(m[1]);
106
+ return { decision: allow ? "allow" : "block", seq: m[2] ? Number(m[2]) : undefined };
57
107
  }
58
108
 
59
109
  confirm(seq: number, decision: ApproveDecision): boolean {
@@ -65,23 +115,39 @@ export class Approver {
65
115
  return true;
66
116
  }
67
117
 
118
+ /** 放行所有待审批请求(开启自动同意时用) */
119
+ allowAll(): number {
120
+ return this.resolveAll("allow");
121
+ }
122
+
68
123
  /** 终止所有待审批请求(「停止」) */
69
124
  terminateAll(): number {
70
- let count = 0;
71
- for (const [seq, item] of [...this.pending]) {
72
- clearTimeout(item.timer);
73
- this.pending.delete(seq);
74
- item.resolve("terminate");
75
- count++;
76
- }
77
- return count;
125
+ return this.resolveAll("terminate");
126
+ }
127
+
128
+ /** 开启/关闭会话级自动同意 */
129
+ setAutoAllow(on: boolean): void {
130
+ this.autoAllow = on;
78
131
  }
79
132
 
80
133
  remember(toolName: string, decision: ApproveDecision) {
81
134
  this.rememberMap.set(toolName, decision);
82
135
  }
83
136
 
137
+ /** 清空会话状态(remember 记忆 + 自动同意开关) */
84
138
  reset() {
85
139
  this.rememberMap.clear();
140
+ this.autoAllow = false;
141
+ }
142
+
143
+ private resolveAll(decision: ApproveDecision): number {
144
+ let count = 0;
145
+ for (const [seq, item] of [...this.pending]) {
146
+ clearTimeout(item.timer);
147
+ this.pending.delete(seq);
148
+ item.resolve(decision);
149
+ count++;
150
+ }
151
+ return count;
86
152
  }
87
153
  }
package/config.ts CHANGED
@@ -4,7 +4,7 @@ import { CONFIG_PATH } from "./constants";
4
4
  export interface ApproveConfig {
5
5
  /** 需要 QQ 审批的工具名(如 bash/powershell/write/edit) */
6
6
  tools: string[];
7
- /** 会话内记住选择:同意/拒绝后同类工具不再询问 */
7
+ /** 会话内记住选择:同意/拒绝后同类工具不再询问(「以后都同意」不受此开关限制) */
8
8
  remember: boolean;
9
9
  }
10
10
 
package/index.ts CHANGED
@@ -80,6 +80,12 @@ export default function (pi: ExtensionAPI) {
80
80
  const needsApproval = cfg.approve.tools.includes(toolName) || destructive;
81
81
  if (!needsApproval) return; // 放行
82
82
 
83
+ // 会话级自动同意(用户回复过「以后都同意」):本会话内直接放行,不落盘、不跨会话
84
+ if (state.approver.autoAllowed) {
85
+ log("[approve] 自动同意放行:", toolName);
86
+ return;
87
+ }
88
+
83
89
  // 会话内已记住的决策
84
90
  const remembered = cfg.approve.remember ? state.approver.isRemembered(toolName) : undefined;
85
91
  if (remembered === "allow") return; // 已记住放行
@@ -104,7 +110,12 @@ export default function (pi: ExtensionAPI) {
104
110
  log("[approve] 用户停止");
105
111
  return { block: true, terminate: true, reason: "QQ 用户停止" };
106
112
  }
107
- // block
113
+ if (decision === "timeout") {
114
+ // 超时不写 remember:一次没回复不该永久封死该工具
115
+ log("[approve] 审批超时 #", seq);
116
+ return { block: true, reason: `QQ 审批超时(#${seq}):${summary}` };
117
+ }
118
+ // block(用户显式拒绝)
108
119
  if (cfg.approve.remember) state.approver.remember(toolName, "block");
109
120
  log("[approve] 用户拒绝 #", seq);
110
121
  return { block: true, reason: `QQ 用户拒绝了 ${summary}` };
@@ -118,6 +129,8 @@ export default function (pi: ExtensionAPI) {
118
129
  return;
119
130
  }
120
131
  state.cfg = cfg;
132
+ // 新会话:清空 remember 记忆与自动同意开关(自动同意只在当前会话有效)
133
+ state.approver.reset();
121
134
  log("[setup] 配置已加载 sandbox=", cfg.sandbox);
122
135
 
123
136
  const restBase = cfg.sandbox ? REST_BASE_SANDBOX : REST_BASE_PROD;
@@ -195,17 +208,52 @@ async function handleMessage(
195
208
  if (parsed) {
196
209
  if (parsed.decision === "terminate") {
197
210
  const n = state.approver.terminateAll();
198
- await replyTo(api, state, msg.openid, `已终止 ${n} 个待审批请求`);
211
+ state.approver.setAutoAllow(false);
212
+ await replyTo(api, state, msg.openid, `已终止 ${n} 个待审批请求,自动同意已关闭`);
199
213
  return;
200
214
  }
201
- const ok = state.approver.confirm(parsed.seq!, parsed.decision);
202
- await replyTo(
203
- api,
204
- state,
205
- msg.openid,
206
- ok ? (parsed.decision === "allow" ? `已批准 #${parsed.seq}` : `已拒绝 #${parsed.seq}`) : `#${parsed.seq} 不存在或已超时`,
207
- );
208
- return;
215
+ if (parsed.decision === "always") {
216
+ // 「以后都同意」= 本会话内全部放行:立刻开启开关并放行当前所有待审批
217
+ state.approver.setAutoAllow(true);
218
+ const n = state.approver.allowAll();
219
+ log("[approve] 开启会话级自动同意,放行", n, "个待审批");
220
+ await replyTo(
221
+ api,
222
+ state,
223
+ msg.openid,
224
+ [
225
+ `已开启自动同意:本会话内需审批的工具全部放行${n > 0 ? `,已放行 ${n} 个待审批请求` : ""}。`,
226
+ "回复「取消自动同意」或 /new 恢复逐条审批。",
227
+ ].join("\n"),
228
+ );
229
+ return;
230
+ }
231
+ if (parsed.decision === "revoke") {
232
+ const was = state.approver.autoAllowed;
233
+ state.approver.setAutoAllow(false);
234
+ log("[approve] 关闭自动同意 was=", was);
235
+ await replyTo(
236
+ api,
237
+ state,
238
+ msg.openid,
239
+ was ? "已关闭自动同意,恢复逐条审批。" : "当前没有开启自动同意,仍需逐条审批。",
240
+ );
241
+ return;
242
+ }
243
+ // allow / block:没写编号时作用于最早的一条待审批
244
+ const seq = parsed.seq ?? state.approver.oldestSeq();
245
+ if (seq !== undefined) {
246
+ const ok = state.approver.confirm(seq, parsed.decision);
247
+ const text = !ok
248
+ ? `#${seq} 不存在或已超时`
249
+ : parsed.decision === "allow"
250
+ ? `已批准 #${seq}`
251
+ : `已拒绝 #${seq}`;
252
+ await replyTo(api, state, msg.openid, text);
253
+ return;
254
+ }
255
+ // 没有待审批:这句「同意/拒绝」当普通消息交给 pi,继续往下走
256
+ log("[msg] 审批类文本但无待审批,按普通消息处理");
209
257
  }
210
258
 
211
259
  // 内置指令
@@ -220,13 +268,22 @@ async function handleMessage(
220
268
  return;
221
269
  }
222
270
  if (cmd === "status") {
223
- await replyTo(api, state, msg.openid, `状态: ${state.busy ? "处理中" : "空闲"}`);
271
+ await replyTo(
272
+ api,
273
+ state,
274
+ msg.openid,
275
+ [
276
+ `状态: ${state.busy ? "处理中" : "空闲"}`,
277
+ `自动同意: ${state.approver.autoAllowed ? "已开启(本会话有效)" : "未开启"}`,
278
+ `待审批: ${state.approver.pendingCount}`,
279
+ ].join("\n"),
280
+ );
224
281
  return;
225
282
  }
226
283
  if (cmd === "new") {
227
284
  state.replyBuffer.reset();
228
285
  state.approver.reset();
229
- await replyTo(api, state, msg.openid, "已重置,下一条消息开启新对话。");
286
+ await replyTo(api, state, msg.openid, "已重置(含审批记忆/自动同意),下一条消息开启新对话。");
230
287
  return;
231
288
  }
232
289
 
package/package.json CHANGED
@@ -1,14 +1,22 @@
1
1
  {
2
2
  "name": "@zxh126/pi-qqbot",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "把 Pi coding agent 接入 QQ 单聊的桥接插件(单会话版)",
5
+ "description": "Bridge the Pi coding agent into QQ private chats, with remote tool-call approval",
6
6
  "license": "MIT",
7
- "keywords": ["pi-package", "qq", "bot"],
7
+ "keywords": ["pi-package", "pi", "qq", "qqbot", "bot", "coding-agent", "approval"],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://gitee.com/zxh126/pi-qqbot.git"
11
+ },
12
+ "homepage": "https://gitee.com/zxh126/pi-qqbot#readme",
13
+ "bugs": {
14
+ "url": "https://gitee.com/zxh126/pi-qqbot/issues"
15
+ },
8
16
  "pi": {
9
17
  "extensions": ["./index.ts"]
10
18
  },
11
- "files": ["*.ts", "qq", "util", "README.md", "LICENSE"],
19
+ "files": ["*.ts", "qq", "util", "README.md", "README.zh-CN.md", "LICENSE"],
12
20
  "dependencies": {
13
21
  "ws": "^8.18.0",
14
22
  "proper-lockfile": "^4.1.2"