feat(round2+round3): 完整实现 A/B/C/D 主线 + round3-F/H 修复
Round2 主线:
- A: 事件落库地基 (RuntimeApp EventStore 单例 + 14 repo wiring)
- B: 执行体对齐 (read-before-edit, verification-before-completion)
- C: 界面对齐 (@opentui/solid, 删除 runtime 依赖)
- D: 经验闭环 (ExperienceMiner, DebuggerRole, CompactorRole)
Round2 补充修复:
- fail-on-missing 反作弊门禁
- projection-store-apply.test.ts 补写
- 3个空壳测试转行为 (evidence-store, recovery-impl, knowledge-store)
- ask 项目根支持 AIRCODING_PROJECT_ROOT
- Worker 事件契约修复 (task.attempt.started → checkpoint)
Round3-F: cpp 工具切换
- 删除 BuiltInToolRegistrar cpp.* 闭包
- 接入 toolchain-cpp 真实 CppToolRegistrar
- canonical envelope {status/output/metadata}
- ExecutorRole system prompt 对齐新工具名
Round3-H: Doctor 5 类报告
- toolchain (cmake/ninja/cppcheck/clangd/g++)
- display (X11/Wayland + ImageMagick)
- network (internet connectivity)
- provider (api_key/base_url/model/connectivity)
Secret 脱敏:
- 状态交接.md: sk- → \${OPENAI_API_KEY}
- .gitignore: 添加 .air/ .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import { execFileSync } from 'child_process'
|
||||
|
||||
export interface DoctorCheck {
|
||||
name: string
|
||||
category: 'self_bootstrap' | 'capability' | 'project' | 'runtime'
|
||||
category: 'self_bootstrap' | 'capability' | 'project' | 'runtime' | 'toolchain' | 'display' | 'network' | 'provider'
|
||||
passed: boolean
|
||||
message: string
|
||||
fixable: boolean
|
||||
@@ -66,6 +66,14 @@ export class DoctorService {
|
||||
checks.push(this.check_capability_deps())
|
||||
}
|
||||
|
||||
// FR-018/§6.12: toolchain / display / network / provider checks
|
||||
if (scope === 'all') {
|
||||
checks.push(...this.check_cpp_toolchain())
|
||||
checks.push(this.check_display())
|
||||
checks.push(await this.check_network())
|
||||
checks.push(...await this.check_provider())
|
||||
}
|
||||
|
||||
const all_passed = checks.every(c => c.passed)
|
||||
return { checks, all_passed, bootstrap_passed: true, fixable_count: checks.filter(c => c.fixable).length }
|
||||
}
|
||||
@@ -219,4 +227,98 @@ export class DoctorService {
|
||||
return { name: 'capability_deps', category: 'capability', passed: false, message: `Capability check failed: ${e.message}`, fixable: true }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== FR-018/§6.12: 5 new categories =====
|
||||
|
||||
private check_cpp_toolchain(): DoctorCheck[] {
|
||||
const tools = ['cmake', 'ninja', 'cppcheck', 'clangd', 'g++']
|
||||
const reports: DoctorCheck[] = []
|
||||
for (const t of tools) {
|
||||
try {
|
||||
const v = execFileSync('which', [t], { stdio: 'pipe', timeout: 3000 }).toString().trim()
|
||||
reports.push({ name: `toolchain.${t}`, category: 'toolchain', passed: true, message: `${t} found at ${v}`, fixable: false })
|
||||
} catch {
|
||||
reports.push({ name: `toolchain.${t}`, category: 'toolchain', passed: false, message: `${t} not found`, fixable: true, fix: `apt install ${t === 'cmake' ? 'cmake' : t === 'ninja' ? 'ninja-build' : t}` })
|
||||
}
|
||||
}
|
||||
return reports
|
||||
}
|
||||
|
||||
private check_display(): DoctorCheck {
|
||||
const display = process.env.DISPLAY
|
||||
const wayland = process.env.WAYLAND_DISPLAY
|
||||
if (!display && !wayland) {
|
||||
return { name: 'display', category: 'display', passed: false, message: 'No DISPLAY/WAYLAND_DISPLAY (gui.screenshot will fail)', fixable: false }
|
||||
}
|
||||
try {
|
||||
execFileSync('which', ['import'], { stdio: 'pipe' })
|
||||
return { name: 'display', category: 'display', passed: true, message: `Display ${display || wayland} + ImageMagick available`, fixable: false }
|
||||
} catch {
|
||||
return { name: 'display', category: 'display', passed: false, message: 'ImageMagick not installed', fixable: true, fix: 'apt install imagemagick' }
|
||||
}
|
||||
}
|
||||
|
||||
private async check_network(): Promise<DoctorCheck> {
|
||||
try {
|
||||
const r = await fetch('https://1.1.1.1', { method: 'HEAD', signal: AbortSignal.timeout(3000) })
|
||||
return { name: 'network.internet', category: 'network', passed: r.ok || r.status > 0, message: `HTTP ${r.status}`, fixable: false }
|
||||
} catch (e: any) {
|
||||
return { name: 'network.internet', category: 'network', passed: false, message: e.message, fixable: false }
|
||||
}
|
||||
}
|
||||
|
||||
private async check_provider(): Promise<DoctorCheck[]> {
|
||||
const reports: DoctorCheck[] = []
|
||||
const apiKey = process.env.AIRCODING_API_KEY || process.env.OPENAI_API_KEY
|
||||
const baseUrl = process.env.OPENAI_BASE_URL || process.env.AIRCODING_API_URL
|
||||
const model = process.env.AIRCODING_MODEL
|
||||
|
||||
reports.push({
|
||||
name: 'provider.api_key',
|
||||
category: 'provider',
|
||||
passed: Boolean(apiKey),
|
||||
message: apiKey ? `API key set (${apiKey.slice(0, 7)}...)` : 'No API key set',
|
||||
fixable: false,
|
||||
})
|
||||
reports.push({
|
||||
name: 'provider.base_url',
|
||||
category: 'provider',
|
||||
passed: Boolean(baseUrl),
|
||||
message: baseUrl ? `Base URL: ${baseUrl}` : 'No base URL set',
|
||||
fixable: false,
|
||||
})
|
||||
reports.push({
|
||||
name: 'provider.model',
|
||||
category: 'provider',
|
||||
passed: Boolean(model),
|
||||
message: model || 'No model set',
|
||||
fixable: false,
|
||||
})
|
||||
|
||||
if (apiKey && baseUrl) {
|
||||
try {
|
||||
const r = await fetch(`${baseUrl.replace(/\/$/, '')}/v1/models`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
reports.push({
|
||||
name: 'provider.connectivity',
|
||||
category: 'provider',
|
||||
passed: r.ok || r.status > 0,
|
||||
message: `HTTP ${r.status}`,
|
||||
fixable: false,
|
||||
})
|
||||
} catch (e: any) {
|
||||
reports.push({
|
||||
name: 'provider.connectivity',
|
||||
category: 'provider',
|
||||
passed: false,
|
||||
message: e.message,
|
||||
fixable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
return reports
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user