codexmate 0.0.38 → 0.0.39

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 (34) hide show
  1. package/cli/builtin-proxy.js +626 -207
  2. package/cli/openai-bridge.js +541 -210
  3. package/cli.js +19 -1
  4. package/package.json +1 -1
  5. package/web-ui/app.js +12 -3
  6. package/web-ui/modules/app.computed.main-tabs.mjs +37 -30
  7. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  8. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  9. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  10. package/web-ui/modules/app.methods.providers.mjs +36 -10
  11. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  12. package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
  13. package/web-ui/modules/config-mode.computed.mjs +3 -3
  14. package/web-ui/modules/i18n.dict.mjs +13 -0
  15. package/web-ui/modules/i18n.mjs +65 -16
  16. package/web-ui/partials/index/layout-header.html +16 -46
  17. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  18. package/web-ui/partials/index/modal-webhook.html +8 -8
  19. package/web-ui/partials/index/modals-basic.html +56 -16
  20. package/web-ui/partials/index/panel-config-claude.html +20 -20
  21. package/web-ui/partials/index/panel-config-codex.html +5 -5
  22. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  23. package/web-ui/partials/index/panel-dashboard.html +62 -77
  24. package/web-ui/partials/index/panel-settings.html +28 -7
  25. package/web-ui/partials/index/panel-trash.html +14 -14
  26. package/web-ui/res/web-ui-render.precompiled.js +846 -539
  27. package/web-ui/styles/controls-forms.css +6 -0
  28. package/web-ui/styles/dashboard.css +46 -14
  29. package/web-ui/styles/layout-shell.css +45 -0
  30. package/web-ui/styles/navigation-panels.css +3 -3
  31. package/web-ui/styles/openclaw-structured.css +383 -33
  32. package/web-ui/styles/responsive.css +68 -0
  33. package/web-ui/styles/sessions-usage.css +105 -9
  34. package/web-ui/styles/settings-panel.css +4 -0
