✨ feat(vscode): 头脑风暴 profile 独立 + codex 命令统一组装收尾
- 头脑风暴/实施/测试会话 profile 彻底独立,互不回退,各自空值回落默认 - 新建工单弹窗的 profile 选择改为头脑风暴专用 brainstormProfilePath - 详情面板新增「头脑风暴配置文件」下拉 - 删除从未接线的死代码 reviewFlow.ts(runReview 全仓库无调用方) - codexCommand 精简为仅 resume/interactive 两种终端命令形态 - 版本号 0.2.55 → 0.2.60
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Single source of truth for assembling `codex` invocations.
|
||||
*
|
||||
* The review flow shells codex three different ways — headless
|
||||
* `codex exec review` (spawned as an arg array), `codex resume <id>` and the
|
||||
* interactive `codex '<prompt>'` TUI (both sent to a terminal). Building the
|
||||
* The review flow shells codex two ways — `codex resume <id>` and the
|
||||
* interactive `codex '<prompt>'` TUI — both sent to a terminal. Building the
|
||||
* flags in one place keeps `--dangerously-bypass-approvals-and-sandbox` and the
|
||||
* optional model / reasoning-effort `-c` overrides from drifting apart.
|
||||
*
|
||||
@@ -12,18 +11,16 @@
|
||||
*/
|
||||
|
||||
export interface CodexCommandOpts {
|
||||
/** 'exec-review' = `codex exec review ...`;'resume' = `codex resume <id>`;'interactive' = `codex '<prompt>'` */
|
||||
mode: 'exec-review' | 'resume' | 'interactive'
|
||||
/** 'resume' = `codex resume <id>`;'interactive' = `codex '<prompt>'` */
|
||||
mode: 'resume' | 'interactive'
|
||||
/** resume 模式必填:codex thread id */
|
||||
sessionId?: string
|
||||
/** interactive / exec-review 模式的 prompt(positional)。调用方保证 interactive 模式下不含单引号。 */
|
||||
/** interactive 模式的 prompt(positional)。调用方保证不含单引号。 */
|
||||
prompt?: string
|
||||
/** 空 = 不传 `-c model=`,用 codex 默认 */
|
||||
model?: string
|
||||
/** 空 = 不传 `-c model_reasoning_effort=`,用 codex 默认。合法值 minimal/low/medium/high/xhigh */
|
||||
reasoningEffort?: string
|
||||
/** exec-review 模式:追加 --json */
|
||||
json?: boolean
|
||||
}
|
||||
|
||||
const BYPASS_FLAG = '--dangerously-bypass-approvals-and-sandbox'
|
||||
@@ -43,39 +40,11 @@ function configOverrides(opts: CodexCommandOpts): string[] {
|
||||
return parts
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared assembly for both output shapes. `quotePrompt` wraps the positional
|
||||
* prompt in single quotes for the shell-string form; the spawn-array form
|
||||
* passes it raw (no shell, so no quoting).
|
||||
*/
|
||||
function buildParts(opts: CodexCommandOpts, quotePrompt: boolean): string[] {
|
||||
const overrides = configOverrides(opts)
|
||||
const prompt = opts.prompt
|
||||
? (quotePrompt ? `'${opts.prompt}'` : opts.prompt)
|
||||
: undefined
|
||||
switch (opts.mode) {
|
||||
case 'exec-review':
|
||||
return [
|
||||
'exec',
|
||||
'review',
|
||||
...overrides,
|
||||
BYPASS_FLAG,
|
||||
...(opts.json ? ['--json'] : []),
|
||||
...(prompt !== undefined ? [prompt] : []),
|
||||
]
|
||||
case 'resume':
|
||||
return ['resume', ...overrides, BYPASS_FLAG, ...(opts.sessionId ? [opts.sessionId] : [])]
|
||||
case 'interactive':
|
||||
return [...overrides, BYPASS_FLAG, ...(prompt !== undefined ? [prompt] : [])]
|
||||
}
|
||||
}
|
||||
|
||||
/** Argument array (without the leading `codex`) for `spawn('codex', args)`. */
|
||||
export function buildCodexArgs(opts: CodexCommandOpts): string[] {
|
||||
return buildParts(opts, false)
|
||||
}
|
||||
|
||||
/** Full shell command string (with `codex` prefix) for `terminal.sendText`. */
|
||||
export function buildCodexCommandString(opts: CodexCommandOpts): string {
|
||||
return ['codex', ...buildParts(opts, true)].join(' ')
|
||||
const overrides = configOverrides(opts)
|
||||
const parts = opts.mode === 'resume'
|
||||
? ['resume', ...overrides, BYPASS_FLAG, ...(opts.sessionId ? [opts.sessionId] : [])]
|
||||
: [...overrides, BYPASS_FLAG, ...(opts.prompt ? [`'${opts.prompt}'`] : [])]
|
||||
return ['codex', ...parts].join(' ')
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* Spawns `codex exec review --json` headlessly and waits for it to finish.
|
||||
*
|
||||
* Output handling: codex itself is responsible for posting the review back
|
||||
* as a PR comment (instructed via the review prompt). This wrapper only
|
||||
* scans codex's NDJSON stdout for the very first `thread.started` event so
|
||||
* the caller can persist the `thread_id` (review session id) for manual
|
||||
* resume; everything after that is ignored. Failures are logged but never
|
||||
* thrown — the webhook coordinator treats review runs as fire-and-forget.
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { logger } from '../logging/logger'
|
||||
import { buildCodexArgs } from './codexCommand'
|
||||
|
||||
export interface RunReviewOpts {
|
||||
workspaceRoot: string
|
||||
/** Already-substituted prompt — placeholders must be resolved by the caller. */
|
||||
prompt: string
|
||||
/** 空 = 不传 `-c model=`,用 codex config.toml 默认。 */
|
||||
model?: string
|
||||
/** 空 = 不传 `-c model_reasoning_effort=`,用 codex 默认。 */
|
||||
reasoningEffort?: string
|
||||
/** Spawn timeout in milliseconds. Defaults to 5 minutes. */
|
||||
timeoutMs?: number
|
||||
/**
|
||||
* Called once with the codex `thread_id` parsed from the first
|
||||
* `thread.started` NDJSON event. Errors thrown inside this callback are
|
||||
* caught and logged — they never propagate out of `runReview`.
|
||||
*/
|
||||
onThreadId?: (id: string) => void | Promise<void>
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 300_000
|
||||
|
||||
/**
|
||||
* Run `codex exec review --json` and wait for it to exit. Errors are
|
||||
* logged as warnings; this function never throws.
|
||||
*/
|
||||
export async function runReview(opts: RunReviewOpts): Promise<void> {
|
||||
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
||||
const args = buildCodexArgs({
|
||||
mode: 'exec-review',
|
||||
prompt: opts.prompt,
|
||||
model: opts.model,
|
||||
reasoningEffort: opts.reasoningEffort,
|
||||
json: true,
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
let child: ReturnType<typeof spawn>
|
||||
try {
|
||||
child = spawn('codex', args, {
|
||||
cwd: opts.workspaceRoot,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'codex exec review 启动失败',
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let timedOut = false
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
}
|
||||
catch {
|
||||
// ignore
|
||||
}
|
||||
}, timeoutMs)
|
||||
|
||||
// NDJSON parsing: accumulate stdout, split on newline, parse each line.
|
||||
// Stop parsing after the first `thread.started` event — let the rest
|
||||
// of stdout drain to /dev/null so codex can keep running.
|
||||
let buf = ''
|
||||
let threadIdHandled = false
|
||||
let stderrTail = ''
|
||||
|
||||
let diagLogged = 0
|
||||
const handleLine = (line: string): void => {
|
||||
if (threadIdHandled || !line.trim())
|
||||
return
|
||||
// Diagnostic: log the first 3 lines so we can inspect codex's actual
|
||||
// event shape if thread id detection misses.
|
||||
if (diagLogged < 3) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `codex stdout[${diagLogged}]`,
|
||||
details: line.slice(0, 500),
|
||||
})
|
||||
diagLogged += 1
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(line)
|
||||
}
|
||||
catch {
|
||||
return
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object')
|
||||
return
|
||||
const obj = parsed as { type?: unknown, thread_id?: unknown, session_id?: unknown, id?: unknown }
|
||||
// Try multiple field names: thread_id (current), session_id (older
|
||||
// codex), id (fallback when the event is a "session created" shape).
|
||||
// Accept any event so long as it carries one of these as a string.
|
||||
const candidate
|
||||
= (typeof obj.thread_id === 'string' && obj.thread_id.length > 0 && obj.thread_id)
|
||||
|| (typeof obj.session_id === 'string' && obj.session_id.length > 0 && obj.session_id)
|
||||
|| (typeof obj.id === 'string' && obj.id.length > 0 && /^[0-9a-f-]{16,}$/i.test(obj.id) && obj.id)
|
||||
|| ''
|
||||
if (candidate) {
|
||||
threadIdHandled = true
|
||||
const id = candidate
|
||||
if (opts.onThreadId) {
|
||||
try {
|
||||
const ret = opts.onThreadId(id)
|
||||
if (ret && typeof (ret as Promise<unknown>).then === 'function') {
|
||||
;(ret as Promise<unknown>).catch((err) => {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'onThreadId 回调异常',
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'onThreadId 回调抛错',
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout?.setEncoding('utf-8')
|
||||
child.stdout?.on('data', (chunk: string) => {
|
||||
if (threadIdHandled)
|
||||
return
|
||||
buf += chunk
|
||||
let idx = buf.indexOf('\n')
|
||||
while (idx >= 0) {
|
||||
const line = buf.slice(0, idx)
|
||||
buf = buf.slice(idx + 1)
|
||||
handleLine(line)
|
||||
if (threadIdHandled) {
|
||||
buf = ''
|
||||
break
|
||||
}
|
||||
idx = buf.indexOf('\n')
|
||||
}
|
||||
})
|
||||
|
||||
child.stderr?.setEncoding('utf-8')
|
||||
child.stderr?.on('data', (chunk: string) => {
|
||||
// Keep only the tail so we can include it in the failure message.
|
||||
stderrTail = (stderrTail + chunk).slice(-2000)
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
clearTimeout(timer)
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: 'codex exec review 进程错误',
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
clearTimeout(timer)
|
||||
if (timedOut) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `codex exec review 超时 (${timeoutMs}ms)`,
|
||||
details: stderrTail.slice(-500),
|
||||
})
|
||||
}
|
||||
else if ((code !== null && code !== 0) || (signal && !timedOut)) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `codex exec review 退出码异常 code=${code} signal=${signal ?? '<none>'}`,
|
||||
details: stderrTail.slice(-500),
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user