Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions scripts/unit-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,31 @@ 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');
});
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', () => {
assert.equal(isSensitivePath('.env'), true);
Expand Down
6 changes: 3 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function taskFromArgs(args, cfg) {
}

/**
* 后台投递一次评审(serve 编排执行,不阻塞前台、不受会话超时约束)。
* 后台投递一次评审(serve 编排执行,引擎 --timeout 默认 15 分钟,插件进程级兜底 6 小时)。
*
* @param {object} args 工具参数。
* @param {object} cfg 归一化配置。
Expand Down Expand Up @@ -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'),
};
Expand Down Expand Up @@ -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 模式的提交号);',
Expand Down
7 changes: 7 additions & 0 deletions src/review-exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ async function executeReviewInner(task) {
excludePaths: task.excludePaths,
concurrencyEnabled: cfg.concurrency_enabled === true,
concurrency: cfg.concurrency,
// 引擎 --timeout:从进程级兜底 review_timeout_ms 折算为分钟(issue #9)。
// 经 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,
});

// 4. 评审模型解析 + key 注入(零落盘:key 只进子进程 env)。
Expand Down
12 changes: 12 additions & 0 deletions src/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ function commandOnPath(cmd) {
* @param {boolean} [opts.concurrencyEnabled] 并发评审开关。
* false(默认)= 单线顺序审查;true = 并行审查(token 消耗增加,用户侧成本控制)。
* @param {number} [opts.concurrency] 并发广度(1..32,默认 8);仅开关开启时生效。
* @param {number} [opts.timeoutMinutes] 引擎 --timeout(并发任务总预算,分钟);
* ≥0 时透传(0 = unlimited,引擎 --timeout 0 语义);
* 不传或非有限/负数 → 不加 --timeout(引擎默认 15 分钟);
* 正值不足 1 分钟按 1 分钟计(防亚分钟值 round 成 0 误开 unlimited)。
* @returns {string[]} 参数字符串数组。
*/
export function buildReviewArgs(opts) {
Expand Down Expand Up @@ -222,6 +226,14 @@ export function buildReviewArgs(opts) {
concurrency = CONCURRENCY_MIN; // 单线
}
args.push('--concurrency', String(concurrency));
// 引擎 --timeout:并发任务总预算(分钟),0=unlimited,引擎默认 15 分钟。
// 数据契约:非有限(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');
return args;
Expand Down
Loading