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.
- package/README.md +220 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +55 -0
- package/core/desktop-service.js +219 -0
- package/core/desktop-state.js +511 -0
- package/core/index.js +1293 -0
- package/core/paths.js +71 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/store.js +218 -0
- package/core/utils.js +178 -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/march-mark.svg +21 -0
- package/desktop/main.js +192 -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 +413 -0
- package/package.json +106 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/postinstall.mjs +28 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/march-mark.svg +22 -0
- package/site/index.html +286 -0
- package/site/styles.css +566 -0
- package/src/App.tsx +1186 -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 +16 -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 +8 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +184 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +944 -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 +76 -0
- package/src/vite-env.d.ts +56 -0
- 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
|
+
}
|