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

fix(desktop): clarify first-launch startup states and diagnostics

Prevent first-launch startup from being masked by the generic employee-config syncing state.
Differentiate config sync, project sync, runtime startup, and gateway connection states, and improve diagnostics and smoke coverage.
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 5c3ae433
This diff is collapsed.
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { ipcMain, shell, type WebContents } from "electron"; import { ipcMain, shell, type WebContents } from "electron";
import { import {
IPC_CHANNELS, IPC_CHANNELS,
...@@ -24,6 +24,7 @@ import type { AppConfigService } from "./services/app-config.js"; ...@@ -24,6 +24,7 @@ import type { AppConfigService } from "./services/app-config.js";
import type { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js"; import type { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js";
import type { DiagnosticsService } from "./services/diagnostics.js"; import type { DiagnosticsService } from "./services/diagnostics.js";
import type { DailyReportService } from "./services/daily-report-service.js"; import type { DailyReportService } from "./services/daily-report-service.js";
import type { SkillCatalogService } from "./services/skill-catalog.js";
import type { SkillClient } from "./services/skill-client.js"; import type { SkillClient } from "./services/skill-client.js";
import type { SkillStoreService } from "./services/skill-store.js"; import type { SkillStoreService } from "./services/skill-store.js";
import { import {
...@@ -33,6 +34,7 @@ import { ...@@ -33,6 +34,7 @@ import {
type LocalOpenClawGatewayConfig type LocalOpenClawGatewayConfig
} from "./services/openclaw-local-config.js"; } from "./services/openclaw-local-config.js";
import type { SecretManager } from "./services/secrets.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 { RuntimeCloudSupervisor } from "./services/runtime-cloud-supervisor.js";
import type { RuntimeSkillBridgeService } from "./services/runtime-skill-bridge.js"; import type { RuntimeSkillBridgeService } from "./services/runtime-skill-bridge.js";
import type { ProjectStoreService } from "./services/project-store.js"; import type { ProjectStoreService } from "./services/project-store.js";
...@@ -72,6 +74,7 @@ interface MainServices { ...@@ -72,6 +74,7 @@ interface MainServices {
profileClient: ProfileClient; profileClient: ProfileClient;
creditClient: CreditClient; creditClient: CreditClient;
skillClient: SkillClient; skillClient: SkillClient;
skillCatalogService: SkillCatalogService;
skillStore: SkillStoreService; skillStore: SkillStoreService;
modelConfigClient: ModelConfigClient; modelConfigClient: ModelConfigClient;
runtimeCloudClient: OpenClawConfigClient; runtimeCloudClient: OpenClawConfigClient;
...@@ -85,6 +88,7 @@ interface MainServices { ...@@ -85,6 +88,7 @@ interface MainServices {
projectSkillRouter: ProjectSkillRouterService; projectSkillRouter: ProjectSkillRouterService;
projectExecutionRouter: ProjectExecutionRouter; projectExecutionRouter: ProjectExecutionRouter;
projectWorkspaceExecutor: ProjectWorkspaceExecutorService; projectWorkspaceExecutor: ProjectWorkspaceExecutorService;
startupLogger: StartupLogger;
appVersion: string; appVersion: string;
systemSummary: SystemSummary; systemSummary: SystemSummary;
localOpenClawConfig?: LocalOpenClawGatewayConfig | null; localOpenClawConfig?: LocalOpenClawGatewayConfig | null;
...@@ -189,6 +193,16 @@ function buildPluginSummaries(runtimeStatus: RuntimeStatus): PluginSummary[] { ...@@ -189,6 +193,16 @@ function buildPluginSummaries(runtimeStatus: RuntimeStatus): PluginSummary[] {
}); });
} }
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_LIMIT = 2;
const MANAGED_RUNTIME_START_RETRY_DELAY_MS = 1500; const MANAGED_RUNTIME_START_RETRY_DELAY_MS = 1500;
const GATEWAY_CONNECT_RETRY_LIMIT = 10; const GATEWAY_CONNECT_RETRY_LIMIT = 10;
...@@ -212,6 +226,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -212,6 +226,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
profileClient, profileClient,
secretManager, secretManager,
skillClient, skillClient,
skillCatalogService,
modelConfigClient, modelConfigClient,
runtimeCloudClient, runtimeCloudClient,
runtimeCloudSupervisor, runtimeCloudSupervisor,
...@@ -224,6 +239,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -224,6 +239,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
projectSkillRouter, projectSkillRouter,
projectExecutionRouter, projectExecutionRouter,
projectWorkspaceExecutor, projectWorkspaceExecutor,
startupLogger,
systemSummary, systemSummary,
localOpenClawConfig localOpenClawConfig
} = services; } = services;
...@@ -422,6 +438,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -422,6 +438,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
let workspaceWarmupTail: Promise<void> = Promise.resolve(); let workspaceWarmupTail: Promise<void> = Promise.resolve();
let workspaceWarmupInFlight = false; let workspaceWarmupInFlight = false;
let bootstrapRecoveryAttempts = 0; let bootstrapRecoveryAttempts = 0;
let lastWorkspaceSummaryLogKey = "";
const queueWorkspaceWarmup = async ( const queueWorkspaceWarmup = async (
reason: string, reason: string,
...@@ -532,29 +549,35 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -532,29 +549,35 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
runtimeStatus, runtimeStatus,
gatewayStatus gatewayStatus
}); });
const shouldWaitForProjectSync = projects.length === 0
&& config.apiKeyConfigured
&& config.setupMode === "employee-key"
&& runtimeCloudStatus.state === "ready"
&& 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,
bundleSyncState: bundleSyncStatus.state
});
}
const chatSummary = projects.length > 0 const chatSummary = projects.length > 0
? baseChatSummary ? baseChatSummary
: bundleSyncFailed : bundleSyncFailed
? { ? {
chatReady: false, chatReady: false,
chatLaunchState: "error" as const, chatLaunchState: "error" as const,
chatStatusMessage: bundleSyncStatus.lastError ?? "工作配置同步失败,请检查网络后重试。", chatStatusMessage: bundleSyncStatus.lastError ?? "Workspace project sync failed. Check network access and retry.",
startupPhase: "error" as const, startupPhase: "error" as const,
startupMessage: bundleSyncStatus.lastError ?? "工作配置同步失败,请检查网络后重试。" startupMessage: bundleSyncStatus.lastError ?? "Workspace project sync failed. Check network access and retry."
} }
: { : shouldWaitForProjectSync
chatReady: false, ? buildProjectSyncSummary(bundleSyncStatus.lastError ?? EMPTY_PROJECT_INVENTORY_MESSAGE)
chatLaunchState: config.apiKeyConfigured ? "starting" as const : baseChatSummary.chatLaunchState, : baseChatSummary;
chatStatusMessage: config.apiKeyConfigured
? EMPTY_PROJECT_INVENTORY_MESSAGE
: baseChatSummary.chatStatusMessage,
startupPhase: config.apiKeyConfigured ? "syncing-config" as const : baseChatSummary.startupPhase,
startupMessage: config.apiKeyConfigured
? EMPTY_PROJECT_INVENTORY_MESSAGE
: baseChatSummary.startupMessage
};
return { const workspaceSummary: WorkspaceSummary = {
shellReady, shellReady,
apiKeyConfigured: config.apiKeyConfigured, apiKeyConfigured: config.apiKeyConfigured,
bindingRequired: !config.apiKeyConfigured, bindingRequired: !config.apiKeyConfigured,
...@@ -585,12 +608,36 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -585,12 +608,36 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
skillCount: skills.length, skillCount: skills.length,
skills, skills,
plugins: buildPluginSummaries(runtimeStatus), plugins: buildPluginSummaries(runtimeStatus),
lastError: runtimeCloudStatus.lastError ?? runtimeStatus.lastError ?? gatewayStatus?.lastError 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 exportDiagnostics = async () => {
const config = await getEffectiveConfig(); const config = await getEffectiveConfig();
const workspaceSummary = await buildWorkspaceSummary();
const [gatewayStatus, gatewayHealth, logs, authSession, runtimeStatus, runtimeLogs, runtimeCloudStatus, runtimeTelemetryStatus] = await Promise.all([ const [gatewayStatus, gatewayHealth, logs, authSession, runtimeStatus, runtimeLogs, runtimeCloudStatus, runtimeTelemetryStatus] = await Promise.all([
gatewayClient.status(), gatewayClient.status(),
gatewayClient.health(), gatewayClient.health(),
...@@ -639,7 +686,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -639,7 +686,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
runtimeStatus, runtimeStatus,
runtimeLogs, runtimeLogs,
runtimeCloudStatus, runtimeCloudStatus,
runtimeTelemetryStatus runtimeTelemetryStatus,
workspaceSummary,
bundleSyncStatus: projectBundleService.getSyncStatus(),
startupLogPath: startupLogger.getSessionLogPath()
}); });
}; };
...@@ -1182,6 +1232,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1182,6 +1232,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
ipcMain.handle(IPC_CHANNELS.skillsList, async () => skillClient.list()); ipcMain.handle(IPC_CHANNELS.skillsList, async () => skillClient.list());
ipcMain.handle(IPC_CHANNELS.modelConfigGetSummary, async () => modelConfigClient.getSummary()); ipcMain.handle(IPC_CHANNELS.modelConfigGetSummary, async () => modelConfigClient.getSummary());
ipcMain.handle(IPC_CHANNELS.systemGetSummary, async () => systemSummary); ipcMain.handle(IPC_CHANNELS.systemGetSummary, async () => systemSummary);
ipcMain.handle(IPC_CHANNELS.skillCatalogList, async () => skillCatalogService.listForActiveProject());
ipcMain.handle(IPC_CHANNELS.projectsList, async () => projectStore.listProjects()); ipcMain.handle(IPC_CHANNELS.projectsList, async () => projectStore.listProjects());
ipcMain.handle(IPC_CHANNELS.projectsSetActive, async (_event, projectId: string) => { ipcMain.handle(IPC_CHANNELS.projectsSetActive, async (_event, projectId: string) => {
...@@ -1298,6 +1349,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1298,6 +1349,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
return buildWorkspaceSummary(); return buildWorkspaceSummary();
} }
}, },
skillCatalog: {
list: () => skillCatalogService.listForActiveProject()
},
auth: { auth: {
getSessionSummary: () => authClient.getSessionSummary(), getSessionSummary: () => authClient.getSessionSummary(),
signIn: (input: SignInInput) => authClient.signIn(input.accessToken), signIn: (input: SignInInput) => authClient.signIn(input.accessToken),
...@@ -1366,3 +1420,12 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1366,3 +1420,12 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
} }
}; };
} }
...@@ -23,6 +23,7 @@ import type { ...@@ -23,6 +23,7 @@ import type {
import type { AppConfigService } from "./app-config.js"; import type { AppConfigService } from "./app-config.js";
import type { RemoteSkillAsset } from "./skill-store.js"; import type { RemoteSkillAsset } from "./skill-store.js";
import type { SecretManager } from "./secrets.js"; import type { SecretManager } from "./secrets.js";
import type { StartupLogger } from "./startup-logger.js";
interface SessionPayload { interface SessionPayload {
user?: { displayName?: string; email?: string }; user?: { displayName?: string; email?: string };
...@@ -510,6 +511,7 @@ export class OpenClawConfigClient { ...@@ -510,6 +511,7 @@ export class OpenClawConfigClient {
private readonly httpClient = new HttpJsonClient(); private readonly httpClient = new HttpJsonClient();
private readonly payloadListeners = new Set<RuntimeCloudPayloadListener>(); private readonly payloadListeners = new Set<RuntimeCloudPayloadListener>();
private readonly cachePath: string; private readonly cachePath: string;
private readonly startupLogger?: StartupLogger;
private payloadCache: OpenClawEmployeeConfigPayload | null = null; private payloadCache: OpenClawEmployeeConfigPayload | null = null;
private statusCache: RuntimeCloudStatus = { private statusCache: RuntimeCloudStatus = {
state: "unconfigured", state: "unconfigured",
...@@ -518,10 +520,11 @@ export class OpenClawConfigClient { ...@@ -518,10 +520,11 @@ export class OpenClawConfigClient {
}; };
private cacheLoaded = false; private cacheLoaded = false;
constructor(configService: AppConfigService, secretManager: SecretManager) { constructor(configService: AppConfigService, secretManager: SecretManager, startupLogger?: StartupLogger) {
this.configService = configService; this.configService = configService;
this.secretManager = secretManager; this.secretManager = secretManager;
this.cachePath = this.configService.getDataPath("config", "runtime-cloud-cache.json"); this.cachePath = this.configService.getDataPath("config", "runtime-cloud-cache.json");
this.startupLogger = startupLogger;
} }
async hydrateCache(): Promise<void> { async hydrateCache(): Promise<void> {
...@@ -627,6 +630,7 @@ export class OpenClawConfigClient { ...@@ -627,6 +630,7 @@ export class OpenClawConfigClient {
private async fetchPayload(action: RuntimeCloudFetchAction): Promise<OpenClawEmployeeConfigPayload> { private async fetchPayload(action: RuntimeCloudFetchAction): Promise<OpenClawEmployeeConfigPayload> {
await this.hydrateCache(); await this.hydrateCache();
const config = await this.configService.load(); const config = await this.configService.load();
const startedAt = Date.now();
const baseUrl = config.runtimeCloudApiBaseUrl.trim().replace(/\/$/, ""); const baseUrl = config.runtimeCloudApiBaseUrl.trim().replace(/\/$/, "");
const apiKey = (await this.secretManager.getApiKey())?.trim(); const apiKey = (await this.secretManager.getApiKey())?.trim();
...@@ -711,6 +715,12 @@ export class OpenClawConfigClient { ...@@ -711,6 +715,12 @@ export class OpenClawConfigClient {
}; };
throw new Error(message); throw new Error(message);
} }
await this.startupLogger?.error("runtime-cloud", "fetch.error", "Runtime cloud fetch failed without cache fallback.", {
action,
baseUrl,
elapsedMs: Date.now() - startedAt,
error: message
});
return this.fail(baseUrl, true, message); return this.fail(baseUrl, true, message);
} }
} }
...@@ -997,3 +1007,9 @@ export class ModelConfigClient { ...@@ -997,3 +1007,9 @@ export class ModelConfigClient {
...@@ -15,9 +15,12 @@ import type { ...@@ -15,9 +15,12 @@ import type {
RuntimeTelemetryStatus, RuntimeTelemetryStatus,
SkillSummary, SkillSummary,
SystemSummary, SystemSummary,
UserProfileSummary UserProfileSummary,
WorkspaceSummary
} from "@qjclaw/shared-types"; } from "@qjclaw/shared-types";
import type { LocalOpenClawGatewayConfig } from "./openclaw-local-config.js"; import type { LocalOpenClawGatewayConfig } from "./openclaw-local-config.js";
import type { ProjectBundleSyncStatus } from "./project-bundle.js";
import type { StartupLogger } from "./startup-logger.js";
interface DiagnosticsSnapshotInput { interface DiagnosticsSnapshotInput {
config: AppConfig; config: AppConfig;
...@@ -38,6 +41,10 @@ interface DiagnosticsSnapshotInput { ...@@ -38,6 +41,10 @@ interface DiagnosticsSnapshotInput {
runtimeLogs?: LogEntry[]; runtimeLogs?: LogEntry[];
runtimeCloudStatus?: RuntimeCloudStatus; runtimeCloudStatus?: RuntimeCloudStatus;
runtimeTelemetryStatus?: RuntimeTelemetryStatus; runtimeTelemetryStatus?: RuntimeTelemetryStatus;
workspaceSummary?: WorkspaceSummary;
bundleSyncStatus?: ProjectBundleSyncStatus;
startupLogPath?: string;
reason?: string;
} }
function toSafeStamp(value: string): string { function toSafeStamp(value: string): string {
...@@ -46,9 +53,11 @@ function toSafeStamp(value: string): string { ...@@ -46,9 +53,11 @@ function toSafeStamp(value: string): string {
export class DiagnosticsService { export class DiagnosticsService {
private readonly userDataPath: string; private readonly userDataPath: string;
private readonly startupLogger?: StartupLogger;
constructor(userDataPath: string) { constructor(userDataPath: string, startupLogger?: StartupLogger) {
this.userDataPath = userDataPath; this.userDataPath = userDataPath;
this.startupLogger = startupLogger;
} }
async exportSnapshot(input: DiagnosticsSnapshotInput): Promise<DiagnosticsExportResult> { async exportSnapshot(input: DiagnosticsSnapshotInput): Promise<DiagnosticsExportResult> {
...@@ -58,6 +67,8 @@ export class DiagnosticsService { ...@@ -58,6 +67,8 @@ export class DiagnosticsService {
const payload = { const payload = {
createdAt, createdAt,
reason: input.reason,
startupLogPath: input.startupLogPath,
app: { app: {
name: input.systemSummary.appName, name: input.systemSummary.appName,
version: input.appVersion, version: input.appVersion,
...@@ -109,6 +120,10 @@ export class DiagnosticsService { ...@@ -109,6 +120,10 @@ export class DiagnosticsService {
config: input.runtimeCloudStatus.config config: input.runtimeCloudStatus.config
} }
: null, : null,
workspace: input.workspaceSummary ?? null,
bundleSync: input.bundleSyncStatus
? { ...input.bundleSyncStatus }
: null,
runtimeTelemetry: input.runtimeTelemetryStatus ?? null, runtimeTelemetry: input.runtimeTelemetryStatus ?? null,
runtime: { runtime: {
status: input.runtimeStatus, status: input.runtimeStatus,
...@@ -133,10 +148,18 @@ export class DiagnosticsService { ...@@ -133,10 +148,18 @@ export class DiagnosticsService {
await mkdir(diagnosticsDir, { recursive: true }); await mkdir(diagnosticsDir, { recursive: true });
await writeFile(filePath, JSON.stringify(payload, null, 2), "utf8"); await writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
await this.startupLogger?.info("diagnostics", "snapshot.exported", "Diagnostics snapshot exported.", {
diagnosticsPath: filePath,
startupLogPath: input.startupLogPath,
reason: input.reason,
workspacePhase: input.workspaceSummary?.startupPhase,
workspaceLaunchState: input.workspaceSummary?.chatLaunchState
});
return { return {
filePath, filePath,
createdAt createdAt,
startupLogPath: input.startupLogPath
}; };
} }
} }
...@@ -8,6 +8,7 @@ import extractZip from "extract-zip"; ...@@ -8,6 +8,7 @@ import extractZip from "extract-zip";
import type { AppConfigService } from "./app-config.js"; import type { AppConfigService } from "./app-config.js";
import type { ProjectStoreService } from "./project-store.js"; import type { ProjectStoreService } from "./project-store.js";
import type { RemoteSkillAsset } from "./skill-store.js"; import type { RemoteSkillAsset } from "./skill-store.js";
import type { StartupLogger } from "./startup-logger.js";
interface BundleManifestRecord { interface BundleManifestRecord {
sourceUrl: string; sourceUrl: string;
...@@ -180,11 +181,13 @@ function logBundle(event: string, details: Record<string, unknown>): void { ...@@ -180,11 +181,13 @@ function logBundle(event: string, details: Record<string, unknown>): void {
export class ProjectBundleService { export class ProjectBundleService {
private readonly configService: AppConfigService; private readonly configService: AppConfigService;
private readonly projectStore: ProjectStoreService; private readonly projectStore: ProjectStoreService;
private readonly startupLogger?: StartupLogger;
private syncStatus: ProjectBundleSyncStatus = { state: "idle" }; private syncStatus: ProjectBundleSyncStatus = { state: "idle" };
constructor(configService: AppConfigService, projectStore: ProjectStoreService) { constructor(configService: AppConfigService, projectStore: ProjectStoreService, startupLogger?: StartupLogger) {
this.configService = configService; this.configService = configService;
this.projectStore = projectStore; this.projectStore = projectStore;
this.startupLogger = startupLogger;
} }
getSyncStatus(): ProjectBundleSyncStatus { getSyncStatus(): ProjectBundleSyncStatus {
...@@ -214,6 +217,13 @@ export class ProjectBundleService { ...@@ -214,6 +217,13 @@ export class ProjectBundleService {
bundleAssetCount: bundleAssets.length bundleAssetCount: bundleAssets.length
}); });
const workspaceRoot = await this.projectStore.getWorkspaceRoot(); const workspaceRoot = await this.projectStore.getWorkspaceRoot();
await this.startupLogger?.info("project-bundle", "sync.start", "Project bundle sync started.", {
action: _action ?? "unknown",
configVersion,
workspaceRoot,
remoteSkillCount: remoteSkills.length,
bundleAssetCount: bundleAssets.length
});
const manifestPath = path.join(workspaceRoot, MANIFESTS_DIR, MANIFEST_FILE); const manifestPath = path.join(workspaceRoot, MANIFESTS_DIR, MANIFEST_FILE);
const currentManifest = (await readJsonFile<Record<string, BundleManifestRecord>>(manifestPath)) ?? {}; const currentManifest = (await readJsonFile<Record<string, BundleManifestRecord>>(manifestPath)) ?? {};
const nextManifest: Record<string, BundleManifestRecord> = {}; const nextManifest: Record<string, BundleManifestRecord> = {};
...@@ -934,3 +944,7 @@ export class ProjectBundleService { ...@@ -934,3 +944,7 @@ export class ProjectBundleService {
import { appendFile, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
export type StartupLogLevel = "info" | "warn" | "error";
export type StartupLogPhase = "bootstrap" | "runtime-cloud" | "project-bundle" | "workspace-summary" | "diagnostics";
interface StartupLogEntry {
ts: string;
level: StartupLogLevel;
phase: StartupLogPhase;
event: string;
message: string;
context?: Record<string, unknown>;
}
function toSafeStamp(value: string): string {
return value.replace(/[:.]/g, "-");
}
function sanitizeUrlLikeString(value: string): string {
if (!/^https?:\/\//i.test(value)) {
return value;
}
try {
const parsed = new URL(value);
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
} catch {
return value;
}
}
function sanitizeValue(value: unknown, key?: string): unknown {
if (value === null || value === undefined) {
return value;
}
if (typeof value === "string") {
if (key && /(token|secret|api[_-]?key|password)/i.test(key)) {
return "<redacted>";
}
return sanitizeUrlLikeString(value);
}
if (Array.isArray(value)) {
return value.map((item) => sanitizeValue(item));
}
if (typeof value === "object") {
const next: Record<string, unknown> = {};
for (const [entryKey, entryValue] of Object.entries(value as Record<string, unknown>)) {
next[entryKey] = sanitizeValue(entryValue, entryKey);
}
return next;
}
return value;
}
export class StartupLogger {
private readonly sessionLogPath: string;
private readonly latestLogPath: string;
private readonly initPromise: Promise<void>;
private writeChain: Promise<void> = Promise.resolve();
constructor(logsPath: string) {
const startupLogsDir = path.join(logsPath, "startup");
const stamp = toSafeStamp(new Date().toISOString());
this.sessionLogPath = path.join(startupLogsDir, `startup-${stamp}.log`);
this.latestLogPath = path.join(startupLogsDir, "startup-latest.log");
this.initPromise = mkdir(startupLogsDir, { recursive: true })
.then(() => Promise.all([
writeFile(this.sessionLogPath, "", "utf8"),
writeFile(this.latestLogPath, "", "utf8")
]))
.then(() => undefined);
}
getSessionLogPath(): string {
return this.sessionLogPath;
}
info(phase: StartupLogPhase, event: string, message: string, context?: Record<string, unknown>): Promise<void> {
return this.log("info", phase, event, message, context);
}
warn(phase: StartupLogPhase, event: string, message: string, context?: Record<string, unknown>): Promise<void> {
return this.log("warn", phase, event, message, context);
}
error(phase: StartupLogPhase, event: string, message: string, context?: Record<string, unknown>): Promise<void> {
return this.log("error", phase, event, message, context);
}
async log(level: StartupLogLevel, phase: StartupLogPhase, event: string, message: string, context?: Record<string, unknown>): Promise<void> {
const entry: StartupLogEntry = {
ts: new Date().toISOString(),
level,
phase,
event,
message,
...(context ? { context: sanitizeValue(context) as Record<string, unknown> } : {})
};
const line = JSON.stringify(entry) + "`n";
this.writeChain = this.writeChain.then(async () => {
await this.initPromise;
await appendFile(this.sessionLogPath, line, "utf8");
await appendFile(this.latestLogPath, line, "utf8");
}).catch(() => undefined);
await this.writeChain;
}
}
...@@ -44,6 +44,9 @@ const desktopApi: DesktopApi = { ...@@ -44,6 +44,9 @@ const desktopApi: DesktopApi = {
list: () => ipcRenderer.invoke(IPC_CHANNELS.projectsList), list: () => ipcRenderer.invoke(IPC_CHANNELS.projectsList),
setActive: (projectId: string) => ipcRenderer.invoke(IPC_CHANNELS.projectsSetActive, projectId) setActive: (projectId: string) => ipcRenderer.invoke(IPC_CHANNELS.projectsSetActive, projectId)
}, },
skillCatalog: {
list: () => ipcRenderer.invoke(IPC_CHANNELS.skillCatalogList)
},
auth: { auth: {
getSessionSummary: () => ipcRenderer.invoke(IPC_CHANNELS.authGetSession), getSessionSummary: () => ipcRenderer.invoke(IPC_CHANNELS.authGetSession),
signIn: (input: SignInInput) => ipcRenderer.invoke(IPC_CHANNELS.authSignIn, input), signIn: (input: SignInInput) => ipcRenderer.invoke(IPC_CHANNELS.authSignIn, input),
......
...@@ -307,6 +307,7 @@ const startupCurtainCopy = { ...@@ -307,6 +307,7 @@ const startupCurtainCopy = {
brandTagline: "START YOUR IDEAS", brandTagline: "START YOUR IDEAS",
loadingLabel: "\u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u5bf9\u8bdd\u73af\u5883", loadingLabel: "\u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u5bf9\u8bdd\u73af\u5883",
syncingConfig: "\u6b63\u5728\u540c\u6b65\u5de5\u4f5c\u914d\u7f6e", syncingConfig: "\u6b63\u5728\u540c\u6b65\u5de5\u4f5c\u914d\u7f6e",
syncingProjects: "\u6b63\u5728\u540c\u6b65\u9879\u76ee\u914d\u7f6e",
startingRuntime: "\u6b63\u5728\u5524\u8d77\u672c\u5730\u52a9\u624b", startingRuntime: "\u6b63\u5728\u5524\u8d77\u672c\u5730\u52a9\u624b",
connectingGateway: "\u6b63\u5728\u5efa\u7acb\u5bf9\u8bdd\u8fde\u63a5", connectingGateway: "\u6b63\u5728\u5efa\u7acb\u5bf9\u8bdd\u8fde\u63a5",
ready: "\u51c6\u5907\u5b8c\u6210\uff0c\u6b63\u5728\u8fdb\u5165\u5bf9\u8bdd", ready: "\u51c6\u5907\u5b8c\u6210\uff0c\u6b63\u5728\u8fdb\u5165\u5bf9\u8bdd",
...@@ -686,6 +687,8 @@ function getStartupProgress(phase: WorkspaceSummary["startupPhase"] | undefined) ...@@ -686,6 +687,8 @@ function getStartupProgress(phase: WorkspaceSummary["startupPhase"] | undefined)
switch (phase) { switch (phase) {
case "syncing-config": case "syncing-config":
return 0.24; return 0.24;
case "syncing-projects":
return 0.4;
case "starting-runtime": case "starting-runtime":
return 0.56; return 0.56;
case "connecting-gateway": case "connecting-gateway":
...@@ -711,6 +714,8 @@ function getStartupCurtainStatus( ...@@ -711,6 +714,8 @@ function getStartupCurtainStatus(
switch (phase) { switch (phase) {
case "syncing-config": case "syncing-config":
return startupCurtainCopy.syncingConfig; return startupCurtainCopy.syncingConfig;
case "syncing-projects":
return startupCurtainCopy.syncingProjects;
case "starting-runtime": case "starting-runtime":
return startupCurtainCopy.startingRuntime; return startupCurtainCopy.startingRuntime;
case "connecting-gateway": case "connecting-gateway":
...@@ -960,7 +965,7 @@ export default function App() { ...@@ -960,7 +965,7 @@ export default function App() {
return; return;
} }
const nextShouldPoll = Boolean(nextWorkspace) && ( const nextShouldPoll = nextWorkspace != null && (
nextWorkspace.chatLaunchState === "starting" nextWorkspace.chatLaunchState === "starting"
|| (!nextWorkspace.shellReady && nextWorkspace.bindingRequired) || (!nextWorkspace.shellReady && nextWorkspace.bindingRequired)
); );
...@@ -1905,7 +1910,7 @@ export default function App() { ...@@ -1905,7 +1910,7 @@ export default function App() {
try { try {
const result = await desktopApi.diagnostics.exportSnapshot(); const result = await desktopApi.diagnostics.exportSnapshot();
setInfoText(ui.exported + result.filePath); setInfoText(ui.exported + result.filePath + (result.startupLogPath ? " | startup: " + result.startupLogPath : ""));
} catch (error) { } catch (error) {
setErrorText(err(error)); setErrorText(err(error));
} }
...@@ -2260,6 +2265,7 @@ export default function App() { ...@@ -2260,6 +2265,7 @@ export default function App() {
<div className="button-row startup-overlay-actions"> <div className="button-row startup-overlay-actions">
<button type="button" disabled={refreshing} onClick={() => void retryStartup()}>{refreshing ? ui.preparing : ui.startupRetry}</button> <button type="button" disabled={refreshing} onClick={() => void retryStartup()}>{refreshing ? ui.preparing : ui.startupRetry}</button>
<button type="button" className="secondary" onClick={() => setViewMode("settings")}>{ui.openSettings}</button> <button type="button" className="secondary" onClick={() => setViewMode("settings")}>{ui.openSettings}</button>
<button type="button" className="secondary" onClick={() => void exportDiagnostics()}>{ui.export}</button>
</div> </div>
) : null} ) : null}
</div> </div>
......
This diff is collapsed.
...@@ -277,33 +277,47 @@ if (startupOnly === 'true') { ...@@ -277,33 +277,47 @@ if (startupOnly === 'true') {
} }
const sendResult = result.sendResult || {}; const sendResult = result.sendResult || {};
const streamSmoke = sendResult.streamSmoke || {}; const streamSmoke = sendResult.streamSmoke || {};
const executionPolicySource = String(streamSmoke.executionPolicySource || ''); const smokeViewMode = String(sendResult.smokeViewMode || 'chat');
if (streamSmoke.phase !== 'completed') { const finalState = result.finalState || {};
throw new Error('Renderer stream smoke did not complete successfully: ' + streamSmoke.phase); if (smokeViewMode === 'skills') {
} if (String(finalState.viewMode || '') !== 'skills') {
if (streamSmoke.fallbackUsed) { throw new Error('Skills smoke did not end on skills view: ' + String(finalState.viewMode || ''));
throw new Error('Renderer stream smoke fell back to non-streaming sendPrompt.'); }
} if (typeof sendResult.skillsPageCatalogCount !== 'number') {
if (!['cloud-default', 'cloud-skill-binding'].includes(executionPolicySource)) { throw new Error('Skills smoke did not report skillsPageCatalogCount.');
throw new Error('Unexpected stream execution policy source: ' + executionPolicySource); }
} if (typeof sendResult.workspaceSkillCount !== 'number') {
if (sendResult.selectedSkillId && streamSmoke.selectedSkillId !== sendResult.selectedSkillId) { throw new Error('Skills smoke did not report workspaceSkillCount.');
throw new Error('Renderer stream selectedSkillId does not match smoke selection.'); }
} } else {
if (Number(streamSmoke.startedEventCount || 0) < 1) { const executionPolicySource = String(streamSmoke.executionPolicySource || '');
throw new Error('Renderer stream smoke did not observe a started event.'); if (streamSmoke.phase !== 'completed') {
} throw new Error('Renderer stream smoke did not complete successfully: ' + streamSmoke.phase);
if (Number(streamSmoke.deltaEventCount || 0) < 1 && !String(streamSmoke.finalContent || '')) { }
throw new Error('Renderer stream smoke did not observe a delta event or final assistant content.'); if (streamSmoke.fallbackUsed) {
} throw new Error('Renderer stream smoke fell back to non-streaming sendPrompt.');
if (Number(streamSmoke.completedEventCount || 0) < 1) { }
throw new Error('Renderer stream smoke did not observe a completed event.'); if (!['cloud-default', 'cloud-skill-binding'].includes(executionPolicySource)) {
} throw new Error('Unexpected stream execution policy source: ' + executionPolicySource);
if (Number(streamSmoke.errorEventCount || 0) !== 0) { }
throw new Error('Renderer stream smoke observed unexpected error events: ' + streamSmoke.errorEventCount); if (sendResult.selectedSkillId && streamSmoke.selectedSkillId !== sendResult.selectedSkillId) {
} throw new Error('Renderer stream selectedSkillId does not match smoke selection.');
if (!String(streamSmoke.renderedContent || streamSmoke.finalContent || '')) { }
throw new Error('Renderer stream smoke did not render assistant content.'); if (Number(streamSmoke.startedEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a started event.');
}
if (Number(streamSmoke.deltaEventCount || 0) < 1 && !String(streamSmoke.finalContent || '')) {
throw new Error('Renderer stream smoke did not observe a delta event or final assistant content.');
}
if (Number(streamSmoke.completedEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a completed event.');
}
if (Number(streamSmoke.errorEventCount || 0) !== 0) {
throw new Error('Renderer stream smoke observed unexpected error events: ' + streamSmoke.errorEventCount);
}
if (!String(streamSmoke.renderedContent || streamSmoke.finalContent || '')) {
throw new Error('Renderer stream smoke did not render assistant content.');
}
} }
if (String(sendResult.system && sendResult.system.userDataPath) !== expectedUserData) { if (String(sendResult.system && sendResult.system.userDataPath) !== expectedUserData) {
throw new Error('Smoke ran against an unexpected userData path: ' + (sendResult.system && sendResult.system.userDataPath)); throw new Error('Smoke ran against an unexpected userData path: ' + (sendResult.system && sendResult.system.userDataPath));
...@@ -312,25 +326,29 @@ if (String(sendResult.system && sendResult.system.logsPath) !== expectedLogs) { ...@@ -312,25 +326,29 @@ if (String(sendResult.system && sendResult.system.logsPath) !== expectedLogs) {
throw new Error('Smoke ran against an unexpected logs path: ' + (sendResult.system && sendResult.system.logsPath)); throw new Error('Smoke ran against an unexpected logs path: ' + (sendResult.system && sendResult.system.logsPath));
} }
const diagnosticsPath = String(sendResult.diagnostics && sendResult.diagnostics.filePath || ''); const diagnosticsPath = String(sendResult.diagnostics && sendResult.diagnostics.filePath || '');
if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) { const diagnostics = diagnosticsPath && fs.existsSync(diagnosticsPath)
throw new Error('Diagnostics snapshot was not produced by smoke.'); ? JSON.parse(fs.readFileSync(diagnosticsPath, 'utf8'))
} : null;
const diagnostics = JSON.parse(fs.readFileSync(diagnosticsPath, 'utf8'));
const runtimeTelemetry = sendResult.runtimeTelemetryAfterWait || sendResult.runtimeTelemetryBeforeWait || {}; const runtimeTelemetry = sendResult.runtimeTelemetryAfterWait || sendResult.runtimeTelemetryBeforeWait || {};
if (!sendResult.runtimeCloudFetch || sendResult.runtimeCloudFetch.state !== 'ready') { if (!sendResult.runtimeCloudFetch || sendResult.runtimeCloudFetch.state !== 'ready') {
throw new Error('Runtime cloud config fetch did not succeed.'); throw new Error('Runtime cloud config fetch did not succeed.');
} }
if (Number(runtimeTelemetry.heartbeatSuccessCount || 0) < 1) { if (smokeViewMode !== 'skills') {
throw new Error('Runtime telemetry did not record a successful heartbeat.'); if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) {
} throw new Error('Diagnostics snapshot was not produced by smoke.');
if (Number(runtimeTelemetry.totalAcceptedEventCount || 0) < 3) { }
throw new Error('Runtime telemetry did not accept the expected event batch count: ' + runtimeTelemetry.totalAcceptedEventCount); if (Number(runtimeTelemetry.heartbeatSuccessCount || 0) < 1) {
} throw new Error('Runtime telemetry did not record a successful heartbeat.');
if (Number(runtimeTelemetry.configSyncSuccessCount || 0) < 1) { }
throw new Error('Runtime telemetry did not record a successful config sync.'); if (Number(runtimeTelemetry.totalAcceptedEventCount || 0) < 3) {
} throw new Error('Runtime telemetry did not accept the expected event batch count: ' + runtimeTelemetry.totalAcceptedEventCount);
if (!diagnostics.runtimeTelemetry) { }
throw new Error('Diagnostics snapshot did not include runtimeTelemetry.'); if (Number(runtimeTelemetry.configSyncSuccessCount || 0) < 1) {
throw new Error('Runtime telemetry did not record a successful config sync.');
}
if (!diagnostics || !diagnostics.runtimeTelemetry) {
throw new Error('Diagnostics snapshot did not include runtimeTelemetry.');
}
} }
if (expectBundled === 'true') { if (expectBundled === 'true') {
const runtimeStatus = sendResult.runtimeStatusAfterProbe || {}; const runtimeStatus = sendResult.runtimeStatusAfterProbe || {};
...@@ -367,7 +385,7 @@ if (expectBundled === 'true') { ...@@ -367,7 +385,7 @@ if (expectBundled === 'true') {
} }
} }
let workspaceEntryValidated = false; let workspaceEntryValidated = false;
if (expectWorkspaceEntry === 'true') { if (expectWorkspaceEntry === 'true' && smokeViewMode !== 'skills') {
const latestStatusLabel = String(streamSmoke.latestStatusLabel || ''); const latestStatusLabel = String(streamSmoke.latestStatusLabel || '');
const statusLabels = Array.isArray(streamSmoke.statusLabels) const statusLabels = Array.isArray(streamSmoke.statusLabels)
? streamSmoke.statusLabels.map((value) => String(value || '')) ? streamSmoke.statusLabels.map((value) => String(value || ''))
......
# Desktop Startup Handoff (2026-04-07)
## Goal
排查并优化 Windows 安装包首启卡在“正在同步员工配置”的问题,重点处理两件事:
1. 修正启动状态汇总,避免把真实错误或后续阶段统一显示成“正在同步员工配置”
2. 增加首启结构化日志和更完整的 diagnostics 信息,便于现场机器定位卡点
## Current Status
当前不是完成态,但主体改动已经做了大半。
已完成:
- 主进程接入 startup logger 基础能力
- diagnostics 导出内容增强
- workspace summary 逻辑已改,新增 `syncing-projects` 阶段,避免“无项目”覆盖真实错误
- UI 已做最小配套修改:
- 支持 `syncing-projects`
- 导出 diagnostics 后显示 `startupLogPath`
- 启动错误遮罩上增加“导出诊断”按钮
未完成:
- 当前还没跑通整仓 typecheck
- 还没跑桌面端/安装包首启验证
- 日志点已接入到部分关键服务,但还没有做完整的 installer 现场回归
## Current Blocker
最新一次整仓类型检查:
```powershell
$env:COREPACK_HOME='D:\qjclaw\.corepack'; corepack pnpm typecheck
```
结果:
- `packages/shared-types` 通过
- `packages/gateway-client` 通过
- `apps/ui` 失败
当前唯一已确认报错:
- `apps/ui/src/App.tsx:969`
- `nextWorkspace` is possibly `null`
对应代码附近逻辑是启动轮询里的:
```ts
const nextWorkspace = await refresh(false);
const nextShouldPoll = Boolean(nextWorkspace) && (
nextWorkspace.chatLaunchState === "starting"
|| (!nextWorkspace.shellReady && nextWorkspace.bindingRequired)
);
```
这里需要把 `nextWorkspace` 收窄后再访问字段。
## Files Changed For This Task
确认与本任务直接相关的文件:
- `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/diagnostics.ts`
- `apps/desktop/src/main/services/project-bundle.ts`
- `apps/desktop/src/main/services/startup-logger.ts`
- `packages/shared-types/src/index.ts`
- `apps/ui/src/App.tsx`
## Important Functional Changes
### 1. New startup logger
新增:
- `apps/desktop/src/main/services/startup-logger.ts`
作用:
-`<logsPath>/startup/` 写 JSONL 风格首启日志
- 生成会话日志和 latest 日志
- 对 token / api key / password / URL 做基本脱敏
### 2. Shared types updated
`packages/shared-types/src/index.ts` 已添加:
- `WorkspaceStartupPhase` 新值:`syncing-projects`
- `DiagnosticsExportResult.startupLogPath?: string`
### 3. Diagnostics export enhanced
`apps/desktop/src/main/services/diagnostics.ts` 当前已支持导出更多现场信息:
- `workspaceSummary`
- `bundleSyncStatus`
- `startupLogPath`
- `reason`
并通过 `startupLogger` 写 diagnostics 导出记录。
### 4. Workspace summary fix
`apps/desktop/src/main/ipc.ts` 是本次最关键的修复点。
当前逻辑已经改成:
- `error` 不再被“无项目”覆盖
- 当 runtime cloud ready 但还没有项目时,进入 `syncing-projects`
- 如果 bundle sync 失败,返回明确错误 summary
- summary 变化会写 startup logger
- diagnostics export 时会附带 startup log path / workspace summary / bundle sync status
这部分是本次修复“首页闪一下又被‘正在同步员工配置’盖回去”的核心。
### 5. Index / service wiring
`apps/desktop/src/main/index.ts` 已做的事情:
- 创建 `startupLogger`
- 注入 `DiagnosticsService`
- 注入 `OpenClawConfigClient`
- 注入 `ProjectBundleService`
- 注入 `registerDesktopIpc`
`cloud-api.ts` / `project-bundle.ts` 已开始接入 logger,但后续仍建议在类型检查通过后再补一次日志点覆盖率检查。
### 6. UI minimal support
`apps/ui/src/App.tsx` 当前的目标性修改只有这些:
- `startupCurtainCopy.syncingProjects`
- `getStartupProgress()` 支持 `syncing-projects`
- `getStartupCurtainStatus()` 支持 `syncing-projects`
- diagnostics 成功提示里包含 `startupLogPath`
- 启动错误遮罩新增 diagnostics 导出按钮
## Other Dirty Files In Worktree
当前 `git diff --name-only` 里还有这些未提交改动:
- `apps/desktop/src/main/services/project-store.ts`
- `apps/desktop/src/preload/index.ts`
- `apps/ui/src/styles.css`
- `build/scripts/electron-smoke.ps1`
这些不在我本次“启动日志/状态机”主线修改的核心列表里,可能是已有改动或其他任务改动。
如果另一个 AI 要继续本任务,不要默认回退这些文件。
## Whether This Affects Another Task
会不会影响,取决于另一个任务改哪些文件。
影响较小的情况:
- 另一个任务不改下列文件:
- `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/diagnostics.ts`
- `apps/desktop/src/main/services/project-bundle.ts`
- `apps/ui/src/App.tsx`
- `packages/shared-types/src/index.ts`
容易冲突的情况:
- 另一个任务要改 `App.tsx`
- 另一个任务要改 desktop main 启动链路
- 另一个任务要改 diagnostics / preload / shared-types IPC 类型
所以如果你只是临时切去改完全无关模块,通常问题不大;如果另一个任务也碰启动、UI 主壳、共享类型或 preload,冲突概率就高。
## Recommended Pause Strategy
如果要先切走做别的任务,建议:
1. 不要继续在当前改动基础上随手混改启动相关文件
2. 让另一个任务尽量避开本任务相关文件
3. 如果另一个 AI 要继续本任务,先读本文件,再跑一次 typecheck,从 UI 的 nullability 报错开始收尾
## Recommended Next Steps For The Next AI
1. 修复 `apps/ui/src/App.tsx:969` 的空值收窄问题
2. 重新运行:
```powershell
$env:COREPACK_HOME='D:\qjclaw\.corepack'; corepack pnpm typecheck
```
3. 如果 typecheck 继续报主进程相关错误,再逐个修:
- `index.ts`
- `cloud-api.ts`
- `project-bundle.ts`
- `ipc.ts`
4. typecheck 通过后,验证桌面端:
- 启动阶段文案是否出现 `syncing-projects`
- diagnostics 导出是否返回 `startupLogPath`
- 启动错误遮罩是否能导出日志
5. 再做安装包/首启验证,重点看是否能区分:
- 员工配置请求失败
- 项目 bundle 同步失败
- runtime 启动失败
- gateway 连接失败
- summary 被覆盖
## Useful Commands
查看工作区变更:
```powershell
git -c safe.directory=D:/qjclaw diff --name-only
```
重新看 UI 变更:
```powershell
git -c safe.directory=D:/qjclaw diff -- apps/ui/src/App.tsx
```
重新跑类型检查:
```powershell
$env:COREPACK_HOME='D:\qjclaw\.corepack'; corepack pnpm typecheck
```
## Notes
- 这个仓库当前在 Windows 环境下,且 `D:\qjclaw` 需要按 safe.directory 方式使用 git
- 我这轮没有做 destructive revert
- 当前最重要的是先保持工作区稳定,不要在 `App.tsx` 上再叠加大改
\ No newline at end of file
export const IPC_CHANNELS = { export const IPC_CHANNELS = {
workspaceGetSummary: "workspace:get-summary", workspaceGetSummary: "workspace:get-summary",
workspaceWarmup: "workspace:warmup", workspaceWarmup: "workspace:warmup",
gatewayStatus: "gateway:status", gatewayStatus: "gateway:status",
...@@ -20,6 +20,7 @@ export const IPC_CHANNELS = { ...@@ -20,6 +20,7 @@ export const IPC_CHANNELS = {
configSave: "config:save", configSave: "config:save",
projectsList: "projects:list", projectsList: "projects:list",
projectsSetActive: "projects:set-active", projectsSetActive: "projects:set-active",
skillCatalogList: "skill-catalog:list",
chatListSessions: "chat:list-sessions", chatListSessions: "chat:list-sessions",
chatListSessionsByProject: "chat:list-sessions-by-project", chatListSessionsByProject: "chat:list-sessions-by-project",
chatCreateSession: "chat:create-session", chatCreateSession: "chat:create-session",
...@@ -62,7 +63,7 @@ export type RuntimeCloudEventType = "startup" | "shutdown" | "message_sent" | "m ...@@ -62,7 +63,7 @@ export type RuntimeCloudEventType = "startup" | "shutdown" | "message_sent" | "m
export type PluginStatus = "included" | "extension" | "unavailable"; export type PluginStatus = "included" | "extension" | "unavailable";
export type SetupMode = "employee-key" | "direct-provider"; 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 WorkspaceStartupPhase = "idle" | "syncing-config" | "syncing-projects" | "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";
...@@ -506,6 +507,7 @@ export interface AppConfig { ...@@ -506,6 +507,7 @@ export interface AppConfig {
export interface DiagnosticsExportResult { export interface DiagnosticsExportResult {
filePath: string; filePath: string;
createdAt: string; createdAt: string;
startupLogPath?: string;
} }
export interface SaveConfigInput { export interface SaveConfigInput {
...@@ -571,6 +573,25 @@ export interface SkillSummary { ...@@ -571,6 +573,25 @@ export interface SkillSummary {
requiresCredits?: number; requiresCredits?: number;
} }
export type SkillCatalogAvailability = "usable" | "info-only";
export type SkillCatalogSource = "project" | "q-skills" | "hybrid";
export interface SkillCatalogItem {
id: string;
name: string;
zhName: string;
description: string;
zhDescription: string;
category: string;
source: SkillCatalogSource;
availability: SkillCatalogAvailability;
selectable: boolean;
isProjectSkill: boolean;
showInSkillsPage: boolean;
searchText: string;
selectionHint?: string;
}
export interface ModelCatalogItemSummary { export interface ModelCatalogItemSummary {
id: string; id: string;
label: string; label: string;
...@@ -656,6 +677,9 @@ export interface DesktopApi { ...@@ -656,6 +677,9 @@ export interface DesktopApi {
list(): Promise<ProjectSummary[]>; list(): Promise<ProjectSummary[]>;
setActive(projectId: string): Promise<WorkspaceSummary>; setActive(projectId: string): Promise<WorkspaceSummary>;
}; };
skillCatalog: {
list(): Promise<SkillCatalogItem[]>;
};
auth: { auth: {
getSessionSummary(): Promise<AuthSessionSummary>; getSessionSummary(): Promise<AuthSessionSummary>;
signIn(input: SignInInput): Promise<AuthSessionSummary>; signIn(input: SignInInput): Promise<AuthSessionSummary>;
...@@ -692,3 +716,4 @@ export interface DesktopApi { ...@@ -692,3 +716,4 @@ export interface DesktopApi {
exportSnapshot(): Promise<DiagnosticsExportResult>; exportSnapshot(): Promise<DiagnosticsExportResult>;
}; };
} }
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