codexmate 0.0.20 → 0.0.22

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 (96) hide show
  1. package/README.md +289 -152
  2. package/README.zh.md +321 -0
  3. package/cli/agents-files.js +224 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +1044 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/openai-bridge.js +950 -0
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/session-usage.concurrent.js +28 -0
  12. package/cli/session-usage.js +112 -0
  13. package/cli/session-usage.models.js +176 -0
  14. package/cli/skills.js +1141 -0
  15. package/cli/zip-commands.js +510 -0
  16. package/cli.js +9408 -9719
  17. package/lib/cli-models-utils.js +109 -1
  18. package/lib/cli-path-utils.js +69 -0
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/download-artifacts.js +77 -0
  21. package/lib/task-orchestrator.js +869 -0
  22. package/package.json +14 -10
  23. package/res/logo.png +0 -0
  24. package/res/vue.global.prod.js +13 -0
  25. package/web-ui/app.js +193 -15
  26. package/web-ui/index.html +5 -1
  27. package/web-ui/logic.agents-diff.mjs +1 -1
  28. package/web-ui/logic.claude.mjs +60 -0
  29. package/web-ui/logic.runtime.mjs +11 -7
  30. package/web-ui/logic.sessions.mjs +372 -21
  31. package/web-ui/modules/api.mjs +22 -1
  32. package/web-ui/modules/app.computed.dashboard.mjs +23 -10
  33. package/web-ui/modules/app.computed.index.mjs +4 -0
  34. package/web-ui/modules/app.computed.main-tabs.mjs +198 -0
  35. package/web-ui/modules/app.computed.session.mjs +521 -9
  36. package/web-ui/modules/app.methods.agents.mjs +62 -11
  37. package/web-ui/modules/app.methods.codex-config.mjs +189 -34
  38. package/web-ui/modules/app.methods.index.mjs +7 -1
  39. package/web-ui/modules/app.methods.install.mjs +24 -20
  40. package/web-ui/modules/app.methods.navigation.mjs +142 -1
  41. package/web-ui/modules/app.methods.openclaw-core.mjs +339 -39
  42. package/web-ui/modules/app.methods.openclaw-editing.mjs +39 -4
  43. package/web-ui/modules/app.methods.openclaw-persist.mjs +122 -4
  44. package/web-ui/modules/app.methods.providers.mjs +192 -53
  45. package/web-ui/modules/app.methods.session-actions.mjs +99 -19
  46. package/web-ui/modules/app.methods.session-browser.mjs +196 -5
  47. package/web-ui/modules/app.methods.session-timeline.mjs +22 -15
  48. package/web-ui/modules/app.methods.session-trash.mjs +3 -0
  49. package/web-ui/modules/app.methods.startup-claude.mjs +70 -71
  50. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  51. package/web-ui/modules/config-mode.computed.mjs +2 -0
  52. package/web-ui/modules/config-template-confirm-pref.mjs +33 -0
  53. package/web-ui/modules/i18n.mjs +1609 -0
  54. package/web-ui/modules/plugins.computed.mjs +220 -0
  55. package/web-ui/modules/plugins.methods.mjs +620 -0
  56. package/web-ui/modules/plugins.storage.mjs +37 -0
  57. package/web-ui/partials/index/layout-footer.html +1 -57
  58. package/web-ui/partials/index/layout-header.html +299 -175
  59. package/web-ui/partials/index/modal-config-template-agents.html +79 -29
  60. package/web-ui/partials/index/modal-confirm-toast.html +1 -1
  61. package/web-ui/partials/index/modal-health-check.html +14 -14
  62. package/web-ui/partials/index/modal-openclaw-config.html +47 -42
  63. package/web-ui/partials/index/modal-skills.html +130 -114
  64. package/web-ui/partials/index/modals-basic.html +71 -102
  65. package/web-ui/partials/index/panel-config-claude.html +50 -12
  66. package/web-ui/partials/index/panel-config-codex.html +34 -37
  67. package/web-ui/partials/index/panel-config-openclaw.html +10 -16
  68. package/web-ui/partials/index/panel-docs.html +147 -0
  69. package/web-ui/partials/index/panel-market.html +38 -38
  70. package/web-ui/partials/index/panel-orchestration.html +397 -0
  71. package/web-ui/partials/index/panel-plugins.html +243 -0
  72. package/web-ui/partials/index/panel-sessions.html +51 -146
  73. package/web-ui/partials/index/panel-settings.html +188 -96
  74. package/web-ui/partials/index/panel-usage.html +353 -0
  75. package/web-ui/session-helpers.mjs +221 -10
  76. package/web-ui/styles/base-theme.css +120 -229
  77. package/web-ui/styles/controls-forms.css +59 -51
  78. package/web-ui/styles/docs-panel.css +247 -0
  79. package/web-ui/styles/layout-shell.css +394 -128
  80. package/web-ui/styles/modals-core.css +18 -3
  81. package/web-ui/styles/navigation-panels.css +184 -183
  82. package/web-ui/styles/plugins-panel.css +518 -0
  83. package/web-ui/styles/responsive.css +102 -62
  84. package/web-ui/styles/sessions-list.css +13 -27
  85. package/web-ui/styles/sessions-preview.css +13 -7
  86. package/web-ui/styles/sessions-toolbar-trash.css +25 -0
  87. package/web-ui/styles/sessions-usage.css +581 -6
  88. package/web-ui/styles/settings-panel.css +166 -0
  89. package/web-ui/styles/skills-list.css +16 -11
  90. package/web-ui/styles/skills-market.css +63 -2
  91. package/web-ui/styles/task-orchestration.css +776 -0
  92. package/web-ui/styles/titles-cards.css +67 -66
  93. package/web-ui/styles.css +4 -0
  94. package/README.en.md +0 -259
  95. package/res/screenshot.png +0 -0
  96. package/res/vue.global.js +0 -18552
