import fs from "node:fs"; import path from "node:path"; import { probeAcpBackend } from "../acp/probe.js"; import { defaultStateFile, type AppConfig } from "../config.js"; import { RoleRegistry } from "../roles/role-registry.js"; import { printUrlHints } from "./net.js"; export async function runDoctor(config: AppConfig, configPath: string): Promise { let problems = 0; console.log(`Config: ${configPath}`); console.log(`Server: ${config.server.host}:${config.server.port}`); try { new RoleRegistry(config); console.log(`Default role: ${config.defaultRole}`); } catch (error) { problems += reportError(error instanceof Error ? error.message : String(error)); } for (const role of config.roles) { if (!fs.existsSync(role.workspace)) problems += reportError(`role '${role.id}' workspace does not exist: ${role.workspace}`); if (role.policy.permissionMode === "auto") console.log(`WARN: role '${role.id}' auto-approves every permission request.`); if (role.policy.permissionMode === "allowlist" && role.policy.allowedTools.includes("bash") && role.policy.allowedCommandPatterns.length === 0) { console.log(`WARN: role '${role.id}' allows bash by name but has no command patterns; bash requests will be denied.`); } } for (const backend of config.backends) { try { const initialized = await probeAcpBackend(backend, config.acp.initializeTimeoutMs); const capabilities = initialized.agentCapabilities; console.log(`ACP '${backend.id}': ready (${initialized.agentInfo?.name || "unknown"} ${initialized.agentInfo?.version || ""})`); console.log(` load=${Boolean(capabilities?.loadSession)} resume=${Boolean(capabilities?.sessionCapabilities?.resume)} list=${Boolean(capabilities?.sessionCapabilities?.list)} close=${Boolean(capabilities?.sessionCapabilities?.close)}`); if (!capabilities?.loadSession && !capabilities?.sessionCapabilities?.resume) problems += reportError(`backend '${backend.id}' cannot restore sessions`); console.log(" model config: not advertised by initialize; using the Kimi default model"); } catch (error) { problems += reportError(`ACP '${backend.id}' initialize failed: ${error instanceof Error ? error.message : String(error)}`); } } const stateFile = defaultStateFile(config); try { const stateDirectory = path.dirname(stateFile); fs.mkdirSync(stateDirectory, { recursive: true }); fs.accessSync(stateDirectory, fs.constants.R_OK | fs.constants.W_OK); console.log(`State file: ${stateFile}`); } catch (error) { problems += reportError(`state directory is unavailable: ${error instanceof Error ? error.message : String(error)}`); } const enabledPlatforms = Object.entries(config.platforms).filter(([, value]) => value.enabled).map(([name]) => name); console.log(`Enabled platforms: ${enabledPlatforms.join(", ") || "none"}`); if (config.platforms.qq.enabled) { console.log(`QQ connection mode: ${config.platforms.qq.connectionMode}`); if (!config.platforms.qq.appId) problems += reportError("QQ appId is empty."); if (!config.platforms.qq.clientSecret || config.platforms.qq.clientSecret === "replace-me") console.log("WARN: QQ clientSecret is missing or placeholder."); if (config.platforms.qq.connectionMode === "webhook" && config.platforms.qq.verifySignature && !(config.platforms.qq.botSecret || config.platforms.qq.clientSecret)) { problems += reportError("QQ botSecret or clientSecret is required for webhook signature verification."); } } await printUrlHints(config); return problems > 0 ? 1 : 0; } function reportError(message: string): 1 { console.log(`ERROR: ${message}`); return 1; }