codexmate 0.0.27 → 0.0.29

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 (51) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/cli/builtin-proxy.js +430 -4
  4. package/cli/openai-bridge.js +498 -13
  5. package/cli.js +130 -41
  6. package/lib/cli-models-utils.js +71 -10
  7. package/lib/cli-webhook.js +126 -0
  8. package/package.json +76 -74
  9. package/plugins/prompt-templates/computed.mjs +1 -1
  10. package/plugins/prompt-templates/methods.mjs +0 -66
  11. package/plugins/prompt-templates/overview.mjs +1 -0
  12. package/web-ui/app.js +21 -16
  13. package/web-ui/index.html +1 -0
  14. package/web-ui/logic.codex.mjs +69 -0
  15. package/web-ui/modules/app.computed.dashboard.mjs +54 -0
  16. package/web-ui/modules/app.computed.session.mjs +22 -17
  17. package/web-ui/modules/app.methods.claude-config.mjs +24 -8
  18. package/web-ui/modules/app.methods.codex-config.mjs +35 -3
  19. package/web-ui/modules/app.methods.index.mjs +2 -0
  20. package/web-ui/modules/app.methods.navigation.mjs +21 -3
  21. package/web-ui/modules/app.methods.providers.mjs +96 -7
  22. package/web-ui/modules/app.methods.session-actions.mjs +3 -6
  23. package/web-ui/modules/app.methods.session-browser.mjs +1 -6
  24. package/web-ui/modules/app.methods.session-trash.mjs +6 -7
  25. package/web-ui/modules/app.methods.startup-claude.mjs +8 -1
  26. package/web-ui/modules/app.methods.webhook.mjs +79 -0
  27. package/web-ui/modules/i18n.dict.mjs +1104 -104
  28. package/web-ui/modules/i18n.mjs +9 -3
  29. package/web-ui/modules/provider-url-display.mjs +17 -0
  30. package/web-ui/partials/index/layout-header.html +25 -0
  31. package/web-ui/partials/index/modals-basic.html +0 -3
  32. package/web-ui/partials/index/panel-config-claude.html +10 -3
  33. package/web-ui/partials/index/panel-config-codex.html +44 -4
  34. package/web-ui/partials/index/panel-plugins.html +3 -29
  35. package/web-ui/partials/index/panel-sessions.html +0 -10
  36. package/web-ui/partials/index/panel-settings.html +93 -177
  37. package/web-ui/partials/index/panel-trash.html +88 -0
  38. package/web-ui/session-helpers.mjs +2 -2
  39. package/web-ui/styles/base-theme.css +47 -34
  40. package/web-ui/styles/controls-forms.css +27 -28
  41. package/web-ui/styles/docs-panel.css +63 -39
  42. package/web-ui/styles/layout-shell.css +69 -46
  43. package/web-ui/styles/modals-core.css +12 -10
  44. package/web-ui/styles/navigation-panels.css +36 -35
  45. package/web-ui/styles/responsive.css +4 -4
  46. package/web-ui/styles/sessions-list.css +10 -6
  47. package/web-ui/styles/settings-panel.css +197 -33
  48. package/web-ui/styles/titles-cards.css +90 -26
  49. package/web-ui/styles/trash-panel.css +90 -0
  50. package/web-ui/styles/webhook.css +81 -0
  51. package/web-ui/styles.css +2 -0
@@ -4,7 +4,9 @@ const I18N_STORAGE_KEY = 'codexmateLang';
4
4
 
5
5
  function normalizeLang(value) {
6
6
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
7
- return normalized === 'en' ? 'en' : 'zh';
7
+ if (normalized === 'en') return 'en';
8
+ if (normalized === 'ja') return 'ja';
9
+ return 'zh';
8
10
  }
9
11
 
10
12
  function interpolate(template, params) {
@@ -26,7 +28,9 @@ export function createI18nMethods() {
26
28
  this.lang = next;
27
29
  try {
28
30
  if (typeof document !== 'undefined' && document.documentElement) {
29
- document.documentElement.lang = next === 'en' ? 'en' : 'zh-CN';
31
+ if (next === 'en') document.documentElement.lang = 'en';
32
+ else if (next === 'ja') document.documentElement.lang = 'ja';
33
+ else document.documentElement.lang = 'zh-CN';
30
34
  }
31
35
  } catch (_) {}
32
36
  },
@@ -40,7 +44,9 @@ export function createI18nMethods() {
40
44
  } catch (_) {}
41
45
  try {
42
46
  if (typeof document !== 'undefined' && document.documentElement) {
43
- document.documentElement.lang = next === 'en' ? 'en' : 'zh-CN';
47
+ if (next === 'en') document.documentElement.lang = 'en';
48
+ else if (next === 'ja') document.documentElement.lang = 'ja';
49
+ else document.documentElement.lang = 'zh-CN';
44
50
  }
45
51
  } catch (_) {}
46
52
  },
