import { randomUUID } from "node:crypto";
import { BrowserWindow, dialog, ipcMain, shell, type OpenDialogOptions, type WebContents } from "electron";
import { copyFile, mkdir, readFile } from "node:fs/promises";
import path from "node:path";
import {
  IPC_CHANNELS,
  type AppConfig,
  type ChatAttachment,
  type ChatCancelStreamResult,
  type ChatMessage,
  type ChatStreamEvent,
  type ConfigSecretId,
  type DesktopApi,
  type GatewayStatus,
  type PluginSummary,
  type ProjectIntentSuggestion,
  type ProjectResolvedAttachment,
  type TaskPanelArtifact,
  type RuntimeCloudFetchAction,
  type RuntimeCloudStatus,
  type RuntimeStatus,
  type SaveConfigInput,
  type SignInInput,
  type SystemSummary,
  type ProjectExecutionDecision,
  type WorkspaceSummary,
  type WorkspaceWarmupResult
} from "@qjclaw/shared-types";
import type { GatewayClient } from "@qjclaw/gateway-client";
import type { RuntimeManager } from "@qjclaw/runtime-manager";
import type { AppConfigService } from "./services/app-config.js";
import type { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js";
import type { DiagnosticsService } from "./services/diagnostics.js";
import type { DailyReportService } from "./services/daily-report-service.js";
import type { SkillCatalogService } from "./services/skill-catalog.js";
import type { SkillStoreService } from "./services/skill-store.js";
import type { SkillClient } from "./services/skill-client.js";
import type { ExpertCatalogService } from "./services/expert-catalog.js";
import {
  resolveEffectiveGatewayToken,
  resolveEffectiveGatewayUrl,
  shouldUseLocalOpenClawGateway,
  type LocalOpenClawGatewayConfig
} from "./services/openclaw-local-config.js";
import type { SecretManager } from "./services/secrets.js";
import type { StartupLogger } from "./services/startup-logger.js";
import type { RuntimeCloudSupervisor } from "./services/runtime-cloud-supervisor.js";
import type { RuntimeSkillBridgeService } from "./services/runtime-skill-bridge.js";
import type { ProjectStoreService } from "./services/project-store.js";
import type { ProjectBundleService } from "./services/project-bundle.js";
import {
  EMPTY_PROJECT_INVENTORY_MESSAGE,
  createSessionForActiveProject,
  listSessionsForActiveProject,
  loadActiveProjectWorkspaceState
} from "./services/project-inventory-state.js";
import type { ProjectChatTargetResolverService } from "./services/project-chat-target-resolver.js";
import type { ProjectContextService } from "./services/project-context.js";
import type { ProjectExecutionRouter } from "./services/project-execution-router.js";
import type { ProjectSkillRouterService } from "./services/project-skill-router.js";
import type { ProjectWorkspaceExecutorService } from "./services/project-workspace-executor.js";
import type { TaskPanelService } from "./services/task-panel-service.js";
import {
  buildProjectModelRuntime,
  materializeProjectModelRuntime
} from "./services/project-model-runtime.js";
import { buildWorkspaceEntryLobsterEnv } from "./services/workspace-entry-lobster-env.js";
import {
  refreshProjectContextAfterExecution,
  shouldRefreshProjectContextAfterExecution
} from "./services/project-context-lifecycle.js";
import {
  runGatewayChatRequestWithRecovery
} from "./chat-gateway-recovery.js";
import {
  buildChatSummary,
  isWorkspaceShellReady,
  shouldRetryBootstrapWarmup,
  shouldRetryManagedRuntimeStartup
} from "./workspace-startup.js";

interface MainServices {
  configService: AppConfigService;
  gatewayClient: GatewayClient;
  runtimeManager: RuntimeManager;
  secretManager: SecretManager;
  diagnosticsService: DiagnosticsService;
  authClient: AuthClient;
  profileClient: ProfileClient;
  creditClient: CreditClient;
  skillClient: SkillClient;
  expertCatalogService: ExpertCatalogService;
  skillCatalogService: SkillCatalogService;
  skillStore: SkillStoreService;
  modelConfigClient: ModelConfigClient;
  runtimeCloudClient: OpenClawConfigClient;
  runtimeCloudSupervisor: RuntimeCloudSupervisor;
  dailyReportService: DailyReportService;
  runtimeSkillBridge: RuntimeSkillBridgeService;
  projectStore: ProjectStoreService;
  projectBundleService: ProjectBundleService;
  projectContextService: ProjectContextService;
  projectChatTargetResolver: ProjectChatTargetResolverService;
  projectSkillRouter: ProjectSkillRouterService;
  projectExecutionRouter: ProjectExecutionRouter;
  projectWorkspaceExecutor: ProjectWorkspaceExecutorService;
  taskPanelService: TaskPanelService;
  startupLogger: StartupLogger;
  appVersion: string;
  systemSummary: SystemSummary;
  localOpenClawConfig?: LocalOpenClawGatewayConfig | null;
}

export interface RegisteredDesktopIpc extends DesktopApi {
  __internal: {
    queueWorkspaceWarmup: (
      reason: string,
      options?: {
        action?: RuntimeCloudFetchAction;
        restart?: boolean;
        config?: AppConfig;
        inputToken?: string;
      }
    ) => Promise<WorkspaceWarmupResult>;
  };
}

function toControlUiUrl(gatewayUrl: string): string {
  const url = new URL(gatewayUrl);
  url.protocol = url.protocol === "wss:" ? "https:" : "http:";
  return url.toString();
}

function sanitizeAttachmentFileComponent(value: string): string {
  const trimmed = value.trim();
  const sanitized = trimmed.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
  return sanitized || "attachment";
}

function normalizeComparablePath(value?: string): string {
  const trimmed = value?.trim();
  if (!trimmed) {
    return "";
  }

  try {
    return path.resolve(trimmed).replace(/[\\/]+/g, "\\").toLowerCase();
  } catch {
    return trimmed.replace(/[\\/]+/g, "\\").toLowerCase();
  }
}

function didWorkspacePathChange(previousConfig: AppConfig, nextConfig: AppConfig): boolean {
  return normalizeComparablePath(previousConfig.workspacePath) !== normalizeComparablePath(nextConfig.workspacePath);
}

const IMAGE_ATTACHMENT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"]);
const DOCUMENT_ATTACHMENT_EXTENSIONS = new Set([".pdf", ".ppt", ".pptx", ".xls", ".xlsx", ".csv", ".tsv", ".doc", ".docx", ".txt", ".md", ".json", ".mp3"]);
const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set([
  ...IMAGE_ATTACHMENT_EXTENSIONS,
  ...DOCUMENT_ATTACHMENT_EXTENSIONS
]);
const BUILTIN_HOME_PROJECT_ID = "home-chat";

function inferAttachmentMimeType(localPath: string, name?: string): string {
  const extension = path.extname(name || localPath).toLowerCase();
  switch (extension) {
    case ".png":
      return "image/png";
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".webp":
      return "image/webp";
    case ".gif":
      return "image/gif";
    case ".bmp":
      return "image/bmp";
    case ".pdf":
      return "application/pdf";
    case ".ppt":
      return "application/vnd.ms-powerpoint";
    case ".pptx":
      return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
    case ".xls":
      return "application/vnd.ms-excel";
    case ".xlsx":
      return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    case ".csv":
      return "text/csv";
    case ".tsv":
      return "text/tab-separated-values";
    case ".doc":
      return "application/msword";
    case ".docx":
      return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
    case ".txt":
      return "text/plain";
    case ".md":
      return "text/markdown";
    case ".json":
      return "application/json";
    case ".mp3":
      return "audio/mpeg";
    default:
      return "application/octet-stream";
  }
}

function inferAttachmentKind(localPath: string, name?: string, mimeType?: string): ChatAttachment["kind"] | null {
  const normalizedMimeType = mimeType?.trim().toLowerCase() || "";
  const extension = path.extname(name || localPath).toLowerCase();
  if (normalizedMimeType.startsWith("image/") || IMAGE_ATTACHMENT_EXTENSIONS.has(extension)) {
    return "image";
  }
  if (SUPPORTED_ATTACHMENT_EXTENSIONS.has(extension)) {
    return "file";
  }
  return null;
}

function normalizeChatAttachmentCandidate(attachment: Partial<ChatAttachment> | null | undefined): ChatAttachment | null {
  if (!attachment) {
    return null;
  }
  const localPath = attachment.localPath?.trim();
  if (!localPath) {
    return null;
  }
  const name = attachment.name?.trim() || path.basename(localPath);
  const mimeType = attachment.mimeType?.trim() || inferAttachmentMimeType(localPath, name);
  const kind = attachment.kind && attachment.kind !== "image" && attachment.kind !== "file"
    ? null
    : inferAttachmentKind(localPath, name, mimeType);
  if (!kind) {
    return null;
  }
  return {
    kind,
    name,
    mimeType,
    localPath
  };
}

async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAttachment | null> {
  const dialogOptions: OpenDialogOptions = {
    title: "Select image",
    properties: ["openFile"],
    filters: [
      { name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "gif", "bmp"] }
    ]
  };
  const result = window
    ? await dialog.showOpenDialog(window, dialogOptions)
    : await dialog.showOpenDialog(dialogOptions);
  if (result.canceled || !result.filePaths.length) {
    return null;
  }

  const localPath = result.filePaths[0]?.trim();
  if (!localPath) {
    return null;
  }

  return normalizeChatAttachmentCandidate({
    kind: "image",
    name: path.basename(localPath) || "image",
    mimeType: inferAttachmentMimeType(localPath),
    localPath
  });
}

async function pickAttachments(window: BrowserWindow | null): Promise<ChatAttachment[]> {
  const dialogOptions: OpenDialogOptions = {
    title: "Select attachments",
    properties: ["openFile", "multiSelections"],
    filters: [
      { name: "Supported files", extensions: [...SUPPORTED_ATTACHMENT_EXTENSIONS].map((value) => value.slice(1)) },
      { name: "Images", extensions: [...IMAGE_ATTACHMENT_EXTENSIONS].map((value) => value.slice(1)) },
      { name: "Audio", extensions: ["mp3"] },
      { name: "Documents", extensions: ["pdf", "mp3", "doc", "docx", "txt", "md", "json"] },
      { name: "Presentations", extensions: ["ppt", "pptx"] },
      { name: "Spreadsheets", extensions: ["xls", "xlsx", "csv", "tsv"] }
    ]
  };
  const result = window
    ? await dialog.showOpenDialog(window, dialogOptions)
    : await dialog.showOpenDialog(dialogOptions);
  if (result.canceled || !result.filePaths.length) {
    return [];
  }

  return result.filePaths.flatMap((filePath) => {
    const localPath = filePath?.trim();
    if (!localPath) {
      return [];
    }
    const attachment = normalizeChatAttachmentCandidate({
      name: path.basename(localPath),
      mimeType: inferAttachmentMimeType(localPath),
      localPath
    });
    return attachment ? [attachment] : [];
  });
}

async function pickWorkspaceDirectory(window: BrowserWindow | null, currentPath?: string): Promise<string | null> {
  const defaultPath = currentPath?.trim();
  const dialogOptions: OpenDialogOptions = {
    title: "Select workspace directory",
    properties: ["openDirectory", "createDirectory"],
    ...(defaultPath ? { defaultPath } : {})
  };
  const result = window
    ? await dialog.showOpenDialog(window, dialogOptions)
    : await dialog.showOpenDialog(dialogOptions);
  if (result.canceled || !result.filePaths.length) {
    return null;
  }

  const selectedPath = result.filePaths[0]?.trim();
  return selectedPath || null;
}

function normalizeChatAttachments(attachments?: ChatAttachment[]): ChatAttachment[] {
  if (!Array.isArray(attachments)) {
    return [];
  }

  const seen = new Set<string>();
  return attachments.flatMap((attachment) => {
    const normalized = normalizeChatAttachmentCandidate(attachment);
    if (!normalized) {
      return [];
    }
    const comparablePath = normalizeComparablePath(normalized.localPath);
    if (seen.has(comparablePath)) {
      return [];
    }
    seen.add(comparablePath);
    return [normalized];
  });
}

function isHomeImageAttachmentRequest(sessionId: string, skillId?: string, attachments?: ChatAttachment[]): boolean {
  if (skillId?.trim()) {
    return false;
  }
  if (!sessionId.startsWith(`project:${BUILTIN_HOME_PROJECT_ID}:`)) {
    return false;
  }

  const normalized = normalizeChatAttachments(attachments);
  return normalized.length > 0 && normalized.every((attachment) => attachment.kind === "image");
}

function normalizeChatCompletionsUrl(rawBaseUrl: string): string {
  const baseUrl = rawBaseUrl.trim().replace(/\/+$/, "");
  if (!baseUrl) {
    return "";
  }
  return baseUrl.endsWith("/chat/completions")
    ? baseUrl
    : `${baseUrl}/chat/completions`;
}

async function materializeProjectAttachments(
  projectRoot: string,
  sessionId: string,
  attachments?: ChatAttachment[]
): Promise<ProjectResolvedAttachment[]> {
  const normalized = normalizeChatAttachments(attachments);
  if (!normalized.length) {
    return [];
  }

  const sessionSlug = sanitizeAttachmentFileComponent(sessionId.replace(/[:]/g, "-"));

  return await Promise.all(normalized.map(async (attachment, index) => {
    const sourceExt = path.extname(attachment.name || attachment.localPath) || path.extname(attachment.localPath) || ".bin";
    const targetRoot = attachment.kind === "image"
      ? path.join(projectRoot, "inputs", "images", "main")
      : path.join(projectRoot, "inputs", "assets", "manual");
    await mkdir(targetRoot, { recursive: true });
    const fileName = `${sessionSlug}-${String(index + 1).padStart(2, "0")}${sourceExt.toLowerCase()}`;
    const targetPath = path.join(targetRoot, fileName);
    await copyFile(attachment.localPath, targetPath);
    return {
      ...attachment,
      localPath: targetPath,
      projectPath: targetPath,
      relativeProjectPath: path.relative(projectRoot, targetPath).replace(/\\/g, "/")
    };
  }));
}

