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.
Files changed (62) hide show
  1. package/README.md +107 -0
  2. package/build/icon.ico +0 -0
  3. package/build/icon.png +0 -0
  4. package/core/apply.js +152 -0
  5. package/core/backup.js +53 -0
  6. package/core/constants.js +78 -0
  7. package/core/desktop-service.js +403 -0
  8. package/core/desktop-state.js +1021 -0
  9. package/core/index.js +1468 -0
  10. package/core/paths.js +99 -0
  11. package/core/presets.js +171 -0
  12. package/core/probe.js +70 -0
  13. package/core/routing.js +334 -0
  14. package/core/store.js +218 -0
  15. package/core/utils.js +225 -0
  16. package/core/writers/codex.js +102 -0
  17. package/core/writers/index.js +16 -0
  18. package/core/writers/openclaw.js +93 -0
  19. package/core/writers/opencode.js +91 -0
  20. package/desktop/assets/fml-icon.png +0 -0
  21. package/desktop/assets/march-mark.svg +26 -0
  22. package/desktop/main.js +275 -0
  23. package/desktop/preload.cjs +67 -0
  24. package/desktop/preload.js +49 -0
  25. package/desktop/renderer/app.js +327 -0
  26. package/desktop/renderer/index.html +130 -0
  27. package/desktop/renderer/styles.css +490 -0
  28. package/package.json +111 -0
  29. package/scripts/build-web.mjs +95 -0
  30. package/scripts/desktop-dev.mjs +90 -0
  31. package/scripts/desktop-pack-win.mjs +81 -0
  32. package/scripts/postinstall.mjs +49 -0
  33. package/scripts/prepublish-check.mjs +57 -0
  34. package/scripts/serve-site.mjs +51 -0
  35. package/site/app.js +10 -0
  36. package/site/assets/fml-icon.png +0 -0
  37. package/site/assets/march-mark.svg +26 -0
  38. package/site/index.html +337 -0
  39. package/site/styles.css +840 -0
  40. package/src/App.tsx +1557 -0
  41. package/src/components/layout/app-sidebar.tsx +103 -0
  42. package/src/components/layout/top-toolbar.tsx +44 -0
  43. package/src/components/layout/workspace-tabs.tsx +32 -0
  44. package/src/components/providers/inspector-panel.tsx +84 -0
  45. package/src/components/providers/metric-strip.tsx +26 -0
  46. package/src/components/providers/provider-editor.tsx +87 -0
  47. package/src/components/providers/provider-table.tsx +85 -0
  48. package/src/components/ui/logo-mark.tsx +32 -0
  49. package/src/features/mcp/mcp-view.tsx +45 -0
  50. package/src/features/prompts/prompts-view.tsx +40 -0
  51. package/src/features/providers/providers-view.tsx +40 -0
  52. package/src/features/providers/types.ts +26 -0
  53. package/src/features/skills/skills-view.tsx +44 -0
  54. package/src/hooks/use-control-workspace.ts +235 -0
  55. package/src/index.css +22 -0
  56. package/src/lib/client.ts +726 -0
  57. package/src/lib/query-client.ts +3 -0
  58. package/src/lib/workspace-sections.ts +34 -0
  59. package/src/main.tsx +14 -0
  60. package/src/types.ts +137 -0
  61. package/src/vite-env.d.ts +64 -0
  62. 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
+ };