codexmate 0.0.26 → 0.0.28

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 (41) hide show
  1. package/README.md +7 -2
  2. package/README.zh.md +7 -2
  3. package/cli/builtin-proxy.js +636 -95
  4. package/cli/openai-bridge.js +497 -5
  5. package/cli.js +75 -29
  6. package/lib/cli-models-utils.js +71 -10
  7. package/package.json +3 -1
  8. package/plugins/prompt-templates/computed.mjs +1 -1
  9. package/plugins/prompt-templates/methods.mjs +0 -66
  10. package/plugins/prompt-templates/overview.mjs +1 -0
  11. package/web-ui/app.js +16 -16
  12. package/web-ui/logic.codex.mjs +56 -0
  13. package/web-ui/logic.sessions.mjs +56 -0
  14. package/web-ui/modules/app.computed.dashboard.mjs +54 -0
  15. package/web-ui/modules/app.computed.session.mjs +48 -0
  16. package/web-ui/modules/app.methods.claude-config.mjs +18 -7
  17. package/web-ui/modules/app.methods.codex-config.mjs +35 -3
  18. package/web-ui/modules/app.methods.providers.mjs +9 -1
  19. package/web-ui/modules/app.methods.session-actions.mjs +2 -5
  20. package/web-ui/modules/app.methods.session-browser.mjs +4 -5
  21. package/web-ui/modules/app.methods.session-trash.mjs +19 -4
  22. package/web-ui/modules/app.methods.startup-claude.mjs +12 -1
  23. package/web-ui/modules/i18n.dict.mjs +28 -32
  24. package/web-ui/modules/provider-url-display.mjs +17 -0
  25. package/web-ui/partials/index/panel-config-claude.html +5 -1
  26. package/web-ui/partials/index/panel-config-codex.html +33 -4
  27. package/web-ui/partials/index/panel-plugins.html +3 -29
  28. package/web-ui/partials/index/panel-sessions.html +0 -10
  29. package/web-ui/partials/index/panel-settings.html +62 -67
  30. package/web-ui/partials/index/panel-usage.html +31 -2
  31. package/web-ui/session-helpers.mjs +2 -2
  32. package/web-ui/styles/base-theme.css +47 -34
  33. package/web-ui/styles/controls-forms.css +27 -28
  34. package/web-ui/styles/layout-shell.css +37 -34
  35. package/web-ui/styles/modals-core.css +12 -10
  36. package/web-ui/styles/navigation-panels.css +36 -35
  37. package/web-ui/styles/responsive.css +4 -4
  38. package/web-ui/styles/sessions-list.css +10 -6
  39. package/web-ui/styles/sessions-usage.css +95 -0
  40. package/web-ui/styles/settings-panel.css +19 -0
  41. package/web-ui/styles/titles-cards.css +90 -26
package/cli.js CHANGED
@@ -222,6 +222,7 @@ const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gp
222
222
  const SPEED_TEST_TIMEOUT_MS = 8000;
223
223
  const MAX_SESSION_LIST_SIZE = 300;
224
224
  const MAX_SESSION_TRASH_LIST_SIZE = 500;
225
+ const DEFAULT_SESSION_TRASH_RETENTION_DAYS = 30;
225
226
  const MAX_EXPORT_MESSAGES = 1000;
226
227
  const DEFAULT_SESSION_DETAIL_MESSAGES = 300;
227
228
  const MAX_SESSION_DETAIL_MESSAGES = 1000;
@@ -5601,6 +5602,35 @@ function readSessionTrashEntries(options = {}) {
5601
5602
  return normalizedEntries;
5602
5603
  }
5603
5604
 