@@ -0,0 +1,17 @@
1
+ export function getProviderDisplayUrl(provider) {
2
+ if (!provider) return '';
3
+ const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
4
+ if (bridge === 'openai') {
5
+ const upstream = typeof provider.upstreamUrl === 'string' ? provider.upstreamUrl.trim() : '';
6
+ if (upstream) return upstream;
7
+ }
8
+ return provider.url || '';
9
+ }
10
+
11
+ export function checkIsTransformProvider(provider) {
12
+ if (!provider || typeof provider !== 'object') return false;
13
+ const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
14
+ if (bridge === 'openai') return true;
15
+ const url = String(provider.url || '');
16
+ return url.includes('/bridge/openai/');
17
+ }
@@ -107,6 +107,12 @@
107
107
  :aria-pressed="(lang || 'zh') === 'en'"
108
108
  :class="{ active: (lang || 'zh') === 'en' }"
109
109
  @click="setLang('en')">EN</button>
110
+ <button
111
+ type="button"
112
+ class="lang-choice-btn"
113
+ :aria-pressed="(lang || 'zh') === 'ja'"
114
+ :class="{ active: (lang || 'zh') === 'ja' }"
115
+ @click="setLang('ja')">日本語</button>
110
116
  </div>
111
117
  </div>
112
118
 
@@ -297,6 +303,19 @@
297
303
  <span>{{ t('side.system.settings.meta') }}</span>
298
304
  </div>
299
305
  </button>
306
+ <button
307
+ id="side-tab-trash"
308
+ data-main-tab="trash"
309
+ :aria-current="mainTab === 'trash' ? 'page' : null"
310
+ :class="['side-item', { active: isMainTabNavActive('trash') }]"
311
+ @pointerdown="onMainTabPointerDown('trash', $event)"
312
+ @click="onMainTabClick('trash', $event)">
313
+ <div class="side-item-title">回收站</div>
314
+ <div class="side-item-meta">
315
+ <span>已删除会话</span>
316
+ <span v-if="sessionTrashCount > 0" class="side-item-badge">{{ sessionTrashCount }}</span>
317
+ </div>
318
+ </button>
300
319
  </div>
301
320
  </div>
302
321
 
@@ -314,6 +333,12 @@
314
333
  :aria-pressed="(lang || 'zh') === 'en'"
315
334
  :class="{ active: (lang || 'zh') === 'en' }"
316
335
  @click="setLang('en')">EN</button>
336
+ <button
337
+ type="button"
338
+ class="lang-choice-btn"
339
+ :aria-pressed="(lang || 'zh') === 'ja'"
340
+ :class="{ active: (lang || 'zh') === 'ja' }"
341
+ @click="setLang('ja')">日本語</button>
317
342
  </div>
318
343
  </div>
319
344
  </aside>
@@ -34,9 +34,6 @@
34
34
  <input type="checkbox" v-model="newProvider.useTransform">
35
35
  {{ t('field.useBuiltinTransform') }}
36
36
  </label>
37
- <div class="form-hint">
38
- {{ t('hint.useBuiltinTransform') }}
39
- </div>
40
37
  </div>
41
38
 
42
39
  <div class="btn-group">
@@ -45,8 +45,7 @@
45
45
  <div class="selector-header">
46
46
  <span class="selector-title">{{ t('claude.presetProviders') }}</span>
47
47
  </div>
48
- <div class="btn-group" style="flex-wrap: wrap; gap: 8px;">
49
- <button type="button" class="btn-mini" @click="closeClaudeConfigModal(); showClaudeConfigModal = true">{{ t('claude.customConfig') }}</button>
48
+ <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
50
49
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showClaudeConfigModal = true">Claude Official</button>
51
50
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showClaudeConfigModal = true">DeepSeek</button>
52
51
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Zhipu GLM</button>
@@ -136,6 +135,7 @@
136
135
  </div>
137
136
  </div>
138
137
 
138
+
139
139
  <div class="card-list">
140
140
  <div v-for="(config, name) in claudeConfigs" :key="name"
141
141
  :class="['card', { active: currentClaudeConfig === name }]"
@@ -149,7 +149,8 @@
149
149
  <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
150
150
  <div class="card-content">
151
151
  <div class="card-title">{{ name }}</div>
