ccnew 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +78 -0
- package/core/desktop-service.js +403 -0
- package/core/desktop-state.js +1021 -0
- package/core/index.js +1468 -0
- package/core/paths.js +99 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/routing.js +334 -0
- package/core/store.js +218 -0
- package/core/utils.js +225 -0
- package/core/writers/codex.js +102 -0
- package/core/writers/index.js +16 -0
- package/core/writers/openclaw.js +93 -0
- package/core/writers/opencode.js +91 -0
- package/desktop/assets/fml-icon.png +0 -0
- package/desktop/assets/march-mark.svg +26 -0
- package/desktop/main.js +275 -0
- package/desktop/preload.cjs +67 -0
- package/desktop/preload.js +49 -0
- package/desktop/renderer/app.js +327 -0
- package/desktop/renderer/index.html +130 -0
- package/desktop/renderer/styles.css +490 -0
- package/package.json +111 -0
- package/scripts/build-web.mjs +95 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/desktop-pack-win.mjs +81 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/prepublish-check.mjs +57 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/fml-icon.png +0 -0
- package/site/assets/march-mark.svg +26 -0
- package/site/index.html +337 -0
- package/site/styles.css +840 -0
- package/src/App.tsx +1557 -0
- package/src/components/layout/app-sidebar.tsx +103 -0
- package/src/components/layout/top-toolbar.tsx +44 -0
- package/src/components/layout/workspace-tabs.tsx +32 -0
- package/src/components/providers/inspector-panel.tsx +84 -0
- package/src/components/providers/metric-strip.tsx +26 -0
- package/src/components/providers/provider-editor.tsx +87 -0
- package/src/components/providers/provider-table.tsx +85 -0
- package/src/components/ui/logo-mark.tsx +32 -0
- package/src/features/mcp/mcp-view.tsx +45 -0
- package/src/features/prompts/prompts-view.tsx +40 -0
- package/src/features/providers/providers-view.tsx +40 -0
- package/src/features/providers/types.ts +26 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +235 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +726 -0
- package/src/lib/query-client.ts +3 -0
- package/src/lib/workspace-sections.ts +34 -0
- package/src/main.tsx +14 -0
- package/src/types.ts +137 -0
- package/src/vite-env.d.ts +64 -0
- package/src-tauri/README.md +11 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppSnapshot,
|
|
3
|
+
McpServer,
|
|
4
|
+
PlatformId,
|
|
5
|
+
PlatformRouting,
|
|
6
|
+
PlatformSnapshot,
|
|
7
|
+
PresetProfile,
|
|
8
|
+
ProbeResult,
|
|
9
|
+
PromptProfile,
|
|
10
|
+
Provider,
|
|
11
|
+
RoutingSnapshot,
|
|
12
|
+
SkillProfile,
|
|
13
|
+
SkillRepo
|
|
14
|
+
} from "../types";
|
|
15
|
+
|
|
16
|
+
const STORAGE_KEY = "fml-control-state";
|
|
17
|
+
const DEFAULT_MODELS = ["gpt-5.4", "gpt-5.3-codex", "gpt-5.2", "gpt-5.2-codex", "gpt-5.1-codex-mini", "gpt-5.1-codex-max"];
|
|
18
|
+
const DEFAULT_PLATFORMS: PlatformId[] = ["codex", "opencode", "openclaw"];
|
|
19
|
+
|
|
20
|
+
type StoredProvider = Omit<Provider, "maskedApiKey"> & { apiKey: string };
|
|
21
|
+
type StoredPlatform = Omit<PlatformSnapshot, "providers"> & { providers: StoredProvider[] };
|
|
22
|
+
type StoredSnapshot = Omit<AppSnapshot, "platforms"> & { platforms: StoredPlatform[] };
|
|
23
|
+
|
|
24
|
+
type DesktopBridge = NonNullable<Window["marchDesktop"]>;
|
|
25
|
+
|
|
26
|
+
type DesktopSnapshotResponse = {
|
|
27
|
+
appName?: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
generatedAt?: string;
|
|
30
|
+
models?: Array<string | { id?: string }>;
|
|
31
|
+
platforms: PlatformSnapshot[];
|
|
32
|
+
mcpServers?: McpServer[];
|
|
33
|
+
prompts?: PromptProfile[];
|
|
34
|
+
skills?: SkillProfile[];
|
|
35
|
+
skillRepos?: SkillRepo[];
|
|
36
|
+
presets?: PresetProfile[];
|
|
37
|
+
routing?: RoutingSnapshot;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ProbePlatformResponse = {
|
|
41
|
+
snapshot?: AppSnapshot;
|
|
42
|
+
results: ProbeResult[];
|
|
43
|
+
best?: ProbeResult | null;
|
|
44
|
+
failover?: {
|
|
45
|
+
previousProviderId?: string;
|
|
46
|
+
nextProviderId?: string;
|
|
47
|
+
} | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ProbeCandidateResponse = {
|
|
51
|
+
result: ProbeResult | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type OpenPathResponse = {
|
|
55
|
+
ok: true;
|
|
56
|
+
targetPath: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ImportExportPresetResponse = {
|
|
60
|
+
canceled?: boolean;
|
|
61
|
+
filePath?: string;
|
|
62
|
+
targetPath?: string;
|
|
63
|
+
count?: number;
|
|
64
|
+
snapshot?: AppSnapshot;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function now() {
|
|
68
|
+
return new Date().toISOString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function maskApiKey(apiKey: string) {
|
|
72
|
+
if (!apiKey) {
|
|
73
|
+
return "(empty)";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (apiKey.length <= 8) {
|
|
77
|
+
return `${apiKey.slice(0, 2)}***${apiKey.slice(-2)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${apiKey.slice(0, 4)}***${apiKey.slice(-4)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeModels(models?: string[]) {
|
|
84
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
85
|
+
return [...DEFAULT_MODELS];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return [...new Set(models.map((item) => `${item || ""}`.trim()).filter(Boolean))];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getDesktopBridge(): DesktopBridge | null {
|
|
92
|
+
if (typeof window === "undefined") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return window.marchDesktop ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createStoredProvider(name: string, baseUrl: string, model: string, apiKey: string, isActive: boolean): StoredProvider {
|
|
100
|
+
return {
|
|
101
|
+
id: `${name.toLowerCase()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
102
|
+
name,
|
|
103
|
+
baseUrl,
|
|
104
|
+
model,
|
|
105
|
+
apiKey,
|
|
106
|
+
isActive,
|
|
107
|
+
createdAt: now(),
|
|
108
|
+
updatedAt: now(),
|
|
109
|
+
health: "unknown",
|
|
110
|
+
lastLatency: null,
|
|
111
|
+
lastCheckedAt: null,
|
|
112
|
+
lastError: null,
|
|
113
|
+
failoverRank: isActive ? 0 : 1,
|
|
114
|
+
failoverRole: isActive ? "primary" : "fallback",
|
|
115
|
+
costTier: "medium"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createRouting(platforms: StoredPlatform[]): RoutingSnapshot {
|
|
120
|
+
const platformStates: PlatformRouting[] = platforms.map((platform) => {
|
|
121
|
+
const activeProvider = platform.providers.find((provider) => provider.isActive) || platform.providers[0] || null;
|
|
122
|
+
return {
|
|
123
|
+
platform: platform.id,
|
|
124
|
+
proxyMode: "provider-base-url",
|
|
125
|
+
autoFailover: false,
|
|
126
|
+
failoverThresholdMs: 1800,
|
|
127
|
+
maxConsecutiveFailures: 2,
|
|
128
|
+
primaryProviderId: activeProvider?.id || null,
|
|
129
|
+
fallbackProviderIds: platform.providers.filter((provider) => provider.id !== activeProvider?.id).map((provider) => provider.id),
|
|
130
|
+
lastFailoverAt: null,
|
|
131
|
+
providerStates: platform.providers.map((provider, index) => ({
|
|
132
|
+
providerId: provider.id,
|
|
133
|
+
failoverRank: provider.id === activeProvider?.id ? 0 : index + 1,
|
|
134
|
+
role: provider.id === activeProvider?.id ? "primary" : "fallback",
|
|
135
|
+
health: provider.health,
|
|
136
|
+
lastLatency: provider.lastLatency,
|
|
137
|
+
lastCheckedAt: provider.lastCheckedAt,
|
|
138
|
+
lastError: provider.lastError,
|
|
139
|
+
consecutiveFailures: 0,
|
|
140
|
+
costTier: provider.costTier
|
|
141
|
+
}))
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
budgetMode: "tiered",
|
|
147
|
+
monthlyBudgetUsd: null,
|
|
148
|
+
platforms: platformStates
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createInitialState(): StoredSnapshot {
|
|
153
|
+
const platforms: StoredPlatform[] = [
|
|
154
|
+
{
|
|
155
|
+
id: "codex",
|
|
156
|
+
label: "Codex",
|
|
157
|
+
currentProviderName: "fhl",
|
|
158
|
+
providerCount: 1,
|
|
159
|
+
targetFiles: ["~/.codex/config.toml", "~/.codex/auth.json"],
|
|
160
|
+
providers: [createStoredProvider("fhl", "https://www.fhl.mom", "gpt-5.4", "", true)]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "opencode",
|
|
164
|
+
label: "OpenCode",
|
|
165
|
+
currentProviderName: "fhl",
|
|
166
|
+
providerCount: 1,
|
|
167
|
+
targetFiles: ["~/.config/opencode/opencode.json", "~/.config/opencode/AGENTS.md"],
|
|
168
|
+
providers: [createStoredProvider("fhl", "https://www.fhl.mom", "gpt-5.4", "", true)]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "openclaw",
|
|
172
|
+
label: "OpenClaw",
|
|
173
|
+
currentProviderName: "fhl",
|
|
174
|
+
providerCount: 1,
|
|
175
|
+
targetFiles: ["~/.openclaw/openclaw.json", "~/.openclaw/agents/main/agent/models.json"],
|
|
176
|
+
providers: [createStoredProvider("fhl", "https://www.fhl.mom/v1", "gpt-5.4", "", true)]
|
|
177
|
+
}
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
appName: "ccon",
|
|
182
|
+
version: "0.1.10",
|
|
183
|
+
generatedAt: now(),
|
|
184
|
+
models: [...DEFAULT_MODELS],
|
|
185
|
+
platforms,
|
|
186
|
+
mcpServers: [],
|
|
187
|
+
prompts: [],
|
|
188
|
+
skills: [],
|
|
189
|
+
skillRepos: [],
|
|
190
|
+
presets: [],
|
|
191
|
+
routing: createRouting(platforms)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadStoredState(): StoredSnapshot {
|
|
196
|
+
const fallback = createInitialState();
|
|
197
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
198
|
+
if (!raw) {
|
|
199
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(fallback));
|
|
200
|
+
return fallback;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const parsed = JSON.parse(raw) as Partial<StoredSnapshot>;
|
|
205
|
+
return {
|
|
206
|
+
...fallback,
|
|
207
|
+
...parsed,
|
|
208
|
+
models: sanitizeModels(parsed.models ?? fallback.models),
|
|
209
|
+
platforms: Array.isArray(parsed.platforms) ? parsed.platforms : fallback.platforms,
|
|
210
|
+
mcpServers: Array.isArray(parsed.mcpServers) ? parsed.mcpServers : fallback.mcpServers,
|
|
211
|
+
prompts: Array.isArray(parsed.prompts) ? parsed.prompts : fallback.prompts,
|
|
212
|
+
skills: Array.isArray(parsed.skills) ? parsed.skills : fallback.skills,
|
|
213
|
+
skillRepos: Array.isArray(parsed.skillRepos) ? parsed.skillRepos : fallback.skillRepos,
|
|
214
|
+
presets: Array.isArray(parsed.presets) ? parsed.presets : fallback.presets,
|
|
215
|
+
routing: parsed.routing ?? fallback.routing
|
|
216
|
+
};
|
|
217
|
+
} catch {
|
|
218
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(fallback));
|
|
219
|
+
return fallback;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function saveStoredState(snapshot: StoredSnapshot) {
|
|
224
|
+
localStorage.setItem(
|
|
225
|
+
STORAGE_KEY,
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
...snapshot,
|
|
228
|
+
generatedAt: now(),
|
|
229
|
+
models: sanitizeModels(snapshot.models),
|
|
230
|
+
platforms: snapshot.platforms.map((platform) => ({
|
|
231
|
+
...platform,
|
|
232
|
+
providerCount: platform.providers.length,
|
|
233
|
+
currentProviderName: platform.providers.find((provider) => provider.isActive)?.name || null
|
|
234
|
+
}))
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toPublic(snapshot: StoredSnapshot): AppSnapshot {
|
|
240
|
+
return {
|
|
241
|
+
...snapshot,
|
|
242
|
+
models: sanitizeModels(snapshot.models),
|
|
243
|
+
platforms: snapshot.platforms.map((platform) => ({
|
|
244
|
+
...platform,
|
|
245
|
+
providerCount: platform.providers.length,
|
|
246
|
+
currentProviderName: platform.providers.find((provider) => provider.isActive)?.name || null,
|
|
247
|
+
providers: platform.providers.map((provider) => ({
|
|
248
|
+
...provider,
|
|
249
|
+
maskedApiKey: maskApiKey(provider.apiKey)
|
|
250
|
+
}))
|
|
251
|
+
}))
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function toStoredPlatform(platform: PlatformSnapshot): StoredPlatform {
|
|
256
|
+
return {
|
|
257
|
+
...platform,
|
|
258
|
+
providers: platform.providers.map((provider) => ({
|
|
259
|
+
...provider,
|
|
260
|
+
apiKey: ""
|
|
261
|
+
}))
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function persistPublicSnapshot(snapshot: AppSnapshot) {
|
|
266
|
+
const current = loadStoredState();
|
|
267
|
+
const next: StoredSnapshot = {
|
|
268
|
+
...current,
|
|
269
|
+
...snapshot,
|
|
270
|
+
platforms: snapshot.platforms.map(toStoredPlatform)
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
saveStoredState(next);
|
|
274
|
+
return snapshot;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function getSnapshotInternal(): Promise<AppSnapshot> {
|
|
278
|
+
const bridge = getDesktopBridge();
|
|
279
|
+
if (bridge) {
|
|
280
|
+
const response = (await bridge.getSnapshot()) as DesktopSnapshotResponse;
|
|
281
|
+
const snapshot: AppSnapshot = {
|
|
282
|
+
appName: response.appName || "ccon",
|
|
283
|
+
version: response.version || "0.1.10",
|
|
284
|
+
generatedAt: response.generatedAt || now(),
|
|
285
|
+
models: sanitizeModels((response.models || []).map((item) => (typeof item === "string" ? item : item?.id || "")).filter(Boolean)),
|
|
286
|
+
platforms: response.platforms || [],
|
|
287
|
+
mcpServers: response.mcpServers || [],
|
|
288
|
+
prompts: response.prompts || [],
|
|
289
|
+
skills: response.skills || [],
|
|
290
|
+
skillRepos: response.skillRepos || [],
|
|
291
|
+
presets: response.presets || [],
|
|
292
|
+
routing:
|
|
293
|
+
response.routing ||
|
|
294
|
+
createRouting(
|
|
295
|
+
(response.platforms || []).map((platform) => toStoredPlatform(platform))
|
|
296
|
+
)
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
persistPublicSnapshot(snapshot);
|
|
300
|
+
return snapshot;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return toPublic(loadStoredState());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function getLocalPlatform(snapshot: StoredSnapshot, platformId: PlatformId) {
|
|
307
|
+
const platform = snapshot.platforms.find((item) => item.id === platformId);
|
|
308
|
+
if (!platform) {
|
|
309
|
+
throw new Error(`Platform not found: ${platformId}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return platform;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function setLocalActiveProvider(platform: StoredPlatform, providerId: string) {
|
|
316
|
+
platform.providers = platform.providers.map((provider) => ({
|
|
317
|
+
...provider,
|
|
318
|
+
isActive: provider.id === providerId,
|
|
319
|
+
failoverRole: provider.id === providerId ? "primary" : "fallback",
|
|
320
|
+
failoverRank: provider.id === providerId ? 0 : provider.failoverRank || 1
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function updateLocalRouting(snapshot: StoredSnapshot, platformId: PlatformId) {
|
|
325
|
+
const platform = getLocalPlatform(snapshot, platformId);
|
|
326
|
+
const routingPlatform = snapshot.routing.platforms.find((item) => item.platform === platformId);
|
|
327
|
+
const active = platform.providers.find((provider) => provider.isActive) || platform.providers[0] || null;
|
|
328
|
+
|
|
329
|
+
if (!routingPlatform) {
|
|
330
|
+
snapshot.routing = createRouting(snapshot.platforms);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
routingPlatform.primaryProviderId = active?.id || null;
|
|
335
|
+
routingPlatform.fallbackProviderIds = platform.providers.filter((provider) => provider.id !== active?.id).map((provider) => provider.id);
|
|
336
|
+
routingPlatform.providerStates = platform.providers.map((provider, index) => ({
|
|
337
|
+
providerId: provider.id,
|
|
338
|
+
failoverRank: provider.id === active?.id ? 0 : index + 1,
|
|
339
|
+
role: provider.id === active?.id ? "primary" : "fallback",
|
|
340
|
+
health: provider.health,
|
|
341
|
+
lastLatency: provider.lastLatency,
|
|
342
|
+
lastCheckedAt: provider.lastCheckedAt,
|
|
343
|
+
lastError: provider.lastError,
|
|
344
|
+
consecutiveFailures: 0,
|
|
345
|
+
costTier: provider.costTier
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export const client = {
|
|
350
|
+
getSnapshot: getSnapshotInternal,
|
|
351
|
+
|
|
352
|
+
async saveProvider(input: { platform: PlatformId; name: string; baseUrl: string; apiKey: string; model: string }) {
|
|
353
|
+
const bridge = getDesktopBridge();
|
|
354
|
+
if (bridge) {
|
|
355
|
+
await bridge.saveProvider(input);
|
|
356
|
+
return getSnapshotInternal();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const snapshot = loadStoredState();
|
|
360
|
+
const platform = getLocalPlatform(snapshot, input.platform);
|
|
361
|
+
const match = platform.providers.find((provider) => provider.name.trim().toLowerCase() === input.name.trim().toLowerCase());
|
|
362
|
+
if (match) {
|
|
363
|
+
match.baseUrl = input.baseUrl;
|
|
364
|
+
match.apiKey = input.apiKey;
|
|
365
|
+
match.model = input.model;
|
|
366
|
+
match.updatedAt = now();
|
|
367
|
+
setLocalActiveProvider(platform, match.id);
|
|
368
|
+
} else {
|
|
369
|
+
const created = createStoredProvider(input.name, input.baseUrl, input.model, input.apiKey, true);
|
|
370
|
+
platform.providers.unshift(created);
|
|
371
|
+
setLocalActiveProvider(platform, created.id);
|
|
372
|
+
}
|
|
373
|
+
updateLocalRouting(snapshot, input.platform);
|
|
374
|
+
saveStoredState(snapshot);
|
|
375
|
+
return toPublic(snapshot);
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
async activateProvider(input: { platform: PlatformId; providerId: string }) {
|
|
379
|
+
const bridge = getDesktopBridge();
|
|
380
|
+
if (bridge) {
|
|
381
|
+
await bridge.activateProvider(input);
|
|
382
|
+
return getSnapshotInternal();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const snapshot = loadStoredState();
|
|
386
|
+
const platform = getLocalPlatform(snapshot, input.platform);
|
|
387
|
+
setLocalActiveProvider(platform, input.providerId);
|
|
388
|
+
updateLocalRouting(snapshot, input.platform);
|
|
389
|
+
saveStoredState(snapshot);
|
|
390
|
+
return toPublic(snapshot);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
async probePlatform(platformId: PlatformId): Promise<ProbePlatformResponse> {
|
|
394
|
+
const bridge = getDesktopBridge();
|
|
395
|
+
if (bridge) {
|
|
396
|
+
const response = (await bridge.probePlatform({ platform: platformId })) as ProbePlatformResponse;
|
|
397
|
+
if (response.snapshot) {
|
|
398
|
+
persistPublicSnapshot(response.snapshot);
|
|
399
|
+
} else {
|
|
400
|
+
await getSnapshotInternal();
|
|
401
|
+
}
|
|
402
|
+
return response;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const snapshot = loadStoredState();
|
|
406
|
+
const platform = getLocalPlatform(snapshot, platformId);
|
|
407
|
+
const results = platform.providers.map((provider, index) => ({
|
|
408
|
+
baseUrl: provider.baseUrl,
|
|
409
|
+
ok: true,
|
|
410
|
+
latency: 90 + index * 33
|
|
411
|
+
}));
|
|
412
|
+
platform.providers = platform.providers.map((provider, index) => ({
|
|
413
|
+
...provider,
|
|
414
|
+
health: "healthy",
|
|
415
|
+
lastLatency: results[index]?.latency ?? null,
|
|
416
|
+
lastCheckedAt: now()
|
|
417
|
+
}));
|
|
418
|
+
updateLocalRouting(snapshot, platformId);
|
|
419
|
+
saveStoredState(snapshot);
|
|
420
|
+
return {
|
|
421
|
+
results,
|
|
422
|
+
best: results[0] || null,
|
|
423
|
+
snapshot: toPublic(snapshot),
|
|
424
|
+
failover: null
|
|
425
|
+
};
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
async probeCandidate(input: { platform: PlatformId; baseUrl: string }): Promise<ProbeCandidateResponse> {
|
|
429
|
+
const bridge = getDesktopBridge();
|
|
430
|
+
if (bridge) {
|
|
431
|
+
return (await bridge.probeCandidate(input)) as ProbeCandidateResponse;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
result: input.baseUrl.trim()
|
|
436
|
+
? {
|
|
437
|
+
baseUrl: input.baseUrl.trim(),
|
|
438
|
+
ok: true,
|
|
439
|
+
latency: 120
|
|
440
|
+
}
|
|
441
|
+
: null
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
async savePreset(input: { name: string; providerName: string; commonBaseUrl: string; openclawBaseUrl: string; model: string }) {
|
|
446
|
+
const bridge = getDesktopBridge();
|
|
447
|
+
if (bridge) {
|
|
448
|
+
await bridge.savePreset(input);
|
|
449
|
+
return getSnapshotInternal();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const snapshot = loadStoredState();
|
|
453
|
+
const payload: PresetProfile = {
|
|
454
|
+
...input,
|
|
455
|
+
source: "custom",
|
|
456
|
+
readonly: false,
|
|
457
|
+
createdAt: now(),
|
|
458
|
+
updatedAt: now()
|
|
459
|
+
};
|
|
460
|
+
const index = snapshot.presets.findIndex((preset) => preset.name.toLowerCase() === input.name.trim().toLowerCase());
|
|
461
|
+
if (index >= 0) {
|
|
462
|
+
snapshot.presets[index] = { ...snapshot.presets[index], ...payload, updatedAt: now() };
|
|
463
|
+
} else {
|
|
464
|
+
snapshot.presets.unshift(payload);
|
|
465
|
+
}
|
|
466
|
+
saveStoredState(snapshot);
|
|
467
|
+
return toPublic(snapshot);
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
async deletePreset(input: { name: string }) {
|
|
471
|
+
const bridge = getDesktopBridge();
|
|
472
|
+
if (bridge) {
|
|
473
|
+
await bridge.deletePreset(input);
|
|
474
|
+
return getSnapshotInternal();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const snapshot = loadStoredState();
|
|
478
|
+
snapshot.presets = snapshot.presets.filter((preset) => preset.name.toLowerCase() !== input.name.trim().toLowerCase());
|
|
479
|
+
saveStoredState(snapshot);
|
|
480
|
+
return toPublic(snapshot);
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
async applyPreset(input: { name: string }) {
|
|
484
|
+
const bridge = getDesktopBridge();
|
|
485
|
+
if (bridge) {
|
|
486
|
+
await bridge.applyPreset(input);
|
|
487
|
+
return getSnapshotInternal();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const snapshot = loadStoredState();
|
|
491
|
+
const preset = snapshot.presets.find((item) => item.name.toLowerCase() === input.name.trim().toLowerCase());
|
|
492
|
+
if (!preset) {
|
|
493
|
+
throw new Error("Preset not found");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for (const platform of DEFAULT_PLATFORMS) {
|
|
497
|
+
const target = getLocalPlatform(snapshot, platform);
|
|
498
|
+
const active = target.providers.find((provider) => provider.isActive) || target.providers[0] || null;
|
|
499
|
+
const apiKey = active?.apiKey || "";
|
|
500
|
+
if (!apiKey) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const provider = createStoredProvider(
|
|
504
|
+
preset.providerName,
|
|
505
|
+
platform === "openclaw" ? preset.openclawBaseUrl : preset.commonBaseUrl,
|
|
506
|
+
preset.model,
|
|
507
|
+
apiKey,
|
|
508
|
+
true
|
|
509
|
+
);
|
|
510
|
+
target.providers = [provider, ...target.providers.filter((item) => item.name.toLowerCase() !== preset.providerName.toLowerCase())];
|
|
511
|
+
setLocalActiveProvider(target, provider.id);
|
|
512
|
+
updateLocalRouting(snapshot, platform);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
saveStoredState(snapshot);
|
|
516
|
+
return toPublic(snapshot);
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
async importPresets(): Promise<ImportExportPresetResponse> {
|
|
520
|
+
const bridge = getDesktopBridge();
|
|
521
|
+
if (bridge) {
|
|
522
|
+
const response = (await bridge.importPresets()) as ImportExportPresetResponse;
|
|
523
|
+
await getSnapshotInternal();
|
|
524
|
+
return response;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return { canceled: true };
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
async exportPresets(): Promise<ImportExportPresetResponse> {
|
|
531
|
+
const bridge = getDesktopBridge();
|
|
532
|
+
if (bridge) {
|
|
533
|
+
return (await bridge.exportPresets()) as ImportExportPresetResponse;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return { canceled: true };
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
async updateRouting(input: {
|
|
540
|
+
platform: PlatformId;
|
|
541
|
+
autoFailover?: boolean;
|
|
542
|
+
failoverThresholdMs?: number;
|
|
543
|
+
maxConsecutiveFailures?: number;
|
|
544
|
+
monthlyBudgetUsd?: number | null;
|
|
545
|
+
budgetMode?: "tiered" | "manual";
|
|
546
|
+
}) {
|
|
547
|
+
const bridge = getDesktopBridge();
|
|
548
|
+
if (bridge) {
|
|
549
|
+
await bridge.updateRouting(input);
|
|
550
|
+
return getSnapshotInternal();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const snapshot = loadStoredState();
|
|
554
|
+
const routingPlatform = snapshot.routing.platforms.find((item) => item.platform === input.platform);
|
|
555
|
+
if (routingPlatform) {
|
|
556
|
+
routingPlatform.autoFailover = typeof input.autoFailover === "boolean" ? input.autoFailover : routingPlatform.autoFailover;
|
|
557
|
+
routingPlatform.failoverThresholdMs = input.failoverThresholdMs ?? routingPlatform.failoverThresholdMs;
|
|
558
|
+
routingPlatform.maxConsecutiveFailures = input.maxConsecutiveFailures ?? routingPlatform.maxConsecutiveFailures;
|
|
559
|
+
}
|
|
560
|
+
if (input.monthlyBudgetUsd !== undefined) {
|
|
561
|
+
snapshot.routing.monthlyBudgetUsd = input.monthlyBudgetUsd;
|
|
562
|
+
}
|
|
563
|
+
if (input.budgetMode) {
|
|
564
|
+
snapshot.routing.budgetMode = input.budgetMode;
|
|
565
|
+
}
|
|
566
|
+
saveStoredState(snapshot);
|
|
567
|
+
return toPublic(snapshot);
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
async toggleMcpServer(input: { serverId: string; platform: PlatformId }) {
|
|
571
|
+
const bridge = getDesktopBridge();
|
|
572
|
+
if (bridge) {
|
|
573
|
+
await bridge.toggleMcp(input);
|
|
574
|
+
return getSnapshotInternal();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const snapshot = loadStoredState();
|
|
578
|
+
snapshot.mcpServers = snapshot.mcpServers.map((server) =>
|
|
579
|
+
server.id !== input.serverId
|
|
580
|
+
? server
|
|
581
|
+
: {
|
|
582
|
+
...server,
|
|
583
|
+
enabledPlatforms: server.enabledPlatforms.includes(input.platform)
|
|
584
|
+
? server.enabledPlatforms.filter((platform) => platform !== input.platform)
|
|
585
|
+
: [...server.enabledPlatforms, input.platform]
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
saveStoredState(snapshot);
|
|
589
|
+
return toPublic(snapshot);
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
async upsertMcp(
|
|
593
|
+
input: McpServer & {
|
|
594
|
+
envText?: string;
|
|
595
|
+
headersText?: string;
|
|
596
|
+
}
|
|
597
|
+
) {
|
|
598
|
+
const bridge = getDesktopBridge();
|
|
599
|
+
if (bridge) {
|
|
600
|
+
await bridge.upsertMcp(input);
|
|
601
|
+
return getSnapshotInternal();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const snapshot = loadStoredState();
|
|
605
|
+
const index = snapshot.mcpServers.findIndex((item) => item.id === input.id);
|
|
606
|
+
if (index >= 0) {
|
|
607
|
+
snapshot.mcpServers[index] = input;
|
|
608
|
+
} else {
|
|
609
|
+
snapshot.mcpServers.unshift(input);
|
|
610
|
+
}
|
|
611
|
+
saveStoredState(snapshot);
|
|
612
|
+
return toPublic(snapshot);
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
async deleteMcp(input: { serverId: string }) {
|
|
616
|
+
const bridge = getDesktopBridge();
|
|
617
|
+
if (bridge) {
|
|
618
|
+
await bridge.deleteMcp(input);
|
|
619
|
+
return getSnapshotInternal();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const snapshot = loadStoredState();
|
|
623
|
+
snapshot.mcpServers = snapshot.mcpServers.filter((item) => item.id !== input.serverId);
|
|
624
|
+
saveStoredState(snapshot);
|
|
625
|
+
return toPublic(snapshot);
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
async togglePrompt(input: { promptId: string }) {
|
|
629
|
+
const bridge = getDesktopBridge();
|
|
630
|
+
if (bridge) {
|
|
631
|
+
await bridge.togglePrompt(input);
|
|
632
|
+
return getSnapshotInternal();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const snapshot = loadStoredState();
|
|
636
|
+
snapshot.prompts = snapshot.prompts.map((prompt) =>
|
|
637
|
+
prompt.id === input.promptId ? { ...prompt, enabled: !prompt.enabled, updatedAt: now() } : prompt
|
|
638
|
+
);
|
|
639
|
+
saveStoredState(snapshot);
|
|
640
|
+
return toPublic(snapshot);
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
async upsertPrompt(input: PromptProfile) {
|
|
644
|
+
const bridge = getDesktopBridge();
|
|
645
|
+
if (bridge) {
|
|
646
|
+
await bridge.upsertPrompt(input);
|
|
647
|
+
return getSnapshotInternal();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const snapshot = loadStoredState();
|
|
651
|
+
const index = snapshot.prompts.findIndex((item) => item.id === input.id);
|
|
652
|
+
if (index >= 0) {
|
|
653
|
+
snapshot.prompts[index] = input;
|
|
654
|
+
} else {
|
|
655
|
+
snapshot.prompts.unshift(input);
|
|
656
|
+
}
|
|
657
|
+
saveStoredState(snapshot);
|
|
658
|
+
return toPublic(snapshot);
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
async deletePrompt(input: { promptId: string }) {
|
|
662
|
+
const bridge = getDesktopBridge();
|
|
663
|
+
if (bridge) {
|
|
664
|
+
await bridge.deletePrompt(input);
|
|
665
|
+
return getSnapshotInternal();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const snapshot = loadStoredState();
|
|
669
|
+
snapshot.prompts = snapshot.prompts.filter((item) => item.id !== input.promptId);
|
|
670
|
+
saveStoredState(snapshot);
|
|
671
|
+
return toPublic(snapshot);
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
async toggleSkillRepo(input: { repoId: string }) {
|
|
675
|
+
const bridge = getDesktopBridge();
|
|
676
|
+
if (bridge) {
|
|
677
|
+
await bridge.toggleSkillRepo(input);
|
|
678
|
+
return getSnapshotInternal();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const snapshot = loadStoredState();
|
|
682
|
+
snapshot.skillRepos = snapshot.skillRepos.map((repo) => (repo.id === input.repoId ? { ...repo, enabled: !repo.enabled } : repo));
|
|
683
|
+
saveStoredState(snapshot);
|
|
684
|
+
return toPublic(snapshot);
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
async upsertSkill(input: SkillProfile) {
|
|
688
|
+
const bridge = getDesktopBridge();
|
|
689
|
+
if (bridge) {
|
|
690
|
+
await bridge.upsertSkill(input);
|
|
691
|
+
return getSnapshotInternal();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const snapshot = loadStoredState();
|
|
695
|
+
const index = snapshot.skills.findIndex((item) => item.id === input.id);
|
|
696
|
+
if (index >= 0) {
|
|
697
|
+
snapshot.skills[index] = input;
|
|
698
|
+
} else {
|
|
699
|
+
snapshot.skills.unshift(input);
|
|
700
|
+
}
|
|
701
|
+
saveStoredState(snapshot);
|
|
702
|
+
return toPublic(snapshot);
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
async deleteSkill(input: { skillId: string }) {
|
|
706
|
+
const bridge = getDesktopBridge();
|
|
707
|
+
if (bridge) {
|
|
708
|
+
await bridge.deleteSkill(input);
|
|
709
|
+
return getSnapshotInternal();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const snapshot = loadStoredState();
|
|
713
|
+
snapshot.skills = snapshot.skills.filter((item) => item.id !== input.skillId);
|
|
714
|
+
saveStoredState(snapshot);
|
|
715
|
+
return toPublic(snapshot);
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
async openPath(input: { targetPath: string }) {
|
|
719
|
+
const bridge = getDesktopBridge();
|
|
720
|
+
if (!bridge) {
|
|
721
|
+
throw new Error("当前模式不支持打开文件位置");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return (await bridge.openPath(input.targetPath)) as OpenPathResponse;
|
|
725
|
+
}
|
|
726
|
+
};
|