5605
+ function purgeExpiredSessionTrashEntries(retentionDays) {
5606
+ const days = Number.isFinite(Number(retentionDays)) && Number(retentionDays) > 0
5607
+ ? Math.floor(Number(retentionDays))
5608
+ : DEFAULT_SESSION_TRASH_RETENTION_DAYS;
5609
+ const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
5610
+ const entries = readSessionTrashEntries({ cleanup: false });
5611
+ if (entries.length === 0) {
5612
+ return { purged: 0 };
5613
+ }
5614
+ const remaining = [];
5615
+ let purgedCount = 0;
5616
+ for (const entry of entries) {
5617
+ const deletedAtMs = Date.parse(entry.deletedAt || entry.updatedAt || '') || 0;
5618
+ if (deletedAtMs > 0 && deletedAtMs < cutoffMs) {
5619
+ const trashFilePath = resolveSessionTrashFilePath(entry);
5620
+ if (trashFilePath) {
5621
+ try { fs.unlinkSync(trashFilePath); } catch (_) {}
5622
+ }
5623
+ purgedCount += 1;
5624
+ } else {
5625
+ remaining.push(entry);
5626
+ }
5627
+ }
5628
+ if (purgedCount > 0) {
5629
+ writeSessionTrashEntries(remaining);
5630
+ }
5631
+ return { purged: purgedCount };
5632
+ }
5633
+
5604
5634
  function buildSessionTrashEntry(summary, options = {}) {
5605
5635
  const source = options.source === 'claude' ? 'claude' : 'codex';
5606
5636
  const sessionId = options.sessionId || summary.sessionId || path.basename(options.originalFilePath || summary.filePath || '', '.jsonl');
@@ -5812,6 +5842,9 @@ async function listSessionTrashItems(params = {}) {
5812
5842
  const limit = Number.isFinite(rawLimit)
5813
5843
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
5814
5844
  : 200;
5845
+ if (params.autoPurge !== false) {
5846
+ purgeExpiredSessionTrashEntries(params.retentionDays);
5847
+ }
5815
5848
  const allEntries = readSessionTrashEntries();
5816
5849
  let items = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy'
5817
5850
  ? allEntries.filter((entry) => entry.source === source)
@@ -9984,6 +10017,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
9984
10017
  result = {
9985
10018
  provider: config.model_provider || '未设置',
9986
10019
  model: config.model || '未设置',
10020
+ currentModels: readCurrentModels(),
9987
10021
  serviceTier,
9988
10022
  modelReasoningEffort,
9989
10023
  modelContextWindow,
@@ -12327,35 +12361,47 @@ function buildMcpProviderListPayload() {
12327
12361
  configReady: !listConfigResult.isVirtual,
12328
12362
  configErrorType: listConfigResult.errorType || '',
12329
12363
  configNotice: listConfigResult.reason || '',
12330
- providers: Object.entries(providers).map(([name, p]) => ({
12331
- name,
12332
- url: p.base_url || '',
12333
- key: maskKey(p.preferred_auth_method || ''),
12334
- hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
12335
- models: Array.isArray(p.models)
12336
- ? p.models
12337
- .filter((model) => model && typeof model === 'object' && !Array.isArray(model))
12338
- .map((model) => ({
12339
- id: typeof model.id === 'string' ? model.id : '',
12340
- name: typeof model.name === 'string' ? model.name : '',
12341
- cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
12342
- ? {
12343
- input: model.cost.input,
12344
- output: model.cost.output,
12345
- cacheRead: model.cost.cacheRead,
12346
- cacheWrite: model.cost.cacheWrite
12347
- }
12348
- : null,
12349
- contextWindow: model.contextWindow,
12350
- maxTokens: model.maxTokens
12351
- }))
12352
- .filter((model) => model.id)
12353
- : [],
12354
- current: name === current,
12355
- readOnly: isBuiltinManagedProvider(name),
12356
- nonDeletable: isNonDeletableProvider(name),
12357
- nonEditable: isNonEditableProvider(name)
12358
- }))
12364
+ providers: Object.entries(providers).map(([name, p]) => {
12365
+ const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
12366
+ let upstreamUrl = '';
12367
+ if (bridge === 'openai') {
12368
+ const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
12369
+ if (upstream && !upstream.error && typeof upstream.baseUrl === 'string') {
12370
+ upstreamUrl = upstream.baseUrl.trim();
12371
+ }
12372
+ }
12373
+ return {
12374
+ name,
12375
+ url: p.base_url || '',
12376
+ upstreamUrl,
12377
+ codexmate_bridge: bridge,
12378
+ key: maskKey(p.preferred_auth_method || ''),
12379
+ hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
12380
+ models: Array.isArray(p.models)
12381
+ ? p.models
12382
+ .filter((model) => model && typeof model === 'object' && !Array.isArray(model))
12383
+ .map((model) => ({
12384
+ id: typeof model.id === 'string' ? model.id : '',
12385
+ name: typeof model.name === 'string' ? model.name : '',
12386
+ cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
12387
+ ? {
12388
+ input: model.cost.input,
12389
+ output: model.cost.output,
12390
+ cacheRead: model.cost.cacheRead,
12391
+ cacheWrite: model.cost.cacheWrite
12392
+ }
12393
+ : null,
12394
+ contextWindow: model.contextWindow,
12395
+ maxTokens: model.maxTokens
12396
+ }))
12397
+ .filter((model) => model.id)
12398
+ : [],
12399
+ current: name === current,
12400
+ readOnly: isBuiltinManagedProvider(name),
12401
+ nonDeletable: isNonDeletableProvider(name),
12402
+ nonEditable: isNonEditableProvider(name)
12403
+ };
12404
+ })
12359
12405
  };
12360
12406
  }
12361
12407
 
@@ -48,37 +48,86 @@ const ANTHROPIC_CLAUDE_MODELS = Object.freeze([
48
48
  'claude-3-haiku'
49
49
  ]);
50
50
 
51
+ const DEEPSEEK_CLAUDE_COMPAT_MODELS = Object.freeze([
52
+ 'DeepSeek-V3.2',
53
+ 'DeepSeek-V3',
54
+ 'DeepSeek-R1',
55
+ 'deepseek-chat'
56
+ ]);
57
+
58
+ const QWEN_CLAUDE_COMPAT_MODELS = Object.freeze([
59
+ 'qwen3-coder',
60
+ 'qwen-max',
61
+ 'qwen-plus',
62
+ 'qwen-turbo'
63
+ ]);
64
+
65
+ const MODELSCOPE_CLAUDE_COMPAT_MODELS = Object.freeze([
66
+ 'ZhipuAI/GLM-5'
67
+ ]);
68
+
51
69
  function normalizeModelCatalogId(value) {
52
70
  return typeof value === 'string' ? value.trim().toLowerCase() : '';
53
71
  }
54
72
 
55
- function isBigModelClaudeCompatibleBaseUrl(baseUrl) {
73
+ function hasPathSegment(baseUrl, segment) {
56
74
  const normalized = normalizeBaseUrl(baseUrl);
57
75
  if (!normalized) return false;
58
76
  try {
59
77
  const parsed = new URL(normalized);
60
- const host = String(parsed.hostname || '').toLowerCase();
61
78
  const pathname = String(parsed.pathname || '').toLowerCase();
62
- const isBigModelHost = host === 'bigmodel.cn' || host.endsWith('.bigmodel.cn');
63
- const hasAnthropicSegment = /(^|\/)anthropic(\/|$)/.test(pathname);
64
- return isBigModelHost && hasAnthropicSegment;
79
+ const escaped = String(segment || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&').toLowerCase();
80
+ return new RegExp(`(^|/)${escaped}(/|$)`).test(pathname);
65
81
  } catch (_) {
66
82
  return false;
67
83
  }
68
84
  }
69
85
 
70
- function isAnthropicBaseUrl(baseUrl) {
86
+ function getBaseUrlHost(baseUrl) {
71
87
  const normalized = normalizeBaseUrl(baseUrl);
72
- if (!normalized) return false;
88
+ if (!normalized) return '';
73
89
  try {
74
90
  const parsed = new URL(normalized);
75
- const host = String(parsed.hostname || '').toLowerCase();
76
- return host === 'api.anthropic.com' || host.endsWith('.anthropic.com');
91
+ return String(parsed.hostname || '').toLowerCase();
77
92
  } catch (_) {
78
- return false;
93
+ return '';
79
94
  }
80
95
  }
81
96
 
97
+ function isHostOrSubdomain(host, domain) {
98
+ return host === domain || host.endsWith(`.${domain}`);
99
+ }
100
+
101
+ function isBigModelClaudeCompatibleBaseUrl(baseUrl) {
102
+ const host = getBaseUrlHost(baseUrl);
103
+ return isHostOrSubdomain(host, 'bigmodel.cn') && hasPathSegment(baseUrl, 'anthropic');
104
+ }
105
+
106
+ function isAnthropicBaseUrl(baseUrl) {
107
+ const host = getBaseUrlHost(baseUrl);
108
+ return isHostOrSubdomain(host, 'anthropic.com');
109
+ }
110
+
111
+ function isDeepSeekClaudeCompatibleBaseUrl(baseUrl) {
112
+ const host = getBaseUrlHost(baseUrl);
113
+ return isHostOrSubdomain(host, 'deepseek.com') && hasPathSegment(baseUrl, 'anthropic');
114
+ }
115
+
116
+ function isQwenClaudeCompatibleBaseUrl(baseUrl) {
117
+ const host = getBaseUrlHost(baseUrl);
118
+ return isHostOrSubdomain(host, 'dashscope.aliyuncs.com') && hasPathSegment(baseUrl, 'anthropic');
119
+ }
120
+
121
+ function isZaiClaudeCompatibleBaseUrl(baseUrl) {
122
+ const host = getBaseUrlHost(baseUrl);
123
+ return isHostOrSubdomain(host, 'z.ai') && hasPathSegment(baseUrl, 'anthropic');
124
+ }
125
+
126
+ function isModelScopeBaseUrl(baseUrl) {
127
+ const host = getBaseUrlHost(baseUrl);
128
+ return isHostOrSubdomain(host, 'modelscope.cn');
129
+ }
130
+
82
131
  function getSupplementalModelsForBaseUrl(baseUrl) {
83
132
  if (isBigModelClaudeCompatibleBaseUrl(baseUrl)) {
84
133
  return [...BIGMODEL_CLAUDE_COMPAT_MODELS];
@@ -86,6 +135,18 @@ function getSupplementalModelsForBaseUrl(baseUrl) {
86
135
  if (isAnthropicBaseUrl(baseUrl)) {
87
136
  return [...ANTHROPIC_CLAUDE_MODELS];
88
137
  }
138
+ if (isDeepSeekClaudeCompatibleBaseUrl(baseUrl)) {
139
+ return [...DEEPSEEK_CLAUDE_COMPAT_MODELS];
140
+ }
141
+ if (isQwenClaudeCompatibleBaseUrl(baseUrl)) {
142
+ return [...QWEN_CLAUDE_COMPAT_MODELS];
143
+ }
144
+ if (isZaiClaudeCompatibleBaseUrl(baseUrl)) {
145
+ return [...BIGMODEL_CLAUDE_COMPAT_MODELS];
146
+ }
147
+ if (isModelScopeBaseUrl(baseUrl)) {
148
+ return [...MODELSCOPE_CLAUDE_COMPAT_MODELS];
149
+ }
89
150
  return [];
90
151
  }
91
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -40,6 +40,8 @@
40
40
  "test:ci": "node tools/ci/run-check.js all",
41
41
  "test:unit": "node tests/unit/run.mjs",
42
42
  "test:e2e": "node tests/e2e/run.js",
43
+ "setup:git": "git remote set-url origin https://github.com/SakuraByteCore/codexmate.git && gh auth setup-git",
44
+ "reset:dev": "node tools/dev/reset-and-dev.js",
43
45
  "pretest": "node tools/ci/ensure-test-deps.js"
44
46
  },
45
47
  "dependencies": {
@@ -98,7 +98,7 @@ function renderTemplate(templateText, values = {}) {
98
98
  const name = String(key || '').trim();
99
99
  if (!name) return '';
100
100
  const value = map[name];
101
- return value == null ? '' : String(value);
101
+ return value == null || String(value).trim() === '' ? _whole : String(value);
102
102
  });
103
103
  }
104
104
 
@@ -358,72 +358,6 @@ export function createPluginsMethods() {
358
358
  this.promptTemplateVarValuesRaw = {};
359
359
  },
360
360
 
361
- addPromptTemplateVariable() {
362
- const draft = normalizePromptTemplateDraft(this.promptTemplateDraftRaw);
363
- if (!draft || !draft.id) return;
364
- if (draft.isBuiltin) {
365
- this.showMessage(typeof this.t === 'function' ? this.t('toast.templates.builtinNotEditable') : 'Built-in templates are not editable', 'error');
366
- return;
367
- }
368
- this.promptTemplateVarDraftName = 'var';
369
- this.promptTemplateVarDraftError = '';
370
- this.showPromptTemplateVarModal = true;
371
- if (typeof this.$nextTick === 'function') {
372
- this.$nextTick(() => {
373
- const input = this.$refs && this.$refs.promptTemplateVarNameInput
374
- ? this.$refs.promptTemplateVarNameInput
375
- : null;
376
- if (input && typeof input.focus === 'function') input.focus();
377
- });
378
- }
379
- },
380
-
381
- closePromptTemplateVarModal() {
382
- this.showPromptTemplateVarModal = false;
383
- this.promptTemplateVarDraftError = '';
384
- },
385
-
386
- confirmAddPromptTemplateVariable() {
387
- const draft = normalizePromptTemplateDraft(this.promptTemplateDraftRaw);
388
- if (!draft || !draft.id) return;
389
- if (draft.isBuiltin) {
390
- this.promptTemplateVarDraftError = typeof this.t === 'function'
391
- ? this.t('toast.templates.builtinNotEditable')
392
- : 'Built-in templates are not editable';
393
- return;
394
- }
395
- const key = typeof this.promptTemplateVarDraftName === 'string'
396
- ? this.promptTemplateVarDraftName.trim()
397
- : '';
398
- if (!key) {
399
- this.promptTemplateVarDraftError = typeof this.t === 'function'
400
- ? this.t('toast.templates.varNameRequired')
401
- : 'Variable name is required';
402
- return;
403
- }
404
- if (!/^[a-zA-Z0-9_.-]+$/.test(key)) {
405
- this.promptTemplateVarDraftError = typeof this.t === 'function'
406
- ? this.t('toast.templates.varNameInvalid')
407
- : 'Variable name may only contain letters, numbers, underscore, dash, dot';
408
- return;
409
- }
410
- const placeholder = `{{${key}}}`;
411
- const current = typeof draft.template === 'string' ? draft.template : '';
412
- if (current.includes(placeholder)) {
413
- this.promptTemplateVarDraftError = typeof this.t === 'function'
414
- ? this.t('toast.templates.varExists')
415
- : 'Variable already exists';
416
- return;
417
- }
418
- const nextText = current && !current.endsWith('\n')
419
- ? `${current}\n${placeholder}\n`
420
- : `${current}${placeholder}\n`;
421
- this.promptTemplateDraftRaw = { ...draft, template: nextText };
422
- this.showPromptTemplateVarModal = false;
423
- this.promptTemplateVarDraftError = '';
424
- this.showMessage(typeof this.t === 'function' ? this.t('toast.templates.varAdded') : 'Variable added', 'success');
425
- },
426
-
427
361
  setPromptVariableValue(name, value) {
428
362
  const key = typeof name === 'string' ? name.trim() : '';
429
363
  if (!key) return;
@@ -7,6 +7,7 @@ import {
7
7
  import { buildBuiltinCommentPolishTemplate } from './comment-polish/index.mjs';
8
8
  import { buildBuiltinRuleAckTemplate } from './rule-ack/index.mjs';
9
9
 
10
+
10
11
  function ensureBuiltinTemplates(rawList, builtins) {
11
12
  const list = Array.isArray(rawList) ? rawList.filter(Boolean) : [];
12
13
  const builtinList = Array.isArray(builtins) ? builtins.filter(Boolean) : [];
package/web-ui/app.js CHANGED
@@ -34,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
34
34
  configMode: 'codex',
35
35
  currentProvider: '',
36
36
  currentModel: '',
37
+ currentModels: {},
37
38
  serviceTier: 'fast',
38
39
  modelReasoningEffort: 'medium',
39
40
  modelContextWindowInput: String(DEFAULT_MODEL_CONTEXT_WINDOW),
@@ -81,9 +82,6 @@ document.addEventListener('DOMContentLoaded', () => {
81
82
  promptComposerPickerKeyword: '',
82
83
  promptComposerSelectedTemplateId: '',
83
84
  promptComposerVarValuesRaw: {},
84
- showPromptTemplateVarModal: false,
85
- promptTemplateVarDraftName: '',
86
- promptTemplateVarDraftError: '',
87
85
  showConfirmDialog: false,
88
86
  confirmDialogTitle: '',
89
87
  confirmDialogMessage: '',
@@ -112,7 +110,6 @@ document.addEventListener('DOMContentLoaded', () => {
112
110
  _pendingCodexApplyOptions: null,
113
111
  agentsContent: '',
114
112
  agentsPath: '',
115
- agentsPath: '',
116
113
  agentsExists: false,
117
114
  agentsLineEnding: '\n',
118
115
  agentsLoading: false,
@@ -158,7 +155,7 @@ document.addEventListener('DOMContentLoaded', () => {
158
155
  ticket: 0
159
156
  },
160
157
  sessionsViewMode: 'browser',
161
- sessionsUsageTimeRange: '7d',
158
+ sessionsUsageTimeRange: (function () { try { const saved = localStorage.getItem('sessionsUsageTimeRange'); if (saved === '7d' || saved === '30d' || saved === 'all') return saved; } catch (_) {} return '7d'; })(),
162
159
  sessionsUsageList: [],
163
160
  sessionsUsageCompareEnabled: false,
164
161
  sessionsUsageSelectedDayKey: '',
@@ -175,7 +172,6 @@ document.addEventListener('DOMContentLoaded', () => {
175
172
  sessionRoleFilter: 'all',
176
173
  sessionTimePreset: 'all',
177
174
  sessionSortMode: 'time',
178
- sessionResumeWithYolo: true,
179
175
  sessionPathOptions: [],
180
176
  sessionPathOptionsLoading: false,
181
177
  sessionPathOptionsMap: {
@@ -259,7 +255,7 @@ document.addEventListener('DOMContentLoaded', () => {
259
255
  installRegistryPreset: 'default',
260
256
  installRegistryCustom: '',
261
257
  installStatusTargets: null,
262
- newProvider: { name: '', url: '', key: '', useTransform: false },
258
+ newProvider: { name: '', url: '', key: '', useTransform: false, _suggestedModel: '' },
263
259
  resetConfigLoading: false,
264
260
  editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
265
261
  newModelName: '',
@@ -346,7 +342,7 @@ document.addEventListener('DOMContentLoaded', () => {
346
342
  codexDownloadLoading: false,
347
343
  codexDownloadProgress: 0,
348
344
  codexDownloadTimer: null,
349
- settingsTab: 'backup',
345
+ settingsTab: 'general',
350
346
  sessionTrashEnabled: true,
351
347
  sessionTrashItems: [],
352
348
  sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
@@ -363,6 +359,7 @@ document.addEventListener('DOMContentLoaded', () => {
363
359
  sessionTrashRestoring: {},
364
360
  sessionTrashPurging: {},
365
361
  sessionTrashClearing: false,
362
+ sessionTrashRetentionDays: 30,
366
363
  claudeImportLoading: false,
367
364
  codexImportLoading: false,
368
365
  codexAuthProfiles: [],
@@ -469,16 +466,11 @@ document.addEventListener('DOMContentLoaded', () => {
469
466
  if (!this.taskOrchestrationTabEnabled && this.mainTab === 'orchestration') {
470
467
  this.mainTab = 'config';
471
468
  }
472
- const savedSessionYolo = localStorage.getItem('codexmateSessionResumeYolo');
473
- if (savedSessionYolo === '0' || savedSessionYolo === 'false') {
474
- this.sessionResumeWithYolo = false;
475
- } else if (savedSessionYolo === '1' || savedSessionYolo === 'true') {
476
- this.sessionResumeWithYolo = true;
477
- }
478
469
  this.restoreSessionFilterCache();
479
470
  this.restoreSessionPinnedMap();
480
471
  this.shareCommandPrefix = this.normalizeShareCommandPrefix(localStorage.getItem('codexmateShareCommandPrefix'));
481
472
  this.sessionTrashEnabled = this.normalizeSessionTrashEnabled(localStorage.getItem('codexmateSessionTrashEnabled'));
473
+ this.sessionTrashRetentionDays = this.normalizeSessionTrashRetentionDays(localStorage.getItem('codexmateSessionTrashRetentionDays'));
482
474
  this.configTemplateDiffConfirmEnabled = loadConfigTemplateDiffConfirmEnabledFromStorage(localStorage);
483
475
  window.addEventListener('resize', this.onWindowResize);
484
476
  window.addEventListener('keydown', this.handleGlobalKeydown);
@@ -498,14 +490,22 @@ document.addEventListener('DOMContentLoaded', () => {
498
490
  console.error('加载 Claude 配置失败:', e);
499
491
  }
500
492
  }
493
+ {
494
+ const savedCurrentClaudeConfig = localStorage.getItem('currentClaudeConfig');
495
+ if (savedCurrentClaudeConfig && this.claudeConfigs[savedCurrentClaudeConfig]) {
496
+ this.currentClaudeConfig = savedCurrentClaudeConfig;
497
+ }
498
+ }
501
499
  if (!this.currentClaudeConfig) {
502
500
  const claudeConfigNames = Object.keys(this.claudeConfigs || {});
503
501
  if (claudeConfigNames.length > 0) {
504
502
  this.currentClaudeConfig = claudeConfigNames[0];
505
- const initialClaudeConfig = this.claudeConfigs[this.currentClaudeConfig];
506
- this.currentClaudeModel = initialClaudeConfig && initialClaudeConfig.model ? initialClaudeConfig.model : '';
507
503
  }
508
504
  }
505
+ if (this.currentClaudeConfig && !this.currentClaudeModel) {
506
+ const initialClaudeConfig = this.claudeConfigs[this.currentClaudeConfig];
507
+ this.currentClaudeModel = initialClaudeConfig && initialClaudeConfig.model ? initialClaudeConfig.model : '';
508
+ }
509
509
  const normalizeOpenclawConfigs = (configs) => {
510
510
  const source = configs && typeof configs === 'object' && !Array.isArray(configs)
511
511
  ? configs
@@ -0,0 +1,56 @@
1
+ // 仅供 web-ui 的 codex 模型选择器与新增 provider 模板按钮使用。
2
+ // 镜像 logic.claude.mjs 的派生方式,但 codex provider 元信息不带 wire_api,
3
+ // 所以 catalog 仅按 baseUrl 的 host/path 命中。
4
+
5
+ const DEFAULT_OPENAI_CODEX_CATALOG = Object.freeze([
6
+ 'gpt-5-codex',
7
+ 'gpt-5',
8
+ 'gpt-5-mini',
9
+ 'gpt-4.1',
10
+ 'o4-mini',
11
+ 'o3-mini'
12
+ ]);
13
+
14
+ const HOST_RULES = Object.freeze([
15
+ { match: (u) => /api\.openai\.com/i.test(u), models: DEFAULT_OPENAI_CODEX_CATALOG },
16
+ { match: (u) => /api\.deepseek\.com/i.test(u), models: ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner'] },
17
+ { match: (u) => /dashscope\.aliyuncs\.com/i.test(u), models: ['qwen3-coder-plus', 'qwen3-coder-flash', 'qwen-max', 'qwen-plus'] },
18
+ { match: (u) => /ark\..*volces\.com/i.test(u), models: ['doubao-seed-1-6-thinking', 'doubao-seed-1-6', 'doubao-1-5-pro-32k', 'doubao-pro-32k'] },
19
+ { match: (u) => /open\.bigmodel\.cn/i.test(u), models: ['glm-4.6', 'glm-4.5', 'glm-4-plus', 'glm-coding'] },
20
+ { match: (u) => /api\.moonshot\.cn|api\.kimi\.com/i.test(u), models: ['moonshot-v1-32k', 'moonshot-v1-128k', 'kimi-latest'] },
21
+ { match: (u) => /api\.minimax/i.test(u), models: ['MiniMax-M2', 'abab6.5s-chat', 'abab6.5-chat'] },
22
+ { match: (u) => /api-inference\.modelscope\.cn/i.test(u), models: ['Qwen/Qwen3-Coder-480B-A35B-Instruct', 'ZhipuAI/GLM-4.5'] },
23
+ { match: (u) => /xiaomimimo\.com/i.test(u), models: ['mimo-v2-pro', 'mimo-v2'] },
24
+ { match: (u) => /ai\.muapi\.cn/i.test(u), models: ['mimo-v2-pro'] }
25
+ ]);
26
+
27
+ function normalizeUrl(url) {
28
+ return typeof url === 'string' ? url.trim().toLowerCase() : '';
29
+ }
30
+
31
+ export function getCodexModelCatalogForProvider(provider) {
32
+ if (!provider || typeof provider !== 'object') return [];
33
+ const url = normalizeUrl(provider.url || provider.baseUrl || '');
34
+ const name = typeof provider.name === 'string' ? provider.name.toLowerCase() : '';
35
+ if (!url) {
36
+ if (/openai/.test(name)) return [...DEFAULT_OPENAI_CODEX_CATALOG];
37
+ return [];
38
+ }
39
+ for (const rule of HOST_RULES) {
40
+ if (rule.match(url)) return [...rule.models];
41
+ }
42
+ return [];
43
+ }
44
+
45
+ // 服务模板表:供面板上的预设按钮使用。
46
+ // model 字段为可选首选项(添加后由前端写入内存字典 currentModels[name])。
47
+ // useTransform=true 表示该服务需通过内建 OpenAI bridge 转发。
48
+ export const CODEX_PROVIDER_TEMPLATES = Object.freeze([
49
+ {
50
+ label: 'MuAPI',
51
+ name: 'muapi',
52
+ url: 'https://ai.muapi.cn/v1',
53
+ model: 'mimo-v2-pro',
54
+ useTransform: true
55
+ }
56
+ ]);
@@ -301,6 +301,62 @@ export function buildUsageHeatmap(sessions = [], options = {}) {
301
301
  };
302
302
  }
303
303
 
304
+ export function buildUsageHourlyHeatmap(sessions = [], options = {}) {
305
+ const list = Array.isArray(sessions) ? sessions : [];
306
+ const range = normalizeUsageRange(options.range);
307
+ const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
308
+ const dayMs = 24 * 60 * 60 * 1000;
309
+ const todayStart = toUtcDayStartMs(now);
310
+
311
+ const normalized = [];
312
+ for (const session of list) {
313
+ if (!session || typeof session !== 'object') continue;
314
+ const source = normalizeSessionSource(session.source, '');
315
+ if (source !== 'codex' && source !== 'claude') continue;
316
+ const updatedAtMs = Date.parse(session.updatedAt || '');
317
+ if (!Number.isFinite(updatedAtMs)) continue;
318
+ const dayStart = toUtcDayStartMs(updatedAtMs);
319
+ if (range !== 'all') {
320
+ const rangeDays = range === '30d' ? 30 : 7;
321
+ const rangeStart = todayStart - ((rangeDays - 1) * dayMs);
322
+ if (dayStart < rangeStart || dayStart > todayStart) continue;
323
+ }
324
+ const stamp = new Date(updatedAtMs);
325
+ const weekday = (stamp.getUTCDay() + 6) % 7;
326
+ const hour = stamp.getUTCHours();
327
+ const messageCount = Number.isFinite(Number(session.messageCount))
328
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
329
+ : 0;
330
+ const tokenTotal = readSessionTotalTokens(session);
331
+ normalized.push({ weekday, hour, messageCount, tokenTotal });
332
+ }
333
+
334
+ const grid = Array.from({ length: 7 }, () =>
335
+ Array.from({ length: 24 }, () => ({ sessionCount: 0, messageCount: 0, tokenTotal: 0 }))
336
+ );
337
+ for (const item of normalized) {
338
+ const cell = grid[item.weekday][item.hour];
339
+ cell.sessionCount += 1;
340
+ cell.messageCount += item.messageCount;
341
+ cell.tokenTotal += item.tokenTotal;
342
+ }
343
+
344
+ let maxSessionCount = 0;
345
+ for (let day = 0; day < 7; day += 1) {
346
+ for (let hour = 0; hour < 24; hour += 1) {
347
+ maxSessionCount = Math.max(maxSessionCount, grid[day][hour].sessionCount);
348
+ }
349
+ }
350
+
351
+ return {
352
+ range,
353
+ grid,
354
+ maxSessionCount: Math.max(1, maxSessionCount),
355
+ weekdayKeys: [0, 1, 2, 3, 4, 5, 6],
356
+ hourLabels: Array.from({ length: 24 }, (_, index) => String(index).padStart(2, '0'))
357
+ };
358
+ }
359
+
304
360
  function buildUsageBuckets(normalizedSessions, options = {}) {
305
361
  const range = normalizeUsageRange(options.range);
306
362
  const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();