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.
Files changed (50) hide show
  1. package/cli/builtin-proxy.js +107 -2
  2. package/cli/config-bootstrap.js +30 -12
  3. package/cli/config-health.js +117 -1
  4. package/cli/local-bridge.js +324 -0
  5. package/cli/openai-bridge.js +195 -31
  6. package/cli.js +245 -28
  7. package/lib/cli-webhook.js +126 -0
  8. package/package.json +1 -1
  9. package/web-ui/app.js +28 -8
  10. package/web-ui/index.html +1 -0
  11. package/web-ui/logic.codex.mjs +13 -0
  12. package/web-ui/modules/app.computed.dashboard.mjs +25 -2
  13. package/web-ui/modules/app.computed.session.mjs +22 -17
  14. package/web-ui/modules/app.methods.claude-config.mjs +12 -2
  15. package/web-ui/modules/app.methods.codex-config.mjs +25 -0
  16. package/web-ui/modules/app.methods.index.mjs +2 -0
  17. package/web-ui/modules/app.methods.navigation.mjs +39 -8
  18. package/web-ui/modules/app.methods.providers.mjs +125 -8
  19. package/web-ui/modules/app.methods.session-actions.mjs +1 -1
  20. package/web-ui/modules/app.methods.session-browser.mjs +1 -1
  21. package/web-ui/modules/app.methods.session-trash.mjs +3 -4
  22. package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
  23. package/web-ui/modules/app.methods.webhook.mjs +79 -0
  24. package/web-ui/modules/i18n.dict.mjs +1109 -72
  25. package/web-ui/modules/i18n.mjs +9 -3
  26. package/web-ui/modules/skills.methods.mjs +1 -0
  27. package/web-ui/partials/index/layout-header.html +25 -0
  28. package/web-ui/partials/index/modals-basic.html +0 -3
  29. package/web-ui/partials/index/panel-config-claude.html +8 -2
  30. package/web-ui/partials/index/panel-config-codex.html +28 -3
  31. package/web-ui/partials/index/panel-dashboard.html +33 -0
  32. package/web-ui/partials/index/panel-market.html +3 -3
  33. package/web-ui/partials/index/panel-plugins.html +2 -2
  34. package/web-ui/partials/index/panel-sessions.html +1 -9
  35. package/web-ui/partials/index/panel-settings.html +71 -134
  36. package/web-ui/partials/index/panel-trash.html +88 -0
  37. package/web-ui/session-helpers.mjs +20 -2
  38. package/web-ui/styles/dashboard.css +132 -0
  39. package/web-ui/styles/docs-panel.css +63 -39
  40. package/web-ui/styles/layout-shell.css +54 -34
  41. package/web-ui/styles/plugins-panel.css +121 -80
  42. package/web-ui/styles/sessions-list.css +41 -43
  43. package/web-ui/styles/sessions-preview.css +34 -38
  44. package/web-ui/styles/sessions-toolbar-trash.css +31 -27
  45. package/web-ui/styles/settings-panel.css +197 -33
  46. package/web-ui/styles/skills-list.css +12 -10
  47. package/web-ui/styles/skills-market.css +67 -44
  48. package/web-ui/styles/trash-panel.css +90 -0
  49. package/web-ui/styles/webhook.css +81 -0
  50. 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
- await this.loadAll();
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 res = await api('add-model', { model: this.newModelName.trim() });
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
+ }