Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
a4d243ae
Commit
a4d243ae
authored
Mar 30, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(desktop): unify startup setup flow and runtime cloud prewarm
parent
f9e6de26
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
824 additions
and
124 deletions
+824
-124
index.ts
apps/desktop/src/main/index.ts
+122
-10
ipc.ts
apps/desktop/src/main/ipc.ts
+333
-57
app-config.ts
apps/desktop/src/main/services/app-config.ts
+83
-34
cloud-api.ts
apps/desktop/src/main/services/cloud-api.ts
+159
-18
index.ts
apps/desktop/src/preload/index.ts
+4
-3
startup-prewarm-plan.zh-CN.md
docs/startup-prewarm-plan.zh-CN.md
+92
-0
index.ts
packages/shared-types/src/index.ts
+31
-2
No files found.
apps/desktop/src/main/index.ts
View file @
a4d243ae
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
{
appendFile
,
readFile
,
writeFile
}
from
"node:fs/promises"
;
import
{
BrowserWindow
,
app
}
from
"electron"
;
import
{
GatewayClient
}
from
"@qjclaw/gateway-client"
;
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
{
registerDesktopIpc
}
from
"./ipc.js"
;
import
{
AppConfigService
}
from
"./services/app-config.js"
;
...
...
@@ -138,6 +138,66 @@ function resolveRequestedRuntimeMode(configMode: RuntimeModePreference): Runtime
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
{
if
(
systemSummary
.
isPackaged
)
{
return
path
.
join
(
systemSummary
.
resourcesPath
,
"vendor"
,
"openclaw-runtime"
);
...
...
@@ -391,6 +451,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const
sessionId
=
state
?.
streamSmoke
?.
sessionId
||
state
?.
activeSessionId
||
"desktop-main"
;
const
runtimeTelemetryAfterWait
=
await
api
.
runtimeTelemetry
.
getStatus
();
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
diagnostics
=
await
api
.
diagnostics
.
exportSnapshot
();
const
health
=
await
api
.
gateway
.
health
();
...
...
@@ -398,8 +460,10 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
return
{
runtimeTelemetryAfterWait
,
sessionId
,
messageCount
:
messages
.
length
,
lastMessage
:
messages
.
at
(
-
1
)
??
null
,
messageCount
:
chatMessages
.
length
,
rawMessageCount
:
messages
.
length
,
lastMessage
:
chatMessages
.
at
(
-
1
)
??
null
,
lastAssistantMessage
,
logCount
:
logs
.
length
,
diagnostics
,
health
,
...
...
@@ -460,6 +524,7 @@ async function bootstrap(): Promise<void> {
}
if (smokeCloudBaseUrl || smokeAuthToken || smokeRuntimeApiKey) {
await configService.save({
setupMode: config.setupMode,
provider: config.provider,
baseUrl: config.baseUrl,
defaultModel: config.defaultModel,
...
...
@@ -484,17 +549,32 @@ async function bootstrap(): Promise<void> {
await deviceIdentityService.load();
const localOpenClawConfig = await loadLocalOpenClawGatewayConfig();
const runtimeCloudClient = new OpenClawConfigClient(configService, secretManager);
await runtimeCloudClient.hydrateCache();
const skillStore = new SkillStoreService(systemSummary.userDataPath);
runtimeCloudClient.onPayloadUpdated(async ({ config: payloadConfig, skills }) => {
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({
vendorRuntimeDir: resolveVendorRuntimeDir(systemSummary),
runtimeDataDir: path.join(systemSummary.userDataPath, "runtime"),
logFilePath: path.join(systemSummary.logsPath, "runtime-manager.log"),
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
});
await runtimeManager.configure();
...
...
@@ -538,9 +618,28 @@ async function bootstrap(): Promise<void> {
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())) {
try {
const shouldUseRuntimeCloud = config.setupMode === "employee-key";
const usingCachedRuntimeCloudConfig = shouldUseRuntimeCloud && runtimeCloudClient.hasCachedPayload();
if (shouldUseRuntimeCloud && !usingCachedRuntimeCloudConfig) {
await runtimeCloudClient.fetchConfig("init");
}
await runtimeManager.start();
const runtimeGatewayConnection = await runtimeManager.getGatewayConnection();
if (runtimeGatewayConnection.url) {
...
...
@@ -550,10 +649,20 @@ async function bootstrap(): Promise<void> {
(await secretManager.getDeviceToken()) ?? undefined
);
}
await gatewayClient.connect().catch(() => undefined);
if (config.setupMode === "employee-key") {
await runtimeCloudSupervisor.start();
if (usingCachedRuntimeCloudConfig) {
scheduleRuntimeCloudRefresh("bootstrap");
}
} else {
await runtimeCloudSupervisor.stop("bootstrap");
}
} catch (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({
...
...
@@ -628,3 +737,6 @@ void bootstrap().catch(async (error) => {
});
apps/desktop/src/main/ipc.ts
View file @
a4d243ae
...
...
@@ -13,7 +13,8 @@ import {
type
SaveConfigInput
,
type
SignInInput
,
type
SystemSummary
,
type
WorkspaceSummary
type
WorkspaceSummary
,
type
WorkspaceWarmupResult
}
from
"@qjclaw/shared-types"
;
import
type
{
GatewayClient
}
from
"@qjclaw/gateway-client"
;
import
type
{
RuntimeManager
}
from
"@qjclaw/runtime-manager"
;
...
...
@@ -134,66 +135,167 @@ function buildPluginSummaries(runtimeStatus: RuntimeStatus): PluginSummary[] {
});
}
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
;
function
delay
(
ms
:
number
):
Promise
<
void
>
{
return
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
ms
));
}
function
isTransientLocalGatewayError
(
message
?:
string
):
boolean
{
if
(
!
message
)
{
return
false
;
}
const
normalized
=
message
.
toLowerCase
();
return
normalized
.
includes
(
"econnrefused"
)
||
normalized
.
includes
(
"failed to connect to ws://127.0.0.1"
)
||
normalized
.
includes
(
"failed to connect to ws://localhost"
)
||
normalized
.
includes
(
"gateway readiness"
)
||
normalized
.
includes
(
"gateway became ready"
)
||
normalized
.
includes
(
"gateway closed during readiness probe"
)
||
normalized
.
includes
(
"gateway closed before readiness probe completed"
)
||
normalized
.
includes
(
"bundled runtime exited before gateway became ready"
);
}
function
toStartupErrorMessage
(
message
:
string
|
undefined
,
fallback
:
string
):
string
{
if
(
isTransientLocalGatewayError
(
message
))
{
return
"
\
u672c
\
u5730
\
u52a9
\
u624b
\
u6682
\
u65f6
\
u6ca1
\
u6709
\
u51c6
\
u5907
\
u597d
\
uff0c
\
u8bf7
\
u91cd
\
u65b0
\
u51c6
\
u5907
\
u3002"
;
}
return
message
??
fallback
;
}
function
shouldRetryManagedRuntimeStartup
(
config
:
AppConfig
,
status
:
RuntimeStatus
):
boolean
{
if
(
config
.
runtimeMode
===
"external-gateway"
||
status
.
processState
!==
"error"
)
{
return
false
;
}
const
bundledRuntimeSelected
=
status
.
selectedMode
===
"bundled-runtime"
||
status
.
activeMode
===
"bundled-runtime"
;
if
(
!
bundledRuntimeSelected
)
{
return
false
;
}
return
isTransientLocalGatewayError
(
status
.
lastError
??
status
.
message
);
}
function
buildChatSummary
(
config
:
AppConfig
,
runtimeStatus
:
RuntimeStatus
,
runtimeCloudStatus
:
RuntimeCloudStatus
,
gatewayStatus
:
GatewayStatus
|
null
):
Pick
<
WorkspaceSummary
,
"chatReady"
|
"chatLaunchState"
|
"chatStatusMessage"
>
{
if
(
!
runtimeCloudStatus
.
apiKeyConfigured
)
{
gatewayStatus
:
GatewayStatus
|
null
,
warmupInFlight
:
boolean
):
Pick
<
WorkspaceSummary
,
"chatReady"
|
"chatLaunchState"
|
"chatStatusMessage"
|
"startupPhase"
|
"startupMessage"
>
{
if
(
!
config
.
apiKeyConfigured
)
{
const
setupMessage
=
config
.
setupMode
===
"direct-provider"
?
"
\
u8bf7
\
u5148
\
u5b8c
\
u6210
\
u5382
\
u5546
\
u4e0e API Key
\
u914d
\
u7f6e
\
u3002"
:
"
\
u8bf7
\
u5148
\
u7ed1
\
u5b9a
\
u5458
\
u5de5
\
u5bc6
\
u94a5
\
u3002"
;
return
{
chatReady
:
false
,
chatLaunchState
:
"unbound"
,
chatStatusMessage
:
"闂備浇宕垫慨鏉懨洪妶澶婂簥闁哄被鍎遍崒銊︾箾閹寸偞鐨戠痪鎯с偢閺岀喓鈧稒顭囩粻姗€鏌¢崱鏇炲祮闁哄本绋戦埥澶娾枍椤撗傜凹閻庨潧銈搁獮鍥敊閻熼澹曢梻鍌氱墛缁嬫帡藟閵忋倖鐓欓柛娑橈功閻帒鈹?"
chatStatusMessage
:
setupMessage
,
startupPhase
:
"idle"
,
startupMessage
:
setupMessage
};
}
if
(
runtimeCloudStatus
.
state
===
"error"
)
{
if
(
config
.
setupMode
===
"employee-key"
&&
runtimeCloudStatus
.
state
===
"error"
)
{
const
runtimeCloudError
=
runtimeCloudStatus
.
lastError
??
"
\
u5458
\
u5de5
\
u914d
\
u7f6e
\
u540c
\
u6b65
\
u5931
\
u8d25
\
uff0c
\
u8bf7
\
u68c0
\
u67e5
\
u5bc6
\
u94a5
\
u6216
\
u7f51
\
u7edc
\
u8fde
\
u63a5
\
u3002"
;
return
{
chatReady
:
false
,
chatLaunchState
:
"error"
,
chatStatusMessage
:
runtimeCloudStatus
.
lastError
??
"OpenClaw 闂備礁鎼ˇ顐﹀疾濠婂牆绀夋慨妞诲亾闁靛棔绶氶獮瀣晝閳ь剛鐚惧澶嬬厸闁割偁鍨洪弳鈺呮⒒閸涱噯鑰挎慨濠冩そ瀵墎鎹勯妸鎰╁€濋弻锝夊Χ閸涱噮妫﹂悗瑙勬礃缁诲牓骞冮埡鍛€绘俊顖滎儠閸嬫ê鈹戦悩顔肩仾闁稿氦鍋愰崚鎺楀礈瑜庨崰鍡涙煥閺囩偛鈧瓕绻?"
chatStatusMessage
:
runtimeCloudError
,
startupPhase
:
"error"
,
startupMessage
:
runtimeCloudError
};
}
const
runtime
CanServeChat
=
runtimeStatus
.
activeMode
===
"external-gateway"
||
runtimeStatus
.
processState
===
"running"
;
if
(
runtimeCanServeChat
&&
gatewayStatus
?.
state
===
"connected"
)
{
const
runtime
Error
=
runtimeStatus
.
lastError
??
runtimeStatus
.
message
;
if
(
warmupInFlight
&&
runtimeStatus
.
processState
===
"error"
&&
isTransientLocalGatewayError
(
runtimeError
)
)
{
return
{
chatReady
:
true
,
chatLaunchState
:
"ready"
,
chatStatusMessage
:
"闂備礁鎼ˇ顐﹀疾濠婂牆绀夋慨妞诲亾闁靛棔绶氶獮瀣晝閳ь剛鐚惧澶嬪仯闁告繂瀚幆鍫ユ煕閵堝棗绗х紒杈ㄦ尰閹峰懘宕妷褜鍞舵繝娈垮枟鑿ч柛鏂挎捣濡叉劙骞掑Δ濠冩櫆闂佺鏈〃鍛?"
chatReady
:
false
,
chatLaunchState
:
"starting"
,
chatStatusMessage
:
"
\
u6b63
\
u5728
\
u91cd
\
u65b0
\
u5524
\
u8d77
\
u672c
\
u5730
\
u52a9
\
u624b
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
,
startupPhase
:
"starting-runtime"
,
startupMessage
:
"
\
u6b63
\
u5728
\
u91cd
\
u65b0
\
u5524
\
u8d77
\
u672c
\
u5730
\
u52a9
\
u624b
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
};
}
const
gatewayError
=
gatewayStatus
?.
lastError
??
gatewayStatus
?.
message
;
if
(
warmupInFlight
&&
gatewayStatus
?.
state
===
"error"
&&
isTransientLocalGatewayError
(
gatewayError
))
{
return
{
chatReady
:
false
,
chatLaunchState
:
"starting"
,
chatStatusMessage
:
"
\
u6b63
\
u5728
\
u91cd
\
u65b0
\
u8fde
\
u63a5
\
u804a
\
u5929
\
u670d
\
u52a1
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
,
startupPhase
:
"connecting-gateway"
,
startupMessage
:
"
\
u6b63
\
u5728
\
u91cd
\
u65b0
\
u8fde
\
u63a5
\
u804a
\
u5929
\
u670d
\
u52a1
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
};
}
if
(
runtimeStatus
.
processState
===
"error"
)
{
const
runtimeErrorMessage
=
toStartupErrorMessage
(
runtimeError
,
"
\
u672c
\
u5730
\
u52a9
\
u624b
\
u542f
\
u52a8
\
u5931
\
u8d25
\
uff0c
\
u8bf7
\
u7a0d
\
u540e
\
u91cd
\
u8bd5
\
u3002"
);
return
{
chatReady
:
false
,
chatLaunchState
:
"error"
,
chatStatusMessage
:
runtimeStatus
.
lastError
??
runtimeStatus
.
message
??
"闂備礁鎼ˇ顐﹀疾濠婂牆绀夋慨妞诲亾闁靛棔绶氶獮瀣晝閳ь剛鐚惧澶嬪仯闁惧繒鎳撻崝瀣煕鎼淬垻鎳囬柡灞剧洴瀵剛鎷犻幓鎺濈€抽梻渚€娼уú锕傚垂瑜版帒绠憸鐗堝笒鍞銈嗙墬缁酣藝椤曗偓閺岋綁鎮╅崣澶婃灎濡炪們鍎查幑鍥春閿濆顫呴柕鍫濇嚀琚濋梺鐟板悑閻n亪宕濈仦瑙f瀺闁靛繈鍊栭崑锝夋煕閵夛絽濡界痪鐐倐閺?"
chatStatusMessage
:
runtimeErrorMessage
,
startupPhase
:
"error"
,
startupMessage
:
runtimeErrorMessage
};
}
if
(
gatewayStatus
?.
state
===
"error"
)
{
const
gatewayErrorMessage
=
toStartupErrorMessage
(
gatewayError
,
"
\
u804a
\
u5929
\
u670d
\
u52a1
\
u8fde
\
u63a5
\
u5931
\
u8d25
\
uff0c
\
u8bf7
\
u7a0d
\
u540e
\
u91cd
\
u8bd5
\
u3002"
);
return
{
chatReady
:
false
,
chatLaunchState
:
"error"
,
chatStatusMessage
:
gatewayStatus
.
lastError
??
gatewayStatus
.
message
??
"缂傚倸鍊搁崐鎼佸疮椤栫偛鍨傞柣銏㈩焾閻鎲告惔鈽嗙劷濠电姵纰嶉崐鐑芥煛婢跺鐏ユい锕€寮剁换婵嬪閳ュ啿濮哥紓渚囧枛婢т粙骞夐幘顔芥櫇闁稿本绋掑▍鏍倵閸忓浜鹃梺鍛婂姈閸庡啿鈻撻懠顒傜=濞达絽澹婇崕蹇曠磼婢跺灏︽鐐插暙铻栭柛娑卞枤閸樻帡鎮楅獮鍨姎闁绘绻愬嵄闁归棿鐒﹂悡?"
chatStatusMessage
:
gatewayErrorMessage
,
startupPhase
:
"error"
,
startupMessage
:
gatewayErrorMessage
};
}
if
(
runtimeStatus
.
selectedMode
===
"bundled-runtime"
&&
runtimeStatus
.
processState
!==
"running"
)
{
const
runtimeCanServeChat
=
runtimeStatus
.
activeMode
===
"external-gateway"
||
runtimeStatus
.
processState
===
"running"
;
if
(
runtimeCanServeChat
&&
gatewayStatus
?.
state
===
"connected"
)
{
return
{
chatReady
:
true
,
chatLaunchState
:
"ready"
,
chatStatusMessage
:
"
\
u804a
\
u5929
\
u670d
\
u52a1
\
u5df2
\
u5c31
\
u7eea
\
u3002"
,
startupPhase
:
"ready"
,
startupMessage
:
"
\
u804a
\
u5929
\
u670d
\
u52a1
\
u5df2
\
u5c31
\
u7eea
\
u3002"
};
}
if
(
config
.
setupMode
===
"employee-key"
&&
(
runtimeCloudStatus
.
state
===
"loading"
||
runtimeCloudStatus
.
state
===
"unconfigured"
))
{
return
{
chatReady
:
false
,
chatLaunchState
:
"starting"
,
chatStatusMessage
:
runtimeStatus
.
message
||
"闂備礁鎼ˇ顐﹀疾濠婂牆绀夋慨妞诲亾闁靛棔绶氶獮瀣晝閳ь剛鐚惧澶嬬厾闁告稑顭崯蹇涙煕閺傚搫浜鹃梻鍌欑窔濞艰崵鎷归悢鐓庣鐎光偓閸曨偆鐣鹃柟鍏肩暘閸斿瞼绮堟径鎰厪濠电偛鐏濋埀顒佺洴瀹曘垽顢楅崟顒傚帾?"
chatStatusMessage
:
"
\
u6b63
\
u5728
\
u540c
\
u6b65
\
u5458
\
u5de5
\
u914d
\
u7f6e
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
,
startupPhase
:
"syncing-config"
,
startupMessage
:
"
\
u6b63
\
u5728
\
u540c
\
u6b65
\
u5458
\
u5de5
\
u914d
\
u7f6e
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
};
}
if
(
runtimeStatus
.
processState
===
"starting"
||
(
runtimeStatus
.
selectedMode
===
"bundled-runtime"
&&
runtimeStatus
.
processState
!==
"running"
))
{
return
{
chatReady
:
false
,
chatLaunchState
:
"starting"
,
chatStatusMessage
:
gatewayStatus
?.
message
??
"濠电姵顔栭崰妤冩崲閹邦喖绶ら柦妯侯檧閼版寧銇勮箛鎾村櫤濞存嚎鍊濋弻锝夊箛椤撶喓绋囨繝銏f硾缁夊墎妲愰幘璇茬闁宠桨鑳舵禒鎾⒑閸涘浼曢柛銉仜?"
chatStatusMessage
:
runtimeStatus
.
message
||
"
\
u6b63
\
u5728
\
u5524
\
u8d77
\
u672c
\
u5730
\
u52a9
\
u624b
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
,
startupPhase
:
"starting-runtime"
,
startupMessage
:
runtimeStatus
.
message
||
"
\
u6b63
\
u5728
\
u5524
\
u8d77
\
u672c
\
u5730
\
u52a9
\
u624b
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
};
}
return
{
chatReady
:
false
,
chatLaunchState
:
"starting"
,
chatStatusMessage
:
gatewayStatus
?.
message
??
"
\
u6b63
\
u5728
\
u8fde
\
u63a5
\
u804a
\
u5929
\
u670d
\
u52a1
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
,
startupPhase
:
"connecting-gateway"
,
startupMessage
:
gatewayStatus
?.
message
??
"
\
u6b63
\
u5728
\
u8fde
\
u63a5
\
u804a
\
u5929
\
u670d
\
u52a1
\
uff0c
\
u8bf7
\
u7a0d
\
u5019
\
u3002"
};
}
export
function
registerDesktopIpc
(
services
:
MainServices
):
DesktopApi
{
const
{
appVersion
,
...
...
@@ -251,6 +353,50 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
);
};
const
connectGatewayClient
=
async
():
Promise
<
void
>
=>
{
const
status
=
await
gatewayClient
.
status
().
catch
(()
=>
null
);
if
(
status
?.
state
===
"connected"
)
{
return
;
}
await
gatewayClient
.
reconnect
().
catch
(()
=>
gatewayClient
.
connect
());
};
const
connectGatewayClientWithRetry
=
async
():
Promise
<
void
>
=>
{
let
lastError
:
unknown
;
for
(
let
attempt
=
1
;
attempt
<=
GATEWAY_CONNECT_RETRY_LIMIT
;
attempt
+=
1
)
{
try
{
await
connectGatewayClient
();
return
;
}
catch
(
error
)
{
lastError
=
error
;
if
(
attempt
>=
GATEWAY_CONNECT_RETRY_LIMIT
)
{
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
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"
;
const
targetGatewayUrl
=
useBundledRuntime
?
runtimeGatewayConnection
.
url
??
nextConfig
.
gatewayUrl
:
resolveEffectiveGatewayUrl
(
nextConfig
.
gatewayUrl
,
localOpenClawConfig
?.
gatewayUrl
);
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"
)
{
...
...
@@ -261,6 +407,22 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
await
runtimeCloudSupervisor
.
stop
(
reason
);
};
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"
);
}
await
syncRuntimeCloudSupervisor
(
`
${
reason
}
-runtime-cloud-refresh`
);
}
catch
{
// Keep cached startup available even if the immediate cloud refresh fails.
}
})();
};
const
startManagedRuntime
=
async
(
reason
:
string
,
options
:
{
...
...
@@ -274,47 +436,124 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
const
apiKey
=
await
secretManager
.
getApiKey
();
if
(
nextConfig
.
runtimeMode
===
"external-gateway"
||
!
apiKey
)
{
await
runtimeCloudSupervisor
.
stop
(
reason
);
if
(
await
shouldRefreshGatewayClient
(
nextConfig
,
options
.
inputToken
))
{
await
reconfigureGatewayClient
(
nextConfig
,
options
.
inputToken
);
await
connectGatewayClientWithRetry
().
catch
(()
=>
undefined
);
}
return
runtimeManager
.
status
();
}
const
shouldUseRuntimeCloud
=
nextConfig
.
setupMode
===
"employee-key"
;
const
usingCachedRuntimeCloudConfig
=
shouldUseRuntimeCloud
&&
(
options
.
action
??
"init"
)
===
"init"
&&
runtimeCloudClient
.
hasCachedPayload
();
if
(
shouldUseRuntimeCloud
&&
!
usingCachedRuntimeCloudConfig
)
{
await
runtimeCloudClient
.
fetchConfig
(
options
.
action
??
"init"
);
const
status
=
options
.
restart
?
await
runtimeManager
.
restart
()
:
await
runtimeManager
.
start
();
}
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
!==
"error"
&&
await
shouldRefreshGatewayClient
(
nextConfig
,
options
.
inputToken
))
{
await
reconfigureGatewayClient
(
nextConfig
,
options
.
inputToken
);
await
connectGatewayClientWithRetry
().
catch
(()
=>
undefined
);
}
if
(
shouldUseRuntimeCloud
)
{
await
syncRuntimeCloudSupervisor
(
reason
);
if
(
usingCachedRuntimeCloudConfig
)
{
scheduleRuntimeCloudRefresh
(
reason
);
}
}
else
{
await
runtimeCloudSupervisor
.
stop
(
reason
);
}
return
status
;
};
const
buildWorkspaceSummary
=
async
():
Promise
<
WorkspaceSummary
>
=>
{
const
runtimeStatus
=
await
runtimeManager
.
status
();
let
runtimeCloudStatus
=
await
runtimeCloudClient
.
getStatus
();
let
workspaceWarmupTail
:
Promise
<
void
>
=
Promise
.
resolve
();
let
workspaceWarmupInFlight
=
false
;
if
(
runtimeCloudStatus
.
apiKeyConfigured
&&
runtimeCloudStatus
.
state
===
"unconfigured"
)
{
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
();
if
(
!
apiKey
)
{
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
{
runtimeCloudStatus
=
await
runtimeCloudClient
.
fetchConfig
(
"init"
);
await
startManagedRuntime
(
reason
,
{
...
options
,
config
:
nextConfig
});
}
catch
{
runtimeCloudStatus
=
await
runtimeCloudClient
.
getStatus
();
}
// Workspace summary and runtime status retain the latest failure details.
}
finally
{
workspaceWarmupInFlight
=
false
;
}
});
const
gatewayStatus
=
runtimeCloudStatus
.
apiKeyConfigured
return
{
accepted
:
true
,
state
:
"scheduled"
,
message
:
alreadyBusy
?
"
\
u540e
\
u53f0
\
u9884
\
u70ed
\
u5df2
\
u6392
\
u961f
\
u3002"
:
"
\
u540e
\
u53f0
\
u9884
\
u70ed
\
u5df2
\
u5f00
\
u59cb
\
u3002"
};
};
const
buildWorkspaceSummary
=
async
():
Promise
<
WorkspaceSummary
>
=>
{
const
config
=
await
getEffectiveConfig
();
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
?
await
gatewayClient
.
status
().
catch
(()
=>
null
)
:
null
;
const
chatSummary
=
buildChatSummary
(
runtimeStatus
,
runtimeCloudStatus
,
gatewayStatus
);
const
chatSummary
=
buildChatSummary
(
config
,
runtimeStatus
,
runtimeCloudStatus
,
gatewayStatus
,
workspaceWarmupInFlight
);
const
skills
=
await
skillStore
.
listWorkspaceSkills
();
return
{
apiKeyConfigured
:
runtimeCloudStatus
.
apiKeyConfigured
,
bindingRequired
:
!
runtimeCloudStatus
.
apiKeyConfigured
,
apiKeyConfigured
:
config
.
apiKeyConfigured
,
bindingRequired
:
!
config
.
apiKeyConfigured
,
setupRequired
:
!
config
.
apiKeyConfigured
,
setupMode
:
config
.
setupMode
,
chatReady
:
chatSummary
.
chatReady
,
chatLaunchState
:
chatSummary
.
chatLaunchState
,
chatStatusMessage
:
chatSummary
.
chatStatusMessage
,
employeeId
:
runtimeCloudStatus
.
config
?.
employeeId
,
employeeName
:
runtimeCloudStatus
.
config
?.
employeeName
,
welcomeMessage
:
runtimeCloudStatus
.
config
?.
welcomeMessage
,
modelId
:
runtimeCloudStatus
.
config
?.
modelId
,
modelDisplayName
:
runtimeCloudStatus
.
config
?.
modelDisplayName
,
configVersion
:
runtimeCloudStatus
.
config
?.
configVersion
,
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
:
runtimeCloudStatus
.
config
?.
modelId
??
config
.
defaultModel
,
modelDisplayName
:
runtimeCloudStatus
.
config
?.
modelDisplayName
??
config
.
defaultModel
,
configVersion
:
config
.
setupMode
===
"employee-key"
?
runtimeCloudStatus
.
config
?.
configVersion
:
undefined
,
lastFetchedAt
:
runtimeCloudStatus
.
lastFetchedAt
??
runtimeCloudStatus
.
config
?.
fetchedAt
,
runtimeCloudState
:
runtimeCloudStatus
.
state
,
runtimeState
:
runtimeStatus
.
processState
,
...
...
@@ -437,6 +676,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
};
ipcMain.handle(IPC_CHANNELS.workspaceGetSummary, async () => buildWorkspaceSummary());
ipcMain.handle(IPC_CHANNELS.workspaceWarmup, async () => queueWorkspaceWarmup("workspace-warmup", { action: "init" }));
ipcMain.handle(IPC_CHANNELS.gatewayStatus, async () => gatewayClient.status());
ipcMain.handle(IPC_CHANNELS.gatewayConnect, async () => gatewayClient.connect());
ipcMain.handle(IPC_CHANNELS.gatewayDisconnect, async () => gatewayClient.disconnect());
...
...
@@ -461,6 +701,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
ipcMain.handle(IPC_CHANNELS.configLoad, async () => getEffectiveConfig());
ipcMain.handle(IPC_CHANNELS.configSave, async (_event, input: SaveConfigInput) => {
const previousConfig = await configService.load();
const config = await configService.save(input);
if (typeof input.apiKey === "string") {
await secretManager.setApiKey(input.apiKey || undefined);
...
...
@@ -471,20 +712,24 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
if (typeof input.authToken === "string") {
await secretManager.setAuthToken(input.authToken || undefined);
}
if (config.setupMode === "direct-provider" || previousConfig.setupMode !== config.setupMode) {
await runtimeCloudClient.clearCache().catch(() => undefined);
}
await runtimeManager.setRequestedMode(config.runtimeMode);
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await startManagedRuntime
("config-save", {
void queueWorkspaceWarmup
("config-save", {
action: "init",
restart: true,
config,
inputToken: input.gatewayToken
});
} 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();
});
...
...
@@ -530,9 +775,8 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
}
});
ipcMain.handle(IPC_CHANNELS.chatStreamPrompt, async (event, sessionId: string, prompt: string, skillId?: string) => {
const executionPolicy = await resolveExecutionPolicy(skillId);
const gatewayPrompt = await prepareGatewayPrompt(prompt, skillId);
const requestId = randomUUID();
let executionPolicy: Awaited<ReturnType<typeof resolveExecutionPolicy>> | null = null;
let settled = false;
let ready = false;
let startedEvent: ChatStreamEvent | null = null;
...
...
@@ -551,6 +795,23 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
runtimeCloudSupervisor.noteMessageReceived(sessionId, prompt, skillId);
try {
queueOrSend({
type: "status",
requestId,
sessionId,
stage: "prepare-request",
label: skillId ? "\u6b63\u5728\u8c03\u53d6\u76f8\u5173\u6280\u80fd" : "\u6b63\u5728\u7406\u89e3\u4f60\u7684\u95ee\u9898"
});
executionPolicy = await resolveExecutionPolicy(skillId);
const gatewayPrompt = await prepareGatewayPrompt(prompt, skillId);
queueOrSend({
type: "status",
requestId,
sessionId,
stage: "await-model",
label: "\u5df2\u6536\u5230\u95ee\u9898\uff0c\u6b63\u5728\u7ec4\u7ec7\u56de\u7b54"
});
const stream = await gatewayClient.streamPrompt(sessionId, gatewayPrompt, {
onStarted: ({ sessionId: nextSessionId, runId }) => {
queueOrSend({
...
...
@@ -558,7 +819,18 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
requestId,
sessionId: nextSessionId,
runId,
executionPolicy
executionPolicy: executionPolicy ?? undefined
});
},
onStatus: ({ sessionId: nextSessionId, runId, stage, label, detail }) => {
queueOrSend({
type: "status",
requestId,
sessionId: nextSessionId,
runId,
stage,
label,
detail
});
},
onDelta: ({ sessionId: nextSessionId, runId, textDelta, fullText }) => {
...
...
@@ -573,20 +845,20 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
},
onCompleted: ({ sessionId: nextSessionId, runId, reply }) => {
settled = true;
runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy.modelId, skillId);
runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy
?
.modelId, skillId);
queueOrSend({
type: "completed",
requestId,
sessionId: nextSessionId,
runId,
reply,
executionPolicy
executionPolicy
: executionPolicy ?? undefined
});
},
onError: ({ sessionId: nextSessionId, runId, error }) => {
settled = true;
runtimeCloudSupervisor.noteError("chat_stream_failed", error.message, {
modelId: executionPolicy.modelId,
modelId: executionPolicy
?
.modelId,
sessionId: nextSessionId
});
queueOrSend({
...
...
@@ -606,19 +878,19 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
requestId,
sessionId: stream.sessionId,
runId: stream.runId,
executionPolicy
executionPolicy
: executionPolicy ?? undefined
});
for (const queuedEvent of queuedEvents) {
emitChatStreamEvent(event.sender, queuedEvent);
}
}, 0);
return { requestId, sessionId: stream.sessionId, runId: stream.runId, executionPolicy };
return { requestId, sessionId: stream.sessionId, runId: stream.runId, executionPolicy
: executionPolicy ?? undefined
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!settled) {
runtimeCloudSupervisor.noteError("chat_stream_failed", message, {
modelId: executionPolicy.modelId,
modelId: executionPolicy
?
.modelId,
sessionId
});
}
...
...
@@ -633,7 +905,8 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
return {
workspace: {
getSummary: () => buildWorkspaceSummary()
getSummary: () => buildWorkspaceSummary(),
warmup: () => queueWorkspaceWarmup("workspace-warmup", { action: "init" })
},
gateway: {
status: () => gatewayClient.status(),
...
...
@@ -679,9 +952,8 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
await runtimeManager.setRequestedMode(config.runtimeMode);
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await startManagedRuntime
("config-save", {
void queueWorkspaceWarmup
("config-save", {
action: "init",
restart: true,
config,
inputToken: input.gatewayToken
});
...
...
@@ -763,3 +1035,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
apps/desktop/src/main/services/app-config.ts
View file @
a4d243ae
import
{
mkdir
,
readFile
,
writeFile
}
from
"node:fs/promises"
;
import
{
mkdir
,
readFile
,
rename
,
rm
,
writeFile
}
from
"node:fs/promises"
;
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_FILE
=
"app-config.json"
;
...
...
@@ -23,6 +23,7 @@ const UI_ROUTE_NAMES = new Set([
]);
interface
LegacyConfig
{
setupMode
?:
SetupMode
;
provider
?:
string
;
baseUrl
?:
string
;
apiKeyConfigured
?:
boolean
;
...
...
@@ -79,6 +80,10 @@ function normalizeRuntimeMode(raw?: string): RuntimeModePreference {
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
{
const
normalized
=
normalizeCloudApiBaseUrl
(
raw
??
""
);
if
(
normalized
)
{
...
...
@@ -91,31 +96,21 @@ function resolveRuntimeCloudApiBaseUrl(raw?: string): string {
export
class
AppConfigService
{
private
readonly
userDataPath
:
string
;
private
ioChain
:
Promise
<
void
>
=
Promise
.
resolve
();
constructor
(
userDataPath
:
string
)
{
this
.
userDataPath
=
userDataPath
;
}
async
load
():
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
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
;
}
return
this
.
runExclusive
(()
=>
this
.
loadUnlocked
());
}
async
save
(
input
:
SaveConfigInput
):
Promise
<
AppConfig
>
{
const
current
=
await
this
.
load
();
return
this
.
runExclusive
(
async
()
=>
{
const
current
=
await
this
.
loadUnlocked
();
const
config
:
AppConfig
=
{
setupMode
:
normalizeSetupMode
(
input
.
setupMode
),
provider
:
input
.
provider
,
baseUrl
:
input
.
baseUrl
,
apiKeyConfigured
:
Boolean
(
input
.
apiKey
)
||
current
.
apiKeyConfigured
,
...
...
@@ -129,10 +124,13 @@ export class AppConfigService {
runtimeMode
:
normalizeRuntimeMode
(
input
.
runtimeMode
)
};
const
filePath
=
this
.
getConfigPath
();
await
mkdir
(
path
.
dirname
(
filePath
),
{
recursive
:
true
});
await
writeFile
(
filePath
,
JSON
.
stringify
(
config
,
null
,
2
),
"utf8"
);
await
this
.
writeConfig
(
config
);
return
config
;
});
}
getDataPath
(...
segments
:
string
[]):
string
{
return
path
.
join
(
this
.
userDataPath
,
...
segments
);
}
private
getConfigPath
():
string
{
...
...
@@ -141,6 +139,7 @@ export class AppConfigService {
private
createDefaultConfig
():
AppConfig
{
return
{
setupMode
:
"employee-key"
,
provider
:
"openai"
,
baseUrl
:
"https://api.openai.com/v1"
,
apiKeyConfigured
:
false
,
...
...
@@ -157,6 +156,7 @@ export class AppConfigService {
private
normalizeConfig
(
config
:
LegacyConfig
):
AppConfig
{
return
{
setupMode
:
normalizeSetupMode
(
config
.
setupMode
),
provider
:
config
.
provider
??
"openai"
,
baseUrl
:
config
.
baseUrl
??
"https://api.openai.com/v1"
,
apiKeyConfigured
:
Boolean
(
config
.
apiKeyConfigured
),
...
...
@@ -170,4 +170,53 @@ export class AppConfigService {
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);
}
}
apps/desktop/src/main/services/cloud-api.ts
View file @
a4d243ae
import
http
from
"node:http"
;
import
{
createHash
}
from
"node:crypto"
;
import
{
mkdir
,
readFile
,
rm
,
writeFile
}
from
"node:fs/promises"
;
import
http
from
"node:http"
;
import
https
from
"node:https"
;
import
path
from
"node:path"
;
import
type
{
AuthSessionSummary
,
CreditSummary
,
...
...
@@ -203,6 +206,10 @@ function cloneJson<T>(value: T): T {
return
JSON
.
parse
(
JSON
.
stringify
(
value
))
as
T
;
}
function
buildApiKeyFingerprint
(
apiKey
:
string
):
string
{
return
createHash
(
"sha256"
).
update
(
apiKey
).
digest
(
"hex"
);
}
function
asRecord
(
value
:
unknown
):
Record
<
string
,
unknown
>
{
return
typeof
value
===
"object"
&&
value
!==
null
?
value
as
Record
<
string
,
unknown
>
:
{};
}
...
...
@@ -227,11 +234,44 @@ type RuntimeCloudPayloadListener = (payload: {
skills
:
RemoteSkillAsset
[];
})
=>
Promise
<
void
>
|
void
;
const
DEFAULT_HTTP_REQUEST_TIMEOUT_MS
=
15
_000
;
interface
OpenClawConfigCacheRecord
{
keyFingerprint
:
string
;
payload
:
OpenClawEmployeeConfigPayload
;
summary
:
RuntimeCloudConfigSummary
;
}
class
HttpJsonClient
{
request
(
url
:
URL
,
options
:
{
method
:
"GET"
|
"POST"
;
headers
?:
Record
<
string
,
string
>
;
body
?:
unknown
}):
Promise
<
string
>
{
request
(
url
:
URL
,
options
:
{
method
:
"GET"
|
"POST"
;
headers
?:
Record
<
string
,
string
>
;
body
?:
unknown
;
timeoutMs
?:
number
;
}
):
Promise
<
string
>
{
const
client
=
url
.
protocol
===
"https:"
?
https
:
http
;
const
timeoutMs
=
options
.
timeoutMs
??
DEFAULT_HTTP_REQUEST_TIMEOUT_MS
;
return
new
Promise
((
resolve
,
reject
)
=>
{
let
settled
=
false
;
const
finishReject
=
(
error
:
Error
)
=>
{
if
(
settled
)
{
return
;
}
settled
=
true
;
reject
(
error
);
};
const
finishResolve
=
(
body
:
string
)
=>
{
if
(
settled
)
{
return
;
}
settled
=
true
;
resolve
(
body
);
};
const
request
=
client
.
request
(
url
,
{
method
:
options
.
method
,
headers
:
{
...
...
@@ -248,15 +288,19 @@ class HttpJsonClient {
response
.
on
(
"end"
,
()
=>
{
const
status
=
response
.
statusCode
??
500
;
if
(
status
<
200
||
status
>=
300
)
{
r
eject
(
new
CloudApiError
(
this
.
extractErrorMessage
(
body
,
status
),
status
));
finishR
eject
(
new
CloudApiError
(
this
.
extractErrorMessage
(
body
,
status
),
status
));
return
;
}
r
esolve
(
body
);
finishR
esolve
(
body
);
});
});
request
.
setTimeout
(
timeoutMs
,
()
=>
{
request
.
destroy
(
new
Error
(
`Cloud API request timed out after
${
Math
.
round
(
timeoutMs
/
1000
)}
s:
${
url
.
origin
}
`
));
});
request
.
on
(
"error"
,
(
error
)
=>
{
r
eject
(
new
Error
(
`Cloud API request failed:
${
error
.
message
}
`
));
finishR
eject
(
new
Error
(
`Cloud API request failed:
${
error
.
message
}
`
));
});
if
(
options
.
body
!==
undefined
)
{
...
...
@@ -465,19 +509,77 @@ export class OpenClawConfigClient {
private readonly secretManager: SecretManager;
private readonly httpClient = new HttpJsonClient();
private readonly payloadListeners = new Set<RuntimeCloudPayloadListener>();
private readonly cachePath: string;
private payloadCache: OpenClawEmployeeConfigPayload | null = null;
private statusCache: RuntimeCloudStatus = {
state: "unconfigured",
baseUrl: "",
apiKeyConfigured: false
};
private cacheLoaded = false;
constructor(configService: AppConfigService, secretManager: SecretManager) {
this.configService = configService;
this.secretManager = secretManager;
this.cachePath = this.configService.getDataPath("config", "runtime-cloud-cache.json");
}
async hydrateCache(): Promise<void> {
if (this.cacheLoaded) {
return;
}
this.cacheLoaded = true;
try {
const raw = await readFile(this.cachePath, "utf8");
const parsed = JSON.parse(raw) as OpenClawConfigCacheRecord;
if (!parsed || typeof parsed !== "object" || !parsed.payload || !parsed.summary) {
throw new Error("Runtime cloud cache is malformed.");
}
if (!parsed.summary.configVersion || !parsed.summary.employeeId) {
throw new Error("Runtime cloud cache is missing required summary fields.");
}
const apiKey = (await this.secretManager.getApiKey())?.trim();
if (!apiKey || parsed.keyFingerprint !== buildApiKeyFingerprint(apiKey)) {
return;
}
this.payloadCache = cloneJson(parsed.payload);
this.statusCache = {
...this.statusCache,
state: "ready",
lastFetchedAt: parsed.summary.fetchedAt,
config: cloneJson(parsed.summary),
lastError: undefined
};
} catch (error) {
if (!this.shouldIgnoreCacheError(error)) {
this.statusCache = {
...this.statusCache,
lastError: error instanceof Error ? error.message : String(error)
};
}
}
}
async clearCache(): Promise<void> {
this.payloadCache = null;
this.statusCache = {
state: "unconfigured",
baseUrl: this.statusCache.baseUrl,
apiKeyConfigured: this.statusCache.apiKeyConfigured
};
this.cacheLoaded = true;
await rm(this.cachePath, { force: true }).catch(() => undefined);
}
hasCachedPayload(): boolean {
return Boolean(this.payloadCache && this.statusCache.config);
}
async getStatus(): Promise<RuntimeCloudStatus> {
await this.hydrateCache();
const config = await this.configService.load();
const apiKey = await this.secretManager.getApiKey();
return {
...
...
@@ -493,7 +595,10 @@ export class OpenClawConfigClient {
}
async buildManagedConfig(defaultConfig: Record<string, unknown>, action: RuntimeCloudFetchAction = "init"): Promise<Record<string, unknown>> {
const payload = await this.fetchPayload(action);
await this.hydrateCache();
const payload = action === "init" && this.payloadCache
? this.payloadCache
: await this.fetchPayload(action);
return this.mergeConfig(defaultConfig, payload);
}
...
...
@@ -508,17 +613,27 @@ export class OpenClawConfigClient {
};
}
private shouldIgnoreCacheError(error: unknown): boolean {
if (!error || typeof error !== "object") {
return false;
}
const candidate = error as NodeJS.ErrnoException;
return candidate.code === "ENOENT" || error instanceof SyntaxError;
}
private async fetchPayload(action: RuntimeCloudFetchAction): Promise<OpenClawEmployeeConfigPayload> {
await this.hydrateCache();
const config = await this.configService.load();
const baseUrl = config.runtimeCloudApiBaseUrl.trim().replace(/\/$/, "");
const apiKey = (await this.secretManager.getApiKey())?.trim();
if (!baseUrl) {
return this.fail(baseUrl, Boolean(apiKey), "
OpenClaw 运行时云端地址未配置。
");
return this.fail(baseUrl, Boolean(apiKey), "
\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u8fd0\u884c\u65f6\u4e91\u7aef\u5730\u5740\u672a\u914d\u7f6e\u3002
");
}
if (!apiKey) {
return this.fail(baseUrl, false, "
请先绑定 OpenClaw employee API Key。
");
return this.fail(baseUrl, false, "
\u8bf7\u5148\u7ed1\u5b9a\u0020\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u0065\u006d\u0070\u006c\u006f\u0079\u0065\u0065\u0020\u0041\u0050\u0049\u0020\u004b\u0065\u0079\u3002
");
}
this.statusCache = {
...
...
@@ -544,22 +659,27 @@ export class OpenClawConfigClient {
try
{
payload
=
JSON
.
parse
(
body
)
as
OpenClawEmployeeConfigPayload
;
}
catch
{
throw
new
Error
(
"
OpenClaw 配置接口返回了无效 JSON。
"
);
throw
new
Error
(
"
\
u004f
\
u0070
\
u0065
\
u006e
\
u0043
\
u006c
\
u0061
\
u0077
\
u0020
\
u914d
\
u7f6e
\
u63a5
\
u53e3
\
u8fd4
\
u56de
\
u4e86
\
u65e0
\
u6548
\
u0020
\
u004a
\
u0053
\
u004f
\
u004e
\
u3002
"
);
}
const
fetchedAt
=
new
Date
().
toISOString
();
if
(
action
===
"sync"
&&
payload
.
changed
===
false
&&
this
.
statusCache
.
config
)
{
const
summary
=
{
...
this
.
statusCache
.
config
,
configVersion
:
payload
.
config_version
??
this
.
statusCache
.
config
.
configVersion
,
fetchedAt
};
this
.
statusCache
=
{
state
:
"ready"
,
baseUrl
,
apiKeyConfigured
:
true
,
lastFetchedAt
:
fetchedAt
,
config
:
{
...
this
.
statusCache
.
config
,
configVersion
:
payload
.
config_version
??
this
.
statusCache
.
config
.
configVersion
,
fetchedAt
}
config
:
summary
,
lastError
:
undefined
};
if
(
this
.
payloadCache
)
{
await
this
.
persistCache
(
summary
,
this
.
payloadCache
,
apiKey
);
}
return
payload
;
}
...
...
@@ -574,14 +694,35 @@ export class OpenClawConfigClient {
config
:
summary
,
lastError
:
undefined
};
await
this
.
persistCache
(
summary
,
this
.
payloadCache
,
apiKey
);
await
this
.
notifyPayloadUpdated
(
action
,
summary
);
return
payload
;
}
catch
(
error
)
{
const
message
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
if
(
this
.
payloadCache
&&
this
.
statusCache
.
config
)
{
this
.
statusCache
=
{
...
this
.
statusCache
,
state
:
"ready"
,
baseUrl
,
apiKeyConfigured
:
true
,
lastError
:
message
};
throw
new
Error
(
message
);
}
return
this
.
fail
(
baseUrl
,
true
,
message
);
}
}
private
async
persistCache
(
summary
:
RuntimeCloudConfigSummary
,
payload
:
OpenClawEmployeeConfigPayload
,
apiKey
:
string
):
Promise
<
void
>
{
const
record
:
OpenClawConfigCacheRecord
=
{
keyFingerprint
:
buildApiKeyFingerprint
(
apiKey
),
summary
,
payload
};
await
mkdir
(
path
.
dirname
(
this
.
cachePath
),
{
recursive
:
true
});
await
writeFile
(
this
.
cachePath
,
JSON
.
stringify
(
record
,
null
,
2
),
"utf8"
);
}
private
async
notifyPayloadUpdated
(
action
:
RuntimeCloudFetchAction
,
config
:
RuntimeCloudConfigSummary
):
Promise
<
void
>
{
const
skills
=
this
.
getRemoteSkillAssets
();
for
(
const
listener
of
this
.
payloadListeners
)
{
...
...
@@ -606,13 +747,13 @@ export class OpenClawConfigClient {
private
validatePayload
(
payload
:
OpenClawEmployeeConfigPayload
):
void
{
if
(
!
payload
.
employee_id
||
!
payload
.
name
)
{
throw
new
Error
(
"
OpenClaw 配置接口缺少员工标识或名称。
"
);
throw
new
Error
(
"
\
u004f
\
u0070
\
u0065
\
u006e
\
u0043
\
u006c
\
u0061
\
u0077
\
u0020
\
u914d
\
u7f6e
\
u63a5
\
u53e3
\
u7f3a
\
u5c11
\
u5458
\
u5de5
\
u6807
\
u8bc6
\
u6216
\
u540d
\
u79f0
\
u3002
"
);
}
if
(
!
payload
.
config_version
)
{
throw
new
Error
(
"
OpenClaw 配置接口缺少 config_version。
"
);
throw
new
Error
(
"
\
u004f
\
u0070
\
u0065
\
u006e
\
u0043
\
u006c
\
u0061
\
u0077
\
u0020
\
u914d
\
u7f6e
\
u63a5
\
u53e3
\
u7f3a
\
u5c11
\
u0020
\
u0063
\
u006f
\
u006e
\
u0066
\
u0069
\
u0067
\
u005f
\
u0076
\
u0065
\
u0072
\
u0073
\
u0069
\
u006f
\
u006e
\
u3002
"
);
}
if
(
!
payload
.
llm
?.
provider
?.
base_url
||
!
payload
.
llm
?.
provider
?.
api_key
||
!
payload
.
llm
?.
model_id
)
{
throw
new
Error
(
"
OpenClaw 配置接口缺少 llm.provider.base_url / api_key / model_id。
"
);
throw
new
Error
(
"
\
u004f
\
u0070
\
u0065
\
u006e
\
u0043
\
u006c
\
u0061
\
u0077
\
u0020
\
u914d
\
u7f6e
\
u63a5
\
u53e3
\
u7f3a
\
u5c11
\
u0020
\
u006c
\
u006c
\
u006d
\
u002e
\
u0070
\
u0072
\
u006f
\
u0076
\
u0069
\
u0064
\
u0065
\
u0072
\
u002e
\
u0062
\
u0061
\
u0073
\
u0065
\
u005f
\
u0075
\
u0072
\
u006c
\
u0020
\
u002f
\
u0020
\
u0061
\
u0070
\
u0069
\
u005f
\
u006b
\
u0065
\
u0079
\
u0020
\
u002f
\
u0020
\
u006d
\
u006f
\
u0064
\
u0065
\
u006c
\
u005f
\
u0069
\
u0064
\
u3002
"
);
}
}
...
...
@@ -735,7 +876,6 @@ export class OpenClawConfigClient {
return
nextConfig
;
}
}
export
class
AuthClient
{
private
readonly
api
:
ProductCloudApiClient
;
...
...
@@ -853,3 +993,4 @@ export class ModelConfigClient {
apps/desktop/src/preload/index.ts
View file @
a4d243ae
import
{
contextBridge
,
ipcRenderer
}
from
"electron"
;
import
{
contextBridge
,
ipcRenderer
}
from
"electron"
;
import
{
IPC_CHANNELS
,
type
ChatStreamListener
,
...
...
@@ -10,7 +10,8 @@ import {
const
desktopApi
:
DesktopApi
=
{
workspace
:
{
getSummary
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
workspaceGetSummary
)
getSummary
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
workspaceGetSummary
),
warmup
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
workspaceWarmup
)
},
gateway
:
{
status
:
()
=>
ipcRenderer
.
invoke
(
IPC_CHANNELS
.
gatewayStatus
),
...
...
docs/startup-prewarm-plan.zh-CN.md
0 → 100644
View file @
a4d243ae
# 启动页预热方案(适配当前代码)
## 背景
当前代码已经把一部分冷启动前移到了主进程,但聊天页仍然会在服务未 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.
聊天页回归
-
进入聊天页后首条消息不应再承担完整冷启动链路。
packages/shared-types/src/index.ts
View file @
a4d243ae
export
const
IPC_CHANNELS
=
{
workspaceGetSummary
:
"workspace:get-summary"
,
workspaceWarmup
:
"workspace:warmup"
,
gatewayStatus
:
"gateway:status"
,
gatewayConnect
:
"gateway:connect"
,
gatewayDisconnect
:
"gateway:disconnect"
,
...
...
@@ -36,7 +37,7 @@
export
type
GatewayState
=
"unknown"
|
"connecting"
|
"connected"
|
"disconnected"
|
"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
CreditStatus
=
"ok"
|
"low"
|
"empty"
;
export
type
RuntimeMode
=
"external-gateway"
|
"bundled-runtime"
;
...
...
@@ -53,10 +54,18 @@ export type RuntimeCloudState = "unconfigured" | "loading" | "ready" | "error";
export
type
RuntimeTelemetryState
=
"idle"
|
"running"
|
"stopped"
|
"error"
;
export
type
RuntimeCloudEventType
=
"startup"
|
"shutdown"
|
"message_sent"
|
"message_received"
|
"error"
|
"config_updated"
;
export
type
PluginStatus
=
"included"
|
"extension"
|
"unavailable"
;
export
type
SetupMode
=
"employee-key"
|
"direct-provider"
;
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
DailyReportDeliveryState
=
"draft"
|
"sent"
|
"failed"
;
export
interface
WorkspaceWarmupResult
{
accepted
:
boolean
;
state
:
"scheduled"
|
"skipped"
;
message
:
string
;
}
export
interface
GatewayStatus
{
state
:
GatewayState
;
url
:
string
;
...
...
@@ -246,9 +255,13 @@ export interface PluginSummary {
export
interface
WorkspaceSummary
{
apiKeyConfigured
:
boolean
;
bindingRequired
:
boolean
;
setupRequired
:
boolean
;
setupMode
:
SetupMode
;
chatReady
:
boolean
;
chatLaunchState
:
ChatLaunchState
;
chatStatusMessage
?:
string
;
startupPhase
:
WorkspaceStartupPhase
;
startupMessage
?:
string
;
employeeId
?:
string
;
employeeName
?:
string
;
welcomeMessage
?:
string
;
...
...
@@ -322,6 +335,16 @@ export interface ChatStreamDeltaEvent {
fullText
?:
string
;
}
export
interface
ChatStreamStatusEvent
{
type
:
"status"
;
requestId
:
string
;
sessionId
:
string
;
runId
?:
string
;
stage
:
string
;
label
:
string
;
detail
?:
string
;
}
export
interface
ChatStreamCompletedEvent
{
type
:
"completed"
;
requestId
:
string
;
...
...
@@ -339,7 +362,7 @@ export interface ChatStreamErrorEvent {
message
:
string
;
}
export
type
ChatStreamEvent
=
ChatStreamStartedEvent
|
ChatStreamDeltaEvent
|
ChatStreamCompletedEvent
|
ChatStreamErrorEvent
;
export
type
ChatStreamEvent
=
ChatStreamStartedEvent
|
ChatStream
StatusEvent
|
ChatStream
DeltaEvent
|
ChatStreamCompletedEvent
|
ChatStreamErrorEvent
;
export
type
ChatStreamListener
=
(
event
:
ChatStreamEvent
)
=>
void
;
...
...
@@ -350,6 +373,7 @@ export interface PromptResult {
}
export
interface
AppConfig
{
setupMode
:
SetupMode
;
provider
:
string
;
baseUrl
:
string
;
apiKeyConfigured
:
boolean
;
...
...
@@ -369,6 +393,7 @@ export interface DiagnosticsExportResult {
}
export
interface
SaveConfigInput
{
setupMode
:
SetupMode
;
provider
:
string
;
baseUrl
:
string
;
apiKey
?:
string
;
...
...
@@ -482,6 +507,7 @@ export interface SystemSummary {
export
interface
DesktopApi
{
workspace
:
{
getSummary
():
Promise
<
WorkspaceSummary
>
;
warmup
():
Promise
<
WorkspaceWarmupResult
>
;
};
gateway
:
{
status
():
Promise
<
GatewayStatus
>
;
...
...
@@ -545,3 +571,6 @@ export interface DesktopApi {
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment