From 6bc6e7d05e4aec2a937b4c0e3f71ad0e38acb4a0 Mon Sep 17 00:00:00 2001 From: Nicholas023 Date: Tue, 20 Oct 2026 02:11:17 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(issue-9):=20=E5=B7=A5=E5=85=B7=20timeou?= =?UTF-8?q?t=20=E6=96=87=E6=A1=88=E5=A4=B1=E5=AE=9E=E2=80=94=E2=80=94build?= =?UTF-8?q?ReviewArgs=20=E9=80=8F=E4=BC=A0=20--timeout=20+=20=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E6=94=B9=E5=87=86=E7=A1=AE=20(closes=20#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/unit-smoke.mjs | 15 +++++++++++++++ src/index.js | 6 +++--- src/review-exec.js | 6 ++++++ src/runner.js | 7 +++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/scripts/unit-smoke.mjs b/scripts/unit-smoke.mjs index 6015adc..a9aefd4 100644 --- a/scripts/unit-smoke.mjs +++ b/scripts/unit-smoke.mjs @@ -107,6 +107,21 @@ t('并发关闭/缺省:一律显式 --concurrency 1(防引擎默认 8 漂移 assert.equal(absent[absent.indexOf('--concurrency') + 1], '1'); }); +t('timeoutMinutes:传 360 → --timeout 360', () => { + const args = buildReviewArgs({ command: 'scan', repoPath: '/repo', timeoutMinutes: 360 }); + assert.ok(args.includes('--timeout'), '应含 --timeout,实际:' + args.join(' ')); + assert.equal(args[args.indexOf('--timeout') + 1], '360'); +}); +t('timeoutMinutes:不传 → 无 --timeout(引擎默认 15 分钟)', () => { + const args = buildReviewArgs({ command: 'scan', repoPath: '/repo' }); + assert.ok(!args.includes('--timeout'), '不应含 --timeout,实际:' + args.join(' ')); +}); +t('timeoutMinutes:传 0 → --timeout 0(unlimited)', () => { + const args = buildReviewArgs({ command: 'scan', repoPath: '/repo', timeoutMinutes: 0 }); + assert.ok(args.includes('--timeout'), '应含 --timeout,实际:' + args.join(' ')); + assert.equal(args[args.indexOf('--timeout') + 1], '0'); +}); + /* ---------- 2. 敏感路径 ---------- */ t('敏感命中:.env / .env.production', () => { assert.equal(isSensitivePath('.env'), true); diff --git a/src/index.js b/src/index.js index a78f213..197c20f 100644 --- a/src/index.js +++ b/src/index.js @@ -106,7 +106,7 @@ function taskFromArgs(args, cfg) { } /** - * 后台投递一次评审(serve 编排执行,不阻塞前台、不受会话超时约束)。 + * 后台投递一次评审(serve 编排执行,引擎 --timeout 默认 15 分钟,插件进程级兜底 6 小时)。 * * @param {object} args 工具参数。 * @param {object} cfg 归一化配置。 @@ -163,7 +163,7 @@ async function dispatchReview(args, cfg, opts = {}) { `- 命令: codecheck ${task.command}${task.repoPath ? ` --repo ${task.repoPath}` : ''}`, `- 任务簿: ${taskPath}`, '', - '评审在后台 serve 中执行,**不阻塞会话、不受超时限制**(慢模型也没关系)。', + '评审超时由引擎 --timeout 控制(默认 15 分钟),插件进程级兜底 6 小时。', '随时用 code_review_status 工具查状态取结果;完成后报告自动落盘留档。', ].join('\n'), }; @@ -228,7 +228,7 @@ export function createReviewTool(ctx, getConfig) { '对代码仓库执行智能代码审查:调用引擎 codecheck(open-code-review 的 fork)读取 git diff 或做全目录扫描,', '用评审模型按三个固定视角挑刺:① 安全与脱敏(硬编码密钥/口令/PII/内部信息泄露 + 安全漏洞);', '② 找 bug(逻辑/边界/异常/并发);③ 冗余与任务无关代码(scope creep/死代码)。', - '【后台执行】发起后立即返回任务 id(任务卡),评审由后台 serve 异步执行:不阻塞会话、不受会话超时限制、慢模型也没关系。', + '【后台执行】发起后立即返回任务 id(任务卡),评审由后台 serve 异步执行:引擎 --timeout 默认 15 分钟,插件进程级兜底 6 小时。', '随后用 code_review_status 工具查状态取结果;完成后报告自动落盘留档(会话内仍可拿完整问题清单)。', '参数:repo_path(仓库路径,省略时尝试取会话工作目录,取不到任务失败并提示);mode(review 模式:uncommitted 工作区未提交 / branch 分支对比 / commit 指定提交;或 scan 全目录扫描);', 'base_ref(branch 模式的对比基线,默认 main);head_ref(branch 模式的目标分支);commit(commit 模式的提交号);', diff --git a/src/review-exec.js b/src/review-exec.js index 31ef573..bc687a9 100644 --- a/src/review-exec.js +++ b/src/review-exec.js @@ -156,6 +156,12 @@ async function executeReviewInner(task) { excludePaths: task.excludePaths, concurrencyEnabled: cfg.concurrency_enabled === true, concurrency: cfg.concurrency, + // 引擎 --timeout:从进程级兜底 review_timeout_ms 折算为分钟(issue #9)。 + // 有限且 ≥0 → 折算分钟透传(0 → 0=unlimited,引擎 --timeout 0 语义); + // 非有限/负数 → undefined(不加该 flag,引擎回退默认 15 分钟)。 + timeoutMinutes: Number.isFinite(cfg.review_timeout_ms) && cfg.review_timeout_ms >= 0 + ? cfg.review_timeout_ms / 60_000 + : undefined, }); // 4. 评审模型解析 + key 注入(零落盘:key 只进子进程 env)。 diff --git a/src/runner.js b/src/runner.js index 93f4764..691e2e1 100644 --- a/src/runner.js +++ b/src/runner.js @@ -184,6 +184,8 @@ function commandOnPath(cmd) { * @param {boolean} [opts.concurrencyEnabled] 并发评审开关。 * false(默认)= 单线顺序审查;true = 并行审查(token 消耗增加,用户侧成本控制)。 * @param {number} [opts.concurrency] 并发广度(1..32,默认 8);仅开关开启时生效。 + * @param {number} [opts.timeoutMinutes] 引擎 --timeout(并发任务总预算,分钟); + * 0 = unlimited;不传或非负数以外 → 不加 --timeout(引擎默认 15 分钟)。 * @returns {string[]} 参数字符串数组。 */ export function buildReviewArgs(opts) { @@ -222,6 +224,11 @@ export function buildReviewArgs(opts) { concurrency = CONCURRENCY_MIN; // 单线 } args.push('--concurrency', String(concurrency)); + // 引擎 --timeout:并发任务总预算(分钟),0=unlimited,引擎默认 15 分钟。 + // 仅在显式传入非负数时透传;否则不加该 flag(引擎回退自身默认 15 分钟)。 + if (typeof opts.timeoutMinutes === 'number' && opts.timeoutMinutes >= 0) { + args.push('--timeout', String(Math.round(opts.timeoutMinutes))); + } // 会话内返回:agent 受众 = 仅摘要(无进度噪音,干净短输出)。 args.push('--audience', 'agent'); return args; From 27a60abf94d640ecf258f1d56fbc6d9b210925a9 Mon Sep 17 00:00:00 2001 From: Nicholas023 Date: Tue, 20 Oct 2026 02:25:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(issue-9):=20timeoutMinutes=20=E4=BA=9A?= =?UTF-8?q?=E5=88=86=E9=92=9F=E5=80=BC=E6=8C=89=201=20=E5=88=86=E9=92=9F?= =?UTF-8?q?=E8=AE=A1=E3=80=81=E9=9D=9E=E6=9C=89=E9=99=90=E5=80=BC=E4=B8=8D?= =?UTF-8?q?=E5=8A=A0=20--timeout=EF=BC=8C=E8=A1=A5=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E4=B8=8E=E6=B3=A8=E9=87=8A=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/unit-smoke.mjs | 10 ++++++++++ src/review-exec.js | 5 +++-- src/runner.js | 13 +++++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/scripts/unit-smoke.mjs b/scripts/unit-smoke.mjs index a9aefd4..c1a6779 100644 --- a/scripts/unit-smoke.mjs +++ b/scripts/unit-smoke.mjs @@ -121,6 +121,16 @@ t('timeoutMinutes:传 0 → --timeout 0(unlimited)', () => { assert.ok(args.includes('--timeout'), '应含 --timeout,实际:' + args.join(' ')); assert.equal(args[args.indexOf('--timeout') + 1], '0'); }); +t('timeoutMinutes:传负数 → 无 --timeout(引擎默认 15 分钟)', () => { + const args = buildReviewArgs({ command: 'scan', repoPath: '/repo', timeoutMinutes: -1 }); + assert.ok(!args.includes('--timeout'), '不应含 --timeout,实际:' + args.join(' ')); +}); +t('timeoutMinutes:传 NaN / Infinity → 无 --timeout(引擎默认 15 分钟)', () => { + const nanArgs = buildReviewArgs({ command: 'scan', repoPath: '/repo', timeoutMinutes: NaN }); + assert.ok(!nanArgs.includes('--timeout'), 'NaN 不应含 --timeout,实际:' + nanArgs.join(' ')); + const infArgs = buildReviewArgs({ command: 'scan', repoPath: '/repo', timeoutMinutes: Infinity }); + assert.ok(!infArgs.includes('--timeout'), 'Infinity 不应含 --timeout,实际:' + infArgs.join(' ')); +}); /* ---------- 2. 敏感路径 ---------- */ t('敏感命中:.env / .env.production', () => { diff --git a/src/review-exec.js b/src/review-exec.js index bc687a9..990e154 100644 --- a/src/review-exec.js +++ b/src/review-exec.js @@ -157,8 +157,9 @@ async function executeReviewInner(task) { concurrencyEnabled: cfg.concurrency_enabled === true, concurrency: cfg.concurrency, // 引擎 --timeout:从进程级兜底 review_timeout_ms 折算为分钟(issue #9)。 - // 有限且 ≥0 → 折算分钟透传(0 → 0=unlimited,引擎 --timeout 0 语义); - // 非有限/负数 → undefined(不加该 flag,引擎回退默认 15 分钟)。 + // 经 normalizeConfig 后 review_timeout_ms ∈ [1, REVIEW_TIMEOUT_MAX](见 config.js 钳制), + // 此处 ≥0 分支为防御性守卫(0 值正常流程走不到):非有限/负数 → undefined, + // 不加 --timeout flag,引擎回退默认 15 分钟。 timeoutMinutes: Number.isFinite(cfg.review_timeout_ms) && cfg.review_timeout_ms >= 0 ? cfg.review_timeout_ms / 60_000 : undefined, diff --git a/src/runner.js b/src/runner.js index 691e2e1..17b3916 100644 --- a/src/runner.js +++ b/src/runner.js @@ -185,7 +185,9 @@ function commandOnPath(cmd) { * false(默认)= 单线顺序审查;true = 并行审查(token 消耗增加,用户侧成本控制)。 * @param {number} [opts.concurrency] 并发广度(1..32,默认 8);仅开关开启时生效。 * @param {number} [opts.timeoutMinutes] 引擎 --timeout(并发任务总预算,分钟); - * 0 = unlimited;不传或非负数以外 → 不加 --timeout(引擎默认 15 分钟)。 + * ≥0 时透传(0 = unlimited,引擎 --timeout 0 语义); + * 不传或非有限/负数 → 不加 --timeout(引擎默认 15 分钟); + * 正值不足 1 分钟按 1 分钟计(防亚分钟值 round 成 0 误开 unlimited)。 * @returns {string[]} 参数字符串数组。 */ export function buildReviewArgs(opts) { @@ -225,9 +227,12 @@ export function buildReviewArgs(opts) { } args.push('--concurrency', String(concurrency)); // 引擎 --timeout:并发任务总预算(分钟),0=unlimited,引擎默认 15 分钟。 - // 仅在显式传入非负数时透传;否则不加该 flag(引擎回退自身默认 15 分钟)。 - if (typeof opts.timeoutMinutes === 'number' && opts.timeoutMinutes >= 0) { - args.push('--timeout', String(Math.round(opts.timeoutMinutes))); + // 数据契约:非有限(NaN/Infinity)或负数不加该 flag(引擎回退自身默认 15 分钟); + // 正值不足 1 分钟按 1 分钟计(防 0.167min round 成 0 误开 unlimited,与用户意图相反)。 + if (typeof opts.timeoutMinutes === 'number' && Number.isFinite(opts.timeoutMinutes) && opts.timeoutMinutes >= 0) { + // 显式 0 保留 unlimited 语义;其余正值至少 1 分钟。 + const minutes = opts.timeoutMinutes === 0 ? 0 : Math.max(1, Math.round(opts.timeoutMinutes)); + args.push('--timeout', String(minutes)); } // 会话内返回:agent 受众 = 仅摘要(无进度噪音,干净短输出)。 args.push('--audience', 'agent');