import type { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
import { exec, execFile, spawn, spawnSync } from 'node:child_process';
import {
  commandExists,
  decodeOutput,
  getPreferredNpmCommand,
  getSpawnCommandSpec,
  runCommand,
  runCommandSync,
} from '../scripts/utils/command-runtime.mjs';

type ProjectDefaults = {
  defaultTheme?: string | null;
};

type ProjectInfo = {
  name?: string | null;
  description?: string | null;
};

type LegacyPromptClient = 'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode';
type GeniePromptClient = 'genie:claude' | 'genie:cursor' | 'genie:codex' | 'genie:gemini' | 'genie:opencode';
type LocalPromptClient = 'local:cursor' | 'local:qoder';
type PromptClient = GeniePromptClient | LocalPromptClient;
type MainIDE = 'cursor' | 'trae' | 'vscode' | 'trae_cn' | 'windsurf' | 'kiro' | 'qoder' | 'antigravity';

const MAIN_IDE_VALUES: MainIDE[] = ['cursor', 'trae', 'vscode', 'trae_cn', 'windsurf', 'kiro', 'qoder', 'antigravity'];

const MAIN_IDE_APP_NAMES: Record<MainIDE, string> = {
  cursor: 'Cursor',
  trae: 'Trae',
  vscode: 'Visual Studio Code',
  trae_cn: 'Trae CN',
  windsurf: 'Windsurf',
  kiro: 'Kiro',
  qoder: 'Qoder',
  antigravity: 'Antigravity',
};

const MAIN_IDE_WINDOWS_APP_PATH_NAMES: Record<MainIDE, string[]> = {
  cursor: ['Cursor'],
  trae: ['Trae', 'TRAE'],
  vscode: ['Visual Studio Code', 'Visual Studio Code Insiders'],
  trae_cn: ['Trae CN', 'TRAE CN', 'Trae', 'TRAE'],
  windsurf: ['Windsurf'],
  kiro: ['Kiro'],
  qoder: ['Qoder'],
  antigravity: ['Antigravity'],
};

const MAIN_IDE_WINDOWS_COMMAND_CANDIDATES: Record<MainIDE, string[]> = {
  cursor: ['cursor'],
  trae: ['trae'],
  vscode: ['code', 'code-insiders'],
  trae_cn: ['trae-cn', 'trae_cn', 'trae'],
  windsurf: ['windsurf'],
  kiro: ['kiro'],
  qoder: ['qoder'],
  antigravity: ['antigravity'],
};

const MAIN_IDE_WINDOWS_EXECUTABLE_NAMES: Record<MainIDE, string[]> = {
  cursor: ['Cursor.exe'],
  trae: ['TRAE.exe', 'Trae.exe'],
  vscode: ['Code.exe', 'Code - Insiders.exe'],
  trae_cn: ['Trae CN.exe', 'TRAE CN.exe', 'TRAE.exe', 'Trae.exe'],
  windsurf: ['Windsurf.exe'],
  kiro: ['Kiro.exe'],
  qoder: ['Qoder.exe'],
  antigravity: ['Antigravity.exe'],
};

type AutomationConfig = {
  defaultPromptClient?: PromptClient | null;
  defaultIDE?: MainIDE | null;
};

type AssistantConfig = {
  webBaseUrl?: string | null;
  apiBaseUrl?: string | null;
};

type AssistantRuntimeSource = 'axhub-genie' | 'config' | 'cloudcli' | 'env' | 'default';

type AssistantHealthStatus =
  | 'ready'
  | 'missing_cli'
  | 'cli_error'
  | 'runtime_unreachable'
  | 'needs_update';

type AssistantCommandSource = 'axhub-genie' | 'cloudcli' | 'default';

type AssistantHealthHints = {
  installGlobal: string;
  start: string;
  status: string;
};

type AssistantHealthInfo = {
  status: AssistantHealthStatus;
  message: string;
  checkedAt: string;
  commandSource: AssistantCommandSource;
  hints: AssistantHealthHints;
};

type AssistantRuntimeInfo = {
  webBaseUrl: string;
  apiBaseUrl: string;
  projectPath: string;
  source: AssistantRuntimeSource;
  health: AssistantHealthInfo;
};

type AssistantRuntimeResolveOptions = {
  autoStart?: boolean;
};

type AssistantBootstrapMode = 'install_global' | 'start_existing';

type AssistantProbeStatus = 'ready' | 'missing_cli' | 'needs_update' | 'cli_error' | 'not_running';

type AssistantProbeResult = {
  status: AssistantProbeStatus;
  message: string;
  commandSource: Exclude<AssistantCommandSource, 'default'>;
  config: AssistantConfig | null;
};

const ASSISTANT_START_CHECK_DELAY_MS = 500;

type SystemConfig = {
  server: Record<string, any>;
  projectDefaults?: ProjectDefaults;
  projectInfo?: ProjectInfo;
  automation?: AutomationConfig;
  assistant?: AssistantConfig;
};

type AgentDocsPaths = {
  configPath: string;
  agentsTemplatePath: string;
  agentsPath: string;
  claudePath: string;
};

const DEFAULT_ASSISTANT_WEB_BASE_URL = 'http://localhost:32123';
const DEFAULT_ASSISTANT_API_BASE_URL = 'http://localhost:32123/api';
const DEFAULT_ASSISTANT_HEALTH_URL = `${DEFAULT_ASSISTANT_WEB_BASE_URL}/health`;
const ASSISTANT_SERVICE_ID = '@axhub/genie';
const ASSISTANT_SERVICE_NAME = 'Axhub Genie';
const ASSISTANT_RUNTIME_LOG_PREFIX = '[assistant-runtime]';
const ASSISTANT_RUNTIME_DEBUG_LOGS_ENABLED = false;
const ASSISTANT_STATUS_TIMEOUT_MS = 8_000;
const COMMAND_AVAILABILITY_TIMEOUT_MS = 2_000;
const PROJECT_OVERVIEW_DOC_RELATIVE_PATH = 'src/docs/project-overview.md';

function logAssistantRuntime(level: 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>) {
  if (!ASSISTANT_RUNTIME_DEBUG_LOGS_ENABLED && level !== 'error') {
    return;
  }

  const payload = meta ? `${ASSISTANT_RUNTIME_LOG_PREFIX} ${message}` : `${ASSISTANT_RUNTIME_LOG_PREFIX} ${message}`;
  if (level === 'error') {
    if (meta) {
      console.error(payload, meta);
      return;
    }
    console.error(payload);
    return;
  }
  if (level === 'warn') {
    if (meta) {
      console.warn(payload, meta);
      return;
    }
    console.warn(payload);
    return;
  }
  if (meta) {
    console.info(payload, meta);
    return;
  }
  console.info(payload);
}

function getAssistantHealthHints(): AssistantHealthHints {
  return {
    installGlobal: 'npx @axhub/genie',
    start: 'axhub-genie',
    status: 'axhub-genie status',
  };
}

function normalizeOptionalString(value: unknown): string | null {
  if (value === null || value === undefined) return null;
  if (typeof value !== 'string') return null;
  const trimmed = value.trim();
  return trimmed ? trimmed : null;
}

function normalizeInlineText(value: unknown): string | null {
  const normalized = normalizeOptionalString(value);
  if (!normalized) return null;
  return normalized.replace(/\r?\n+/g, ' ').trim() || null;
}

function normalizeProjectDefaults(value: unknown): ProjectDefaults {
  if (!value || typeof value !== 'object') {
    return { defaultTheme: null };
  }
  const defaults = value as ProjectDefaults;
  return {
    defaultTheme: normalizeOptionalString(defaults.defaultTheme)
  };
}

function normalizeProjectInfo(value: unknown): ProjectInfo {
  if (!value || typeof value !== 'object') {
    return { name: null, description: null };
  }
  const info = value as ProjectInfo;
  return {
    name: normalizeInlineText(info.name),
    description: normalizeInlineText(info.description)
  };
}

function normalizePromptClient(value: unknown): PromptClient | null {
  if (value === null || value === undefined) return null;
  if (typeof value !== 'string') return null;

  const normalized = value.trim().toLowerCase();
  if (
    normalized === 'genie:claude'
    || normalized === 'genie:cursor'
    || normalized === 'genie:codex'
    || normalized === 'genie:gemini'
    || normalized === 'genie:opencode'
  ) return normalized;
  if (normalized === 'local:cursor' || normalized === 'local:qoder') return normalized;
  if (normalized === 'claude') return 'genie:claude';
  if (normalized === 'cursor') return 'genie:cursor';
  if (normalized === 'codex') return 'genie:codex';
  if (normalized === 'gemini') return 'genie:gemini';
  if (normalized === 'opencode') return 'genie:opencode';

  return null;
}

function normalizeExecutePromptClient(value: unknown): LegacyPromptClient | null {
  if (typeof value !== 'string') return null;
  const normalized = value.trim().toLowerCase();
  if (
    normalized === 'claude'
    || normalized === 'cursor'
    || normalized === 'codex'
    || normalized === 'gemini'
    || normalized === 'opencode'
  ) {
    return normalized;
  }
  return null;
}

function normalizeMainIDE(value: unknown): MainIDE | null {
  if (value === null || value === undefined) return null;
  if (typeof value !== 'string') return null;
  const normalized = value.trim().toLowerCase() as MainIDE;
  return MAIN_IDE_VALUES.includes(normalized) ? normalized : null;
}

function normalizeAutomationConfig(value: unknown): AutomationConfig {
  if (!value || typeof value !== 'object') {
    return { defaultPromptClient: null, defaultIDE: null };
  }
  const config = value as AutomationConfig;
  return {
    defaultPromptClient: normalizePromptClient(config.defaultPromptClient),
    defaultIDE: normalizeMainIDE(config.defaultIDE),
  };
}

function trimTrailingSlashes(value: string): string {
  return value.replace(/\/+$/g, '');
}

function normalizeBaseUrl(value: unknown): string | null {
  const normalized = normalizeOptionalString(value);
  if (!normalized) return null;
  return trimTrailingSlashes(normalized);
}

function normalizeAssistantConfig(value: unknown): AssistantConfig {
  if (!value || typeof value !== 'object') {
    return { webBaseUrl: null, apiBaseUrl: null };
  }
  const config = value as AssistantConfig;
  return {
    webBaseUrl: normalizeBaseUrl(config.webBaseUrl),
    apiBaseUrl: normalizeBaseUrl(config.apiBaseUrl),
  };
}

function readSystemConfig(configPath: string): SystemConfig {
  let config: SystemConfig = { server: { host: 'localhost', allowLAN: true } };

  if (fs.existsSync(configPath)) {
    const fileContent = fs.readFileSync(configPath, 'utf8');
    config = JSON.parse(fileContent);
  }

  if (!config.server || typeof config.server !== 'object') {
    config.server = { host: 'localhost', allowLAN: true };
  }
  if (config.server.allowLAN === undefined) {
    config.server.allowLAN = true;
  }

  config.projectDefaults = normalizeProjectDefaults(config.projectDefaults);
  config.projectInfo = normalizeProjectInfo(config.projectInfo);
  config.automation = normalizeAutomationConfig(config.automation);
  config.assistant = normalizeAssistantConfig(config.assistant);

  return config;
}

function extractAssistantConfigFromStatusPayload(parsed: any): AssistantConfig | null {
  const endpoint = parsed?.endpoint ?? parsed?.assistant?.endpoint ?? parsed?.runtime?.endpoint ?? {};
  const webBaseUrl = normalizeBaseUrl(
    endpoint?.frontendUrl
    ?? endpoint?.webBaseUrl
    ?? endpoint?.webUrl
    ?? parsed?.frontendUrl
    ?? parsed?.webBaseUrl
    ?? parsed?.webUrl
  );
  const apiBaseUrl = normalizeBaseUrl(
    endpoint?.apiBaseUrl
    ?? endpoint?.apiUrl
    ?? parsed?.apiBaseUrl
    ?? parsed?.apiUrl
  );

  if (!webBaseUrl && !apiBaseUrl) {
    return null;
  }

  return {
    webBaseUrl,
    apiBaseUrl,
  };
}

function containsNeedsUpdateHint(text: string): boolean {
  const normalized = text.trim();
  if (!normalized) return false;
  return /(need\s*update|needs\s*update|outdated|upgrade|please\s*update|版本过旧|需要更新|请更新)/i.test(normalized);
}

function containsNotRunningHint(text: string): boolean {
  const normalized = text.trim();
  if (!normalized) return false;
  return /(not\s*running|service\s*not\s*running|not\s*started|未启动|尚未启动|未运行|服务未运行)/i.test(normalized);
}

function containsMissingCommandHint(text: string): boolean {
  const normalized = text.trim();
  if (!normalized) return false;
  return /(not\s+recognized\s+as\s+an?\s+internal|command\s+not\s+found|no\s+such\s+file|未找到|不是内部或外部命令)/i.test(normalized);
}

function readAssistantStatusFromCli(command: 'axhub-genie' | 'cloudcli', args: string[]): AssistantProbeResult {
  const commandSource: Exclude<AssistantCommandSource, 'default'> = command === 'axhub-genie' ? 'axhub-genie' : 'cloudcli';

  try {
    if (!commandExists(command)) {
      return {
        status: 'missing_cli',
        message: `未检测到 ${command} 命令`,
        commandSource,
        config: null,
      };
    }

    const result = runCommandSync({
      command,
      args,
      timeoutMs: ASSISTANT_STATUS_TIMEOUT_MS,
    });

    if (result.error) {
      const error = result.error as NodeJS.ErrnoException;
      const errCode = error.code;
      if (errCode === 'ENOENT') {
        return {
          status: 'missing_cli',
          message: `未检测到 ${command} 命令`,
          commandSource,
          config: null,
        };
      }

      if (errCode === 'ETIMEDOUT' || /timed?\s*out/i.test(error.message || '')) {
        return {
          status: 'not_running',
          message: `${command} status 执行超时（>${ASSISTANT_STATUS_TIMEOUT_MS}ms），请确认服务已启动后重试`,
          commandSource,
          config: null,
        };
      }

      return {
        status: 'cli_error',
        message: `${command} 执行失败: ${error.message || 'unknown error'}`,
        commandSource,
        config: null,
      };
    }

    const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : '';
    const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
    const mergedOutput = [stdout, stderr].filter(Boolean).join('\n');

    if (containsMissingCommandHint(mergedOutput)) {
      return {
        status: 'missing_cli',
        message: `未检测到 ${command} 命令`,
        commandSource,
        config: null,
      };
    }

    if (result.status !== 0) {
      if (command === 'axhub-genie' && containsNeedsUpdateHint(mergedOutput)) {
        return {
          status: 'needs_update',
          message: `检测到 ${command} 版本可能过旧，请更新后重试`,
          commandSource,
          config: null,
        };
      }

      if (command === 'axhub-genie' && containsNotRunningHint(mergedOutput)) {
        return {
          status: 'not_running',
          message: `${command} 服务未启动`,
          commandSource,
          config: null,
        };
      }

      return {
        status: 'cli_error',
        message: `${command} status 执行失败${mergedOutput ? `: ${mergedOutput}` : ''}`,
        commandSource,
        config: null,
      };
    }

    if (!stdout) {
      return {
        status: 'cli_error',
        message: `${command} status 未返回有效输出`,
        commandSource,
        config: null,
      };
    }

    let parsed: any = null;
    try {
      parsed = JSON.parse(stdout);
    } catch {
      return {
        status: 'cli_error',
        message: `${command} status 返回内容无法解析为 JSON`,
        commandSource,
        config: null,
      };
    }

    const config = extractAssistantConfigFromStatusPayload(parsed);
    const running = typeof parsed?.running === 'boolean' ? parsed.running : null;

    if (running === false) {
      return {
        status: 'not_running',
        message: `${command} 服务未启动`,
        commandSource,
        config,
      };
    }

    if (!config) {
      return {
        status: 'cli_error',
        message: `${command} status 返回中未发现可用地址`,
        commandSource,
        config: null,
      };
    }

    return {
      status: 'ready',
      message: `${command} 已就绪`,
      commandSource,
      config,
    };
  } catch (error: any) {
    return {
      status: 'cli_error',
      message: `${command} status 检查失败: ${error?.message || 'unknown error'}`,
      commandSource,
      config: null,
    };
  }
}

function readAxhubGenieStatus(): AssistantProbeResult {
  const probe = readAssistantStatusFromCli('axhub-genie', ['status', '--json']);
  const level = probe.status === 'ready' ? 'info' : 'warn';
  logAssistantRuntime(level, 'axhub-genie status --json', {
    status: probe.status,
    message: probe.message,
    config: probe.config || null,
  });
  return probe;
}

function readCloudCliAssistantStatus(): AssistantProbeResult {
  return readAssistantStatusFromCli('cloudcli', ['status', '--json']);
}

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function runExecutableCommandInBackground(command: string, args: string[], cwd: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const spawnSpec = getSpawnCommandSpec(command, args, process.platform);

    logAssistantRuntime('info', '后台执行可执行命令', {
      command,
      args,
      spawnCommand: spawnSpec.command,
      spawnArgs: spawnSpec.args,
      cwd,
      platform: process.platform,
    });

    const child = spawn(spawnSpec.command, spawnSpec.args, {
      cwd,
      detached: true,
      stdio: ['ignore', 'ignore', 'pipe'],
      windowsHide: spawnSpec.windowsHide,
      shell: false,
    });

    if (typeof (child as any)?.once !== 'function') {
      if (typeof child.unref === 'function') {
        child.unref();
      }
      resolve();
      return;
    }

    let settled = false;
    let stderrText = '';
    if (child.stderr && typeof (child.stderr as any).on === 'function') {
      (child.stderr as any).on('data', (chunk: Buffer | string) => {
        const text = typeof chunk === 'string' ? chunk : decodeOutput(chunk);
        if (!text) return;
        if (stderrText.length >= 4000) return;
        stderrText += text;
      });
    }

    const finish = (error?: Error) => {
      if (settled) return;
      settled = true;
      if (typeof child.unref === 'function') {
        child.unref();
      }
      if (error) {
        reject(error);
        return;
      }
      resolve();
    };

    child.once('error', (error) => {
      logAssistantRuntime('error', '可执行命令触发失败', {
        command,
        args,
        cwd,
        error: (error as Error)?.message || 'unknown error',
      });
      finish(error as Error);
    });

    child.once('spawn', () => {
      logAssistantRuntime('info', '可执行命令已触发', {
        command,
        args,
        cwd,
        pid: typeof child.pid === 'number' ? child.pid : null,
      });
      if ((child.stderr as any)?.unref && typeof (child.stderr as any).unref === 'function') {
        (child.stderr as any).unref();
      }
      setTimeout(() => finish(), 150);
    });

    child.once('exit', (code, signal) => {
      const stderrSnippet = stderrText.trim().slice(0, 500);
      const exitMeta = {
        command,
        args,
        cwd,
        code,
        signal,
        stderr: stderrSnippet || null,
      };

      if (typeof code === 'number' && code !== 0) {
        logAssistantRuntime('error', '可执行命令异常退出', exitMeta);
        finish(new Error(`命令退出 code=${code}${stderrSnippet ? ` stderr=${stderrSnippet}` : ''}`));
        return;
      }

      logAssistantRuntime('warn', '可执行命令提前退出', exitMeta);
      finish();
    });

    setTimeout(() => finish(), 500);
  });
}

