codexmate 0.0.38 → 0.0.40

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 (43) hide show
  1. package/cli/builtin-proxy.js +626 -207
  2. package/cli/config-bootstrap.js +6 -1
  3. package/cli/openai-bridge.js +541 -210
  4. package/cli.js +189 -4
  5. package/package.json +1 -1
  6. package/plugins/prompt-templates/computed.mjs +61 -3
  7. package/plugins/prompt-templates/manifest.mjs +3 -0
  8. package/web-ui/app.js +14 -3
  9. package/web-ui/modules/app.computed.main-tabs.mjs +39 -30
  10. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  11. package/web-ui/modules/app.methods.index.mjs +2 -0
  12. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  13. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  14. package/web-ui/modules/app.methods.providers.mjs +36 -10
  15. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  16. package/web-ui/modules/app.methods.startup-claude.mjs +7 -0
  17. package/web-ui/modules/app.methods.tool-config-permissions.mjs +87 -0
  18. package/web-ui/modules/config-mode.computed.mjs +3 -3
  19. package/web-ui/modules/i18n/locales/en.mjs +1140 -0
  20. package/web-ui/modules/i18n/locales/ja.mjs +1130 -0
  21. package/web-ui/modules/i18n/locales/vi.mjs +239 -0
  22. package/web-ui/modules/i18n/locales/zh.mjs +1143 -0
  23. package/web-ui/modules/i18n.dict.mjs +9 -3195
  24. package/web-ui/modules/i18n.mjs +65 -16
  25. package/web-ui/partials/index/layout-header.html +16 -46
  26. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  27. package/web-ui/partials/index/modal-webhook.html +8 -8
  28. package/web-ui/partials/index/modals-basic.html +56 -16
  29. package/web-ui/partials/index/panel-config-claude.html +51 -21
  30. package/web-ui/partials/index/panel-config-codex.html +34 -5
  31. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  32. package/web-ui/partials/index/panel-dashboard.html +62 -77
  33. package/web-ui/partials/index/panel-settings.html +28 -7
  34. package/web-ui/partials/index/panel-trash.html +14 -14
  35. package/web-ui/res/web-ui-render.precompiled.js +1783 -1386
  36. package/web-ui/styles/controls-forms.css +99 -0
  37. package/web-ui/styles/dashboard.css +46 -14
  38. package/web-ui/styles/layout-shell.css +45 -0
  39. package/web-ui/styles/navigation-panels.css +3 -3
  40. package/web-ui/styles/openclaw-structured.css +383 -33
  41. package/web-ui/styles/responsive.css +68 -0
  42. package/web-ui/styles/sessions-usage.css +105 -9
  43. package/web-ui/styles/settings-panel.css +4 -0
@@ -43,93 +43,99 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
43
43
  };
44
44
  }
45
45
 
