@zxh126/pi-qqbot 2.3.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/approver.ts CHANGED
@@ -1,174 +1,174 @@
1
- /**
2
- * QQ 远程审批:
3
- * - 每个待审批请求生成编号,发到 QQ,等用户回复「同意/拒绝 N」或「停止」。
4
- * - register 返回一个 Promise,QQ 回复后 resolve 决策。
5
- * - remember 记忆:会话内同意/拒绝某工具后,同类工具不再询问。
6
- * - 自动同意(「以后都同意」):会话级开关,开启后本会话内所有需审批的工具直接放行,
7
- * 直到 /new、新会话或回复「取消自动同意」为止(不跨会话、不写盘)。
8
- */
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
- }
19
-
20
- interface PendingApproval {
21
- summary: string;
22
- timer: ReturnType<typeof setTimeout>;
23
- resolve: (decision: ApproveDecision) => void;
24
- }
25
-
26
- /**
27
- * 「以后都同意」类表达。
28
- *
29
- * 这些正则全部要求**整句匹配**(parseReply 里先归一化再卡长度):
30
- * 用户把审批提示整段引回来、或写了「你这句话里说『取消自动同意』」时,
31
- * 不能把文案里的词当成指令执行。
32
- */
33
- const RE_ALWAYS =
34
- /^(?:(?:以后|之后|今后|一直|全都|全部|都)\s*(?:都)?\s*(?:同意|允许|批准|放行)|别再(?:问|烦|提示)了?|always\s*(?:allow|yes))$/i;
35
- /** 「取消自动同意」类表达(含「以后都不同意」),同样要求整句 */
36
- const RE_REVOKE =
37
- /^(?:(?:取消|关闭|结束|停止)\s*自动同意|恢复审批|(?:以后|之后|今后|都|全部)\s*(?:都)?\s*不(?:同意|允许|批准|放行))$/;
38
- /** 「停止」终止本轮(index.ts 里同时会 abort 当前 run) */
39
- const RE_TERMINATE = /^停止$/;
40
- /** 带或不带编号的批准/拒绝 */
41
- const RE_DECIDE = /^(同意|批准|放行|允许|allow|拒绝|驳回|deny)\s*(\d+)?$/i;
42
-
43
- /** 整句长度上限:超过它就是聊天(比如引用了审批提示),不是审批回复 */
44
- const MAX_REPLY_CHARS = 12;
45
-
46
- /** 去掉空白与常见标点/引号,把「同意 2。」和「\"同意2\"」归一成「同意2」 */
47
- function normalizeReply(text: string): string {
48
- return String(text ?? "")
49
- .replace(/[\s\u3000]+/g, "")
50
- .replace(/[,。!?、,.!?;;::…~~"'“”‘’「」『』()()\[\]【】]/g, "");
51
- }
52
-
53
- export class Approver {
54
- private pending = new Map<number, PendingApproval>();
55
- private rememberMap = new Map<string, ApproveDecision>();
56
- private nextSeq = 1;
57
- private autoAllow = false;
58
- private timeoutMs: number;
59
-
60
- constructor(timeoutMs: number) {
61
- this.timeoutMs = timeoutMs;
62
- }
63
-
64
- /** 会话级自动同意是否开启 */
65
- get autoAllowed(): boolean {
66
- return this.autoAllow;
67
- }
68
-
69
- /** 待审批请求数量 */
70
- get pendingCount(): number {
71
- return this.pending.size;
72
- }
73
-
74
- /** 会话内是否已记住某工具的决策 */
75
- isRemembered(toolName: string): ApproveDecision | undefined {
76
- return this.rememberMap.get(toolName);
77
- }
78
-
79
- register(
80
- summary: string,
81
- onTimeout?: () => void,
82
- ): { seq: number; done: Promise<ApproveDecision> } {
83
- const seq = this.nextSeq++;
84
- const done = new Promise<ApproveDecision>((resolve) => {
85
- const timer = setTimeout(() => {
86
- this.pending.delete(seq);
87
- resolve("timeout");
88
- onTimeout?.();
89
- }, this.timeoutMs);
90
- this.pending.set(seq, { summary, timer, resolve });
91
- });
92
- return { seq, done };
93
- }
94
-
95
- /**
96
- * 审批提示。默认两三行就够:用户反馈过「太长」;
97
- * strict 用于每次都要单独确认的工具(发文件),所以不提「以后都同意」。
98
- */
99
- render(seq: number, opts: { strict?: boolean } = {}): string {
100
- const item = this.pending.get(seq);
101
- const hint = opts.strict
102
- ? `回「同意 ${seq}」或「拒绝 ${seq}」,需单独确认。`
103
- : `回「同意 ${seq}」或「拒绝 ${seq}」;想全部放行回「以后都同意」。`;
104
- return [`[审批 #${seq}] ${item?.summary ?? ""}`, hint].join("\n");
105
- }
106
-
107
- /** 最早(编号最小)的待审批请求 */
108
- oldestSeq(): number | undefined {
109
- let oldest: number | undefined;
110
- for (const seq of this.pending.keys()) {
111
- if (oldest === undefined || seq < oldest) oldest = seq;
112
- }
113
- return oldest;
114
- }
115
-
116
- parseReply(text: string): ParsedReply | null {
117
- const raw = String(text ?? "").trim();
118
- if (!raw) return null;
119
- const t = normalizeReply(raw);
120
- if (!t || t.length > MAX_REPLY_CHARS) return null;
121
- if (RE_REVOKE.test(t)) return { decision: "revoke" };
122
- if (RE_ALWAYS.test(t)) return { decision: "always" };
123
- if (RE_TERMINATE.test(t)) return { decision: "terminate" };
124
- const m = RE_DECIDE.exec(t);
125
- if (!m) return null;
126
- const allow = /^(同意|批准|放行|允许|allow)$/i.test(m[1]);
127
- return { decision: allow ? "allow" : "block", seq: m[2] ? Number(m[2]) : undefined };
128
- }
129
-
130
- confirm(seq: number, decision: ApproveDecision): boolean {
131
- const item = this.pending.get(seq);
132
- if (!item) return false;
133
- clearTimeout(item.timer);
134
- this.pending.delete(seq);
135
- item.resolve(decision);
136
- return true;
137
- }
138
-
139
- /** 放行所有待审批请求(开启自动同意时用) */
140
- allowAll(): number {
141
- return this.resolveAll("allow");
142
- }
143
-
144
- /** 终止所有待审批请求(「停止」) */
145
- terminateAll(): number {
146
- return this.resolveAll("terminate");
147
- }
148
-
149
- /** 开启/关闭会话级自动同意 */
150
- setAutoAllow(on: boolean): void {
151
- this.autoAllow = on;
152
- }
153
-
154
- remember(toolName: string, decision: ApproveDecision) {
155
- this.rememberMap.set(toolName, decision);
156
- }
157
-
158
- /** 清空会话状态(remember 记忆 + 自动同意开关) */
159
- reset() {
160
- this.rememberMap.clear();
161
- this.autoAllow = false;
162
- }
163
-
164
- private resolveAll(decision: ApproveDecision): number {
165
- let count = 0;
166
- for (const [seq, item] of [...this.pending]) {
167
- clearTimeout(item.timer);
168
- this.pending.delete(seq);
169
- item.resolve(decision);
170
- count++;
171
- }
172
- return count;
173
- }
174
- }
1
+ /**
2
+ * QQ 远程审批:
3
+ * - 每个待审批请求生成编号,发到 QQ,等用户回复「同意/拒绝 N」或「停止」。
4
+ * - register 返回一个 Promise,QQ 回复后 resolve 决策。
5
+ * - remember 记忆:会话内同意/拒绝某工具后,同类工具不再询问。
6
+ * - 自动同意(「以后都同意」):会话级开关,开启后本会话内所有需审批的工具直接放行,
7
+ * 直到 /new、新会话或回复「取消自动同意」为止(不跨会话、不写盘)。
8
+ */
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
+ }
19
+
20
+ interface PendingApproval {
21
+ summary: string;
22
+ timer: ReturnType<typeof setTimeout>;
23
+ resolve: (decision: ApproveDecision) => void;
24
+ }
25
+
26
+ /**
27
+ * 「以后都同意」类表达。
28
+ *
29
+ * 这些正则全部要求**整句匹配**(parseReply 里先归一化再卡长度):
30
+ * 用户把审批提示整段引回来、或写了「你这句话里说『取消自动同意』」时,
31
+ * 不能把文案里的词当成指令执行。
32
+ */
33
+ const RE_ALWAYS =
34
+ /^(?:(?:以后|之后|今后|一直|全都|全部|都)\s*(?:都)?\s*(?:同意|允许|批准|放行)|别再(?:问|烦|提示)了?|always\s*(?:allow|yes))$/i;
35
+ /** 「取消自动同意」类表达(含「以后都不同意」),同样要求整句 */
36
+ const RE_REVOKE =
37
+ /^(?:(?:取消|关闭|结束|停止)\s*自动同意|恢复审批|(?:以后|之后|今后|都|全部)\s*(?:都)?\s*不(?:同意|允许|批准|放行))$/;
38
+ /** 「停止」终止本轮(index.ts 里同时会 abort 当前 run) */
39
+ const RE_TERMINATE = /^停止$/;
40
+ /** 带或不带编号的批准/拒绝 */
41
+ const RE_DECIDE = /^(同意|批准|放行|允许|allow|拒绝|驳回|deny)\s*(\d+)?$/i;
42
+
43
+ /** 整句长度上限:超过它就是聊天(比如引用了审批提示),不是审批回复 */
44
+ const MAX_REPLY_CHARS = 12;
45
+
46
+ /** 去掉空白与常见标点/引号,把「同意 2。」和「\"同意2\"」归一成「同意2」 */
47
+ function normalizeReply(text: string): string {
48
+ return String(text ?? "")
49
+ .replace(/[\s\u3000]+/g, "")
50
+ .replace(/[,。!?、,.!?;;::…~~"'“”‘’「」『』()()\[\]【】]/g, "");
51
+ }
52
+
53
+ export class Approver {
54
+ private pending = new Map<number, PendingApproval>();
55
+ private rememberMap = new Map<string, ApproveDecision>();
56
+ private nextSeq = 1;
57
+ private autoAllow = false;
58
+ private timeoutMs: number;
59
+
60
+ constructor(timeoutMs: number) {
61
+ this.timeoutMs = timeoutMs;
62
+ }
63
+
64
+ /** 会话级自动同意是否开启 */
65
+ get autoAllowed(): boolean {
66
+ return this.autoAllow;
67
+ }
68
+
69
+ /** 待审批请求数量 */
70
+ get pendingCount(): number {
71
+ return this.pending.size;
72
+ }
73
+
74
+ /** 会话内是否已记住某工具的决策 */
75
+ isRemembered(toolName: string): ApproveDecision | undefined {
76
+ return this.rememberMap.get(toolName);
77
+ }
78
+
79
+ register(
80
+ summary: string,
81
+ onTimeout?: () => void,
82
+ ): { seq: number; done: Promise<ApproveDecision> } {
83
+ const seq = this.nextSeq++;
84
+ const done = new Promise<ApproveDecision>((resolve) => {
85
+ const timer = setTimeout(() => {
86
+ this.pending.delete(seq);
87
+ resolve("timeout");
88
+ onTimeout?.();
89
+ }, this.timeoutMs);
90
+ this.pending.set(seq, { summary, timer, resolve });
91
+ });
92
+ return { seq, done };
93
+ }
94
+
95
+ /**
96
+ * 审批提示。默认两三行就够:用户反馈过「太长」;
97
+ * strict 用于每次都要单独确认的工具(发文件),所以不提「以后都同意」。
98
+ */
99
+ render(seq: number, opts: { strict?: boolean } = {}): string {
100
+ const item = this.pending.get(seq);
101
+ const hint = opts.strict
102
+ ? `回「同意 ${seq}」或「拒绝 ${seq}」,需单独确认。`
103
+ : `回「同意 ${seq}」或「拒绝 ${seq}」;想全部放行回「以后都同意」。`;
104
+ return [`[审批 #${seq}] ${item?.summary ?? ""}`, hint].join("\n");
105
+ }
106
+
107
+ /** 最早(编号最小)的待审批请求 */
108
+ oldestSeq(): number | undefined {
109
+ let oldest: number | undefined;
110
+ for (const seq of this.pending.keys()) {
111
+ if (oldest === undefined || seq < oldest) oldest = seq;
112
+ }
113
+ return oldest;
114
+ }
115
+
116
+ parseReply(text: string): ParsedReply | null {
117
+ const raw = String(text ?? "").trim();
118
+ if (!raw) return null;
119
+ const t = normalizeReply(raw);
120
+ if (!t || t.length > MAX_REPLY_CHARS) return null;
121
+ if (RE_REVOKE.test(t)) return { decision: "revoke" };
122
+ if (RE_ALWAYS.test(t)) return { decision: "always" };
123
+ if (RE_TERMINATE.test(t)) return { decision: "terminate" };
124
+ const m = RE_DECIDE.exec(t);
125
+ if (!m) return null;
126
+ const allow = /^(同意|批准|放行|允许|allow)$/i.test(m[1]);
127
+ return { decision: allow ? "allow" : "block", seq: m[2] ? Number(m[2]) : undefined };
128
+ }
129
+
130
+ confirm(seq: number, decision: ApproveDecision): boolean {
131
+ const item = this.pending.get(seq);
132
+ if (!item) return false;
133
+ clearTimeout(item.timer);
134
+ this.pending.delete(seq);
135
+ item.resolve(decision);
136
+ return true;
137
+ }
138
+
139
+ /** 放行所有待审批请求(开启自动同意时用) */
140
+ allowAll(): number {
141
+ return this.resolveAll("allow");
142
+ }
143
+
144
+ /** 终止所有待审批请求(「停止」) */
145
+ terminateAll(): number {
146
+ return this.resolveAll("terminate");
147
+ }
148
+
149
+ /** 开启/关闭会话级自动同意 */
150
+ setAutoAllow(on: boolean): void {
151
+ this.autoAllow = on;
152
+ }
153
+
154
+ remember(toolName: string, decision: ApproveDecision) {
155
+ this.rememberMap.set(toolName, decision);
156
+ }
157
+
158
+ /** 清空会话状态(remember 记忆 + 自动同意开关) */
159
+ reset() {
160
+ this.rememberMap.clear();
161
+ this.autoAllow = false;
162
+ }
163
+
164
+ private resolveAll(decision: ApproveDecision): number {
165
+ let count = 0;
166
+ for (const [seq, item] of [...this.pending]) {
167
+ clearTimeout(item.timer);
168
+ this.pending.delete(seq);
169
+ item.resolve(decision);
170
+ count++;
171
+ }
172
+ return count;
173
+ }
174
+ }