const PLUGIN_SPECS = [
  {
    id: "spreadsheet-tools",
    name: "Spreadsheet Tools",
    description: "Read and process Excel or CSV files.",
    packages: ["openpyxl", "pandas"],
    includedByDefault: true
  },
  {
    id: "document-tools",
    name: "Document Tools",
    description: "Read and process txt, md, docx, or pdf files.",
    packages: ["pypdf", "python-docx", "charset-normalizer"],
    includedByDefault: true
  },
  {
    id: "web-tools",
    name: "Web Tools",
    description: "Fetch and extract web content.",
    packages: ["requests", "beautifulsoup4", "lxml"],
    includedByDefault: true
  },
  {
    id: "file-tools",
    name: "File Tools",
    description: "Copy, move, and archive files.",
    packages: [],
    includedByDefault: true
  },
  {
    id: "runtime-diagnostics",
    name: "Runtime Diagnostics",
    description: "Inspect runtime status and logs.",
    packages: [],
    includedByDefault: true
  },
  {
    id: "browser-automation",
    name: "Browser Automation",
    description: "Automate browser actions with scripts.",
    packages: ["playwright"],
    includedByDefault: true
  },
  {
    id: "ocr-tools",
    name: "OCR Tools",
    description: "Recognize text from images or scans.",
    packages: [],
    includedByDefault: false
  }
] as const;

function buildPluginSummaries(runtimeStatus: RuntimeStatus): PluginSummary[] {
  const installedPackages = new Set((runtimeStatus.installedPythonPackages ?? []).map((item) => item.toLowerCase()));
  const runtimeReady = runtimeStatus.payloadState === "ready" && runtimeStatus.processState === "running";

  return PLUGIN_SPECS.map((plugin) => {
    if (!plugin.includedByDefault) {
      return {
        id: plugin.id,
        name: plugin.name,
        description: plugin.description,
        status: "extension" as const,
        includedByDefault: false
      };
    }

    const hasPackages = plugin.packages.every((pkg) => installedPackages.has(pkg));
    const available = runtimeReady && (plugin.packages.length === 0 || hasPackages);
    return {
      id: plugin.id,
      name: plugin.name,
      description: plugin.description,
      status: available ? "included" as const : "unavailable" as const,
      includedByDefault: true
    };
  });
}

function buildProjectSyncSummary(message: string): Pick<WorkspaceSummary, "chatReady" | "chatLaunchState" | "chatStatusMessage" | "startupPhase" | "startupMessage"> {
  return {
    chatReady: false,
    chatLaunchState: "starting",
    chatStatusMessage: message,
    startupPhase: "syncing-projects",
    startupMessage: message
  };
}

const MANAGED_RUNTIME_START_RETRY_LIMIT = 2;
const MANAGED_RUNTIME_START_RETRY_DELAY_MS = 1500;
const GATEWAY_CONNECT_RETRY_LIMIT = 2;
const GATEWAY_CONNECT_RETRY_DELAY_MS = 1000;
const BUNDLED_GATEWAY_CONNECT_RETRY_LIMIT = 4;
const BUNDLED_GATEWAY_CONNECT_TIMEOUT_MS = 4000;
const EXTERNAL_GATEWAY_CONNECT_TIMEOUT_MS = 5000;
const BOOTSTRAP_RECOVERY_RETRY_LIMIT = 2;
const BOOTSTRAP_RECOVERY_RETRY_DELAY_MS = 2000;

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

interface ActiveChatStream {
  requestId: string;
  assistantMessageId: string;
  sessionId?: string;
  runId?: string;
  cancelled: boolean;
  markStopped: () => Promise<void>;
}

