march-control-cli 0.1.3

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 (53) hide show
  1. package/README.md +220 -0
  2. package/core/apply.js +152 -0
  3. package/core/backup.js +53 -0
  4. package/core/constants.js +55 -0
  5. package/core/desktop-service.js +219 -0
  6. package/core/desktop-state.js +511 -0
  7. package/core/index.js +1293 -0
  8. package/core/paths.js +71 -0
  9. package/core/presets.js +171 -0
  10. package/core/probe.js +70 -0
  11. package/core/store.js +218 -0
  12. package/core/utils.js +178 -0
  13. package/core/writers/codex.js +102 -0
  14. package/core/writers/index.js +16 -0
  15. package/core/writers/openclaw.js +93 -0
  16. package/core/writers/opencode.js +91 -0
  17. package/desktop/assets/march-mark.svg +21 -0
  18. package/desktop/main.js +192 -0
  19. package/desktop/preload.js +49 -0
  20. package/desktop/renderer/app.js +327 -0
  21. package/desktop/renderer/index.html +130 -0
  22. package/desktop/renderer/styles.css +413 -0
  23. package/package.json +106 -0
  24. package/scripts/desktop-dev.mjs +90 -0
  25. package/scripts/postinstall.mjs +28 -0
  26. package/scripts/serve-site.mjs +51 -0
  27. package/site/app.js +10 -0
  28. package/site/assets/march-mark.svg +22 -0
  29. package/site/index.html +286 -0
  30. package/site/styles.css +566 -0
  31. package/src/App.tsx +1186 -0
  32. package/src/components/layout/app-sidebar.tsx +103 -0
  33. package/src/components/layout/top-toolbar.tsx +44 -0
  34. package/src/components/layout/workspace-tabs.tsx +32 -0
  35. package/src/components/providers/inspector-panel.tsx +84 -0
  36. package/src/components/providers/metric-strip.tsx +26 -0
  37. package/src/components/providers/provider-editor.tsx +87 -0
  38. package/src/components/providers/provider-table.tsx +85 -0
  39. package/src/components/ui/logo-mark.tsx +16 -0
  40. package/src/features/mcp/mcp-view.tsx +45 -0
  41. package/src/features/prompts/prompts-view.tsx +40 -0
  42. package/src/features/providers/providers-view.tsx +40 -0
  43. package/src/features/providers/types.ts +8 -0
  44. package/src/features/skills/skills-view.tsx +44 -0
  45. package/src/hooks/use-control-workspace.ts +184 -0
  46. package/src/index.css +22 -0
  47. package/src/lib/client.ts +944 -0
  48. package/src/lib/query-client.ts +3 -0
  49. package/src/lib/workspace-sections.ts +34 -0
  50. package/src/main.tsx +14 -0
  51. package/src/types.ts +76 -0
  52. package/src/vite-env.d.ts +56 -0
  53. package/src-tauri/README.md +11 -0
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # March Control
2
+
3
+ `march` is a provider configuration manager for:
4
+
5
+ - Codex
6
+ - OpenCode
7
+ - OpenClaw
8
+
9
+ It provides both:
10
+
11
+ - CLI (`march` / `mch`)
12
+ - Desktop/web workspace in this repo (`src/`, `desktop/`, `src-tauri/`)
13
+
14
+ ## CLI Features
15
+
16
+ - One-shot setup: write provider configs to multiple tools
17
+ - Provider lifecycle commands per platform:
18
+ - `add`
19
+ - `list`
20
+ - `current`
21
+ - `use`
22
+ - `edit`
23
+ - `clone`
24
+ - `remove`
25
+ - Preset management:
26
+ - `preset list`
27
+ - `preset add`
28
+ - `preset use`
29
+ - `preset remove`
30
+ - URL probe before apply
31
+ - Safe backup/rollback before writing config files
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install -g march-control-cli
37
+ ```
38
+
39
+ Install now includes:
40
+
41
+ - Chinese postinstall banner with quick-start commands
42
+ - Branded setup flow with step-by-step output (`[1/4] ... [4/4]`)
43
+ - Dual command entrypoints: `march` and `mch`
44
+
45
+ ## Quick Start
46
+
47
+ Interactive quick setup:
48
+
49
+ ```bash
50
+ march
51
+ ```
52
+
53
+ Non-interactive setup:
54
+
55
+ ```bash
56
+ march setup -k sk-your-key
57
+
58
+ # Setup using a preset
59
+ march setup --preset march -k sk-your-key
60
+ ```
61
+
62
+ Open full interactive menu:
63
+
64
+ ```bash
65
+ march menu
66
+
67
+ # same command alias
68
+ mch menu
69
+ ```
70
+
71
+ ## Command Examples
72
+
73
+ ```bash
74
+ # Codex
75
+ march cx list
76
+ march cx current
77
+ march cx add -n march -u https://gmncode.cn -k sk-xxx --model gpt-5.4
78
+ march cx use march
79
+ march cx edit march --model gpt-5.3-codex
80
+ march cx clone march -n march-backup
81
+ march cx remove march-backup
82
+
83
+ # OpenCode / OpenClaw
84
+ march oc list
85
+ march ow list
86
+
87
+ # Presets
88
+ march preset list
89
+ march preset add -n team-a --provider-name teama --base-url https://example.com
90
+ march preset use team-a -k sk-xxx -p codex,opencode
91
+ march preset remove team-a
92
+ ```
93
+
94
+ Probe endpoints:
95
+
96
+ ```bash
97
+ march probe -u https://gmncode.cn -u https://gmncode.cn/v1
98
+ ```
99
+
100
+ ## Local Development
101
+
102
+ Run CLI directly from source:
103
+
104
+ ```bash
105
+ npm run start
106
+ ```
107
+
108
+ Syntax check CLI core:
109
+
110
+ ```bash
111
+ npm run check
112
+ ```
113
+
114
+ Type check frontend:
115
+
116
+ ```bash
117
+ npm run typecheck
118
+ ```
119
+
120
+ Run frontend dev server:
121
+
122
+ ```bash
123
+ npm run dev
124
+ ```
125
+
126
+ Default dev URL:
127
+
128
+ ```text
129
+ http://127.0.0.1:3001
130
+ ```
131
+
132
+ Build frontend:
133
+
134
+ ```bash
135
+ npm run build:web
136
+ ```
137
+
138
+ ## 本地验证(CLI 体验)
139
+
140
+ ```bash
141
+ # 1) 基础检查
142
+ npm run check
143
+ npm run desktop:check
144
+
145
+ # 2) 查看安装后欢迎页(本地模拟)
146
+ node scripts/postinstall.mjs
147
+
148
+ # 3) 验证中文帮助
149
+ node core/index.js --help
150
+ node core/index.js preset --help
151
+ node core/index.js cx --help
152
+ ```
153
+
154
+ 隔离目录实测(不污染当前用户目录):
155
+
156
+ ```bash
157
+ $env:MARCH_HOME="C:\\tmp\\march-smoke"
158
+ node core/index.js setup --preset march -k sk-demo-1234 --platforms codex,opencode --no-probe --no-backup
159
+ ```
160
+
161
+ ## 客户安装模拟
162
+
163
+ ```bash
164
+ # 在项目目录打包
165
+ npm pack
166
+
167
+ # 卸载旧版本(可选)
168
+ npm uninstall -g march-control-cli
169
+
170
+ # 安装你刚打出来的 tgz 包(把版本号替换成当前文件名)
171
+ npm install -g .\\march-control-cli-0.1.3.tgz
172
+
173
+ # 客户侧常用验证命令
174
+ march --help
175
+ mch --help
176
+ march
177
+ march setup --preset march -k sk-demo-1234 --platforms codex --no-probe
178
+ ```
179
+
180
+ ## Desktop
181
+
182
+ Run desktop shell:
183
+
184
+ ```bash
185
+ npm run desktop:dev
186
+ ```
187
+
188
+ If Electron runtime is missing:
189
+
190
+ ```bash
191
+ npm run desktop:install-runtime
192
+ ```
193
+
194
+ Check desktop entry files:
195
+
196
+ ```bash
197
+ npm run desktop:check
198
+ ```
199
+
200
+ Build Windows packages:
201
+
202
+ ```bash
203
+ npm run desktop:pack
204
+ ```
205
+
206
+ ## ccman Parity Roadmap
207
+
208
+ Current parity focus completed:
209
+
210
+ - Provider lifecycle parity (`edit/remove/clone`)
211
+ - Scriptable non-interactive commands
212
+ - Interactive menu aligned with lifecycle actions
213
+ - Preset manager (`preset list/add/use/remove`)
214
+
215
+ Next suggested parity items:
216
+
217
+ 1. Import/export command set
218
+ 2. MCP profile manager for Codex/OpenClaw ecosystem
219
+ 3. Optional WebDAV sync module for shared config state
220
+ 4. Desktop preset CRUD + one-click preset apply
package/core/apply.js ADDED
@@ -0,0 +1,152 @@
1
+ import { createPlatformBackup, restorePlatformBackup } from "./backup.js";
2
+ import { getPlatformTargetFiles } from "./paths.js";
3
+ import {
4
+ cloneProvider,
5
+ getCurrentProvider,
6
+ removeProvider,
7
+ resolveStoredProvider,
8
+ switchProvider,
9
+ updateProvider,
10
+ upsertProvider
11
+ } from "./store.js";
12
+ import { writePlatformConfig } from "./writers/index.js";
13
+
14
+ export function applyProvider(platform, providerInput, options = {}) {
15
+ const backup = options.backup === false ? null : createPlatformBackup(platform);
16
+
17
+ try {
18
+ const provider = upsertProvider(platform, providerInput, { activate: options.activate !== false });
19
+ writePlatformConfig(platform, provider, {
20
+ mode: options.overwrite ? "overwrite" : "merge"
21
+ });
22
+
23
+ return {
24
+ provider,
25
+ backupDir: backup?.backupDir || null,
26
+ targetFiles: getPlatformTargetFiles(platform)
27
+ };
28
+ } catch (error) {
29
+ if (backup) {
30
+ restorePlatformBackup(backup);
31
+ }
32
+
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ export function applyStoredProvider(platform, nameOrId, options = {}) {
38
+ const backup = options.backup === false ? null : createPlatformBackup(platform);
39
+
40
+ try {
41
+ const provider = resolveStoredProvider(platform, nameOrId);
42
+ if (options.activate !== false) {
43
+ switchProvider(platform, nameOrId);
44
+ }
45
+
46
+ const current = getCurrentProvider(platform) || provider;
47
+ writePlatformConfig(platform, current, {
48
+ mode: options.overwrite ? "overwrite" : "merge"
49
+ });
50
+
51
+ return {
52
+ provider: current,
53
+ backupDir: backup?.backupDir || null,
54
+ targetFiles: getPlatformTargetFiles(platform)
55
+ };
56
+ } catch (error) {
57
+ if (backup) {
58
+ restorePlatformBackup(backup);
59
+ }
60
+
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ export function applyEditedProvider(platform, nameOrId, updates, options = {}) {
66
+ const backup = options.backup === false ? null : createPlatformBackup(platform);
67
+
68
+ try {
69
+ const provider = updateProvider(platform, nameOrId, updates, {
70
+ activate: options.activate === true
71
+ });
72
+ const current = getCurrentProvider(platform);
73
+
74
+ if (current) {
75
+ writePlatformConfig(platform, current, {
76
+ mode: options.overwrite ? "overwrite" : "merge"
77
+ });
78
+ }
79
+
80
+ return {
81
+ provider,
82
+ activeProvider: current,
83
+ backupDir: backup?.backupDir || null,
84
+ targetFiles: getPlatformTargetFiles(platform)
85
+ };
86
+ } catch (error) {
87
+ if (backup) {
88
+ restorePlatformBackup(backup);
89
+ }
90
+
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ export function applyClonedProvider(platform, nameOrId, input, options = {}) {
96
+ const backup = options.backup === false ? null : createPlatformBackup(platform);
97
+
98
+ try {
99
+ const provider = cloneProvider(platform, nameOrId, input, {
100
+ activate: options.activate === true
101
+ });
102
+ const current = getCurrentProvider(platform);
103
+
104
+ if (current) {
105
+ writePlatformConfig(platform, current, {
106
+ mode: options.overwrite ? "overwrite" : "merge"
107
+ });
108
+ }
109
+
110
+ return {
111
+ provider,
112
+ activeProvider: current,
113
+ backupDir: backup?.backupDir || null,
114
+ targetFiles: getPlatformTargetFiles(platform)
115
+ };
116
+ } catch (error) {
117
+ if (backup) {
118
+ restorePlatformBackup(backup);
119
+ }
120
+
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ export function applyRemovedProvider(platform, nameOrId, options = {}) {
126
+ const backup = options.backup === false ? null : createPlatformBackup(platform);
127
+
128
+ try {
129
+ const result = removeProvider(platform, nameOrId, {
130
+ activateFallback: options.activateFallback !== false
131
+ });
132
+
133
+ if (result.currentProvider) {
134
+ writePlatformConfig(platform, result.currentProvider, {
135
+ mode: options.overwrite ? "overwrite" : "merge"
136
+ });
137
+ }
138
+
139
+ return {
140
+ removedProvider: result.removedProvider,
141
+ activeProvider: result.currentProvider,
142
+ backupDir: backup?.backupDir || null,
143
+ targetFiles: getPlatformTargetFiles(platform)
144
+ };
145
+ } catch (error) {
146
+ if (backup) {
147
+ restorePlatformBackup(backup);
148
+ }
149
+
150
+ throw error;
151
+ }
152
+ }
package/core/backup.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getBackupDirRoot, getPlatformTargetFiles } from "./paths.js";
4
+ import { ensureDir, writeJson } from "./utils.js";
5
+
6
+ export function createPlatformBackup(platform) {
7
+ const backupDir = path.join(
8
+ getBackupDirRoot(),
9
+ `${platform}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
10
+ );
11
+
12
+ ensureDir(backupDir);
13
+
14
+ const entries = getPlatformTargetFiles(platform).map((originalPath, index) => {
15
+ if (!fs.existsSync(originalPath)) {
16
+ return { originalPath, backupPath: null, existed: false };
17
+ }
18
+
19
+ const backupPath = path.join(
20
+ backupDir,
21
+ `${String(index).padStart(2, "0")}-${path.basename(originalPath).replace(/[^\w.-]/g, "_")}.bak`
22
+ );
23
+
24
+ fs.copyFileSync(originalPath, backupPath);
25
+ return { originalPath, backupPath, existed: true };
26
+ });
27
+
28
+ writeJson(path.join(backupDir, "manifest.json"), {
29
+ platform,
30
+ createdAt: new Date().toISOString(),
31
+ entries
32
+ });
33
+
34
+ return { backupDir, entries };
35
+ }
36
+
37
+ export function restorePlatformBackup(result) {
38
+ for (const entry of result.entries) {
39
+ if (entry.existed) {
40
+ if (!entry.backupPath || !fs.existsSync(entry.backupPath)) {
41
+ throw new Error(`Missing backup file: ${entry.backupPath || entry.originalPath}`);
42
+ }
43
+
44
+ ensureDir(path.dirname(entry.originalPath));
45
+ fs.copyFileSync(entry.backupPath, entry.originalPath);
46
+ continue;
47
+ }
48
+
49
+ if (fs.existsSync(entry.originalPath)) {
50
+ fs.rmSync(entry.originalPath, { force: true });
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,55 @@
1
+ export const APP_NAME = "march";
2
+ export const DEFAULT_PROVIDER_NAME = "march";
3
+ export const DEFAULT_BASE_URL = "https://gmncode.cn";
4
+ export const DEFAULT_OPENCLAW_BASE_URL = "https://gmncode.cn/v1";
5
+ export const DEFAULT_PRIMARY_MODEL = "gpt-5.4";
6
+
7
+ export const PLATFORM_META = {
8
+ codex: {
9
+ label: "Codex",
10
+ command: "cx",
11
+ defaultBaseUrl: DEFAULT_BASE_URL
12
+ },
13
+ opencode: {
14
+ label: "OpenCode",
15
+ command: "oc",
16
+ defaultBaseUrl: DEFAULT_BASE_URL
17
+ },
18
+ openclaw: {
19
+ label: "OpenClaw",
20
+ command: "ow",
21
+ defaultBaseUrl: DEFAULT_OPENCLAW_BASE_URL
22
+ }
23
+ };
24
+
25
+ export const SUPPORTED_PLATFORMS = Object.keys(PLATFORM_META);
26
+ export const DEFAULT_PLATFORM_SELECTION = [...SUPPORTED_PLATFORMS];
27
+ export const DEFAULT_CANDIDATE_BASE_URLS = [DEFAULT_BASE_URL];
28
+
29
+ export const BUILTIN_PRESETS = [
30
+ {
31
+ name: "march",
32
+ providerName: "march",
33
+ commonBaseUrl: "https://gmncode.cn",
34
+ openclawBaseUrl: "https://gmncode.cn/v1",
35
+ model: DEFAULT_PRIMARY_MODEL
36
+ },
37
+ {
38
+ name: "openai",
39
+ providerName: "openai",
40
+ commonBaseUrl: "https://api.openai.com/v1",
41
+ openclawBaseUrl: "https://api.openai.com/v1",
42
+ model: DEFAULT_PRIMARY_MODEL
43
+ }
44
+ ];
45
+
46
+ export const MODEL_DEFINITIONS = [
47
+ { id: "gpt-5.4", reasoning: true, contextWindow: 1050000, maxTokens: 128000 },
48
+ { id: "gpt-5.3-codex", reasoning: false, contextWindow: 400000, maxTokens: 128000 },
49
+ { id: "gpt-5.2", reasoning: true, contextWindow: 1050000, maxTokens: 128000 },
50
+ { id: "gpt-5.2-codex", reasoning: false, contextWindow: 400000, maxTokens: 128000 },
51
+ { id: "gpt-5.1", reasoning: true, contextWindow: 1050000, maxTokens: 128000 },
52
+ { id: "gpt-5.1-codex", reasoning: false, contextWindow: 400000, maxTokens: 128000 },
53
+ { id: "gpt-5.1-codex-mini", reasoning: false, contextWindow: 400000, maxTokens: 128000 },
54
+ { id: "gpt-5.1-codex-max", reasoning: false, contextWindow: 400000, maxTokens: 128000 }
55
+ ];
@@ -0,0 +1,219 @@
1
+ import { applyProvider, applyStoredProvider } from "./apply.js";
2
+ import {
3
+ DEFAULT_BASE_URL,
4
+ DEFAULT_OPENCLAW_BASE_URL,
5
+ DEFAULT_PRIMARY_MODEL,
6
+ DEFAULT_PROVIDER_NAME,
7
+ MODEL_DEFINITIONS,
8
+ PLATFORM_META,
9
+ SUPPORTED_PLATFORMS
10
+ } from "./constants.js";
11
+ import {
12
+ deleteMcpServerFromDesktop as deleteMcpInState,
13
+ deletePromptFromDesktop as deletePromptInState,
14
+ deleteSkillFromDesktop as deleteSkillInState,
15
+ getDesktopState,
16
+ toggleMcpServerForPlatform,
17
+ togglePromptFromDesktop as togglePromptInState,
18
+ toggleSkillRepoFromDesktop as toggleSkillRepoInState,
19
+ upsertMcpServerFromDesktop as upsertMcpInState,
20
+ upsertPromptFromDesktop as upsertPromptInState,
21
+ upsertSkillFromDesktop as upsertSkillInState
22
+ } from "./desktop-state.js";
23
+ import { getPlatformTargetFiles } from "./paths.js";
24
+ import { getBestProbeResult, probeBaseUrls } from "./probe.js";
25
+ import { getCurrentProvider, listProviders } from "./store.js";
26
+ import { buildOpenClawBaseUrl, maskApiKey, normalizeBaseUrl } from "./utils.js";
27
+
28
+ function serializeProvider(provider, currentProviderId) {
29
+ return {
30
+ id: provider.id,
31
+ name: provider.name,
32
+ baseUrl: provider.baseUrl,
33
+ model: provider.model || DEFAULT_PRIMARY_MODEL,
34
+ maskedApiKey: maskApiKey(provider.apiKey),
35
+ createdAt: provider.createdAt || null,
36
+ updatedAt: provider.updatedAt || null,
37
+ isActive: provider.id === currentProviderId
38
+ };
39
+ }
40
+
41
+ function buildPlatformSnapshot(platform) {
42
+ const providers = listProviders(platform);
43
+ const currentProvider = getCurrentProvider(platform);
44
+
45
+ return {
46
+ id: platform,
47
+ label: PLATFORM_META[platform].label,
48
+ command: PLATFORM_META[platform].command,
49
+ defaultBaseUrl: platform === "openclaw" ? DEFAULT_OPENCLAW_BASE_URL : DEFAULT_BASE_URL,
50
+ defaultProviderName: DEFAULT_PROVIDER_NAME,
51
+ currentProviderId: currentProvider?.id || null,
52
+ currentProviderName: currentProvider?.name || null,
53
+ providerCount: providers.length,
54
+ targetFiles: getPlatformTargetFiles(platform),
55
+ providers: providers.map((provider) => serializeProvider(provider, currentProvider?.id || null))
56
+ };
57
+ }
58
+
59
+ function validatePlatform(platform) {
60
+ if (!SUPPORTED_PLATFORMS.includes(platform)) {
61
+ throw new Error(`Unsupported platform: ${platform}`);
62
+ }
63
+ }
64
+
65
+ function buildInputForPlatform(platform, input) {
66
+ const name = `${input?.name || DEFAULT_PROVIDER_NAME}`.trim() || DEFAULT_PROVIDER_NAME;
67
+ const apiKey = `${input?.apiKey || ""}`.trim();
68
+ const fallbackBaseUrl = platform === "openclaw" ? DEFAULT_OPENCLAW_BASE_URL : DEFAULT_BASE_URL;
69
+ const baseUrl = normalizeBaseUrl(`${input?.baseUrl || fallbackBaseUrl}`.trim());
70
+ const model = `${input?.model || DEFAULT_PRIMARY_MODEL}`.trim() || DEFAULT_PRIMARY_MODEL;
71
+
72
+ if (!apiKey) {
73
+ throw new Error("API Key 不能为空");
74
+ }
75
+
76
+ return {
77
+ name,
78
+ apiKey,
79
+ baseUrl,
80
+ model
81
+ };
82
+ }
83
+
84
+ export function getDesktopSnapshot() {
85
+ const desktopState = getDesktopState();
86
+
87
+ return {
88
+ appName: "March 控制台",
89
+ version: "0.3.0-alpha",
90
+ generatedAt: new Date().toISOString(),
91
+ models: MODEL_DEFINITIONS,
92
+ platforms: SUPPORTED_PLATFORMS.map((platform) => buildPlatformSnapshot(platform)),
93
+ mcpServers: desktopState.mcpServers,
94
+ prompts: desktopState.prompts,
95
+ skills: desktopState.skills,
96
+ skillRepos: desktopState.skillRepos
97
+ };
98
+ }
99
+
100
+ export function saveProviderFromDesktop(platform, input, options = {}) {
101
+ validatePlatform(platform);
102
+ const providerInput = buildInputForPlatform(platform, input);
103
+ const result = applyProvider(platform, providerInput, {
104
+ backup: options.backup !== false,
105
+ overwrite: options.overwrite === true,
106
+ activate: options.activate !== false
107
+ });
108
+
109
+ return {
110
+ snapshot: buildPlatformSnapshot(platform),
111
+ result: {
112
+ backupDir: result.backupDir,
113
+ targetFiles: result.targetFiles
114
+ }
115
+ };
116
+ }
117
+
118
+ export function activateProviderFromDesktop(platform, nameOrId, options = {}) {
119
+ validatePlatform(platform);
120
+ const result = applyStoredProvider(platform, nameOrId, {
121
+ backup: options.backup !== false,
122
+ overwrite: options.overwrite === true,
123
+ activate: true
124
+ });
125
+
126
+ return {
127
+ snapshot: buildPlatformSnapshot(platform),
128
+ result: {
129
+ backupDir: result.backupDir,
130
+ targetFiles: result.targetFiles
131
+ }
132
+ };
133
+ }
134
+
135
+ export function toggleMcpFromDesktop(serverId, platform) {
136
+ validatePlatform(platform);
137
+ toggleMcpServerForPlatform(serverId, platform);
138
+ return getDesktopSnapshot();
139
+ }
140
+
141
+ export function upsertMcpFromDesktop(input) {
142
+ upsertMcpInState(input);
143
+ return getDesktopSnapshot();
144
+ }
145
+
146
+ export function deleteMcpFromDesktop(serverId) {
147
+ deleteMcpInState(serverId);
148
+ return getDesktopSnapshot();
149
+ }
150
+
151
+ export function togglePromptFromDesktop(promptId) {
152
+ togglePromptInState(promptId);
153
+ return getDesktopSnapshot();
154
+ }
155
+
156
+ export function upsertPromptFromDesktop(input) {
157
+ upsertPromptInState(input);
158
+ return getDesktopSnapshot();
159
+ }
160
+
161
+ export function deletePromptFromDesktop(promptId) {
162
+ deletePromptInState(promptId);
163
+ return getDesktopSnapshot();
164
+ }
165
+
166
+ export function upsertSkillFromDesktop(input) {
167
+ upsertSkillInState(input);
168
+ return getDesktopSnapshot();
169
+ }
170
+
171
+ export function deleteSkillFromDesktop(skillId) {
172
+ deleteSkillInState(skillId);
173
+ return getDesktopSnapshot();
174
+ }
175
+
176
+ export function toggleSkillRepoFromDesktop(repoId) {
177
+ toggleSkillRepoInState(repoId);
178
+ return getDesktopSnapshot();
179
+ }
180
+
181
+ export async function probePlatformProvidersFromDesktop(platform) {
182
+ validatePlatform(platform);
183
+ const snapshot = buildPlatformSnapshot(platform);
184
+ const providerUrls = snapshot.providers.map((provider) => provider.baseUrl);
185
+
186
+ if (providerUrls.length === 0) {
187
+ return {
188
+ snapshot,
189
+ results: [],
190
+ best: null
191
+ };
192
+ }
193
+
194
+ const results = await probeBaseUrls(providerUrls, { timeoutMs: 5000 });
195
+ return {
196
+ snapshot,
197
+ results,
198
+ best: getBestProbeResult(results)
199
+ };
200
+ }
201
+
202
+ export async function probeCandidateFromDesktop(platform, baseUrl) {
203
+ validatePlatform(platform);
204
+ const normalizedBaseUrl = normalizeBaseUrl(`${baseUrl || ""}`.trim());
205
+
206
+ if (!normalizedBaseUrl) {
207
+ throw new Error("测试地址不能为空");
208
+ }
209
+
210
+ const finalUrl =
211
+ platform === "openclaw" && !normalizedBaseUrl.endsWith("/v1")
212
+ ? buildOpenClawBaseUrl(normalizedBaseUrl)
213
+ : normalizedBaseUrl;
214
+ const results = await probeBaseUrls([finalUrl], { timeoutMs: 5000 });
215
+
216
+ return {
217
+ result: results[0] || null
218
+ };
219
+ }