152
- <div class="card-subtitle">{{ config.model || t('claude.model.unset') }}</div>
152
+ <div class="card-subtitle card-subtitle-model">{{ config.model || t('claude.model.unset') }}</div>
153
+ <div class="card-subtitle card-subtitle-url" v-if="config.baseUrl">{{ config.baseUrl }}</div>
153
154
  </div>
154
155
  </div>
155
156
  <div class="card-trailing">
@@ -166,6 +167,12 @@
166
167
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
167
168
  </svg>
168
169
  </button>
170
+ <button class="card-action-btn" @click="openCloneClaudeConfigModal(name, config)" :aria-label="t('claude.action.cloneAria', { name })" :title="t('claude.action.clone')">
171
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
172
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
173
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
174
+ </svg>
175
+ </button>
169
176
  <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" :aria-label="t('config.shareCommand.aria')">
170
177
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
178
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
@@ -38,6 +38,27 @@
38
38
  {{ t('config.addProvider') }}
39
39
  </button>
40
40
 
41
+ <!-- 服务预设 -->
42
+ <div class="selector-section" v-if="isCodexConfigMode && codexProviderTemplates.length">
43
+ <div class="selector-header">
44
+ <span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
45
+ </div>
46
+ <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
47
+ <button
48
+ v-for="tpl in codexProviderTemplates"
49
+ :key="tpl.name"
50
+ type="button"
51
+ class="btn-mini"
52
+ @click="newProvider.name = tpl.name;
53
+ newProvider.url = tpl.url;
54
+ newProvider._suggestedModel = tpl.model || '';
55
+ newProvider.useTransform = !!tpl.useTransform;
56
+ showAddModal = true">
57
+ {{ tpl.label }}
58
+ </button>
59
+ </div>
60
+ </div>
61
+
41
62
  <!-- 模型选择器 -->
42
63
  <div class="selector-section">
43
64
  <div class="selector-header">
@@ -55,15 +76,20 @@
55
76
  :disabled="codexModelsLoading"
56
77
  >
57
78
  <option v-if="codexModelsLoading" value="">{{ t('config.modelLoading') }}</option>
58
- <option v-else v-for="model in models" :key="model" :value="model">{{ model }}</option>
79
+ <option v-else v-for="model in codexModelOptions" :key="model" :value="model">{{ model }}</option>
59
80
  </select>
60
81
  <input
61
82
  v-if="!codexModelsLoading && (modelsSource !== 'remote' || !modelsHasCurrent)"
62
83
  class="model-input"
63
84
  v-model="currentModel"
64
85
  @blur="onModelChange"
86
+ @keyup.enter="onModelChange"
65
87
  :placeholder="activeProviderModelPlaceholder"
88
+ :list="codexModelHasList ? 'codex-model-options' : null"
66
89
  >
90
+ <datalist v-if="codexModelHasList" id="codex-model-options">
91
+ <option v-for="model in codexModelOptions" :key="model" :value="model"></option>
92
+ </datalist>
67
93
  <div class="config-template-hint" v-if="modelsSource === 'unlimited'">
68
94
  {{ t('config.models.unlimited') }}
69
95
  </div>
@@ -204,14 +230,17 @@
204
230
  role="button"
205
231
  :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
206
232
  <div class="card-leading">
207
- <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}</div>
233
+ <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
208
234
  <div class="card-content">
209
235
  <div class="card-title">
210
236
  <span>{{ provider.name }}</span>
211
237
  <span v-if="provider.readOnly" class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
212
238
  </div>
213
- <div class="card-subtitle">
214
- {{ provider.url || t('config.url.unset') }}
239
+ <div class="card-subtitle card-subtitle-model">
240
+ {{ activeProviderModel(provider.name) || t('config.model.unset') }}
241
+ </div>
242
+ <div class="card-subtitle card-subtitle-url">
243
+ {{ displayProviderUrl(provider) || t('config.url.unset') }}
215
244
  </div>
216
245
  </div>
217
246
  </div>
@@ -262,6 +291,17 @@
262
291
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
263
292
  </svg>
264
293
  </button>
294
+ <button
295
+ v-if="!provider.readOnly"
296
+ class="card-action-btn"
297
+ @click="openCloneProviderModal(provider)"
298
+ :aria-label="t('config.provider.clone.aria', { name: provider.name })"
299
+ :title="t('config.provider.clone')">
300
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
301
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
302
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
303
+ </svg>
304
+ </button>
265
305
  <button
266
306
  v-if="!provider.readOnly"
267
307
  class="card-action-btn delete"
@@ -176,7 +176,7 @@
176
176
  <template v-else>
177
177
  <div class="prompt-editor-head">
178
178
  <div class="prompt-editor-title-row">
