codexmate 0.0.28 → 0.0.30
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/cli/builtin-proxy.js +107 -2
- package/cli/config-bootstrap.js +30 -12
- package/cli/config-health.js +117 -1
- package/cli/local-bridge.js +324 -0
- package/cli/openai-bridge.js +195 -31
- package/cli.js +245 -28
- package/lib/cli-webhook.js +126 -0
- package/package.json +1 -1
- package/web-ui/app.js +28 -8
- package/web-ui/index.html +1 -0
- package/web-ui/logic.codex.mjs +13 -0
- package/web-ui/modules/app.computed.dashboard.mjs +25 -2
- package/web-ui/modules/app.computed.session.mjs +22 -17
- package/web-ui/modules/app.methods.claude-config.mjs +12 -2
- package/web-ui/modules/app.methods.codex-config.mjs +25 -0
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.navigation.mjs +39 -8
- package/web-ui/modules/app.methods.providers.mjs +125 -8
- package/web-ui/modules/app.methods.session-actions.mjs +1 -1
- package/web-ui/modules/app.methods.session-browser.mjs +1 -1
- package/web-ui/modules/app.methods.session-trash.mjs +3 -4
- package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
- package/web-ui/modules/app.methods.webhook.mjs +79 -0
- package/web-ui/modules/i18n.dict.mjs +1109 -72
- package/web-ui/modules/i18n.mjs +9 -3
- package/web-ui/modules/skills.methods.mjs +1 -0
- package/web-ui/partials/index/layout-header.html +25 -0
- package/web-ui/partials/index/modals-basic.html +0 -3
- package/web-ui/partials/index/panel-config-claude.html +8 -2
- package/web-ui/partials/index/panel-config-codex.html +28 -3
- package/web-ui/partials/index/panel-dashboard.html +33 -0
- package/web-ui/partials/index/panel-market.html +3 -3
- package/web-ui/partials/index/panel-plugins.html +2 -2
- package/web-ui/partials/index/panel-sessions.html +1 -9
- package/web-ui/partials/index/panel-settings.html +71 -134
- package/web-ui/partials/index/panel-trash.html +88 -0
- package/web-ui/session-helpers.mjs +20 -2
- package/web-ui/styles/dashboard.css +132 -0
- package/web-ui/styles/docs-panel.css +63 -39
- package/web-ui/styles/layout-shell.css +54 -34
- package/web-ui/styles/plugins-panel.css +121 -80
- package/web-ui/styles/sessions-list.css +41 -43
- package/web-ui/styles/sessions-preview.css +34 -38
- package/web-ui/styles/sessions-toolbar-trash.css +31 -27
- package/web-ui/styles/settings-panel.css +197 -33
- package/web-ui/styles/skills-list.css +12 -10
- package/web-ui/styles/skills-market.css +67 -44
- package/web-ui/styles/trash-panel.css +90 -0
- package/web-ui/styles/webhook.css +81 -0
- package/web-ui/styles.css +2 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const PROVIDER_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
2
2
|
const RESERVED_PROXY_PROVIDER_NAME = 'codexmate-proxy';
|
|
3
|
+
const RESERVED_LOCAL_PROVIDER_NAME = 'local';
|
|
3
4
|
|
|
4
5
|
function normalizeText(value) {
|
|
5
6
|
return typeof value === 'string' ? value.trim() : '';
|
|
@@ -21,7 +22,7 @@ function isValidHttpUrl(value) {
|
|
|
21
22
|
|
|
22
23
|
function isReservedProviderCreationNameInput(name) {
|
|
23
24
|
const normalized = normalizeText(name).toLowerCase();
|
|
24
|
-
return normalized === RESERVED_PROXY_PROVIDER_NAME;
|
|
25
|
+
return normalized === RESERVED_PROXY_PROVIDER_NAME || normalized === RESERVED_LOCAL_PROVIDER_NAME;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function isValidProviderNameInputValue(name) {
|
|
@@ -48,6 +49,12 @@ function normalizeProviderDraftState(target) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function maskKeyLocal(key) {
|
|
53
|
+
if (!key) return '';
|
|
54
|
+
if (key.length <= 8) return '****';
|
|
55
|
+
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
function getProviderValidationForContext(vm, mode = 'add') {
|
|
52
59
|
const draft = mode === 'edit' ? vm.editingProvider : vm.newProvider;
|
|
53
60
|
const editingName = mode === 'edit' ? normalizeText(draft && draft.name) : '';
|
|
@@ -158,10 +165,25 @@ export function createProvidersMethods(options = {}) {
|
|
|
158
165
|
return;
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
// 本地更新:构造新 provider 对象并追加到列表
|
|
169
|
+
const newProvider = {
|
|
170
|
+
name: validation.name,
|
|
171
|
+
url: validation.url,
|
|
172
|
+
upstreamUrl: '',
|
|
173
|
+
codexmate_bridge: payload.useTransform ? 'openai' : '',
|
|
174
|
+
key: maskKeyLocal(payload.key),
|
|
175
|
+
hasKey: !!payload.key,
|
|
176
|
+
models: [],
|
|
177
|
+
current: false,
|
|
178
|
+
readOnly: false,
|
|
179
|
+
nonDeletable: false,
|
|
180
|
+
nonEditable: false
|
|
181
|
+
};
|
|
182
|
+
this.providersList = [...this.providersList, newProvider];
|
|
183
|
+
|
|
161
184
|
this.showMessage('操作成功', 'success');
|
|
162
185
|
this.closeAddModal();
|
|
163
|
-
|
|
164
|
-
// loadAll 会重拉 status 覆盖字典,所以暗示模型在 loadAll 之后再写。
|
|
186
|
+
|
|
165
187
|
if (suggestedModel) {
|
|
166
188
|
if (!this.currentModels || typeof this.currentModels !== 'object') this.currentModels = {};
|
|
167
189
|
this.currentModels[validation.name] = suggestedModel;
|
|
@@ -237,17 +259,42 @@ export function createProvidersMethods(options = {}) {
|
|
|
237
259
|
this.showMessage(res.error, 'error');
|
|
238
260
|
return;
|
|
239
261
|
}
|
|
262
|
+
|
|
263
|
+
// 本地更新:从列表中移除
|
|
264
|
+
this.providersList = this.providersList.filter(p => p.name !== name);
|
|
265
|
+
|
|
266
|
+
// 清理 currentModels
|
|
267
|
+
if (this.currentModels && this.currentModels[name]) {
|
|
268
|
+
delete this.currentModels[name];
|
|
269
|
+
}
|
|
270
|
+
|
|
240
271
|
if (res.switched && res.provider) {
|
|
272
|
+
this.currentProvider = res.provider;
|
|
273
|
+
if (res.model) this.currentModel = res.model;
|
|
274
|
+
// 更新 current 标记
|
|
275
|
+
this.providersList = this.providersList.map(p => ({
|
|
276
|
+
...p,
|
|
277
|
+
current: p.name === res.provider
|
|
278
|
+
}));
|
|
241
279
|
this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
|
|
242
280
|
} else {
|
|
243
281
|
this.showMessage('操作成功', 'success');
|
|
244
282
|
}
|
|
245
|
-
await this.loadAll();
|
|
246
283
|
} catch (_) {
|
|
247
284
|
this.showMessage('删除失败', 'error');
|
|
248
285
|
}
|
|
249
286
|
},
|
|
250
287
|
|
|
288
|
+
openCloneProviderModal(provider) {
|
|
289
|
+
this.newProvider = {
|
|
290
|
+
name: '',
|
|
291
|
+
url: normalizeProviderUrl(provider.url || ''),
|
|
292
|
+
key: '',
|
|
293
|
+
useTransform: !!(provider.codexmate_bridge || '').trim() || /\/bridge\/openai\//.test(provider.url || '')
|
|
294
|
+
};
|
|
295
|
+
this.showAddModal = true;
|
|
296
|
+
},
|
|
297
|
+
|
|
251
298
|
async openEditModal(provider) {
|
|
252
299
|
const requestId = Symbol('openEditModal');
|
|
253
300
|
this._openEditModalRequestId = requestId;
|
|
@@ -319,9 +366,22 @@ export function createProvidersMethods(options = {}) {
|
|
|
319
366
|
this.showMessage(res.error, 'error');
|
|
320
367
|
return;
|
|
321
368
|
}
|
|
369
|
+
|
|
370
|
+
// 本地更新:更新列表中对应 provider 的 url 和 key
|
|
371
|
+
this.providersList = this.providersList.map(p => {
|
|
372
|
+
if (p.name === validation.name) {
|
|
373
|
+
return {
|
|
374
|
+
...p,
|
|
375
|
+
url: validation.url,
|
|
376
|
+
key: params.key ? maskKeyLocal(params.key) : p.key,
|
|
377
|
+
hasKey: params.key ? true : p.hasKey
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return p;
|
|
381
|
+
});
|
|
382
|
+
|
|
322
383
|
this.closeEditModal();
|
|
323
384
|
this.showMessage('操作成功', 'success');
|
|
324
|
-
await this.loadAll();
|
|
325
385
|
} catch (e) {
|
|
326
386
|
this.showMessage('更新失败', 'error');
|
|
327
387
|
}
|
|
@@ -356,13 +416,26 @@ export function createProvidersMethods(options = {}) {
|
|
|
356
416
|
return this.showMessage('请输入模型', 'error');
|
|
357
417
|
}
|
|
358
418
|
try {
|
|
359
|
-
const
|
|
419
|
+
const modelName = this.newModelName.trim();
|
|
420
|
+
const res = await api('add-model', { model: modelName });
|
|
360
421
|
if (res.error) {
|
|
361
422
|
this.showMessage(res.error, 'error');
|
|
362
423
|
} else {
|
|
424
|
+
// 本地更新:在当前 provider 的 models 中追加
|
|
425
|
+
this.providersList = this.providersList.map(p => {
|
|
426
|
+
if (p.name === this.currentProvider) {
|
|
427
|
+
const exists = p.models.some(m => m.id === modelName);
|
|
428
|
+
if (!exists) {
|
|
429
|
+
return {
|
|
430
|
+
...p,
|
|
431
|
+
models: [...p.models, { id: modelName, name: modelName, cost: null, contextWindow: undefined, maxTokens: undefined }]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return p;
|
|
436
|
+
});
|
|
363
437
|
this.showMessage('操作成功', 'success');
|
|
364
438
|
this.closeModelModal();
|
|
365
|
-
await this.loadAll();
|
|
366
439
|
}
|
|
367
440
|
} catch (_) {
|
|
368
441
|
this.showMessage('新增模型失败', 'error');
|
|
@@ -375,8 +448,17 @@ export function createProvidersMethods(options = {}) {
|
|
|
375
448
|
if (res.error) {
|
|
376
449
|
this.showMessage(res.error, 'error');
|
|
377
450
|
} else {
|
|
451
|
+
// 本地更新:从当前 provider 的 models 中移除
|
|
452
|
+
this.providersList = this.providersList.map(p => {
|
|
453
|
+
if (p.name === this.currentProvider) {
|
|
454
|
+
return {
|
|
455
|
+
...p,
|
|
456
|
+
models: p.models.filter(m => m.id !== model)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return p;
|
|
460
|
+
});
|
|
378
461
|
this.showMessage('操作成功', 'success');
|
|
379
|
-
await this.loadAll();
|
|
380
462
|
}
|
|
381
463
|
} catch (_) {
|
|
382
464
|
this.showMessage('删除模型失败', 'error');
|
|
@@ -407,6 +489,41 @@ export function createProvidersMethods(options = {}) {
|
|
|
407
489
|
: null;
|
|
408
490
|
const key = config ? config.apiKey : '';
|
|
409
491
|
return this.formatKey(key);
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
async loadLocalBridgeExcluded() {
|
|
495
|
+
try {
|
|
496
|
+
const res = await api('local-bridge-get-excluded');
|
|
497
|
+
if (res && Array.isArray(res.excludedProviders)) {
|
|
498
|
+
this.localBridgeExcluded = res.excludedProviders;
|
|
499
|
+
}
|
|
500
|
+
} catch (e) { /* ignore */ }
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
async toggleLocalBridgeExcluded(providerName) {
|
|
504
|
+
const name = String(providerName || '').trim();
|
|
505
|
+
if (!name) return;
|
|
506
|
+
const idx = this.localBridgeExcluded.indexOf(name);
|
|
507
|
+
const next = [...this.localBridgeExcluded];
|
|
508
|
+
if (idx >= 0) {
|
|
509
|
+
next.splice(idx, 1);
|
|
510
|
+
} else {
|
|
511
|
+
next.push(name);
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const res = await api('local-bridge-set-excluded', { names: next });
|
|
515
|
+
if (res && !res.error) {
|
|
516
|
+
this.localBridgeExcluded = next;
|
|
517
|
+
}
|
|
518
|
+
} catch (e) { /* ignore */ }
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
isLocalBridgeExcluded(providerName) {
|
|
522
|
+
return this.localBridgeExcluded.indexOf(String(providerName || '').trim()) >= 0;
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
localBridgeCandidateProviders() {
|
|
526
|
+
return (this.providersList || []).filter(p => p && p.name !== 'local' && p.name !== 'codexmate-proxy' && p.codexmate_bridge !== 'local');
|
|
410
527
|
}
|
|
411
528
|
};
|
|
412
529
|
}
|
|
@@ -251,7 +251,7 @@ export function createSessionActionMethods(options = {}) {
|
|
|
251
251
|
|
|
252
252
|
getShareCommandPrefixInvocation() {
|
|
253
253
|
const prefix = this.normalizeShareCommandPrefix(this.shareCommandPrefix);
|
|
254
|
-
return prefix === 'codexmate' ? 'codexmate' : 'npm start';
|
|
254
|
+
return prefix === 'codexmate' ? 'codexmate' : 'npm start --';
|
|
255
255
|
},
|
|
256
256
|
|
|
257
257
|
setShareCommandPrefix(value) {
|
|
@@ -693,7 +693,7 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
693
693
|
for (const session of visible) {
|
|
694
694
|
if (!session || typeof session !== 'object') continue;
|
|
695
695
|
const messageCountRaw = Number(session.messageCount);
|
|
696
|
-
const shouldHydrate = !Number.isFinite(messageCountRaw) || messageCountRaw === 0;
|
|
696
|
+
const shouldHydrate = !Number.isFinite(messageCountRaw) || (messageCountRaw === 0 && !session.__messageCountExact);
|
|
697
697
|
if (!shouldHydrate) continue;
|
|
698
698
|
const key = this.getSessionExportKey(session);
|
|
699
699
|
if (!key) continue;
|
|
@@ -221,13 +221,12 @@ export function createSessionTrashMethods(options = {}) {
|
|
|
221
221
|
async switchSettingsTab(tab, options = {}) {
|
|
222
222
|
const nextTab = this.normalizeSettingsTab(tab);
|
|
223
223
|
this.settingsTab = nextTab;
|
|
224
|
+
if (typeof this.saveNavState === 'function') {
|
|
225
|
+
this.saveNavState();
|
|
226
|
+
}
|
|
224
227
|
if (nextTab !== 'data') {
|
|
225
228
|
return;
|
|
226
229
|
}
|
|
227
|
-
const forceRefresh = options.forceRefresh === true;
|
|
228
|
-
if (forceRefresh || !this.sessionTrashLoadedOnce) {
|
|
229
|
-
await this.loadSessionTrash({ forceRefresh });
|
|
230
|
-
}
|
|
231
230
|
},
|
|
232
231
|
|
|
233
232
|
async loadSessionTrashCount(options = {}) {
|
|
@@ -118,6 +118,7 @@ export function createStartupClaudeMethods(options = {}) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
this.providersList = listRes.providers;
|
|
121
|
+
if (typeof this.loadLocalBridgeExcluded === 'function') { this.loadLocalBridgeExcluded(); }
|
|
121
122
|
if (statusRes.configReady === false) {
|
|
122
123
|
this.showMessage('配置已加载', 'info');
|
|
123
124
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { api } from './api.mjs';
|
|
2
|
+
|
|
3
|
+
export function createWebhookMethods() {
|
|
4
|
+
return {
|
|
5
|
+
async loadWebhookSettings() {
|
|
6
|
+
try {
|
|
7
|
+
const data = await api('get-webhook-config');
|
|
8
|
+
if (data && typeof data === 'object' && !data.error) {
|
|
9
|
+
this.webhookConfig = {
|
|
10
|
+
enabled: !!data.enabled,
|
|
11
|
+
url: typeof data.url === 'string' ? data.url : '',
|
|
12
|
+
events: Array.isArray(data.events) && data.events.length
|
|
13
|
+
? data.events.slice()
|
|
14
|
+
: this.webhookEventOptions.slice()
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
this.webhookTestResult = { ok: false, error: e && e.message ? e.message : String(e) };
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async saveWebhookSettings() {
|
|
23
|
+
this.webhookSaving = true;
|
|
24
|
+
try {
|
|
25
|
+
const cfg = {
|
|
26
|
+
enabled: !!this.webhookConfig.enabled,
|
|
27
|
+
url: typeof this.webhookConfig.url === 'string' ? this.webhookConfig.url.trim() : '',
|
|
28
|
+
events: Array.isArray(this.webhookConfig.events) ? this.webhookConfig.events.slice() : []
|
|
29
|
+
};
|
|
30
|
+
const saved = await api('set-webhook-config', { config: cfg });
|
|
31
|
+
if (saved && typeof saved === 'object' && !saved.error) {
|
|
32
|
+
this.webhookConfig = {
|
|
33
|
+
enabled: !!saved.enabled,
|
|
34
|
+
url: typeof saved.url === 'string' ? saved.url : '',
|
|
35
|
+
events: Array.isArray(saved.events) ? saved.events.slice() : []
|
|
36
|
+
};
|
|
37
|
+
this.webhookTestResult = { ok: true, status: 'saved' };
|
|
38
|
+
} else {
|
|
39
|
+
this.webhookTestResult = { ok: false, error: (saved && saved.error) || 'save failed' };
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
this.webhookTestResult = { ok: false, error: e && e.message ? e.message : String(e) };
|
|
43
|
+
} finally {
|
|
44
|
+
this.webhookSaving = false;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async testWebhook() {
|
|
49
|
+
this.webhookTesting = true;
|
|
50
|
+
try {
|
|
51
|
+
const cfg = {
|
|
52
|
+
enabled: true,
|
|
53
|
+
url: typeof this.webhookConfig.url === 'string' ? this.webhookConfig.url.trim() : '',
|
|
54
|
+
events: Array.isArray(this.webhookConfig.events) && this.webhookConfig.events.length
|
|
55
|
+
? this.webhookConfig.events.slice()
|
|
56
|
+
: this.webhookEventOptions.slice()
|
|
57
|
+
};
|
|
58
|
+
const r = await api('test-webhook', { config: cfg });
|
|
59
|
+
this.webhookTestResult = r || { ok: false, error: 'no result' };
|
|
60
|
+
} catch (e) {
|
|
61
|
+
this.webhookTestResult = { ok: false, error: e && e.message ? e.message : String(e) };
|
|
62
|
+
} finally {
|
|
63
|
+
this.webhookTesting = false;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
toggleWebhookEvent(eventName) {
|
|
68
|
+
if (!Array.isArray(this.webhookConfig.events)) {
|
|
69
|
+
this.webhookConfig.events = [];
|
|
70
|
+
}
|
|
71
|
+
const idx = this.webhookConfig.events.indexOf(eventName);
|
|
72
|
+
if (idx === -1) {
|
|
73
|
+
this.webhookConfig.events.push(eventName);
|
|
74
|
+
} else {
|
|
75
|
+
this.webhookConfig.events.splice(idx, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|