add-coder 0.3.33 → 0.3.34
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/package.json +1 -1
- package/templates/.add-coder-src-hash.json +70 -70
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +4 -0
- package/templates/adapters/claude/hooks/notification.mjs +4 -0
- package/templates/adapters/claude/hooks/permission-denied.mjs +4 -0
- package/templates/adapters/claude/hooks/permission-gate.mjs +4 -0
- package/templates/adapters/claude/hooks/post-tool-use.mjs +4 -0
- package/templates/adapters/claude/hooks/pre-compact.mjs +4 -0
- package/templates/adapters/claude/hooks/pre-tool-use.mjs +4 -0
- package/templates/adapters/claude/hooks/prompt-submit.mjs +4 -0
- package/templates/adapters/claude/hooks/session-end.mjs +4 -0
- package/templates/adapters/claude/hooks/session-start.mjs +4 -0
- package/templates/adapters/claude/hooks/stop-check.mjs +87 -6
- package/templates/adapters/claude/hooks/stop-failure.mjs +4 -0
- package/templates/adapters/claude/hooks/subagent-guard.mjs +4 -0
- package/templates/adapters/claude/hooks/subagent-stop.mjs +4 -0
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +4 -0
- package/templates/adapters/codex/hooks/notification.mjs +4 -0
- package/templates/adapters/codex/hooks/permission-gate.mjs +4 -0
- package/templates/adapters/codex/hooks/post-tool-failure.mjs +4 -0
- package/templates/adapters/codex/hooks/post-tool-use.mjs +4 -0
- package/templates/adapters/codex/hooks/pre-compact.mjs +4 -0
- package/templates/adapters/codex/hooks/pre-tool-use.mjs +4 -0
- package/templates/adapters/codex/hooks/prompt-submit.mjs +4 -0
- package/templates/adapters/codex/hooks/session-end.mjs +4 -0
- package/templates/adapters/codex/hooks/session-start.mjs +4 -0
- package/templates/adapters/codex/hooks/stop-check.mjs +87 -6
- package/templates/adapters/codex/hooks/subagent-guard.mjs +4 -0
- package/templates/adapters/codex/hooks/subagent-stop.mjs +4 -0
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +4 -0
- package/templates/adapters/qoder/hooks/notification.mjs +4 -0
- package/templates/adapters/qoder/hooks/permission-gate.mjs +4 -0
- package/templates/adapters/qoder/hooks/post-tool-failure.mjs +4 -0
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +4 -0
- package/templates/adapters/qoder/hooks/pre-compact.mjs +4 -0
- package/templates/adapters/qoder/hooks/pre-tool-use.mjs +4 -0
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +4 -0
- package/templates/adapters/qoder/hooks/review-checklist.mjs +4 -0
- package/templates/adapters/qoder/hooks/session-end.mjs +4 -0
- package/templates/adapters/qoder/hooks/session-start.mjs +4 -0
- package/templates/adapters/qoder/hooks/stop-check.mjs +87 -6
- package/templates/adapters/qoder/hooks/subagent-guard.mjs +4 -0
- package/templates/adapters/qoder/hooks/subagent-stop.mjs +4 -0
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +4 -0
- package/templates/adapters/trae/hooks/notification.mjs +4 -0
- package/templates/adapters/trae/hooks/permission-gate.mjs +4 -0
- package/templates/adapters/trae/hooks/post-tool-failure.mjs +4 -0
- package/templates/adapters/trae/hooks/post-tool-use.mjs +4 -0
- package/templates/adapters/trae/hooks/pre-compact.mjs +4 -0
- package/templates/adapters/trae/hooks/pre-tool-use.mjs +4 -0
- package/templates/adapters/trae/hooks/prompt-submit.mjs +4 -0
- package/templates/adapters/trae/hooks/session-end.mjs +4 -0
- package/templates/adapters/trae/hooks/session-start.mjs +4 -0
- package/templates/adapters/trae/hooks/stop-check.mjs +87 -6
- package/templates/adapters/trae/hooks/subagent-guard.mjs +4 -0
- package/templates/adapters/trae/hooks/subagent-stop.mjs +4 -0
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +4 -0
- package/templates/adapters/vscode/hooks/notification.mjs +4 -0
- package/templates/adapters/vscode/hooks/permission-gate.mjs +4 -0
- package/templates/adapters/vscode/hooks/post-tool-failure.mjs +4 -0
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +4 -0
- package/templates/adapters/vscode/hooks/pre-compact.mjs +4 -0
- package/templates/adapters/vscode/hooks/pre-tool-use.mjs +4 -0
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +4 -0
- package/templates/adapters/vscode/hooks/session-end.mjs +4 -0
- package/templates/adapters/vscode/hooks/session-start.mjs +4 -0
- package/templates/adapters/vscode/hooks/stop-check.mjs +87 -6
- package/templates/adapters/vscode/hooks/subagent-guard.mjs +4 -0
- package/templates/adapters/vscode/hooks/subagent-stop.mjs +4 -0
- package/templates/core/governance/rules.ts +4 -0
- package/templates/core/governance/stop-router.ts +101 -7
|
@@ -192,6 +192,10 @@ var protocol = {
|
|
|
192
192
|
".trae",
|
|
193
193
|
".codex"
|
|
194
194
|
]
|
|
195
|
+
},
|
|
196
|
+
"stop": {
|
|
197
|
+
"max_prompt_per_context": 3,
|
|
198
|
+
"window_minutes": 30
|
|
195
199
|
}
|
|
196
200
|
};
|
|
197
201
|
|
|
@@ -402,6 +406,10 @@ function buildStopContext(quadrant, info) {
|
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
// templates/core/governance/stop-router.ts
|
|
409
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
410
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
411
|
+
import { tmpdir } from "node:os";
|
|
412
|
+
import { join as join2 } from "node:path";
|
|
405
413
|
var StopRouter = class {
|
|
406
414
|
/** 主路由:返回 exit code(0 放行 / 2 阻断) */
|
|
407
415
|
run() {
|
|
@@ -430,14 +438,20 @@ var StopRouter = class {
|
|
|
430
438
|
}
|
|
431
439
|
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
432
440
|
/**
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
441
|
+
* Q4 验收决策(双维度组合——2026-08-14 Task 9.4.4④ 上提,回流: I2):
|
|
442
|
+
* 维度 0(新增,临时方案): 弹框频控——同项目同 Plan 窗口内达上限 → 降级放行
|
|
443
|
+
* 维度 1(前置): DB 任务进度(step = done/total,数值且 done<total → 未完成阻断提示)
|
|
444
|
+
* 维度 2: checklist 质量(checkAddCompleteness 未闭环 → 阻断)
|
|
445
|
+
* 互补非替代——codex 原 DB 进度分流语义上提 core,core checklist 质量语义保留。
|
|
446
|
+
*/
|
|
438
447
|
q4Check(plan, rounds, step, handoff, addRoute) {
|
|
439
|
-
void plan;
|
|
440
448
|
void rounds;
|
|
449
|
+
const limited = this.stopPromptLimited(plan);
|
|
450
|
+
if (limited) {
|
|
451
|
+
process.stderr.write(`[ADD Stop] \u5DF2\u8FBE\u672C Plan \u5F39\u6846\u4E0A\u9650(${this.stopPromptMax()})\uFF0C\u672C\u6B21\u653E\u884C\u3002\u8BF7\u5728\u540E\u7EED\u5B9E\u65BD\u4E2D\u5B8C\u6210\u9A8C\u6536\u95ED\u73AF\uFF08devlog + handoff\uFF09\u540E\u505C\u6B62\u3002
|
|
452
|
+
`);
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
441
455
|
const [donePart, totalPart] = step.split("/");
|
|
442
456
|
if (/^\d+$/.test(donePart) && /^\d+$/.test(totalPart) && Number(donePart) < Number(totalPart)) {
|
|
443
457
|
return this.emitQ4Unclosed(
|
|
@@ -454,6 +468,73 @@ var StopRouter = class {
|
|
|
454
468
|
clearDevAction();
|
|
455
469
|
return this.emitQ4Pass();
|
|
456
470
|
}
|
|
471
|
+
// ── 弹框频控(临时方案,规则真源: hook-protocol-rules.toml [protocol.stop])──
|
|
472
|
+
// 计数隔离: 文件级 = PROJECT_DIR md5(跨项目不互扰);key 级 = planName(同项目跨 Plan 不互扰)
|
|
473
|
+
// 故障降级: 哨兵读/写异常 → fail-open 放行(不阻塞主流程)
|
|
474
|
+
/** 哨兵记录:同一项目同一 Plan 的弹框计数 */
|
|
475
|
+
stopPromptMax() {
|
|
476
|
+
const cfg = protocol.stop;
|
|
477
|
+
return cfg?.max_prompt_per_context ?? 3;
|
|
478
|
+
}
|
|
479
|
+
/** 哨兵窗口(毫秒) */
|
|
480
|
+
stopPromptWindowMs() {
|
|
481
|
+
const cfg = protocol.stop;
|
|
482
|
+
return (cfg?.window_minutes ?? 30) * 6e4;
|
|
483
|
+
}
|
|
484
|
+
/** 哨兵文件路径(项目隔离:PROJECT_DIR md5) */
|
|
485
|
+
stopSentinelPath() {
|
|
486
|
+
const projectDir = process.env.PROJECT_DIR || process.env.QODER_PROJECT_DIR || process.env.QODERCN_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
487
|
+
const md5 = createHash2("md5").update(projectDir).digest("hex").slice(0, 8);
|
|
488
|
+
return join2(tmpdir(), `add_stop_${md5}.json`);
|
|
489
|
+
}
|
|
490
|
+
/** 写哨兵(失败静默,fail-open) */
|
|
491
|
+
writeStopPromptSentinel(data) {
|
|
492
|
+
try {
|
|
493
|
+
writeFileSync2(this.stopSentinelPath(), JSON.stringify(data), "utf-8");
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 频控判定:返回 true = 已达上限应放行(不弹框)
|
|
499
|
+
* 生成态缺失 protocol.stop(TOML 未同步)→ 跳过频控保持既有行为(生成链幂等校验兜底)
|
|
500
|
+
*/
|
|
501
|
+
stopPromptLimited(plan) {
|
|
502
|
+
try {
|
|
503
|
+
const stopCfg = protocol.stop;
|
|
504
|
+
if (!stopCfg) return false;
|
|
505
|
+
const max = this.stopPromptMax();
|
|
506
|
+
const windowMs = this.stopPromptWindowMs();
|
|
507
|
+
const path3 = this.stopSentinelPath();
|
|
508
|
+
let data = {};
|
|
509
|
+
if (existsSync2(path3)) {
|
|
510
|
+
try {
|
|
511
|
+
data = JSON.parse(readFileSync2(path3, "utf-8")) ?? {};
|
|
512
|
+
} catch {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
const entry = data[plan];
|
|
518
|
+
if (!entry) {
|
|
519
|
+
data[plan] = { count: 1, lastPromptAt: now };
|
|
520
|
+
this.writeStopPromptSentinel(data);
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
if (now - entry.lastPromptAt > windowMs) {
|
|
524
|
+
data[plan] = { count: 1, lastPromptAt: now };
|
|
525
|
+
this.writeStopPromptSentinel(data);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (entry.count >= max) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
data[plan] = { count: entry.count + 1, lastPromptAt: now };
|
|
532
|
+
this.writeStopPromptSentinel(data);
|
|
533
|
+
return false;
|
|
534
|
+
} catch {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
457
538
|
/** Q0: DB 不可用(core: stderr + 2) */
|
|
458
539
|
emitQ0(reason) {
|
|
459
540
|
process.stderr.write(
|
|
@@ -192,6 +192,10 @@ var protocol = {
|
|
|
192
192
|
".trae",
|
|
193
193
|
".codex"
|
|
194
194
|
]
|
|
195
|
+
},
|
|
196
|
+
"stop": {
|
|
197
|
+
"max_prompt_per_context": 3,
|
|
198
|
+
"window_minutes": 30
|
|
195
199
|
}
|
|
196
200
|
};
|
|
197
201
|
|
|
@@ -402,6 +406,10 @@ function buildStopContext(quadrant, info) {
|
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
// templates/core/governance/stop-router.ts
|
|
409
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
410
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
411
|
+
import { tmpdir } from "node:os";
|
|
412
|
+
import { join as join2 } from "node:path";
|
|
405
413
|
var StopRouter = class {
|
|
406
414
|
/** 主路由:返回 exit code(0 放行 / 2 阻断) */
|
|
407
415
|
run() {
|
|
@@ -430,14 +438,20 @@ var StopRouter = class {
|
|
|
430
438
|
}
|
|
431
439
|
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
432
440
|
/**
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
441
|
+
* Q4 验收决策(双维度组合——2026-08-14 Task 9.4.4④ 上提,回流: I2):
|
|
442
|
+
* 维度 0(新增,临时方案): 弹框频控——同项目同 Plan 窗口内达上限 → 降级放行
|
|
443
|
+
* 维度 1(前置): DB 任务进度(step = done/total,数值且 done<total → 未完成阻断提示)
|
|
444
|
+
* 维度 2: checklist 质量(checkAddCompleteness 未闭环 → 阻断)
|
|
445
|
+
* 互补非替代——codex 原 DB 进度分流语义上提 core,core checklist 质量语义保留。
|
|
446
|
+
*/
|
|
438
447
|
q4Check(plan, rounds, step, handoff, addRoute) {
|
|
439
|
-
void plan;
|
|
440
448
|
void rounds;
|
|
449
|
+
const limited = this.stopPromptLimited(plan);
|
|
450
|
+
if (limited) {
|
|
451
|
+
process.stderr.write(`[ADD Stop] \u5DF2\u8FBE\u672C Plan \u5F39\u6846\u4E0A\u9650(${this.stopPromptMax()})\uFF0C\u672C\u6B21\u653E\u884C\u3002\u8BF7\u5728\u540E\u7EED\u5B9E\u65BD\u4E2D\u5B8C\u6210\u9A8C\u6536\u95ED\u73AF\uFF08devlog + handoff\uFF09\u540E\u505C\u6B62\u3002
|
|
452
|
+
`);
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
441
455
|
const [donePart, totalPart] = step.split("/");
|
|
442
456
|
if (/^\d+$/.test(donePart) && /^\d+$/.test(totalPart) && Number(donePart) < Number(totalPart)) {
|
|
443
457
|
return this.emitQ4Unclosed(
|
|
@@ -454,6 +468,73 @@ var StopRouter = class {
|
|
|
454
468
|
clearDevAction();
|
|
455
469
|
return this.emitQ4Pass();
|
|
456
470
|
}
|
|
471
|
+
// ── 弹框频控(临时方案,规则真源: hook-protocol-rules.toml [protocol.stop])──
|
|
472
|
+
// 计数隔离: 文件级 = PROJECT_DIR md5(跨项目不互扰);key 级 = planName(同项目跨 Plan 不互扰)
|
|
473
|
+
// 故障降级: 哨兵读/写异常 → fail-open 放行(不阻塞主流程)
|
|
474
|
+
/** 哨兵记录:同一项目同一 Plan 的弹框计数 */
|
|
475
|
+
stopPromptMax() {
|
|
476
|
+
const cfg = protocol.stop;
|
|
477
|
+
return cfg?.max_prompt_per_context ?? 3;
|
|
478
|
+
}
|
|
479
|
+
/** 哨兵窗口(毫秒) */
|
|
480
|
+
stopPromptWindowMs() {
|
|
481
|
+
const cfg = protocol.stop;
|
|
482
|
+
return (cfg?.window_minutes ?? 30) * 6e4;
|
|
483
|
+
}
|
|
484
|
+
/** 哨兵文件路径(项目隔离:PROJECT_DIR md5) */
|
|
485
|
+
stopSentinelPath() {
|
|
486
|
+
const projectDir = process.env.PROJECT_DIR || process.env.QODER_PROJECT_DIR || process.env.QODERCN_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
487
|
+
const md5 = createHash2("md5").update(projectDir).digest("hex").slice(0, 8);
|
|
488
|
+
return join2(tmpdir(), `add_stop_${md5}.json`);
|
|
489
|
+
}
|
|
490
|
+
/** 写哨兵(失败静默,fail-open) */
|
|
491
|
+
writeStopPromptSentinel(data) {
|
|
492
|
+
try {
|
|
493
|
+
writeFileSync2(this.stopSentinelPath(), JSON.stringify(data), "utf-8");
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 频控判定:返回 true = 已达上限应放行(不弹框)
|
|
499
|
+
* 生成态缺失 protocol.stop(TOML 未同步)→ 跳过频控保持既有行为(生成链幂等校验兜底)
|
|
500
|
+
*/
|
|
501
|
+
stopPromptLimited(plan) {
|
|
502
|
+
try {
|
|
503
|
+
const stopCfg = protocol.stop;
|
|
504
|
+
if (!stopCfg) return false;
|
|
505
|
+
const max = this.stopPromptMax();
|
|
506
|
+
const windowMs = this.stopPromptWindowMs();
|
|
507
|
+
const path3 = this.stopSentinelPath();
|
|
508
|
+
let data = {};
|
|
509
|
+
if (existsSync2(path3)) {
|
|
510
|
+
try {
|
|
511
|
+
data = JSON.parse(readFileSync2(path3, "utf-8")) ?? {};
|
|
512
|
+
} catch {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
const entry = data[plan];
|
|
518
|
+
if (!entry) {
|
|
519
|
+
data[plan] = { count: 1, lastPromptAt: now };
|
|
520
|
+
this.writeStopPromptSentinel(data);
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
if (now - entry.lastPromptAt > windowMs) {
|
|
524
|
+
data[plan] = { count: 1, lastPromptAt: now };
|
|
525
|
+
this.writeStopPromptSentinel(data);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (entry.count >= max) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
data[plan] = { count: entry.count + 1, lastPromptAt: now };
|
|
532
|
+
this.writeStopPromptSentinel(data);
|
|
533
|
+
return false;
|
|
534
|
+
} catch {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
457
538
|
/** Q0: DB 不可用(core: stderr + 2) */
|
|
458
539
|
emitQ0(reason) {
|
|
459
540
|
process.stderr.write(
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
hasDevAction,
|
|
13
13
|
} from "./common.js"
|
|
14
14
|
import { buildStopContext } from "./context-inject.js"
|
|
15
|
+
import { protocol } from "./rules.js"
|
|
16
|
+
import { createHash } from "node:crypto"
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
18
|
+
import { tmpdir } from "node:os"
|
|
19
|
+
import { join } from "node:path"
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Stop 四象限分流路由(Q0-Q4,与 bash stop-check.sh 同语义):
|
|
@@ -65,14 +70,20 @@ export class StopRouter {
|
|
|
65
70
|
// ─────────────────────────── 扩展点 ───────────────────────────
|
|
66
71
|
|
|
67
72
|
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
* Q4 验收决策(双维度组合——2026-08-14 Task 9.4.4④ 上提,回流: I2):
|
|
74
|
+
* 维度 0(新增,临时方案): 弹框频控——同项目同 Plan 窗口内达上限 → 降级放行
|
|
75
|
+
* 维度 1(前置): DB 任务进度(step = done/total,数值且 done<total → 未完成阻断提示)
|
|
76
|
+
* 维度 2: checklist 质量(checkAddCompleteness 未闭环 → 阻断)
|
|
77
|
+
* 互补非替代——codex 原 DB 进度分流语义上提 core,core checklist 质量语义保留。
|
|
78
|
+
*/
|
|
79
|
+
protected q4Check(plan: string, rounds: string, step: string, handoff: string, addRoute: string): number {
|
|
75
80
|
void rounds
|
|
81
|
+
// 维度 0: 弹框频控(达上限 → 放行,防 Stop 阻断刷屏死循环)
|
|
82
|
+
const limited = this.stopPromptLimited(plan)
|
|
83
|
+
if (limited) {
|
|
84
|
+
process.stderr.write(`[ADD Stop] 已达本 Plan 弹框上限(${this.stopPromptMax()}),本次放行。请在后续实施中完成验收闭环(devlog + handoff)后停止。\n`)
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
76
87
|
// 维度 1: DB 任务进度(step 格式 done/total,如 11/64)
|
|
77
88
|
const [donePart, totalPart] = step.split("/")
|
|
78
89
|
if (
|
|
@@ -97,6 +108,89 @@ export class StopRouter {
|
|
|
97
108
|
return this.emitQ4Pass()
|
|
98
109
|
}
|
|
99
110
|
|
|
111
|
+
// ── 弹框频控(临时方案,规则真源: hook-protocol-rules.toml [protocol.stop])──
|
|
112
|
+
// 计数隔离: 文件级 = PROJECT_DIR md5(跨项目不互扰);key 级 = planName(同项目跨 Plan 不互扰)
|
|
113
|
+
// 故障降级: 哨兵读/写异常 → fail-open 放行(不阻塞主流程)
|
|
114
|
+
|
|
115
|
+
/** 哨兵记录:同一项目同一 Plan 的弹框计数 */
|
|
116
|
+
protected stopPromptMax(): number {
|
|
117
|
+
const cfg = protocol.stop as { max_prompt_per_context?: number } | undefined
|
|
118
|
+
return cfg?.max_prompt_per_context ?? 3
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 哨兵窗口(毫秒) */
|
|
122
|
+
protected stopPromptWindowMs(): number {
|
|
123
|
+
const cfg = protocol.stop as { window_minutes?: number } | undefined
|
|
124
|
+
return (cfg?.window_minutes ?? 30) * 60_000
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 哨兵文件路径(项目隔离:PROJECT_DIR md5) */
|
|
128
|
+
protected stopSentinelPath(): string {
|
|
129
|
+
const projectDir =
|
|
130
|
+
process.env.PROJECT_DIR ||
|
|
131
|
+
process.env.QODER_PROJECT_DIR ||
|
|
132
|
+
process.env.QODERCN_PROJECT_DIR ||
|
|
133
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
134
|
+
process.cwd()
|
|
135
|
+
const md5 = createHash("md5").update(projectDir).digest("hex").slice(0, 8)
|
|
136
|
+
return join(tmpdir(), `add_stop_${md5}.json`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 写哨兵(失败静默,fail-open) */
|
|
140
|
+
protected writeStopPromptSentinel(data: Record<string, { count: number; lastPromptAt: number }>): void {
|
|
141
|
+
try {
|
|
142
|
+
writeFileSync(this.stopSentinelPath(), JSON.stringify(data), "utf-8")
|
|
143
|
+
} catch {
|
|
144
|
+
// 静默:哨兵写失败不影响主流程
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 频控判定:返回 true = 已达上限应放行(不弹框)
|
|
150
|
+
* 生成态缺失 protocol.stop(TOML 未同步)→ 跳过频控保持既有行为(生成链幂等校验兜底)
|
|
151
|
+
*/
|
|
152
|
+
protected stopPromptLimited(plan: string): boolean {
|
|
153
|
+
try {
|
|
154
|
+
const stopCfg = protocol.stop as
|
|
155
|
+
| { max_prompt_per_context?: number; window_minutes?: number }
|
|
156
|
+
| undefined
|
|
157
|
+
if (!stopCfg) return false // 生成态缺失 → 跳过频控
|
|
158
|
+
const max = this.stopPromptMax()
|
|
159
|
+
const windowMs = this.stopPromptWindowMs()
|
|
160
|
+
const path = this.stopSentinelPath()
|
|
161
|
+
let data: Record<string, { count: number; lastPromptAt: number }> = {}
|
|
162
|
+
if (existsSync(path)) {
|
|
163
|
+
try {
|
|
164
|
+
data = JSON.parse(readFileSync(path, "utf-8")) ?? {}
|
|
165
|
+
} catch {
|
|
166
|
+
return true // 哨兵损坏 → fail-open 放行
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const now = Date.now()
|
|
170
|
+
const entry = data[plan]
|
|
171
|
+
if (!entry) {
|
|
172
|
+
// 首次触发:计数 1,正常弹框
|
|
173
|
+
data[plan] = { count: 1, lastPromptAt: now }
|
|
174
|
+
this.writeStopPromptSentinel(data)
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
if (now - entry.lastPromptAt > windowMs) {
|
|
178
|
+
// 窗口过期:重置计数 1,正常弹框(新窗口)
|
|
179
|
+
data[plan] = { count: 1, lastPromptAt: now }
|
|
180
|
+
this.writeStopPromptSentinel(data)
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
if (entry.count >= max) {
|
|
184
|
+
return true // 窗口内已达上限 → 放行(不写回,保持计数)
|
|
185
|
+
}
|
|
186
|
+
data[plan] = { count: entry.count + 1, lastPromptAt: now }
|
|
187
|
+
this.writeStopPromptSentinel(data)
|
|
188
|
+
return false
|
|
189
|
+
} catch {
|
|
190
|
+
return true // 自身故障 → fail-open 放行
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
100
194
|
/** Q0: DB 不可用(core: stderr + 2) */
|
|
101
195
|
protected emitQ0(reason: string): number {
|
|
102
196
|
process.stderr.write(
|