async function startAxhubGenieAndWait(projectPath: string): Promise<AssistantProbeResult> {
  const startCommand = getAssistantHealthHints().start;
  logAssistantRuntime('info', '准备自动启动 Axhub Genie', {
    command: startCommand,
    cwd: projectPath,
  });

  try {
    await runExecutableCommandInBackground(startCommand, [], projectPath);
  } catch (error: any) {
    logAssistantRuntime('error', '启动命令触发失败', {
      command: startCommand,
      cwd: projectPath,
      error: error?.message || 'unknown error',
    });
    return {
      status: 'cli_error',
      message: `自动启动 Axhub Genie 失败: ${error?.message || 'unknown error'}`,
      commandSource: 'axhub-genie',
      config: null,
    };
  }

  await sleep(ASSISTANT_START_CHECK_DELAY_MS);
  const probe = readAxhubGenieStatus();

  if (probe.status === 'ready') {
    logAssistantRuntime('info', 'Axhub Genie 自动启动成功', {
      config: probe.config || null,
    });
    return {
      ...probe,
      message: 'Axhub Genie 已自动启动并就绪',
    };
  }

  if (probe.status === 'missing_cli' || probe.status === 'needs_update') {
    logAssistantRuntime('warn', '自动启动提前结束', {
      reason: probe.status,
      message: probe.message,
    });
    return probe;
  }

  if (probe.status === 'cli_error') {
    logAssistantRuntime('error', '自动启动失败（cli_error）', {
      message: probe.message,
    });
    return {
      ...probe,
      status: 'not_running',
      message: `${probe.message}。Axhub Genie 自动启动失败，请手动执行 axhub-genie 后重试`,
    };
  }

  logAssistantRuntime('error', '自动启动失败（启动后单次检查未就绪）', {
    status: probe.status,
    message: probe.message,
    config: probe.config || null,
  });
  return {
    ...probe,
    status: 'not_running',
    message: 'Axhub Genie 自动启动失败，请手动执行 axhub-genie 后重试',
  };
}

