Commit a4d243ae authored by AI-甘富林's avatar AI-甘富林

feat(desktop): unify startup setup flow and runtime cloud prewarm

parent f9e6de26
import path from "node:path"; import path from "node:path";
import { appendFile, readFile, writeFile } from "node:fs/promises"; import { appendFile, readFile, writeFile } from "node:fs/promises";
import { BrowserWindow, app } from "electron"; import { BrowserWindow, app } from "electron";
import { GatewayClient } from "@qjclaw/gateway-client"; import { GatewayClient } from "@qjclaw/gateway-client";
import { RuntimeManager } from "@qjclaw/runtime-manager"; import { RuntimeManager } from "@qjclaw/runtime-manager";
import type { RuntimeModePreference, SystemSummary } from "@qjclaw/shared-types"; import type { AppConfig, RuntimeModePreference, SystemSummary } from "@qjclaw/shared-types";
import { createMainWindow } from "./create-window.js"; import { createMainWindow } from "./create-window.js";
import { registerDesktopIpc } from "./ipc.js"; import { registerDesktopIpc } from "./ipc.js";
import { AppConfigService } from "./services/app-config.js"; import { AppConfigService } from "./services/app-config.js";
...@@ -138,6 +138,66 @@ function resolveRequestedRuntimeMode(configMode: RuntimeModePreference): Runtime ...@@ -138,6 +138,66 @@ function resolveRequestedRuntimeMode(configMode: RuntimeModePreference): Runtime
return override === "bundled-runtime" || override === "external-gateway" ? override : configMode; return override === "bundled-runtime" || override === "external-gateway" ? override : configMode;
} }
function buildDirectProviderManagedConfig(defaultConfig: Record<string, unknown>, config: AppConfig, apiKey: string): Record<string, unknown> {
const nextConfig = structuredClone(defaultConfig);
const providerKey = "direct-provider";
const modelId = config.defaultModel || "gpt-5.4-mini";
const modelLabel = modelId;
const apiMode = config.provider === "anthropic" ? "anthropic-messages" : "openai-completions";
const modelsSection = (nextConfig.models && typeof nextConfig.models === "object" ? nextConfig.models : {}) as Record<string, unknown>;
const providers = (modelsSection.providers && typeof modelsSection.providers === "object" ? modelsSection.providers : {}) as Record<string, unknown>;
const existingProvider = (providers[providerKey] && typeof providers[providerKey] === "object" ? providers[providerKey] : {}) as Record<string, unknown>;
const authSection = (nextConfig.auth && typeof nextConfig.auth === "object" ? nextConfig.auth : {}) as Record<string, unknown>;
const authProfiles = (authSection.profiles && typeof authSection.profiles === "object" ? authSection.profiles : {}) as Record<string, unknown>;
const agentsSection = (nextConfig.agents && typeof nextConfig.agents === "object" ? nextConfig.agents : {}) as Record<string, unknown>;
const agentDefaults = (agentsSection.defaults && typeof agentsSection.defaults === "object" ? agentsSection.defaults : {}) as Record<string, unknown>;
const modelDefaults = (agentDefaults.model && typeof agentDefaults.model === "object" ? agentDefaults.model : {}) as Record<string, unknown>;
const modelAliases = (agentDefaults.models && typeof agentDefaults.models === "object" ? agentDefaults.models : {}) as Record<string, unknown>;
authProfiles[providerKey + ":default"] = {
provider: providerKey,
mode: "api_key"
};
authSection.profiles = authProfiles;
nextConfig.auth = authSection;
providers[providerKey] = {
...existingProvider,
baseUrl: config.baseUrl,
apiKey,
api: apiMode,
models: [
{
id: modelId,
name: modelLabel,
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0
},
maxTokens: 2048
}
]
};
modelsSection.mode = "merge";
modelsSection.providers = providers;
nextConfig.models = modelsSection;
modelDefaults.primary = providerKey + "/" + modelId;
modelDefaults.fallbacks = [];
modelAliases[providerKey + "/" + modelId] = {
alias: modelLabel
};
agentDefaults.model = modelDefaults;
agentDefaults.models = modelAliases;
agentsSection.defaults = agentDefaults;
nextConfig.agents = agentsSection;
return nextConfig;
}
function resolveVendorRuntimeDir(systemSummary: SystemSummary): string { function resolveVendorRuntimeDir(systemSummary: SystemSummary): string {
if (systemSummary.isPackaged) { if (systemSummary.isPackaged) {
return path.join(systemSummary.resourcesPath, "vendor", "openclaw-runtime"); return path.join(systemSummary.resourcesPath, "vendor", "openclaw-runtime");
...@@ -391,6 +451,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -391,6 +451,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const sessionId = state?.streamSmoke?.sessionId || state?.activeSessionId || "desktop-main"; const sessionId = state?.streamSmoke?.sessionId || state?.activeSessionId || "desktop-main";
const runtimeTelemetryAfterWait = await api.runtimeTelemetry.getStatus(); const runtimeTelemetryAfterWait = await api.runtimeTelemetry.getStatus();
const messages = await api.chat.listMessages(sessionId); const messages = await api.chat.listMessages(sessionId);
const chatMessages = messages.filter((message) => message.role === "assistant" || message.role === "user");
const lastAssistantMessage = [...chatMessages].reverse().find((message) => message.role === "assistant") ?? null;
const logs = await api.gateway.tailLogs(20); const logs = await api.gateway.tailLogs(20);
const diagnostics = await api.diagnostics.exportSnapshot(); const diagnostics = await api.diagnostics.exportSnapshot();
const health = await api.gateway.health(); const health = await api.gateway.health();
...@@ -398,8 +460,10 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -398,8 +460,10 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
return { return {
runtimeTelemetryAfterWait, runtimeTelemetryAfterWait,
sessionId, sessionId,
messageCount: messages.length, messageCount: chatMessages.length,
lastMessage: messages.at(-1) ?? null, rawMessageCount: messages.length,
lastMessage: chatMessages.at(-1) ?? null,
lastAssistantMessage,
logCount: logs.length, logCount: logs.length,
diagnostics, diagnostics,
health, health,
...@@ -460,6 +524,7 @@ async function bootstrap(): Promise<void> { ...@@ -460,6 +524,7 @@ async function bootstrap(): Promise<void> {
} }
if (smokeCloudBaseUrl || smokeAuthToken || smokeRuntimeApiKey) { if (smokeCloudBaseUrl || smokeAuthToken || smokeRuntimeApiKey) {
await configService.save({ await configService.save({
setupMode: config.setupMode,
provider: config.provider, provider: config.provider,
baseUrl: config.baseUrl, baseUrl: config.baseUrl,
defaultModel: config.defaultModel, defaultModel: config.defaultModel,
...@@ -484,17 +549,32 @@ async function bootstrap(): Promise<void> { ...@@ -484,17 +549,32 @@ async function bootstrap(): Promise<void> {
await deviceIdentityService.load(); await deviceIdentityService.load();
const localOpenClawConfig = await loadLocalOpenClawGatewayConfig(); const localOpenClawConfig = await loadLocalOpenClawGatewayConfig();
const runtimeCloudClient = new OpenClawConfigClient(configService, secretManager); const runtimeCloudClient = new OpenClawConfigClient(configService, secretManager);
await runtimeCloudClient.hydrateCache();
const skillStore = new SkillStoreService(systemSummary.userDataPath); const skillStore = new SkillStoreService(systemSummary.userDataPath);
runtimeCloudClient.onPayloadUpdated(async ({ config: payloadConfig, skills }) => { runtimeCloudClient.onPayloadUpdated(async ({ config: payloadConfig, skills }) => {
await skillStore.reconcile(skills, payloadConfig.configVersion); await skillStore.reconcile(skills, payloadConfig.configVersion);
}); });
const cachedRuntimeCloudStatus = await runtimeCloudClient.getStatus();
if (cachedRuntimeCloudStatus.config) {
await skillStore.reconcile(runtimeCloudClient.getRemoteSkillAssets(), cachedRuntimeCloudStatus.config.configVersion).catch(() => undefined);
}
const runtimeManager = new RuntimeManager({ const runtimeManager = new RuntimeManager({
vendorRuntimeDir: resolveVendorRuntimeDir(systemSummary), vendorRuntimeDir: resolveVendorRuntimeDir(systemSummary),
runtimeDataDir: path.join(systemSummary.userDataPath, "runtime"), runtimeDataDir: path.join(systemSummary.userDataPath, "runtime"),
logFilePath: path.join(systemSummary.logsPath, "runtime-manager.log"), logFilePath: path.join(systemSummary.logsPath, "runtime-manager.log"),
requestedMode: resolveRequestedRuntimeMode(config.runtimeMode), requestedMode: resolveRequestedRuntimeMode(config.runtimeMode),
managedConfigResolver: async ({ action, defaultConfig }) => runtimeCloudClient.buildManagedConfig(defaultConfig, action), managedConfigResolver: async ({ action, defaultConfig }) => {
const latestConfig = await configService.load();
const apiKey = await secretManager.getApiKey();
if (latestConfig.setupMode === "direct-provider") {
if (!apiKey) {
throw new Error("Direct provider API Key is not configured.");
}
return buildDirectProviderManagedConfig(defaultConfig, latestConfig, apiKey);
}
return runtimeCloudClient.buildManagedConfig(defaultConfig, action);
},
strictBundledRuntime: systemSummary.isPackaged strictBundledRuntime: systemSummary.isPackaged
}); });
await runtimeManager.configure(); await runtimeManager.configure();
...@@ -538,9 +618,28 @@ async function bootstrap(): Promise<void> { ...@@ -538,9 +618,28 @@ async function bootstrap(): Promise<void> {
dailyReportService.handleActivity(event); dailyReportService.handleActivity(event);
}); });
const scheduleRuntimeCloudRefresh = (reason: string) => {
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");
}
} catch (error) {
console.warn(`${reason} runtime cloud refresh skipped:`, error instanceof Error ? error.message : String(error));
}
})();
};
if (resolveRequestedRuntimeMode(config.runtimeMode) !== "external-gateway" && (await secretManager.getApiKey())) { if (resolveRequestedRuntimeMode(config.runtimeMode) !== "external-gateway" && (await secretManager.getApiKey())) {
try { try {
const shouldUseRuntimeCloud = config.setupMode === "employee-key";
const usingCachedRuntimeCloudConfig = shouldUseRuntimeCloud && runtimeCloudClient.hasCachedPayload();
if (shouldUseRuntimeCloud && !usingCachedRuntimeCloudConfig) {
await runtimeCloudClient.fetchConfig("init"); await runtimeCloudClient.fetchConfig("init");
}
await runtimeManager.start(); await runtimeManager.start();
const runtimeGatewayConnection = await runtimeManager.getGatewayConnection(); const runtimeGatewayConnection = await runtimeManager.getGatewayConnection();
if (runtimeGatewayConnection.url) { if (runtimeGatewayConnection.url) {
...@@ -550,10 +649,20 @@ async function bootstrap(): Promise<void> { ...@@ -550,10 +649,20 @@ async function bootstrap(): Promise<void> {
(await secretManager.getDeviceToken()) ?? undefined (await secretManager.getDeviceToken()) ?? undefined
); );
} }
await gatewayClient.connect().catch(() => undefined);
if (config.setupMode === "employee-key") {
await runtimeCloudSupervisor.start(); await runtimeCloudSupervisor.start();
if (usingCachedRuntimeCloudConfig) {
scheduleRuntimeCloudRefresh("bootstrap");
}
} else {
await runtimeCloudSupervisor.stop("bootstrap");
}
} catch (error) { } catch (error) {
console.error("Bundled runtime bootstrap skipped:", error instanceof Error ? error.message : String(error)); console.error("Bundled runtime bootstrap skipped:", error instanceof Error ? error.message : String(error));
} }
} else if (resolveRequestedRuntimeMode(config.runtimeMode) === "external-gateway") {
void gatewayClient.connect().catch(() => undefined);
} }
registerDesktopIpc({ registerDesktopIpc({
...@@ -628,3 +737,6 @@ void bootstrap().catch(async (error) => { ...@@ -628,3 +737,6 @@ void bootstrap().catch(async (error) => {
}); });
This diff is collapsed.
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { AppConfig, RuntimeModePreference, SaveConfigInput } from "@qjclaw/shared-types"; import type { AppConfig, RuntimeModePreference, SaveConfigInput, SetupMode } from "@qjclaw/shared-types";
const CONFIG_DIR = "config"; const CONFIG_DIR = "config";
const CONFIG_FILE = "app-config.json"; const CONFIG_FILE = "app-config.json";
...@@ -23,6 +23,7 @@ const UI_ROUTE_NAMES = new Set([ ...@@ -23,6 +23,7 @@ const UI_ROUTE_NAMES = new Set([
]); ]);
interface LegacyConfig { interface LegacyConfig {
setupMode?: SetupMode;
provider?: string; provider?: string;
baseUrl?: string; baseUrl?: string;
apiKeyConfigured?: boolean; apiKeyConfigured?: boolean;
...@@ -79,6 +80,10 @@ function normalizeRuntimeMode(raw?: string): RuntimeModePreference { ...@@ -79,6 +80,10 @@ function normalizeRuntimeMode(raw?: string): RuntimeModePreference {
return raw === "bundled-runtime" || raw === "external-gateway" ? raw : "bundled-runtime"; return raw === "bundled-runtime" || raw === "external-gateway" ? raw : "bundled-runtime";
} }
function normalizeSetupMode(raw?: string): SetupMode {
return raw === "direct-provider" ? raw : "employee-key";
}
function resolveRuntimeCloudApiBaseUrl(raw?: string): string { function resolveRuntimeCloudApiBaseUrl(raw?: string): string {
const normalized = normalizeCloudApiBaseUrl(raw ?? ""); const normalized = normalizeCloudApiBaseUrl(raw ?? "");
if (normalized) { if (normalized) {
...@@ -91,31 +96,21 @@ function resolveRuntimeCloudApiBaseUrl(raw?: string): string { ...@@ -91,31 +96,21 @@ function resolveRuntimeCloudApiBaseUrl(raw?: string): string {
export class AppConfigService { export class AppConfigService {
private readonly userDataPath: string; private readonly userDataPath: string;
private ioChain: Promise<void> = Promise.resolve();
constructor(userDataPath: string) { constructor(userDataPath: string) {
this.userDataPath = userDataPath; this.userDataPath = userDataPath;
} }
async load(): Promise<AppConfig> { async load(): Promise<AppConfig> {
const filePath = this.getConfigPath(); return this.runExclusive(() => this.loadUnlocked());
await mkdir(path.dirname(filePath), { recursive: true });
try {
const raw = await readFile(filePath, "utf8");
const parsed = JSON.parse(raw) as LegacyConfig;
const config = this.normalizeConfig(parsed);
await writeFile(filePath, JSON.stringify(config, null, 2), "utf8");
return config;
} catch {
const defaults = this.createDefaultConfig();
await writeFile(filePath, JSON.stringify(defaults, null, 2), "utf8");
return defaults;
}
} }
async save(input: SaveConfigInput): Promise<AppConfig> { async save(input: SaveConfigInput): Promise<AppConfig> {
const current = await this.load(); return this.runExclusive(async () => {
const current = await this.loadUnlocked();
const config: AppConfig = { const config: AppConfig = {
setupMode: normalizeSetupMode(input.setupMode),
provider: input.provider, provider: input.provider,
baseUrl: input.baseUrl, baseUrl: input.baseUrl,
apiKeyConfigured: Boolean(input.apiKey) || current.apiKeyConfigured, apiKeyConfigured: Boolean(input.apiKey) || current.apiKeyConfigured,
...@@ -129,10 +124,13 @@ export class AppConfigService { ...@@ -129,10 +124,13 @@ export class AppConfigService {
runtimeMode: normalizeRuntimeMode(input.runtimeMode) runtimeMode: normalizeRuntimeMode(input.runtimeMode)
}; };
const filePath = this.getConfigPath(); await this.writeConfig(config);
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, JSON.stringify(config, null, 2), "utf8");
return config; return config;
});
}
getDataPath(...segments: string[]): string {
return path.join(this.userDataPath, ...segments);
} }
private getConfigPath(): string { private getConfigPath(): string {
...@@ -141,6 +139,7 @@ export class AppConfigService { ...@@ -141,6 +139,7 @@ export class AppConfigService {
private createDefaultConfig(): AppConfig { private createDefaultConfig(): AppConfig {
return { return {
setupMode: "employee-key",
provider: "openai", provider: "openai",
baseUrl: "https://api.openai.com/v1", baseUrl: "https://api.openai.com/v1",
apiKeyConfigured: false, apiKeyConfigured: false,
...@@ -157,6 +156,7 @@ export class AppConfigService { ...@@ -157,6 +156,7 @@ export class AppConfigService {
private normalizeConfig(config: LegacyConfig): AppConfig { private normalizeConfig(config: LegacyConfig): AppConfig {
return { return {
setupMode: normalizeSetupMode(config.setupMode),
provider: config.provider ?? "openai", provider: config.provider ?? "openai",
baseUrl: config.baseUrl ?? "https://api.openai.com/v1", baseUrl: config.baseUrl ?? "https://api.openai.com/v1",
apiKeyConfigured: Boolean(config.apiKeyConfigured), apiKeyConfigured: Boolean(config.apiKeyConfigured),
...@@ -170,4 +170,53 @@ export class AppConfigService { ...@@ -170,4 +170,53 @@ export class AppConfigService {
runtimeMode: normalizeRuntimeMode(config.runtimeMode ?? process.env.QJCLAW_RUNTIME_MODE) runtimeMode: normalizeRuntimeMode(config.runtimeMode ?? process.env.QJCLAW_RUNTIME_MODE)
}; };
} }
private async runExclusive<T>(operation: () => Promise<T>): Promise<T> {
const next = this.ioChain.then(operation, operation);
this.ioChain = next.then(() => undefined, () => undefined);
return next;
}
private async loadUnlocked(): Promise<AppConfig> {
const filePath = this.getConfigPath();
await mkdir(path.dirname(filePath), { recursive: true });
try {
const raw = await readFile(filePath, "utf8");
const parsed = JSON.parse(raw) as LegacyConfig;
const config = this.normalizeConfig(parsed);
await this.writeConfig(config);
return config;
} catch (error) {
if (!this.shouldResetToDefaults(error)) {
throw error;
}
const defaults = this.createDefaultConfig();
await this.writeConfig(defaults);
return defaults;
}
}
private shouldResetToDefaults(error: unknown): boolean {
if (!error || typeof error !== "object") {
return false;
}
const candidate = error as NodeJS.ErrnoException;
if (candidate.code === "ENOENT") {
return true;
}
return error instanceof SyntaxError;
}
private async writeConfig(config: AppConfig): Promise<void> {
const filePath = this.getConfigPath();
const tempPath = `${filePath}.tmp`;
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(tempPath, JSON.stringify(config, null, 2), "utf8");
await rm(filePath, { force: true });
await rename(tempPath, filePath);
}
} }
This diff is collapsed.
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
import { import {
IPC_CHANNELS, IPC_CHANNELS,
type ChatStreamListener, type ChatStreamListener,
...@@ -10,7 +10,8 @@ import { ...@@ -10,7 +10,8 @@ import {
const desktopApi: DesktopApi = { const desktopApi: DesktopApi = {
workspace: { workspace: {
getSummary: () => ipcRenderer.invoke(IPC_CHANNELS.workspaceGetSummary) getSummary: () => ipcRenderer.invoke(IPC_CHANNELS.workspaceGetSummary),
warmup: () => ipcRenderer.invoke(IPC_CHANNELS.workspaceWarmup)
}, },
gateway: { gateway: {
status: () => ipcRenderer.invoke(IPC_CHANNELS.gatewayStatus), status: () => ipcRenderer.invoke(IPC_CHANNELS.gatewayStatus),
......
# 启动页预热方案(适配当前代码)
## 背景
当前代码已经把一部分冷启动前移到了主进程,但聊天页仍然会在服务未 ready 时提前打开,并通过发送区禁发来暴露启动过程。用户感知上会变成“聊天窗口已经打开,但发送按钮是灰的,还要等待准备环境”。
这次方案的目标是把初始化完整收拢到启动页,聊天页只在 `chatReady === true` 后进入;同时对云配置增加“缓存优先 + 后台增量同步”,缩短二次启动时间。
## 当前代码问题定位
- 主进程已经有预热链路:`apps/desktop/src/main/index.ts`
- 启动时会拉取员工配置、启动 bundled runtime、连接 gateway。
- 状态聚合已经存在:`apps/desktop/src/main/ipc.ts`
- `WorkspaceSummary` 已包含 `chatReady``chatLaunchState``startupPhase``startupMessage`
- 问题主要出在渲染层:`apps/ui/src/App.tsx`
- 聊天页在未 ready 时提前进入。
- 发送按钮和提示文案绑定到运行时 ready 状态,导致界面像“卡住”。
## 本次适配
### 1. 主进程预热保留,但改成缓存优先
-`OpenClawConfigClient` 中加入运行时云配置缓存。
- 缓存内容包含:
- 上次成功的员工配置 payload
- 配置摘要
- 当前员工密钥指纹
- 启动时优先尝试读取缓存:
- 如果缓存命中且密钥未变,bundled runtime 直接基于缓存配置启动。
- 启动完成后再后台执行一次云端刷新。
- 如果缓存不存在或密钥变更:
- 仍按原始链路阻塞拉取 `fetchConfig("init")`
### 2. 缓存失效策略
- 当用户更换或清空员工密钥时,立即清理旧缓存。
- 缓存只在密钥指纹一致时复用,避免把上一位员工的配置拿来启动。
### 3. 启动页成为聊天入口
- 聊天视图新增启动页门禁:
- 服务未 ready 时,只展示启动页,不渲染聊天消息区和发送区。
- 服务 ready 后,自动进入聊天页。
- 启动页展示:
- 主状态文案
- 进度条
- 四段步骤:读取本地配置、准备本地助手、连接聊天服务、进入对话
- 失败时保留“重新准备”和“打开设置”入口
### 4. 聊天页恢复正常可发状态
- 聊天页不再把发送按钮绑定到启动期状态。
- 进入聊天页后:
- 发送按钮只受“已绑定、输入非空、未发送中、未保存中”控制。
- `ensureChatAvailable()` 保留,但只用于异常恢复:
- 理论上不再承担首开冷启动主路径。
### 5. 现有优化继续保留
- 只展示 `user` / `assistant` 主消息。
- 保留可折叠“思考过程”面板。
- 保留 `completed``delta` 的兜底显示逻辑。
## 涉及文件
- `apps/desktop/src/main/index.ts`
- `apps/desktop/src/main/ipc.ts`
- `apps/desktop/src/main/services/cloud-api.ts`
- `apps/desktop/src/main/services/app-config.ts`
- `apps/ui/src/App.tsx`
- `apps/ui/src/styles.css`
## 验收标准
- 已绑定情况下,打开应用先看到启动页,而不是灰按钮聊天页。
- 二次启动且缓存可用时,启动页等待明显短于首次启动。
- 启动完成后进入聊天页,发送按钮默认可用。
- 更换员工密钥后,不复用旧员工缓存。
- runtime 或 gateway 异常掉线时,仍能通过恢复逻辑重新可用。
## 测试建议
1. 首次启动,无缓存
- 应显示启动页并完成完整预热。
2. 再次启动,有缓存
- 应明显更快进入聊天页。
3. 更换员工密钥
- 旧缓存应失效,不应沿用旧员工配置。
4. 断网启动
- 有缓存时可继续进入;无缓存时停留在启动失败页。
5. 聊天页回归
- 进入聊天页后首条消息不应再承担完整冷启动链路。
export const IPC_CHANNELS = { export const IPC_CHANNELS = {
workspaceGetSummary: "workspace:get-summary", workspaceGetSummary: "workspace:get-summary",
workspaceWarmup: "workspace:warmup",
gatewayStatus: "gateway:status", gatewayStatus: "gateway:status",
gatewayConnect: "gateway:connect", gatewayConnect: "gateway:connect",
gatewayDisconnect: "gateway:disconnect", gatewayDisconnect: "gateway:disconnect",
...@@ -36,7 +37,7 @@ ...@@ -36,7 +37,7 @@
export type GatewayState = "unknown" | "connecting" | "connected" | "disconnected" | "error"; export type GatewayState = "unknown" | "connecting" | "connected" | "disconnected" | "error";
export type LogLevel = "info" | "warn" | "error"; export type LogLevel = "info" | "warn" | "error";
export type MessageRole = "system" | "user" | "assistant"; export type MessageRole = "system" | "user" | "assistant" | "tool" | "toolResult";
export type AuthSessionState = "authenticated" | "anonymous" | "expired" | "error"; export type AuthSessionState = "authenticated" | "anonymous" | "expired" | "error";
export type CreditStatus = "ok" | "low" | "empty"; export type CreditStatus = "ok" | "low" | "empty";
export type RuntimeMode = "external-gateway" | "bundled-runtime"; export type RuntimeMode = "external-gateway" | "bundled-runtime";
...@@ -53,10 +54,18 @@ export type RuntimeCloudState = "unconfigured" | "loading" | "ready" | "error"; ...@@ -53,10 +54,18 @@ export type RuntimeCloudState = "unconfigured" | "loading" | "ready" | "error";
export type RuntimeTelemetryState = "idle" | "running" | "stopped" | "error"; export type RuntimeTelemetryState = "idle" | "running" | "stopped" | "error";
export type RuntimeCloudEventType = "startup" | "shutdown" | "message_sent" | "message_received" | "error" | "config_updated"; export type RuntimeCloudEventType = "startup" | "shutdown" | "message_sent" | "message_received" | "error" | "config_updated";
export type PluginStatus = "included" | "extension" | "unavailable"; export type PluginStatus = "included" | "extension" | "unavailable";
export type SetupMode = "employee-key" | "direct-provider";
export type ChatLaunchState = "unbound" | "starting" | "ready" | "error"; export type ChatLaunchState = "unbound" | "starting" | "ready" | "error";
export type WorkspaceStartupPhase = "idle" | "syncing-config" | "starting-runtime" | "connecting-gateway" | "ready" | "error";
export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed" | "removed"; export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed" | "removed";
export type DailyReportDeliveryState = "draft" | "sent" | "failed"; export type DailyReportDeliveryState = "draft" | "sent" | "failed";
export interface WorkspaceWarmupResult {
accepted: boolean;
state: "scheduled" | "skipped";
message: string;
}
export interface GatewayStatus { export interface GatewayStatus {
state: GatewayState; state: GatewayState;
url: string; url: string;
...@@ -246,9 +255,13 @@ export interface PluginSummary { ...@@ -246,9 +255,13 @@ export interface PluginSummary {
export interface WorkspaceSummary { export interface WorkspaceSummary {
apiKeyConfigured: boolean; apiKeyConfigured: boolean;
bindingRequired: boolean; bindingRequired: boolean;
setupRequired: boolean;
setupMode: SetupMode;
chatReady: boolean; chatReady: boolean;
chatLaunchState: ChatLaunchState; chatLaunchState: ChatLaunchState;
chatStatusMessage?: string; chatStatusMessage?: string;
startupPhase: WorkspaceStartupPhase;
startupMessage?: string;
employeeId?: string; employeeId?: string;
employeeName?: string; employeeName?: string;
welcomeMessage?: string; welcomeMessage?: string;
...@@ -322,6 +335,16 @@ export interface ChatStreamDeltaEvent { ...@@ -322,6 +335,16 @@ export interface ChatStreamDeltaEvent {
fullText?: string; fullText?: string;
} }
export interface ChatStreamStatusEvent {
type: "status";
requestId: string;
sessionId: string;
runId?: string;
stage: string;
label: string;
detail?: string;
}
export interface ChatStreamCompletedEvent { export interface ChatStreamCompletedEvent {
type: "completed"; type: "completed";
requestId: string; requestId: string;
...@@ -339,7 +362,7 @@ export interface ChatStreamErrorEvent { ...@@ -339,7 +362,7 @@ export interface ChatStreamErrorEvent {
message: string; message: string;
} }
export type ChatStreamEvent = ChatStreamStartedEvent | ChatStreamDeltaEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent; export type ChatStreamEvent = ChatStreamStartedEvent | ChatStreamStatusEvent | ChatStreamDeltaEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent;
export type ChatStreamListener = (event: ChatStreamEvent) => void; export type ChatStreamListener = (event: ChatStreamEvent) => void;
...@@ -350,6 +373,7 @@ export interface PromptResult { ...@@ -350,6 +373,7 @@ export interface PromptResult {
} }
export interface AppConfig { export interface AppConfig {
setupMode: SetupMode;
provider: string; provider: string;
baseUrl: string; baseUrl: string;
apiKeyConfigured: boolean; apiKeyConfigured: boolean;
...@@ -369,6 +393,7 @@ export interface DiagnosticsExportResult { ...@@ -369,6 +393,7 @@ export interface DiagnosticsExportResult {
} }
export interface SaveConfigInput { export interface SaveConfigInput {
setupMode: SetupMode;
provider: string; provider: string;
baseUrl: string; baseUrl: string;
apiKey?: string; apiKey?: string;
...@@ -482,6 +507,7 @@ export interface SystemSummary { ...@@ -482,6 +507,7 @@ export interface SystemSummary {
export interface DesktopApi { export interface DesktopApi {
workspace: { workspace: {
getSummary(): Promise<WorkspaceSummary>; getSummary(): Promise<WorkspaceSummary>;
warmup(): Promise<WorkspaceWarmupResult>;
}; };
gateway: { gateway: {
status(): Promise<GatewayStatus>; status(): Promise<GatewayStatus>;
...@@ -545,3 +571,6 @@ export interface DesktopApi { ...@@ -545,3 +571,6 @@ export interface DesktopApi {
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment