dsh-rule-engine 0.5.11 → 0.5.13
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 +30 -99
- package/lib/core/authorization.js +517 -513
- package/lib/core/config.js +73 -66
- package/lib/core/contract.js +41 -0
- package/lib/core/guard-core.js +754 -724
- package/lib/core/intent.js +11 -3
- package/lib/core/label-fingerprint.js +145 -0
- package/lib/core/lexicon.js +72 -72
- package/lib/core/parser.js +111 -103
- package/lib/core/patterns.js +42 -3
- package/lib/core/state.js +324 -301
- package/lib/core/understander.js +221 -150
- package/lib/index.js +1788 -1663
- package/lib/service.js +17 -2
- package/package.json +1 -1
- package/scripts/publish-aptitude-check.mjs +144 -0
- package/scripts/verify-all.mjs +4 -0
|
@@ -1,513 +1,517 @@
|
|
|
1
|
-
// authorization.js - 授权证据结构化匹配(P0:授权关联具体操作,区分询问与授权)
|
|
2
|
-
// 纯函数,可独立测试。
|
|
3
|
-
import { GENERIC_EXEC_TOOLS, pathTarget, commandText, writeTargetPathsFromCommand, extractCopyPaths } from "./patterns.js";
|
|
4
|
-
import { QUESTION_RE } from "./intent.js";
|
|
5
|
-
import {
|
|
6
|
-
APPROVAL_EXEC_RE,
|
|
7
|
-
APPROVAL_WORDS_RE,
|
|
8
|
-
DIRECTIVE_WORDS_RE,
|
|
9
|
-
REJECTION_WORDS_RE,
|
|
10
|
-
PLAN_ONLY_RE,
|
|
11
|
-
ACTION_WORDS_RE
|
|
12
|
-
} from "./lexicon.js";
|
|
13
|
-
|
|
14
|
-
export const AUTH_TTL_MS = 10 * 60 * 1000;
|
|
15
|
-
|
|
16
|
-
// 0.5.11(用户拍板):词表唯一源 = lexicon.js——授权判定所用的许可词/指令词/拒绝词/动作词
|
|
17
|
-
// 一律从那里 import;本文件不再自持定义(原 AUTH_WORDS_RE/DIRECTIVE_RE/APPROVAL_RE/
|
|
18
|
-
// REJECTION_RE/PLAN_ONLY_RE/EXEC_ACTION_RE 删除,同语义四处定义的历史漂移根因清除)。
|
|
19
|
-
|
|
20
|
-
// 授权/批准判定用:许可词(允许/同意/可以/是/好/确认/授权/执行/批准/去吧/开始/ok/yes)
|
|
21
|
-
const AUTH_WORDS_RE = APPROVAL_WORDS_RE;
|
|
22
|
-
// 指令式动作词(用户命令式指令 = 视为授权执行)
|
|
23
|
-
const DIRECTIVE_RE = DIRECTIVE_WORDS_RE;
|
|
24
|
-
// 纯批准词(选项/自由文本判定用)
|
|
25
|
-
const APPROVAL_RE = /(?:允许|同意|可以|是|好|确认|授权|执行|批准|去吧|开始|ok|yes)/i;
|
|
26
|
-
const REJECTION_RE = REJECTION_WORDS_RE;
|
|
27
|
-
|
|
28
|
-
// 0.5.11:执行许可确认词唯一源(lexicon.js APPROVAL_EXEC_RE,组合=许可词[0,16]执行语);
|
|
29
|
-
// 纯"确认方案/理解/方向"不构成(PLAN_ONLY_RE 亦唯一源)。
|
|
30
|
-
|
|
31
|
-
// 选项/自由文本里的明确动作词(用于 ask 结果判定:选择"执行/调整/修复…"即视为批准)
|
|
32
|
-
// 0.5.11:唯一源 = lexicon.js ACTION_WORDS_RE(与 intent 意图判定同表,不再二份)
|
|
33
|
-
const EXEC_ACTION_RE = ACTION_WORDS_RE;
|
|
34
|
-
|
|
35
|
-
const TYPE_HINTS = [
|
|
36
|
-
{ re: /删除|移除|remove|delete/i, type: "delete" },
|
|
37
|
-
{ re: /修改|编辑|替换|写入|写(?:入|文件|作|下|好|完|成)?|改(?:为|成)?|保存|另存为|转化|转换|edit|write|replace/i, type: "write" },
|
|
38
|
-
{ re: /备份|backup/i, type: "backup" },
|
|
39
|
-
{ re: /git\s+(push|commit)|提交|推送/i, type: "git" },
|
|
40
|
-
// 命令类只在有明确命令上下文时推断;"执行/开始/做"等宽泛指令保持 any,避免把普通文件变更误限为 command
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ re:
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
{ re:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
[
|
|
174
|
-
[
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (boundPaths.length === 0) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
parts.push(q);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1
|
+
// authorization.js - 授权证据结构化匹配(P0:授权关联具体操作,区分询问与授权)
|
|
2
|
+
// 纯函数,可独立测试。
|
|
3
|
+
import { GENERIC_EXEC_TOOLS, pathTarget, commandText, writeTargetPathsFromCommand, extractCopyPaths } from "./patterns.js";
|
|
4
|
+
import { QUESTION_RE } from "./intent.js";
|
|
5
|
+
import {
|
|
6
|
+
APPROVAL_EXEC_RE,
|
|
7
|
+
APPROVAL_WORDS_RE,
|
|
8
|
+
DIRECTIVE_WORDS_RE,
|
|
9
|
+
REJECTION_WORDS_RE,
|
|
10
|
+
PLAN_ONLY_RE,
|
|
11
|
+
ACTION_WORDS_RE
|
|
12
|
+
} from "./lexicon.js";
|
|
13
|
+
|
|
14
|
+
export const AUTH_TTL_MS = 10 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
// 0.5.11(用户拍板):词表唯一源 = lexicon.js——授权判定所用的许可词/指令词/拒绝词/动作词
|
|
17
|
+
// 一律从那里 import;本文件不再自持定义(原 AUTH_WORDS_RE/DIRECTIVE_RE/APPROVAL_RE/
|
|
18
|
+
// REJECTION_RE/PLAN_ONLY_RE/EXEC_ACTION_RE 删除,同语义四处定义的历史漂移根因清除)。
|
|
19
|
+
|
|
20
|
+
// 授权/批准判定用:许可词(允许/同意/可以/是/好/确认/授权/执行/批准/去吧/开始/ok/yes)
|
|
21
|
+
const AUTH_WORDS_RE = APPROVAL_WORDS_RE;
|
|
22
|
+
// 指令式动作词(用户命令式指令 = 视为授权执行)
|
|
23
|
+
const DIRECTIVE_RE = DIRECTIVE_WORDS_RE;
|
|
24
|
+
// 纯批准词(选项/自由文本判定用)
|
|
25
|
+
const APPROVAL_RE = /(?:允许|同意|可以|是|好|确认|授权|执行|批准|去吧|开始|ok|yes)/i;
|
|
26
|
+
const REJECTION_RE = REJECTION_WORDS_RE;
|
|
27
|
+
|
|
28
|
+
// 0.5.11:执行许可确认词唯一源(lexicon.js APPROVAL_EXEC_RE,组合=许可词[0,16]执行语);
|
|
29
|
+
// 纯"确认方案/理解/方向"不构成(PLAN_ONLY_RE 亦唯一源)。
|
|
30
|
+
|
|
31
|
+
// 选项/自由文本里的明确动作词(用于 ask 结果判定:选择"执行/调整/修复…"即视为批准)
|
|
32
|
+
// 0.5.11:唯一源 = lexicon.js ACTION_WORDS_RE(与 intent 意图判定同表,不再二份)
|
|
33
|
+
const EXEC_ACTION_RE = ACTION_WORDS_RE;
|
|
34
|
+
|
|
35
|
+
const TYPE_HINTS = [
|
|
36
|
+
{ re: /删除|移除|remove|delete/i, type: "delete" },
|
|
37
|
+
{ re: /修改|编辑|替换|写入|写(?:入|文件|作|下|好|完|成)?|改(?:为|成)?|保存|另存为|转化|转换|edit|write|replace/i, type: "write" },
|
|
38
|
+
{ re: /备份|backup/i, type: "backup" },
|
|
39
|
+
{ re: /git\s+(push|commit)|提交|推送/i, type: "git" },
|
|
40
|
+
// 命令类只在有明确命令上下文时推断;"执行/开始/做"等宽泛指令保持 any,避免把普通文件变更误限为 command
|
|
41
|
+
// 0.5.12(F1 重证):command 规则补 ①引号包裹路径 ②无扩展名可执行文件(tsc/tsdown 等)
|
|
42
|
+
// ③pnpm/yarn/bun 显式收录(此前靠 npm\s+ 子串巧合命中 pnpm,非设计保证)
|
|
43
|
+
{ re: /命令|运行|执行\s*(?:命令|脚本|以下|程序)|run|execute|pwsh|bash|node\s+(?!-e\b|-p\b|--eval\b|--print\b)(?:['"])?[\w./\\-]+(?:['"])?(?:\s|$)|(?:npm|pnpm|yarn|bun)\s+(?:run|exec|install|add)|(?:^|[^a-z])(?:tsc|tsdown|vite|webpack|rollup)\b/i, type: "command" },
|
|
44
|
+
{ re: /技能|skill/i, type: "skill" },
|
|
45
|
+
// 0.5.12(F1 重证):network 词表由裸子串改词边界+上下文——`fetch` 出现在目录名/标识符中
|
|
46
|
+
//(dsh-web-fetch-playwright)不再误判;真网络形态(URL/调用/下载动词)仍命中
|
|
47
|
+
{ re: /下载|网络|https?:\/\/\S*|(?:^|[^a-z])(?:curl|fetch|wget|iwr|Invoke-WebRequest|Invoke-RestMethod)\s*(?:\(|\.|[^\w]|$)/i, type: "network" },
|
|
48
|
+
// 只读/审查类(置末位,2026-08-25 RB-05):不覆盖在前类别的变更语义(“修改并审查”仍按 write/delete),
|
|
49
|
+
// 仅当无其它类型命中时给只读类型——配合 ACTION_RE 只读词(审查/阅读/验证等),使“审查文件”类执行分点
|
|
50
|
+
// 授权类型为 analysis(只读),不再落入 any 宽泛授权。
|
|
51
|
+
{ re: /审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|评估|分析|研究|排查|核实|梳理|调查|诊断|读取|读(?:出|一下|一遍|完)?|展示|打开|提取/i, type: "analysis" }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// C2(2026-08-28 阶段二):`、`(中文顿号)从非路径字符黑名单移除——Windows 中文目录名
|
|
55
|
+
// 常见顿号("D:\1、工作\..."——用户工作目录),原正则在此截断 → 授权记录成 "d:/1",
|
|
56
|
+
// 真实操作路径匹配失败(ERR-LU50QQ:已授权范围 [write d:/1] 拦 d:/1、工作/...)。
|
|
57
|
+
// 保守边界:顿号后跟空格+新词时可能合并(fail-closed——提取过长 → 授权匹配失败 → 拦而非放),
|
|
58
|
+
// 不放大授权;引号包裹分支本就不受此影响。
|
|
59
|
+
const PATH_TOKEN_RE = /["'][A-Za-z]:[\\/](?!\/)[^"']+["']|[A-Za-z]:[\\/](?!\/)[^\s'"`,。;:!?()【】《》]+|(?:^|[\s"'`])\/(?:[^\s"'`,。;:!?()【】《》、]+)/g;
|
|
60
|
+
|
|
61
|
+
export function normalizePath(p) {
|
|
62
|
+
if (typeof p !== "string") return "";
|
|
63
|
+
return p.replace(/\\/g, "/").toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 从文本推断操作类型(取第一个命中) */
|
|
67
|
+
export function inferTypeFromText(text) {
|
|
68
|
+
const s = String(text || "");
|
|
69
|
+
for (const hint of TYPE_HINTS) {
|
|
70
|
+
if (hint.re.test(s)) return hint.type;
|
|
71
|
+
}
|
|
72
|
+
return "any";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 0.5.10 建议1①(用户审查采纳):ask 答复文本 → 结构化操作类型(修复"弹窗授权宽泛 any 接不住真操作")。
|
|
76
|
+
* 返回 write | command | any(analysis/artifact 类本就不需授权,不落入授权记录)。
|
|
77
|
+
* 明确操作词 → 具体类型;含糊/纯查看 → any(保守:TTL 短兜底)。 */
|
|
78
|
+
export function classifyAskScopeType(text) {
|
|
79
|
+
const s = String(text || "");
|
|
80
|
+
const hasWrite = /(?:修改|编辑|写入|覆盖|保存|删除|丢弃|移除|移动|改名|创建|新建|上传|复制|下载到|发布|推送|提交|写文件)/i.test(s);
|
|
81
|
+
const hasCommand = /(?:pwsh|powershell|命令|脚本|执行|运行|诊断|排查|体检)/i.test(s);
|
|
82
|
+
if (hasWrite && !hasCommand) return "write";
|
|
83
|
+
if (hasCommand && !hasWrite) return "command";
|
|
84
|
+
// 同时含/都不含 → any(保守;不含时 TTL 短)
|
|
85
|
+
return "any";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 从文本推断全部命中的操作类型(用于一条子句含多个操作,如“修改 A 并删除 B”) */
|
|
89
|
+
export function inferTypesFromText(text) {
|
|
90
|
+
const s = String(text || "");
|
|
91
|
+
const types = [];
|
|
92
|
+
for (const hint of TYPE_HINTS) {
|
|
93
|
+
if (hint.re.test(s)) types.push(hint.type);
|
|
94
|
+
}
|
|
95
|
+
return types.length ? [...new Set(types)] : ["any"];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 从文本提取路径候选(批次 4 统一入口:pairActionScopes 与 inferPathPrefixesFromText 共用)。
|
|
100
|
+
* 处理两类噪声:
|
|
101
|
+
* 1. 前缀冗余:引号路径同时命中两分支 → 截断前缀(如 "d:/deepseek")被更长匹配覆盖 → 丢弃;
|
|
102
|
+
* 2. 截断可疑:裸含空格路径(无引号)只能提取到空格前的半截("D:\workspace\docs\..." → "d:/workspace")
|
|
103
|
+
* ——匹配后紧跟空白且后随 token 是路径字符开头且非盘符/引号 → 判定为截断 → 丢弃(fail-closed:
|
|
104
|
+
* 宁缺授权,拦截后请用户给完整/引号路径,也不把半截前缀当成授权)。
|
|
105
|
+
* @returns {Array<{index:number,path:string}>} 去噪后的候选(保留原文索引供就近配对)
|
|
106
|
+
*/
|
|
107
|
+
function extractPathCandidates(text) {
|
|
108
|
+
const s = String(text || "");
|
|
109
|
+
const all = [...s.matchAll(PATH_TOKEN_RE)]
|
|
110
|
+
.map((m) => {
|
|
111
|
+
const rawTok = m[0];
|
|
112
|
+
const after = s.slice(m.index + rawTok.length);
|
|
113
|
+
const afterChar = after.charAt(0);
|
|
114
|
+
const afterWord = after.match(/^[ \t]+(\S+)/)?.[1] || "";
|
|
115
|
+
const truncated = !/["']$/.test(rawTok) // 引号包裹完整路径不可能是截断
|
|
116
|
+
&& /\s/.test(afterChar)
|
|
117
|
+
&& afterWord.length > 0
|
|
118
|
+
&& !/^("|')?[A-Za-z]:[\\/]/i.test(afterWord) // 后随是新盘符路径 → 不是延续
|
|
119
|
+
&& /^[A-Za-z0-9_.[\]\\/-]/i.test(afterWord); // 后随是路径字符(非中文标点/动词)
|
|
120
|
+
return { index: m.index, path: normalizePath(rawTok.replace(/^["']|["']$/g, "")), truncated };
|
|
121
|
+
})
|
|
122
|
+
.filter((p) => p.path && !p.truncated);
|
|
123
|
+
// 前缀冗余:完整匹配覆盖截断前缀(后一字符为空白/结束符)
|
|
124
|
+
return all.filter(
|
|
125
|
+
(p) => !all.some((q) => q !== p && q.path.startsWith(p.path) && /[\s"'`,。;:!?()【】《》、]/.test(q.path.slice(p.path.length, p.path.length + 1) || ""))
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 从文本提取全部绝对路径(用于一条子句含多个路径) */
|
|
130
|
+
export function inferPathPrefixesFromText(text) {
|
|
131
|
+
const unique = [...new Set(extractPathCandidates(text).map((p) => p.path))];
|
|
132
|
+
return unique;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 委托任务书检测(批次 3,2026-08-24;R2④ 实测驱动)。
|
|
137
|
+
* 特征:≥2 步骤枚举 + 显式绝对路径 + 文本较长 + 委托框架词(任务/步骤/目录下/只允许…)。
|
|
138
|
+
* 命中 → strict 模式:无路径绑定的动作子句不产生全局 scope(委托最小 scope 收紧)。
|
|
139
|
+
* 普通用户消息(短/无步骤枚举/无框架词)不命中 → 保持现状(规则 22⑤ 宽泛指令语义)。
|
|
140
|
+
*/
|
|
141
|
+
const DELEGATION_MARKER_RE = /(?:任务|只允许|仅限|受限于|范围内|目录下|依次执行|执行以下|步骤)/;
|
|
142
|
+
export function isDelegationText(text) {
|
|
143
|
+
const s = String(text || "");
|
|
144
|
+
const stepCount = (s.match(/(?:^|\n)\s*\d+[.、).]\s*/g) || []).length;
|
|
145
|
+
if (stepCount < 2) return false;
|
|
146
|
+
if (s.length < 120) return false;
|
|
147
|
+
if (!/(["'][A-Za-z]:[\\/]|[A-Za-z]:[\\/](?!\/))/.test(s)) return false;
|
|
148
|
+
return DELEGATION_MARKER_RE.test(s);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 从本回合执行子句推导授权范围(规则 22 粒度升级,2026-08-24)。
|
|
153
|
+
* R6④ 分点级精确配对(2026-08-24):每个 execute 子句按「动作词 → 就近路径」精确配对,
|
|
154
|
+
* 不再对 type×path 做笛卡尔积——"修改 A 并删除 B"只产生 {write,A} 与 {delete,B},
|
|
155
|
+
* 绝不产生 {write,B}/{delete,A} 等用户没说的组合。
|
|
156
|
+
* 批次 3(2026-08-24):委托任务书(isDelegationText)→ strict 模式,无路径子句跳过(最小 scope)。
|
|
157
|
+
* @param {object} intents parseUserIntents 返回值
|
|
158
|
+
* @returns {Array<{type:string,pathPrefix:string,source:string,clauseId?:string}>}
|
|
159
|
+
*/
|
|
160
|
+
export function scopesFromIntents(intents) {
|
|
161
|
+
if (!intents || !Array.isArray(intents.clauses)) return [];
|
|
162
|
+
const strict = isDelegationText(intents.raw || "");
|
|
163
|
+
const scopes = [];
|
|
164
|
+
for (const c of intents.clauses) {
|
|
165
|
+
if (c.type !== "execute") continue;
|
|
166
|
+
scopes.push(...pairActionScopes(c.raw || "", c.id, strict));
|
|
167
|
+
}
|
|
168
|
+
return scopes;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 动作词 → 操作类型(精确配对用;"执行/运行"保持 any,避免把宽泛指令误限为 command)
|
|
172
|
+
const VERB_TYPE_RE = [
|
|
173
|
+
[/删除|移除|清理|清空|丢弃/i, "delete"],
|
|
174
|
+
[/备份/i, "backup"],
|
|
175
|
+
[/提交|推送/i, "git"],
|
|
176
|
+
[/下载/i, "network"],
|
|
177
|
+
[/执行|运行/i, "any"],
|
|
178
|
+
[/写|保存|另存为|创建|复制|移动|修改|编辑|替换|改|补|修|做|安装|卸载|修复|重建|启动|停止|调整|改进|优化|升级|迁移|整理|添加|增加/i, "write"]
|
|
179
|
+
];
|
|
180
|
+
const VERB_RE =
|
|
181
|
+
/删除|移除|修改|编辑|替换|写入|写|保存|另存为|创建|复制|移动|备份|提交|推送|下载|执行|运行|安装|卸载|清理|修复|重建|启动|停止|调整|改进|优化|升级|迁移|整理|添加|增加|改|补|修|做|实施|推进|继续|处理|解决|转化|转换|读取|读(?:完|出|一下|一遍|出来)?|展示|打开|提取|还原|导入|导出|落盘|落地/g;
|
|
182
|
+
|
|
183
|
+
function verbType(word, fullText) {
|
|
184
|
+
for (const [re, type] of VERB_TYPE_RE) {
|
|
185
|
+
if (re.test(word)) {
|
|
186
|
+
// 宽泛"执行/运行"在出现具体命令上下文时仍收窄为 command
|
|
187
|
+
if (type === "any" && /node\s+[\w./\\-]+\.(?:m?js|cjs)|npm\s+(?:run|exec|install)|run\s+|execute/i.test(fullText)) return "command";
|
|
188
|
+
return type;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return inferTypeFromText(fullText);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** R6④:子句内动作-路径就近配对(纯函数)。strict(委托任务书)下无路径子句跳过,不产生全局。 */
|
|
195
|
+
export function pairActionScopes(raw, clauseId, strict = false) {
|
|
196
|
+
const text = String(raw || "");
|
|
197
|
+
const verbs = [...text.matchAll(VERB_RE)].map((m) => ({ index: m.index, word: m[0] }));
|
|
198
|
+
// 批次 4:统一路径提取(去前缀冗余 + 截断可疑)——仅保留可靠 token 供就近配对
|
|
199
|
+
const paths = extractPathCandidates(text);
|
|
200
|
+
if (verbs.length === 0) {
|
|
201
|
+
// 无动词的 execute 子句(如【执行】标签正文无动作词)→ 回退整句推断(保持旧语义)
|
|
202
|
+
const types = inferTypesFromText(text);
|
|
203
|
+
const pathList = inferPathPrefixesFromText(text);
|
|
204
|
+
// 批次 3:strict(委托任务书)且无路径 → 跳过该子句(最小 scope:无明确路径不授权)
|
|
205
|
+
if (strict && pathList.length === 0) return out;
|
|
206
|
+
const list = pathList.length > 0 ? pathList : [""];
|
|
207
|
+
const out = [];
|
|
208
|
+
for (const type of types) {
|
|
209
|
+
for (const pathPrefix of list) {
|
|
210
|
+
if (!out.some((s) => s.type === type && s.pathPrefix === pathPrefix)) out.push({ type, pathPrefix, source: "clause", clauseId });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
const scopes = [];
|
|
216
|
+
for (const v of verbs) {
|
|
217
|
+
// 绑定规则:该动词之后、中间无其他动词间隔的全部路径("修改 A 和 B" → A、B 都绑);
|
|
218
|
+
// 无后续路径时取动词之前同条件的全部路径("把 X 和 Y 修改");再否则全局。
|
|
219
|
+
const between = (p) => verbs.some((v2) => v2.index !== v.index && v2.index > Math.min(p.index, v.index) && v2.index < Math.max(p.index, v.index));
|
|
220
|
+
const after = paths.filter((p) => p.index > v.index && !between(p));
|
|
221
|
+
let boundPaths = after.map((p) => p.path);
|
|
222
|
+
if (boundPaths.length === 0) {
|
|
223
|
+
const before = paths.filter((p) => p.index < v.index && !between(p));
|
|
224
|
+
boundPaths = before.map((p) => p.path);
|
|
225
|
+
}
|
|
226
|
+
if (boundPaths.length === 0) {
|
|
227
|
+
// 批次 3:strict(委托任务书)且无路径 → 跳过(最小 scope);普通消息 → 全局兜底(规则 22⑤)
|
|
228
|
+
if (strict) continue;
|
|
229
|
+
boundPaths = [""];
|
|
230
|
+
}
|
|
231
|
+
const type = verbType(v.word, text);
|
|
232
|
+
for (const pathPrefix of boundPaths) {
|
|
233
|
+
if (!scopes.some((s) => s.type === type && s.pathPrefix === pathPrefix)) {
|
|
234
|
+
scopes.push({ type, pathPrefix, source: "clause", clauseId });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return scopes;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 格式化授权范围(用于规则 22 拒绝原因) */
|
|
242
|
+
export function describeScopes(scopes) {
|
|
243
|
+
if (!Array.isArray(scopes) || scopes.length === 0) return "无";
|
|
244
|
+
return scopes
|
|
245
|
+
.map((s) => `${s.type || "any"}|路径 ${s.pathPrefix || "全局"}`)
|
|
246
|
+
.join(";");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 从文本提取路径前缀(取最长的路径 token;支持引号包裹的含空格路径) */
|
|
250
|
+
export function inferPathPrefixFromText(text) {
|
|
251
|
+
const s = String(text || "");
|
|
252
|
+
const matches = s.match(PATH_TOKEN_RE) || [];
|
|
253
|
+
if (matches.length === 0) return "";
|
|
254
|
+
const normalized = matches
|
|
255
|
+
.map((m) => normalizePath(m.replace(/^["']|["']$/g, "")))
|
|
256
|
+
.filter(Boolean);
|
|
257
|
+
if (normalized.length === 0) return "";
|
|
258
|
+
return normalized.sort((a, b) => b.length - a.length)[0];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 从一次工具调用推导操作范围 */
|
|
262
|
+
export function operationOf(toolName, args) {
|
|
263
|
+
const name = String(toolName || "");
|
|
264
|
+
const p = pathTarget(args);
|
|
265
|
+
const cmd = commandText(args);
|
|
266
|
+
let type = "any";
|
|
267
|
+
let pathPrefix = "";
|
|
268
|
+
let pathPrefixes = [];
|
|
269
|
+
|
|
270
|
+
if (name === "edit" || name === "write" || (name === "str_replace_editor" && args?.command !== "view")) {
|
|
271
|
+
type = "write";
|
|
272
|
+
pathPrefix = normalizePath(p || "");
|
|
273
|
+
if (pathPrefix) pathPrefixes = [pathPrefix];
|
|
274
|
+
} else if (name === "pwsh" || name === "bash") {
|
|
275
|
+
const text = cmd || "";
|
|
276
|
+
type = inferTypeFromText(text);
|
|
277
|
+
if (/git\s+(push|commit)/i.test(text)) type = "git";
|
|
278
|
+
else if (/remove-item|rm\s+-r|rmdir|del\s+/i.test(text)) type = "delete";
|
|
279
|
+
else if (/backup|(?:copy-item|move-item|rename-item)[^\n]*(?:\.bak|\.backups|trash-)/i.test(text)) type = "backup";
|
|
280
|
+
else if (/set-content|add-content|out-file|writealltext|copy-item|move-item|rename-item/i.test(text)) type = "write";
|
|
281
|
+
const writeTargets = writeTargetPathsFromCommand(text).map(normalizePath).filter(Boolean);
|
|
282
|
+
pathPrefixes = [...writeTargets];
|
|
283
|
+
const cp = extractCopyPaths(text);
|
|
284
|
+
if (cp && cp.source) pathPrefixes.push(normalizePath(cp.source));
|
|
285
|
+
pathPrefixes = [...new Set(pathPrefixes.filter(Boolean))];
|
|
286
|
+
// P0-1d:主写目标优先取真实写参数(Copy-Item 的 Destination 等),
|
|
287
|
+
// 避免"源路径比目标长"时最长路径误选源(13A 误拦);授权匹配仍走多候选 pathPrefixes
|
|
288
|
+
pathPrefix = writeTargets[0] || pathPrefixes.sort((a, b) => b.length - a.length)[0] || inferPathPrefixFromText(text);
|
|
289
|
+
} else if (name === "skill") {
|
|
290
|
+
type = "skill";
|
|
291
|
+
pathPrefix = "";
|
|
292
|
+
} else if (GENERIC_EXEC_TOOLS.has(name)) {
|
|
293
|
+
type = "command";
|
|
294
|
+
pathPrefix = "";
|
|
295
|
+
} else if (name === "ask_user_question") {
|
|
296
|
+
type = "ask";
|
|
297
|
+
pathPrefix = "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { type, pathPrefix, pathPrefixes };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 路径是否在授权边界内:相等,或为授权路径的子路径(子路径以 / 为边界,防止文件前缀误伤) */
|
|
304
|
+
function pathUnderAuth(p, authPath) {
|
|
305
|
+
if (p === authPath) return true;
|
|
306
|
+
return p.startsWith(authPath.endsWith("/") ? authPath : authPath + "/");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** 判断授权记录是否匹配本次操作(支持源/目标多路径任一匹配)。
|
|
310
|
+
* 2026-08-24(R6.3):纯 startsWith 改为边界匹配——授权 "d:/a.txt" 不再放行 "d:/a.txt.bak"(同前缀文件误伤),
|
|
311
|
+
* 目录前缀(含无尾斜杠的目录名)仍正确匹配子路径。 */
|
|
312
|
+
export function authMatches(auth, op) {
|
|
313
|
+
if (!auth || !op) return false;
|
|
314
|
+
if (auth.type !== "any" && op.type !== "any" && auth.type !== op.type) return false;
|
|
315
|
+
if (auth.pathPrefix) {
|
|
316
|
+
const candidates = Array.isArray(op.pathPrefixes) && op.pathPrefixes.length > 0
|
|
317
|
+
? op.pathPrefixes
|
|
318
|
+
: (op.pathPrefix ? [op.pathPrefix] : []);
|
|
319
|
+
if (candidates.length === 0) return false;
|
|
320
|
+
const authPath = normalizePath(auth.pathPrefix);
|
|
321
|
+
if (!candidates.some((p) => pathUnderAuth(normalizePath(p), authPath))) return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 在授权列表中查找匹配项,返回最近一条(过期记录忽略) */
|
|
327
|
+
export function findMatchingAuth(auths, op, now = Date.now()) {
|
|
328
|
+
if (!Array.isArray(auths)) return null;
|
|
329
|
+
const matches = auths.filter((a) => authMatches(a, op) && (!a.expiresAt || a.expiresAt > now));
|
|
330
|
+
if (matches.length === 0) return null;
|
|
331
|
+
return matches.sort((a, b) => (b.at || 0) - (a.at || 0))[0];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function describeAuth(auth) {
|
|
335
|
+
if (!auth) return "无";
|
|
336
|
+
const type = auth.type || "any";
|
|
337
|
+
const path = auth.pathPrefix ? `路径 ${auth.pathPrefix}` : "全局";
|
|
338
|
+
const expires = auth.expiresAt ? `|到期 ${new Date(auth.expiresAt).toISOString()}` : "";
|
|
339
|
+
return `${type}|${path}|${new Date(auth.at || 0).toISOString()}${expires}|仅本次运行有效`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function describeOp(op) {
|
|
343
|
+
if (!op) return "未知";
|
|
344
|
+
return `${op.type || "any"}|路径 ${op.pathPrefix || "未指定"}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** 用户消息是否为“询问影响”而非授权(词表与 intent.js 统一,2026-08-24) */
|
|
348
|
+
export function isQuestionMessage(text) {
|
|
349
|
+
return QUESTION_RE.test(String(text || ""));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 用户消息是否包含明确执行许可(确认/同意/授权 + 执行语;且不是询问句) */
|
|
353
|
+
export function isAuthMessage(text) {
|
|
354
|
+
const s = String(text || "");
|
|
355
|
+
return APPROVAL_EXEC_RE.test(s) && !isQuestionMessage(s);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 用户消息是否为直接命令式指令(非询问,视为授权执行) */
|
|
359
|
+
export function isDirectiveMessage(text) {
|
|
360
|
+
const s = String(text || "");
|
|
361
|
+
return DIRECTIVE_RE.test(s) && !isQuestionMessage(s);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** 选项/自由文本是否表示批准(纯"确认方案/理解/方向"不算批准) */
|
|
365
|
+
export function isApprovalText(text) {
|
|
366
|
+
const s = String(text || "");
|
|
367
|
+
if (PLAN_ONLY_RE.test(s) && !EXEC_ACTION_RE.test(s) && !APPROVAL_EXEC_RE.test(s)) return false;
|
|
368
|
+
return APPROVAL_RE.test(s) || EXEC_ACTION_RE.test(s);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** 选项/自由文本是否表示拒绝 */
|
|
372
|
+
export function isRejectionText(text) {
|
|
373
|
+
return REJECTION_RE.test(String(text || ""));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 从 ask_user_question 的 arguments 中提取问题文本(用于推断授权范围) */
|
|
377
|
+
export function askQuestionText(questions) {
|
|
378
|
+
if (!Array.isArray(questions)) return "";
|
|
379
|
+
const parts = [];
|
|
380
|
+
for (const q of questions) {
|
|
381
|
+
if (q && typeof q === "object") {
|
|
382
|
+
if (q.question) parts.push(q.question);
|
|
383
|
+
if (q.header) parts.push(q.header);
|
|
384
|
+
if (q.detail) parts.push(q.detail);
|
|
385
|
+
if (Array.isArray(q.options)) {
|
|
386
|
+
for (const o of q.options) {
|
|
387
|
+
if (o && typeof o === "object") {
|
|
388
|
+
if (o.label) parts.push(o.label);
|
|
389
|
+
if (o.description) parts.push(o.description);
|
|
390
|
+
} else if (typeof o === "string") {
|
|
391
|
+
parts.push(o);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} else if (typeof q === "string") {
|
|
396
|
+
parts.push(q);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return parts.join(" ");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 仅取 ask 的问题/标题/说明,不含选项文本。
|
|
404
|
+
* 2026-08-24 修复:选项描述里的“仅会话内说明/不跨会话保留”等字样不得影响授权范围判定。
|
|
405
|
+
*/
|
|
406
|
+
export function askQuestionCoreText(questions) {
|
|
407
|
+
if (!Array.isArray(questions)) return "";
|
|
408
|
+
const parts = [];
|
|
409
|
+
for (const q of questions) {
|
|
410
|
+
if (q && typeof q === "object") {
|
|
411
|
+
if (q.question) parts.push(q.question);
|
|
412
|
+
if (q.header) parts.push(q.header);
|
|
413
|
+
if (q.detail) parts.push(q.detail);
|
|
414
|
+
} else if (typeof q === "string") {
|
|
415
|
+
parts.push(q);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return parts.join(" ");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** 提取 ask 结果中用户实际选择的文本(selected 标签 + 自定义输入),用于授权范围判定 */
|
|
422
|
+
export function askResultSelectedText(result) {
|
|
423
|
+
const answers = extractAnswers(result);
|
|
424
|
+
if (!answers || answers.length === 0) return "";
|
|
425
|
+
const parts = [];
|
|
426
|
+
for (const item of answers) {
|
|
427
|
+
const texts = [...(item.selected || []), item.custom || ""].filter(Boolean);
|
|
428
|
+
for (const t of texts) parts.push(t);
|
|
429
|
+
}
|
|
430
|
+
return parts.join(" ");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 判定 ask 授权是否应为“会话级/全局长时”授权。
|
|
435
|
+
* 只匹配明确的范围词,不再匹配裸“会话内”(避免“仅会话内说明”把一次小授权放大成 12h 全局)。
|
|
436
|
+
*/
|
|
437
|
+
const SESSION_WIDE_RE = /(?:全部|本会话|本次会话|整个会话|当前会话|会话内所有|会话内全部|所有后续|剩余所有|所有操作|整个profile|整个工作区|全部推进项|全部操作)/i;
|
|
438
|
+
export function isSessionWideAskText(text) {
|
|
439
|
+
return SESSION_WIDE_RE.test(String(text || ""));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** 从 ask_user_question 的结果中提取是否批准 */
|
|
443
|
+
export function askResultApproved(result) {
|
|
444
|
+
const answers = extractAnswers(result);
|
|
445
|
+
if (!answers || answers.length === 0) return false;
|
|
446
|
+
let approved = false;
|
|
447
|
+
let rejected = false;
|
|
448
|
+
for (const item of answers) {
|
|
449
|
+
const texts = [...(item.selected || []), item.custom || ""].filter(Boolean);
|
|
450
|
+
for (const t of texts) {
|
|
451
|
+
if (isRejectionText(t)) rejected = true;
|
|
452
|
+
else if (isApprovalText(t)) approved = true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return approved && !rejected;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* C3(2026-08-28 阶段二):ask 结果是否为【明确拒绝】——区别于"未响应/超时/无选择"。
|
|
460
|
+
* 用户说"你也没给我时间回复啊":engine 旧逻辑把「任何未批准」一律记为拒绝(auth-reject +
|
|
461
|
+
* askRejections 池)→ 5 分钟内再有 ask 被 __ask-throttle 拦 —— 用户没回 ≠ 用户拒绝,
|
|
462
|
+
* 节流烧掉的是"用户可能还没来得及看"。仅当结果文本含明确拒绝词才记拒绝。
|
|
463
|
+
*/
|
|
464
|
+
export function askResultRejected(result) {
|
|
465
|
+
const answers = extractAnswers(result);
|
|
466
|
+
if (!answers || answers.length === 0) return false;
|
|
467
|
+
for (const item of answers) {
|
|
468
|
+
const texts = [...(item.selected || []), item.custom || ""].filter(Boolean);
|
|
469
|
+
for (const t of texts) {
|
|
470
|
+
if (isRejectionText(t)) return true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function extractToolResultText(result) {
|
|
477
|
+
const msg = result?.message ?? result;
|
|
478
|
+
const content = Array.isArray(msg?.content)
|
|
479
|
+
? msg.content
|
|
480
|
+
: Array.isArray(result?.content)
|
|
481
|
+
? result.content
|
|
482
|
+
: null;
|
|
483
|
+
if (!content) return null;
|
|
484
|
+
const block = content.find((b) => b && b.type === "tool-result");
|
|
485
|
+
if (!block) return null;
|
|
486
|
+
const inner = Array.isArray(block.content) ? block.content : [];
|
|
487
|
+
const text = inner
|
|
488
|
+
.filter((b) => b && b.type === "text" && typeof b.text === "string")
|
|
489
|
+
.map((b) => b.text)
|
|
490
|
+
.join("\n");
|
|
491
|
+
return text || null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function extractAnswers(result) {
|
|
495
|
+
if (!result) return [];
|
|
496
|
+
if (Array.isArray(result)) return result;
|
|
497
|
+
if (typeof result === "string") {
|
|
498
|
+
try {
|
|
499
|
+
return extractAnswers(JSON.parse(result));
|
|
500
|
+
} catch {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// tool/result 事件实际形状:message.content[0] = { type:'tool-result', content:[{type:'text', text:'{...}'}] }
|
|
505
|
+
const toolText = extractToolResultText(result);
|
|
506
|
+
if (toolText) {
|
|
507
|
+
try {
|
|
508
|
+
return extractAnswers(JSON.parse(toolText));
|
|
509
|
+
} catch {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (Array.isArray(result.answers)) return result.answers;
|
|
514
|
+
if (Array.isArray(result.value?.answers)) return result.value.answers;
|
|
515
|
+
if (Array.isArray(result.result?.answers)) return result.result.answers;
|
|
516
|
+
return [];
|
|
517
|
+
}
|