function resolveRuntimeEndpoints(params: {
  statusConfig: AssistantConfig | null;
  configAssistant: AssistantConfig;
  envAssistant: AssistantConfig;
}): { webBaseUrl: string; apiBaseUrl: string; source: AssistantRuntimeSource } {
  const candidates: Array<{ source: AssistantRuntimeSource; value: AssistantConfig | null }> = [
    { source: 'axhub-genie', value: params.statusConfig },
    { source: 'config', value: params.configAssistant },
    { source: 'env', value: params.envAssistant },
    {
      source: 'default',
      value: {
        webBaseUrl: DEFAULT_ASSISTANT_WEB_BASE_URL,
        apiBaseUrl: DEFAULT_ASSISTANT_API_BASE_URL,
      },
    },
  ];

  let webBaseUrl: string | null = null;
  let apiBaseUrl: string | null = null;
  let source: AssistantRuntimeSource = 'default';

  for (const candidate of candidates) {
    const value = candidate.value;
    if (!value) continue;

    if (!webBaseUrl && value.webBaseUrl) {
      webBaseUrl = value.webBaseUrl;
      if (source === 'default') {
        source = candidate.source;
      }
    }

    if (!apiBaseUrl && value.apiBaseUrl) {
      apiBaseUrl = value.apiBaseUrl;
      if (source === 'default') {
        source = candidate.source;
      }
    }

    if (webBaseUrl && apiBaseUrl) {
      break;
    }
  }

  return {
    webBaseUrl: normalizeBaseUrl(webBaseUrl) || DEFAULT_ASSISTANT_WEB_BASE_URL,
    apiBaseUrl: apiBaseUrl || DEFAULT_ASSISTANT_API_BASE_URL,
    source,
  };
}