179
- <input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraft.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" :aria-label="t('plugins.promptTemplates.editor.nameAria')">
179
+ <input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraftRaw.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" :aria-label="t('plugins.promptTemplates.editor.nameAria')">
180
180
  <div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
181
181
  <button type="button" class="btn-mini" @click="duplicatePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.duplicate') }}</button>
182
182
  <button type="button" class="btn-mini delete" @click="deletePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.delete') }}</button>
@@ -189,14 +189,14 @@
189
189
  <div v-if="promptTemplateDraft.isBuiltin && (promptTemplateDraft.createdBy || (promptTemplateDraft.maintainers && promptTemplateDraft.maintainers.length))" class="plugins-panel-note">
190
190
  {{ t('plugins.meta.attribution', { createdBy: promptTemplateDraft.createdBy || '', maintainers: (promptTemplateDraft.maintainers || []).join(', ') }) }}
191
191
  </div>
192
- <input class="form-input" type="text" v-model.trim="promptTemplateDraft.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" :aria-label="t('plugins.promptTemplates.editor.descAria')">
192
+ <input class="form-input" type="text" v-model.trim="promptTemplateDraftRaw.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" :aria-label="t('plugins.promptTemplates.editor.descAria')">
193
193
  </div>
194
194
 
195
195
  <div class="prompt-editor-body">
196
196
  <label class="form-label">{{ t('plugins.promptTemplates.editor.templateLabel') }}</label>
197
197
  <textarea
198
198
  class="form-input prompt-editor-textarea"
199
- v-model="promptTemplateDraft.template"
199
+ v-model="promptTemplateDraftRaw.template"
200
200
  :disabled="promptTemplateDraft.isBuiltin"
201
201
  rows="10"
202
202
  spellcheck="false"
@@ -210,7 +210,6 @@
210
210
  <div class="plugins-panel-note">{{ t('plugins.promptTemplates.vars.hint') }}</div>
211
211
  </div>
212
212
  <div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
213
- <button type="button" class="btn-mini" @click="addPromptTemplateVariable" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.add') }}</button>
214
213
  <button type="button" class="btn-mini" @click="resetPromptVariableValues" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.reset') }}</button>
215
214
  </div>
216
215
  </div>
@@ -252,28 +251,3 @@
252
251
  style="display:none"
253
252
  @change="handlePromptTemplatesImportChange">
254
253
 
255
- <!-- 新增变量(Prompt Templates) -->
256
- <div v-if="showPromptTemplateVarModal" class="modal-overlay" @click.self="closePromptTemplateVarModal">
257
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="add-prompt-var-modal-title">
258
- <div class="modal-title" id="add-prompt-var-modal-title">{{ t('plugins.promptTemplates.varModal.title') }}</div>
259
-
260
- <div class="form-group">
261
- <label class="form-label">{{ t('plugins.promptTemplates.varModal.nameLabel') }}</label>
262
- <input
263
- ref="promptTemplateVarNameInput"
264
- v-model.trim="promptTemplateVarDraftName"
265
- :class="['form-input', { invalid: !!promptTemplateVarDraftError }]"
266
- :placeholder="t('placeholder.varNameExample')"
267
- autocomplete="off"
268
- spellcheck="false"
269
- @keydown.enter.prevent="confirmAddPromptTemplateVariable">
270
- <div v-if="promptTemplateVarDraftError" class="form-hint form-error">{{ promptTemplateVarDraftError }}</div>
271
- <div v-else class="form-hint">{{ t('hint.varNameRules') }}</div>
272
- </div>
273
-
274
- <div class="btn-group">
275
- <button class="btn btn-cancel" @click="closePromptTemplateVarModal">{{ t('plugins.promptTemplates.varModal.cancel') }}</button>
276
- <button class="btn btn-confirm" @click="confirmAddPromptTemplateVariable">{{ t('plugins.promptTemplates.varModal.add') }}</button>
277
- </div>
278
- </div>
279
- </div>
@@ -97,16 +97,6 @@
97
97
  </button>
98
98
  </div>
99
99
  </div>
100
- <div class="session-toolbar-footer">
101
- <label class="quick-option">
102
- <input
103
- type="checkbox"
104
- v-model="sessionResumeWithYolo"
105
- @change="onSessionResumeYoloChange"
106
- >
107
- {{ t('sessions.resumeYolo') }}
108
- </label>
109
- </div>
110
100
  <div v-if="hasActiveSessionFilters()" class="session-filter-chips" :aria-label="t('sessions.filters.copyLink')">
111
101
  <button
112
102
  v-for="chip in getSessionFilterChips()"