package/cli.js CHANGED
@@ -2081,12 +2081,22 @@ function addProviderToConfig(params = {}) {
2081
2081
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2082
2082
  const url = typeof params.url === 'string' ? params.url.trim() : '';
2083
2083
  const key = typeof params.key === 'string' ? params.key.trim() : '';
2084
+ const requireModel = !!params.requireModel;
2085
+ const fallbackModel = (() => {
2086
+ if (requireModel) return '';
2087
+ const list = readModels();
2088
+ return Array.isArray(list) && typeof list[0] === 'string' ? list[0].trim() : '';
2089
+ })();
2090
+ const model = typeof params.model === 'string' && params.model.trim()
2091
+ ? params.model.trim()
2092
+ : fallbackModel;
2084
2093
  const useTransform = !!params.useTransform;
2085
2094
  const allowManaged = !!params.allowManaged;
2086
2095
  const normalizedUrl = normalizeBaseUrl(url);
2087
2096
 
2088
2097
  if (!name) return { error: '名称不能为空' };
2089
2098
  if (!url) return { error: 'URL 不能为空' };
2099
+ if (!model) return { error: '模型名称不能为空' };
2090
2100
  if (!isValidProviderName(name)) {
2091
2101
  return { error: '名称仅支持字母/数字/._-' };
2092
2102
  }
@@ -2163,6 +2173,7 @@ function addProviderToConfig(params = {}) {
2163
2173
  `wire_api = "responses"`,
2164
2174
  `requires_openai_auth = ${requiresOpenaiAuth ? 'true' : 'false'}`,
2165
2175
  `preferred_auth_method = "${safeKey}"`,
2176
+ `models = [{ id = "${escapeTomlBasicString(model)}", name = "${escapeTomlBasicString(model)}" }]`,
2166
2177
  ...extraLines,
2167
2178
  `request_max_retries = 4`,
2168
2179
  `stream_max_retries = 10`,
@@ -2173,6 +2184,13 @@ function addProviderToConfig(params = {}) {
2173
2184
 
2174
2185
  try {
2175
2186
  writeConfig(newContent);
2187
+ const models = readModels();
2188
+ if (!models.includes(model)) {
2189
+ writeModels([...models, model]);
2190
+ }
2191
+ const currentModels = readCurrentModels();
2192
+ currentModels[name] = model;
2193
+ writeCurrentModels(currentModels);
2176
2194
  } catch (e) {
2177
2195
  return { error: `写入配置失败: ${e.message}` };
2178
2196
  }
@@ -10866,7 +10884,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10866
10884
  result = buildConfigTemplateDiff(params || {});
10867
10885
  break;
10868
10886
  case 'add-provider':
10869
- result = addProviderToConfig(params || {});
10887
+ result = addProviderToConfig({ ...(params || {}), requireModel: true });
10870
10888
  break;
10871
10889
  case 'update-provider':
10872
10890
  result = updateProviderInConfig(params || {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -62,11 +62,13 @@ document.addEventListener('DOMContentLoaded', () => {
62
62
  messageType: '',
63
63
  showAddModal: false,
64
64
  showEditModal: false,
65
+ showAddProviderKey: false,
65
66
  showEditProviderKey: false,
66
67
  showModelModal: false,
67
68
  showModelListModal: false,
68
69
  showClaudeConfigModal: false,
69
70
  showEditConfigModal: false,
71
+ showAddClaudeConfigKey: false,
70
72
  showEditClaudeConfigKey: false,
71
73
  showOpenclawConfigModal: false,
72
74
  showConfigTemplateModal: false,
@@ -268,7 +270,7 @@ document.addEventListener('DOMContentLoaded', () => {
268
270
  installRegistryPreset: 'default',
269
271
  installRegistryCustom: '',
270
272
  installStatusTargets: null,
271
- newProvider: { name: '', url: '', key: '', useTransform: false, _suggestedModel: '' },
273
+ newProvider: { name: '', url: '', key: '', model: '', useTransform: false },
272
274
  resetConfigLoading: false,
273
275
  editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
274
276
  newModelName: '',
@@ -293,7 +295,8 @@ document.addEventListener('DOMContentLoaded', () => {
293
295
  currentOpenclawConfig: '',
294
296
  openclawConfigs: {
295
297
  '默认配置': {
296
- content: DEFAULT_OPENCLAW_TEMPLATE
298
+ content: DEFAULT_OPENCLAW_TEMPLATE,
299
+ isDefault: true
297
300
  }
298
301
  },
299
302
  openclawEditing: { name: '', content: '', lockName: false },
@@ -343,6 +346,11 @@ document.addEventListener('DOMContentLoaded', () => {
343
346
  overrideModels: true,
344
347
  showKey: false
345
348
  },
349
+ openclawAccordionStep: 1,
350
+ openclawValidation: {
351
+ providerName: { valid: true, message: '' },
352
+ modelId: { valid: true, message: '' }
353
+ },
346
354
  openclawAgentsList: [],
347
355
  openclawProviders: [],
348
356
  openclawMissingProviders: [],
@@ -567,7 +575,8 @@ document.addEventListener('DOMContentLoaded', () => {
567
575
  : { content: DEFAULT_OPENCLAW_TEMPLATE };
568
576
  const normalized = {
569
577
  '默认配置': {
570
- content: typeof defaultEntry.content === 'string' ? defaultEntry.content : DEFAULT_OPENCLAW_TEMPLATE
578
+ content: typeof defaultEntry.content === 'string' ? defaultEntry.content : DEFAULT_OPENCLAW_TEMPLATE,
579
+ isDefault: true
571
580
  }
572
581
  };
573
582
  for (const [name, value] of Object.entries(source)) {
@@ -43,93 +43,97 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
43
43
  };
44
44
  }
45
45
 
46
- function createTaskDraftChecklist(metrics) {
46
+ function translateTaskText(t, key, fallback, params = null) {
47
+ return typeof t === 'function' ? t(key, params) : fallback;
48
+ }
49
+
50
+ function createTaskDraftChecklist(metrics, t = null) {
47
51
  const workflowReady = metrics.engine !== 'workflow' || metrics.workflowCount > 0;
48
52
  const scopeReady = metrics.hasNotes || !metrics.allowWrite;
49
53
  const previewReady = metrics.hasPlan && metrics.planIssues.length === 0;
50
54
  return [
51
55
  {
52
56
  key: 'target',
53
- label: '目标',
57
+ label: translateTaskText(t, 'orchestration.readiness.target.label', '目标'),
54
58
  done: metrics.hasTarget,
55
- detail: metrics.hasTarget ? '已写目标' : '还没写目标'
59
+ detail: metrics.hasTarget ? translateTaskText(t, 'orchestration.readiness.target.done', '已写目标') : translateTaskText(t, 'orchestration.readiness.target.missing', '还没写目标')
56
60
  },
57
61
  {
58
62
  key: 'engine',
59
- label: metrics.engine === 'workflow' ? 'Workflow' : '执行策略',
63
+ label: metrics.engine === 'workflow' ? 'Workflow' : translateTaskText(t, 'orchestration.readiness.engine.label', '执行策略'),
60
64
  done: workflowReady,
61
65
  detail: metrics.engine === 'workflow'
62
- ? (metrics.workflowCount > 0 ? `已选 ${metrics.workflowCount} 个 Workflow` : '还没选 Workflow ID')
63
- : '使用 Codex 规划节点'
66
+ ? (metrics.workflowCount > 0 ? translateTaskText(t, 'orchestration.readiness.workflow.done', `已选 ${metrics.workflowCount} 个 Workflow`, { count: metrics.workflowCount }) : translateTaskText(t, 'orchestration.readiness.workflow.missing', '还没选 Workflow ID'))
67
+ : translateTaskText(t, 'orchestration.readiness.engine.codex', '使用 Codex 规划节点')
64
68
  },
65
69
  {
66
70
  key: 'scope',
67
- label: '边界',
71
+ label: translateTaskText(t, 'orchestration.readiness.scope.label', '边界'),
68
72
  done: scopeReady,
69
73
  detail: metrics.hasNotes
70
- ? '已补充说明'
71
- : (metrics.allowWrite ? '建议补说明后再写入' : '当前是只读,可直接试')
74
+ ? translateTaskText(t, 'orchestration.readiness.scope.done', '已补充说明')
75
+ : (metrics.allowWrite ? translateTaskText(t, 'orchestration.readiness.scope.writeHint', '建议补说明后再写入') : translateTaskText(t, 'orchestration.readiness.scope.readonlyHint', '当前是只读,可直接试'))
72
76
  },
73
77
  {
74
78
  key: 'preview',
75
- label: '预览',
79
+ label: translateTaskText(t, 'orchestration.readiness.preview.label', '预览'),
76
80
  done: previewReady,
77
81
  detail: !metrics.hasPlan
78
- ? '还没生成计划'
79
- : (metrics.planIssues.length > 0 ? `有 ${metrics.planIssues.length} 个阻塞项` : `计划可用,${metrics.planNodeCount} 个节点`)
82
+ ? translateTaskText(t, 'orchestration.readiness.preview.missing', '还没生成计划')
83
+ : (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
84
  }
81
85
  ];
82
86
  }
83
87
 
84
- function createTaskDraftReadiness(metrics) {
88
+ function createTaskDraftReadiness(metrics, t = null) {
85
89
  if (!metrics.hasTarget) {
86
90
  return {
87
91
  tone: 'neutral',
88
- title: '先写目标',
89
- summary: '先把想完成的结果写清楚,再让编排器拆节点。'
92
+ title: translateTaskText(t, 'orchestration.readiness.empty.title', '先写目标'),
93
+ summary: translateTaskText(t, 'orchestration.readiness.empty.summary', '先把想完成的结果写清楚,再让编排器拆节点。')
90
94
  };
91
95
  }
92
96
  if (metrics.engine === 'workflow' && metrics.workflowCount === 0) {
93
97
  return {
94
98
  tone: 'warn',
95
- title: '缺少 Workflow',
96
- summary: '你已经选了 Workflow 模式,但还没指定可复用流程。'
99
+ title: translateTaskText(t, 'orchestration.readiness.workflow.title', '缺少 Workflow'),
100
+ summary: translateTaskText(t, 'orchestration.readiness.workflow.summary', '你已经选了 Workflow 模式,但还没指定可复用流程。')
97
101
  };
98
102
  }
99
103
  if (!metrics.hasPlan) {
100
104
  return {
101
105
  tone: 'warn',
102
- title: '建议先预览',
103
- summary: '草稿已成形,先生成一次计划,确认节点和依赖再执行。'
106
+ title: translateTaskText(t, 'orchestration.readiness.preview.title', '建议先预览'),
107
+ summary: translateTaskText(t, 'orchestration.readiness.preview.summary', '草稿已成形,先生成一次计划,确认节点和依赖再执行。')
104
108
  };
105
109
  }
106
110
  if (metrics.planIssues.length > 0) {
107
111
  return {
108
112
  tone: 'error',
109
- title: '预览有阻塞',
110
- summary: `当前计划里还有 ${metrics.planIssues.length} 个阻塞项,先处理它们。`
113
+ title: translateTaskText(t, 'orchestration.readiness.blocked.title', '预览有阻塞'),
114
+ summary: translateTaskText(t, 'orchestration.readiness.blocked.summary', `当前计划里还有 ${metrics.planIssues.length} 个阻塞项,先处理它们。`, { count: metrics.planIssues.length })
111
115
  };
112
116
  }
113
117
  if (metrics.planWarnings.length > 0) {
114
118
  return {
115
119
  tone: 'warn',
116
- title: '可以执行,但有提醒',
117
- summary: `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`
120
+ title: translateTaskText(t, 'orchestration.readiness.warn.title', '可以执行,但有提醒'),
121
+ summary: translateTaskText(t, 'orchestration.readiness.warn.summary', `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`, { count: metrics.planWarnings.length })
118
122
  };
119
123
  }
120
124
  if (metrics.dryRun) {
121
125
  return {
122
126
  tone: 'success',
123
- title: '适合先预演',
124
- summary: '现在可以安全地跑一次仅预演,先看结果再决定是否真实执行。'
127
+ title: translateTaskText(t, 'orchestration.readiness.dryRun.title', '适合先预演'),
128
+ summary: translateTaskText(t, 'orchestration.readiness.dryRun.summary', '现在可以安全地跑一次仅预演,先看结果再决定是否真实执行。')
125
129
  };
126
130
  }
127
131
  return {
128
132
  tone: 'success',
129
- title: '可以执行',
133
+ title: translateTaskText(t, 'orchestration.readiness.ready.title', '可以执行'),
130
134
  summary: metrics.followUpCount > 0
131
- ? `主目标和收尾动作都已具备,可以直接执行或入队。`
132
- : '主目标已经够清楚了,可以直接执行或入队。'
135
+ ? translateTaskText(t, 'orchestration.readiness.ready.withFollowUps', `主目标和收尾动作都已具备,可以直接执行或入队。`)
136
+ : translateTaskText(t, 'orchestration.readiness.ready.summary', '主目标已经够清楚了,可以直接执行或入队。')
133
137
  };
134
138
  }
135
139
 
@@ -144,6 +148,7 @@ export function createMainTabsComputed() {
144
148
  if (this.mainTab === 'market') return this.t('kicker.market');
145
149
  if (this.mainTab === 'plugins') return this.t('kicker.plugins');
146
150
  if (this.mainTab === 'docs') return this.t('kicker.docs');
151
+ if (this.mainTab === 'trash') return this.t('kicker.trash');
147
152
  return this.t('kicker.settings');
148
153
  },
149
154
  mainTabTitle() {
@@ -155,6 +160,7 @@ export function createMainTabsComputed() {
155
160
  if (this.mainTab === 'market') return this.t('title.market');
156
161
  if (this.mainTab === 'plugins') return this.t('title.plugins');
157
162
  if (this.mainTab === 'docs') return this.t('title.docs');
163
+ if (this.mainTab === 'trash') return this.t('settings.trash.title');
158
164
  return this.t('title.settings');
159
165
  },
160
166
  mainTabSubtitle() {
@@ -166,6 +172,7 @@ export function createMainTabsComputed() {
166
172
  if (this.mainTab === 'market') return this.t('subtitle.market');
167
173
  if (this.mainTab === 'plugins') return this.t('subtitle.plugins');
168
174
  if (this.mainTab === 'docs') return this.t('subtitle.docs');
175
+ if (this.mainTab === 'trash') return this.t('settings.trash.meta');
169
176
  return this.t('subtitle.settings');
170
177
  },
171
178
  taskOrchestrationSelectedRun() {
@@ -196,10 +203,10 @@ export function createMainTabsComputed() {
196
203
  return readTaskOrchestrationDraftMetrics(this.taskOrchestration);
197
204
  },
198
205
  taskOrchestrationDraftChecklist() {
199
- return createTaskDraftChecklist(this.taskOrchestrationDraftMetrics);
206
+ return createTaskDraftChecklist(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
200
207
  },
201
208
  taskOrchestrationDraftReadiness() {
202
- return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics);
209
+ return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
203
210
  }
204
211
  };
205
212
  }
@@ -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');
@@ -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) {