function getAssistantBootstrapHints() {
  return getAssistantHealthHints();
}

function createAssistantHealthInfo(params: {
  status: AssistantHealthStatus;
  message: string;
  commandSource: AssistantCommandSource;
}): AssistantHealthInfo {
  return {
    status: params.status,
    message: params.message,
    checkedAt: new Date().toISOString(),
    commandSource: params.commandSource,
    hints: getAssistantHealthHints(),
  };
}

function normalizeAssistantBootstrapMode(value: unknown): AssistantBootstrapMode | null {
  if (typeof value !== 'string') return null;
  const normalized = value.trim();
  if (normalized === 'install_global' || normalized === 'start_existing') {
    return normalized;
  }
  return null;
}

function getPreferredNpmCommandForBootstrap(): string {
  return getPreferredNpmCommand();
}

function buildAssistantBootstrapCommand(mode: AssistantBootstrapMode): string {
  const hints = getAssistantHealthHints();
  if (mode === 'install_global') {
    const npmCommand = getPreferredNpmCommandForBootstrap();
    return `${npmCommand} install -g @axhub/genie && ${hints.start}`;
  }
  return hints.start;
}

async function runAssistantBootstrap(mode: AssistantBootstrapMode, cwd: string): Promise<void> {
  const startCommand = getAssistantHealthHints().start;

  if (mode === 'install_global') {
    const npmCommand = getPreferredNpmCommandForBootstrap();
    const installResult = await runCommand({
      command: npmCommand,
      args: ['install', '-g', '@axhub/genie'],
      cwd,
      capture: true,
      timeoutMs: 180_000,
    });

    if (installResult.code !== 0) {
      throw new Error(installResult.stderr || installResult.stdout || 'npm install -g @axhub/genie failed');
    }
  }

  await runExecutableCommandInBackground(startCommand, [], cwd);
}

function isCommandAvailable(command: string, args: string[] = ['--version']): boolean {
  try {
    if (!commandExists(command)) {
      return false;
    }

    const execution = runCommandSync({
      command,
      args,
      timeoutMs: COMMAND_AVAILABILITY_TIMEOUT_MS,
    });
    return execution.status === 0;
  } catch {
    return false;
  }
}

function validateBootstrapPrerequisites(mode: AssistantBootstrapMode): string | null {
  if (mode === 'install_global') {
    if (!isCommandAvailable('npm')) {
      return 'npm 未安装，无法自动安装 Axhub Genie';
    }
    return null;
  }

  if (!isCommandAvailable('axhub-genie')) {
    return '未检测到 axhub-genie 命令，请先安装后重试';
  }

  return null;
}

async function verifyAssistantHealthEndpoint(): Promise<{ ok: boolean; message: string }> {
  const healthUrl = DEFAULT_ASSISTANT_HEALTH_URL;

  try {
    const response = await fetch(healthUrl, { method: 'GET' });
    if (!response.ok) {
      return { ok: false, message: `/health 探测失败: status ${response.status}` };
    }

    const appIdentifier = response.headers.get('X-App-Identifier') || response.headers.get('x-app-identifier') || '';
    let payload: any = null;
    try {
      payload = await response.json();
    } catch {
      return { ok: false, message: '/health 响应不是有效 JSON' };
    }

    const serviceId = payload?.service?.id || '';
    const serviceName = payload?.service?.name || '';

    const idMatched = serviceId === ASSISTANT_SERVICE_ID || appIdentifier === ASSISTANT_SERVICE_ID;
    const nameMatched = typeof serviceName === 'string' && serviceName.toLowerCase().includes(ASSISTANT_SERVICE_NAME.toLowerCase());

    if (!idMatched && !nameMatched) {
      return { ok: false, message: '健康检查服务身份不匹配（非 Axhub Genie）' };
    }

    if (payload?.status !== 'ok') {
      return { ok: false, message: `健康检查状态异常: ${String(payload?.status || 'unknown')}` };
    }

    return { ok: true, message: 'Axhub Genie 健康检查通过' };
  } catch (error: any) {
    return { ok: false, message: `健康检查请求失败: ${error?.message || 'unknown error'}` };
  }
}