46
- function createTaskDraftChecklist(metrics) {
46
+ function translateTaskText(t, key, fallback, params = null) {
47
+ if (typeof t !== 'function') return fallback;
48
+ const translated = t(key, params);
49
+ return translated === key ? fallback : translated;
50
+ }
51
+
52
+ function createTaskDraftChecklist(metrics, t = null) {
47
53
  const workflowReady = metrics.engine !== 'workflow' || metrics.workflowCount > 0;
48
54
  const scopeReady = metrics.hasNotes || !metrics.allowWrite;
49
55
  const previewReady = metrics.hasPlan && metrics.planIssues.length === 0;
50
56
  return [
51
57
  {
52
58
  key: 'target',
53
- label: '目标',
59
+ label: translateTaskText(t, 'orchestration.readiness.target.label', '目标'),
54
60
  done: metrics.hasTarget,
55
- detail: metrics.hasTarget ? '已写目标' : '还没写目标'
61
+ detail: metrics.hasTarget ? translateTaskText(t, 'orchestration.readiness.target.done', '已写目标') : translateTaskText(t, 'orchestration.readiness.target.missing', '还没写目标')
56
62
  },
57
63
  {
58
64
  key: 'engine',
59
- label: metrics.engine === 'workflow' ? 'Workflow' : '执行策略',
65
+ label: metrics.engine === 'workflow' ? 'Workflow' : translateTaskText(t, 'orchestration.readiness.engine.label', '执行策略'),
60
66
  done: workflowReady,
61
67
  detail: metrics.engine === 'workflow'
62
- ? (metrics.workflowCount > 0 ? `已选 ${metrics.workflowCount} 个 Workflow` : '还没选 Workflow ID')
63
- : '使用 Codex 规划节点'
68
+ ? (metrics.workflowCount > 0 ? translateTaskText(t, 'orchestration.readiness.workflow.done', `已选 ${metrics.workflowCount} 个 Workflow`, { count: metrics.workflowCount }) : translateTaskText(t, 'orchestration.readiness.workflow.missing', '还没选 Workflow ID'))
69
+ : translateTaskText(t, 'orchestration.readiness.engine.codex', '使用 Codex 规划节点')
64
70
  },
65
71
  {
66
72
  key: 'scope',
67
- label: '边界',
73
+ label: translateTaskText(t, 'orchestration.readiness.scope.label', '边界'),
68
74
  done: scopeReady,
69
75
  detail: metrics.hasNotes
70
- ? '已补充说明'
71
- : (metrics.allowWrite ? '建议补说明后再写入' : '当前是只读,可直接试')
76
+ ? translateTaskText(t, 'orchestration.readiness.scope.done', '已补充说明')
77
+ : (metrics.allowWrite ? translateTaskText(t, 'orchestration.readiness.scope.writeHint', '建议补说明后再写入') : translateTaskText(t, 'orchestration.readiness.scope.readonlyHint', '当前是只读,可直接试'))
72
78
  },
73
79
  {
74
80
  key: 'preview',
75
- label: '预览',
81
+ label: translateTaskText(t, 'orchestration.readiness.preview.label', '预览'),
76
82
  done: previewReady,
77
83
  detail: !metrics.hasPlan
78
- ? '还没生成计划'
79
- : (metrics.planIssues.length > 0 ? `有 ${metrics.planIssues.length} 个阻塞项` : `计划可用,${metrics.planNodeCount} 个节点`)
84
+ ? translateTaskText(t, 'orchestration.readiness.preview.missing', '还没生成计划')
85
+ : (metrics.planIssues.length > 0 ? translateTaskText(t, 'orchestration.readiness.preview.blocked', `有 ${metrics.planIssues.length} 个阻塞项`, { count: metrics.planIssues.length }) : translateTaskText(t, 'orchestration.readiness.preview.ready', `计划可用,${metrics.planNodeCount} 个节点`, { count: metrics.planNodeCount }))
80
86
  }
81
87
  ];
82
88
  }
83
89
 
84
- function createTaskDraftReadiness(metrics) {
90
+ function createTaskDraftReadiness(metrics, t = null) {
85
91
  if (!metrics.hasTarget) {
86
92
  return {
87
93
  tone: 'neutral',
88
- title: '先写目标',
89
- summary: '先把想完成的结果写清楚,再让编排器拆节点。'
94
+ title: translateTaskText(t, 'orchestration.readiness.empty.title', '先写目标'),
95
+ summary: translateTaskText(t, 'orchestration.readiness.empty.summary', '先把想完成的结果写清楚,再让编排器拆节点。')
90
96
  };
91
97
  }
92
98
  if (metrics.engine === 'workflow' && metrics.workflowCount === 0) {
93
99
  return {
94
100
  tone: 'warn',
95
- title: '缺少 Workflow',
96
- summary: '你已经选了 Workflow 模式,但还没指定可复用流程。'
101
+ title: translateTaskText(t, 'orchestration.readiness.workflow.title', '缺少 Workflow'),
102
+ summary: translateTaskText(t, 'orchestration.readiness.workflow.summary', '你已经选了 Workflow 模式,但还没指定可复用流程。')
97
103
  };
98
104
  }
99
105
  if (!metrics.hasPlan) {
100
106
  return {
101
107
  tone: 'warn',
102
- title: '建议先预览',
103
- summary: '草稿已成形,先生成一次计划,确认节点和依赖再执行。'
108
+ title: translateTaskText(t, 'orchestration.readiness.preview.title', '建议先预览'),
109
+ summary: translateTaskText(t, 'orchestration.readiness.preview.summary', '草稿已成形,先生成一次计划,确认节点和依赖再执行。')
104
110
  };
105
111
  }
106
112
  if (metrics.planIssues.length > 0) {
107
113
  return {
108
114
  tone: 'error',
109
- title: '预览有阻塞',
110
- summary: `当前计划里还有 ${metrics.planIssues.length} 个阻塞项,先处理它们。`
115
+ title: translateTaskText(t, 'orchestration.readiness.blocked.title', '预览有阻塞'),
116
+ summary: translateTaskText(t, 'orchestration.readiness.blocked.summary', `当前计划里还有 ${metrics.planIssues.length} 个阻塞项,先处理它们。`, { count: metrics.planIssues.length })
111
117
  };
112
118
  }
113
119
  if (metrics.planWarnings.length > 0) {
114
120
  return {
115
121
  tone: 'warn',
116
- title: '可以执行,但有提醒',
117
- summary: `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`
122
+ title: translateTaskText(t, 'orchestration.readiness.warn.title', '可以执行,但有提醒'),
123
+ summary: translateTaskText(t, 'orchestration.readiness.warn.summary', `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`, { count: metrics.planWarnings.length })
118
124
  };
119
125
  }
120
126
  if (metrics.dryRun) {
121
127
  return {
122
128
  tone: 'success',
123
- title: '适合先预演',
124
- summary: '现在可以安全地跑一次仅预演,先看结果再决定是否真实执行。'
129
+ title: translateTaskText(t, 'orchestration.readiness.dryRun.title', '适合先预演'),
130
+ summary: translateTaskText(t, 'orchestration.readiness.dryRun.summary', '现在可以安全地跑一次仅预演,先看结果再决定是否真实执行。')
125
131
  };
126
132
  }
127
133
  return {
128
134
  tone: 'success',
129
- title: '可以执行',
135
+ title: translateTaskText(t, 'orchestration.readiness.ready.title', '可以执行'),
130
136
  summary: metrics.followUpCount > 0
131
- ? `主目标和收尾动作都已具备,可以直接执行或入队。`
132
- : '主目标已经够清楚了,可以直接执行或入队。'
137
+ ? translateTaskText(t, 'orchestration.readiness.ready.withFollowUps', `主目标和收尾动作都已具备,可以直接执行或入队。`)
138
+ : translateTaskText(t, 'orchestration.readiness.ready.summary', '主目标已经够清楚了,可以直接执行或入队。')
133
139
  };
134
140
  }
135
141
 
@@ -144,6 +150,7 @@ export function createMainTabsComputed() {
144
150
  if (this.mainTab === 'market') return this.t('kicker.market');
145
151
  if (this.mainTab === 'plugins') return this.t('kicker.plugins');
146
152
  if (this.mainTab === 'docs') return this.t('kicker.docs');
153
+ if (this.mainTab === 'trash') return this.t('kicker.trash');
147
154
  return this.t('kicker.settings');
148
155
  },
149
156
  mainTabTitle() {
@@ -155,6 +162,7 @@ export function createMainTabsComputed() {
155
162
  if (this.mainTab === 'market') return this.t('title.market');
156
163
  if (this.mainTab === 'plugins') return this.t('title.plugins');
157
164
  if (this.mainTab === 'docs') return this.t('title.docs');
165
+ if (this.mainTab === 'trash') return this.t('settings.trash.title');
158
166
  return this.t('title.settings');
159
167
  },
160
168
  mainTabSubtitle() {
@@ -166,6 +174,7 @@ export function createMainTabsComputed() {
166
174
  if (this.mainTab === 'market') return this.t('subtitle.market');
167
175
  if (this.mainTab === 'plugins') return this.t('subtitle.plugins');
168
176
  if (this.mainTab === 'docs') return this.t('subtitle.docs');
177
+ if (this.mainTab === 'trash') return this.t('settings.trash.meta');
169
178
  return this.t('subtitle.settings');
170
179
  },
171
180
  taskOrchestrationSelectedRun() {
@@ -196,10 +205,10 @@ export function createMainTabsComputed() {
196
205
  return readTaskOrchestrationDraftMetrics(this.taskOrchestration);
197
206
  },
198
207
  taskOrchestrationDraftChecklist() {
199
- return createTaskDraftChecklist(this.taskOrchestrationDraftMetrics);
208
+ return createTaskDraftChecklist(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
200
209
  },
201
210
  taskOrchestrationDraftReadiness() {
202
- return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics);
211
+ return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
203
212
  }
204
213
  };
205
214
  }
@@ -1,3 +1,67 @@
1
+ function normalizeClaudeText(value) {
2
+ return typeof value === 'string' ? value.trim() : '';
3
+ }
4
+
5
+ function normalizeClaudeBaseUrl(value) {
6
+ return normalizeClaudeText(value).replace(/\/+$/g, '');
7
+ }
8
+
9
+ function isValidClaudeHttpUrl(value) {
10
+ if (!value) return false;
11
+ try {
12
+ const parsed = new URL(value);
13
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
14
+ } catch (_) {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function getClaudeConfigValidationForContext(vm, mode = 'add') {
20
+ const draft = mode === 'edit' ? vm.editingConfig : vm.newClaudeConfig;
21
+ const name = normalizeClaudeText(draft && draft.name);
22
+ const apiKey = normalizeClaudeText(draft && draft.apiKey);
23
+ const externalCredentialType = normalizeClaudeText(draft && draft.externalCredentialType);
24
+ const baseUrl = normalizeClaudeBaseUrl(draft && draft.baseUrl);
25
+ const model = normalizeClaudeText(draft && draft.model);
26
+ const errors = {
27
+ name: '',
28
+ apiKey: '',
29
+ baseUrl: '',
30
+ model: ''
31
+ };
32
+
33
+ if (!name) {
34
+ errors.name = '配置名称不能为空';
35
+ } else if (mode === 'add' && vm.claudeConfigs && vm.claudeConfigs[name]) {
36
+ errors.name = '名称已存在';
37
+ }
38
+
39
+ if (!apiKey && !externalCredentialType) {
40
+ errors.apiKey = 'API Key 必填';
41
+ }
42
+
43
+ if (!baseUrl) {
44
+ errors.baseUrl = 'Base URL 必填';
45
+ } else if (!isValidClaudeHttpUrl(baseUrl)) {
46
+ errors.baseUrl = 'Base URL 仅支持 http/https';
47
+ }
48
+
49
+ if (!model) {
50
+ errors.model = '模型名称必填';
51
+ }
52
+
53
+ return {
54
+ mode,
55
+ name,
56
+ apiKey,
57
+ externalCredentialType,
58
+ baseUrl,
59
+ model,
60
+ errors,
61
+ ok: !errors.name && !errors.apiKey && !errors.baseUrl && !errors.model
62
+ };
63
+ }
64
+
1
65
  export function createClaudeConfigMethods(options = {}) {
2
66
  const { api } = options;
3
67
 
@@ -54,14 +118,31 @@ export function createClaudeConfigMethods(options = {}) {
54
118
  baseUrl: config.baseUrl || '',
55
119
  model: config.model || ''
56
120
  };
121
+ this.showAddClaudeConfigKey = false;
57
122
  this.showClaudeConfigModal = true;
58
123
  },
59
124
 
125
+ getClaudeConfigValidation(mode = 'add') {
126
+ return getClaudeConfigValidationForContext(this, mode);
127
+ },
128
+
129
+ claudeConfigFieldError(mode, fieldName) {
130
+ const validation = getClaudeConfigValidationForContext(this, mode);
131
+ return validation && validation.errors && typeof validation.errors[fieldName] === 'string'
132
+ ? validation.errors[fieldName]
133
+ : '';
134
+ },
135
+
136
+ canSubmitClaudeConfig(mode = 'add') {
137
+ return getClaudeConfigValidationForContext(this, mode).ok;
138
+ },
139
+
60
140
  openEditConfigModal(name) {
61
141
  const config = this.claudeConfigs[name];
62
142
  this.editingConfig = {
63
143
  name: name,
64
144
  apiKey: config.apiKey || '',
145
+ externalCredentialType: config.externalCredentialType || '',
65
146
  baseUrl: config.baseUrl || '',
66
147
  model: config.model || ''
67
148
  };
@@ -70,7 +151,14 @@ export function createClaudeConfigMethods(options = {}) {
70
151
  },
71
152
 
72
153
  updateConfig() {
73
- const name = this.editingConfig.name;
154
+ const validation = getClaudeConfigValidationForContext(this, 'edit');
155
+ if (!validation.ok) {
156
+ return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || '请检查 Claude 配置', 'error');
157
+ }
158
+ const name = validation.name;
159
+ this.editingConfig.apiKey = validation.apiKey;
160
+ this.editingConfig.baseUrl = validation.baseUrl;
161
+ this.editingConfig.model = validation.model;
74
162
  this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
75
163
  this.saveClaudeConfigs();
76
164
  this.showMessage('操作成功', 'success');
@@ -83,7 +171,7 @@ export function createClaudeConfigMethods(options = {}) {
83
171
  closeEditConfigModal() {
84
172
  this.showEditConfigModal = false;
85
173
  this.showEditClaudeConfigKey = false;
86
- this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
174
+ this.editingConfig = { name: '', apiKey: '', externalCredentialType: '', baseUrl: '', model: '' };
87
175
  },
88
176
 
89
177
  toggleEditClaudeConfigKey() {
@@ -91,7 +179,14 @@ export function createClaudeConfigMethods(options = {}) {
91
179
  },
92
180
 
93
181
  async saveAndApplyConfig() {
94
- const name = this.editingConfig.name;
182
+ const validation = getClaudeConfigValidationForContext(this, 'edit');
183
+ if (!validation.ok) {
184
+ return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || '请检查 Claude 配置', 'error');
185
+ }
186
+ const name = validation.name;
187
+ this.editingConfig.apiKey = validation.apiKey;
188
+ this.editingConfig.baseUrl = validation.baseUrl;
189
+ this.editingConfig.model = validation.model;
95
190
  this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
96
191
  this.saveClaudeConfigs();
97
192
 
@@ -125,13 +220,15 @@ export function createClaudeConfigMethods(options = {}) {
125
220
  },
126
221
 
127
222
  addClaudeConfig() {
128
- if (!this.newClaudeConfig.name || !this.newClaudeConfig.name.trim()) {
129
- return this.showMessage('请输入名称', 'error');
130
- }
131
- const name = this.newClaudeConfig.name.trim();
132
- if (this.claudeConfigs[name]) {
133
- return this.showMessage('名称已存在', 'error');
223
+ const validation = getClaudeConfigValidationForContext(this, 'add');
224
+ if (!validation.ok) {
225
+ return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || '请检查 Claude 配置', 'error');
134
226
  }
227
+ this.newClaudeConfig.name = validation.name;
228
+ this.newClaudeConfig.apiKey = validation.apiKey;
229
+ this.newClaudeConfig.baseUrl = validation.baseUrl;
230
+ this.newClaudeConfig.model = validation.model;
231
+ const name = validation.name;
135
232
  const duplicateName = this.findDuplicateClaudeConfigName(this.newClaudeConfig);
136
233
  if (duplicateName) {
137
234
  return this.showMessage('配置已存在', 'info');
@@ -199,6 +296,7 @@ export function createClaudeConfigMethods(options = {}) {
199
296
 
200
297
  closeClaudeConfigModal() {
201
298
  this.showClaudeConfigModal = false;
299
+ this.showAddClaudeConfigKey = false;
202
300
  this.newClaudeConfig = {
203
301
  name: '',
204
302
  apiKey: '',
@@ -207,6 +305,10 @@ export function createClaudeConfigMethods(options = {}) {
207
305
  };
208
306
  },
209
307
 
308
+ toggleAddClaudeConfigKey() {
309
+ this.showAddClaudeConfigKey = !this.showAddClaudeConfigKey;
310
+ },
311
+
210
312
  async loadClaudeLocalBridgeStatus() {
211
313
  try {
212
314
  const res = await api('claude-local-bridge-status');
@@ -20,6 +20,7 @@ import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs
20
20
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
21
21
  import { createProvidersMethods } from './app.methods.providers.mjs';
22
22
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
23
+ import { createToolConfigPermissionMethods } from './app.methods.tool-config-permissions.mjs';
23
24
  import { createTaskOrchestrationMethods } from './app.methods.task-orchestration.mjs';
24
25
  import { createSessionActionMethods } from './app.methods.session-actions.mjs';
25
26
  import { createSessionBrowserMethods } from './app.methods.session-browser.mjs';
@@ -81,6 +82,7 @@ export function createAppMethods() {
81
82
  ...createAgentsMethods({ api, apiWithMeta }),
82
83
  ...createProvidersMethods({ api }),
83
84
  ...createClaudeConfigMethods({ api }),
85
+ ...createToolConfigPermissionMethods({ api }),
84
86
  ...createOpenclawCoreMethods(),
85
87
  ...createOpenclawEditingMethods(),
86
88
  ...createOpenclawPersistMethods({
@@ -367,6 +367,54 @@ export function createOpenclawEditingMethods() {
367
367
  this.showMessage('保存本地 OpenClaw 配置失败', 'error');
368
368
  return false;
369
369
  }
370
+ },
371
+
372
+ // Accordion stepper methods
373
+ toggleAccordionStep(step) {
374
+ if (this.openclawAccordionStep === step) {
375
+ // Don't allow collapsing the current step
376
+ return;
377
+ }
378
+ this.openclawAccordionStep = step;
379
+ },
380
+
381
+ nextAccordionStep() {
382
+ if (this.openclawAccordionStep < 3) {
383
+ this.openclawAccordionStep++;
384
+ }
385
+ },
386
+
387
+ prevAccordionStep() {
388
+ if (this.openclawAccordionStep > 1) {
389
+ this.openclawAccordionStep--;
390
+ }
391
+ },
392
+
393
+ finishAccordionStep() {
394
+ this.openclawAccordionStep = 4; // Mark as complete
395
+ this.applyOpenclawQuickToText();
396
+ },
397
+
398
+ validateProviderName() {
399
+ const name = (this.openclawQuick.providerName || '').trim();
400
+ if (!name) {
401
+ this.openclawValidation.providerName = { valid: false, message: '必填' };
402
+ return;
403
+ }
404
+ if (name.includes('/')) {
405
+ this.openclawValidation.providerName = { valid: false, message: '不能包含 "/"' };
406
+ return;
407
+ }
408
+ this.openclawValidation.providerName = { valid: true, message: '' };
409
+ },
410
+
411
+ validateModelId() {
412
+ const id = (this.openclawQuick.modelId || '').trim();
413
+ if (!id) {
414
+ this.openclawValidation.modelId = { valid: false, message: '必填' };
415
+ return;
416
+ }
417
+ this.openclawValidation.modelId = { valid: true, message: '' };
370
418
  }
371
419
  };
372
420
  }
@@ -1,4 +1,4 @@
1
- const DEFAULT_OPENCLAW_CONFIG_NAME = '默认配置';
1
+ export const DEFAULT_OPENCLAW_CONFIG_NAME = '默认配置';
2
2
 
3
3
  function buildNormalizedOpenclawConfigs(configs, defaultContent = '') {
4
4
  const source = configs && typeof configs === 'object' && !Array.isArray(configs)
@@ -11,7 +11,8 @@ function buildNormalizedOpenclawConfigs(configs, defaultContent = '') {
11
11
  : { content: defaultContent };
12
12
  const normalized = {
13
13
  [DEFAULT_OPENCLAW_CONFIG_NAME]: {
14
- content: typeof defaultEntry.content === 'string' ? defaultEntry.content : defaultContent
14
+ content: typeof defaultEntry.content === 'string' ? defaultEntry.content : defaultContent,
15
+ isDefault: true
15
16
  }
16
17
  };
17
18
  for (const [name, value] of Object.entries(source)) {
@@ -25,7 +26,8 @@ function syncDefaultOpenclawConfigState(vm, content, options = {}) {
25
26
  const nextContent = typeof content === 'string' ? content : '';
26
27
  vm.openclawConfigs = buildNormalizedOpenclawConfigs(vm.openclawConfigs, nextContent);
27
28
  vm.openclawConfigs[DEFAULT_OPENCLAW_CONFIG_NAME] = {
28
- content: nextContent
29
+ content: nextContent,
30
+ isDefault: true
29
31
  };
30
32
  if (typeof options.path === 'string') {
31
33
  vm.openclawConfigPath = options.path;
@@ -51,6 +53,10 @@ export function createOpenclawPersistMethods(options = {}) {
51
53
  } = options;
52
54
 
53
55
  return {
56
+ isDefaultOpenclawConfig(name, config = null) {
57
+ return !!(config && config.isDefault === true) || name === DEFAULT_OPENCLAW_CONFIG_NAME;
58
+ },
59
+
54
60
  syncDefaultOpenclawConfigEntry(options = {}) {
55
61
  const silent = !!options.silent;
56
62
  return api('get-openclaw-config')
@@ -104,7 +110,7 @@ export function createOpenclawPersistMethods(options = {}) {
104
110
 
105
111
  openOpenclawEditModal(name) {
106
112
  const existing = this.openclawConfigs[name];
107
- const isDefaultConfig = name === DEFAULT_OPENCLAW_CONFIG_NAME;
113
+ const isDefaultConfig = this.isDefaultOpenclawConfig(name, existing);
108
114
  const modalToken = (Number(this.openclawModalLoadToken || 0) + 1);
109
115
  this.openclawModalLoadToken = modalToken;
110
116
  this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
@@ -146,7 +152,7 @@ export function createOpenclawPersistMethods(options = {}) {
146
152
  const force = !!options.force;
147
153
  const fallbackToTemplate = options.fallbackToTemplate !== false;
148
154
  const syncDefaultEntry = options.syncDefaultEntry === true
149
- || (this.openclawEditing && this.openclawEditing.lockName && this.openclawEditing.name === DEFAULT_OPENCLAW_CONFIG_NAME);
155
+ || (this.openclawEditing && this.openclawEditing.lockName && this.isDefaultOpenclawConfig(this.openclawEditing.name));
150
156
  const modalToken = Number(options.modalToken || this.openclawModalLoadToken || 0);
151
157
  const expectedEditorContent = typeof options.expectedEditorContent === 'string'
152
158
  ? options.expectedEditorContent
@@ -260,7 +266,7 @@ export function createOpenclawPersistMethods(options = {}) {
260
266
  if (this.openclawSaving || this.openclawApplying) {
261
267
  return;
262
268
  }
263
- if (this.openclawEditing && this.openclawEditing.lockName && this.openclawEditing.name === DEFAULT_OPENCLAW_CONFIG_NAME) {
269
+ if (this.openclawEditing && this.openclawEditing.lockName && this.isDefaultOpenclawConfig(this.openclawEditing.name)) {
264
270
  this.showMessage('默认配置代表当前系统配置,请使用“保存并应用”', 'info');
265
271
  return;
266
272
  }
@@ -312,7 +318,7 @@ export function createOpenclawPersistMethods(options = {}) {
312
318
  },
313
319
 
314
320
  async deleteOpenclawConfig(name) {
315
- if (name === DEFAULT_OPENCLAW_CONFIG_NAME) {
321
+ if (this.isDefaultOpenclawConfig(name, this.openclawConfigs && this.openclawConfigs[name])) {
316
322
  return this.showMessage('默认配置始终映射当前系统配置,不可删除', 'info');
317
323
  }
318
324
  if (Object.keys(this.openclawConfigs).length <= 1) {
@@ -47,6 +47,12 @@ function normalizeProviderDraftState(target) {
47
47
  if (typeof target.url === 'string') {
48
48
  target.url = normalizeProviderUrl(target.url);
49
49
  }
50
+ if (typeof target.model === 'string') {
51
+ target.model = target.model.trim();
52
+ }
53
+ if (typeof target.key === 'string') {
54
+ target.key = target.key.trim();
55
+ }
50
56
  }
51
57
 
52
58
  function maskKeyLocal(key) {
@@ -66,9 +72,13 @@ function getProviderValidationForContext(vm, mode = 'add') {
66
72
  const editingName = mode === 'edit' ? normalizeText(draft && draft.name) : '';
67
73
  const name = normalizeText(draft && draft.name);
68
74
  const url = normalizeProviderUrl(draft && draft.url);
75
+ const model = normalizeText(draft && draft.model);
76
+ const key = normalizeText(draft && draft.key);
69
77
  const errors = {
70
78
  name: '',
71
- url: ''
79
+ url: '',
80
+ key: '',
81
+ model: ''
72
82
  };
73
83
 
74
84
  if (mode === 'add') {
@@ -91,12 +101,22 @@ function getProviderValidationForContext(vm, mode = 'add') {
91
101
  errors.url = 'URL 仅支持 http/https';
92
102
  }
93
103
 
104
+ if (mode === 'add' && !key) {
105
+ errors.key = 'API Key 必填';
106
+ }
107
+
108
+ if (mode === 'add' && !model) {
109
+ errors.model = '模型名称必填';
110
+ }
111
+
94
112
  return {
95
113
  mode,
96
114
  name,
97
115
  url,
116
+ key,
117
+ model,
98
118
  errors,
99
- ok: !errors.name && !errors.url
119
+ ok: !errors.name && !errors.url && !errors.key && !errors.model
100
120
  };
101
121
  }
102
122
 
@@ -150,21 +170,20 @@ export function createProvidersMethods(options = {}) {
150
170
  normalizeProviderDraftState(this.newProvider);
151
171
  const validation = getProviderValidationForContext(this, 'add');
152
172
  if (!validation.ok) {
153
- return this.showMessage(validation.errors.name || validation.errors.url || '名称和URL必填', 'error');
173
+ return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || '名称、URL、API Key 和模型名称必填', 'error');
154
174
  }
155
175
 
156
176
  try {
157
177
  const payload = {
158
178
  name: validation.name,
159
179
  url: validation.url,
160
- key: this.newProvider.key || ''
180
+ key: validation.key,
181
+ model: validation.model
161
182
  };
162
183
  if (this.newProvider && this.newProvider.useTransform) {
163
184
  payload.useTransform = true;
164
185
  }
165
- const suggestedModel = typeof this.newProvider._suggestedModel === 'string'
166
- ? this.newProvider._suggestedModel.trim()
167
- : '';
186
+ const suggestedModel = validation.model;
168
187
  const res = await api('add-provider', payload);
169
188
  if (res.error) {
170
189
  this.showMessage(res.error, 'error');
@@ -179,7 +198,7 @@ export function createProvidersMethods(options = {}) {
179
198
  codexmate_bridge: payload.useTransform ? 'openai' : '',
180
199
  key: maskKeyLocal(payload.key),
181
200
  hasKey: !!payload.key,
182
- models: [],
201
+ models: suggestedModel ? [{ id: suggestedModel, name: suggestedModel, cost: null, contextWindow: undefined, maxTokens: undefined }] : [],
183
202
  current: false,
184
203
  readOnly: false,
185
204
  nonDeletable: false,
@@ -208,7 +227,7 @@ export function createProvidersMethods(options = {}) {
208
227
  const configured = !!(provider && provider.hasKey);
209
228
  return {
210
229
  configured,
211
- text: configured ? '已配置' : '未配置'
230
+ text: configured ? this.t('common.configured') : this.t('common.notConfigured')
212
231
  };
213
232
  },
214
233
 
@@ -300,8 +319,10 @@ export function createProvidersMethods(options = {}) {
300
319
  name: '',
301
320
  url: cloneUrl,
302
321
  key: '',
322
+ model: '',
303
323
  useTransform: isTransform
304
324
  };
325
+ this.showAddProviderKey = false;
305
326
  this.showAddModal = true;
306
327
  },
307
328
 
@@ -513,7 +534,12 @@ export function createProvidersMethods(options = {}) {
513
534
 
514
535
  closeAddModal() {
515
536
  this.showAddModal = false;
516
- this.newProvider = { name: '', url: '', key: '', useTransform: false, _suggestedModel: '' };
537
+ this.showAddProviderKey = false;
538
+ this.newProvider = { name: '', url: '', key: '', model: '', useTransform: false };
539
+ },
540
+
541
+ toggleAddProviderKey() {
542
+ this.showAddProviderKey = !this.showAddProviderKey;
517
543
  },
518
544
 
519
545
  closeModelModal() {