export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc {
  const {
    appVersion,
    authClient,
    configService,
    creditClient,
    diagnosticsService,
    gatewayClient,
    runtimeManager,
    profileClient,
    secretManager,
    skillStore,
    skillClient,
    expertCatalogService,
    skillCatalogService,
    modelConfigClient,
    runtimeCloudClient,
    runtimeCloudSupervisor,
    dailyReportService,
    runtimeSkillBridge,
    projectStore,
    projectBundleService,
    projectContextService,
    projectChatTargetResolver,
    projectSkillRouter,
    projectExecutionRouter,
    projectWorkspaceExecutor,
    taskPanelService,
    startupLogger,
    systemSummary,
    localOpenClawConfig
  } = services;

  const shouldUseDiscoveredGateway = (runtimeMode: AppConfig["runtimeMode"]): boolean => {
    return shouldUseLocalOpenClawGateway(systemSummary.isPackaged, runtimeMode);
  };

  const getDiscoveredGatewayUrl = (runtimeMode: AppConfig["runtimeMode"]): string | undefined => {
    return shouldUseDiscoveredGateway(runtimeMode) ? localOpenClawConfig?.gatewayUrl : undefined;
  };

  const getDiscoveredGatewayToken = (runtimeMode: AppConfig["runtimeMode"]): string | undefined => {
    return shouldUseDiscoveredGateway(runtimeMode) ? localOpenClawConfig?.gatewayToken : undefined;
  };

  const getEffectiveGatewayToken = async (config?: AppConfig, inputToken?: string): Promise<string | undefined> => {
    if (inputToken) {
      return inputToken;
    }

    const nextConfig = config ?? await configService.load();
    return resolveEffectiveGatewayToken(
      await secretManager.getGatewayToken(),
      getDiscoveredGatewayToken(nextConfig.runtimeMode)
    );
  };

  const applySecretStateToConfig = async (config: AppConfig): Promise<AppConfig> => {
    const [
      apiKey,
      gatewayToken,
      authToken,
      imageModelApiKey,
      videoModelApiKey,
      copywritingModelApiKey,
      digitalHumanVolcAccessKey,
      digitalHumanVolcSecretKey,
      digitalHumanQiniuAccessKey,
      digitalHumanQiniuSecretKey,
      videoAnalyzerApiKey,
      replicationBriefApiKey,
      vectCutApiKey,
      xhsFeishuAppId,
      xhsFeishuAppSecret,
      xhsFeishuAppToken,
      xhsFeishuTableId
    ] = await Promise.all([
      secretManager.getApiKey(),
      getEffectiveGatewayToken(config),
      secretManager.getAuthToken(),
      secretManager.getImageModelApiKey(),
      secretManager.getVideoModelApiKey(),
      secretManager.getCopywritingModelApiKey(),
      secretManager.getDigitalHumanVolcAccessKey(),
      secretManager.getDigitalHumanVolcSecretKey(),
      secretManager.getDigitalHumanQiniuAccessKey(),
      secretManager.getDigitalHumanQiniuSecretKey(),
      secretManager.getVideoAnalyzerApiKey(),
      secretManager.getReplicationBriefApiKey(),
      secretManager.getVectCutApiKey(),
      secretManager.getXhsFeishuAppId(),
      secretManager.getXhsFeishuAppSecret(),
      secretManager.getXhsFeishuAppToken(),
      secretManager.getXhsFeishuTableId()
    ]);

    const nextConfig: AppConfig = {
      ...config,
      gatewayUrl: resolveEffectiveGatewayUrl(config.gatewayUrl, getDiscoveredGatewayUrl(config.runtimeMode)),
      apiKeyConfigured: Boolean(apiKey),
      gatewayTokenConfigured: Boolean(gatewayToken),
      authTokenConfigured: Boolean(authToken),
      expertModelConfig: {
        image: {
          ...config.expertModelConfig.image,
          apiKeyConfigured: Boolean(imageModelApiKey)
        },
        video: {
          ...config.expertModelConfig.video,
          apiKeyConfigured: Boolean(videoModelApiKey)
        },
        copywriting: {
          ...config.expertModelConfig.copywriting,
          apiKeyConfigured: Boolean(copywritingModelApiKey)
        },
        digitalHuman: {
          ...config.expertModelConfig.digitalHuman,
          volcAccessKeyConfigured: Boolean(digitalHumanVolcAccessKey),
          volcSecretKeyConfigured: Boolean(digitalHumanVolcSecretKey),
          qiniuAccessKeyConfigured: Boolean(digitalHumanQiniuAccessKey),
          qiniuSecretKeyConfigured: Boolean(digitalHumanQiniuSecretKey)
        }
      },
      douyinRuntimeConfig: {
        videoAnalyzer: {
          ...config.douyinRuntimeConfig.videoAnalyzer,
          apiKeyConfigured: Boolean(videoAnalyzerApiKey)
        },
        replicationBrief: {
          ...config.douyinRuntimeConfig.replicationBrief,
          apiKeyConfigured: Boolean(replicationBriefApiKey)
        },
        vectcut: {
          ...config.douyinRuntimeConfig.vectcut,
          apiKeyConfigured: Boolean(vectCutApiKey)
        }
      },
      xhsFeishuConfig: {
        appIdConfigured: Boolean(xhsFeishuAppId),
        appSecretConfigured: Boolean(xhsFeishuAppSecret),
        appTokenConfigured: Boolean(xhsFeishuAppToken),
        tableIdConfigured: Boolean(xhsFeishuTableId)
      }
    };

    const secretStateChanged = nextConfig.apiKeyConfigured !== config.apiKeyConfigured
      || nextConfig.gatewayTokenConfigured !== config.gatewayTokenConfigured
      || nextConfig.authTokenConfigured !== config.authTokenConfigured
      || nextConfig.expertModelConfig.image.apiKeyConfigured !== config.expertModelConfig.image.apiKeyConfigured
      || nextConfig.expertModelConfig.video.apiKeyConfigured !== config.expertModelConfig.video.apiKeyConfigured
      || nextConfig.expertModelConfig.copywriting.apiKeyConfigured !== config.expertModelConfig.copywriting.apiKeyConfigured
      || nextConfig.expertModelConfig.digitalHuman.volcAccessKeyConfigured !== config.expertModelConfig.digitalHuman.volcAccessKeyConfigured
      || nextConfig.expertModelConfig.digitalHuman.volcSecretKeyConfigured !== config.expertModelConfig.digitalHuman.volcSecretKeyConfigured
      || nextConfig.expertModelConfig.digitalHuman.qiniuAccessKeyConfigured !== config.expertModelConfig.digitalHuman.qiniuAccessKeyConfigured
      || nextConfig.expertModelConfig.digitalHuman.qiniuSecretKeyConfigured !== config.expertModelConfig.digitalHuman.qiniuSecretKeyConfigured
      || nextConfig.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured !== config.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured
      || nextConfig.douyinRuntimeConfig.replicationBrief.apiKeyConfigured !== config.douyinRuntimeConfig.replicationBrief.apiKeyConfigured
      || nextConfig.douyinRuntimeConfig.vectcut.apiKeyConfigured !== config.douyinRuntimeConfig.vectcut.apiKeyConfigured
      || nextConfig.xhsFeishuConfig.appIdConfigured !== config.xhsFeishuConfig.appIdConfigured
      || nextConfig.xhsFeishuConfig.appSecretConfigured !== config.xhsFeishuConfig.appSecretConfigured
      || nextConfig.xhsFeishuConfig.appTokenConfigured !== config.xhsFeishuConfig.appTokenConfigured
      || nextConfig.xhsFeishuConfig.tableIdConfigured !== config.xhsFeishuConfig.tableIdConfigured;

    if (secretStateChanged) {
      await configService.persist({
        ...nextConfig,
        gatewayUrl: config.gatewayUrl
      });
    }

    return nextConfig;
  };

  const getEffectiveConfig = async () => {
    return applySecretStateToConfig(await configService.load());
  };

  const revealConfigSecret = async (secretId: ConfigSecretId): Promise<string | null> => {
    let value: string | undefined;

    switch (secretId) {
      case "lobsterKey":
        value = await secretManager.getApiKey();
        break;
      case "copywritingModelApiKey":
        value = await secretManager.getCopywritingModelApiKey();
        break;
      case "imageModelApiKey":
        value = await secretManager.getImageModelApiKey();
        break;
      case "videoModelApiKey":
        value = await secretManager.getVideoModelApiKey();
        break;
      case "digitalHumanVolcAccessKey":
        value = await secretManager.getDigitalHumanVolcAccessKey();
        break;
      case "digitalHumanVolcSecretKey":
        value = await secretManager.getDigitalHumanVolcSecretKey();
        break;
      case "digitalHumanQiniuAccessKey":
        value = await secretManager.getDigitalHumanQiniuAccessKey();
        break;
      case "digitalHumanQiniuSecretKey":
        value = await secretManager.getDigitalHumanQiniuSecretKey();
        break;
      case "videoAnalyzerApiKey":
        value = await secretManager.getVideoAnalyzerApiKey();
        break;
      case "replicationBriefApiKey":
        value = await secretManager.getReplicationBriefApiKey();
        break;
      case "vectcutApiKey":
        value = await secretManager.getVectCutApiKey();
        break;
      case "xhsFeishuAppId":
        value = await secretManager.getXhsFeishuAppId();
        break;
      case "xhsFeishuAppSecret":
        value = await secretManager.getXhsFeishuAppSecret();
        break;
      case "xhsFeishuAppToken":
        value = await secretManager.getXhsFeishuAppToken();
        break;
      case "xhsFeishuTableId":
        value = await secretManager.getXhsFeishuTableId();
        break;
      default:
        throw new Error("Unsupported config secret id.");
    }

    return value ? value : null;
  };

  const prepareProjectModelRuntime = async (projectId: string, projectRoot: string): Promise<Record<string, string>> => {
    const config = await configService.load();
    const [
      copywritingApiKey,
      imageApiKey,
      videoApiKey,
      digitalHumanVolcAccessKey,
      digitalHumanVolcSecretKey,
      digitalHumanQiniuAccessKey,
      digitalHumanQiniuSecretKey,
      videoAnalyzerApiKey,
      replicationBriefApiKey,
      vectCutApiKey,
      xhsFeishuAppId,
      xhsFeishuAppSecret,
      xhsFeishuAppToken,
      xhsFeishuTableId
    ] = await Promise.all([
      secretManager.getCopywritingModelApiKey(),
      secretManager.getImageModelApiKey(),
      secretManager.getVideoModelApiKey(),
      secretManager.getDigitalHumanVolcAccessKey(),
      secretManager.getDigitalHumanVolcSecretKey(),
      secretManager.getDigitalHumanQiniuAccessKey(),
      secretManager.getDigitalHumanQiniuSecretKey(),
      secretManager.getVideoAnalyzerApiKey(),
      secretManager.getReplicationBriefApiKey(),
      secretManager.getVectCutApiKey(),
      secretManager.getXhsFeishuAppId(),
      secretManager.getXhsFeishuAppSecret(),
      secretManager.getXhsFeishuAppToken(),
      secretManager.getXhsFeishuTableId()
    ]);
    const runtime = buildProjectModelRuntime(projectId, config, {
      copywritingApiKey,
      imageApiKey,
      videoApiKey,
      digitalHumanVolcAccessKey,
      digitalHumanVolcSecretKey,
      digitalHumanQiniuAccessKey,
      digitalHumanQiniuSecretKey,
      videoAnalyzerApiKey,
      replicationBriefApiKey,
      vectCutApiKey,
      xhsFeishuAppId,
      xhsFeishuAppSecret,
      xhsFeishuAppToken,
      xhsFeishuTableId
    });
    const envFilePath = await materializeProjectModelRuntime(projectRoot, runtime);

    void startupLogger.info("workspace-summary", "project-model-runtime", "Prepared runtime project model config.", {
      projectId,
      envFilePath,
      envKeys: runtime.summary.envKeys,
      copywritingBaseUrl: runtime.summary.copywritingBaseUrl,
      copywritingModelId: runtime.summary.copywritingModelId,
      imageBaseUrl: runtime.summary.imageBaseUrl,
      imageModelId: runtime.summary.imageModelId
    });

    return runtime.env;
  };

  const prepareWorkspaceEntryLobsterEnv = async (): Promise<Record<string, string>> => {
    return buildWorkspaceEntryLobsterEnv(await secretManager.getApiKey());
  };

  const resolveConfiguredChatModel = async (config?: AppConfig) => {
    const nextConfig = config ?? await getEffectiveConfig();
    const baseUrl = nextConfig.expertModelConfig.copywriting.baseUrl.trim();
    const modelId = (nextConfig.expertModelConfig.copywriting.modelId ?? "").trim();
    const apiKey = (await secretManager.getCopywritingModelApiKey())?.trim();
    const missing: string[] = [];

    if (!baseUrl) {
      missing.push("base_url");
    }
    if (!apiKey) {
      missing.push("api_key");
    }
    if (!modelId) {
      missing.push("model_id");
    }

    return {
      baseUrl,
      modelId,
      apiKey,
      apiKeyConfigured: Boolean(apiKey),
      missing
    };
  };

  const readImageAttachmentDataUrls = async (attachments: ChatAttachment[]) => {
    return Promise.all(attachments.map(async (attachment) => {
      const buffer = await readFile(attachment.localPath);
      const mimeType = attachment.mimeType?.trim() || inferAttachmentMimeType(attachment.localPath, attachment.name);
      return `data:${mimeType};base64,${buffer.toString("base64")}`;
    }));
  };

  const readImageAttachmentDataUrl = async (attachment: ChatAttachment): Promise<string | null> => {
    const normalized = normalizeChatAttachmentCandidate(attachment);
    if (!normalized || normalized.kind !== "image") {
      return null;
    }

    try {
      const buffer = await readFile(normalized.localPath);
      const mimeType = normalized.mimeType?.trim() || inferAttachmentMimeType(normalized.localPath, normalized.name);
      return `data:${mimeType};base64,${buffer.toString("base64")}`;
    } catch {
      return null;
    }
  };

  const extractChatCompletionText = (payload: unknown): string => {
    const response = payload as {
      choices?: Array<{
        message?: {
          content?: unknown;
        };
        text?: unknown;
      }>;
    };
    const content = response.choices?.[0]?.message?.content ?? response.choices?.[0]?.text;
    if (typeof content === "string") {
      return content.trim();
    }
    if (Array.isArray(content)) {
      return content.map((part) => {
        if (typeof part === "string") {
          return part;
        }
        if (part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string") {
          return (part as { text: string }).text;
        }
        return "";
      }).join("").trim();
    }
    return "";
  };

  const requestCopywritingChatCompletion = async (input: {
    prompt: string;
    attachments: ChatAttachment[];
    includeImages: boolean;
  }): Promise<string> => {
    const chatModel = await resolveConfiguredChatModel();
    if (chatModel.missing.length > 0) {
      throw new Error(`请先在客户端设置中配置文案模型（首页对话兜底）：${chatModel.missing.join("、")}`);
    }

    const imageUrls = input.includeImages
      ? await readImageAttachmentDataUrls(input.attachments)
      : [];
    const content = imageUrls.length > 0
      ? [
          { type: "text", text: input.prompt },
          ...imageUrls.map((url) => ({
            type: "image_url",
            image_url: { url }
          }))
        ]
      : input.prompt;
    const response = await fetch(normalizeChatCompletionsUrl(chatModel.baseUrl), {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${chatModel.apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        model: chatModel.modelId,
        messages: [
          {
            role: "user",
            content
          }
        ]
      })
    });
    const responseText = await response.text();
    let responsePayload: unknown = null;
    if (responseText.trim()) {
      try {
        responsePayload = JSON.parse(responseText);
      } catch {
        responsePayload = null;
      }
    }
    if (!response.ok) {
      const errorMessage = responsePayload && typeof responsePayload === "object"
        ? ((responsePayload as { error?: { message?: unknown }; message?: unknown }).error?.message
          ?? (responsePayload as { message?: unknown }).message)
        : undefined;
      throw new Error(typeof errorMessage === "string" && errorMessage.trim()
        ? errorMessage.trim()
        : `Copywriting model request failed with HTTP ${response.status}.`);
    }
    const replyText = extractChatCompletionText(responsePayload);
    if (!replyText) {
      throw new Error("Copywriting model returned an empty response.");
    }
    return replyText;
  };

  const requestHomeImageChatCompletion = async (prompt: string, attachments: ChatAttachment[]): Promise<string> => {
    try {
      return await requestCopywritingChatCompletion({
        prompt,
        attachments,
        includeImages: true
      });
    } catch (error) {
      const chatModel = await resolveConfiguredChatModel();
      if (chatModel.missing.length > 0) {
        throw error;
      }
      return requestCopywritingChatCompletion({
        prompt,
        attachments: [],
        includeImages: false
      });
    }
  };

  const resolveGatewayClientTarget = async (
    config?: AppConfig,
    inputToken?: string
  ): Promise<{ gatewayUrl: string; gatewayToken?: string }> => {
    const nextConfig = config ?? await configService.load();
    const runtimeStatus = await runtimeManager.status();
    const runtimeGatewayConnection = await runtimeManager.getGatewayConnection();
    const useBundledRuntime = runtimeStatus.activeMode === "bundled-runtime" && typeof runtimeGatewayConnection.url === "string";

    if (useBundledRuntime) {
      return {
        gatewayUrl: runtimeGatewayConnection.url!,
        gatewayToken: runtimeGatewayConnection.token
      };
    }

    return {
      gatewayUrl: resolveEffectiveGatewayUrl(nextConfig.gatewayUrl, getDiscoveredGatewayUrl(nextConfig.runtimeMode)),
      gatewayToken: await getEffectiveGatewayToken(nextConfig, inputToken)
    };
  };

  const reconfigureGatewayClient = async (config?: AppConfig, inputToken?: string): Promise<void> => {
    const { gatewayUrl, gatewayToken } = await resolveGatewayClientTarget(config, inputToken);

    await gatewayClient.reconfigure(
      gatewayUrl,
      gatewayToken,
      (await secretManager.getDeviceToken()) ?? undefined
    );
  };

  type GatewayConnectRetryMode = "external" | "bundled";

  const connectGatewayClient = async (timeoutMs = EXTERNAL_GATEWAY_CONNECT_TIMEOUT_MS): Promise<void> => {
    const status = await gatewayClient.status().catch(() => null);
    if (status?.state === "connected") {
      return;
    }

    await gatewayClient.reconnect(timeoutMs).catch(() => gatewayClient.connect(timeoutMs));
  };

  const connectGatewayClientWithRetry = async (mode: GatewayConnectRetryMode = "external"): Promise<void> => {
    const retryLimit = mode === "bundled" ? BUNDLED_GATEWAY_CONNECT_RETRY_LIMIT : GATEWAY_CONNECT_RETRY_LIMIT;
    const timeoutMs = mode === "bundled" ? BUNDLED_GATEWAY_CONNECT_TIMEOUT_MS : EXTERNAL_GATEWAY_CONNECT_TIMEOUT_MS;
    let lastError: unknown;
    for (let attempt = 1; attempt <= retryLimit; attempt += 1) {
      try {
        await connectGatewayClient(timeoutMs);
        return;
      } catch (error) {
        lastError = error;
        if (attempt >= retryLimit) {
          break;
        }
        await delay(GATEWAY_CONNECT_RETRY_DELAY_MS);
      }
    }

    throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "Failed to connect Gateway client."));
  };

  const shouldRefreshGatewayClient = async (config?: AppConfig, inputToken?: string): Promise<boolean> => {
    if (inputToken) {
      return true;
    }

    const { gatewayUrl: targetGatewayUrl } = await resolveGatewayClientTarget(config, inputToken);
    const currentStatus = await gatewayClient.status().catch(() => null);

    return currentStatus?.state !== "connected" || currentStatus.url !== targetGatewayUrl;
  };

    const syncRuntimeCloudSupervisor = async (reason: string): Promise<void> => {
    const runtimeStatus = await runtimeManager.status();
    if (runtimeStatus.activeMode === "bundled-runtime" && runtimeStatus.processState === "running") {
      await runtimeCloudSupervisor.start();
      return;
    }

    await runtimeCloudSupervisor.stop(reason);
  };

  const scheduleRuntimeCloudRefresh = (reason: string) => {
    if (runtimeCloudRefreshInFlight) {
      return;
    }

    runtimeCloudRefreshInFlight = true;
    void (async () => {
      const previousConfigVersion = (await runtimeCloudClient.getStatus()).config?.configVersion;
      try {
        const status = await runtimeCloudClient.fetchConfig(previousConfigVersion ? "sync" : "init");
        const nextConfigVersion = status.config?.configVersion;
        if (previousConfigVersion && nextConfigVersion && previousConfigVersion !== nextConfigVersion) {
          await runtimeManager.syncManagedConfig("sync");
        }
        await syncRuntimeCloudSupervisor(`${reason}-runtime-cloud-refresh`);
      } catch {
        // Keep cached startup available even if the immediate cloud refresh fails.
      } finally {
        runtimeCloudRefreshInFlight = false;
      }
    })();
  };

  const syncCachedRemoteProjectsToWorkspace = async (
    reason: string,
    options: {
      action?: RuntimeCloudFetchAction;
      config?: AppConfig;
    } = {}
  ): Promise<void> => {
    const nextConfig = options.config ?? await configService.load();
    if (nextConfig.setupMode !== "employee-key") {
      return;
    }

    const apiKey = (await secretManager.getApiKey())?.trim();
    if (!apiKey) {
      return;
    }

    let runtimeCloudStatus = await runtimeCloudClient.getStatus();
    let remoteSkills = runtimeCloudClient.getRemoteSkillAssets();
    if (!runtimeCloudStatus.config || remoteSkills.length === 0) {
      runtimeCloudStatus = await runtimeCloudClient.fetchConfig(options.action ?? "init");
      remoteSkills = runtimeCloudClient.getRemoteSkillAssets();
    }

    if (!runtimeCloudStatus.config) {
      return;
    }

    await skillStore.reconcile(remoteSkills, runtimeCloudStatus.config.configVersion);
    await projectBundleService.syncRemoteBundles(remoteSkills, runtimeCloudStatus.config.configVersion, options.action ?? "sync");
    await startupLogger.info("project-bundle", "workspace-project-sync", "Synced cached runtime cloud projects into the current workspace root.", {
      reason,
      workspacePath: nextConfig.workspacePath,
      remoteSkillCount: remoteSkills.length,
      configVersion: runtimeCloudStatus.config.configVersion
    });
  };

  const startManagedRuntime = async (
    reason: string,
    options: {
      action?: RuntimeCloudFetchAction;
      restart?: boolean;
      config?: AppConfig;
      inputToken?: string;
    } = {}
  ): Promise<RuntimeStatus> => {
    const nextConfig = options.config ?? await configService.load();
    const apiKey = await secretManager.getApiKey();
    const canPrewarmBundledRuntime = nextConfig.setupMode === "employee-key"
      && nextConfig.runtimeMode !== "external-gateway";
    if (nextConfig.runtimeMode === "external-gateway" || (!apiKey && !canPrewarmBundledRuntime)) {
      await runtimeCloudSupervisor.stop(reason);
      if (await shouldRefreshGatewayClient(nextConfig, options.inputToken)) {
        await reconfigureGatewayClient(nextConfig, options.inputToken);
        await connectGatewayClientWithRetry("external").catch(() => undefined);
      }
      return runtimeManager.status();
    }

    const shouldUseRuntimeCloud = nextConfig.setupMode === "employee-key";
    const canUseRuntimeCloudConfig = shouldUseRuntimeCloud && Boolean(apiKey);
    if (canUseRuntimeCloudConfig) {
      await runtimeCloudClient.hydrateCache();
    }
    const usingCachedRuntimeCloudConfig = canUseRuntimeCloudConfig && (options.action ?? "init") === "init" && runtimeCloudClient.hasCachedPayload();
    if (canUseRuntimeCloudConfig && !usingCachedRuntimeCloudConfig) {
      await runtimeCloudClient.fetchConfig(options.action ?? "init");
    }
    let status = options.restart ? await runtimeManager.restart() : await runtimeManager.start();
    for (let attempt = 1; attempt < MANAGED_RUNTIME_START_RETRY_LIMIT && shouldRetryManagedRuntimeStartup(nextConfig, status); attempt += 1) {
      await runtimeManager.stop().catch(() => undefined);
      await delay(MANAGED_RUNTIME_START_RETRY_DELAY_MS);
      status = await runtimeManager.start();
    }

    if (status.processState === "running" && await shouldRefreshGatewayClient(nextConfig, options.inputToken)) {
      await reconfigureGatewayClient(nextConfig, options.inputToken);
      await connectGatewayClientWithRetry("bundled").catch(() => undefined);
    }
    if (canUseRuntimeCloudConfig) {
      await syncRuntimeCloudSupervisor(reason);
      if (usingCachedRuntimeCloudConfig) {
        if (!initialRuntimeCloudRefreshScheduled) {
          initialRuntimeCloudRefreshScheduled = true;
          scheduleRuntimeCloudRefresh(reason);
        }
      }
    } else {
      await runtimeCloudSupervisor.stop(reason);
    }
    return status;
  };

  const chatGatewayRecoveryCoordinator = {
    getConfig: () => getEffectiveConfig(),
    runtimeStatus: () => runtimeManager.status(),
    gatewayStatus: () => gatewayClient.status().catch(() => null),
    startManagedRuntime,
    shouldRefreshGatewayClient,
    reconfigureGatewayClient,
    connectGatewayClientWithRetry
  };

  let workspaceWarmupTail: Promise<void> = Promise.resolve();
  let workspaceWarmupInFlight = false;
  let runtimeCloudRefreshInFlight = false;
  let initialRuntimeCloudRefreshScheduled = false;
  let bootstrapRecoveryAttempts = 0;
  let lastWorkspaceSummaryLogKey = "";

  const queueWorkspaceWarmup = async (
    reason: string,
    options: {
      action?: RuntimeCloudFetchAction;
      restart?: boolean;
      config?: AppConfig;
      inputToken?: string;
    } = {}
  ): Promise<WorkspaceWarmupResult> => {
    const nextConfig = options.config ?? await configService.load();
    const apiKey = await secretManager.getApiKey();
    const canPrewarmBundledRuntime = nextConfig.setupMode === "employee-key"
      && nextConfig.runtimeMode !== "external-gateway";
    if (!apiKey && !canPrewarmBundledRuntime) {
      return {
        accepted: false,
        state: "skipped",
        message: nextConfig.setupMode === "direct-provider"
          ? "\u5c1a\u672a\u5b8c\u6210\u5382\u5546\u4e0e API Key \u914d\u7f6e\uff0c\u5df2\u8df3\u8fc7\u540e\u53f0\u9884\u70ed\u3002"
          : "\u5c1a\u672a\u7ed1\u5b9a\u5458\u5de5\u5bc6\u94a5\uff0c\u5df2\u8df3\u8fc7\u540e\u53f0\u9884\u70ed\u3002"
      };
    }

    const alreadyBusy = workspaceWarmupInFlight;
    workspaceWarmupTail = workspaceWarmupTail
      .catch(() => undefined)
      .then(async () => {
        workspaceWarmupInFlight = true;
        try {
          const bootstrapRecoveryEnabled = reason === "bootstrap";
          bootstrapRecoveryAttempts = 0;

          while (true) {
            await startManagedRuntime(reason, {
              ...options,
              config: nextConfig
            });

            const runtimeStatus = await runtimeManager.status();
            const gatewayStatus = await gatewayClient.status().catch(() => null);
            const shouldRetryBootstrap = bootstrapRecoveryEnabled
              && bootstrapRecoveryAttempts < BOOTSTRAP_RECOVERY_RETRY_LIMIT
              && shouldRetryBootstrapWarmup({
                config: nextConfig,
                runtimeStatus,
                gatewayStatus,
                isPackaged: systemSummary.isPackaged
              });
            if (!shouldRetryBootstrap) {
              break;
            }

            bootstrapRecoveryAttempts += 1;
            await delay(BOOTSTRAP_RECOVERY_RETRY_DELAY_MS);
          }
        } catch {
          // Workspace summary and runtime status retain the latest failure details.
        } finally {
          workspaceWarmupInFlight = false;
          bootstrapRecoveryAttempts = 0;
        }
      });

    return {
      accepted: true,
      state: "scheduled",
      message: alreadyBusy ? "\u540e\u53f0\u9884\u70ed\u5df2\u6392\u961f\u3002" : "\u540e\u53f0\u9884\u70ed\u5df2\u5f00\u59cb\u3002"
    };
  };

  const saveAppConfig = async (input: SaveConfigInput): Promise<AppConfig> => {
    const previousConfig = await configService.load();
    const config = await configService.save(input);
    const workspacePathChanged = didWorkspacePathChange(previousConfig, config);
    const shouldSyncChatManagedConfig = config.setupMode === "employee-key" && config.runtimeMode !== "external-gateway" && (
      typeof input.expertModelConfig?.copywriting?.baseUrl === "string"
      || typeof input.expertModelConfig?.copywriting?.apiKey === "string"
      || typeof input.expertModelConfig?.copywriting?.modelId === "string"
    );
    if (typeof input.apiKey === "string") {
      await secretManager.setApiKey(input.apiKey || undefined);
    }
    if (typeof input.gatewayToken === "string") {
      await secretManager.setGatewayToken(input.gatewayToken || undefined);
    }
    if (typeof input.authToken === "string") {
      await secretManager.setAuthToken(input.authToken || undefined);
    }
    if (typeof input.expertModelConfig?.image?.apiKey === "string") {
      await secretManager.setImageModelApiKey(input.expertModelConfig.image.apiKey || undefined);
    }
    if (typeof input.expertModelConfig?.video?.apiKey === "string") {
      await secretManager.setVideoModelApiKey(input.expertModelConfig.video.apiKey || undefined);
    }
    if (typeof input.expertModelConfig?.copywriting?.apiKey === "string") {
      await secretManager.setCopywritingModelApiKey(input.expertModelConfig.copywriting.apiKey || undefined);
    }
    if (typeof input.expertModelConfig?.digitalHuman?.volcAccessKey === "string") {
      await secretManager.setDigitalHumanVolcAccessKey(input.expertModelConfig.digitalHuman.volcAccessKey || undefined);
    }
    if (typeof input.expertModelConfig?.digitalHuman?.volcSecretKey === "string") {
      await secretManager.setDigitalHumanVolcSecretKey(input.expertModelConfig.digitalHuman.volcSecretKey || undefined);
    }
    if (typeof input.expertModelConfig?.digitalHuman?.qiniuAccessKey === "string") {
      await secretManager.setDigitalHumanQiniuAccessKey(input.expertModelConfig.digitalHuman.qiniuAccessKey || undefined);
    }
    if (typeof input.expertModelConfig?.digitalHuman?.qiniuSecretKey === "string") {
      await secretManager.setDigitalHumanQiniuSecretKey(input.expertModelConfig.digitalHuman.qiniuSecretKey || undefined);
    }
    if (typeof input.douyinRuntimeConfig?.videoAnalyzer?.apiKey === "string") {
      await secretManager.setVideoAnalyzerApiKey(input.douyinRuntimeConfig.videoAnalyzer.apiKey || undefined);
    }
    if (typeof input.douyinRuntimeConfig?.replicationBrief?.apiKey === "string") {
      await secretManager.setReplicationBriefApiKey(input.douyinRuntimeConfig.replicationBrief.apiKey || undefined);
    }
    if (typeof input.douyinRuntimeConfig?.vectcut?.apiKey === "string") {
      await secretManager.setVectCutApiKey(input.douyinRuntimeConfig.vectcut.apiKey || undefined);
    }
    if (typeof input.xhsFeishuConfig?.appId === "string") {
      await secretManager.setXhsFeishuAppId(input.xhsFeishuConfig.appId || undefined);
    }
    if (typeof input.xhsFeishuConfig?.appSecret === "string") {
      await secretManager.setXhsFeishuAppSecret(input.xhsFeishuConfig.appSecret || undefined);
    }
    if (typeof input.xhsFeishuConfig?.appToken === "string") {
      await secretManager.setXhsFeishuAppToken(input.xhsFeishuConfig.appToken || undefined);
    }
    if (typeof input.xhsFeishuConfig?.tableId === "string") {
      await secretManager.setXhsFeishuTableId(input.xhsFeishuConfig.tableId || undefined);
    }
    if (
      config.setupMode === "direct-provider"
      || previousConfig.setupMode !== config.setupMode
      || previousConfig.runtimeCloudApiBaseUrl !== config.runtimeCloudApiBaseUrl
      || (config.setupMode === "employee-key"
        && typeof input.apiKey === "string"
        && input.apiKey.trim().length > 0)
    ) {
      await runtimeCloudClient.clearCache().catch(() => undefined);
    }
    await runtimeManager.setRequestedMode(config.runtimeMode);
    if (shouldSyncChatManagedConfig) {
      await runtimeManager.syncManagedConfig("sync");
    }

    if (workspacePathChanged) {
      await syncCachedRemoteProjectsToWorkspace("config-save", {
        action: "init",
        config
      }).catch(async (error) => {
        const message = error instanceof Error ? error.message : String(error);
        console.error("[workspace-config]", "workspace-project-sync.error", message);
        await startupLogger.error("project-bundle", "workspace-project-sync.error", "Failed to sync cached runtime cloud projects after workspace path change.", {
          workspacePath: config.workspacePath,
          error: message
        });
      });
    }

    if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
      await reconfigureGatewayClient(config, input.gatewayToken);
      void queueWorkspaceWarmup("config-save", {
        action: "init",
        config,
        inputToken: input.gatewayToken,
        restart: config.setupMode === "employee-key"
          && typeof input.apiKey === "string"
          && input.apiKey.trim().length > 0
      });
    } else {
      await runtimeCloudSupervisor.stop("config-save");
      await reconfigureGatewayClient(config, input.gatewayToken);
      if (config.setupMode === "employee-key") {
        await syncRuntimeCloudSupervisor("config-save");
      }
    }
    void dailyReportService.runDueCheck().catch(() => undefined);
    return getEffectiveConfig();
  };

  const buildWorkspaceSummary = async (): Promise<WorkspaceSummary> => {
    const config = await getEffectiveConfig();
    await projectStore.initialize();
    const runtimeStatus = await runtimeManager.status();
    const runtimeCloudStatus: RuntimeCloudStatus = config.setupMode === "employee-key"
      ? await runtimeCloudClient.getStatus()
      : {
          state: config.apiKeyConfigured ? "ready" : "unconfigured",
          baseUrl: config.baseUrl,
          apiKeyConfigured: config.apiKeyConfigured,
          lastFetchedAt: undefined,
          lastError: undefined,
          config: undefined
        };

    const gatewayStatus = config.apiKeyConfigured || (config.setupMode === "employee-key" && config.runtimeMode !== "external-gateway")
      ? await gatewayClient.status().catch(() => null)
      : null;
    const runtimeTelemetryStatus = runtimeCloudSupervisor.getStatus();
    const baseChatSummary = buildChatSummary({
      config,
      runtimeStatus,
      runtimeCloudStatus,
      gatewayStatus,
      warmupInFlight: workspaceWarmupInFlight,
      runtimeCloudRefreshInFlight,
      runtimeCloudConfigSyncInFlight: runtimeTelemetryStatus.configSyncInFlight,
      isPackaged: systemSummary.isPackaged
    });
    const {
      projects,
      currentProject,
      sessions,
      skills
    } = await loadActiveProjectWorkspaceState(projectStore);
    const missingClientChatModel = !config.expertModelConfig.copywriting.baseUrl.trim()
      || !config.expertModelConfig.copywriting.apiKeyConfigured
      || !(config.expertModelConfig.copywriting.modelId ?? "").trim();
    const bundleSyncStatus = projectBundleService.getSyncStatus();
    const bundleSyncFailed = bundleSyncStatus.state === "error";
    const shellReady = !bundleSyncFailed && isWorkspaceShellReady({
      config,
      runtimeStatus,
      gatewayStatus
    });
    const hasUsableProjectInventory = projects.some((project) => project.ready)
      || Boolean(currentProject?.isBuiltinHome && currentProject.ready);
    const shouldWaitForProjectSync = config.apiKeyConfigured
      && config.setupMode === "employee-key"
      && runtimeCloudStatus.state === "ready"
      && bundleSyncStatus.state === "syncing"
      && !hasUsableProjectInventory
      && baseChatSummary.chatLaunchState !== "error";
    if (shouldWaitForProjectSync && baseChatSummary.startupPhase !== "syncing-projects") {
      void startupLogger.warn("workspace-summary", "phase.override", "Project sync phase is overriding the base startup phase because no project inventory is available yet.", {
        baseLaunchState: baseChatSummary.chatLaunchState,
        basePhase: baseChatSummary.startupPhase,
        runtimeCloudState: runtimeCloudStatus.state,
        projectCount: projects.length,
        hasUsableProjectInventory,
        bundleSyncState: bundleSyncStatus.state
      });
    }
    const chatSummary = bundleSyncFailed
      ? {
          chatReady: false,
          chatLaunchState: "error" as const,
          chatStatusMessage: bundleSyncStatus.lastError ?? "Workspace project sync failed. Check network access and retry.",
          startupPhase: "error" as const,
          startupMessage: bundleSyncStatus.lastError ?? "Workspace project sync failed. Check network access and retry."
        }
      : currentProject?.isBuiltinHome && missingClientChatModel
        ? {
            chatReady: false,
            chatLaunchState: "error" as const,
            chatStatusMessage: "请先在客户端设置中配置文案模型（首页对话兜底）：base_url、api_key、model_id。",
            startupPhase: "error" as const,
            startupMessage: "请先在客户端设置中配置文案模型（首页对话兜底）：base_url、api_key、model_id。"
          }
      : shouldWaitForProjectSync
        ? buildProjectSyncSummary(bundleSyncStatus.lastError ?? EMPTY_PROJECT_INVENTORY_MESSAGE)
        : baseChatSummary;

    const workspaceSummary: WorkspaceSummary = {
      shellReady,
      apiKeyConfigured: config.apiKeyConfigured,
      bindingRequired: !config.apiKeyConfigured,
      setupRequired: !config.apiKeyConfigured,
      setupMode: config.setupMode,
      chatReady: chatSummary.chatReady,
      chatLaunchState: chatSummary.chatLaunchState,
      chatStatusMessage: chatSummary.chatStatusMessage,
      startupPhase: chatSummary.startupPhase,
      startupMessage: chatSummary.startupMessage,
      employeeId: config.setupMode === "employee-key" ? runtimeCloudStatus.config?.employeeId : undefined,
      employeeName: config.setupMode === "employee-key" ? runtimeCloudStatus.config?.employeeName : undefined,
      welcomeMessage: config.setupMode === "employee-key" ? runtimeCloudStatus.config?.welcomeMessage : undefined,
      modelId: config.expertModelConfig.copywriting.modelId || undefined,
      modelDisplayName: config.expertModelConfig.copywriting.modelId || undefined,
      configVersion: config.setupMode === "employee-key" ? runtimeCloudStatus.config?.configVersion : undefined,
      lastFetchedAt: runtimeCloudStatus.lastFetchedAt ?? runtimeCloudStatus.config?.fetchedAt,
      runtimeCloudState: runtimeCloudStatus.state,
      runtimeState: runtimeStatus.processState,
      runtimeMessage: runtimeStatus.message,
      currentProjectId: currentProject?.id,
      currentProjectName: currentProject?.displayName ?? currentProject?.name,
      projectVersion: currentProject?.version,
      projectReady: currentProject?.ready ?? false,
      projectCount: projects.length,
      projects,
      sessions,
      skillCount: skills.length,
      skills,
      plugins: buildPluginSummaries(runtimeStatus),
      lastError: bundleSyncStatus.lastError ?? runtimeCloudStatus.lastError ?? runtimeStatus.lastError ?? gatewayStatus?.lastError
    };

    const workspaceSummaryLogKey = JSON.stringify({
      phase: workspaceSummary.startupPhase,
      launchState: workspaceSummary.chatLaunchState,
      projectCount: workspaceSummary.projectCount,
      runtimeCloudState: workspaceSummary.runtimeCloudState,
      runtimeState: workspaceSummary.runtimeState,
      lastError: workspaceSummary.lastError ?? ""
    });
    if (workspaceSummaryLogKey !== lastWorkspaceSummaryLogKey) {
      lastWorkspaceSummaryLogKey = workspaceSummaryLogKey;
      void startupLogger.info("workspace-summary", "transition", "Workspace startup summary changed.", {
        phase: workspaceSummary.startupPhase,
        launchState: workspaceSummary.chatLaunchState,
        projectCount: workspaceSummary.projectCount,
        runtimeCloudState: workspaceSummary.runtimeCloudState,
        runtimeState: workspaceSummary.runtimeState,
        bundleSyncState: bundleSyncStatus.state,
        lastError: workspaceSummary.lastError
      });
    }

    return workspaceSummary;
  };

  const exportDiagnostics = async () => {
    const config = await getEffectiveConfig();
    const workspaceSummary = await buildWorkspaceSummary();
    const [gatewayStatus, gatewayHealth, logs, authSession, runtimeStatus, runtimeLogs, runtimeCloudStatus, runtimeTelemetryStatus] = await Promise.all([
      gatewayClient.status(),
      gatewayClient.health(),
      gatewayClient.tailLogs(200),
      authClient.getSessionSummary(),
      runtimeManager.status(),
      runtimeManager.tailLogs(200),
      runtimeCloudClient.getStatus(),
      Promise.resolve(runtimeCloudSupervisor.getStatus())
    ]);

    let profile = null;
    let credits = null;
    let skills = [] as Awaited<ReturnType<SkillClient["list"]>>;
    let modelConfig = null;
    let cloudApiError: string | undefined;

    if (authSession.state === "authenticated") {
      try {
        [profile, credits, skills, modelConfig] = await Promise.all([
          profileClient.getSummary(),
          creditClient.getSummary(),
          skillClient.list(),
          modelConfigClient.getSummary()
        ]);
      } catch (error) {
        cloudApiError = error instanceof Error ? error.message : String(error);
      }
    }

    return diagnosticsService.exportSnapshot({
      config,
      gatewayStatus,
      gatewayHealth,
      logs,
      appVersion,
      localOpenClawConfig,
      secretBackend: secretManager.describeBackend(),
      authSession,
      profile,
      credits,
      skills,
      modelConfig,
      cloudApiError,
      systemSummary,
      runtimeStatus,
      runtimeLogs,
      runtimeCloudStatus,
      runtimeTelemetryStatus,
      workspaceSummary,
      bundleSyncStatus: projectBundleService.getSyncStatus(),
      startupLogPath: startupLogger.getSessionLogPath()
    });
  };

  const resolveExecutionPolicy = async (
    projectId: string,
    skillId?: string,
    executionKind?: "workspace-entry" | "skill" | "chat-fallback"
  ) => {
    const config = await getEffectiveConfig();
    const skills = await projectStore.listProjectSkills(projectId);
    const selectedSkill = skillId ? skills.find((skill) => skill.id === skillId) : undefined;

    if (executionKind === "workspace-entry") {
      const workspaceEntryModelId = config.expertModelConfig.copywriting.modelId
        || config.expertModelConfig.image.modelId
        || config.expertModelConfig.video.modelId
        || "workspace-entry";
      return {
        source: "client-config" as const,
        modelId: workspaceEntryModelId,
        modelLabel: workspaceEntryModelId,
        routingMode: "platform-managed" as const,
        skillId,
        skillName: selectedSkill?.name,
        message: "Workspace-entry project is using client-configured expert models."
      };
    }

    const chatModel = await resolveConfiguredChatModel(config);
    if (chatModel.missing.length > 0) {
      throw new Error(`请先在客户端设置中配置文案模型（首页对话兜底）：${chatModel.missing.join("、")}`);
    }

    return {
      source: "client-config" as const,
      modelId: chatModel.modelId,
      modelLabel: chatModel.modelId,
      routingMode: "platform-managed" as const,
      skillId,
      skillName: selectedSkill?.name,
      message: skillId
        ? `Skill ${selectedSkill?.name ?? skillId} is using client copywriting model ${chatModel.modelId}.`
        : `Using client copywriting model ${chatModel.modelId}.`
    };
  };

  const streamListeners = new Set<(payload: ChatStreamEvent) => void>();
  const activeChatStreams = new Map<string, ActiveChatStream>();

  const broadcastChatStreamEvent = (payload: ChatStreamEvent, sender?: WebContents) => {
    if (sender && !sender.isDestroyed()) {
      sender.send(IPC_CHANNELS.chatStreamEvent, payload);
    }

    for (const listener of streamListeners) {
      try {
        listener(payload);
      } catch {
        // Ignore listener failures so one consumer does not break the stream.
      }
    }
  };

  const prepareGatewayPrompt = async (decision: ProjectExecutionDecision, projectId: string): Promise<string | undefined> => {
    if (decision.kind === "chat-fallback") {
      await runtimeSkillBridge.clearManagedSkills();
      return decision.preparedPrompt;
    }

    if (decision.kind === "skill") {
      const runtimeStatus = await runtimeManager.status();
      if (runtimeStatus.activeMode !== "bundled-runtime") {
        throw new Error("Selected skills currently require bundled runtime. Switch from external gateway mode and try again.");
      }
      const prepared = await runtimeSkillBridge.preparePrompt(decision.preparedPrompt, decision.skillId, projectId);
      return prepared.prompt;
    }

    await runtimeSkillBridge.clearManagedSkills();
    return undefined;
  };

  const createChatMessage = (
    role: ChatMessage["role"],
    content: string,
    overrides: Partial<ChatMessage> = {}
  ): ChatMessage => {
    const attachments = normalizeChatAttachments(overrides.attachments);
    return {
      id: overrides.id ?? randomUUID(),
      role,
      content,
      createdAt: overrides.createdAt ?? new Date().toISOString(),
      ...(attachments.length ? { attachments } : {}),
      streamState: overrides.streamState,
      statusLabel: overrides.statusLabel,
      statusDetail: overrides.statusDetail
    };
  };

  const isXhsProject = (value: string): boolean => /xiaohongshu|xhs|小红书/.test(value);
  const isDouyinProject = (value: string): boolean => /douyin|抖音|tiktok/.test(value);
  const isZhihuProject = (value: string): boolean => /zhihu|知乎/.test(value);

  const resolveTaskPanelExpertName = (projectId: string, projectRoot: string): string | null => {
    const normalizedProjectId = projectId.trim().toLowerCase();
    const projectBaseName = path.basename(projectRoot).toLowerCase();
    if (isXhsProject(normalizedProjectId)) {
      return "小红书专家";
    }
    if (isDouyinProject(normalizedProjectId)) {
      return "抖音专家";
    }
    if (isZhihuProject(normalizedProjectId)) {
      return "知乎专家";
    }
    if (isXhsProject(projectBaseName)) {
      return "小红书专家";
    }
    if (isDouyinProject(projectBaseName)) {
      return "抖音专家";
    }
    if (isZhihuProject(projectBaseName)) {
      return "知乎专家";
    }
    return null;
  };

  const toTaskPanelTitle = (prompt: string): string => {
    const firstLine = prompt.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "未命名任务";
    return firstLine.length > 40 ? firstLine.slice(0, 40) + "..." : firstLine;
  };

  const recordWorkspaceTaskPanelExecution = async (input: {
    sessionId: string;
    projectId: string;
    projectRoot: string;
    prompt: string;
    runId: string;
    artifacts?: TaskPanelArtifact[];
  }): Promise<void> => {
    const expertName = resolveTaskPanelExpertName(input.projectId, input.projectRoot);
    if (!expertName) {
      return;
    }

    try {
      await taskPanelService.recordWorkspaceExecution({
        sessionId: input.sessionId,
        runId: input.runId,
        expertName,
        taskTitle: toTaskPanelTitle(input.prompt),
        completedAt: new Date().toISOString(),
        messageCount: 2,
        artifacts: input.artifacts ?? []
      });
    } catch (error) {
      await startupLogger.warn("diagnostics", "task-panel.archive-failed", "Failed to archive workspace-entry task panel data.", {
        sessionId: input.sessionId,
        projectId: input.projectId,
        runId: input.runId,
        error: error instanceof Error ? error.message : String(error)
      });
    }
  };

  const sendHomeImagePrompt = async (
    sessionId: string,
    prompt: string,
    attachments?: ChatAttachment[]
  ) => {
    const normalizedAttachments = normalizeChatAttachments(attachments);
    const sessionState = await projectStore.getSessionState(sessionId);
    const executionPolicy = await resolveExecutionPolicy(sessionState.projectId, undefined, "chat-fallback");
    await projectStore.setSessionSelectedSkill(sessionId, null);
    await projectStore.updateSessionLastActive(sessionId);
    await ensureLocalTranscript(sessionId);
    await projectStore.appendSessionMessage(sessionId, createChatMessage("user", prompt, {
      attachments: normalizedAttachments
    }));
    runtimeCloudSupervisor.noteMessageReceived(sessionId, prompt, undefined);
    try {
      const replyContent = await requestHomeImageChatCompletion(prompt, normalizedAttachments);
      const reply = createChatMessage("assistant", replyContent);
      await projectStore.appendSessionMessage(sessionId, reply);
      await projectStore.updateSessionLastActive(sessionId).catch(() => undefined);
      runtimeCloudSupervisor.noteMessageSent(sessionId, reply.content, executionPolicy.modelId, undefined);
      return {
        sessionId,
        reply,
        executionPolicy
      };
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      runtimeCloudSupervisor.noteError("chat_send_failed", message, {
        modelId: executionPolicy.modelId,
        sessionId
      });
      throw error;
    }
  };

  const ensureLocalTranscript = async (sessionId: string): Promise<ChatMessage[]> => {
    const localMessages = await projectStore.listSessionMessages(sessionId);
    if (localMessages.length > 0) {
      return localMessages;
    }

    try {
      const gatewayMessages = await gatewayClient.listMessages(sessionId);
      if (gatewayMessages.length > 0) {
        await projectStore.seedSessionMessages(sessionId, gatewayMessages);
      }
      return gatewayMessages;
    } catch {
      return localMessages;
    }
  };

  const resolveProjectIntentSuggestion = async (prompt: string, currentProjectId?: string): Promise<ProjectIntentSuggestion | null> => {
    const route = await projectChatTargetResolver.resolveIntentSuggestion(prompt, currentProjectId);
    if (!route) {
      return null;
    }

    const projects = await projectStore.listProjects();
    const project = projects.find((item) => item.id === route.projectId);
    if (!project || project.isBuiltinHome) {
      return null;
    }

    return {
      projectId: project.id,
      projectName: project.name,
      projectDisplayName: project.displayName?.trim() || project.name,
      score: route.score,
      confidence: route.confidence,
      reason: route.reason,
      matchedAliases: route.matchedAliases
    };
  };

  const prepareProjectAwareExecution = async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => {
    const requestedSkillId = skillId?.trim() ? skillId.trim() : null;
    const target = await projectChatTargetResolver.resolve(sessionId, prompt, requestedSkillId);
    const resolvedAttachments = await materializeProjectAttachments(
      target.sessionState.projectRoot,
      target.sessionState.sessionId,
      attachments
    );
    if (resolvedAttachments.length > 0 && requestedSkillId) {
      throw new Error("Attachments are only supported when the project routes through workspace-entry.");
    }
    const resolvedSessionId = target.sessionState.sessionId;
    const projectConfig = await projectStore.getProjectPackageConfig(target.sessionState.projectId);
    const snapshot = await projectContextService.getSnapshot(target.sessionState.projectId);
    const declaredWorkspaceEntryDecision = requestedSkillId
      ? null
      : await projectExecutionRouter.decide({
          sessionId: resolvedSessionId,
          projectId: target.sessionState.projectId,
          projectRoot: target.sessionState.projectRoot,
          userPrompt: prompt,
          context: snapshot,
          selectedSkillId: null,
          attachments: resolvedAttachments,
          projectConfig
        });
    const preferWorkspaceEntry = resolvedAttachments.length > 0 || declaredWorkspaceEntryDecision?.kind === "workspace-entry";
    const autoSkillRoute = requestedSkillId
      || preferWorkspaceEntry
      ? null
      : await projectSkillRouter.resolve(target.sessionState.projectId, prompt);
    const defaultEntryRoute = (!requestedSkillId && !autoSkillRoute && projectConfig?.defaultEntry?.type === "skill")
      ? ((await projectStore.getProjectSkillTarget(target.sessionState.projectId, projectConfig.defaultEntry.id))
        ? {
            skillId: projectConfig.defaultEntry.id,
            reason: `project default entry ${projectConfig.defaultEntry.id}`,
            score: 0
          }
        : null)
      : null;
    const resolvedSkillRoute = autoSkillRoute ?? defaultEntryRoute;
    const candidateSkillId = requestedSkillId ?? resolvedSkillRoute?.skillId ?? null;

    const decision = candidateSkillId
      ? await projectExecutionRouter.decide({
          sessionId: resolvedSessionId,
          projectId: target.sessionState.projectId,
          projectRoot: target.sessionState.projectRoot,
          userPrompt: prompt,
          context: snapshot,
          selectedSkillId: candidateSkillId,
          attachments: resolvedAttachments,
          projectConfig
        })
      : (declaredWorkspaceEntryDecision ?? await projectExecutionRouter.decide({
          sessionId: resolvedSessionId,
          projectId: target.sessionState.projectId,
          projectRoot: target.sessionState.projectRoot,
          userPrompt: prompt,
          context: snapshot,
          selectedSkillId: null,
          attachments: resolvedAttachments,
          projectConfig
        }));
    if (resolvedAttachments.length > 0 && decision.kind !== "workspace-entry") {
      throw new Error("Attachments currently require a project workspace-entry route.");
    }
    const selectedSkillId = decision.kind === "skill" ? decision.skillId : null;
    await projectStore.setSessionSelectedSkill(resolvedSessionId, selectedSkillId);
    const reboundSessionState = await projectStore.getSessionState(resolvedSessionId);
    if (reboundSessionState.contextSnapshotId !== snapshot.snapshotId) {
      await projectStore.bindSessionContextSnapshot(resolvedSessionId, snapshot.snapshotId);
    }
    const executionSkillId = decision.kind === "skill" ? decision.skillId : undefined;
    const executionPolicy = await resolveExecutionPolicy(reboundSessionState.projectId, executionSkillId, decision.kind);
    const gatewayPrompt = await prepareGatewayPrompt(decision, reboundSessionState.projectId);
    return {
      sessionState: reboundSessionState,
      snapshot,
      decision,
      executionPolicy,
      gatewayPrompt,
      attachments: resolvedAttachments,
      route: target.route,
      autoRouted: target.autoRouted,
      previousProjectId: target.previousProjectId,
      skillRoute: resolvedSkillRoute,
      autoSelectedSkill: !requestedSkillId && decision.kind === "skill" && Boolean(resolvedSkillRoute)
    };
  };

  const listChatMessages = async (sessionId: string): Promise<ChatMessage[]> => ensureLocalTranscript(sessionId);
  const sendPrompt = async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => {
    if (isHomeImageAttachmentRequest(sessionId, skillId, attachments)) {
      return sendHomeImagePrompt(sessionId, prompt, attachments);
    }

    const preparedExecution = await prepareProjectAwareExecution(sessionId, prompt, skillId, attachments);
    const executionSessionId = preparedExecution.sessionState.sessionId;
    const executionSkillId = preparedExecution.decision.kind === "skill" ? preparedExecution.decision.skillId : undefined;
    const shouldScheduleContextRefresh = shouldRefreshProjectContextAfterExecution(preparedExecution.decision);
    await projectStore.updateSessionLastActive(executionSessionId);
    await ensureLocalTranscript(executionSessionId);
    await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, {
      attachments: preparedExecution.attachments
    }));
      runtimeCloudSupervisor.noteMessageReceived(executionSessionId, prompt, executionSkillId);
      try {
        if (preparedExecution.decision.kind === "workspace-entry") {
          const lobsterEnv = await prepareWorkspaceEntryLobsterEnv();
          const projectModelEnv = await prepareProjectModelRuntime(
            preparedExecution.sessionState.projectId,
            preparedExecution.sessionState.projectRoot
          );
          const result = await projectWorkspaceExecutor.execute({
            sessionId: executionSessionId,
            projectRoot: preparedExecution.sessionState.projectRoot,
            prompt: preparedExecution.decision.preparedPrompt,
            userPrompt: prompt,
            attachments: preparedExecution.attachments,
            extraEnv: {
              ...projectModelEnv,
              ...lobsterEnv
            }
          });
          if ("handoff" in result) {
            const fallbackExecutionPolicy = await resolveExecutionPolicy(
              preparedExecution.sessionState.projectId,
              undefined,
              "chat-fallback"
            );
            const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
              reason: "chat-send",
              execute: () => gatewayClient.sendPrompt(executionSessionId, result.handoff.content)
            });
            await projectStore.appendSessionMessage(fallbackResult.sessionId, fallbackResult.reply);
            await projectStore.updateSessionLastActive(fallbackResult.sessionId).catch(() => undefined);
            runtimeCloudSupervisor.noteMessageSent(
              fallbackResult.sessionId,
              fallbackResult.reply.content,
              fallbackExecutionPolicy.modelId,
              executionSkillId
            );
            await recordWorkspaceTaskPanelExecution({
              sessionId: executionSessionId,
              projectId: preparedExecution.sessionState.projectId,
              projectRoot: preparedExecution.sessionState.projectRoot,
              prompt,
              runId: result.runId,
              artifacts: result.artifacts
            });
            return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy };
          }
          await projectStore.appendSessionMessage(executionSessionId, result.reply);
          await projectStore.updateSessionLastActive(executionSessionId).catch(() => undefined);
          runtimeCloudSupervisor.noteMessageSent(executionSessionId, result.reply.content, preparedExecution.executionPolicy.modelId, executionSkillId);
          await recordWorkspaceTaskPanelExecution({
            sessionId: executionSessionId,
            projectId: preparedExecution.sessionState.projectId,
            projectRoot: preparedExecution.sessionState.projectRoot,
            prompt,
            runId: result.runId,
            artifacts: result.artifacts
          });
          return {
          sessionId: executionSessionId,
          reply: result.reply,
          executionPolicy: preparedExecution.executionPolicy
        };
      }
      const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
        reason: "chat-send",
        execute: () => gatewayClient.sendPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt)
      });
      await projectStore.appendSessionMessage(result.sessionId, result.reply);
      await projectStore.updateSessionLastActive(result.sessionId).catch(() => undefined);
      runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, preparedExecution.executionPolicy.modelId, executionSkillId);
      await recordWorkspaceTaskPanelExecution({
        sessionId: result.sessionId,
        projectId: preparedExecution.sessionState.projectId,
        projectRoot: preparedExecution.sessionState.projectRoot,
        prompt,
        runId: result.reply.id || randomUUID(),
        artifacts: []
      });
      return { ...result, executionPolicy: preparedExecution.executionPolicy };
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      runtimeCloudSupervisor.noteError("chat_send_failed", message, {
        modelId: preparedExecution.executionPolicy.modelId,
        sessionId: executionSessionId
      });
      throw error;
    } finally {
      if (shouldScheduleContextRefresh) {
        void refreshProjectContextAfterExecution({
          sessionId: executionSessionId,
          projectId: preparedExecution.sessionState.projectId,
          projectContextService,
          projectStore
        });
      }
    }
  };
  const streamPrompt = async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[], sender?: WebContents) => {
    const requestId = randomUUID();
    const userMessageId = randomUUID();
    const assistantMessageId = randomUUID();
    let executionPolicy: Awaited<ReturnType<typeof resolveExecutionPolicy>> | null = null;
    let executionSkillId: string | undefined;
    let executionSessionId = sessionId;
    let refreshProjectId: string | null = null;
    let shouldScheduleContextRefresh = false;
    let contextRefreshQueued = false;
    let settled = false;
    let ready = false;
    let startedEvent: ChatStreamEvent | null = null;
    const queuedEvents: ChatStreamEvent[] = [];
    let assistantTranscript = createChatMessage("assistant", "", {
      id: assistantMessageId,
      streamState: "streaming"
    });
    let transcriptWriteChain = Promise.resolve();
    const queueAssistantTranscriptWrite = (nextMessage: ChatMessage) => {
      assistantTranscript = nextMessage;
      transcriptWriteChain = transcriptWriteChain
        .catch(() => undefined)
        .then(async () => {
          await projectStore.upsertSessionMessage(executionSessionId, nextMessage);
        });
      return transcriptWriteChain;
    };
    const updateAssistantTranscript = (updater: (current: ChatMessage) => ChatMessage) => {
      if (activeChatStream.cancelled) {
        return transcriptWriteChain;
      }
      const nextMessage = updater(assistantTranscript);
      return queueAssistantTranscriptWrite(nextMessage);
    };
    const activeChatStream: ActiveChatStream = {
      requestId,
      assistantMessageId,
      sessionId: executionSessionId,
      cancelled: false,
      markStopped: async () => {
        await queueAssistantTranscriptWrite({
          ...assistantTranscript,
          content: assistantTranscript.content,
          streamState: undefined,
          statusLabel: "已停止",
          statusDetail: undefined
        });
      }
    };
    const finishActiveChatStream = () => {
      if (activeChatStreams.get(requestId) === activeChatStream) {
        activeChatStreams.delete(requestId);
      }
    };
    activeChatStreams.set(requestId, activeChatStream);
    const queueProjectContextRefresh = () => {
      if (contextRefreshQueued || !shouldScheduleContextRefresh || !refreshProjectId) {
        return;
      }
      contextRefreshQueued = true;
      void refreshProjectContextAfterExecution({
        sessionId: executionSessionId,
        projectId: refreshProjectId,
        projectContextService,
        projectStore
      });
    };
    const queueOrSend = (payload: ChatStreamEvent) => {
      if (activeChatStream.cancelled) {
        return;
      }
      if (!ready) {
        if (payload.type === "started") {
          startedEvent = payload;
        } else {
          queuedEvents.push(payload);
        }
        return;
      }
      broadcastChatStreamEvent(payload, sender);
    };
    const flushQueuedEvents = (fallbackStarted?: ChatStreamEvent) => {
      setTimeout(() => {
        if (activeChatStream.cancelled) {
          return;
        }
        if (startedEvent) {
          broadcastChatStreamEvent(startedEvent, sender);
        } else if (fallbackStarted) {
          broadcastChatStreamEvent(fallbackStarted, sender);
        }
        for (const queuedEvent of queuedEvents) {
          broadcastChatStreamEvent(queuedEvent, sender);
        }
      }, 0);
    };
    const startHomeImageStream = async () => {
      const normalizedAttachments = normalizeChatAttachments(attachments);
      const sessionState = await projectStore.getSessionState(sessionId);
      executionSessionId = sessionState.sessionId;
      executionPolicy = await resolveExecutionPolicy(sessionState.projectId, undefined, "chat-fallback");
      await projectStore.setSessionSelectedSkill(executionSessionId, null);
      await projectStore.updateSessionLastActive(executionSessionId);
      await ensureLocalTranscript(executionSessionId);
      await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, {
        id: userMessageId,
        attachments: normalizedAttachments
      }));
      await queueAssistantTranscriptWrite(createChatMessage("assistant", "", {
        id: assistantMessageId,
        createdAt: assistantTranscript.createdAt,
        streamState: "streaming",
        statusLabel: "Question received, preparing response"
      }));
      runtimeCloudSupervisor.noteMessageReceived(executionSessionId, prompt, undefined);
      const runId = randomUUID();
      activeChatStream.sessionId = executionSessionId;
      activeChatStream.runId = runId;
      queueOrSend({
        type: "status",
        requestId,
        sessionId: executionSessionId,
        runId,
        stage: "await-model",
        label: "Question received, preparing response"
      });
      ready = true;
      flushQueuedEvents({
        type: "started",
        requestId,
        sessionId: executionSessionId,
        runId,
        executionPolicy: executionPolicy ?? undefined
      });
      void (async () => {
        try {
          const replyContent = await requestHomeImageChatCompletion(prompt, normalizedAttachments);
          if (activeChatStream.cancelled) {
            return;
          }
          const reply = createChatMessage("assistant", replyContent, {
            id: assistantMessageId
          });
          settled = true;
          finishActiveChatStream();
          await updateAssistantTranscript((current) => ({
            ...current,
            content: reply.content,
            createdAt: reply.createdAt,
            streamState: undefined,
            statusLabel: undefined,
            statusDetail: undefined
          }));
          await projectStore.updateSessionLastActive(executionSessionId).catch(() => undefined);
          runtimeCloudSupervisor.noteMessageSent(executionSessionId, reply.content, executionPolicy?.modelId, undefined);
          queueOrSend({
            type: "completed",
            requestId,
            sessionId: executionSessionId,
            runId,
            reply,
            executionPolicy: executionPolicy ?? undefined
          });
        } catch (error) {
          if (activeChatStream.cancelled) {
            return;
          }
          settled = true;
          finishActiveChatStream();
          const message = error instanceof Error ? error.message : String(error);
          await updateAssistantTranscript((current) => ({
            ...current,
            content: current.content.trim() ? current.content : message,
            streamState: "error",
            statusLabel: undefined,
            statusDetail: undefined
          }));
          runtimeCloudSupervisor.noteError("chat_stream_failed", message, {
            modelId: executionPolicy?.modelId,
            sessionId: executionSessionId
          });
          queueOrSend({
            type: "error",
            requestId,
            sessionId: executionSessionId,
            runId,
            message
          });
        }
      })();
      return {
        requestId,
        sessionId: executionSessionId,
        runId,
        userMessageId,
        assistantMessageId,
        executionPolicy: executionPolicy ?? undefined
      };
    };
    try {
      if (isHomeImageAttachmentRequest(sessionId, skillId, attachments)) {
        return await startHomeImageStream();
      }

      const initialStatusLabel = skillId ? "Preparing project context and skill" : "Preparing project context";
      queueOrSend({
        type: "status",
        requestId,
        sessionId,
        stage: "prepare-request",
        label: initialStatusLabel
      });
      const preparedExecution = await prepareProjectAwareExecution(sessionId, prompt, skillId, attachments);
      executionSessionId = preparedExecution.sessionState.sessionId;
      executionPolicy = preparedExecution.executionPolicy;
      executionSkillId = preparedExecution.decision.kind === "skill" ? preparedExecution.decision.skillId : undefined;
      refreshProjectId = preparedExecution.sessionState.projectId;
      shouldScheduleContextRefresh = shouldRefreshProjectContextAfterExecution(preparedExecution.decision);
      if (preparedExecution.autoRouted && preparedExecution.route) {
        queueOrSend({
          type: "status",
          requestId,
          sessionId: executionSessionId,
          stage: "route-project",
          label: `Routing to project ${preparedExecution.sessionState.projectId}`,
          detail: preparedExecution.route.reason
        });
      }
      if (preparedExecution.autoSelectedSkill && preparedExecution.skillRoute) {
        queueOrSend({
          type: "status",
          requestId,
          sessionId: executionSessionId,
          stage: "route-skill",
          label: `Routing to skill ${preparedExecution.skillRoute.skillId}`,
          detail: preparedExecution.skillRoute.reason
        });
      }
      await projectStore.updateSessionLastActive(executionSessionId);
      await ensureLocalTranscript(executionSessionId);
      await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, {
        id: userMessageId,
        attachments: preparedExecution.attachments
      }));
      await queueAssistantTranscriptWrite(createChatMessage("assistant", "", {
        id: assistantMessageId,
        createdAt: assistantTranscript.createdAt,
        streamState: "streaming",
        statusLabel: initialStatusLabel
      }));
      runtimeCloudSupervisor.noteMessageReceived(executionSessionId, prompt, executionSkillId);
      const awaitingLabel = "Question received, preparing response";
      await updateAssistantTranscript((current) => ({
        ...current,
        streamState: "streaming",
        statusLabel: awaitingLabel,
        statusDetail: undefined
      }));
      queueOrSend({
        type: "status",
        requestId,
        sessionId: executionSessionId,
        stage: "await-model",
        label: awaitingLabel
      });
      if (preparedExecution.decision.kind === "workspace-entry") {
        ready = true;
        flushQueuedEvents();
        void (async () => {
          try {
            const lobsterEnv = await prepareWorkspaceEntryLobsterEnv();
            const projectModelEnv = await prepareProjectModelRuntime(
              preparedExecution.sessionState.projectId,
              preparedExecution.sessionState.projectRoot
            );
            const result = await projectWorkspaceExecutor.execute({
              sessionId: executionSessionId,
              projectRoot: preparedExecution.sessionState.projectRoot,
              prompt: preparedExecution.decision.preparedPrompt,
              userPrompt: prompt,
              attachments: preparedExecution.attachments,
              extraEnv: {
                ...projectModelEnv,
                ...lobsterEnv
              }
            }, {
              onStarted: (runId) => {
                activeChatStream.sessionId = executionSessionId;
                activeChatStream.runId = runId;
                queueOrSend({
                  type: "started",
                  requestId,
                  sessionId: executionSessionId,
                  runId,
                  executionPolicy: executionPolicy ?? undefined
                });
              },
              onStatus: (stage, label, detail) => {
                if (activeChatStream.cancelled) {
                  return;
                }
                void updateAssistantTranscript((current) => ({
                  ...current,
                  streamState: "streaming",
                  statusLabel: label,
                  statusDetail: detail
                }));
                queueOrSend({
                  type: "status",
                  requestId,
                  sessionId: executionSessionId,
                  stage,
                  label,
                  detail
                });
              },
              onDelta: (textDelta, fullText, runId) => {
                if (activeChatStream.cancelled) {
                  return;
                }
                activeChatStream.sessionId = executionSessionId;
                activeChatStream.runId = runId;
                void updateAssistantTranscript((current) => ({
                  ...current,
                  content: fullText && fullText.length >= current.content.length
                    ? fullText
                    : current.content + textDelta,
                  streamState: "streaming",
                  statusLabel: undefined,
                  statusDetail: undefined
                }));
                queueOrSend({
                  type: "delta",
                  requestId,
                  sessionId: executionSessionId,
                  runId,
                  textDelta,
                  fullText
                });
              }
            });
            if ("handoff" in result) {
              if (activeChatStream.cancelled) {
                return;
              }
              executionPolicy = await resolveExecutionPolicy(
                preparedExecution.sessionState.projectId,
                undefined,
                "chat-fallback"
              );
              await updateAssistantTranscript((current) => ({
                ...current,
                streamState: "streaming",
                statusLabel: "Routing to chat model",
                statusDetail: undefined
              }));
              queueOrSend({
                type: "status",
                requestId,
                sessionId: executionSessionId,
                runId: result.runId,
                stage: "chat-fallback",
                label: "Routing to chat model"
              });
              await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
                reason: "chat-stream",
                execute: () => gatewayClient.streamPrompt(executionSessionId, result.handoff.content, {
                  onStarted: ({ sessionId: nextSessionId, runId }) => {
                    executionSessionId = nextSessionId;
                    activeChatStream.sessionId = nextSessionId;
                    activeChatStream.runId = runId;
                    queueOrSend({
                      type: "started",
                      requestId,
                      sessionId: nextSessionId,
                      runId,
                      executionPolicy: executionPolicy ?? undefined
                    });
                  },
                  onStatus: ({ sessionId: nextSessionId, runId, stage, label, detail }) => {
                    if (activeChatStream.cancelled) {
                      return;
                    }
                    executionSessionId = nextSessionId;
                    activeChatStream.sessionId = nextSessionId;
                    activeChatStream.runId = runId;
                    void updateAssistantTranscript((current) => ({
                      ...current,
                      streamState: "streaming",
                      statusLabel: label,
                      statusDetail: detail
                    }));
                    queueOrSend({
                      type: "status",
                      requestId,
                      sessionId: nextSessionId,
                      runId,
                      stage,
                      label,
                      detail
                    });
                  },
                  onDelta: ({ sessionId: nextSessionId, runId, textDelta, fullText }) => {
                    if (activeChatStream.cancelled) {
                      return;
                    }
                    executionSessionId = nextSessionId;
                    activeChatStream.sessionId = nextSessionId;
                    activeChatStream.runId = runId;
                    void updateAssistantTranscript((current) => ({
                      ...current,
                      content: fullText && fullText.length >= current.content.length
                        ? fullText
                        : current.content + textDelta,
                      streamState: "streaming",
                      statusLabel: undefined,
                      statusDetail: undefined
                    }));
                    queueOrSend({
                      type: "delta",
                      requestId,
                      sessionId: nextSessionId,
                      runId,
                      textDelta,
                      fullText
                    });
                  },
                  onCompleted: ({ sessionId: nextSessionId, runId, reply }) => {
                    if (activeChatStream.cancelled) {
                      return;
                    }
                    executionSessionId = nextSessionId;
                    activeChatStream.sessionId = nextSessionId;
                    activeChatStream.runId = runId;
                    settled = true;
                    finishActiveChatStream();
                    void (async () => {
                      await updateAssistantTranscript((current) => ({
                        ...current,
                        content: reply.content,
                        createdAt: reply.createdAt,
                        streamState: undefined,
                        statusLabel: undefined,
                        statusDetail: undefined
                      }));
                    await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined);
                  })().catch(() => undefined);
                    runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy?.modelId, executionSkillId);
                    void recordWorkspaceTaskPanelExecution({
                      sessionId: executionSessionId,
                      projectId: preparedExecution.sessionState.projectId,
                      projectRoot: preparedExecution.sessionState.projectRoot,
                      prompt,
                      runId: result.runId,
                      artifacts: result.artifacts
                    });
                    queueOrSend({
                      type: "completed",
                      requestId,
                      sessionId: nextSessionId,
                      runId,
                      reply,
                      executionPolicy: executionPolicy ?? undefined
                    });
                    queueProjectContextRefresh();
                  },
                  onError: ({ sessionId: nextSessionId, runId, error }) => {
                    if (activeChatStream.cancelled) {
                      return;
                    }
                    executionSessionId = nextSessionId;
                    activeChatStream.sessionId = nextSessionId;
                    activeChatStream.runId = runId;
                    settled = true;
                    finishActiveChatStream();
                    const errorCategory = typeof (error as Error & { errorCategory?: unknown }).errorCategory === "string"
                      ? String((error as Error & { errorCategory?: unknown }).errorCategory).trim()
                      : "";
                    void updateAssistantTranscript((current) => ({
                      ...current,
                      content: current.content.trim() ? current.content : error.message,
                      streamState: "error",
                      statusLabel: undefined,
                      statusDetail: undefined
                    }));
                    runtimeCloudSupervisor.noteError("chat_stream_failed", error.message, {
                      modelId: executionPolicy?.modelId,
                      sessionId: nextSessionId
                    });
                    queueOrSend({
                      type: "error",
                      requestId,
                      sessionId: nextSessionId,
                      runId,
                      message: error.message,
                      errorCategory: errorCategory || undefined
                    });
                    queueProjectContextRefresh();
                  }
                })
              });
              return;
            }
            if (activeChatStream.cancelled) {
              return;
            }
            settled = true;
            activeChatStream.sessionId = executionSessionId;
            activeChatStream.runId = result.runId;
            finishActiveChatStream();
            await updateAssistantTranscript((current) => ({
              ...current,
              content: result.reply.content,
              createdAt: result.reply.createdAt,
              streamState: undefined,
              statusLabel: undefined,
              statusDetail: undefined
            }));
            await projectStore.updateSessionLastActive(executionSessionId).catch(() => undefined);
            runtimeCloudSupervisor.noteMessageSent(executionSessionId, result.reply.content, executionPolicy?.modelId, executionSkillId);
            await recordWorkspaceTaskPanelExecution({
              sessionId: executionSessionId,
              projectId: preparedExecution.sessionState.projectId,
              projectRoot: preparedExecution.sessionState.projectRoot,
              prompt,
              runId: result.runId,
              artifacts: result.artifacts
            });
            queueOrSend({
              type: "completed",
              requestId,
              sessionId: executionSessionId,
              runId: result.runId,
              reply: result.reply,
              executionPolicy: executionPolicy ?? undefined
            });
          } catch (error) {
            if (activeChatStream.cancelled) {
              return;
            }
            settled = true;
            finishActiveChatStream();
            const message = error instanceof Error ? error.message : String(error);
            const errorCategory = error instanceof Error && typeof (error as Error & { errorCategory?: unknown }).errorCategory === "string"
              ? String((error as Error & { errorCategory?: unknown }).errorCategory).trim()
              : "";
            await updateAssistantTranscript((current) => ({
              ...current,
              content: current.content.trim() ? current.content : message,
              streamState: "error",
              statusLabel: undefined,
              statusDetail: undefined
            }));
            runtimeCloudSupervisor.noteError("chat_stream_failed", message, {
              modelId: executionPolicy?.modelId,
              sessionId: executionSessionId
            });
            queueOrSend({
              type: "error",
              requestId,
              sessionId: executionSessionId,
              message,
              errorCategory: errorCategory || undefined
            });
          } finally {
            queueProjectContextRefresh();
          }
        })();
        return {
          requestId,
          sessionId: executionSessionId,
          userMessageId,
          assistantMessageId,
          executionPolicy: executionPolicy ?? undefined
        };
      }
      const stream = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
        reason: "chat-stream",
        execute: () => gatewayClient.streamPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt, {
          onStarted: ({ sessionId: nextSessionId, runId }) => {
            executionSessionId = nextSessionId;
            activeChatStream.sessionId = nextSessionId;
            activeChatStream.runId = runId;
            queueOrSend({
              type: "started",
              requestId,
              sessionId: nextSessionId,
              runId,
              executionPolicy: executionPolicy ?? undefined
            });
          },
          onStatus: ({ sessionId: nextSessionId, runId, stage, label, detail }) => {
            if (activeChatStream.cancelled) {
              return;
            }
            executionSessionId = nextSessionId;
            activeChatStream.sessionId = nextSessionId;
            activeChatStream.runId = runId;
            void updateAssistantTranscript((current) => ({
              ...current,
              streamState: "streaming",
              statusLabel: label,
              statusDetail: detail
            }));
            queueOrSend({
              type: "status",
              requestId,
              sessionId: nextSessionId,
              runId,
              stage,
              label,
              detail
            });
          },
          onDelta: ({ sessionId: nextSessionId, runId, textDelta, fullText }) => {
            if (activeChatStream.cancelled) {
              return;
            }
            executionSessionId = nextSessionId;
            activeChatStream.sessionId = nextSessionId;
            activeChatStream.runId = runId;
            void updateAssistantTranscript((current) => ({
              ...current,
              content: fullText && fullText.length >= current.content.length
                ? fullText
                : current.content + textDelta,
              streamState: "streaming",
              statusLabel: undefined,
              statusDetail: undefined
            }));
            queueOrSend({
              type: "delta",
              requestId,
              sessionId: nextSessionId,
              runId,
              textDelta,
              fullText
            });
          },
          onCompleted: ({ sessionId: nextSessionId, runId, reply }) => {
            if (activeChatStream.cancelled) {
              return;
            }
            executionSessionId = nextSessionId;
            activeChatStream.sessionId = nextSessionId;
            activeChatStream.runId = runId;
            settled = true;
            finishActiveChatStream();
            void (async () => {
              await updateAssistantTranscript((current) => ({
                ...current,
                content: reply.content,
                createdAt: reply.createdAt,
                streamState: undefined,
                statusLabel: undefined,
                statusDetail: undefined
              }));
              await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined);
            })().catch(() => undefined);
            runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy?.modelId, executionSkillId);
            void recordWorkspaceTaskPanelExecution({
              sessionId: nextSessionId,
              projectId: preparedExecution.sessionState.projectId,
              projectRoot: preparedExecution.sessionState.projectRoot,
              prompt,
              runId,
              artifacts: []
            });
            queueOrSend({
              type: "completed",
              requestId,
              sessionId: nextSessionId,
              runId,
              reply,
              executionPolicy: executionPolicy ?? undefined
            });
            queueProjectContextRefresh();
          },
          onError: ({ sessionId: nextSessionId, runId, error }) => {
            if (activeChatStream.cancelled) {
              return;
            }
            executionSessionId = nextSessionId;
            activeChatStream.sessionId = nextSessionId;
            activeChatStream.runId = runId;
            settled = true;
            finishActiveChatStream();
            const errorCategory = typeof (error as Error & { errorCategory?: unknown }).errorCategory === "string"
              ? String((error as Error & { errorCategory?: unknown }).errorCategory).trim()
              : "";
            void updateAssistantTranscript((current) => ({
              ...current,
              content: current.content.trim() ? current.content : error.message,
              streamState: "error",
              statusLabel: undefined,
              statusDetail: undefined
            }));
            runtimeCloudSupervisor.noteError("chat_stream_failed", error.message, {
              modelId: executionPolicy?.modelId,
              sessionId: nextSessionId
            });
            queueOrSend({
              type: "error",
              requestId,
              sessionId: nextSessionId,
              runId,
              message: error.message,
              errorCategory: errorCategory || undefined
            });
            queueProjectContextRefresh();
          }
        })
      });
      ready = true;
      activeChatStream.sessionId = stream.sessionId;
      activeChatStream.runId = stream.runId;
      flushQueuedEvents({
        type: "started",
        requestId,
        sessionId: stream.sessionId,
        runId: stream.runId,
        executionPolicy: executionPolicy ?? undefined
      });
      return {
        requestId,
        sessionId: stream.sessionId,
        runId: stream.runId,
        userMessageId,
        assistantMessageId,
        executionPolicy: executionPolicy ?? undefined
      };
    } catch (error) {
      if (activeChatStream.cancelled) {
        return {
          requestId,
          sessionId: activeChatStream.sessionId ?? executionSessionId,
          runId: activeChatStream.runId,
          userMessageId,
          assistantMessageId,
          executionPolicy: executionPolicy ?? undefined
        };
      }
      finishActiveChatStream();
      const message = error instanceof Error ? error.message : String(error);
      if (!settled) {
        runtimeCloudSupervisor.noteError("chat_stream_failed", message, {
          modelId: executionPolicy?.modelId,
          sessionId: executionSessionId
        });
      }
      queueProjectContextRefresh();
      throw error;
    }
  };
  const cancelStream = async (requestId: string, runId?: string, sessionId?: string, sender?: WebContents): Promise<ChatCancelStreamResult> => {
    const activeStream = activeChatStreams.get(requestId)
      ?? [...activeChatStreams.values()].find((stream) => Boolean(runId) && stream.runId === runId);

    if (!activeStream) {
      return {
        requestId,
        sessionId,
        runId,
        localCancelled: false,
        remoteCancelled: false,
        message: "No active stream found."
      };
    }

    activeStream.cancelled = true;
    activeChatStreams.delete(activeStream.requestId);
    await activeStream.markStopped().catch(() => undefined);

    const effectiveRunId = runId ?? activeStream.runId;
    const effectiveSessionId = sessionId ?? activeStream.sessionId;
    let remoteCancelled = false;
    if (effectiveRunId) {
      const cancelResult = await gatewayClient.cancelChatRun(effectiveRunId).catch(() => null);
      remoteCancelled = Boolean(cancelResult?.remoteCancelled);
    }

    const result: ChatCancelStreamResult = {
      requestId: activeStream.requestId,
      sessionId: effectiveSessionId,
      runId: effectiveRunId,
      localCancelled: true,
      remoteCancelled,
      message: "已停止"
    };

    broadcastChatStreamEvent({
      type: "cancelled",
      requestId: activeStream.requestId,
      sessionId: effectiveSessionId,
      runId: effectiveRunId,
      message: result.message,
      remoteCancelled
    }, sender);

    return result;
  };
  ipcMain.handle(IPC_CHANNELS.workspaceGetSummary, async () => buildWorkspaceSummary());
  ipcMain.handle(IPC_CHANNELS.workspaceWarmup, async () => queueWorkspaceWarmup("workspace-warmup", { action: "init" }));
  ipcMain.handle(IPC_CHANNELS.windowMinimize, async (event) => {
    BrowserWindow.fromWebContents(event.sender)?.minimize();
  });
  ipcMain.handle(IPC_CHANNELS.windowMaximize, async (event) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    if (!window) {
      return;
    }
    if (window.isMaximized()) {
      window.unmaximize();
      return;
    }
    window.maximize();
  });
  ipcMain.handle(IPC_CHANNELS.windowClose, async (event) => {
    BrowserWindow.fromWebContents(event.sender)?.close();
  });
  ipcMain.handle(IPC_CHANNELS.gatewayStatus, async () => gatewayClient.status());
  ipcMain.handle(IPC_CHANNELS.gatewayConnect, async () => gatewayClient.connect());
  ipcMain.handle(IPC_CHANNELS.gatewayDisconnect, async () => gatewayClient.disconnect());
  ipcMain.handle(IPC_CHANNELS.gatewayReconnect, async () => gatewayClient.reconnect());
  ipcMain.handle(IPC_CHANNELS.gatewayHealth, async () => gatewayClient.health());
  ipcMain.handle(IPC_CHANNELS.gatewayTailLogs, async (_event, limit?: number) => gatewayClient.tailLogs(limit));

  ipcMain.handle(IPC_CHANNELS.runtimeGetStatus, async () => runtimeManager.status());
  ipcMain.handle(IPC_CHANNELS.runtimeTailLogs, async (_event, limit?: number) => runtimeManager.tailLogs(limit));
  ipcMain.handle(IPC_CHANNELS.runtimeStart, async () => startManagedRuntime("runtime-start", { action: "init" }));
  ipcMain.handle(IPC_CHANNELS.runtimeStop, async () => {
    await runtimeCloudSupervisor.stop("runtime-stop");
    const status = await runtimeManager.stop();
    await reconfigureGatewayClient();
    return status;
  });
  ipcMain.handle(IPC_CHANNELS.runtimeRestart, async () => startManagedRuntime("runtime-restart", { action: "init", restart: true }));
  ipcMain.handle(IPC_CHANNELS.runtimeHealth, async () => runtimeManager.health());
  ipcMain.handle(IPC_CHANNELS.runtimeCloudGetStatus, async () => runtimeCloudClient.getStatus());
  ipcMain.handle(IPC_CHANNELS.runtimeCloudFetchConfig, async (_event, action) => runtimeCloudClient.fetchConfig(action));
  ipcMain.handle(IPC_CHANNELS.runtimeTelemetryGetStatus, async () => runtimeCloudSupervisor.getStatus());

  ipcMain.handle(IPC_CHANNELS.configLoad, async () => getEffectiveConfig());
  ipcMain.handle(IPC_CHANNELS.configPickWorkspaceDirectory, async (event, currentPath?: string) => {
    return pickWorkspaceDirectory(BrowserWindow.fromWebContents(event.sender), currentPath);
  });
  ipcMain.handle(IPC_CHANNELS.configSave, async (_event, input: SaveConfigInput) => saveAppConfig(input));
  ipcMain.handle(IPC_CHANNELS.configRevealSecret, async (_event, secretId: ConfigSecretId) => revealConfigSecret(secretId));

  ipcMain.handle(IPC_CHANNELS.authGetSession, async () => authClient.getSessionSummary());
  ipcMain.handle(IPC_CHANNELS.authSignIn, async (_event, input: SignInInput) => {
    const session = await authClient.signIn(input.accessToken);
    const config = await getEffectiveConfig();
    await configService.save({
      setupMode: config.setupMode,
      provider: config.provider,
      baseUrl: config.baseUrl,
      apiKey: undefined,
      gatewayToken: undefined,
      authToken: input.accessToken,
      defaultModel: config.defaultModel,
      workspacePath: config.workspacePath,
      gatewayUrl: config.gatewayUrl,
      cloudApiBaseUrl: config.cloudApiBaseUrl,
      runtimeCloudApiBaseUrl: config.runtimeCloudApiBaseUrl,
      runtimeMode: config.runtimeMode
    });
    return session;
  });
  ipcMain.handle(IPC_CHANNELS.authSignOut, async () => {
    const session = await authClient.signOut();
    const config = await getEffectiveConfig();
    await configService.save({
      setupMode: config.setupMode,
      provider: config.provider,
      baseUrl: config.baseUrl,
      apiKey: undefined,
      gatewayToken: undefined,
      authToken: "",
      defaultModel: config.defaultModel,
      workspacePath: config.workspacePath,
      gatewayUrl: config.gatewayUrl,
      cloudApiBaseUrl: config.cloudApiBaseUrl,
      runtimeCloudApiBaseUrl: config.runtimeCloudApiBaseUrl,
      runtimeMode: config.runtimeMode
    });
    return session;
  });
  ipcMain.handle(IPC_CHANNELS.profileGetSummary, async () => profileClient.getSummary());
  ipcMain.handle(IPC_CHANNELS.creditsGetSummary, async () => creditClient.getSummary());
  ipcMain.handle(IPC_CHANNELS.skillsList, async () => skillClient.list());
  ipcMain.handle(IPC_CHANNELS.expertsList, async () => expertCatalogService.list());
  ipcMain.handle(IPC_CHANNELS.modelConfigGetSummary, async () => modelConfigClient.getSummary());
  ipcMain.handle(IPC_CHANNELS.systemGetSummary, async () => systemSummary);
  ipcMain.handle(IPC_CHANNELS.tasksListByDate, async (_event, date: string) => taskPanelService.listByDate(date));
  ipcMain.handle(IPC_CHANNELS.skillCatalogList, async () => skillCatalogService.listForActiveProject());

  ipcMain.handle(IPC_CHANNELS.projectsList, async () => projectStore.listProjects());
  ipcMain.handle(IPC_CHANNELS.projectsSetActive, async (_event, projectId: string) => {
    await projectStore.setActiveProject(projectId);
    return buildWorkspaceSummary();
  });
  ipcMain.handle(IPC_CHANNELS.projectsResolveIntent, async (_event, prompt: string, currentProjectId?: string) => {
    return resolveProjectIntentSuggestion(prompt, currentProjectId);
  });

  ipcMain.handle(IPC_CHANNELS.chatListSessions, async () => {
    const sessions = await listSessionsForActiveProject(projectStore);
    runtimeCloudSupervisor.noteSessions(sessions.map((session) => session.id));
    return sessions;
  });
  ipcMain.handle(IPC_CHANNELS.chatListSessionsByProject, async (_event, projectId: string) => {
    const sessions = await projectStore.listSessions(projectId);
    runtimeCloudSupervisor.noteSessions(sessions.map((session) => session.id));
    return sessions;
  });
  ipcMain.handle(IPC_CHANNELS.chatCreateSession, async (_event, title?: string) => {
    const session = await createSessionForActiveProject(projectStore, title);
    runtimeCloudSupervisor.noteSessions([session.id]);
    return session;
  });
  ipcMain.handle(IPC_CHANNELS.chatCreateSessionForProject, async (_event, projectId: string, title?: string) => {
    const session = await projectStore.createSession(title, projectId);
    runtimeCloudSupervisor.noteSessions([session.id]);
    return session;
  });
  ipcMain.handle(IPC_CHANNELS.chatCloseSession, async (_event, sessionId: string) => {
    const sessions = await projectStore.closeSession(sessionId);
    runtimeCloudSupervisor.noteSessions(sessions.map((session) => session.id));
    return sessions;
  });
  ipcMain.handle(IPC_CHANNELS.chatListMessages, async (_event, sessionId: string) => listChatMessages(sessionId));
  ipcMain.handle(IPC_CHANNELS.chatPickAttachments, async (event) => pickAttachments(BrowserWindow.fromWebContents(event.sender)));
  ipcMain.handle(IPC_CHANNELS.chatPickImageAttachment, async (event) => pickImageAttachment(BrowserWindow.fromWebContents(event.sender)));
  ipcMain.handle(IPC_CHANNELS.chatReadImageAttachmentDataUrl, async (_event, attachment: ChatAttachment) => readImageAttachmentDataUrl(attachment));
  ipcMain.handle(IPC_CHANNELS.chatSendPrompt, async (_event, sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => {
    return sendPrompt(sessionId, prompt, skillId, attachments);
  });
  ipcMain.handle(IPC_CHANNELS.chatStreamPrompt, async (event, sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => {
    return streamPrompt(sessionId, prompt, skillId, attachments, event.sender);
  });
  ipcMain.handle(IPC_CHANNELS.chatCancelStream, async (event, requestId: string, runId?: string, sessionId?: string) => {
    return cancelStream(requestId, runId, sessionId, event.sender);
  });
  ipcMain.handle(IPC_CHANNELS.diagnosticsOpenControlUi, async () => {
    const config = await getEffectiveConfig();
    await shell.openExternal(toControlUiUrl(config.gatewayUrl));
  });
  ipcMain.handle(IPC_CHANNELS.diagnosticsExportSnapshot, async () => exportDiagnostics());

  return {
    workspace: {
      getSummary: () => buildWorkspaceSummary(),
      warmup: () => queueWorkspaceWarmup("workspace-warmup", { action: "init" })
    },
    window: {
      minimize: async () => undefined,
      maximize: async () => undefined,
      close: async () => undefined
    },
    gateway: {
      status: () => gatewayClient.status(),
      connect: () => gatewayClient.connect(),
      disconnect: () => gatewayClient.disconnect(),
      reconnect: () => gatewayClient.reconnect(),
      health: () => gatewayClient.health(),
      tailLogs: (limit?: number) => gatewayClient.tailLogs(limit)
    },
    runtime: {
      getStatus: () => runtimeManager.status(),
      tailLogs: (limit?: number) => runtimeManager.tailLogs(limit),
      start: async () => startManagedRuntime("runtime-start", { action: "init" }),
      stop: async () => {
        await runtimeCloudSupervisor.stop("runtime-stop");
        const status = await runtimeManager.stop();
        await reconfigureGatewayClient();
        return status;
      },
      restart: async () => startManagedRuntime("runtime-restart", { action: "init", restart: true }),
      health: () => runtimeManager.health()
    },
    runtimeCloud: {
      getStatus: () => runtimeCloudClient.getStatus(),
      fetchConfig: (action) => runtimeCloudClient.fetchConfig(action)
    },
    runtimeTelemetry: {
      getStatus: () => Promise.resolve(runtimeCloudSupervisor.getStatus())
    },
    config: {
      load: () => getEffectiveConfig(),
      pickWorkspaceDirectory: (currentPath?: string) => pickWorkspaceDirectory(null, currentPath),
      save: (input: SaveConfigInput) => saveAppConfig(input),
      revealSecret: (secretId: ConfigSecretId) => revealConfigSecret(secretId)
    },
    projects: {
      list: () => projectStore.listProjects(),
      setActive: async (projectId: string) => {
        await projectStore.setActiveProject(projectId);
        return buildWorkspaceSummary();
      },
      resolveIntent: (prompt: string, currentProjectId?: string) => resolveProjectIntentSuggestion(prompt, currentProjectId)
    },
    skillCatalog: {
      list: () => skillCatalogService.listForActiveProject()
    },
    auth: {
      getSessionSummary: () => authClient.getSessionSummary(),
      signIn: (input: SignInInput) => authClient.signIn(input.accessToken),
      signOut: () => authClient.signOut()
    },
    profile: {
      getSummary: () => profileClient.getSummary()
    },
    credits: {
      getSummary: () => creditClient.getSummary()
    },
    skills: {
      list: () => skillClient.list()
    },
    experts: {
      list: () => Promise.resolve(expertCatalogService.list())
    },
    modelConfig: {
      getSummary: () => modelConfigClient.getSummary()
    },
    system: {
      getSummary: () => Promise.resolve(systemSummary)
    },
    tasks: {
      listByDate: (date: string) => taskPanelService.listByDate(date)
    },
    chat: {
      listSessions: async () => {
        const sessions = await listSessionsForActiveProject(projectStore);
        runtimeCloudSupervisor.noteSessions(sessions.map((session) => session.id));
        return sessions;
      },
      listSessionsByProject: async (projectId: string) => {
        const sessions = await projectStore.listSessions(projectId);
        runtimeCloudSupervisor.noteSessions(sessions.map((session) => session.id));
        return sessions;
      },
      createSession: async (title?: string) => {
        const session = await createSessionForActiveProject(projectStore, title);
        runtimeCloudSupervisor.noteSessions([session.id]);
        return session;
      },
      createSessionForProject: async (projectId: string, title?: string) => {
        const session = await projectStore.createSession(title, projectId);
        runtimeCloudSupervisor.noteSessions([session.id]);
        return session;
      },
      closeSession: async (sessionId: string) => {
        const sessions = await projectStore.closeSession(sessionId);
        runtimeCloudSupervisor.noteSessions(sessions.map((session) => session.id));
        return sessions;
      },
      listMessages: (sessionId: string) => listChatMessages(sessionId),
      pickAttachments: async () => pickAttachments(BrowserWindow.getFocusedWindow() ?? null),
      pickImageAttachment: async () => pickImageAttachment(BrowserWindow.getFocusedWindow() ?? null),
      readImageAttachmentDataUrl: async (attachment: ChatAttachment) => readImageAttachmentDataUrl(attachment),
      sendPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => sendPrompt(sessionId, prompt, skillId, attachments),
      streamPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => streamPrompt(sessionId, prompt, skillId, attachments),
      cancelStream: (requestId: string, runId?: string, sessionId?: string) => cancelStream(requestId, runId, sessionId),
      onStreamEvent: (listener) => {
        streamListeners.add(listener);
        return () => {
          streamListeners.delete(listener);
        };
      }
    },
    diagnostics: {
      openControlUi: async () => {
        const config = await getEffectiveConfig();
        await shell.openExternal(toControlUiUrl(config.gatewayUrl));
      },
      exportSnapshot: () => exportDiagnostics()
    },
    __internal: {
      queueWorkspaceWarmup
    }
  };
}