async function resolveAssistantRuntime(
  config: SystemConfig,
  projectPath: string,
  options?: AssistantRuntimeResolveOptions,
): Promise<AssistantRuntimeInfo> {
  const shouldAutoStart = options?.autoStart !== false;
  logAssistantRuntime('info', '开始解析助手运行时', { projectPath, autoStart: shouldAutoStart });
  const healthProbe = await verifyAssistantHealthEndpoint();
  logAssistantRuntime(healthProbe.ok ? 'info' : 'warn', '固定地址健康检查结果', {
    healthUrl: DEFAULT_ASSISTANT_HEALTH_URL,
    ok: healthProbe.ok,
    message: healthProbe.message,
  });
  if (healthProbe.ok) {
    return {
      webBaseUrl: DEFAULT_ASSISTANT_WEB_BASE_URL,
      apiBaseUrl: DEFAULT_ASSISTANT_API_BASE_URL,
      projectPath,
      source: 'default',
      health: createAssistantHealthInfo({
        status: 'ready',
        message: healthProbe.message,
        commandSource: 'default',
      }),
    };
  }

  const configAssistant = normalizeAssistantConfig(config.assistant);
  const envAssistant: AssistantConfig = {
    webBaseUrl: normalizeBaseUrl(process.env.AXHUB_ASSISTANT_WEB_BASE_URL),
    apiBaseUrl: normalizeBaseUrl(process.env.AXHUB_ASSISTANT_API_BASE_URL),
  };

  let axhubGenieStatus = readAxhubGenieStatus();

  if (axhubGenieStatus.status === 'not_running' && shouldAutoStart) {
    axhubGenieStatus = await startAxhubGenieAndWait(projectPath);
  } else if (axhubGenieStatus.status === 'not_running' && !shouldAutoStart) {
    logAssistantRuntime('info', '检测到未启动，但本次请求禁用自动启动', {
      projectPath,
    });
  }

  const resolvedEndpoints = resolveRuntimeEndpoints({
    statusConfig: axhubGenieStatus.status === 'ready' ? axhubGenieStatus.config : null,
    configAssistant,
    envAssistant,
  });
  logAssistantRuntime('info', '地址解析结果', {
    source: resolvedEndpoints.source,
    webBaseUrl: resolvedEndpoints.webBaseUrl,
    apiBaseUrl: resolvedEndpoints.apiBaseUrl,
    status: axhubGenieStatus.status,
  });

  let healthStatus: AssistantHealthStatus = 'runtime_unreachable';
  let healthMessage = '未找到可用的助手地址，请确认 Axhub Genie 已启动';
  const commandSource: AssistantCommandSource = 'axhub-genie';

  if (axhubGenieStatus.status === 'ready') {
    healthStatus = 'ready';
    healthMessage = `已通过 axhub-genie status --json 获取服务地址（默认 /health 探测失败：${healthProbe.message}）`;
  } else if (axhubGenieStatus.status === 'missing_cli') {
    healthStatus = 'missing_cli';
    healthMessage = '未检测到 axhub-genie 命令，请先安装后重试';
  } else if (axhubGenieStatus.status === 'needs_update') {
    healthStatus = 'needs_update';
    healthMessage = axhubGenieStatus.message;
  } else if (axhubGenieStatus.status === 'not_running') {
    healthStatus = 'runtime_unreachable';
    healthMessage = 'Axhub Genie 自动启动失败，请手动执行 axhub-genie 后重试';
  } else if (axhubGenieStatus.status === 'cli_error') {
    healthStatus = 'cli_error';
    healthMessage = axhubGenieStatus.message;
  }

  const runtime = {
    webBaseUrl: resolvedEndpoints.webBaseUrl,
    apiBaseUrl: resolvedEndpoints.apiBaseUrl,
    projectPath,
    source: resolvedEndpoints.source,
    health: createAssistantHealthInfo({
      status: healthStatus,
      message: healthMessage,
      commandSource,
    }),
  };
  logAssistantRuntime(healthStatus === 'ready' ? 'info' : 'warn', '运行时判定完成', {
    healthStatus,
    healthMessage,
    commandSource,
    source: runtime.source,
    webBaseUrl: runtime.webBaseUrl,
    apiBaseUrl: runtime.apiBaseUrl,
  });
  return runtime;
}

export const __assistantRuntimeTestUtils = {
  extractAssistantConfigFromStatusPayload,
  containsNeedsUpdateHint,
  normalizeAssistantBootstrapMode,
  buildAssistantBootstrapCommand,
  getAssistantBootstrapHints,
  validateBootstrapPrerequisites,
  readAssistantStatusFromCli,
  startAxhubGenieAndWait,
  resolveRuntimeEndpoints,
  resolveAssistantRuntime,
  verifyAssistantHealthEndpoint,
};

function buildAgentApiUrl(apiBaseUrl: string): string {
  const normalized = trimTrailingSlashes(apiBaseUrl);
  if (/\/api$/i.test(normalized)) {
    return `${normalized}/agent`;
  }
  return `${normalized}/api/agent`;
}

type AgentStreamNavigation = {
  sessionId: string;
  sessionUrl: string;
};

function extractAgentStreamError(payload: unknown): string | null {
  if (!payload || typeof payload !== 'object') {
    return null;
  }

  const record = payload as Record<string, unknown>;
  if (typeof record.error === 'string' && record.error.trim()) {
    return record.error.trim();
  }

  if (typeof record.message === 'string' && record.message.trim()) {
    return record.message.trim();
  }

  const data = record.data;
  if (!data || typeof data !== 'object') {
    return null;
  }

  const nested = data as Record<string, unknown>;
  if (typeof nested.error === 'string' && nested.error.trim()) {
    return nested.error.trim();
  }

  if (typeof nested.message === 'string' && nested.message.trim()) {
    return nested.message.trim();
  }

  return null;
}

function parseSseJsonEvent(rawBlock: string): Record<string, unknown> | null {
  const trimmedBlock = rawBlock.trim();
  if (!trimmedBlock) {
    return null;
  }

  const dataLines = trimmedBlock
    .split(/\r?\n/)
    .filter((line) => line.startsWith('data:'))
    .map((line) => line.slice(5).trim());

  if (dataLines.length === 0) {
    return null;
  }

  const payloadText = dataLines.join('\n').trim();
  if (!payloadText) {
    return null;
  }

  try {
    const parsed = JSON.parse(payloadText);
    if (!parsed || typeof parsed !== 'object') {
      return null;
    }
    return parsed as Record<string, unknown>;
  } catch {
    return null;
  }
}

async function readAgentNavigationFromSse(response: Response, webBaseUrl: string): Promise<AgentStreamNavigation> {
  if (!response.body) {
    throw new Error('Agent API 未返回可读取的数据流');
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let lastKnownSessionId = '';
  let lastKnownSessionUrl = '';
  let terminalError: string | null = null;

  const resolveNavigation = (): AgentStreamNavigation | null => {
    if (!lastKnownSessionId && !lastKnownSessionUrl) {
      return null;
    }

    const resolvedUrl = lastKnownSessionUrl
      || (lastKnownSessionId ? `${webBaseUrl}/session/${encodeURIComponent(lastKnownSessionId)}` : '');

    if (!resolvedUrl) {
      return null;
    }

    return {
      sessionId: lastKnownSessionId,
      sessionUrl: resolvedUrl,
    };
  };

  const consumeEvent = (payload: Record<string, unknown>) => {
    const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId.trim() : '';
    const sessionUrl = typeof payload.sessionUrl === 'string' ? payload.sessionUrl.trim() : '';

    if (sessionId) {
      lastKnownSessionId = sessionId;
    }
    if (sessionUrl) {
      lastKnownSessionUrl = sessionUrl;
    }

    const eventType = typeof payload.type === 'string' ? payload.type.trim() : '';
    if (eventType === 'session-created' || eventType === 'open-only') {
      return resolveNavigation();
    }

    if (eventType === 'session-aborted') {
      terminalError = 'Session aborted';
      return null;
    }

    if (eventType === 'codex-error' || eventType === 'claude-error' || eventType === 'error') {
      terminalError = extractAgentStreamError(payload) || terminalError;
      return null;
    }

    return resolveNavigation();
  };

  try {
    while (true) {
      const { done, value } = await reader.read();
      buffer += decoder.decode(value || new Uint8Array(), { stream: !done });

      let separatorIndex = buffer.search(/\r?\n\r?\n/);
      while (separatorIndex >= 0) {
        const rawBlock = buffer.slice(0, separatorIndex);
        buffer = buffer.slice(separatorIndex + (buffer[separatorIndex] === '\r' ? 4 : 2));

        const payload = parseSseJsonEvent(rawBlock);
        if (payload) {
          const navigation = consumeEvent(payload);
          if (navigation) {
            void reader.cancel().catch(() => undefined);
            return navigation;
          }
        }

        separatorIndex = buffer.search(/\r?\n\r?\n/);
      }

      if (done) {
        const tailPayload = parseSseJsonEvent(buffer);
        if (tailPayload) {
          const navigation = consumeEvent(tailPayload);
          if (navigation) {
            return navigation;
          }
        }
        break;
      }
    }
  } finally {
    reader.releaseLock();
  }

  if (terminalError) {
    throw new Error(`Agent API 调用失败: ${terminalError}`);
  }

  throw new Error('Agent API 返回缺少 sessionUrl/sessionId');
}

function quoteForShell(value: string) {
  return `"${String(value).replace(/["\\$`]/g, '\\$&')}"`;
}

