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,327 @@
1
+ const state = {
2
+ snapshot: null,
3
+ activePlatform: "codex",
4
+ probeResult: null,
5
+ toastTimer: null
6
+ };
7
+
8
+ const platformTabs = document.querySelector("#platformTabs");
9
+ const statsGrid = document.querySelector("#statsGrid");
10
+ const providerList = document.querySelector("#providerList");
11
+ const targetFiles = document.querySelector("#targetFiles");
12
+ const probeResults = document.querySelector("#probeResults");
13
+ const modelSelect = document.querySelector("#modelSelect");
14
+ const providerForm = document.querySelector("#providerForm");
15
+ const probeButton = document.querySelector("#probeButton");
16
+ const refreshButton = document.querySelector("#refreshButton");
17
+ const probeCandidateButton = document.querySelector("#probeCandidateButton");
18
+ const candidateProbeStatus = document.querySelector("#candidateProbeStatus");
19
+ const heroTitle = document.querySelector("#heroTitle");
20
+ const heroSubtitle = document.querySelector("#heroSubtitle");
21
+ const toast = document.querySelector("#toast");
22
+
23
+ function getActivePlatformSnapshot() {
24
+ return state.snapshot?.platforms?.find((item) => item.id === state.activePlatform) || null;
25
+ }
26
+
27
+ function setToast(message, isError = false) {
28
+ toast.textContent = message;
29
+ toast.style.background = isError ? "rgba(122, 30, 30, 0.96)" : "rgba(18, 37, 49, 0.94)";
30
+ toast.classList.add("visible");
31
+
32
+ if (state.toastTimer) {
33
+ clearTimeout(state.toastTimer);
34
+ }
35
+
36
+ state.toastTimer = setTimeout(() => {
37
+ toast.classList.remove("visible");
38
+ }, 2600);
39
+ }
40
+
41
+ function renderPlatformTabs() {
42
+ if (!state.snapshot) {
43
+ platformTabs.innerHTML = "";
44
+ return;
45
+ }
46
+
47
+ platformTabs.innerHTML = state.snapshot.platforms
48
+ .map(
49
+ (platform) => `
50
+ <button class="platform-tab ${platform.id === state.activePlatform ? "active" : ""}" data-platform="${platform.id}" type="button">
51
+ <small>${platform.command}</small>
52
+ <strong>${platform.label}</strong>
53
+ <span>${platform.providerCount} saved</span>
54
+ </button>
55
+ `
56
+ )
57
+ .join("");
58
+
59
+ for (const button of platformTabs.querySelectorAll("[data-platform]")) {
60
+ button.addEventListener("click", () => {
61
+ state.activePlatform = button.dataset.platform;
62
+ state.probeResult = null;
63
+ render();
64
+ });
65
+ }
66
+ }
67
+
68
+ function renderStats(platform) {
69
+ statsGrid.innerHTML = `
70
+ <article class="stat">
71
+ <span>Current Platform</span>
72
+ <strong>${platform.label}</strong>
73
+ </article>
74
+ <article class="stat">
75
+ <span>Saved Providers</span>
76
+ <strong>${platform.providerCount}</strong>
77
+ </article>
78
+ <article class="stat">
79
+ <span>Current Provider</span>
80
+ <strong>${platform.currentProviderName || "None"}</strong>
81
+ </article>
82
+ <article class="stat">
83
+ <span>Target Files</span>
84
+ <strong>${platform.targetFiles.length}</strong>
85
+ </article>
86
+ `;
87
+ }
88
+
89
+ function renderProviders(platform) {
90
+ if (platform.providers.length === 0) {
91
+ providerList.innerHTML = `<div class="empty">当前平台还没有保存任何 Provider。</div>`;
92
+ return;
93
+ }
94
+
95
+ providerList.innerHTML = platform.providers
96
+ .map(
97
+ (provider) => `
98
+ <article class="provider-card ${provider.isActive ? "active" : ""}">
99
+ <header>
100
+ <div>
101
+ <strong>${provider.name}</strong>
102
+ <span>${provider.baseUrl}</span>
103
+ <small>${provider.model} · ${provider.maskedApiKey}</small>
104
+ </div>
105
+ ${provider.isActive ? '<span class="badge">Active</span>' : ""}
106
+ </header>
107
+ <div>
108
+ <button type="button" data-activate="${provider.id}">${provider.isActive ? "当前启用" : "切换到此配置"}</button>
109
+ </div>
110
+ </article>
111
+ `
112
+ )
113
+ .join("");
114
+
115
+ for (const button of providerList.querySelectorAll("[data-activate]")) {
116
+ button.addEventListener("click", async () => {
117
+ const providerId = button.dataset.activate;
118
+ button.disabled = true;
119
+
120
+ try {
121
+ const response = await window.marchDesktop.activateProvider({
122
+ platform: state.activePlatform,
123
+ nameOrId: providerId
124
+ });
125
+ await refreshSnapshot(response.snapshot);
126
+ setToast("已切换到所选 Provider。");
127
+ } catch (error) {
128
+ setToast(error.message, true);
129
+ } finally {
130
+ button.disabled = false;
131
+ }
132
+ });
133
+ }
134
+ }
135
+
136
+ function renderTargetFiles(platform) {
137
+ targetFiles.innerHTML = platform.targetFiles
138
+ .map(
139
+ (filePath) => `
140
+ <article class="file-item">
141
+ <header>
142
+ <div>
143
+ <strong>${platform.label}</strong>
144
+ <code>${filePath}</code>
145
+ </div>
146
+ <button type="button" data-open-path="${filePath}">打开</button>
147
+ </header>
148
+ </article>
149
+ `
150
+ )
151
+ .join("");
152
+
153
+ for (const button of targetFiles.querySelectorAll("[data-open-path]")) {
154
+ button.addEventListener("click", async () => {
155
+ await window.marchDesktop.openPath(button.dataset.openPath);
156
+ });
157
+ }
158
+ }
159
+
160
+ function renderProbeResults() {
161
+ if (!state.probeResult?.results?.length) {
162
+ probeResults.innerHTML = `<div class="empty">还没有测速结果。</div>`;
163
+ return;
164
+ }
165
+
166
+ const bestBaseUrl = state.probeResult.best?.baseUrl || "";
167
+
168
+ probeResults.innerHTML = state.probeResult.results
169
+ .map((result) => {
170
+ const isBest = result.baseUrl === bestBaseUrl;
171
+ return `
172
+ <article class="probe-row ${isBest ? "best" : ""}">
173
+ <header>
174
+ <div>
175
+ <strong>${result.baseUrl}</strong>
176
+ <span>${result.ok ? "可用" : "失败"} · ${result.ok ? `${result.latency} ms` : result.error}</span>
177
+ </div>
178
+ ${isBest ? '<span class="badge">Best</span>' : ""}
179
+ </header>
180
+ </article>
181
+ `;
182
+ })
183
+ .join("");
184
+ }
185
+
186
+ function renderModelOptions() {
187
+ if (!state.snapshot) {
188
+ return;
189
+ }
190
+
191
+ modelSelect.innerHTML = state.snapshot.models
192
+ .map((model) => `<option value="${model.id}">${model.id}</option>`)
193
+ .join("");
194
+ }
195
+
196
+ function populateForm(platform) {
197
+ const currentProvider = platform.providers.find((provider) => provider.isActive) || null;
198
+ document.querySelector("#providerName").value = currentProvider?.name || platform.defaultProviderName;
199
+ document.querySelector("#baseUrl").value = currentProvider?.baseUrl || platform.defaultBaseUrl;
200
+ document.querySelector("#apiKey").value = "";
201
+ modelSelect.value = currentProvider?.model || state.snapshot.models[0]?.id || "gpt-5.4";
202
+ candidateProbeStatus.textContent = "等待测试";
203
+ }
204
+
205
+ function renderHeader(platform) {
206
+ heroTitle.textContent = `ccon / ${platform.label}`;
207
+ heroSubtitle.textContent = `保存 ${platform.providerCount} 个 Provider,当前启用 ${platform.currentProviderName || "无"}。`;
208
+ }
209
+
210
+ function render() {
211
+ const platform = getActivePlatformSnapshot();
212
+ if (!platform) {
213
+ return;
214
+ }
215
+
216
+ renderPlatformTabs();
217
+ renderStats(platform);
218
+ renderProviders(platform);
219
+ renderTargetFiles(platform);
220
+ renderProbeResults();
221
+ renderHeader(platform);
222
+ populateForm(platform);
223
+ }
224
+
225
+ async function refreshSnapshot(preloadedSnapshot = null) {
226
+ if (!state.snapshot || !preloadedSnapshot) {
227
+ state.snapshot = await window.marchDesktop.getSnapshot();
228
+ } else {
229
+ state.snapshot = {
230
+ ...state.snapshot,
231
+ platforms: state.snapshot.platforms.map((item) =>
232
+ item.id === preloadedSnapshot.id ? preloadedSnapshot : item
233
+ )
234
+ };
235
+ }
236
+
237
+ if (!state.snapshot.platforms.some((item) => item.id === state.activePlatform)) {
238
+ state.activePlatform = state.snapshot.platforms[0]?.id || "codex";
239
+ }
240
+
241
+ renderModelOptions();
242
+ render();
243
+ }
244
+
245
+ providerForm.addEventListener("submit", async (event) => {
246
+ event.preventDefault();
247
+
248
+ const submitButton = providerForm.querySelector('[type="submit"]');
249
+ submitButton.disabled = true;
250
+
251
+ try {
252
+ const response = await window.marchDesktop.saveProvider({
253
+ platform: state.activePlatform,
254
+ input: {
255
+ name: document.querySelector("#providerName").value,
256
+ baseUrl: document.querySelector("#baseUrl").value,
257
+ apiKey: document.querySelector("#apiKey").value,
258
+ model: modelSelect.value
259
+ }
260
+ });
261
+ state.probeResult = null;
262
+ await refreshSnapshot(response.snapshot);
263
+ setToast("Provider 已保存并启用。");
264
+ } catch (error) {
265
+ setToast(error.message, true);
266
+ } finally {
267
+ submitButton.disabled = false;
268
+ }
269
+ });
270
+
271
+ probeButton.addEventListener("click", async () => {
272
+ probeButton.disabled = true;
273
+ probeButton.textContent = "测速中...";
274
+
275
+ try {
276
+ state.probeResult = await window.marchDesktop.probePlatform({
277
+ platform: state.activePlatform
278
+ });
279
+ renderProbeResults();
280
+ setToast(state.probeResult.best ? `最快地址: ${state.probeResult.best.baseUrl}` : "当前没有可测地址。");
281
+ } catch (error) {
282
+ setToast(error.message, true);
283
+ } finally {
284
+ probeButton.disabled = false;
285
+ probeButton.textContent = "测速当前平台";
286
+ }
287
+ });
288
+
289
+ probeCandidateButton.addEventListener("click", async () => {
290
+ probeCandidateButton.disabled = true;
291
+ candidateProbeStatus.textContent = "正在测试...";
292
+
293
+ try {
294
+ const result = await window.marchDesktop.probeCandidate({
295
+ platform: state.activePlatform,
296
+ baseUrl: document.querySelector("#baseUrl").value
297
+ });
298
+
299
+ if (!result.result) {
300
+ candidateProbeStatus.textContent = "没有返回结果";
301
+ } else if (result.result.ok) {
302
+ candidateProbeStatus.textContent = `可用,延迟 ${result.result.latency} ms`;
303
+ } else {
304
+ candidateProbeStatus.textContent = `失败: ${result.result.error}`;
305
+ }
306
+ } catch (error) {
307
+ candidateProbeStatus.textContent = error.message;
308
+ } finally {
309
+ probeCandidateButton.disabled = false;
310
+ }
311
+ });
312
+
313
+ refreshButton.addEventListener("click", async () => {
314
+ refreshButton.disabled = true;
315
+
316
+ try {
317
+ state.probeResult = null;
318
+ await refreshSnapshot();
319
+ setToast("数据已刷新。");
320
+ } catch (error) {
321
+ setToast(error.message, true);
322
+ } finally {
323
+ refreshButton.disabled = false;
324
+ }
325
+ });
326
+
327
+ await refreshSnapshot();
@@ -0,0 +1,130 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ccon</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="./styles.css" />
14
+ </head>
15
+ <body>
16
+ <div class="shell">
17
+ <aside class="sidebar">
18
+ <div class="brand">
19
+ <img src="../assets/fml-icon.png" alt="ccon" />
20
+ <div>
21
+ <strong>ccon</strong>
22
+ <span>relay desktop</span>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="platforms" id="platformTabs"></div>
27
+
28
+ <div class="sidebar-card">
29
+ <span>Default Route</span>
30
+ <strong>www.fhl.mom</strong>
31
+ <small>Codex / OpenCode 默认接入主线路,OpenClaw 使用 /v1,并把状态和配置文件集中展示。</small>
32
+ </div>
33
+ </aside>
34
+
35
+ <main class="workspace">
36
+ <header class="hero">
37
+ <div>
38
+ <span class="eyebrow">Desktop Control Panel</span>
39
+ <h1 id="heroTitle">ccon</h1>
40
+ <p id="heroSubtitle">统一管理 Provider、写入本地配置、测速和快速切换。</p>
41
+ </div>
42
+
43
+ <div class="hero-actions">
44
+ <button id="refreshButton" type="button">刷新数据</button>
45
+ <button id="probeButton" type="button" class="solid">测速当前平台</button>
46
+ </div>
47
+ </header>
48
+
49
+ <section class="stats" id="statsGrid"></section>
50
+
51
+ <section class="grid">
52
+ <article class="panel">
53
+ <div class="panel-head">
54
+ <div>
55
+ <span class="eyebrow">Providers</span>
56
+ <h2>已保存配置</h2>
57
+ </div>
58
+ </div>
59
+ <div class="provider-list" id="providerList"></div>
60
+ </article>
61
+
62
+ <article class="panel">
63
+ <div class="panel-head">
64
+ <div>
65
+ <span class="eyebrow">Quick Add</span>
66
+ <h2>新增或覆盖 Provider</h2>
67
+ </div>
68
+ </div>
69
+
70
+ <form id="providerForm" class="form">
71
+ <label>
72
+ <span>名称</span>
73
+ <input id="providerName" name="providerName" type="text" placeholder="fhl" />
74
+ </label>
75
+
76
+ <label>
77
+ <span>Base URL</span>
78
+ <input id="baseUrl" name="baseUrl" type="url" placeholder="https://www.fhl.mom" />
79
+ </label>
80
+
81
+ <label>
82
+ <span>API Key</span>
83
+ <input id="apiKey" name="apiKey" type="password" placeholder="sk-..." />
84
+ </label>
85
+
86
+ <label>
87
+ <span>模型</span>
88
+ <select id="modelSelect" name="model"></select>
89
+ </label>
90
+
91
+ <div class="form-actions">
92
+ <button id="probeCandidateButton" type="button">测试此地址</button>
93
+ <button type="submit" class="solid">保存并启用</button>
94
+ </div>
95
+ </form>
96
+
97
+ <div class="inline-status" id="candidateProbeStatus">等待测试</div>
98
+ </article>
99
+
100
+ <article class="panel">
101
+ <div class="panel-head">
102
+ <div>
103
+ <span class="eyebrow">Config Targets</span>
104
+ <h2>会写入这些文件</h2>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="file-list" id="targetFiles"></div>
109
+ </article>
110
+
111
+ <article class="panel">
112
+ <div class="panel-head">
113
+ <div>
114
+ <span class="eyebrow">Latency</span>
115
+ <h2>线路测速结果</h2>
116
+ </div>
117
+ </div>
118
+
119
+ <div class="probe-list" id="probeResults">
120
+ <div class="empty">还没有测速结果。</div>
121
+ </div>
122
+ </article>
123
+ </section>
124
+ </main>
125
+ </div>
126
+
127
+ <div class="toast" id="toast"></div>
128
+ <script type="module" src="./app.js"></script>
129
+ </body>
130
+ </html>