✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下

This commit is contained in:
cruldra committed 2026-09-26 21:12:47 +08:00
1 parent badc722640
commit fb53bb727c
34 files changed
+2726 -43

No files matched your search

+121 -12
View File
@@ -58,6 +58,14 @@ export class ClaudeTimeoutError extends ClaudeError {
}
}
/** 调用方叫停(AbortSignal)时进程已被结束。 */
export class ClaudeCancelledError extends ClaudeError {
constructor(stderr: string) {
super('Claude 调用已停止', stderr)
this.name = 'ClaudeCancelledError'
}
}
interface ParsedClaudeJson {
session_id: string
result: string
@@ -185,21 +193,33 @@ export type ClaudeProgressListener = (label: string) => void
/**
* 按行切 stdout:stream-json 每行一个对象,但 data 事件的 chunk 边界和行边界
* 无关,一行常被拆进两个 chunk,所以末尾那段不完整的残行要留到下次拼上。
* 无关,一行常被拆进两个 chunk,所以末尾那段不完整的残行要留到下次拼上;
* 进程结束时 flush 交出最后那段没带换行的。
*/
function createLineFeeder(onLine: (line: string) => void): { feed: (chunk: string) => void, flush: () => void } {
let pending = ''
return {
feed: (chunk) => {
pending += chunk
const lines = pending.split('\n')
pending = lines.pop() ?? ''
lines.forEach(onLine)
},
flush: () => {
if (pending !== '')
onLine(pending)
pending = ''
},
}
}
function createProgressFeeder(onProgress?: ClaudeProgressListener): (chunk: string) => void {
if (!onProgress)
return () => {}
let pending = ''
return (chunk) => {
pending += chunk
const lines = pending.split('\n')
pending = lines.pop() ?? ''
for (const line of lines) {
for (const label of summarizeClaudeStreamLine(line))
onProgress(label)
}
}
return createLineFeeder((line) => {
for (const label of summarizeClaudeStreamLine(line))
onProgress(label)
}).feed
}
function buildStreamJsonLine(prompt: string, images: ClaudeImage[]): string {
@@ -233,7 +253,7 @@ function buildStreamJsonLine(prompt: string, images: ClaudeImage[]): string {
* - CLAUDECODE / CLAUDE_CODE_ENTRYPOINT / CLAUDE_CODE_SESSION_ID / CLAUDE_CODE_SESSION:
* 从父 Claude 会话继承会让子进程当成嵌套会话直接静默退出、零输出。
*/
const CLAUDE_MISSING_MSG = '未检测到 claude CLI,请确认已安装并在 PATH 中(常见位置 ~/.local/bin)'
export const CLAUDE_MISSING_MSG = '未检测到 claude CLI,请确认已安装并在 PATH 中(常见位置 ~/.local/bin)'
function buildClaudeChildEnv(bin?: string): NodeJS.ProcessEnv {
// 剔除所有 ANTHROPIC_* 是为了让 `--settings` profile 成为 provider/鉴权的唯一来源
@@ -537,3 +557,92 @@ function spawnClaudeStreamed(
}
})
}
/**
* 跑一次 claude(参数由调用方拼好,一般带 `--output-format stream-json`),stdout 每出来完整的一行就交给 onLine。
* 正常退出时 resolve;找不到 claude、退出码非零、输出过大时 reject ClaudeError,超时 ClaudeTimeoutError,
* signal 叫停时结束进程并 reject ClaudeCancelledError。
*/
export function runClaudeLines(opts: {
args: string[]
cwd: string
onLine: (line: string) => void
timeoutMs?: number
signal?: AbortSignal
/** 默认按 resolveClaudeBin 找 */
bin?: string
}): Promise<void> {
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
const bin = opts.bin ?? resolveClaudeBin()
if (!bin)
return Promise.reject(new ClaudeError(CLAUDE_MISSING_MSG, ''))
if (opts.signal?.aborted)
return Promise.reject(new ClaudeCancelledError(''))
return new Promise<void>((resolve, reject) => {
let stdout = ''
let stderr = ''
let stdoutBytes = 0
let settled = false
const lines = createLineFeeder(opts.onLine)
const child = spawn(bin, opts.args, {
cwd: opts.cwd,
env: buildClaudeChildEnv(bin),
stdio: ['ignore', 'pipe', 'pipe'],
})
const timer = setTimeout(() => {
child.kill('SIGKILL')
settle(new ClaudeTimeoutError(`Claude 调用超时(${Math.round(timeoutMs / 1000)}s)`, stderr))
}, timeoutMs)
function onAbort(): void {
child.kill('SIGTERM')
settle(new ClaudeCancelledError(stderr))
}
function settle(error?: ClaudeError): void {
if (settled)
return
settled = true
clearTimeout(timer)
opts.signal?.removeEventListener('abort', onAbort)
if (error)
reject(error)
else
resolve()
}
opts.signal?.addEventListener('abort', onAbort, { once: true })
child.on('error', (err) => {
const errCode = (err as NodeJS.ErrnoException).code
settle(new ClaudeError(errCode === 'ENOENT' ? CLAUDE_MISSING_MSG : `Claude 调用失败: ${err.message}`, stderr))
})
child.stdout.on('data', (chunk: Buffer) => {
if (settled)
return
stdoutBytes += chunk.length
if (stdoutBytes > MAX_BUFFER_BYTES) {
child.kill('SIGKILL')
settle(new ClaudeError(`Claude 输出超过 ${MAX_BUFFER_BYTES} bytes`, stderr))
return
}
const text = chunk.toString('utf8')
stdout += text
lines.feed(text)
})
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString('utf8')
})
child.on('close', (code) => {
if (settled)
return
lines.flush()
settle(code === 0
? undefined
: new ClaudeError(`Claude 退出码非零 (${code}): ${buildClaudeExitDiagnostic(stderr, stdout)}`, stderr))
})
})
}