function quoteForPowerShellSingle(value: string) {
  return `'${String(value).replace(/'/g, "''")}'`;
}

function resolveWindowsExecutablePath(candidates: string[]): string | null {
  for (const candidate of candidates) {
    const trimmed = candidate.trim();
    if (!trimmed) continue;

    const result = spawnSync('where', [trimmed], {
      encoding: 'utf8',
      windowsHide: true,
    });

    if (result.status !== 0) continue;

    const lines = String(result.stdout || '')
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean);

    if (!lines.length) continue;

    const exePath = lines.find((line) => /\.exe$/i.test(line));
    if (exePath) {
      return exePath;
    }

    const commandWrapper = lines.find((line) => /\.(cmd|bat)$/i.test(line));
    if (commandWrapper) {
      const inferredExePath = commandWrapper.replace(/\.(cmd|bat)$/i, '.exe');
      if (fs.existsSync(inferredExePath)) {
        return inferredExePath;
      }
      return commandWrapper;
    }

    return lines[0] || null;
  }

  return null;
}

function resolveWindowsExecutableFromRegistry(executableNames: string[]): string | null {
  const keyRoots = [
    'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths',
    'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths',
    'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths',
  ];

  for (const executableName of executableNames) {
    const normalizedName = executableName.trim();
    if (!normalizedName) continue;

    for (const keyRoot of keyRoots) {
      const key = `${keyRoot}\\${normalizedName}`;
      const query = spawnSync('reg', ['query', key, '/ve'], {
        encoding: 'utf8',
        windowsHide: true,
      });

      if (query.status !== 0 || query.error) {
        continue;
      }

      const output = String(query.stdout || '');
      const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
      const matchedLine = lines.find((line) => /REG_\w+/i.test(line));
      if (!matchedLine) {
        continue;
      }

      const valueMatch = matchedLine.match(/REG_\w+\s+(.+)$/i);
      if (!valueMatch || !valueMatch[1]) {
        continue;
      }

      const resolvedPath = valueMatch[1].trim().replace(/^"|"$/g, '');
      if (resolvedPath && fs.existsSync(resolvedPath)) {
        return resolvedPath;
      }
    }
  }

  return null;
}

function tryOpenWindowsIDEByAppPathNames(
  appPathNames: string[],
  targetPath: string,
  callback: (error: Error | null, stdout?: string | Buffer, stderr?: string | Buffer) => void,
) {
  const candidates = appPathNames.map((name) => name.trim()).filter(Boolean);

  const tryNext = (index: number) => {
    if (index >= candidates.length) {
      callback(new Error('No compatible Windows app path name found'));
      return;
    }

    execFile(
      'powershell',
      [
        '-NoProfile',
        '-NonInteractive',
        '-WindowStyle',
        'Hidden',
        '-Command',
        `Start-Process -FilePath ${quoteForPowerShellSingle(candidates[index])} -ArgumentList ${quoteForPowerShellSingle(targetPath)} -ErrorAction Stop`,
      ],
      { windowsHide: true },
      (error, stdout, stderr) => {
        if (!error) {
          callback(null, stdout, stderr);
          return;
        }

        tryNext(index + 1);
      },
    );
  };

  tryNext(0);
}

function buildProjectInfoSection(
  projectInfo: ProjectInfo,
  projectDefaults: ProjectDefaults,
  hasProjectOverviewDoc: boolean
): string {
  const lines: string[] = [];
  const projectName = normalizeInlineText(projectInfo.name);
  const projectDescription = normalizeInlineText(projectInfo.description);
  const defaultTheme = normalizeOptionalString(projectDefaults.defaultTheme);

  if (projectName) lines.push(`- 项目名称：${projectName}`);
  if (projectDescription) lines.push(`- 项目简介：${projectDescription}`);
  if (hasProjectOverviewDoc) lines.push(`- 项目总文档：\`${PROJECT_OVERVIEW_DOC_RELATIVE_PATH}\``);
  if (defaultTheme) lines.push(`- 默认主题：\`src/themes/${defaultTheme}\``);

  if (!lines.length) return '';
  return ['## 📌 项目信息', '', ...lines].join('\n');
}

function renderAgentsTemplate(
  template: string,
  projectInfo: ProjectInfo,
  projectDefaults: ProjectDefaults,
  hasProjectOverviewDoc: boolean
) {
  const projectInfoSection = buildProjectInfoSection(projectInfo, projectDefaults, hasProjectOverviewDoc);
  let content = template;

  if (content.includes('{{PROJECT_INFO_SECTION}}')) {
    content = content.replace('{{PROJECT_INFO_SECTION}}', projectInfoSection);
    return content;
  }

  const sectionRegex = /^## 📌 项目信息[\s\S]*?(?=^##\s|\s*$)/m;
  if (sectionRegex.test(content)) {
    return content.replace(sectionRegex, projectInfoSection);
  }

  return content;
}

function writeAgentDocs(
  templatePath: string,
  agentsPath: string,
  claudePath: string,
  projectInfo: ProjectInfo,
  projectDefaults: ProjectDefaults
): boolean {
  if (!fs.existsSync(templatePath)) return false;
  const template = fs.readFileSync(templatePath, 'utf8');
  const projectRoot = path.dirname(templatePath);
  const projectOverviewDocPath = path.resolve(projectRoot, PROJECT_OVERVIEW_DOC_RELATIVE_PATH);
  const nextAgentsContent = renderAgentsTemplate(
    template,
    projectInfo,
    projectDefaults,
    fs.existsSync(projectOverviewDocPath)
  );
  fs.writeFileSync(agentsPath, nextAgentsContent, 'utf8');
  fs.writeFileSync(claudePath, nextAgentsContent, 'utf8');
  return true;
}

function syncAgentDocsFromConfig(paths: AgentDocsPaths): void {
  const { configPath, agentsTemplatePath, agentsPath, claudePath } = paths;
  let config: SystemConfig = { server: { host: 'localhost', allowLAN: true } };

  if (fs.existsSync(configPath)) {
    const fileContent = fs.readFileSync(configPath, 'utf8');
    config = JSON.parse(fileContent);
  }

  const projectDefaults = normalizeProjectDefaults(config.projectDefaults);
  const projectInfo = normalizeProjectInfo(config.projectInfo);
  writeAgentDocs(agentsTemplatePath, agentsPath, claudePath, projectInfo, projectDefaults);
}

/**
 * 配置管理 API 插件
 * 提供配置文件的读取和保存功能
 */