@@ -139,7 +139,9 @@ export function createOpenclawEditingMethods() {
139
139
  this.openclawEditing.content = this.stringifyOpenclawConfig(config);
140
140
  this.refreshOpenclawProviders(config);
141
141
  this.refreshOpenclawAgentsList(config);
142
- this.fillOpenclawQuickFromConfig(config);
142
+ this.fillOpenclawQuickFromConfig(config, {
143
+ authProfilesByProvider: this.openclawAuthProfilesByProvider
144
+ });
143
145
  this.showMessage('已写入', 'success');
144
146
  },
145
147
 
@@ -170,7 +172,9 @@ export function createOpenclawEditingMethods() {
170
172
  const models = ensureObject(config.models);
171
173
  const providers = ensureObject(models.providers);
172
174
  const provider = ensureObject(providers[providerName]);
173
- const baseUrl = (this.openclawQuick.baseUrl || '').trim();
175
+ const baseUrl = this.openclawQuick.baseUrlReadOnly
176
+ ? ''
177
+ : (this.openclawQuick.baseUrl || '').trim();
174
178
  if (!baseUrl && !provider.baseUrl) {
175
179
  this.showMessage('请填写 URL', 'error');
176
180
  return;
@@ -188,7 +192,21 @@ export function createOpenclawEditingMethods() {
188
192
  }
189
193
 
190
194
  const shouldOverrideProvider = !!this.openclawQuick.overrideProvider;
191
- const apiKey = (this.openclawQuick.apiKey || '').trim();
195
+ const apiKey = this.openclawQuick.apiKeyReadOnly
196
+ ? ''
197
+ : (this.openclawQuick.apiKey || '').trim();
198
+ const apiKeySourceKind = typeof this.openclawQuick.apiKeySourceKind === 'string'
199
+ ? this.openclawQuick.apiKeySourceKind.trim()
200
+ : '';
201
+ const apiKeySourceProfileId = typeof this.openclawQuick.apiKeySourceProfileId === 'string'
202
+ ? this.openclawQuick.apiKeySourceProfileId.trim()
203
+ : '';
204
+ const apiKeySourceWriteField = typeof this.openclawQuick.apiKeySourceWriteField === 'string'
205
+ ? this.openclawQuick.apiKeySourceWriteField.trim()
206
+ : '';
207
+ const apiKeySourceOriginalValue = typeof this.openclawQuick.apiKeySourceOriginalValue === 'string'
208
+ ? this.openclawQuick.apiKeySourceOriginalValue.trim()
209
+ : '';
192
210
  const apiType = (this.openclawQuick.apiType || '').trim();
193
211
  const setProviderField = (key, value) => {
194
212
  if (!value) return;
@@ -198,7 +216,24 @@ export function createOpenclawEditingMethods() {
198
216
  };
199
217
  setProviderField('baseUrl', baseUrl);
200
218
  setProviderField('api', apiType);
201
- if (apiKey) {
219
+ if (apiKeySourceKind === 'auth-profile' && apiKeySourceProfileId && apiKeySourceWriteField) {
220
+ const pending = this.openclawPendingAuthProfileUpdates
221
+ && typeof this.openclawPendingAuthProfileUpdates === 'object'
222
+ && !Array.isArray(this.openclawPendingAuthProfileUpdates)
223
+ ? { ...this.openclawPendingAuthProfileUpdates }
224
+ : {};
225
+ if (apiKey && apiKey !== apiKeySourceOriginalValue) {
226
+ pending[apiKeySourceProfileId] = {
227
+ profileId: apiKeySourceProfileId,
228
+ provider: providerName,
229
+ field: apiKeySourceWriteField,
230
+ value: apiKey
231
+ };
232
+ } else {
233
+ delete pending[apiKeySourceProfileId];
234
+ }
235
+ this.openclawPendingAuthProfileUpdates = pending;
236
+ } else if (apiKey) {
202
237
  setProviderField('apiKey', apiKey);
203
238
  }
204
239
 
@@ -1,3 +1,49 @@
1
+ const DEFAULT_OPENCLAW_CONFIG_NAME = '默认配置';
2
+
3
+ function buildNormalizedOpenclawConfigs(configs, defaultContent = '') {
4
+ const source = configs && typeof configs === 'object' && !Array.isArray(configs)
5
+ ? configs
6
+ : {};
7
+ const defaultEntry = source[DEFAULT_OPENCLAW_CONFIG_NAME]
8
+ && typeof source[DEFAULT_OPENCLAW_CONFIG_NAME] === 'object'
9
+ && !Array.isArray(source[DEFAULT_OPENCLAW_CONFIG_NAME])
10
+ ? source[DEFAULT_OPENCLAW_CONFIG_NAME]
11
+ : { content: defaultContent };
12
+ const normalized = {
13
+ [DEFAULT_OPENCLAW_CONFIG_NAME]: {
14
+ content: typeof defaultEntry.content === 'string' ? defaultEntry.content : defaultContent
15
+ }
16
+ };
17
+ for (const [name, value] of Object.entries(source)) {
18
+ if (name === DEFAULT_OPENCLAW_CONFIG_NAME) continue;
19
+ normalized[name] = value;
20
+ }
21
+ return normalized;
22
+ }
23
+
24
+ function syncDefaultOpenclawConfigState(vm, content, options = {}) {
25
+ const nextContent = typeof content === 'string' ? content : '';
26
+ vm.openclawConfigs = buildNormalizedOpenclawConfigs(vm.openclawConfigs, nextContent);
27
+ vm.openclawConfigs[DEFAULT_OPENCLAW_CONFIG_NAME] = {
28
+ content: nextContent
29
+ };
30
+ if (typeof options.path === 'string') {
31
+ vm.openclawConfigPath = options.path;
32
+ }
33
+ if (typeof options.exists === 'boolean') {
34
+ vm.openclawConfigExists = options.exists;
35
+ }
36
+ if (options.lineEnding === '\r\n' || options.lineEnding === '\n') {
37
+ vm.openclawLineEnding = options.lineEnding;
38
+ }
39
+ if (!vm.currentOpenclawConfig || !Object.prototype.hasOwnProperty.call(vm.openclawConfigs, vm.currentOpenclawConfig)) {
40
+ vm.currentOpenclawConfig = DEFAULT_OPENCLAW_CONFIG_NAME;
41
+ }
42
+ if (options.persist !== false && typeof vm.saveOpenclawConfigs === 'function') {
43
+ vm.saveOpenclawConfigs();
44
+ }
45
+ }
46
+
1
47
  export function createOpenclawPersistMethods(options = {}) {
2
48
  const {
3
49
  api,
@@ -5,10 +51,39 @@ export function createOpenclawPersistMethods(options = {}) {
5
51
  } = options;
6
52
 
7
53
  return {
54
+ syncDefaultOpenclawConfigEntry(options = {}) {
55
+ const silent = !!options.silent;
56
+ return api('get-openclaw-config')
57
+ .then((res) => {
58
+ if (res && !res.error) {
59
+ this.openclawAuthProfilesByProvider = res && res.authProfilesByProvider && typeof res.authProfilesByProvider === 'object' && !Array.isArray(res.authProfilesByProvider)
60
+ ? res.authProfilesByProvider
61
+ : {};
62
+ const nextContent = res.exists && typeof res.content === 'string' && res.content.trim()
63
+ ? res.content
64
+ : defaultOpenclawTemplate;
65
+ syncDefaultOpenclawConfigState(this, nextContent, {
66
+ path: res.path || this.openclawConfigPath,
67
+ exists: !!res.exists,
68
+ lineEnding: res.lineEnding === '\r\n' ? '\r\n' : '\n',
69
+ persist: true
70
+ });
71
+ }
72
+ return res;
73
+ })
74
+ .catch((e) => {
75
+ if (!silent) {
76
+ this.showMessage('加载 OpenClaw 默认配置失败', 'error');
77
+ }
78
+ return { error: e && e.message ? e.message : String(e) };
79
+ });
80
+ },
81
+
8
82
  openOpenclawAddModal() {
9
83
  const modalToken = (Number(this.openclawModalLoadToken || 0) + 1);
10
84
  this.openclawModalLoadToken = modalToken;
11
85
  this.openclawEditorTitle = '添加 OpenClaw 配置';
86
+ this.openclawPendingAuthProfileUpdates = {};
12
87
  this.openclawEditing = {
13
88
  name: '',
14
89
  content: '',
@@ -29,9 +104,11 @@ export function createOpenclawPersistMethods(options = {}) {
29
104
 
30
105
  openOpenclawEditModal(name) {
31
106
  const existing = this.openclawConfigs[name];
107
+ const isDefaultConfig = name === DEFAULT_OPENCLAW_CONFIG_NAME;
32
108
  const modalToken = (Number(this.openclawModalLoadToken || 0) + 1);
33
109
  this.openclawModalLoadToken = modalToken;
34
110
  this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
111
+ this.openclawPendingAuthProfileUpdates = {};
35
112
  this.openclawEditing = {
36
113
  name,
37
114
  content: this.openclawHasContent(existing) ? existing.content : '',
@@ -41,8 +118,9 @@ export function createOpenclawPersistMethods(options = {}) {
41
118
  this.showOpenclawConfigModal = true;
42
119
  void this.loadOpenclawConfigFromFile({
43
120
  silent: true,
44
- force: false,
45
- fallbackToTemplate: false,
121
+ force: isDefaultConfig,
122
+ fallbackToTemplate: isDefaultConfig,
123
+ syncDefaultEntry: isDefaultConfig,
46
124
  modalToken,
47
125
  expectedEditorContent: this.openclawEditing.content
48
126
  });
@@ -58,6 +136,7 @@ export function createOpenclawPersistMethods(options = {}) {
58
136
  this.openclawEditing = { name: '', content: '', lockName: false };
59
137
  this.openclawSaving = false;
60
138
  this.openclawApplying = false;
139
+ this.openclawPendingAuthProfileUpdates = {};
61
140
  this.resetOpenclawStructured();
62
141
  this.resetOpenclawQuick();
63
142
  },
@@ -66,6 +145,8 @@ export function createOpenclawPersistMethods(options = {}) {
66
145
  const silent = !!options.silent;
67
146
  const force = !!options.force;
68
147
  const fallbackToTemplate = options.fallbackToTemplate !== false;
148
+ const syncDefaultEntry = options.syncDefaultEntry === true
149
+ || (this.openclawEditing && this.openclawEditing.lockName && this.openclawEditing.name === DEFAULT_OPENCLAW_CONFIG_NAME);
69
150
  const modalToken = Number(options.modalToken || this.openclawModalLoadToken || 0);
70
151
  const expectedEditorContent = typeof options.expectedEditorContent === 'string'
71
152
  ? options.expectedEditorContent
@@ -90,6 +171,10 @@ export function createOpenclawPersistMethods(options = {}) {
90
171
  this.openclawConfigPath = res.path || '';
91
172
  this.openclawConfigExists = !!res.exists;
92
173
  this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
174
+ this.openclawAuthProfilesByProvider = res && res.authProfilesByProvider && typeof res.authProfilesByProvider === 'object' && !Array.isArray(res.authProfilesByProvider)
175
+ ? res.authProfilesByProvider
176
+ : {};
177
+ this.openclawPendingAuthProfileUpdates = {};
93
178
  const hasContent = !!(res.content && res.content.trim());
94
179
  const currentContent = typeof this.openclawEditing.content === 'string'
95
180
  ? this.openclawEditing.content
@@ -98,11 +183,21 @@ export function createOpenclawPersistMethods(options = {}) {
98
183
  const shouldOverride = force
99
184
  ? (!currentContent.trim() || !editorChangedSinceRequest)
100
185
  : (!currentContent || !currentContent.trim());
186
+ const fallbackContent = fallbackToTemplate ? defaultOpenclawTemplate : '';
187
+ const nextContent = hasContent ? res.content : fallbackContent;
101
188
  if (hasContent && shouldOverride) {
102
189
  this.openclawEditing.content = res.content;
103
190
  } else if (!hasContent && shouldOverride && fallbackToTemplate) {
104
191
  this.openclawEditing.content = defaultOpenclawTemplate;
105
192
  }
193
+ if (syncDefaultEntry) {
194
+ syncDefaultOpenclawConfigState(this, nextContent, {
195
+ path: res.path || '',
196
+ exists: !!res.exists,
197
+ lineEnding: this.openclawLineEnding,
198
+ persist: true
199
+ });
200
+ }
106
201
  this.syncOpenclawStructuredFromText({ silent: true });
107
202
  if (!silent) {
108
203
  this.showMessage('加载完成', 'success');
@@ -165,6 +260,10 @@ export function createOpenclawPersistMethods(options = {}) {
165
260
  if (this.openclawSaving || this.openclawApplying) {
166
261
  return;
167
262
  }
263
+ if (this.openclawEditing && this.openclawEditing.lockName && this.openclawEditing.name === DEFAULT_OPENCLAW_CONFIG_NAME) {
264
+ this.showMessage('默认配置代表当前系统配置,请使用“保存并应用”', 'info');
265
+ return;
266
+ }
168
267
  this.openclawSaving = true;
169
268
  try {
170
269
  const name = this.persistOpenclawConfig();
@@ -186,7 +285,8 @@ export function createOpenclawPersistMethods(options = {}) {
186
285
  const config = this.openclawConfigs[name];
187
286
  const res = await api('apply-openclaw-config', {
188
287
  content: config.content,
189
- lineEnding: this.openclawLineEnding
288
+ lineEnding: this.openclawLineEnding,
289
+ authProfileUpdates: Object.values(this.openclawPendingAuthProfileUpdates || {})
190
290
  });
191
291
  if (res.error || res.success === false) {
192
292
  this.showMessage(res.error || '应用配置失败', 'error');
@@ -194,6 +294,13 @@ export function createOpenclawPersistMethods(options = {}) {
194
294
  }
195
295
  this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
196
296
  this.openclawConfigExists = true;
297
+ syncDefaultOpenclawConfigState(this, config.content, {
298
+ path: this.openclawConfigPath,
299
+ exists: true,
300
+ lineEnding: this.openclawLineEnding,
301
+ persist: true
302
+ });
303
+ this.openclawPendingAuthProfileUpdates = {};
197
304
  const targetTip = res.targetPath ? `(${res.targetPath})` : '';
198
305
  this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
199
306
  this.closeOpenclawConfigModal({ force: true });
@@ -205,6 +312,9 @@ export function createOpenclawPersistMethods(options = {}) {
205
312
  },
206
313
 
207
314
  async deleteOpenclawConfig(name) {
315
+ if (name === DEFAULT_OPENCLAW_CONFIG_NAME) {
316
+ return this.showMessage('默认配置始终映射当前系统配置,不可删除', 'info');
317
+ }
208
318
  if (Object.keys(this.openclawConfigs).length <= 1) {
209
319
  return this.showMessage('至少保留一项', 'error');
210
320
  }
@@ -233,13 +343,21 @@ export function createOpenclawPersistMethods(options = {}) {
233
343
  try {
234
344
  const res = await api('apply-openclaw-config', {
235
345
  content: config.content,
236
- lineEnding: this.openclawLineEnding
346
+ lineEnding: this.openclawLineEnding,
347
+ authProfileUpdates: Object.values(this.openclawPendingAuthProfileUpdates || {})
237
348
  });
238
349
  if (res.error || res.success === false) {
239
350
  this.showMessage(res.error || '应用配置失败', 'error');
240
351
  } else {
241
352
  this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
242
353
  this.openclawConfigExists = true;
354
+ syncDefaultOpenclawConfigState(this, config.content, {
355
+ path: this.openclawConfigPath,
356
+ exists: true,
357
+ lineEnding: this.openclawLineEnding,
358
+ persist: true
359
+ });
360
+ this.openclawPendingAuthProfileUpdates = {};
243
361
  const targetTip = res.targetPath ? `(${res.targetPath})` : '';
244
362
  this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
245
363
  }
@@ -1,30 +1,155 @@
1
+ const PROVIDER_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
2
+ const RESERVED_PROXY_PROVIDER_NAME = 'codexmate-proxy';
3
+
4
+ function normalizeText(value) {
5
+ return typeof value === 'string' ? value.trim() : '';
6
+ }
7
+
8
+ function normalizeProviderUrl(value) {
9
+ return normalizeText(value).replace(/\/+$/g, '');
10
+ }
11
+
12
+ function isValidHttpUrl(value) {
13
+ if (!value) return false;
14
+ try {
15
+ const parsed = new URL(value);
16
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
17
+ } catch (_) {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function isReservedProviderCreationNameInput(name) {
23
+ const normalized = normalizeText(name).toLowerCase();
24
+ return normalized === RESERVED_PROXY_PROVIDER_NAME;
25
+ }
26
+
27
+ function isValidProviderNameInputValue(name) {
28
+ return PROVIDER_NAME_PATTERN.test(normalizeText(name));
29
+ }
30
+
31
+ function isValidProviderUrlInputValue(url) {
32
+ return isValidHttpUrl(normalizeProviderUrl(url));
33
+ }
34
+
35
+ function findProviderByName(list, name) {
36
+ const target = normalizeText(name);
37
+ if (!target) return null;
38
+ return (Array.isArray(list) ? list : []).find((item) => item && normalizeText(item.name) === target) || null;
39
+ }
40
+
41
+ function normalizeProviderDraftState(target) {
42
+ if (!target || typeof target !== 'object') return;
43
+ if (typeof target.name === 'string') {
44
+ target.name = target.name.trim();
45
+ }
46
+ if (typeof target.url === 'string') {
47
+ target.url = normalizeProviderUrl(target.url);
48
+ }
49
+ }
50
+
51
+ function getProviderValidationForContext(vm, mode = 'add') {
52
+ const draft = mode === 'edit' ? vm.editingProvider : vm.newProvider;
53
+ const editingName = mode === 'edit' ? normalizeText(draft && draft.name) : '';
54
+ const name = normalizeText(draft && draft.name);
55
+ const url = normalizeProviderUrl(draft && draft.url);
56
+ const errors = {
57
+ name: '',
58
+ url: ''
59
+ };
60
+
61
+ if (mode === 'add') {
62
+ if (!name) {
63
+ errors.name = '名称不能为空';
64
+ } else if (!isValidProviderNameInputValue(name)) {
65
+ errors.name = '名称仅支持字母/数字/._-';
66
+ } else if (isReservedProviderCreationNameInput(name)) {
67
+ errors.name = 'codexmate-proxy 为保留名称,不可手动添加';
68
+ } else if (findProviderByName(vm.providersList, name)) {
69
+ errors.name = '名称已存在';
70
+ }
71
+ } else if (!editingName) {
72
+ errors.name = '提供商名称不能为空';
73
+ }
74
+
75
+ if (!url) {
76
+ errors.url = 'URL 必填';
77
+ } else if (!isValidProviderUrlInputValue(url)) {
78
+ errors.url = 'URL 仅支持 http/https';
79
+ }
80
+
81
+ return {
82
+ mode,
83
+ name,
84
+ url,
85
+ errors,
86
+ ok: !errors.name && !errors.url
87
+ };
88
+ }
89
+
90
+ function canSubmitProviderForContext(vm, mode = 'add') {
91
+ if (mode === 'edit' && vm.editingProvider && (vm.editingProvider.readOnly || vm.editingProvider.nonEditable)) {
92
+ return false;
93
+ }
94
+ return getProviderValidationForContext(vm, mode).ok;
95
+ }
96
+
1
97
  export function createProvidersMethods(options = {}) {
2
98
  const { api } = options;
3
99
 
4
100
  return {
101
+ normalizeProviderDraft(mode = 'add') {
102
+ normalizeProviderDraftState(mode === 'edit' ? this.editingProvider : this.newProvider);
103
+ },
104
+
105
+ isReservedProviderCreationName(name) {
106
+ return isReservedProviderCreationNameInput(name);
107
+ },
108
+
109
+ isValidProviderNameInput(name) {
110
+ return isValidProviderNameInputValue(name);
111
+ },
112
+
113
+ isValidProviderUrlInput(url) {
114
+ return isValidProviderUrlInputValue(url);
115
+ },
116
+
117
+ findProviderByName(name) {
118
+ return findProviderByName(this.providersList, name);
119
+ },
120
+
121
+ getProviderValidation(mode = 'add') {
122
+ return getProviderValidationForContext(this, mode);
123
+ },
124
+
125
+ providerFieldError(mode, fieldName) {
126
+ const validation = getProviderValidationForContext(this, mode);
127
+ return validation && validation.errors && typeof validation.errors[fieldName] === 'string'
128
+ ? validation.errors[fieldName]
129
+ : '';
130
+ },
131
+
132
+ canSubmitProvider(mode = 'add') {
133
+ return canSubmitProviderForContext(this, mode);
134
+ },
135
+
5
136
  async addProvider() {
6
- const rawName = typeof this.newProvider.name === 'string' ? this.newProvider.name : '';
7
- const rawUrl = typeof this.newProvider.url === 'string' ? this.newProvider.url.trim() : '';
8
- if (!rawName || !rawUrl) {
9
- return this.showMessage('名称和URL必填', 'error');
10
- }
11
- const name = rawName.trim();
12
- if (!name) {
13
- return this.showMessage('名称不能为空', 'error');
14
- }
15
- if (name.toLowerCase() === 'local') {
16
- return this.showMessage('local provider 为系统保留名称,不可新增', 'error');
17
- }
18
- if (this.providersList.some(item => item.name === name)) {
19
- return this.showMessage('名称已存在', 'error');
137
+ normalizeProviderDraftState(this.newProvider);
138
+ const validation = getProviderValidationForContext(this, 'add');
139
+ if (!validation.ok) {
140
+ return this.showMessage(validation.errors.name || validation.errors.url || '名称和URL必填', 'error');
20
141
  }
21
142
 
22
143
  try {
23
- const res = await api('add-provider', {
24
- name,
25
- url: rawUrl,
144
+ const payload = {
145
+ name: validation.name,
146
+ url: validation.url,
26
147
  key: this.newProvider.key || ''
27
- });
148
+ };
149
+ if (this.newProvider && this.newProvider.useTransform) {
150
+ payload.useTransform = true;
151
+ }
152
+ const res = await api('add-provider', payload);
28
153
  if (res.error) {
29
154
  this.showMessage(res.error, 'error');
30
155
  return;
@@ -43,22 +168,7 @@ export function createProvidersMethods(options = {}) {
43
168
  return list.find((item) => !!(item && item.current)) || null;
44
169
  },
45
170
 
46
- isLocalLikeProvider(providerOrName) {
47
- if (!providerOrName) return false;
48
- const rawName = typeof providerOrName === 'object'
49
- ? String(providerOrName.name || '')
50
- : String(providerOrName);
51
- const normalized = rawName.trim().toLowerCase();
52
- return normalized === 'local';
53
- },
54
-
55
171
  providerPillState(provider) {
56
- if (this.isLocalLikeProvider(provider)) {
57
- const currentProfile = this.getCurrentCodexAuthProfile();
58
- return currentProfile
59
- ? { configured: true, text: '已登录' }
60
- : { configured: false, text: '未登录' };
61
- }
62
172
  const configured = !!(provider && provider.hasKey);
63
173
  return {
64
174
  configured,
@@ -88,18 +198,10 @@ export function createProvidersMethods(options = {}) {
88
198
  isNonDeletableProvider(providerOrName) {
89
199
  if (!providerOrName) return false;
90
200
  if (typeof providerOrName === 'object') {
91
- const directName = String(providerOrName.name || '').trim().toLowerCase();
92
- if (directName === 'local') {
93
- return true;
94
- }
95
201
  return !!providerOrName.nonDeletable;
96
202
  }
97
203
  const name = String(providerOrName).trim();
98
204
  if (!name) return false;
99
- const normalized = name.toLowerCase();
100
- if (normalized === 'local') {
101
- return true;
102
- }
103
205
  const target = (this.providersList || []).find((item) => item && item.name === name);
104
206
  return !!(target && target.nonDeletable);
105
207
  },
@@ -113,7 +215,7 @@ export function createProvidersMethods(options = {}) {
113
215
  },
114
216
 
115
217
  shouldAllowProviderShare(provider) {
116
- return !this.isReadOnlyProvider(provider) && !this.isLocalLikeProvider(provider);
218
+ return !this.isReadOnlyProvider(provider);
117
219
  },
118
220
 
119
221
  async deleteProvider(name) {
@@ -138,19 +240,50 @@ export function createProvidersMethods(options = {}) {
138
240
  }
139
241
  },
140
242
 
141
- openEditModal(provider) {
243
+ async openEditModal(provider) {
244
+ const requestId = Symbol('openEditModal');
245
+ this._openEditModalRequestId = requestId;
142
246
  if (!this.shouldShowProviderEdit(provider)) {
143
247
  this.showMessage('该 provider 为保留项,不可编辑', 'info');
144
248
  return;
145
249
  }
250
+ const isTransformProvider = (() => {
251
+ if (!provider || typeof provider !== 'object') return false;
252
+ const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
253
+ if (bridge === 'openai') return true;
254
+ const url = String(provider.url || '');
255
+ return url.includes('/bridge/openai/');
256
+ })();
146
257
  this.editingProvider = {
147
258
  name: provider.name,
148
- url: provider.url || '',
259
+ url: normalizeProviderUrl(provider.url || ''),
149
260
  key: '',
150
261
  readOnly: !!provider.readOnly,
151
- nonEditable: this.isNonDeletableProvider(provider)
262
+ nonEditable: typeof provider.nonEditable === 'boolean'
263
+ ? provider.nonEditable
264
+ : this.isNonDeletableProvider(provider),
265
+ useTransform: isTransformProvider
152
266
  };
153
267
  this.showEditModal = true;
268
+
269
+ if (isTransformProvider) {
270
+ try {
271
+ const res = await api('openai-bridge-get-provider', { name: provider.name });
272
+ if (
273
+ this._openEditModalRequestId === requestId
274
+ && this.showEditModal
275
+ && this.editingProvider
276
+ && this.editingProvider.name === provider.name
277
+ && res && !res.error
278
+ && typeof res.baseUrl === 'string'
279
+ && res.baseUrl.trim()
280
+ ) {
281
+ this.editingProvider.url = normalizeProviderUrl(res.baseUrl);
282
+ }
283
+ } catch (_) {
284
+ // ignore
285
+ }
286
+ }
154
287
  },
155
288
 
156
289
  async updateProvider() {
@@ -159,13 +292,16 @@ export function createProvidersMethods(options = {}) {
159
292
  this.closeEditModal();
160
293
  return;
161
294
  }
162
- const url = typeof this.editingProvider.url === 'string' ? this.editingProvider.url.trim() : '';
163
- if (!url) {
164
- return this.showMessage('URL 必填', 'error');
295
+ normalizeProviderDraftState(this.editingProvider);
296
+ const validation = getProviderValidationForContext(this, 'edit');
297
+ if (!validation.ok) {
298
+ return this.showMessage(validation.errors.name || validation.errors.url || 'URL 必填', 'error');
165
299
  }
166
300
 
167
- const name = this.editingProvider.name;
168
- const params = { name, url };
301
+ const params = { name: validation.name, url: validation.url };
302
+ if (this.editingProvider && this.editingProvider.useTransform) {
303
+ params.useTransform = true;
304
+ }
169
305
  if (typeof this.editingProvider.key === 'string' && this.editingProvider.key.trim()) {
170
306
  params.key = this.editingProvider.key;
171
307
  }
@@ -185,7 +321,7 @@ export function createProvidersMethods(options = {}) {
185
321
 
186
322
  closeEditModal() {
187
323
  this.showEditModal = false;
188
- this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false };
324
+ this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false };
189
325
  },
190
326
 
191
327
  async resetConfig() {
@@ -241,7 +377,7 @@ export function createProvidersMethods(options = {}) {
241
377
 
242
378
  closeAddModal() {
243
379
  this.showAddModal = false;
244
- this.newProvider = { name: '', url: '', key: '' };
380
+ this.newProvider = { name: '', url: '', key: '', useTransform: false };
245
381
  },
246
382
 
247
383
  closeModelModal() {
@@ -258,7 +394,10 @@ export function createProvidersMethods(options = {}) {
258
394
  },
259
395
 
260
396
  displayApiKey(configName) {
261
- const key = this.claudeConfigs[configName]?.apiKey;
397
+ const config = this.claudeConfigs && this.claudeConfigs[configName]
398
+ ? this.claudeConfigs[configName]
399
+ : null;
400
+ const key = config ? config.apiKey : '';
262
401
  return this.formatKey(key);
263
402
  }
264
403
  };