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
0ab5ca4c
Commit
0ab5ca4c
authored
Mar 26, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
skill链路打通
parent
9ae4391b
Changes
11
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1337 additions
and
48 deletions
+1337
-48
index.ts
apps/desktop/src/main/index.ts
+24
-4
ipc.ts
apps/desktop/src/main/ipc.ts
+33
-7
cloud-api.ts
apps/desktop/src/main/services/cloud-api.ts
+41
-29
runtime-cloud-supervisor.ts
apps/desktop/src/main/services/runtime-cloud-supervisor.ts
+1
-1
runtime-skill-bridge.ts
apps/desktop/src/main/services/runtime-skill-bridge.ts
+165
-0
skill-client.ts
apps/desktop/src/main/services/skill-client.ts
+14
-0
skill-store.ts
apps/desktop/src/main/services/skill-store.ts
+290
-0
App.tsx
apps/ui/src/App.tsx
+29
-7
cloud-skill-flow.md
docs/cloud-skill-flow.md
+364
-0
cloud-skill-flow.zh-CN.md
docs/cloud-skill-flow.zh-CN.md
+363
-0
index.ts
packages/shared-types/src/index.ts
+13
-0
No files found.
apps/desktop/src/main/index.ts
View file @
0ab5ca4c
...
@@ -7,13 +7,16 @@ import type { RuntimeModePreference, SystemSummary } from "@qjclaw/shared-types"
...
@@ -7,13 +7,16 @@ import type { RuntimeModePreference, SystemSummary } from "@qjclaw/shared-types"
import
{
createMainWindow
}
from
"./create-window.js"
;
import
{
createMainWindow
}
from
"./create-window.js"
;
import
{
registerDesktopIpc
}
from
"./ipc.js"
;
import
{
registerDesktopIpc
}
from
"./ipc.js"
;
import
{
AppConfigService
}
from
"./services/app-config.js"
;
import
{
AppConfigService
}
from
"./services/app-config.js"
;
import
{
AuthClient
,
CreditClient
,
ModelConfigClient
,
OpenClawConfigClient
,
ProfileClient
,
SkillClient
}
from
"./services/cloud-api.js"
;
import
{
AuthClient
,
CreditClient
,
ModelConfigClient
,
OpenClawConfigClient
,
ProfileClient
}
from
"./services/cloud-api.js"
;
import
{
DeviceIdentityService
}
from
"./services/device-identity.js"
;
import
{
DeviceIdentityService
}
from
"./services/device-identity.js"
;
import
{
DiagnosticsService
}
from
"./services/diagnostics.js"
;
import
{
DiagnosticsService
}
from
"./services/diagnostics.js"
;
import
{
loadLocalOpenClawGatewayConfig
,
resolveEffectiveGatewayUrl
}
from
"./services/openclaw-local-config.js"
;
import
{
loadLocalOpenClawGatewayConfig
,
resolveEffectiveGatewayUrl
}
from
"./services/openclaw-local-config.js"
;
import
{
SecretManager
}
from
"./services/secrets.js"
;
import
{
SecretManager
}
from
"./services/secrets.js"
;
import
{
startSmokeCloudApiServer
}
from
"./services/smoke-cloud-api.js"
;
import
{
startSmokeCloudApiServer
}
from
"./services/smoke-cloud-api.js"
;
import
{
RuntimeCloudSupervisor
}
from
"./services/runtime-cloud-supervisor.js"
;
import
{
RuntimeCloudSupervisor
}
from
"./services/runtime-cloud-supervisor.js"
;
import
{
RuntimeSkillBridgeService
}
from
"./services/runtime-skill-bridge.js"
;
import
{
SkillClient
}
from
"./services/skill-client.js"
;
import
{
SkillStoreService
}
from
"./services/skill-store.js"
;
interface
RendererSmokeState
{
interface
RendererSmokeState
{
usingMockApi
:
boolean
;
usingMockApi
:
boolean
;
...
@@ -262,7 +265,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -262,7 +265,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
result
.
initialState
=
initialState
;
result
.
initialState
=
initialState
;
await
trace
(
"runSmokeTest:initial-state-ready"
);
await
trace
(
"runSmokeTest:initial-state-ready"
);
const
prompt
=
`qjc smoke stream
${
new
Date
().
toISOString
()}
`
;
const
prompt
=
process
.
env
.
QJCLAW_SMOKE_PROMPT
?.
trim
()
||
`qjc smoke stream
${
new
Date
().
toISOString
()}
`
;
const
preferredSkillId
=
process
.
env
.
QJCLAW_SMOKE_SKILL_ID
?.
trim
();
await
trace
(
"runSmokeTest:before-send-script"
);
await
trace
(
"runSmokeTest:before-send-script"
);
const
sendResult
=
await
window
.
webContents
.
executeJavaScript
(
`(async () => {
const
sendResult
=
await
window
.
webContents
.
executeJavaScript
(
`(async () => {
const api = window.qjcDesktop;
const api = window.qjcDesktop;
...
@@ -277,6 +281,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -277,6 +281,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const smokeBaseUrl =
${
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_CLOUD_API_BASE_URL
??
""
)};
const smokeBaseUrl =
${
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_CLOUD_API_BASE_URL
??
""
)};
const
smokeToken
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_AUTH_TOKEN
??
""
)};
const
smokeToken
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_AUTH_TOKEN
??
""
)};
const
smokeRuntimeApiKey
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_RUNTIME_CLOUD_API_KEY
??
"smoke-runtime-api-key"
)};
const
smokeRuntimeApiKey
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_RUNTIME_CLOUD_API_KEY
??
"smoke-runtime-api-key"
)};
const
preferredSkillId
=
$
{
JSON
.
stringify
(
process
.
env
.
QJCLAW_SMOKE_SKILL_ID
?.
trim
()
??
""
)};
if
(
smokeBaseUrl
)
{
if
(
smokeBaseUrl
)
{
const
current
=
await
api
.
config
.
load
();
const
current
=
await
api
.
config
.
load
();
await
api
.
config
.
save
({
await
api
.
config
.
save
({
...
@@ -332,7 +337,12 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
...
@@ -332,7 +337,12 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const
credits
=
session
.
state
===
"authenticated"
?
await
api
.
credits
.
getSummary
()
:
null
;
const
credits
=
session
.
state
===
"authenticated"
?
await
api
.
credits
.
getSummary
()
:
null
;
const
skills
=
session
.
state
===
"authenticated"
?
await
api
.
skills
.
list
()
:
[];
const
skills
=
session
.
state
===
"authenticated"
?
await
api
.
skills
.
list
()
:
[];
const
workspace
=
await
api
.
workspace
.
getSummary
();
const
workspace
=
await
api
.
workspace
.
getSummary
();
const
selectedSkillId
=
workspace
.
skills
[
0
]?.
id
??
skills
[
0
]?.
id
;
const
selectedSkillId
=
preferredSkillId
?
(
workspace
.
skills
.
find
((
skill
)
=>
skill
.
id
===
preferredSkillId
)?.
id
??
skills
.
find
((
skill
)
=>
skill
.
id
===
preferredSkillId
)?.
id
??
workspace
.
skills
[
0
]?.
id
??
skills
[
0
]?.
id
)
:
(
workspace
.
skills
[
0
]?.
id
??
skills
[
0
]?.
id
);
const
sessions
=
await
api
.
chat
.
listSessions
();
const
sessions
=
await
api
.
chat
.
listSessions
();
const
sessionId
=
state
?.
activeSessionId
||
sessions
[
0
]?.
id
||
"desktop-main"
;
const
sessionId
=
state
?.
activeSessionId
||
sessions
[
0
]?.
id
||
"desktop-main"
;
const
system
=
await
api
.
system
.
getSummary
();
const
system
=
await
api
.
system
.
getSummary
();
...
@@ -473,6 +483,10 @@ async function bootstrap(): Promise<void> {
...
@@ -473,6 +483,10 @@ async function bootstrap(): Promise<void> {
await deviceIdentityService.load();
await deviceIdentityService.load();
const localOpenClawConfig = await loadLocalOpenClawGatewayConfig();
const localOpenClawConfig = await loadLocalOpenClawGatewayConfig();
const runtimeCloudClient = new OpenClawConfigClient(configService, secretManager);
const runtimeCloudClient = new OpenClawConfigClient(configService, secretManager);
const skillStore = new SkillStoreService(systemSummary.userDataPath);
runtimeCloudClient.onPayloadUpdated(async ({ config: payloadConfig, skills }) => {
await skillStore.reconcile(skills, payloadConfig.configVersion);
});
const runtimeManager = new RuntimeManager({
const runtimeManager = new RuntimeManager({
vendorRuntimeDir: resolveVendorRuntimeDir(systemSummary),
vendorRuntimeDir: resolveVendorRuntimeDir(systemSummary),
...
@@ -493,10 +507,13 @@ async function bootstrap(): Promise<void> {
...
@@ -493,10 +507,13 @@ async function bootstrap(): Promise<void> {
}
}
});
});
const runtimeSkillBridge = new RuntimeSkillBridgeService(skillStore, runtimeManager);
await runtimeSkillBridge.clearManagedSkills().catch(() => undefined);
const authClient = new AuthClient(configService, secretManager);
const authClient = new AuthClient(configService, secretManager);
const profileClient = new ProfileClient(configService, secretManager);
const profileClient = new ProfileClient(configService, secretManager);
const creditClient = new CreditClient(configService, secretManager);
const creditClient = new CreditClient(configService, secretManager);
const skillClient = new SkillClient(
runtimeCloudClient
);
const skillClient = new SkillClient(
skillStore
);
const modelConfigClient = new ModelConfigClient(configService, secretManager);
const modelConfigClient = new ModelConfigClient(configService, secretManager);
const runtimeCloudSupervisor = new RuntimeCloudSupervisor({
const runtimeCloudSupervisor = new RuntimeCloudSupervisor({
appVersion: app.getVersion(),
appVersion: app.getVersion(),
...
@@ -530,9 +547,11 @@ async function bootstrap(): Promise<void> {
...
@@ -530,9 +547,11 @@ async function bootstrap(): Promise<void> {
profileClient,
profileClient,
creditClient,
creditClient,
skillClient,
skillClient,
skillStore,
modelConfigClient,
modelConfigClient,
runtimeCloudClient,
runtimeCloudClient,
runtimeCloudSupervisor,
runtimeCloudSupervisor,
runtimeSkillBridge,
systemSummary,
systemSummary,
localOpenClawConfig
localOpenClawConfig
});
});
...
@@ -548,6 +567,7 @@ async function bootstrap(): Promise<void> {
...
@@ -548,6 +567,7 @@ async function bootstrap(): Promise<void> {
void (async () => {
void (async () => {
await runtimeCloudSupervisor.stop("app-before-quit");
await runtimeCloudSupervisor.stop("app-before-quit");
await runtimeManager.stop();
await runtimeManager.stop();
await runtimeSkillBridge.clearManagedSkills().catch(() => undefined);
if (stopSmokeCloudApiServer) {
if (stopSmokeCloudApiServer) {
await stopSmokeCloudApiServer();
await stopSmokeCloudApiServer();
stopSmokeCloudApiServer = undefined;
stopSmokeCloudApiServer = undefined;
...
...
apps/desktop/src/main/ipc.ts
View file @
0ab5ca4c
...
@@ -17,11 +17,14 @@ import {
...
@@ -17,11 +17,14 @@ import {
import
type
{
GatewayClient
}
from
"@qjclaw/gateway-client"
;
import
type
{
GatewayClient
}
from
"@qjclaw/gateway-client"
;
import
type
{
RuntimeManager
}
from
"@qjclaw/runtime-manager"
;
import
type
{
RuntimeManager
}
from
"@qjclaw/runtime-manager"
;
import
type
{
AppConfigService
}
from
"./services/app-config.js"
;
import
type
{
AppConfigService
}
from
"./services/app-config.js"
;
import
type
{
AuthClient
,
CreditClient
,
ModelConfigClient
,
OpenClawConfigClient
,
ProfileClient
,
SkillClient
}
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
{
SkillClient
}
from
"./services/skill-client.js"
;
import
type
{
SkillStoreService
}
from
"./services/skill-store.js"
;
import
{
resolveEffectiveGatewayUrl
,
type
LocalOpenClawGatewayConfig
}
from
"./services/openclaw-local-config.js"
;
import
{
resolveEffectiveGatewayUrl
,
type
LocalOpenClawGatewayConfig
}
from
"./services/openclaw-local-config.js"
;
import
type
{
SecretManager
}
from
"./services/secrets.js"
;
import
type
{
SecretManager
}
from
"./services/secrets.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"
;
interface
MainServices
{
interface
MainServices
{
configService
:
AppConfigService
;
configService
:
AppConfigService
;
...
@@ -33,9 +36,11 @@ interface MainServices {
...
@@ -33,9 +36,11 @@ interface MainServices {
profileClient
:
ProfileClient
;
profileClient
:
ProfileClient
;
creditClient
:
CreditClient
;
creditClient
:
CreditClient
;
skillClient
:
SkillClient
;
skillClient
:
SkillClient
;
skillStore
:
SkillStoreService
;
modelConfigClient
:
ModelConfigClient
;
modelConfigClient
:
ModelConfigClient
;
runtimeCloudClient
:
OpenClawConfigClient
;
runtimeCloudClient
:
OpenClawConfigClient
;
runtimeCloudSupervisor
:
RuntimeCloudSupervisor
;
runtimeCloudSupervisor
:
RuntimeCloudSupervisor
;
runtimeSkillBridge
:
RuntimeSkillBridgeService
;
appVersion
:
string
;
appVersion
:
string
;
systemSummary
:
SystemSummary
;
systemSummary
:
SystemSummary
;
localOpenClawConfig
?:
LocalOpenClawGatewayConfig
|
null
;
localOpenClawConfig
?:
LocalOpenClawGatewayConfig
|
null
;
...
@@ -190,9 +195,11 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -190,9 +195,11 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
profileClient
,
profileClient
,
secretManager
,
secretManager
,
skillClient
,
skillClient
,
skillStore
,
modelConfigClient
,
modelConfigClient
,
runtimeCloudClient
,
runtimeCloudClient
,
runtimeCloudSupervisor
,
runtimeCloudSupervisor
,
runtimeSkillBridge
,
systemSummary
,
systemSummary
,
localOpenClawConfig
localOpenClawConfig
}
=
services
;
}
=
services
;
...
@@ -258,7 +265,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -258,7 +265,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
?
await
gatewayClient
.
status
().
catch
(()
=>
null
)
?
await
gatewayClient
.
status
().
catch
(()
=>
null
)
:
null
;
:
null
;
const
chatSummary
=
buildChatSummary
(
runtimeStatus
,
runtimeCloudStatus
,
gatewayStatus
);
const
chatSummary
=
buildChatSummary
(
runtimeStatus
,
runtimeCloudStatus
,
gatewayStatus
);
const
skills
=
await
runtimeCloudClient
.
ge
tWorkspaceSkills
();
const
skills
=
await
skillStore
.
lis
tWorkspaceSkills
();
return
{
return
{
apiKeyConfigured
:
runtimeCloudStatus
.
apiKeyConfigured
,
apiKeyConfigured
:
runtimeCloudStatus
.
apiKeyConfigured
,
...
@@ -341,7 +348,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -341,7 +348,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
const
config
=
await
getEffectiveConfig
();
const
config
=
await
getEffectiveConfig
();
const
[
runtimeCloudStatus
,
skills
]
=
await
Promise
.
all
([
const
[
runtimeCloudStatus
,
skills
]
=
await
Promise
.
all
([
runtimeCloudClient
.
getStatus
(),
runtimeCloudClient
.
getStatus
(),
runtimeCloudClient
.
ge
tWorkspaceSkills
()
skillStore
.
lis
tWorkspaceSkills
()
]);
]);
const
selectedSkill
=
skillId
?
skills
.
find
((
skill
)
=>
skill
.
id
===
skillId
)
:
undefined
;
const
selectedSkill
=
skillId
?
skills
.
find
((
skill
)
=>
skill
.
id
===
skillId
)
:
undefined
;
const
configuredModelId
=
runtimeCloudStatus
.
config
?.
modelId
;
const
configuredModelId
=
runtimeCloudStatus
.
config
?.
modelId
;
...
@@ -378,6 +385,21 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -378,6 +385,21 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
sender
.
send
(
IPC_CHANNELS
.
chatStreamEvent
,
payload
);
sender
.
send
(
IPC_CHANNELS
.
chatStreamEvent
,
payload
);
};
};
const
prepareGatewayPrompt
=
async
(
prompt
:
string
,
skillId
?:
string
):
Promise
<
string
>
=>
{
if
(
!
skillId
)
{
await
runtimeSkillBridge
.
clearManagedSkills
();
return
prompt
;
}
const
runtimeStatus
=
await
runtimeManager
.
status
();
if
(
runtimeStatus
.
activeMode
!==
"bundled-runtime"
)
{
throw
new
Error
(
"Selected skills currently require bundled runtime. Switch from external gateway mode and try again."
);
}
const
prepared
=
await
runtimeSkillBridge
.
preparePrompt
(
prompt
,
skillId
);
return
prepared
.
prompt
;
};
ipcMain
.
handle
(
IPC_CHANNELS
.
workspaceGetSummary
,
async
()
=>
buildWorkspaceSummary
());
ipcMain
.
handle
(
IPC_CHANNELS
.
workspaceGetSummary
,
async
()
=>
buildWorkspaceSummary
());
ipcMain
.
handle
(
IPC_CHANNELS
.
gatewayStatus
,
async
()
=>
gatewayClient
.
status
());
ipcMain
.
handle
(
IPC_CHANNELS
.
gatewayStatus
,
async
()
=>
gatewayClient
.
status
());
ipcMain
.
handle
(
IPC_CHANNELS
.
gatewayConnect
,
async
()
=>
gatewayClient
.
connect
());
ipcMain
.
handle
(
IPC_CHANNELS
.
gatewayConnect
,
async
()
=>
gatewayClient
.
connect
());
...
@@ -462,9 +484,10 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -462,9 +484,10 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
ipcMain
.
handle
(
IPC_CHANNELS
.
chatListMessages
,
async
(
_event
,
sessionId
:
string
)
=>
gatewayClient
.
listMessages
(
sessionId
));
ipcMain
.
handle
(
IPC_CHANNELS
.
chatListMessages
,
async
(
_event
,
sessionId
:
string
)
=>
gatewayClient
.
listMessages
(
sessionId
));
ipcMain
.
handle
(
IPC_CHANNELS
.
chatSendPrompt
,
async
(
_event
,
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
ipcMain
.
handle
(
IPC_CHANNELS
.
chatSendPrompt
,
async
(
_event
,
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
gatewayPrompt
=
await
prepareGatewayPrompt
(
prompt
,
skillId
);
runtimeCloudSupervisor
.
noteMessageReceived
(
sessionId
,
prompt
,
skillId
);
runtimeCloudSupervisor
.
noteMessageReceived
(
sessionId
,
prompt
,
skillId
);
try
{
try
{
const
result
=
await
gatewayClient
.
sendPrompt
(
sessionId
,
p
rompt
);
const
result
=
await
gatewayClient
.
sendPrompt
(
sessionId
,
gatewayP
rompt
);
runtimeCloudSupervisor
.
noteMessageSent
(
result
.
sessionId
,
result
.
reply
.
content
,
executionPolicy
.
modelId
,
skillId
);
runtimeCloudSupervisor
.
noteMessageSent
(
result
.
sessionId
,
result
.
reply
.
content
,
executionPolicy
.
modelId
,
skillId
);
return
{
...
result
,
executionPolicy
};
return
{
...
result
,
executionPolicy
};
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -478,6 +501,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -478,6 +501,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
});
});
ipcMain
.
handle
(
IPC_CHANNELS
.
chatStreamPrompt
,
async
(
event
,
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
ipcMain
.
handle
(
IPC_CHANNELS
.
chatStreamPrompt
,
async
(
event
,
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
gatewayPrompt
=
await
prepareGatewayPrompt
(
prompt
,
skillId
);
const
requestId
=
randomUUID
();
const
requestId
=
randomUUID
();
let
settled
=
false
;
let
settled
=
false
;
let
ready
=
false
;
let
ready
=
false
;
...
@@ -497,7 +521,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -497,7 +521,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
runtimeCloudSupervisor
.
noteMessageReceived
(
sessionId
,
prompt
,
skillId
);
runtimeCloudSupervisor
.
noteMessageReceived
(
sessionId
,
prompt
,
skillId
);
try
{
try
{
const
stream
=
await
gatewayClient
.
streamPrompt
(
sessionId
,
p
rompt
,
{
const
stream
=
await
gatewayClient
.
streamPrompt
(
sessionId
,
gatewayP
rompt
,
{
onStarted
:
({
sessionId
:
nextSessionId
,
runId
})
=>
{
onStarted
:
({
sessionId
:
nextSessionId
,
runId
})
=>
{
queueOrSend
({
queueOrSend
({
type
:
"started"
,
type
:
"started"
,
...
@@ -675,9 +699,10 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -675,9 +699,10 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
listMessages
:
(
sessionId
:
string
)
=>
gatewayClient
.
listMessages
(
sessionId
),
listMessages
:
(
sessionId
:
string
)
=>
gatewayClient
.
listMessages
(
sessionId
),
sendPrompt
:
async
(
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
sendPrompt
:
async
(
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
gatewayPrompt
=
await
prepareGatewayPrompt
(
prompt
,
skillId
);
runtimeCloudSupervisor
.
noteMessageReceived
(
sessionId
,
prompt
,
skillId
);
runtimeCloudSupervisor
.
noteMessageReceived
(
sessionId
,
prompt
,
skillId
);
try
{
try
{
const
result
=
await
gatewayClient
.
sendPrompt
(
sessionId
,
p
rompt
);
const
result
=
await
gatewayClient
.
sendPrompt
(
sessionId
,
gatewayP
rompt
);
runtimeCloudSupervisor
.
noteMessageSent
(
result
.
sessionId
,
result
.
reply
.
content
,
executionPolicy
.
modelId
,
skillId
);
runtimeCloudSupervisor
.
noteMessageSent
(
result
.
sessionId
,
result
.
reply
.
content
,
executionPolicy
.
modelId
,
skillId
);
return
{
...
result
,
executionPolicy
};
return
{
...
result
,
executionPolicy
};
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -691,7 +716,8 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
...
@@ -691,7 +716,8 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
},
},
streamPrompt
:
async
(
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
streamPrompt
:
async
(
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
)
=>
{
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
executionPolicy
=
await
resolveExecutionPolicy
(
skillId
);
const
stream
=
await
gatewayClient
.
streamPrompt
(
sessionId
,
prompt
);
const
gatewayPrompt
=
await
prepareGatewayPrompt
(
prompt
,
skillId
);
const
stream
=
await
gatewayClient
.
streamPrompt
(
sessionId
,
gatewayPrompt
);
return
{
return
{
requestId
:
randomUUID
(),
requestId
:
randomUUID
(),
sessionId
:
stream
.
sessionId
,
sessionId
:
stream
.
sessionId
,
...
...
apps/desktop/src/main/services/cloud-api.ts
View file @
0ab5ca4c
...
@@ -13,10 +13,10 @@ import type {
...
@@ -13,10 +13,10 @@ import type {
RuntimeCloudStatus
,
RuntimeCloudStatus
,
SkillModelBindingMode
,
SkillModelBindingMode
,
SkillSummary
,
SkillSummary
,
UserProfileSummary
,
UserProfileSummary
WorkspaceSkillSummary
}
from
"@qjclaw/shared-types"
;
}
from
"@qjclaw/shared-types"
;
import
type
{
AppConfigService
}
from
"./app-config.js"
;
import
type
{
AppConfigService
}
from
"./app-config.js"
;
import
type
{
RemoteSkillAsset
}
from
"./skill-store.js"
;
import
type
{
SecretManager
}
from
"./secrets.js"
;
import
type
{
SecretManager
}
from
"./secrets.js"
;
interface
SessionPayload
{
interface
SessionPayload
{
...
@@ -206,16 +206,25 @@ function asRecord(value: unknown): Record<string, unknown> {
...
@@ -206,16 +206,25 @@ function asRecord(value: unknown): Record<string, unknown> {
}
}
function
to
WorkspaceSkillSummaries
(
payload
?:
OpenClawEmployeeConfigPayload
|
null
):
WorkspaceSkillSummary
[]
{
function
to
RemoteSkillAssets
(
payload
?:
OpenClawEmployeeConfigPayload
|
null
):
RemoteSkillAsset
[]
{
return
(
payload
?.
skills
??
[]).
map
((
binding
,
index
)
=>
({
return
(
payload
?.
skills
??
[]).
map
((
binding
,
index
)
=>
({
id
:
binding
.
skill_id
??
binding
.
binding_id
??
`skill-
${
index
+
1
}
`
,
bindingId
:
binding
.
binding_id
??
`binding-
${
index
+
1
}
`
,
skillId
:
binding
.
skill_id
??
binding
.
binding_id
??
`skill-
${
index
+
1
}
`
,
name
:
binding
.
skill
?.
title
??
binding
.
skill_id
??
`Skill
${
index
+
1
}
`
,
name
:
binding
.
skill
?.
title
??
binding
.
skill_id
??
`Skill
${
index
+
1
}
`
,
description
:
binding
.
skill
?.
description
??
'Enabled for this employee and ready to use in chat.'
,
description
:
binding
.
skill
?.
description
??
"Enabled for this employee and ready to use in chat."
,
category
:
binding
.
skill
?.
category
??
'general'
,
category
:
binding
.
skill
?.
category
??
"general"
,
enabled
:
true
fileName
:
binding
.
skill
?.
file_name
??
undefined
,
fileSize
:
typeof
binding
.
skill
?.
file_size
===
"number"
?
binding
.
skill
.
file_size
:
undefined
,
downloadUrl
:
binding
.
skill
?.
download_url
??
undefined
}));
}));
}
}
type
RuntimeCloudPayloadListener
=
(
payload
:
{
action
:
RuntimeCloudFetchAction
;
config
:
RuntimeCloudConfigSummary
;
skills
:
RemoteSkillAsset
[];
})
=>
Promise
<
void
>
|
void
;
class
HttpJsonClient
{
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
}):
Promise
<
string
>
{
const
client
=
url
.
protocol
===
"https:"
?
https
:
http
;
const
client
=
url
.
protocol
===
"https:"
?
https
:
http
;
...
@@ -371,6 +380,8 @@ class ProductCloudApiClient {
...
@@ -371,6 +380,8 @@ class ProductCloudApiClient {
description
:
item
.
description
??
"No description provided."
,
description
:
item
.
description
??
"No description provided."
,
category
:
item
.
category
??
"general"
,
category
:
item
.
category
??
"general"
,
enabled
:
item
.
enabled
??
true
,
enabled
:
item
.
enabled
??
true
,
ready
:
item
.
enabled
??
true
,
downloadState
:
"ready"
,
requiresCredits
:
item
.
requiresCredits
requiresCredits
:
item
.
requiresCredits
}));
}));
}
}
...
@@ -451,6 +462,7 @@ export class OpenClawConfigClient {
...
@@ -451,6 +462,7 @@ export class OpenClawConfigClient {
private readonly configService: AppConfigService;
private readonly configService: AppConfigService;
private readonly secretManager: SecretManager;
private readonly secretManager: SecretManager;
private readonly httpClient = new HttpJsonClient();
private readonly httpClient = new HttpJsonClient();
private readonly payloadListeners = new Set<RuntimeCloudPayloadListener>();
private payloadCache: OpenClawEmployeeConfigPayload | null = null;
private payloadCache: OpenClawEmployeeConfigPayload | null = null;
private statusCache: RuntimeCloudStatus = {
private statusCache: RuntimeCloudStatus = {
state: "unconfigured",
state: "unconfigured",
...
@@ -483,8 +495,15 @@ export class OpenClawConfigClient {
...
@@ -483,8 +495,15 @@ export class OpenClawConfigClient {
return this.mergeConfig(defaultConfig, payload);
return this.mergeConfig(defaultConfig, payload);
}
}
async getWorkspaceSkills(): Promise<WorkspaceSkillSummary[]> {
getRemoteSkillAssets(): RemoteSkillAsset[] {
return toWorkspaceSkillSummaries(this.payloadCache);
return toRemoteSkillAssets(this.payloadCache);
}
onPayloadUpdated(listener: RuntimeCloudPayloadListener): () => void {
this.payloadListeners.add(listener);
return () => {
this.payloadListeners.delete(listener);
};
}
}
private async fetchPayload(action: RuntimeCloudFetchAction): Promise<OpenClawEmployeeConfigPayload> {
private async fetchPayload(action: RuntimeCloudFetchAction): Promise<OpenClawEmployeeConfigPayload> {
...
@@ -553,6 +572,7 @@ export class OpenClawConfigClient {
...
@@ -553,6 +572,7 @@ export class OpenClawConfigClient {
config
:
summary
,
config
:
summary
,
lastError
:
undefined
lastError
:
undefined
};
};
await
this
.
notifyPayloadUpdated
(
action
,
summary
);
return
payload
;
return
payload
;
}
catch
(
error
)
{
}
catch
(
error
)
{
const
message
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
const
message
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
...
@@ -560,6 +580,17 @@ export class OpenClawConfigClient {
...
@@ -560,6 +580,17 @@ export class OpenClawConfigClient {
}
}
}
}
private
async
notifyPayloadUpdated
(
action
:
RuntimeCloudFetchAction
,
config
:
RuntimeCloudConfigSummary
):
Promise
<
void
>
{
const
skills
=
this
.
getRemoteSkillAssets
();
for
(
const
listener
of
this
.
payloadListeners
)
{
try
{
await
listener
({
action
,
config
,
skills
});
}
catch
{
// Keep runtime cloud config usable even if local skill sync fails.
}
}
}
private
fail
(
baseUrl
:
string
,
apiKeyConfigured
:
boolean
,
message
:
string
):
never
{
private
fail
(
baseUrl
:
string
,
apiKeyConfigured
:
boolean
,
message
:
string
):
never
{
this
.
statusCache
=
{
this
.
statusCache
=
{
...
this
.
statusCache
,
...
this
.
statusCache
,
...
@@ -747,26 +778,6 @@ export class CreditClient {
...
@@ -747,26 +778,6 @@ export class CreditClient {
}
}
}
}
export
class
SkillClient
{
private
readonly
runtimeCloudClient
:
OpenClawConfigClient
;
constructor
(
runtimeCloudClient
:
OpenClawConfigClient
)
{
this
.
runtimeCloudClient
=
runtimeCloudClient
;
}
async
list
():
Promise
<
SkillSummary
[]
>
{
const
skills
=
await
this
.
runtimeCloudClient
.
getWorkspaceSkills
();
return
skills
.
map
((
skill
)
=>
({
id
:
skill
.
id
,
name
:
skill
.
name
,
description
:
skill
.
description
,
category
:
skill
.
category
,
enabled
:
skill
.
enabled
,
requiresCredits
:
0
}));
}
}
export
class
ModelConfigClient
{
export
class
ModelConfigClient
{
private
readonly
api
:
ProductCloudApiClient
;
private
readonly
api
:
ProductCloudApiClient
;
...
@@ -780,3 +791,4 @@ export class ModelConfigClient {
...
@@ -780,3 +791,4 @@ export class ModelConfigClient {
}
}
apps/desktop/src/main/services/runtime-cloud-supervisor.ts
View file @
0ab5ca4c
...
@@ -50,7 +50,7 @@ interface RuntimeCloudSupervisorOptions {
...
@@ -50,7 +50,7 @@ interface RuntimeCloudSupervisorOptions {
}
}
const
DEFAULT_HEARTBEAT_INTERVAL_MS
=
30000
;
const
DEFAULT_HEARTBEAT_INTERVAL_MS
=
30000
;
const
DEFAULT_CONFIG_SYNC_INTERVAL_MS
=
60
000
;
const
DEFAULT_CONFIG_SYNC_INTERVAL_MS
=
4
*
60
*
60
*
1
000
;
const
DEFAULT_EVENT_FLUSH_INTERVAL_MS
=
10000
;
const
DEFAULT_EVENT_FLUSH_INTERVAL_MS
=
10000
;
const
DEFAULT_EVENT_BATCH_SIZE
=
20
;
const
DEFAULT_EVENT_BATCH_SIZE
=
20
;
const
MAX_EVENT_BATCH_SIZE
=
100
;
const
MAX_EVENT_BATCH_SIZE
=
100
;
...
...
apps/desktop/src/main/services/runtime-skill-bridge.ts
0 → 100644
View file @
0ab5ca4c
import
{
mkdir
,
readdir
,
readFile
,
rm
,
writeFile
}
from
"node:fs/promises"
;
import
path
from
"node:path"
;
import
type
{
RuntimeManager
}
from
"@qjclaw/runtime-manager"
;
import
type
{
SkillExecutionTarget
,
SkillStoreService
}
from
"./skill-store.js"
;
interface
PreparedSkillExecution
{
prompt
:
string
;
skillName
:
string
;
runtimeSkillName
:
string
;
runtimeSkillDir
?:
string
;
localPath
:
string
;
}
const
MANAGED_SKILL_PREFIX
=
"qjclaw-cloud-"
;
function
slugify
(
value
:
string
):
string
{
return
value
.
toLowerCase
()
.
replace
(
/
[^
a-z0-9
]
+/g
,
"-"
)
.
replace
(
/^-+|-+$/g
,
""
)
.
slice
(
0
,
48
)
||
"skill"
;
}
function
stripBom
(
content
:
string
):
string
{
return
content
.
replace
(
/^
\u
FEFF/
,
""
);
}
function
extractFrontmatterName
(
content
:
string
):
string
|
undefined
{
const
trimmed
=
stripBom
(
content
);
if
(
!
trimmed
.
startsWith
(
"---"
))
{
return
undefined
;
}
const
endIndex
=
trimmed
.
indexOf
(
"
\n
---"
,
3
);
if
(
endIndex
<
0
)
{
return
undefined
;
}
const
header
=
trimmed
.
slice
(
3
,
endIndex
);
const
match
=
header
.
match
(
/^name:
\s
*
(
.+
)
$/m
);
return
match
?.[
1
]?.
trim
().
replace
(
/^
[\'\"]
|
[\'\"]
$/g
,
""
)
||
undefined
;
}
function
applyRuntimeSkillName
(
content
:
string
,
runtimeSkillName
:
string
):
string
{
const
normalized
=
stripBom
(
content
);
if
(
normalized
.
startsWith
(
"---"
))
{
const
endIndex
=
normalized
.
indexOf
(
"
\n
---"
,
3
);
if
(
endIndex
>=
0
)
{
const
header
=
normalized
.
slice
(
3
,
endIndex
);
const
body
=
normalized
.
slice
(
endIndex
+
4
);
const
nextHeader
=
/^name:
\s
*.+$/m
.
test
(
header
)
?
header
.
replace
(
/^name:
\s
*.+$/m
,
`name:
${
runtimeSkillName
}
`
)
:
`name:
${
runtimeSkillName
}
\n
${
header
.
trimStart
()}
`
;
return
`---\n
${
nextHeader
}
\n---
${
body
}
`
;
}
}
return
`---\nname:
${
runtimeSkillName
}
\n---\n\n
${
normalized
}
`
;
}
export
class
RuntimeSkillBridgeService
{
private
readonly
skillStore
:
SkillStoreService
;
private
readonly
runtimeManager
:
RuntimeManager
;
constructor
(
skillStore
:
SkillStoreService
,
runtimeManager
:
RuntimeManager
)
{
this
.
skillStore
=
skillStore
;
this
.
runtimeManager
=
runtimeManager
;
}
async
preparePrompt
(
prompt
:
string
,
skillId
:
string
):
Promise
<
PreparedSkillExecution
>
{
const
target
=
await
this
.
skillStore
.
getExecutionTarget
(
skillId
);
if
(
!
target
)
{
throw
new
Error
(
"The selected skill is not ready locally yet."
);
}
const
runtimeStatus
=
await
this
.
runtimeManager
.
status
();
const
activation
=
await
this
.
activateRuntimeSkill
(
target
,
runtimeStatus
.
runtimeDataDir
);
return
{
prompt
:
this
.
buildPrompt
(
prompt
,
target
.
name
,
activation
.
runtimeSkillName
),
skillName
:
target
.
name
,
runtimeSkillName
:
activation
.
runtimeSkillName
,
runtimeSkillDir
:
activation
.
runtimeSkillDir
,
localPath
:
target
.
localPath
};
}
async
clearManagedSkills
():
Promise
<
void
>
{
const
runtimeStatus
=
await
this
.
runtimeManager
.
status
();
const
skillsRoot
=
await
this
.
resolveManagedSkillsRoot
(
runtimeStatus
.
runtimeDataDir
);
await
this
.
removeManagedSkillDirs
(
skillsRoot
);
}
private
async
activateRuntimeSkill
(
target
:
SkillExecutionTarget
,
runtimeDataDir
:
string
):
Promise
<
{
runtimeSkillName
:
string
;
runtimeSkillDir
:
string
}
>
{
const
content
=
await
readFile
(
target
.
localPath
,
"utf8"
);
const
sourceName
=
extractFrontmatterName
(
content
)
??
(
target
.
fileName
?
path
.
parse
(
target
.
fileName
).
name
:
undefined
)
??
target
.
name
??
target
.
skillId
;
const
runtimeSkillName
=
`
${
MANAGED_SKILL_PREFIX
}${
slugify
(
sourceName
)}
`
;
const
skillsRoot
=
await
this
.
resolveManagedSkillsRoot
(
runtimeDataDir
);
const
runtimeSkillDir
=
path
.
join
(
skillsRoot
,
runtimeSkillName
);
const
materializedContent
=
applyRuntimeSkillName
(
content
,
runtimeSkillName
);
await
mkdir
(
skillsRoot
,
{
recursive
:
true
});
await
this
.
removeManagedSkillDirs
(
skillsRoot
);
await
mkdir
(
runtimeSkillDir
,
{
recursive
:
true
});
await
writeFile
(
path
.
join
(
runtimeSkillDir
,
"SKILL.md"
),
materializedContent
,
"utf8"
);
await
writeFile
(
path
.
join
(
runtimeSkillDir
,
".qjclaw-skill.json"
),
JSON
.
stringify
({
source
:
"cloud"
,
selectedSkillName
:
target
.
name
,
runtimeSkillName
,
localPath
:
target
.
localPath
,
materializedAt
:
new
Date
().
toISOString
()
},
null
,
2
),
"utf8"
);
return
{
runtimeSkillName
,
runtimeSkillDir
};
}
private
async
resolveManagedSkillsRoot
(
runtimeDataDir
:
string
):
Promise
<
string
>
{
try
{
const
runtimePaths
=
this
.
runtimeManager
.
resolveBundledPaths
();
const
manifestPath
=
path
.
join
(
runtimePaths
.
runtimeDir
,
"runtime-manifest.json"
);
const
manifestRaw
=
await
readFile
(
manifestPath
,
"utf8"
);
const
manifest
=
JSON
.
parse
(
stripBom
(
manifestRaw
))
as
{
sourceOpenClawEntry
?:
string
};
const
sourceEntry
=
typeof
manifest
.
sourceOpenClawEntry
===
"string"
?
manifest
.
sourceOpenClawEntry
.
trim
()
:
""
;
if
(
sourceEntry
)
{
return
path
.
join
(
path
.
dirname
(
sourceEntry
),
"skills"
);
}
}
catch
{
// Fall back to a runtime-local skills directory when the bundled manifest is unavailable.
}
return
path
.
join
(
runtimeDataDir
,
"skills"
);
}
private
buildPrompt
(
originalPrompt
:
string
,
displayName
:
string
,
runtimeSkillName
:
string
):
string
{
return
[
`You must use the installed OpenClaw skill \"
${
runtimeSkillName
}
\" for this request.`
,
`The user explicitly selected the desktop skill \"
${
displayName
}
\".`
,
"Do not answer with a generic chat response before trying to invoke that local skill."
,
""
,
"User request:"
,
originalPrompt
].
join
(
"
\n
"
);
}
private
async
removeManagedSkillDirs
(
skillsRoot
:
string
):
Promise
<
void
>
{
let
entries
:
Array
<
{
name
:
string
;
isDirectory
:
()
=>
boolean
}
>
=
[];
try
{
entries
=
await
readdir
(
skillsRoot
,
{
withFileTypes
:
true
});
}
catch
{
return
;
}
await
Promise
.
all
(
entries
.
filter
((
entry
)
=>
entry
.
isDirectory
()
&&
entry
.
name
.
startsWith
(
MANAGED_SKILL_PREFIX
))
.
map
((
entry
)
=>
rm
(
path
.
join
(
skillsRoot
,
entry
.
name
),
{
recursive
:
true
,
force
:
true
}))
);
}
}
apps/desktop/src/main/services/skill-client.ts
0 → 100644
View file @
0ab5ca4c
import
type
{
SkillSummary
}
from
"@qjclaw/shared-types"
;
import
type
{
SkillStoreService
}
from
"./skill-store.js"
;
export
class
SkillClient
{
private
readonly
skillStore
:
SkillStoreService
;
constructor
(
skillStore
:
SkillStoreService
)
{
this
.
skillStore
=
skillStore
;
}
list
():
Promise
<
SkillSummary
[]
>
{
return
this
.
skillStore
.
listSkills
();
}
}
apps/desktop/src/main/services/skill-store.ts
0 → 100644
View file @
0ab5ca4c
import
http
from
"node:http"
;
import
https
from
"node:https"
;
import
{
mkdir
,
readFile
,
rename
,
unlink
,
writeFile
}
from
"node:fs/promises"
;
import
path
from
"node:path"
;
import
type
{
SkillDownloadState
,
SkillSummary
,
WorkspaceSkillSummary
}
from
"@qjclaw/shared-types"
;
export
interface
RemoteSkillAsset
{
bindingId
:
string
;
skillId
:
string
;
name
:
string
;
description
:
string
;
category
:
string
;
fileName
?:
string
;
fileSize
?:
number
;
downloadUrl
?:
string
;
}
export
interface
SkillExecutionTarget
{
skillId
:
string
;
name
:
string
;
fileName
?:
string
;
localPath
:
string
;
}
interface
SkillManifestEntry
extends
RemoteSkillAsset
{
localPath
?:
string
;
downloadState
:
SkillDownloadState
;
lastSyncedAt
?:
string
;
lastDownloadedAt
?:
string
;
lastError
?:
string
;
remoteConfigVersion
?:
string
;
}
const
SKILLS_DIR
=
"skills"
;
const
MANIFEST_FILE
=
"manifest.json"
;
const
REDIRECT_STATUS_CODES
=
new
Set
([
301
,
302
,
307
,
308
]);
const
MAX_REDIRECTS
=
5
;
function
toSummary
(
entry
:
SkillManifestEntry
):
WorkspaceSkillSummary
{
return
{
id
:
entry
.
skillId
,
name
:
entry
.
name
,
description
:
entry
.
description
,
category
:
entry
.
category
,
enabled
:
entry
.
downloadState
!==
"removed"
,
ready
:
entry
.
downloadState
===
"ready"
,
downloadState
:
entry
.
downloadState
,
fileName
:
entry
.
fileName
,
fileSize
:
entry
.
fileSize
,
lastSyncedAt
:
entry
.
lastSyncedAt
,
lastError
:
entry
.
lastError
};
}
function
compareEntries
(
left
:
SkillManifestEntry
,
right
:
SkillManifestEntry
):
number
{
return
left
.
name
.
localeCompare
(
right
.
name
,
"zh-CN"
);
}
export
class
SkillStoreService
{
private
readonly
skillsRoot
:
string
;
private
readonly
manifestPath
:
string
;
constructor
(
userDataPath
:
string
)
{
this
.
skillsRoot
=
path
.
join
(
userDataPath
,
SKILLS_DIR
);
this
.
manifestPath
=
path
.
join
(
this
.
skillsRoot
,
MANIFEST_FILE
);
}
async
reconcile
(
remoteSkills
:
RemoteSkillAsset
[],
configVersion
?:
string
):
Promise
<
void
>
{
await
mkdir
(
this
.
skillsRoot
,
{
recursive
:
true
});
const
now
=
new
Date
().
toISOString
();
const
currentEntries
=
await
this
.
readManifest
();
const
currentById
=
new
Map
(
currentEntries
.
map
((
entry
)
=>
[
entry
.
skillId
,
entry
]));
const
nextEntries
:
SkillManifestEntry
[]
=
[];
for
(
const
remoteSkill
of
remoteSkills
)
{
const
current
=
currentById
.
get
(
remoteSkill
.
skillId
);
const
nextEntry
:
SkillManifestEntry
=
{
...
current
,
...
remoteSkill
,
localPath
:
remoteSkill
.
fileName
?
path
.
join
(
this
.
skillsRoot
,
remoteSkill
.
skillId
,
remoteSkill
.
fileName
)
:
current
?.
localPath
,
downloadState
:
current
?.
downloadState
??
"pending"
,
lastSyncedAt
:
now
,
remoteConfigVersion
:
configVersion
??
current
?.
remoteConfigVersion
};
if
(
!
remoteSkill
.
downloadUrl
||
!
remoteSkill
.
fileName
)
{
nextEntries
.
push
({
...
nextEntry
,
downloadState
:
"failed"
,
lastError
:
"技能下载地址或文件名缺失。"
});
continue
;
}
if
(
!
this
.
needsDownload
(
current
,
nextEntry
))
{
nextEntries
.
push
({
...
nextEntry
,
downloadState
:
"ready"
,
lastError
:
undefined
});
continue
;
}
try
{
const
downloadingEntry
:
SkillManifestEntry
=
{
...
nextEntry
,
downloadState
:
"downloading"
,
lastError
:
undefined
};
const
downloadedEntry
=
await
this
.
downloadSkill
(
downloadingEntry
);
nextEntries
.
push
(
downloadedEntry
);
}
catch
(
error
)
{
nextEntries
.
push
({
...
nextEntry
,
downloadState
:
"failed"
,
lastError
:
error
instanceof
Error
?
error
.
message
:
String
(
error
)
});
}
}
const
remoteSkillIds
=
new
Set
(
remoteSkills
.
map
((
skill
)
=>
skill
.
skillId
));
for
(
const
current
of
currentEntries
)
{
if
(
!
remoteSkillIds
.
has
(
current
.
skillId
))
{
nextEntries
.
push
({
...
current
,
downloadState
:
"removed"
,
lastSyncedAt
:
now
,
remoteConfigVersion
:
configVersion
??
current
.
remoteConfigVersion
});
}
}
await
this
.
writeManifest
(
nextEntries
);
}
async
listWorkspaceSkills
():
Promise
<
WorkspaceSkillSummary
[]
>
{
const
entries
=
await
this
.
readManifest
();
return
entries
.
filter
((
entry
)
=>
entry
.
downloadState
!==
"removed"
)
.
sort
(
compareEntries
)
.
map
(
toSummary
);
}
async
listSkills
():
Promise
<
SkillSummary
[]
>
{
const
entries
=
await
this
.
readManifest
();
return
entries
.
filter
((
entry
)
=>
entry
.
downloadState
!==
"removed"
)
.
sort
(
compareEntries
)
.
map
((
entry
)
=>
({
...
toSummary
(
entry
),
requiresCredits
:
0
}));
}
async
getExecutionTarget
(
skillId
:
string
):
Promise
<
SkillExecutionTarget
|
undefined
>
{
const
entries
=
await
this
.
readManifest
();
const
entry
=
entries
.
find
((
item
)
=>
item
.
skillId
===
skillId
&&
item
.
downloadState
===
"ready"
&&
typeof
item
.
localPath
===
"string"
);
if
(
!
entry
?.
localPath
)
{
return
undefined
;
}
return
{
skillId
:
entry
.
skillId
,
name
:
entry
.
name
,
fileName
:
entry
.
fileName
,
localPath
:
entry
.
localPath
};
}
private
needsDownload
(
current
:
SkillManifestEntry
|
undefined
,
next
:
SkillManifestEntry
):
boolean
{
if
(
!
current
)
{
return
true
;
}
if
(
current
.
downloadState
!==
"ready"
)
{
return
true
;
}
if
(
!
current
.
localPath
)
{
return
true
;
}
return
current
.
downloadUrl
!==
next
.
downloadUrl
||
current
.
fileName
!==
next
.
fileName
||
current
.
fileSize
!==
next
.
fileSize
;
}
private
async
downloadSkill
(
entry
:
SkillManifestEntry
):
Promise
<
SkillManifestEntry
>
{
if
(
!
entry
.
downloadUrl
||
!
entry
.
fileName
||
!
entry
.
localPath
)
{
throw
new
Error
(
"技能下载元数据不完整,无法落盘。"
);
}
const
targetDir
=
path
.
dirname
(
entry
.
localPath
);
const
tempPath
=
`
${
entry
.
localPath
}
.tmp-
${
Date
.
now
()}
`
;
const
downloadedAt
=
new
Date
().
toISOString
();
await
mkdir
(
targetDir
,
{
recursive
:
true
});
try
{
const
payload
=
await
this
.
downloadToBuffer
(
new
URL
(
entry
.
downloadUrl
));
await
writeFile
(
tempPath
,
payload
);
await
unlink
(
entry
.
localPath
).
catch
(()
=>
undefined
);
await
rename
(
tempPath
,
entry
.
localPath
);
}
catch
(
error
)
{
await
unlink
(
tempPath
).
catch
(()
=>
undefined
);
throw
error
;
}
return
{
...
entry
,
downloadState
:
"ready"
,
lastDownloadedAt
:
downloadedAt
,
lastError
:
undefined
};
}
private
async
downloadToBuffer
(
url
:
URL
,
redirectCount
=
0
):
Promise
<
Buffer
>
{
const
client
=
url
.
protocol
===
"https:"
?
https
:
http
;
return
new
Promise
<
Buffer
>
((
resolve
,
reject
)
=>
{
const
request
=
client
.
get
(
url
,
(
response
)
=>
{
const
status
=
response
.
statusCode
??
500
;
const
location
=
response
.
headers
.
location
;
if
(
location
&&
REDIRECT_STATUS_CODES
.
has
(
status
))
{
if
(
redirectCount
>=
MAX_REDIRECTS
)
{
reject
(
new
Error
(
"技能下载重定向次数过多。"
));
response
.
resume
();
return
;
}
const
redirectUrl
=
new
URL
(
location
,
url
);
response
.
resume
();
void
this
.
downloadToBuffer
(
redirectUrl
,
redirectCount
+
1
).
then
(
resolve
,
reject
);
return
;
}
if
(
status
<
200
||
status
>=
300
)
{
reject
(
new
Error
(
`技能下载失败,HTTP 状态码
${
status
}
。`
));
response
.
resume
();
return
;
}
const
chunks
:
Buffer
[]
=
[];
response
.
on
(
"data"
,
(
chunk
)
=>
{
chunks
.
push
(
Buffer
.
isBuffer
(
chunk
)
?
chunk
:
Buffer
.
from
(
chunk
));
});
response
.
on
(
"end"
,
()
=>
{
resolve
(
Buffer
.
concat
(
chunks
));
});
});
request
.
on
(
"error"
,
(
error
)
=>
{
reject
(
new
Error
(
`技能下载失败:
${
error
.
message
}
`
));
});
});
}
private
async
readManifest
():
Promise
<
SkillManifestEntry
[]
>
{
await
mkdir
(
this
.
skillsRoot
,
{
recursive
:
true
});
try
{
const
raw
=
await
readFile
(
this
.
manifestPath
,
"utf8"
);
const
parsed
=
JSON
.
parse
(
raw
);
if
(
!
Array
.
isArray
(
parsed
))
{
return
[];
}
return
parsed
.
filter
((
entry
):
entry
is
SkillManifestEntry
=>
typeof
entry
?.
skillId
===
"string"
);
}
catch
{
return
[];
}
}
private
async
writeManifest
(
entries
:
SkillManifestEntry
[]):
Promise
<
void
>
{
const
nextEntries
=
[...
entries
].
sort
(
compareEntries
);
const
tempPath
=
`
${
this
.
manifestPath
}
.tmp-
${
Date
.
now
()}
`
;
await
mkdir
(
this
.
skillsRoot
,
{
recursive
:
true
});
await
writeFile
(
tempPath
,
JSON
.
stringify
(
nextEntries
,
null
,
2
),
"utf8"
);
await
unlink
(
this
.
manifestPath
).
catch
(()
=>
undefined
);
await
rename
(
tempPath
,
this
.
manifestPath
);
}
}
apps/ui/src/App.tsx
View file @
0ab5ca4c
...
@@ -99,7 +99,9 @@ const DEFAULT_SKILL = {
...
@@ -99,7 +99,9 @@ const DEFAULT_SKILL = {
name
:
"默认对话"
,
name
:
"默认对话"
,
description
:
"通用对话技能"
,
description
:
"通用对话技能"
,
category
:
"通用"
,
category
:
"通用"
,
enabled
:
true
enabled
:
true
,
ready
:
true
,
downloadState
:
"ready"
as
const
};
};
const
ui
=
{
const
ui
=
{
...
@@ -190,8 +192,8 @@ const mockDesktopApi = {
...
@@ -190,8 +192,8 @@ const mockDesktopApi = {
runtimeMessage
:
"mock"
,
runtimeMessage
:
"mock"
,
skillCount
:
2
,
skillCount
:
2
,
skills
:
[
skills
:
[
{
id
:
"sheet"
,
name
:
"
表格工具"
,
description
:
"处理电子表格和数据统计。"
,
category
:
"办公"
,
enabled
:
true
},
{
id
:
"sheet"
,
name
:
"
Spreadsheet Tools"
,
description
:
"Process spreadsheets and data summaries."
,
category
:
"office"
,
enabled
:
true
,
ready
:
true
,
downloadState
:
"ready"
,
fileName
:
"sheet.md"
},
{
id
:
"doc"
,
name
:
"
文档工具"
,
description
:
"处理文档和内容整理。"
,
category
:
"办公"
,
enabled
:
true
}
{
id
:
"doc"
,
name
:
"
Document Tools"
,
description
:
"Process documents and content organization."
,
category
:
"office"
,
enabled
:
true
,
ready
:
true
,
downloadState
:
"ready"
,
fileName
:
"doc.md"
}
],
],
plugins
:
[
plugins
:
[
{
id
:
"spreadsheet-tools"
,
name
:
"表格工具"
,
description
:
"读取、统计和处理 Excel、CSV 等常见表格文件。"
,
status
:
"included"
,
includedByDefault
:
true
},
{
id
:
"spreadsheet-tools"
,
name
:
"表格工具"
,
description
:
"读取、统计和处理 Excel、CSV 等常见表格文件。"
,
status
:
"included"
,
includedByDefault
:
true
},
...
@@ -320,6 +322,23 @@ function getPluginCopy(plugin: WorkspaceSummary["plugins"][number]) {
...
@@ -320,6 +322,23 @@ function getPluginCopy(plugin: WorkspaceSummary["plugins"][number]) {
return
pluginDisplayMap
[
plugin
.
id
]
??
{
name
:
plugin
.
name
,
description
:
plugin
.
description
};
return
pluginDisplayMap
[
plugin
.
id
]
??
{
name
:
plugin
.
name
,
description
:
plugin
.
description
};
}
}
function
getSkillStatusText
(
skill
:
WorkspaceSummary
[
"skills"
][
number
])
{
switch
(
skill
.
downloadState
)
{
case
"ready"
:
return
"Ready"
;
case
"downloading"
:
return
"Syncing"
;
case
"failed"
:
return
skill
.
lastError
?
`Failed:
${
skill
.
lastError
}
`
:
"Failed"
;
case
"pending"
:
return
"Pending"
;
case
"removed"
:
return
"Removed"
;
default
:
return
"Unknown"
;
}
}
function
canExchangeMessages
(
runtimeStatus
:
RuntimeStatus
|
null
,
gatewayStatus
:
GatewayStatus
|
null
)
{
function
canExchangeMessages
(
runtimeStatus
:
RuntimeStatus
|
null
,
gatewayStatus
:
GatewayStatus
|
null
)
{
if
(
!
runtimeStatus
||
gatewayStatus
?.
state
!==
"connected"
)
{
if
(
!
runtimeStatus
||
gatewayStatus
?.
state
!==
"connected"
)
{
return
false
;
return
false
;
...
@@ -351,7 +370,9 @@ export default function App() {
...
@@ -351,7 +370,9 @@ export default function App() {
const
activeStreamRef
=
useRef
<
ActiveStreamState
|
null
>
(
null
);
const
activeStreamRef
=
useRef
<
ActiveStreamState
|
null
>
(
null
);
const
[
streamSmoke
,
setStreamSmoke
]
=
useState
<
SmokeStreamSnapshot
|
null
>
(
null
);
const
[
streamSmoke
,
setStreamSmoke
]
=
useState
<
SmokeStreamSnapshot
|
null
>
(
null
);
const
effectiveSkills
=
useMemo
(()
=>
(
workspace
?.
skills
?.
length
?
[
DEFAULT_SKILL
,
...
workspace
.
skills
]
:
[
DEFAULT_SKILL
]),
[
workspace
]);
const
catalogSkills
=
workspace
?.
skills
??
[];
const
readySkills
=
useMemo
(()
=>
catalogSkills
.
filter
((
skill
)
=>
skill
.
ready
),
[
catalogSkills
]);
const
effectiveSkills
=
useMemo
(()
=>
(
readySkills
.
length
?
[
DEFAULT_SKILL
,
...
readySkills
]
:
[
DEFAULT_SKILL
]),
[
readySkills
]);
const
selectedSkill
=
useMemo
(()
=>
effectiveSkills
.
find
((
skill
)
=>
skill
.
id
===
selectedSkillId
)
??
effectiveSkills
[
0
]
??
DEFAULT_SKILL
,
[
effectiveSkills
,
selectedSkillId
]);
const
selectedSkill
=
useMemo
(()
=>
effectiveSkills
.
find
((
skill
)
=>
skill
.
id
===
selectedSkillId
)
??
effectiveSkills
[
0
]
??
DEFAULT_SKILL
,
[
effectiveSkills
,
selectedSkillId
]);
const
chatLaunchState
:
ChatLaunchState
=
workspace
?.
chatLaunchState
??
(
workspace
?.
apiKeyConfigured
?
"starting"
:
"unbound"
);
const
chatLaunchState
:
ChatLaunchState
=
workspace
?.
chatLaunchState
??
(
workspace
?.
apiKeyConfigured
?
"starting"
:
"unbound"
);
const
chatStatusMessage
=
workspace
?.
chatStatusMessage
??
(
chatLaunchState
===
"starting"
?
ui
.
startingHint
:
chatLaunchState
===
"error"
?
ui
.
chatNotReadyError
:
""
);
const
chatStatusMessage
=
workspace
?.
chatStatusMessage
??
(
chatLaunchState
===
"starting"
?
ui
.
startingHint
:
chatLaunchState
===
"error"
?
ui
.
chatNotReadyError
:
""
);
...
@@ -426,7 +447,8 @@ export default function App() {
...
@@ -426,7 +447,8 @@ export default function App() {
setWorkspacePathDraft
((
current
)
=>
current
||
nextConfig
.
workspacePath
);
setWorkspacePathDraft
((
current
)
=>
current
||
nextConfig
.
workspacePath
);
setGatewayStatus
(
statusResult
);
setGatewayStatus
(
statusResult
);
const
nextSkills
=
nextWorkspace
.
skills
.
length
?
[
DEFAULT_SKILL
,
...
nextWorkspace
.
skills
]
:
[
DEFAULT_SKILL
];
const
nextReadySkills
=
nextWorkspace
.
skills
.
filter
((
skill
)
=>
skill
.
ready
);
const
nextSkills
=
nextReadySkills
.
length
?
[
DEFAULT_SKILL
,
...
nextReadySkills
]
:
[
DEFAULT_SKILL
];
if
(
!
nextSkills
.
some
((
skill
)
=>
skill
.
id
===
selectedSkillId
))
{
if
(
!
nextSkills
.
some
((
skill
)
=>
skill
.
id
===
selectedSkillId
))
{
setSelectedSkillId
(
nextSkills
[
0
].
id
);
setSelectedSkillId
(
nextSkills
[
0
].
id
);
}
}
...
@@ -1032,7 +1054,7 @@ export default function App() {
...
@@ -1032,7 +1054,7 @@ export default function App() {
</
div
>
</
div
>
</
section
>
</
section
>
)
:
null
}
)
:
null
}
{
viewMode
===
"skills"
?
<
section
className=
"panel catalog-list"
><
div
className=
"scroll-panel"
>
{
effectiveSkills
.
map
((
skill
)
=>
<
button
key=
{
skill
.
id
}
type=
"button"
className=
"catalog-item"
onClick=
{
()
=>
{
setSelectedSkillId
(
skill
.
id
);
setViewMode
(
"chat"
);
}
}
><
strong
>
{
skill
.
name
}
</
strong
><
p
>
{
skill
.
description
}
</
p
></
button
>)
}{
!
effective
Skills
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noSkillCards
}
</
div
>
:
null
}
</
div
></
section
>
:
null
}
{
viewMode
===
"skills"
?
<
section
className=
"panel catalog-list"
><
div
className=
"scroll-panel"
>
{
catalogSkills
.
map
((
skill
)
=>
<
button
key=
{
skill
.
id
}
type=
"button"
className=
"catalog-item"
disabled=
{
!
skill
.
ready
}
onClick=
{
()
=>
{
if
(
!
skill
.
ready
)
{
return
;
}
setSelectedSkillId
(
skill
.
id
);
setViewMode
(
"chat"
);
}
}
><
strong
>
{
skill
.
name
}
</
strong
><
p
>
{
skill
.
description
}
</
p
><
p
>
{
getSkillStatusText
(
skill
)
}{
skill
.
fileName
?
` - ${skill.fileName}`
:
""
}
</
p
></
button
>)
}{
!
catalog
Skills
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noSkillCards
}
</
div
>
:
null
}
</
div
></
section
>
:
null
}
{
viewMode
===
"plugins"
?
<
section
className=
"panel catalog-list"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
pluginTitle
}
</
h3
></
div
></
div
><
div
className=
"scroll-panel"
>
{
workspace
?.
plugins
.
map
((
plugin
)
=>
{
const
copy
=
getPluginCopy
(
plugin
);
return
<
article
key=
{
plugin
.
id
}
className=
"catalog-item static"
><
strong
>
{
copy
.
name
}
</
strong
><
p
>
{
copy
.
description
}
</
p
></
article
>;
})
}{
!
workspace
?.
plugins
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noPlugins
}
</
div
>
:
null
}
</
div
></
section
>
:
null
}
{
viewMode
===
"plugins"
?
<
section
className=
"panel catalog-list"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
pluginTitle
}
</
h3
></
div
></
div
><
div
className=
"scroll-panel"
>
{
workspace
?.
plugins
.
map
((
plugin
)
=>
{
const
copy
=
getPluginCopy
(
plugin
);
return
<
article
key=
{
plugin
.
id
}
className=
"catalog-item static"
><
strong
>
{
copy
.
name
}
</
strong
><
p
>
{
copy
.
description
}
</
p
></
article
>;
})
}{
!
workspace
?.
plugins
.
length
?
<
div
className=
"empty-state"
>
{
ui
.
noPlugins
}
</
div
>
:
null
}
</
div
></
section
>
:
null
}
{
viewMode
===
"settings"
?
<
div
className=
"page-stack"
><
section
className=
"panel settings-panel"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
settingsTitle
}
</
h3
><
p
>
{
ui
.
settingsDesc
}
</
p
></
div
><
StatusChip
tone=
{
workspace
?.
apiKeyConfigured
?
"positive"
:
"warning"
}
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
StatusChip
></
div
><
div
className=
"form-grid single"
><
label
>
{
ui
.
apiKey
}
<
input
type=
"password"
value=
{
apiKeyDraft
}
placeholder=
{
workspace
?.
apiKeyConfigured
?
ui
.
changeApiKey
:
ui
.
apiKeyPlaceholder
}
onChange=
{
(
event
)
=>
setApiKeyDraft
(
event
.
target
.
value
)
}
/></
label
><
label
>
{
ui
.
workspacePath
}
<
input
value=
{
workspacePathDraft
}
onChange=
{
(
event
)
=>
setWorkspacePathDraft
(
event
.
target
.
value
)
}
/></
label
></
div
><
div
className=
"button-row"
><
button
disabled=
{
saving
}
onClick=
{
()
=>
void
saveConfig
(
apiKeyDraft
)
}
>
{
saving
?
ui
.
saving
:
ui
.
save
}
</
button
></
div
><
div
className=
"mini-info"
><
span
>
{
ui
.
currentBinding
}
</
span
><
strong
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
strong
></
div
></
section
><
section
className=
"panel settings-panel"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
diagnostics
}
</
h3
><
p
>
{
ui
.
diagnosticsDesc
}
</
p
></
div
></
div
><
div
className=
"mini-info"
><
span
>
{
ui
.
workspacePath
}
</
span
><
strong
>
{
config
?.
workspacePath
||
workspacePathDraft
||
ui
.
none
}
</
strong
></
div
><
div
className=
"button-row"
><
button
className=
"secondary"
onClick=
{
()
=>
void
exportDiagnostics
()
}
>
{
ui
.
export
}
</
button
></
div
></
section
></
div
>
:
null
}
{
viewMode
===
"settings"
?
<
div
className=
"page-stack"
><
section
className=
"panel settings-panel"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
settingsTitle
}
</
h3
><
p
>
{
ui
.
settingsDesc
}
</
p
></
div
><
StatusChip
tone=
{
workspace
?.
apiKeyConfigured
?
"positive"
:
"warning"
}
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
StatusChip
></
div
><
div
className=
"form-grid single"
><
label
>
{
ui
.
apiKey
}
<
input
type=
"password"
value=
{
apiKeyDraft
}
placeholder=
{
workspace
?.
apiKeyConfigured
?
ui
.
changeApiKey
:
ui
.
apiKeyPlaceholder
}
onChange=
{
(
event
)
=>
setApiKeyDraft
(
event
.
target
.
value
)
}
/></
label
><
label
>
{
ui
.
workspacePath
}
<
input
value=
{
workspacePathDraft
}
onChange=
{
(
event
)
=>
setWorkspacePathDraft
(
event
.
target
.
value
)
}
/></
label
></
div
><
div
className=
"button-row"
><
button
disabled=
{
saving
}
onClick=
{
()
=>
void
saveConfig
(
apiKeyDraft
)
}
>
{
saving
?
ui
.
saving
:
ui
.
save
}
</
button
></
div
><
div
className=
"mini-info"
><
span
>
{
ui
.
currentBinding
}
</
span
><
strong
>
{
workspace
?.
apiKeyConfigured
?
ui
.
bound
:
ui
.
unbound
}
</
strong
></
div
></
section
><
section
className=
"panel settings-panel"
><
div
className=
"section-head compact"
><
div
><
h3
>
{
ui
.
diagnostics
}
</
h3
><
p
>
{
ui
.
diagnosticsDesc
}
</
p
></
div
></
div
><
div
className=
"mini-info"
><
span
>
{
ui
.
workspacePath
}
</
span
><
strong
>
{
config
?.
workspacePath
||
workspacePathDraft
||
ui
.
none
}
</
strong
></
div
><
div
className=
"button-row"
><
button
className=
"secondary"
onClick=
{
()
=>
void
exportDiagnostics
()
}
>
{
ui
.
export
}
</
button
></
div
></
section
></
div
>
:
null
}
</
main
>
</
main
>
...
...
docs/cloud-skill-flow.md
0 → 100644
View file @
0ab5ca4c
This diff is collapsed.
Click to expand it.
docs/cloud-skill-flow.zh-CN.md
0 → 100644
View file @
0ab5ca4c
This diff is collapsed.
Click to expand it.
packages/shared-types/src/index.ts
View file @
0ab5ca4c
...
@@ -54,6 +54,7 @@ export type RuntimeTelemetryState = "idle" | "running" | "stopped" | "error";
...
@@ -54,6 +54,7 @@ export type RuntimeTelemetryState = "idle" | "running" | "stopped" | "error";
export
type
RuntimeCloudEventType
=
"startup"
|
"shutdown"
|
"message_sent"
|
"message_received"
|
"error"
|
"config_updated"
;
export
type
RuntimeCloudEventType
=
"startup"
|
"shutdown"
|
"message_sent"
|
"message_received"
|
"error"
|
"config_updated"
;
export
type
PluginStatus
=
"included"
|
"extension"
|
"unavailable"
;
export
type
PluginStatus
=
"included"
|
"extension"
|
"unavailable"
;
export
type
ChatLaunchState
=
"unbound"
|
"starting"
|
"ready"
|
"error"
;
export
type
ChatLaunchState
=
"unbound"
|
"starting"
|
"ready"
|
"error"
;
export
type
SkillDownloadState
=
"pending"
|
"downloading"
|
"ready"
|
"failed"
|
"removed"
;
export
interface
GatewayStatus
{
export
interface
GatewayStatus
{
state
:
GatewayState
;
state
:
GatewayState
;
...
@@ -200,6 +201,12 @@ export interface WorkspaceSkillSummary {
...
@@ -200,6 +201,12 @@ export interface WorkspaceSkillSummary {
description
:
string
;
description
:
string
;
category
:
string
;
category
:
string
;
enabled
:
boolean
;
enabled
:
boolean
;
ready
:
boolean
;
downloadState
:
SkillDownloadState
;
fileName
?:
string
;
fileSize
?:
number
;
lastSyncedAt
?:
string
;
lastError
?:
string
;
}
}
export
interface
PluginSummary
{
export
interface
PluginSummary
{
...
@@ -388,6 +395,12 @@ export interface SkillSummary {
...
@@ -388,6 +395,12 @@ export interface SkillSummary {
description
:
string
;
description
:
string
;
category
:
string
;
category
:
string
;
enabled
:
boolean
;
enabled
:
boolean
;
ready
:
boolean
;
downloadState
:
SkillDownloadState
;
fileName
?:
string
;
fileSize
?:
number
;
lastSyncedAt
?:
string
;
lastError
?:
string
;
requiresCredits
?:
number
;
requiresCredits
?:
number
;
}
}
...
...
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