export function configApiPlugin(): Plugin {
  const projectRoot = path.resolve(__dirname, '..');
  const configPath = path.resolve(projectRoot, '.axhub', 'make', 'axhub.config.json');
  const agentsPath = path.resolve(projectRoot, 'AGENTS.md');
  const claudePath = path.resolve(projectRoot, 'CLAUDE.md');
  const agentsTemplatePath = path.resolve(projectRoot, 'AGENTS.template.md');

  return {
    name: 'config-api-plugin',
    configureServer(server: any) {
      try {
        syncAgentDocsFromConfig({
          configPath,
          agentsTemplatePath,
          agentsPath,
          claudePath
        });
      } catch (e: any) {
        console.warn('Failed to sync AGENTS.md on server start:', e?.message || e);
      }

      // GET /api/config - 读取配置
      server.middlewares.use(async (req: any, res: any, next: any) => {
        if (req.method === 'GET' && req.url === '/api/config') {
          try {
            const config = readSystemConfig(configPath);
            
            // 移除 port 字段（不对外暴露，固定使用 51720 起始）
            if (config.server && 'port' in config.server) {
              delete config.server.port;
            }

            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify({
              ...config,
              projectPath: projectRoot,
            }));
          } catch (e: any) {
            console.error('Error reading config:', e);
            res.statusCode = 500;
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify({ error: e?.message || 'Failed to read config' }));
          }
          return;
        }

        if (req.method === 'GET' && req.url?.startsWith('/api/assistant/runtime')) {
          try {
            const url = new URL(req.url, 'http://localhost');
            if (url.pathname !== '/api/assistant/runtime') {
              next();
              return;
            }

            const autoStartParam = (url.searchParams.get('autoStart') || '').trim().toLowerCase();
            const shouldAutoStart = !(autoStartParam === '0' || autoStartParam === 'false' || autoStartParam === 'no');
            const config = readSystemConfig(configPath);
            const runtime = await resolveAssistantRuntime(config, projectRoot, {
              autoStart: shouldAutoStart,
            });

            // When accessed via LAN, rewrite localhost URLs so remote clients can reach the API
            const requestHost = String(req.headers?.host || '').split(':')[0];
            if (requestHost && requestHost !== 'localhost' && requestHost !== '127.0.0.1') {
              const rewriteLocalhostUrl = (urlStr: string): string => {
                try {
                  const parsed = new URL(urlStr);
                  if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
                    parsed.hostname = requestHost;
                    return parsed.toString().replace(/\/+$/, '');
                  }
                } catch { /* keep original */ }
                return urlStr;
              };
              if (runtime.apiBaseUrl) runtime.apiBaseUrl = rewriteLocalhostUrl(runtime.apiBaseUrl);
              if (runtime.webBaseUrl) runtime.webBaseUrl = rewriteLocalhostUrl(runtime.webBaseUrl);
            }

            res.statusCode = 200;
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify(runtime));
          } catch (e: any) {
            const requestHost = String(req.headers?.host || '').split(':')[0];
            const fallbackApiHost = (requestHost && requestHost !== 'localhost' && requestHost !== '127.0.0.1')
              ? requestHost : 'localhost';
            const fallbackWebHost = fallbackApiHost;
            const fallback: AssistantRuntimeInfo = {
              webBaseUrl: `http://${fallbackWebHost}:${DEFAULT_ASSISTANT_WEB_BASE_URL.match(/:(\d+)/)?.[1] || '32123'}`,
              apiBaseUrl: `http://${fallbackApiHost}:32123/api`,
              projectPath: projectRoot,
              source: 'default',
              health: createAssistantHealthInfo({
                status: 'runtime_unreachable',
                message: '助手运行时检查失败，请稍后重试',
                commandSource: 'default',
              }),
            };

            res.statusCode = 200;
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify(fallback));
          }
          return;
        }

        if (req.method === 'POST' && req.url === '/api/assistant/bootstrap') {
          const chunks: Buffer[] = [];
          let totalLength = 0;

          req.on('data', (chunk: Buffer) => {
            totalLength += chunk.length;
            if (totalLength > 1024 * 10) {
              res.statusCode = 413;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: 'Payload too large' }));
              req.destroy();
              return;
            }
            chunks.push(chunk);
          });

          req.on('end', async () => {
            try {
              const raw = Buffer.concat(chunks).toString('utf8');
              const body = raw ? JSON.parse(raw) : {};
              const mode = normalizeAssistantBootstrapMode(body?.mode);

              if (!mode) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'Invalid bootstrap mode', hints: getAssistantBootstrapHints() }));
                return;
              }

              const prerequisiteError = validateBootstrapPrerequisites(mode);
              if (prerequisiteError) {
                res.statusCode = 500;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: prerequisiteError, hints: getAssistantBootstrapHints() }));
                return;
              }

              await runAssistantBootstrap(mode, projectRoot);

              const config = readSystemConfig(configPath);
              const runtime = await resolveAssistantRuntime(config, projectRoot);

              res.statusCode = 200;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({
                success: true,
                mode,
                message: '已触发启动，请稍后重试',
                runtime,
              }));
            } catch (e: any) {
              res.statusCode = 500;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({
                error: e?.message || '无法自动启动 Axhub Genie',
                hints: getAssistantBootstrapHints(),
              }));
            }
          });

          return;
        }

        // POST /api/config - 保存配置
        if (req.method === 'POST' && req.url === '/api/config') {
          const chunks: Buffer[] = [];
          let totalLength = 0;

          req.on('data', (chunk: Buffer) => {
            totalLength += chunk.length;
            if (totalLength > 1024 * 10) { // 10KB 限制
              res.statusCode = 413;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: 'Payload too large' }));
              req.destroy();
              return;
            }
            chunks.push(chunk);
          });

          req.on('end', () => {
            try {
              const raw = Buffer.concat(chunks).toString('utf8');
              const newConfig: SystemConfig = JSON.parse(raw);

              // 验证配置格式
              if (!newConfig.server || typeof newConfig.server !== 'object') {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'Invalid config format' }));
                return;
              }

              // 移除 port 字段（不允许配置，固定使用 51720 起始）
              if (newConfig.server && 'port' in newConfig.server) {
                delete newConfig.server.port;
              }

              // 校验/归一化 projectDefaults
              const projectDefaults = normalizeProjectDefaults(newConfig.projectDefaults);
              const projectInfo = normalizeProjectInfo(newConfig.projectInfo);
              const automation = normalizeAutomationConfig(newConfig.automation);
              const assistant = normalizeAssistantConfig(newConfig.assistant);
              newConfig.projectDefaults = projectDefaults;
              newConfig.projectInfo = projectInfo;
              newConfig.automation = automation;
              newConfig.assistant = assistant;

              // 使用模板生成 AGENTS.md（项目参考规范）
              if (!writeAgentDocs(agentsTemplatePath, agentsPath, claudePath, projectInfo, projectDefaults)) {
                console.warn('AGENTS.template.md not found, skip regenerating AGENTS.md');
              }

              // 保存配置文件
              fs.mkdirSync(path.dirname(configPath), { recursive: true });
              fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');

              res.statusCode = 200;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ 
                success: true, 
                message: '配置已保存（已根据模板同步 AGENTS.md）' 
              }));
            } catch (e: any) {
              console.error('Error saving config:', e);
              res.statusCode = 500;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: e?.message || 'Failed to save config' }));
            }
          });
          return;
        }

        if (req.method === 'POST' && req.url === '/api/ide/open') {
          const chunks: Buffer[] = [];
          let totalLength = 0;

          req.on('data', (chunk: Buffer) => {
            totalLength += chunk.length;
            if (totalLength > 1024 * 10) {
              res.statusCode = 413;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: 'Payload too large' }));
              req.destroy();
              return;
            }
            chunks.push(chunk);
          });

          req.on('end', () => {
            try {
              const raw = Buffer.concat(chunks).toString('utf8');
              const body = JSON.parse(raw || '{}');

              let configuredIDE: MainIDE | null = null;
              if (fs.existsSync(configPath)) {
                const fileContent = fs.readFileSync(configPath, 'utf8');
                const savedConfig = JSON.parse(fileContent) as SystemConfig;
                configuredIDE = normalizeMainIDE(savedConfig?.automation?.defaultIDE);
              }

              const ide = normalizeMainIDE(body?.ide) || configuredIDE;
              if (!ide) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'Main IDE is not configured' }));
                return;
              }

              const rawTargetPath = typeof body?.targetPath === 'string' ? body.targetPath.trim() : '';
              const targetPath = rawTargetPath ? rawTargetPath : projectRoot;
              const absoluteTargetPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(projectRoot, targetPath);
              const ideAppName = MAIN_IDE_APP_NAMES[ide];
              const windowsAppPathNames = MAIN_IDE_WINDOWS_APP_PATH_NAMES[ide] || [ideAppName];

              const command = process.platform === 'win32'
                ? `powershell -NoProfile -Command Start-Process -FilePath ${quoteForPowerShellSingle(windowsAppPathNames[0] || ideAppName)} -ArgumentList ${quoteForPowerShellSingle(absoluteTargetPath)} -ErrorAction Stop`
                : `open -a ${quoteForShell(ideAppName)} ${quoteForShell(absoluteTargetPath)}`;

              const handleOpenCommandResult = (error: Error | null, _stdout?: string | Buffer, stderr?: string | Buffer) => {
                const stderrText = typeof stderr === 'string'
                  ? stderr.trim()
                  : Buffer.isBuffer(stderr)
                    ? stderr.toString('utf8').trim()
                    : '';

                if (error && stderrText) {
                  res.statusCode = 500;
                  res.setHeader('Content-Type', 'application/json; charset=utf-8');
                  res.end(JSON.stringify({ error: `打开 ${ideAppName} 失败: ${stderrText || error.message}` }));
                  return;
                }

                res.statusCode = 200;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({
                  success: true,
                  ide,
                  targetPath: absoluteTargetPath,
                  command,
                }));
              };

              if (process.platform === 'win32') {
                const executableCandidates = [
                  ...(MAIN_IDE_WINDOWS_COMMAND_CANDIDATES[ide] || []),
                  ideAppName,
                ];
                const executableNameCandidates = MAIN_IDE_WINDOWS_EXECUTABLE_NAMES[ide] || [];

                const executablePath =
                  resolveWindowsExecutablePath(executableCandidates)
                  || resolveWindowsExecutablePath(executableNameCandidates)
                  || resolveWindowsExecutableFromRegistry(executableNameCandidates);

                if (executablePath) {
                  const spawnSpec = getSpawnCommandSpec(executablePath, [absoluteTargetPath], process.platform);

                  const child = spawn(spawnSpec.command, spawnSpec.args, {
                    detached: true,
                    stdio: 'ignore',
                    windowsHide: spawnSpec.windowsHide,
                    shell: false,
                  });

                  child.once('error', (spawnError) => {
                    res.statusCode = 500;
                    res.setHeader('Content-Type', 'application/json; charset=utf-8');
                    res.end(JSON.stringify({ error: `打开 ${ideAppName} 失败: ${spawnError.message}` }));
                  });

                  child.once('spawn', () => {
                    child.unref();
                    res.statusCode = 200;
                    res.setHeader('Content-Type', 'application/json; charset=utf-8');
                    res.end(JSON.stringify({
                      success: true,
                      ide,
                      targetPath: absoluteTargetPath,
                      command: `${quoteForShell(executablePath)} ${quoteForShell(absoluteTargetPath)}`,
                    }));
                  });

                  return;
                }

                tryOpenWindowsIDEByAppPathNames(
                  windowsAppPathNames,
                  absoluteTargetPath,
                  handleOpenCommandResult,
                );
              } else {
                exec(command, handleOpenCommandResult);
              }
            } catch (e: any) {
              res.statusCode = 500;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: e?.message || 'Failed to open IDE' }));
            }
          });

          return;
        }

        if (req.method === 'POST' && req.url === '/api/prompt/execute') {
          const chunks: Buffer[] = [];
          let totalLength = 0;

          req.on('data', (chunk: Buffer) => {
            totalLength += chunk.length;
            if (totalLength > 1024 * 1024) {
              res.statusCode = 413;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: 'Payload too large' }));
              req.destroy();
              return;
            }
            chunks.push(chunk);
          });

          req.on('end', async () => {
            try {
              const raw = Buffer.concat(chunks).toString('utf8');
              const body = JSON.parse(raw || '{}');
              const client = normalizeExecutePromptClient(body?.client);
              const prompt = typeof body?.prompt === 'string' ? body.prompt.trim() : '';
              const scene = typeof body?.scene === 'string' ? body.scene.trim() : '';

              if (!client) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'Invalid client' }));
                return;
              }

              if (!prompt) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'Prompt is required' }));
                return;
              }

              if (!scene) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'Scene is required' }));
                return;
              }

              const config = readSystemConfig(configPath);
              const assistantRuntime = await resolveAssistantRuntime(config, projectRoot);

              if (assistantRuntime.health.status !== 'ready') {
                throw new Error(`${assistantRuntime.health.message}。可尝试：${getAssistantHealthHints().installGlobal}`);
              }

              const provider = client;
              const agentApiUrl = buildAgentApiUrl(assistantRuntime.apiBaseUrl);

              const upstreamResponse = await fetch(agentApiUrl, {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                  'Accept': 'text/event-stream, application/json',
                },
                body: JSON.stringify({
                  projectPath: assistantRuntime.projectPath,
                  provider,
                  message: prompt,
                  stream: true,
                }),
              });

              if (!upstreamResponse.ok) {
                const upstreamText = await upstreamResponse.text();
                let upstreamData: any = null;
                try {
                  upstreamData = upstreamText ? JSON.parse(upstreamText) : null;
                } catch {
                  upstreamData = null;
                }
                const upstreamError = upstreamData?.error || upstreamData?.message || upstreamText || `status ${upstreamResponse.status}`;
                throw new Error(`Agent API 调用失败: ${upstreamError}`);
              }

              const navigation = await readAgentNavigationFromSse(upstreamResponse, assistantRuntime.webBaseUrl);

              res.statusCode = 200;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({
                success: true,
                url: navigation.sessionUrl,
                sessionId: navigation.sessionId,
                scene,
                provider,
              }));
            } catch (e: any) {
              res.statusCode = 500;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: e?.message || 'Failed to execute prompt' }));
            }
          });

          return;
        }

        next();
      });
    }
  };
}
