codexmate 0.0.8 → 0.0.9

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.
package/web-ui/app.js ADDED
@@ -0,0 +1,2841 @@
1
+ import {
2
+ normalizeClaudeValue,
3
+ normalizeClaudeConfig,
4
+ normalizeClaudeSettingsEnv,
5
+ matchClaudeConfigFromSettings,
6
+ findDuplicateClaudeConfigName,
7
+ formatLatency,
8
+ buildSpeedTestIssue,
9
+ isSessionQueryEnabled,
10
+ buildSessionListParams
11
+ } from './logic.mjs';
12
+
13
+ document.addEventListener('DOMContentLoaded', () => {
14
+ if (typeof Vue === 'undefined') {
15
+ console.error('Vue 库未能在 DOMContentLoaded 触发前加载完成。');
16
+ const fallbackTarget = document.querySelector('#app') || document.querySelector('[v-cloak]');
17
+ if (fallbackTarget) {
18
+ fallbackTarget.removeAttribute('v-cloak');
19
+ fallbackTarget.classList.remove('v-cloak');
20
+ fallbackTarget.innerHTML = '';
21
+ const notice = document.createElement('div');
22
+ notice.className = 'fallback-message';
23
+ notice.textContent = 'Web UI 加载失败:Vue 未加载。请检查网络或刷新页面。';
24
+ fallbackTarget.appendChild(notice);
25
+ }
26
+ return;
27
+ }
28
+ const { createApp } = Vue;
29
+ const API_BASE = (location && location.origin && location.origin !== 'null')
30
+ ? location.origin
31
+ : 'http://localhost:3737';
32
+ const DEFAULT_OPENCLAW_TEMPLATE = `{
33
+ // OpenClaw config (JSON5)
34
+ agent: {
35
+ model: "gpt-4.1"
36
+ },
37
+ agents: {
38
+ defaults: {
39
+ workspace: "~/.openclaw/workspace"
40
+ }
41
+ }
42
+ }`;
43
+
44
+ async function api(action, params = {}) {
45
+ const res = await fetch(`${API_BASE}/api`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ action, params })
49
+ });
50
+ return await res.json();
51
+ }
52
+
53
+ const app = createApp({
54
+ data() {
55
+ return {
56
+ mainTab: 'config',
57
+ configMode: 'codex',
58
+ currentProvider: '',
59
+ currentModel: '',
60
+ serviceTier: 'fast',
61
+ providersList: [],
62
+ models: [],
63
+ codexModelsLoading: false,
64
+ modelsSource: 'remote',
65
+ modelsHasCurrent: true,
66
+ claudeModels: [],
67
+ claudeModelsSource: 'idle',
68
+ claudeModelsHasCurrent: true,
69
+ claudeModelsLoading: false,
70
+ loading: true,
71
+ initError: '',
72
+ message: '',
73
+ messageType: '',
74
+ showAddModal: false,
75
+ showEditModal: false,
76
+ showModelModal: false,
77
+ showModelListModal: false,
78
+ showClaudeConfigModal: false,
79
+ showEditConfigModal: false,
80
+ showOpenclawConfigModal: false,
81
+ showConfigTemplateModal: false,
82
+ showAgentsModal: false,
83
+ configTemplateContent: '',
84
+ configTemplateApplying: false,
85
+ agentsContent: '',
86
+ agentsPath: '',
87
+ agentsExists: false,
88
+ agentsLineEnding: '\n',
89
+ agentsLoading: false,
90
+ agentsSaving: false,
91
+ agentsContext: 'codex',
92
+ agentsModalTitle: 'AGENTS.md 编辑器',
93
+ agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
94
+ sessionsList: [],
95
+ sessionsLoading: false,
96
+ sessionFilterSource: 'codex',
97
+ sessionPathFilter: '',
98
+ sessionQuery: '',
99
+ sessionRoleFilter: 'all',
100
+ sessionTimePreset: 'all',
101
+ sessionResumeWithYolo: true,
102
+ sessionPathOptions: [],
103
+ sessionPathOptionsLoading: false,
104
+ sessionPathOptionsMap: {
105
+ all: [],
106
+ codex: [],
107
+ claude: []
108
+ },
109
+ sessionPathOptionsLoadedMap: {
110
+ all: false,
111
+ codex: false,
112
+ claude: false
113
+ },
114
+ sessionPathRequestSeq: 0,
115
+ sessionExporting: {},
116
+ sessionCloning: {},
117
+ sessionDeleting: {},
118
+ activeSession: null,
119
+ activeSessionMessages: [],
120
+ activeSessionDetailError: '',
121
+ activeSessionDetailClipped: false,
122
+ sessionDetailLoading: false,
123
+ sessionDetailRequestSeq: 0,
124
+ sessionStandalone: false,
125
+ sessionStandaloneError: '',
126
+ sessionStandaloneText: '',
127
+ sessionStandaloneTitle: '',
128
+ sessionStandaloneSourceLabel: '',
129
+ sessionStandaloneLoading: false,
130
+ sessionStandaloneRequestSeq: 0,
131
+ speedResults: {},
132
+ speedLoading: {},
133
+ claudeSpeedResults: {},
134
+ claudeSpeedLoading: {},
135
+ claudeShareLoading: {},
136
+ providerShareLoading: {},
137
+ newProvider: { name: '', url: '', key: '' },
138
+ editingProvider: { name: '', url: '', key: '' },
139
+ newModelName: '',
140
+ currentClaudeConfig: '',
141
+ currentClaudeModel: '',
142
+ editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
143
+ claudeConfigs: {
144
+ '智谱GLM': {
145
+ apiKey: '',
146
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
147
+ model: 'glm-4.7',
148
+ hasKey: false
149
+ }
150
+ },
151
+ newClaudeConfig: {
152
+ name: '',
153
+ apiKey: '',
154
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
155
+ model: 'glm-4.7'
156
+ },
157
+ currentOpenclawConfig: '',
158
+ openclawConfigs: {
159
+ '默认配置': {
160
+ content: DEFAULT_OPENCLAW_TEMPLATE
161
+ }
162
+ },
163
+ openclawEditing: { name: '', content: '', lockName: false },
164
+ openclawEditorTitle: '添加 OpenClaw 配置',
165
+ openclawConfigPath: '',
166
+ openclawConfigExists: false,
167
+ openclawLineEnding: '\n',
168
+ openclawFileLoading: false,
169
+ openclawSaving: false,
170
+ openclawApplying: false,
171
+ openclawWorkspaceFileName: 'SOUL.md',
172
+ agentsWorkspaceFileName: '',
173
+ openclawStructured: {
174
+ agentPrimary: '',
175
+ agentFallbacks: [],
176
+ workspace: '',
177
+ timeout: '',
178
+ contextTokens: '',
179
+ maxConcurrent: '',
180
+ envItems: [],
181
+ toolsProfile: 'default',
182
+ toolsAllow: [],
183
+ toolsDeny: []
184
+ },
185
+ openclawQuick: {
186
+ providerName: '',
187
+ baseUrl: '',
188
+ apiKey: '',
189
+ apiType: 'openai-responses',
190
+ modelId: '',
191
+ modelName: '',
192
+ contextWindow: '',
193
+ maxTokens: '',
194
+ setPrimary: true,
195
+ overrideProvider: true,
196
+ overrideModels: true,
197
+ showKey: false
198
+ },
199
+ openclawAgentsList: [],
200
+ openclawProviders: [],
201
+ openclawMissingProviders: [],
202
+ healthCheckLoading: false,
203
+ healthCheckResult: null,
204
+ healthCheckRemote: false
205
+ }
206
+ },
207
+ mounted() {
208
+ this.initSessionStandalone();
209
+ const savedSessionYolo = localStorage.getItem('codexmateSessionResumeYolo');
210
+ if (savedSessionYolo === '0' || savedSessionYolo === 'false') {
211
+ this.sessionResumeWithYolo = false;
212
+ } else if (savedSessionYolo === '1' || savedSessionYolo === 'true') {
213
+ this.sessionResumeWithYolo = true;
214
+ }
215
+ const savedConfigs = localStorage.getItem('claudeConfigs');
216
+ if (savedConfigs) {
217
+ try {
218
+ this.claudeConfigs = JSON.parse(savedConfigs);
219
+ for (const [name, config] of Object.entries(this.claudeConfigs)) {
220
+ if (config.apiKey && config.apiKey.includes('****')) {
221
+ config.apiKey = '';
222
+ config.hasKey = false;
223
+ }
224
+ }
225
+ localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
226
+ } catch (e) {
227
+ console.error('加载 Claude 配置失败:', e);
228
+ }
229
+ }
230
+ void this.refreshClaudeSelectionFromSettings({ silent: true });
231
+ const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
232
+ if (savedOpenclawConfigs) {
233
+ try {
234
+ this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
235
+ const configNames = Object.keys(this.openclawConfigs);
236
+ if (configNames.length > 0) {
237
+ this.currentOpenclawConfig = configNames[0];
238
+ }
239
+ } catch (e) {
240
+ console.error('加载 OpenClaw 配置失败:', e);
241
+ }
242
+ } else {
243
+ const configNames = Object.keys(this.openclawConfigs);
244
+ if (configNames.length > 0) {
245
+ this.currentOpenclawConfig = configNames[0];
246
+ }
247
+ }
248
+ this.loadAll();
249
+ },
250
+
251
+ computed: {
252
+ isSessionQueryEnabled() {
253
+ return isSessionQueryEnabled(this.sessionFilterSource);
254
+ },
255
+ sessionQueryPlaceholder() {
256
+ if (this.isSessionQueryEnabled) {
257
+ return '关键词检索(支持 Codex/Claude,例:claude code)';
258
+ }
259
+ return '当前来源暂不支持关键词检索';
260
+ },
261
+ claudeModelHasList() {
262
+ return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
263
+ },
264
+ claudeModelOptions() {
265
+ const list = Array.isArray(this.claudeModels) ? [...this.claudeModels] : [];
266
+ const current = (this.currentClaudeModel || '').trim();
267
+ if (current && !list.includes(current)) {
268
+ list.unshift(current);
269
+ }
270
+ return list;
271
+ }
272
+ },
273
+ methods: {
274
+ async loadAll() {
275
+ this.loading = true;
276
+ this.initError = '';
277
+ try {
278
+ const statusRes = await api('status');
279
+ const listRes = await api('list');
280
+
281
+ if (statusRes.error) {
282
+ this.initError = statusRes.error;
283
+ } else {
284
+ this.currentProvider = statusRes.provider;
285
+ this.currentModel = statusRes.model;
286
+ {
287
+ const tier = typeof statusRes.serviceTier === 'string'
288
+ ? statusRes.serviceTier.trim().toLowerCase()
289
+ : '';
290
+ this.serviceTier = tier === 'fast' ? 'fast' : (tier ? 'standard' : 'fast');
291
+ }
292
+ this.providersList = listRes.providers;
293
+ await this.loadModelsForProvider(this.currentProvider);
294
+ if (statusRes.configReady === false) {
295
+ this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板。请在模板编辑器确认后创建。', 'info');
296
+ }
297
+ if (statusRes.initNotice) {
298
+ this.showMessage(statusRes.initNotice, 'info');
299
+ }
300
+ this.maybeShowStarPrompt();
301
+ }
302
+ } catch (e) {
303
+ this.initError = '连接失败: ' + e.message;
304
+ } finally {
305
+ this.loading = false;
306
+ }
307
+ },
308
+
309
+ async loadModelsForProvider(providerName) {
310
+ this.codexModelsLoading = true;
311
+ if (!providerName) {
312
+ this.models = [];
313
+ this.modelsSource = 'unlimited';
314
+ this.modelsHasCurrent = true;
315
+ this.codexModelsLoading = false;
316
+ return;
317
+ }
318
+ try {
319
+ const res = await api('models', { provider: providerName });
320
+ if (res.unlimited) {
321
+ this.models = [];
322
+ this.modelsSource = 'unlimited';
323
+ this.modelsHasCurrent = true;
324
+ return;
325
+ }
326
+ if (res.error) {
327
+ this.showMessage('模型列表获取失败: ' + res.error, 'error');
328
+ this.models = [];
329
+ this.modelsSource = 'error';
330
+ this.modelsHasCurrent = true;
331
+ return;
332
+ }
333
+ const list = Array.isArray(res.models) ? res.models : [];
334
+ this.models = list;
335
+ this.modelsSource = res.source || 'remote';
336
+ this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
337
+ } catch (e) {
338
+ this.showMessage('模型列表获取失败: ' + e.message, 'error');
339
+ this.models = [];
340
+ this.modelsSource = 'error';
341
+ this.modelsHasCurrent = true;
342
+ } finally {
343
+ this.codexModelsLoading = false;
344
+ }
345
+ },
346
+
347
+ getCurrentClaudeConfig() {
348
+ if (!this.currentClaudeConfig) return null;
349
+ return this.claudeConfigs[this.currentClaudeConfig] || null;
350
+ },
351
+
352
+ normalizeClaudeValue,
353
+
354
+ normalizeClaudeConfig(config) {
355
+ return normalizeClaudeConfig(config);
356
+ },
357
+
358
+ normalizeClaudeSettingsEnv(env) {
359
+ return normalizeClaudeSettingsEnv(env);
360
+ },
361
+
362
+ matchClaudeConfigFromSettings(env) {
363
+ return matchClaudeConfigFromSettings(this.claudeConfigs, env);
364
+ },
365
+
366
+ findDuplicateClaudeConfigName(config) {
367
+ return findDuplicateClaudeConfigName(this.claudeConfigs, config);
368
+ },
369
+
370
+ async refreshClaudeSelectionFromSettings(options = {}) {
371
+ const configNames = Object.keys(this.claudeConfigs || {});
372
+ if (configNames.length === 0) {
373
+ this.currentClaudeConfig = '';
374
+ this.currentClaudeModel = '';
375
+ this.resetClaudeModelsState();
376
+ return;
377
+ }
378
+ const silent = !!options.silent;
379
+ try {
380
+ const res = await api('get-claude-settings');
381
+ if (res && res.error) {
382
+ if (!silent) {
383
+ this.showMessage('读取 Claude 配置失败: ' + res.error, 'error');
384
+ }
385
+ return;
386
+ }
387
+ const matchName = this.matchClaudeConfigFromSettings((res && res.env) || {});
388
+ if (matchName) {
389
+ if (this.currentClaudeConfig !== matchName) {
390
+ this.currentClaudeConfig = matchName;
391
+ }
392
+ this.refreshClaudeModelContext();
393
+ return;
394
+ }
395
+ this.currentClaudeConfig = '';
396
+ this.currentClaudeModel = '';
397
+ this.resetClaudeModelsState();
398
+ if (!silent) {
399
+ const tip = res && res.exists
400
+ ? '当前 Claude settings.json 与本地配置不匹配,已取消选中'
401
+ : '未检测到 Claude settings.json,已取消选中';
402
+ this.showMessage(tip, 'info');
403
+ }
404
+ } catch (e) {
405
+ if (!silent) {
406
+ this.showMessage('读取 Claude 配置失败: ' + e.message, 'error');
407
+ }
408
+ }
409
+ },
410
+
411
+ syncClaudeModelFromConfig() {
412
+ const config = this.getCurrentClaudeConfig();
413
+ this.currentClaudeModel = config && config.model ? config.model : '';
414
+ },
415
+
416
+ refreshClaudeModelContext() {
417
+ this.syncClaudeModelFromConfig();
418
+ this.loadClaudeModels();
419
+ },
420
+
421
+ resetClaudeModelsState() {
422
+ this.claudeModels = [];
423
+ this.claudeModelsSource = 'idle';
424
+ this.claudeModelsHasCurrent = true;
425
+ this.claudeModelsLoading = false;
426
+ },
427
+
428
+ updateClaudeModelsCurrent() {
429
+ const currentModel = (this.currentClaudeModel || '').trim();
430
+ this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
431
+ },
432
+
433
+ async loadClaudeModels() {
434
+ const config = this.getCurrentClaudeConfig();
435
+ if (!config) {
436
+ this.resetClaudeModelsState();
437
+ return;
438
+ }
439
+ const baseUrl = (config.baseUrl || '').trim();
440
+ const apiKey = (config.apiKey || '').trim();
441
+
442
+ if (!baseUrl) {
443
+ this.resetClaudeModelsState();
444
+ return;
445
+ }
446
+
447
+ this.claudeModelsLoading = true;
448
+ try {
449
+ const res = await api('models-by-url', { baseUrl, apiKey });
450
+ if (res.unlimited) {
451
+ this.claudeModels = [];
452
+ this.claudeModelsSource = 'unlimited';
453
+ this.claudeModelsHasCurrent = true;
454
+ return;
455
+ }
456
+ if (res.error) {
457
+ this.showMessage('模型列表获取失败: ' + res.error, 'error');
458
+ this.claudeModels = [];
459
+ this.claudeModelsSource = 'error';
460
+ this.claudeModelsHasCurrent = true;
461
+ return;
462
+ }
463
+ const list = Array.isArray(res.models) ? res.models : [];
464
+ this.claudeModels = list;
465
+ this.claudeModelsSource = res.source || 'remote';
466
+ this.updateClaudeModelsCurrent();
467
+ } catch (e) {
468
+ this.showMessage('模型列表获取失败: ' + e.message, 'error');
469
+ this.claudeModels = [];
470
+ this.claudeModelsSource = 'error';
471
+ this.claudeModelsHasCurrent = true;
472
+ } finally {
473
+ this.claudeModelsLoading = false;
474
+ }
475
+ },
476
+
477
+ openClaudeConfigModal() {
478
+ this.showClaudeConfigModal = true;
479
+ },
480
+
481
+ maybeShowStarPrompt() {
482
+ const storageKey = 'codexmateStarPrompted';
483
+ if (localStorage.getItem(storageKey)) {
484
+ return;
485
+ }
486
+ this.showMessage('如果 Codex Mate 对你有帮助,欢迎到 GitHub 点个 Star。', 'info');
487
+ localStorage.setItem(storageKey, '1');
488
+ },
489
+
490
+ switchConfigMode(mode) {
491
+ this.mainTab = 'config';
492
+ this.configMode = mode;
493
+ if (mode === 'claude') {
494
+ this.refreshClaudeModelContext();
495
+ }
496
+ },
497
+
498
+ switchMainTab(tab) {
499
+ this.mainTab = tab;
500
+ if (tab === 'sessions' && this.sessionsList.length === 0) {
501
+ this.loadSessions();
502
+ }
503
+ if (tab === 'config' && this.configMode === 'claude') {
504
+ this.refreshClaudeModelContext();
505
+ }
506
+ },
507
+
508
+ getSessionStandaloneContext() {
509
+ try {
510
+ const url = new URL(window.location.href);
511
+ if (url.pathname !== '/session') {
512
+ return { requested: false, params: null, error: '' };
513
+ }
514
+
515
+ const source = (url.searchParams.get('source') || '').trim().toLowerCase();
516
+ const sessionId = (url.searchParams.get('sessionId') || url.searchParams.get('id') || '').trim();
517
+ const filePath = (url.searchParams.get('filePath') || url.searchParams.get('path') || '').trim();
518
+ let error = '';
519
+ if (!source) {
520
+ error = '缺少 source 参数';
521
+ } else if (source !== 'codex' && source !== 'claude') {
522
+ error = 'source 仅支持 codex 或 claude';
523
+ }
524
+ if (!sessionId && !filePath) {
525
+ error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
526
+ }
527
+
528
+ if (error) {
529
+ return { requested: true, params: null, error };
530
+ }
531
+
532
+ return {
533
+ requested: true,
534
+ params: {
535
+ source,
536
+ sessionId,
537
+ filePath
538
+ },
539
+ error: ''
540
+ };
541
+ } catch (_) {
542
+ return { requested: false, params: null, error: '' };
543
+ }
544
+ },
545
+
546
+ initSessionStandalone() {
547
+ const context = this.getSessionStandaloneContext();
548
+ if (!context.requested) return;
549
+
550
+ this.sessionStandalone = true;
551
+ this.mainTab = 'sessions';
552
+
553
+ if (context.error || !context.params) {
554
+ this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
555
+ return;
556
+ }
557
+
558
+ const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
559
+ this.activeSession = {
560
+ source: context.params.source,
561
+ sourceLabel,
562
+ sessionId: context.params.sessionId,
563
+ filePath: context.params.filePath,
564
+ title: context.params.sessionId || context.params.filePath || '会话'
565
+ };
566
+ this.activeSessionMessages = [];
567
+ this.activeSessionDetailError = '';
568
+ this.activeSessionDetailClipped = false;
569
+ this.sessionStandaloneError = '';
570
+ this.sessionStandaloneText = '';
571
+ this.sessionStandaloneTitle = this.activeSession.title || '会话';
572
+ this.sessionStandaloneSourceLabel = sourceLabel;
573
+ this.loadSessionStandalonePlain();
574
+ },
575
+
576
+ buildSessionStandaloneUrl(session) {
577
+ if (!session) return '';
578
+ const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
579
+ if (!source || (source !== 'codex' && source !== 'claude')) return '';
580
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
581
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
582
+ if (!sessionId && !filePath) return '';
583
+ const origin = window.location.origin && window.location.origin !== 'null'
584
+ ? window.location.origin
585
+ : API_BASE;
586
+ const params = new URLSearchParams();
587
+ params.set('source', source);
588
+ if (sessionId) params.set('sessionId', sessionId);
589
+ if (filePath) params.set('filePath', filePath);
590
+ return `${origin}/session?${params.toString()}`;
591
+ },
592
+
593
+ openSessionStandalone(session) {
594
+ const url = this.buildSessionStandaloneUrl(session);
595
+ if (!url) {
596
+ this.showMessage('当前会话无法生成新页链接', 'error');
597
+ return;
598
+ }
599
+ window.open(url, '_blank', 'noopener');
600
+ },
601
+
602
+ getSessionExportKey(session) {
603
+ return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
604
+ },
605
+
606
+ isResumeCommandAvailable(session) {
607
+ if (!session) return false;
608
+ const source = String(session.source || '').trim().toLowerCase();
609
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
610
+ return source === 'codex' && !!sessionId;
611
+ },
612
+
613
+ isCloneAvailable(session) {
614
+ if (!session) return false;
615
+ const source = String(session.source || '').trim().toLowerCase();
616
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
617
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
618
+ return source === 'codex' && (!!sessionId || !!filePath);
619
+ },
620
+
621
+ isDeleteAvailable(session) {
622
+ if (!session) return false;
623
+ const source = String(session.source || '').trim().toLowerCase();
624
+ if (source !== 'codex' && source !== 'claude') return false;
625
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
626
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
627
+ return !!sessionId || !!filePath;
628
+ },
629
+
630
+ buildResumeCommand(session) {
631
+ const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
632
+ const arg = this.quoteResumeArg(sessionId);
633
+ if (this.sessionResumeWithYolo) {
634
+ return `codex --yolo resume ${arg}`;
635
+ }
636
+ return `codex resume ${arg}`;
637
+ },
638
+
639
+ quoteShellArg(value) {
640
+ const text = typeof value === 'string' ? value : String(value || '');
641
+ if (!text) return "''";
642
+ if (/^[a-zA-Z0-9._-]+$/.test(text)) return text;
643
+ const escaped = text.replace(/'/g, "'\\''");
644
+ return `'${escaped}'`;
645
+ },
646
+
647
+ quoteResumeArg(value) {
648
+ return this.quoteShellArg(value);
649
+ },
650
+
651
+ fallbackCopyText(text) {
652
+ let textarea = null;
653
+ try {
654
+ textarea = document.createElement('textarea');
655
+ textarea.value = text;
656
+ textarea.setAttribute('readonly', '');
657
+ textarea.style.position = 'fixed';
658
+ textarea.style.top = '-9999px';
659
+ textarea.style.left = '-9999px';
660
+ textarea.style.opacity = '0';
661
+ document.body.appendChild(textarea);
662
+ textarea.select();
663
+ textarea.setSelectionRange(0, textarea.value.length);
664
+ return document.execCommand('copy');
665
+ } catch (e) {
666
+ return false;
667
+ } finally {
668
+ if (textarea && textarea.parentNode) {
669
+ textarea.parentNode.removeChild(textarea);
670
+ }
671
+ }
672
+ },
673
+
674
+ copyAgentsContent() {
675
+ const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
676
+ if (!text) {
677
+ this.showMessage('没有可复制的内容', 'info');
678
+ return;
679
+ }
680
+ const ok = this.fallbackCopyText(text);
681
+ if (ok) {
682
+ this.showMessage('已复制 AGENTS.md 内容', 'success');
683
+ return;
684
+ }
685
+ this.showMessage('复制失败,请手动复制内容', 'error');
686
+ },
687
+
688
+ async copyResumeCommand(session) {
689
+ if (!this.isResumeCommandAvailable(session)) {
690
+ this.showMessage('当前会话不支持生成恢复命令', 'error');
691
+ return;
692
+ }
693
+ const command = this.buildResumeCommand(session);
694
+ const ok = this.fallbackCopyText(command);
695
+ if (ok) {
696
+ this.showMessage('已复制恢复命令', 'success');
697
+ return;
698
+ }
699
+ try {
700
+ if (navigator.clipboard && window.isSecureContext) {
701
+ await navigator.clipboard.writeText(command);
702
+ this.showMessage('已复制恢复命令', 'success');
703
+ return;
704
+ }
705
+ } catch (e) {
706
+ // keep fallback failure message
707
+ }
708
+ this.showMessage('复制失败,请手动复制命令', 'error');
709
+ },
710
+
711
+ buildProviderShareCommand(payload) {
712
+ if (!payload || typeof payload !== 'object') return '';
713
+ const name = typeof payload.name === 'string' ? payload.name.trim() : '';
714
+ const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
715
+ const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
716
+ if (!name || !baseUrl) return '';
717
+
718
+ const nameArg = this.quoteShellArg(name);
719
+ const urlArg = this.quoteShellArg(baseUrl);
720
+ const keyArg = apiKey ? this.quoteShellArg(apiKey) : '';
721
+ const switchCmd = `codexmate switch ${nameArg}`;
722
+ const addCmd = apiKey
723
+ ? `codexmate add ${nameArg} ${urlArg} ${keyArg}`
724
+ : `codexmate add ${nameArg} ${urlArg}`;
725
+ return `${addCmd} && ${switchCmd}`;
726
+ },
727
+
728
+ buildClaudeShareCommand(payload) {
729
+ if (!payload || typeof payload !== 'object') return '';
730
+ const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
731
+ const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
732
+ const model = typeof payload.model === 'string' && payload.model.trim()
733
+ ? payload.model.trim()
734
+ : 'glm-4.7';
735
+ if (!baseUrl || !apiKey) return '';
736
+ const urlArg = this.quoteShellArg(baseUrl);
737
+ const keyArg = this.quoteShellArg(apiKey);
738
+ const modelArg = this.quoteShellArg(model);
739
+ return `codexmate claude ${urlArg} ${keyArg} ${modelArg}`;
740
+ },
741
+
742
+ async copyProviderShareCommand(provider) {
743
+ const name = provider && typeof provider.name === 'string' ? provider.name.trim() : '';
744
+ if (!name) {
745
+ this.showMessage('提供商名称无效', 'error');
746
+ return;
747
+ }
748
+ if (this.providerShareLoading[name]) {
749
+ return;
750
+ }
751
+ this.providerShareLoading[name] = true;
752
+ try {
753
+ const res = await api('export-provider', { name });
754
+ if (res && res.error) {
755
+ this.showMessage(res.error, 'error');
756
+ return;
757
+ }
758
+ const command = this.buildProviderShareCommand(res && res.payload ? res.payload : null);
759
+ if (!command) {
760
+ this.showMessage('分享命令生成失败', 'error');
761
+ return;
762
+ }
763
+ const ok = this.fallbackCopyText(command);
764
+ if (ok) {
765
+ this.showMessage('已复制分享命令', 'success');
766
+ return;
767
+ }
768
+ try {
769
+ if (navigator.clipboard && window.isSecureContext) {
770
+ await navigator.clipboard.writeText(command);
771
+ this.showMessage('已复制分享命令', 'success');
772
+ return;
773
+ }
774
+ } catch (e) {
775
+ // keep fallback failure message
776
+ }
777
+ this.showMessage('复制失败,请手动复制命令', 'error');
778
+ } catch (e) {
779
+ this.showMessage('生成分享命令失败: ' + e.message, 'error');
780
+ } finally {
781
+ this.providerShareLoading[name] = false;
782
+ }
783
+ },
784
+
785
+ async copyClaudeShareCommand(name) {
786
+ const config = this.claudeConfigs[name];
787
+ if (!config) {
788
+ this.showMessage('配置不存在', 'error');
789
+ return;
790
+ }
791
+ if (this.claudeShareLoading[name]) return;
792
+ this.claudeShareLoading[name] = true;
793
+ try {
794
+ const res = await api('export-claude-share', { config });
795
+ if (res && res.error) {
796
+ this.showMessage(res.error, 'error');
797
+ return;
798
+ }
799
+ const command = this.buildClaudeShareCommand(res && res.payload ? res.payload : null);
800
+ if (!command) {
801
+ this.showMessage('分享命令生成失败', 'error');
802
+ return;
803
+ }
804
+ const ok = this.fallbackCopyText(command);
805
+ if (ok) {
806
+ this.showMessage('已复制分享命令', 'success');
807
+ return;
808
+ }
809
+ try {
810
+ if (navigator.clipboard && window.isSecureContext) {
811
+ await navigator.clipboard.writeText(command);
812
+ this.showMessage('已复制分享命令', 'success');
813
+ return;
814
+ }
815
+ } catch (e) {
816
+ // fall through
817
+ }
818
+ this.showMessage('复制失败,请手动复制命令', 'error');
819
+ } catch (e) {
820
+ this.showMessage('生成分享命令失败: ' + e.message, 'error');
821
+ } finally {
822
+ this.claudeShareLoading[name] = false;
823
+ }
824
+ },
825
+
826
+ async cloneSession(session) {
827
+ if (!this.isCloneAvailable(session)) {
828
+ this.showMessage('当前会话不支持克隆', 'error');
829
+ return;
830
+ }
831
+ const key = this.getSessionExportKey(session);
832
+ if (this.sessionCloning[key]) {
833
+ return;
834
+ }
835
+ this.sessionCloning[key] = true;
836
+ try {
837
+ const res = await api('clone-session', {
838
+ source: session.source,
839
+ sessionId: session.sessionId,
840
+ filePath: session.filePath
841
+ });
842
+ if (res.error) {
843
+ this.showMessage(res.error, 'error');
844
+ return;
845
+ }
846
+
847
+ this.showMessage('会话已克隆', 'success');
848
+ await this.loadSessions();
849
+ if (res.sessionId) {
850
+ const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
851
+ if (matched) {
852
+ await this.selectSession(matched);
853
+ }
854
+ }
855
+ } catch (e) {
856
+ this.showMessage('克隆失败: ' + e.message, 'error');
857
+ } finally {
858
+ this.sessionCloning[key] = false;
859
+ }
860
+ },
861
+
862
+ async deleteSession(session) {
863
+ if (!this.isDeleteAvailable(session)) {
864
+ this.showMessage('当前会话不支持删除', 'error');
865
+ return;
866
+ }
867
+ const key = this.getSessionExportKey(session);
868
+ if (this.sessionDeleting[key]) {
869
+ return;
870
+ }
871
+ this.sessionDeleting[key] = true;
872
+ try {
873
+ const res = await api('delete-session', {
874
+ source: session.source,
875
+ sessionId: session.sessionId,
876
+ filePath: session.filePath
877
+ });
878
+ if (res.error) {
879
+ this.showMessage(res.error, 'error');
880
+ return;
881
+ }
882
+ this.showMessage('会话已删除', 'success');
883
+ await this.loadSessions();
884
+ } catch (e) {
885
+ this.showMessage('删除失败: ' + e.message, 'error');
886
+ } finally {
887
+ this.sessionDeleting[key] = false;
888
+ }
889
+ },
890
+
891
+ normalizeSessionPathValue(value) {
892
+ if (typeof value !== 'string') return '';
893
+ return value.trim();
894
+ },
895
+
896
+ mergeSessionPathOptions(baseList = [], incomingList = []) {
897
+ const merged = [];
898
+ const seen = new Set();
899
+ const append = (items) => {
900
+ if (!Array.isArray(items)) return;
901
+ for (const item of items) {
902
+ const value = this.normalizeSessionPathValue(item);
903
+ if (!value) continue;
904
+ const key = value.toLowerCase();
905
+ if (seen.has(key)) continue;
906
+ seen.add(key);
907
+ merged.push(value);
908
+ }
909
+ };
910
+
911
+ append(baseList);
912
+ append(incomingList);
913
+ return merged;
914
+ },
915
+
916
+ extractPathOptionsFromSessions(sessions) {
917
+ const paths = [];
918
+ if (!Array.isArray(sessions)) {
919
+ return paths;
920
+ }
921
+
922
+ const seen = new Set();
923
+ for (const session of sessions) {
924
+ const value = this.normalizeSessionPathValue(session && session.cwd ? session.cwd : '');
925
+ if (!value) continue;
926
+ const key = value.toLowerCase();
927
+ if (seen.has(key)) continue;
928
+ seen.add(key);
929
+ paths.push(value);
930
+ }
931
+ return paths;
932
+ },
933
+
934
+ syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
935
+ const targetSource = source === 'claude' ? 'claude' : 'codex';
936
+ const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
937
+ ? this.sessionPathOptionsMap[targetSource]
938
+ : [];
939
+ const merged = mergeWithExisting
940
+ ? this.mergeSessionPathOptions(current, nextOptions)
941
+ : this.mergeSessionPathOptions([], nextOptions);
942
+ this.sessionPathOptionsMap = {
943
+ ...this.sessionPathOptionsMap,
944
+ [targetSource]: merged
945
+ };
946
+ this.refreshSessionPathOptions(targetSource);
947
+ },
948
+
949
+ refreshSessionPathOptions(source) {
950
+ const targetSource = source === 'claude' ? 'claude' : 'codex';
951
+ const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
952
+ ? [...this.sessionPathOptionsMap[targetSource]]
953
+ : [];
954
+ const selected = this.normalizeSessionPathValue(this.sessionPathFilter);
955
+ if (selected && !base.some(item => item.toLowerCase() === selected.toLowerCase())) {
956
+ base.unshift(selected);
957
+ }
958
+ if (targetSource === this.sessionFilterSource) {
959
+ this.sessionPathOptions = base;
960
+ }
961
+ },
962
+
963
+ async loadSessionPathOptions(options = {}) {
964
+ const source = options.source === 'claude' ? 'claude' : 'codex';
965
+ const forceRefresh = !!options.forceRefresh;
966
+ const loaded = !!this.sessionPathOptionsLoadedMap[source];
967
+ if (!forceRefresh && loaded) {
968
+ return;
969
+ }
970
+
971
+ const requestSeq = ++this.sessionPathRequestSeq;
972
+ this.sessionPathOptionsLoading = true;
973
+ try {
974
+ const res = await api('list-session-paths', {
975
+ source,
976
+ limit: 500,
977
+ forceRefresh
978
+ });
979
+ if (requestSeq !== this.sessionPathRequestSeq) {
980
+ return;
981
+ }
982
+ if (res && !res.error && Array.isArray(res.paths)) {
983
+ this.syncSessionPathOptionsForSource(source, res.paths, true);
984
+ this.sessionPathOptionsLoadedMap = {
985
+ ...this.sessionPathOptionsLoadedMap,
986
+ [source]: true
987
+ };
988
+ }
989
+ } catch (_) {
990
+ // 路径补全失败不影响会话主流程
991
+ } finally {
992
+ if (requestSeq === this.sessionPathRequestSeq) {
993
+ this.sessionPathOptionsLoading = false;
994
+ }
995
+ }
996
+ },
997
+
998
+ onSessionResumeYoloChange() {
999
+ const value = this.sessionResumeWithYolo ? '1' : '0';
1000
+ localStorage.setItem('codexmateSessionResumeYolo', value);
1001
+ },
1002
+
1003
+ async onSessionSourceChange() {
1004
+ this.refreshSessionPathOptions(this.sessionFilterSource);
1005
+ await this.loadSessions();
1006
+ },
1007
+
1008
+ async onSessionPathFilterChange() {
1009
+ await this.loadSessions();
1010
+ },
1011
+
1012
+ async onSessionFilterChange() {
1013
+ await this.loadSessions();
1014
+ },
1015
+
1016
+ async clearSessionFilters() {
1017
+ this.sessionFilterSource = 'codex';
1018
+ this.sessionPathFilter = '';
1019
+ this.sessionQuery = '';
1020
+ this.sessionRoleFilter = 'all';
1021
+ this.sessionTimePreset = 'all';
1022
+ await this.onSessionSourceChange();
1023
+ },
1024
+
1025
+ getRecordKey(message) {
1026
+ if (!message || !Number.isInteger(message.recordLineIndex) || message.recordLineIndex < 0) {
1027
+ return '';
1028
+ }
1029
+ return String(message.recordLineIndex);
1030
+ },
1031
+
1032
+ getRecordRenderKey(message, idx) {
1033
+ const recordKey = this.getRecordKey(message);
1034
+ if (recordKey) {
1035
+ return `record-${recordKey}`;
1036
+ }
1037
+ return `record-fallback-${idx}-${message && message.timestamp ? message.timestamp : ''}`;
1038
+ },
1039
+
1040
+ syncActiveSessionMessageCount(messageCount) {
1041
+ if (!Number.isFinite(messageCount) || messageCount < 0) return;
1042
+ if (this.activeSession) {
1043
+ this.activeSession.messageCount = messageCount;
1044
+ }
1045
+ const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
1046
+ if (!activeKey) return;
1047
+ const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === activeKey);
1048
+ if (matched) {
1049
+ matched.messageCount = messageCount;
1050
+ }
1051
+ },
1052
+
1053
+ async loadSessions() {
1054
+ if (this.sessionsLoading) return;
1055
+ this.sessionsLoading = true;
1056
+ this.activeSessionDetailError = '';
1057
+ const params = buildSessionListParams({
1058
+ source: this.sessionFilterSource,
1059
+ pathFilter: this.sessionPathFilter,
1060
+ query: this.sessionQuery,
1061
+ roleFilter: this.sessionRoleFilter,
1062
+ timeRangePreset: this.sessionTimePreset
1063
+ });
1064
+ try {
1065
+ const res = await api('list-sessions', params);
1066
+ if (res.error) {
1067
+ this.showMessage(res.error, 'error');
1068
+ this.sessionsList = [];
1069
+ this.activeSession = null;
1070
+ this.activeSessionMessages = [];
1071
+ this.activeSessionDetailClipped = false;
1072
+ } else {
1073
+ this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
1074
+ this.syncSessionPathOptionsForSource(
1075
+ this.sessionFilterSource,
1076
+ this.extractPathOptionsFromSessions(this.sessionsList),
1077
+ true
1078
+ );
1079
+ if (this.sessionsList.length === 0) {
1080
+ this.activeSession = null;
1081
+ this.activeSessionMessages = [];
1082
+ this.activeSessionDetailClipped = false;
1083
+ } else {
1084
+ const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
1085
+ const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
1086
+ this.activeSession = matched || this.sessionsList[0];
1087
+ await this.loadActiveSessionDetail();
1088
+ }
1089
+ void this.loadSessionPathOptions({ source: this.sessionFilterSource });
1090
+ }
1091
+ } catch (e) {
1092
+ this.sessionsList = [];
1093
+ this.activeSession = null;
1094
+ this.activeSessionMessages = [];
1095
+ this.activeSessionDetailClipped = false;
1096
+ this.showMessage('加载会话失败: ' + e.message, 'error');
1097
+ } finally {
1098
+ this.sessionsLoading = false;
1099
+ }
1100
+ },
1101
+
1102
+ async selectSession(session) {
1103
+ if (!session) return;
1104
+ if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
1105
+ this.activeSession = session;
1106
+ this.activeSessionMessages = [];
1107
+ this.activeSessionDetailError = '';
1108
+ this.activeSessionDetailClipped = false;
1109
+ await this.loadActiveSessionDetail();
1110
+ },
1111
+
1112
+ async loadSessionStandalonePlain() {
1113
+ if (!this.activeSession) {
1114
+ this.sessionStandaloneText = '';
1115
+ this.sessionStandaloneTitle = '会话';
1116
+ this.sessionStandaloneSourceLabel = '';
1117
+ this.sessionStandaloneError = '';
1118
+ return;
1119
+ }
1120
+
1121
+ const requestSeq = ++this.sessionStandaloneRequestSeq;
1122
+ this.sessionStandaloneLoading = true;
1123
+ this.sessionStandaloneError = '';
1124
+ try {
1125
+ const res = await api('session-plain', {
1126
+ source: this.activeSession.source,
1127
+ sessionId: this.activeSession.sessionId,
1128
+ filePath: this.activeSession.filePath
1129
+ });
1130
+
1131
+ if (requestSeq !== this.sessionStandaloneRequestSeq) {
1132
+ return;
1133
+ }
1134
+
1135
+ if (res.error) {
1136
+ this.sessionStandaloneText = '';
1137
+ this.sessionStandaloneError = res.error;
1138
+ return;
1139
+ }
1140
+
1141
+ this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
1142
+ this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
1143
+ this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
1144
+ } catch (e) {
1145
+ if (requestSeq !== this.sessionStandaloneRequestSeq) {
1146
+ return;
1147
+ }
1148
+ this.sessionStandaloneText = '';
1149
+ this.sessionStandaloneError = '加载会话内容失败: ' + e.message;
1150
+ } finally {
1151
+ if (requestSeq === this.sessionStandaloneRequestSeq) {
1152
+ this.sessionStandaloneLoading = false;
1153
+ }
1154
+ }
1155
+ },
1156
+
1157
+ async loadActiveSessionDetail() {
1158
+ if (!this.activeSession) {
1159
+ this.activeSessionMessages = [];
1160
+ this.activeSessionDetailError = '';
1161
+ this.activeSessionDetailClipped = false;
1162
+ return;
1163
+ }
1164
+
1165
+ const requestSeq = ++this.sessionDetailRequestSeq;
1166
+ this.sessionDetailLoading = true;
1167
+ this.activeSessionDetailError = '';
1168
+ try {
1169
+ const res = await api('session-detail', {
1170
+ source: this.activeSession.source,
1171
+ sessionId: this.activeSession.sessionId,
1172
+ filePath: this.activeSession.filePath,
1173
+ messageLimit: 300
1174
+ });
1175
+
1176
+ if (requestSeq !== this.sessionDetailRequestSeq) {
1177
+ return;
1178
+ }
1179
+
1180
+ if (res.error) {
1181
+ this.activeSessionMessages = [];
1182
+ this.activeSessionDetailClipped = false;
1183
+ this.activeSessionDetailError = res.error;
1184
+ return;
1185
+ }
1186
+
1187
+ this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
1188
+ this.activeSessionDetailClipped = !!res.clipped;
1189
+ if (this.activeSession) {
1190
+ if (res.sourceLabel) {
1191
+ this.activeSession.sourceLabel = res.sourceLabel;
1192
+ }
1193
+ if (res.sessionId) {
1194
+ this.activeSession.sessionId = res.sessionId;
1195
+ if (!this.activeSession.title) {
1196
+ this.activeSession.title = res.sessionId;
1197
+ }
1198
+ }
1199
+ if (res.filePath) {
1200
+ this.activeSession.filePath = res.filePath;
1201
+ }
1202
+ }
1203
+ if (res.updatedAt) {
1204
+ this.activeSession.updatedAt = res.updatedAt;
1205
+ }
1206
+ if (res.cwd) {
1207
+ this.activeSession.cwd = res.cwd;
1208
+ }
1209
+ if (Number.isFinite(res.totalMessages)) {
1210
+ this.syncActiveSessionMessageCount(res.totalMessages);
1211
+ }
1212
+ } catch (e) {
1213
+ if (requestSeq !== this.sessionDetailRequestSeq) {
1214
+ return;
1215
+ }
1216
+ this.activeSessionMessages = [];
1217
+ this.activeSessionDetailClipped = false;
1218
+ this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
1219
+ } finally {
1220
+ if (requestSeq === this.sessionDetailRequestSeq) {
1221
+ this.sessionDetailLoading = false;
1222
+ }
1223
+ }
1224
+ },
1225
+
1226
+ downloadTextFile(fileName, content) {
1227
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1228
+ const url = URL.createObjectURL(blob);
1229
+ const link = document.createElement('a');
1230
+ link.href = url;
1231
+ link.download = fileName;
1232
+ link.click();
1233
+ URL.revokeObjectURL(url);
1234
+ },
1235
+
1236
+ async exportSession(session) {
1237
+ const key = this.getSessionExportKey(session);
1238
+ if (this.sessionExporting[key]) return;
1239
+
1240
+ this.sessionExporting[key] = true;
1241
+ try {
1242
+ const res = await api('export-session', {
1243
+ source: session.source,
1244
+ sessionId: session.sessionId,
1245
+ filePath: session.filePath
1246
+ });
1247
+ if (res.error) {
1248
+ this.showMessage(res.error, 'error');
1249
+ return;
1250
+ }
1251
+
1252
+ const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
1253
+ this.downloadTextFile(fileName, res.content || '');
1254
+ if (res.truncated) {
1255
+ const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
1256
+ this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
1257
+ } else {
1258
+ this.showMessage('会话导出完成', 'success');
1259
+ }
1260
+ } catch (e) {
1261
+ this.showMessage('导出失败: ' + e.message, 'error');
1262
+ } finally {
1263
+ this.sessionExporting[key] = false;
1264
+ }
1265
+ },
1266
+
1267
+ async switchProvider(name) {
1268
+ this.currentProvider = name;
1269
+ await this.loadModelsForProvider(name);
1270
+ await this.openConfigTemplateEditor();
1271
+ },
1272
+
1273
+ async onModelChange() {
1274
+ await this.openConfigTemplateEditor();
1275
+ },
1276
+
1277
+ async onServiceTierChange() {
1278
+ await this.openConfigTemplateEditor();
1279
+ },
1280
+
1281
+ async runHealthCheck() {
1282
+ this.healthCheckLoading = true;
1283
+ this.healthCheckResult = null;
1284
+ try {
1285
+ const res = await api('config-health-check', {
1286
+ remote: false
1287
+ });
1288
+ if (res && typeof res === 'object') {
1289
+ const issues = Array.isArray(res.issues) ? [...res.issues] : [];
1290
+ let remote = res.remote || null;
1291
+ {
1292
+ const providers = (this.providersList || [])
1293
+ .filter(provider => provider && provider.name);
1294
+ const tasks = providers.map(provider =>
1295
+ this.runSpeedTest(provider.name, { silent: true })
1296
+ .then(result => ({ name: provider.name, result }))
1297
+ .catch(err => ({
1298
+ name: provider.name,
1299
+ result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
1300
+ }))
1301
+ );
1302
+ const pairs = await Promise.all(tasks);
1303
+ const results = {};
1304
+ for (const pair of pairs) {
1305
+ results[pair.name] = pair.result || null;
1306
+ const issue = this.buildSpeedTestIssue(pair.name, pair.result);
1307
+ if (issue) issues.push(issue);
1308
+ }
1309
+ remote = {
1310
+ type: 'speed-test',
1311
+ results
1312
+ };
1313
+ }
1314
+
1315
+ const ok = issues.length === 0;
1316
+ this.healthCheckResult = {
1317
+ ...res,
1318
+ ok,
1319
+ issues,
1320
+ remote
1321
+ };
1322
+ if (ok) {
1323
+ this.showMessage('健康检查通过', 'success');
1324
+ }
1325
+ } else {
1326
+ this.healthCheckResult = null;
1327
+ this.showMessage('健康检查失败:返回数据异常', 'error');
1328
+ }
1329
+ } catch (e) {
1330
+ this.healthCheckResult = null;
1331
+ this.showMessage('健康检查失败: ' + e.message, 'error');
1332
+ } finally {
1333
+ if (this.configMode === 'claude') {
1334
+ try {
1335
+ const entries = Object.entries(this.claudeConfigs || {});
1336
+ await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)));
1337
+ } catch (e) {}
1338
+ }
1339
+ this.healthCheckLoading = false;
1340
+ }
1341
+ },
1342
+
1343
+ escapeTomlString(value) {
1344
+ return String(value || '')
1345
+ .replace(/\\/g, '\\\\')
1346
+ .replace(/"/g, '\\"');
1347
+ },
1348
+
1349
+ async openConfigTemplateEditor(options = {}) {
1350
+ try {
1351
+ const res = await api('get-config-template', {
1352
+ provider: this.currentProvider,
1353
+ model: this.currentModel,
1354
+ serviceTier: this.serviceTier
1355
+ });
1356
+ if (res.error) {
1357
+ this.showMessage(res.error, 'error');
1358
+ return;
1359
+ }
1360
+ let template = res.template || '';
1361
+ const appendHint = typeof options.appendHint === 'string' ? options.appendHint.trim() : '';
1362
+ const appendBlock = typeof options.appendBlock === 'string' ? options.appendBlock.trim() : '';
1363
+ if (appendHint) {
1364
+ template = `${template.trimEnd()}\n\n# -------------------------------\n# ${appendHint}\n# -------------------------------\n`;
1365
+ }
1366
+ if (appendBlock) {
1367
+ template = `${template.trimEnd()}\n\n${appendBlock}\n`;
1368
+ }
1369
+ this.configTemplateContent = template;
1370
+ this.showConfigTemplateModal = true;
1371
+ } catch (e) {
1372
+ this.showMessage('加载模板失败: ' + e.message, 'error');
1373
+ }
1374
+ },
1375
+
1376
+ closeConfigTemplateModal() {
1377
+ this.showConfigTemplateModal = false;
1378
+ this.configTemplateContent = '';
1379
+ },
1380
+
1381
+ async applyConfigTemplate() {
1382
+ if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
1383
+ this.showMessage('模板内容不能为空', 'error');
1384
+ return;
1385
+ }
1386
+
1387
+ this.configTemplateApplying = true;
1388
+ try {
1389
+ const res = await api('apply-config-template', {
1390
+ template: this.configTemplateContent
1391
+ });
1392
+ if (res.error) {
1393
+ this.showMessage(res.error, 'error');
1394
+ return;
1395
+ }
1396
+ this.showMessage('模板已应用到 config.toml', 'success');
1397
+ this.closeConfigTemplateModal();
1398
+ await this.loadAll();
1399
+ } catch (e) {
1400
+ this.showMessage('应用模板失败: ' + e.message, 'error');
1401
+ } finally {
1402
+ this.configTemplateApplying = false;
1403
+ }
1404
+ },
1405
+
1406
+ async openAgentsEditor() {
1407
+ this.setAgentsModalContext('codex');
1408
+ this.agentsLoading = true;
1409
+ try {
1410
+ const res = await api('get-agents-file');
1411
+ if (res.error) {
1412
+ this.showMessage(res.error, 'error');
1413
+ return;
1414
+ }
1415
+ this.agentsContent = res.content || '';
1416
+ this.agentsPath = res.path || '';
1417
+ this.agentsExists = !!res.exists;
1418
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1419
+ this.showAgentsModal = true;
1420
+ } catch (e) {
1421
+ this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
1422
+ } finally {
1423
+ this.agentsLoading = false;
1424
+ }
1425
+ },
1426
+
1427
+ async openOpenclawAgentsEditor() {
1428
+ this.setAgentsModalContext('openclaw');
1429
+ this.agentsLoading = true;
1430
+ try {
1431
+ const res = await api('get-openclaw-agents-file');
1432
+ if (res.error) {
1433
+ this.showMessage(res.error, 'error');
1434
+ return;
1435
+ }
1436
+ if (res.configError) {
1437
+ this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
1438
+ }
1439
+ this.agentsContent = res.content || '';
1440
+ this.agentsPath = res.path || '';
1441
+ this.agentsExists = !!res.exists;
1442
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1443
+ this.showAgentsModal = true;
1444
+ } catch (e) {
1445
+ this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
1446
+ } finally {
1447
+ this.agentsLoading = false;
1448
+ }
1449
+ },
1450
+
1451
+ async openOpenclawWorkspaceEditor() {
1452
+ const fileName = (this.openclawWorkspaceFileName || '').trim();
1453
+ if (!fileName) {
1454
+ this.showMessage('请输入工作区文件名', 'error');
1455
+ return;
1456
+ }
1457
+ this.setAgentsModalContext('openclaw-workspace', { fileName });
1458
+ this.agentsLoading = true;
1459
+ try {
1460
+ const res = await api('get-openclaw-workspace-file', { fileName });
1461
+ if (res.error) {
1462
+ this.showMessage(res.error, 'error');
1463
+ return;
1464
+ }
1465
+ if (res.configError) {
1466
+ this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
1467
+ }
1468
+ this.agentsContent = res.content || '';
1469
+ this.agentsPath = res.path || '';
1470
+ this.agentsExists = !!res.exists;
1471
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1472
+ this.showAgentsModal = true;
1473
+ } catch (e) {
1474
+ this.showMessage('加载 OpenClaw 工作区文件失败: ' + e.message, 'error');
1475
+ } finally {
1476
+ this.agentsLoading = false;
1477
+ }
1478
+ },
1479
+
1480
+ setAgentsModalContext(context, options = {}) {
1481
+ if (context === 'openclaw-workspace') {
1482
+ const fileName = (options.fileName || this.openclawWorkspaceFileName || 'AGENTS.md').trim();
1483
+ this.agentsContext = 'openclaw-workspace';
1484
+ this.agentsWorkspaceFileName = fileName;
1485
+ this.agentsModalTitle = `OpenClaw 工作区文件: ${fileName}`;
1486
+ this.agentsModalHint = `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`;
1487
+ return;
1488
+ }
1489
+ this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
1490
+ if (this.agentsContext === 'openclaw') {
1491
+ this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
1492
+ this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
1493
+ } else {
1494
+ this.agentsModalTitle = 'AGENTS.md 编辑器';
1495
+ this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
1496
+ }
1497
+ this.agentsWorkspaceFileName = '';
1498
+ },
1499
+
1500
+ closeAgentsModal() {
1501
+ this.showAgentsModal = false;
1502
+ this.agentsContent = '';
1503
+ this.agentsPath = '';
1504
+ this.agentsExists = false;
1505
+ this.agentsLineEnding = '\n';
1506
+ this.agentsSaving = false;
1507
+ this.agentsWorkspaceFileName = '';
1508
+ this.setAgentsModalContext('codex');
1509
+ },
1510
+
1511
+ async applyAgentsContent() {
1512
+ this.agentsSaving = true;
1513
+ try {
1514
+ let action = 'apply-agents-file';
1515
+ const params = {
1516
+ content: this.agentsContent,
1517
+ lineEnding: this.agentsLineEnding
1518
+ };
1519
+ if (this.agentsContext === 'openclaw') {
1520
+ action = 'apply-openclaw-agents-file';
1521
+ } else if (this.agentsContext === 'openclaw-workspace') {
1522
+ action = 'apply-openclaw-workspace-file';
1523
+ params.fileName = this.agentsWorkspaceFileName;
1524
+ }
1525
+ const res = await api(action, params);
1526
+ if (res.error) {
1527
+ this.showMessage(res.error, 'error');
1528
+ return;
1529
+ }
1530
+ const successLabel = this.agentsContext === 'openclaw-workspace'
1531
+ ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
1532
+ : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
1533
+ this.showMessage(successLabel, 'success');
1534
+ this.closeAgentsModal();
1535
+ } catch (e) {
1536
+ this.showMessage('保存文件失败: ' + e.message, 'error');
1537
+ } finally {
1538
+ this.agentsSaving = false;
1539
+ }
1540
+ },
1541
+
1542
+ async addProvider() {
1543
+ if (!this.newProvider.name || !this.newProvider.url) {
1544
+ return this.showMessage('名称和URL必填', 'error');
1545
+ }
1546
+ const name = this.newProvider.name.trim();
1547
+ if (!name) {
1548
+ return this.showMessage('名称不能为空', 'error');
1549
+ }
1550
+ if (this.providersList.some(item => item.name === name)) {
1551
+ return this.showMessage('提供商已存在', 'error');
1552
+ }
1553
+
1554
+ const safeName = this.escapeTomlString(name);
1555
+ const safeUrl = this.escapeTomlString(this.newProvider.url.trim());
1556
+ const safeKey = this.escapeTomlString(this.newProvider.key || '');
1557
+ const newProviderBlock = `[model_providers.${safeName}]\nname = "${safeName}"\nbase_url = "${safeUrl}"\nwire_api = "responses"\nrequires_openai_auth = false\npreferred_auth_method = "${safeKey}"\nrequest_max_retries = 4\nstream_max_retries = 10\nstream_idle_timeout_ms = 300000`;
1558
+
1559
+ this.currentProvider = name;
1560
+ this.showMessage('已生成新增模板,请确认后应用', 'info');
1561
+ this.closeAddModal();
1562
+ await this.openConfigTemplateEditor({
1563
+ appendHint: `新增 provider: ${name}(请检查字段后应用)`,
1564
+ appendBlock: newProviderBlock
1565
+ });
1566
+ },
1567
+
1568
+ async deleteProvider(name) {
1569
+ if (!confirm(`确定删除提供商 "${name}"?`)) return;
1570
+ this.showMessage('请在模板中手动删除该 provider 配置块后应用', 'info');
1571
+ await this.openConfigTemplateEditor({
1572
+ appendHint: `请手动删除 [model_providers.${name}] 配置块,并确认 model_provider 指向有效 provider`
1573
+ });
1574
+ },
1575
+
1576
+ openEditModal(provider) {
1577
+ this.editingProvider = {
1578
+ name: provider.name,
1579
+ url: provider.url || '',
1580
+ key: ''
1581
+ };
1582
+ this.showEditModal = true;
1583
+ },
1584
+
1585
+ async updateProvider() {
1586
+ if (!this.editingProvider.url) {
1587
+ return this.showMessage('URL 必填', 'error');
1588
+ }
1589
+
1590
+ const name = this.editingProvider.name;
1591
+ const safeUrl = this.escapeTomlString(this.editingProvider.url.trim());
1592
+ const safeKey = this.escapeTomlString(this.editingProvider.key || '');
1593
+ this.closeEditModal();
1594
+ this.showMessage('已生成更新模板,请确认后应用', 'info');
1595
+ await this.openConfigTemplateEditor({
1596
+ appendHint: `请将 [model_providers.${name}] 中 base_url 更新为 ${safeUrl}${safeKey ? ',并更新 preferred_auth_method' : ''}`
1597
+ });
1598
+ },
1599
+
1600
+ closeEditModal() {
1601
+ this.showEditModal = false;
1602
+ this.editingProvider = { name: '', url: '', key: '' };
1603
+ },
1604
+
1605
+ async addModel() {
1606
+ if (!this.newModelName || !this.newModelName.trim()) {
1607
+ return this.showMessage('请输入模型名称', 'error');
1608
+ }
1609
+ const res = await api('add-model', { model: this.newModelName.trim() });
1610
+ if (res.error) {
1611
+ this.showMessage(res.error, 'error');
1612
+ } else {
1613
+ this.showMessage('已添加', 'success');
1614
+ this.closeModelModal();
1615
+ await this.loadAll();
1616
+ }
1617
+ },
1618
+
1619
+ async removeModel(model) {
1620
+ if (!confirm(`确定删除模型 "${model}"?`)) return;
1621
+ const res = await api('delete-model', { model });
1622
+ if (res.error) {
1623
+ this.showMessage(res.error, 'error');
1624
+ } else {
1625
+ this.showMessage('已删除', 'success');
1626
+ await this.loadAll();
1627
+ }
1628
+ },
1629
+
1630
+ closeAddModal() {
1631
+ this.showAddModal = false;
1632
+ this.newProvider = { name: '', url: '', key: '' };
1633
+ },
1634
+
1635
+ closeModelModal() {
1636
+ this.showModelModal = false;
1637
+ this.newModelName = '';
1638
+ },
1639
+
1640
+ formatKey(key) {
1641
+ if (!key) return '(未设置)';
1642
+ if (key.length > 10) {
1643
+ return key.substring(0, 3) + '****' + key.substring(key.length - 3);
1644
+ }
1645
+ return '****';
1646
+ },
1647
+
1648
+ displayApiKey(configName) {
1649
+ const key = this.claudeConfigs[configName]?.apiKey;
1650
+ return this.formatKey(key);
1651
+ },
1652
+
1653
+ switchClaudeConfig(name) {
1654
+ this.currentClaudeConfig = name;
1655
+ this.refreshClaudeModelContext();
1656
+ },
1657
+
1658
+ onClaudeModelChange() {
1659
+ const name = this.currentClaudeConfig;
1660
+ if (!name) {
1661
+ this.showMessage('请先选择配置', 'error');
1662
+ return;
1663
+ }
1664
+ const model = (this.currentClaudeModel || '').trim();
1665
+ if (!model) {
1666
+ this.showMessage('请输入模型', 'error');
1667
+ return;
1668
+ }
1669
+ const existing = this.claudeConfigs[name] || {};
1670
+ this.currentClaudeModel = model;
1671
+ this.claudeConfigs[name] = {
1672
+ apiKey: existing.apiKey || '',
1673
+ baseUrl: existing.baseUrl || '',
1674
+ model: model,
1675
+ hasKey: !!existing.apiKey
1676
+ };
1677
+ this.saveClaudeConfigs();
1678
+ this.updateClaudeModelsCurrent();
1679
+ if (!this.claudeConfigs[name].apiKey) {
1680
+ this.showMessage('该配置未设置 API Key,请先编辑', 'error');
1681
+ return;
1682
+ }
1683
+ this.applyClaudeConfig(name);
1684
+ },
1685
+
1686
+ saveClaudeConfigs() {
1687
+ localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
1688
+ },
1689
+
1690
+ openEditConfigModal(name) {
1691
+ const config = this.claudeConfigs[name];
1692
+ this.editingConfig = {
1693
+ name: name,
1694
+ apiKey: config.apiKey || '',
1695
+ baseUrl: config.baseUrl || '',
1696
+ model: config.model || ''
1697
+ };
1698
+ this.showEditConfigModal = true;
1699
+ },
1700
+
1701
+ updateConfig() {
1702
+ const name = this.editingConfig.name;
1703
+ this.claudeConfigs[name] = {
1704
+ apiKey: this.editingConfig.apiKey,
1705
+ baseUrl: this.editingConfig.baseUrl,
1706
+ model: this.editingConfig.model,
1707
+ hasKey: !!this.editingConfig.apiKey
1708
+ };
1709
+ this.saveClaudeConfigs();
1710
+ this.showMessage('配置已更新', 'success');
1711
+ this.closeEditConfigModal();
1712
+ if (name === this.currentClaudeConfig) {
1713
+ this.refreshClaudeModelContext();
1714
+ }
1715
+ },
1716
+
1717
+ closeEditConfigModal() {
1718
+ this.showEditConfigModal = false;
1719
+ this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
1720
+ },
1721
+
1722
+ async saveAndApplyConfig() {
1723
+ const name = this.editingConfig.name;
1724
+ this.claudeConfigs[name] = {
1725
+ apiKey: this.editingConfig.apiKey,
1726
+ baseUrl: this.editingConfig.baseUrl,
1727
+ model: this.editingConfig.model,
1728
+ hasKey: !!this.editingConfig.apiKey
1729
+ };
1730
+ this.saveClaudeConfigs();
1731
+
1732
+ const config = this.claudeConfigs[name];
1733
+ if (!config.apiKey) {
1734
+ this.showMessage('已保存,未应用:请先输入 API Key', 'info');
1735
+ this.closeEditConfigModal();
1736
+ if (name === this.currentClaudeConfig) {
1737
+ this.refreshClaudeModelContext();
1738
+ }
1739
+ return;
1740
+ }
1741
+
1742
+ const res = await api('apply-claude-config', { config });
1743
+ if (res.error || res.success === false) {
1744
+ this.showMessage(res.error || '应用 Claude 配置失败', 'error');
1745
+ } else {
1746
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
1747
+ this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
1748
+ this.closeEditConfigModal();
1749
+ if (name === this.currentClaudeConfig) {
1750
+ this.refreshClaudeModelContext();
1751
+ }
1752
+ }
1753
+ },
1754
+
1755
+ addClaudeConfig() {
1756
+ if (!this.newClaudeConfig.name || !this.newClaudeConfig.name.trim()) {
1757
+ return this.showMessage('请输入配置名称', 'error');
1758
+ }
1759
+ const name = this.newClaudeConfig.name.trim();
1760
+ if (this.claudeConfigs[name]) {
1761
+ return this.showMessage('配置名称已存在', 'error');
1762
+ }
1763
+ const duplicateName = this.findDuplicateClaudeConfigName(this.newClaudeConfig);
1764
+ if (duplicateName) {
1765
+ return this.showMessage('已存在相同配置,已忽略添加', 'info');
1766
+ }
1767
+
1768
+ this.claudeConfigs[name] = {
1769
+ apiKey: this.newClaudeConfig.apiKey,
1770
+ baseUrl: this.newClaudeConfig.baseUrl,
1771
+ model: this.newClaudeConfig.model,
1772
+ hasKey: !!this.newClaudeConfig.apiKey
1773
+ };
1774
+
1775
+ this.currentClaudeConfig = name;
1776
+ this.saveClaudeConfigs();
1777
+ this.showMessage('配置已添加', 'success');
1778
+ this.closeClaudeConfigModal();
1779
+ this.refreshClaudeModelContext();
1780
+ },
1781
+
1782
+ deleteClaudeConfig(name) {
1783
+ if (Object.keys(this.claudeConfigs).length <= 1) {
1784
+ return this.showMessage('至少保留一个配置', 'error');
1785
+ }
1786
+
1787
+ if (!confirm(`确定删除配置 "${name}"?`)) return;
1788
+
1789
+ delete this.claudeConfigs[name];
1790
+ if (this.currentClaudeConfig === name) {
1791
+ this.currentClaudeConfig = Object.keys(this.claudeConfigs)[0];
1792
+ }
1793
+ this.saveClaudeConfigs();
1794
+ this.showMessage('配置已删除', 'success');
1795
+ this.refreshClaudeModelContext();
1796
+ },
1797
+
1798
+ async applyClaudeConfig(name) {
1799
+ this.currentClaudeConfig = name;
1800
+ this.refreshClaudeModelContext();
1801
+ const config = this.claudeConfigs[name];
1802
+
1803
+ if (!config.apiKey) {
1804
+ return this.showMessage('该配置未设置 API Key,请先编辑', 'error');
1805
+ }
1806
+
1807
+ const res = await api('apply-claude-config', { config });
1808
+ if (res.error || res.success === false) {
1809
+ this.showMessage(res.error || '应用 Claude 配置失败', 'error');
1810
+ } else {
1811
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
1812
+ this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
1813
+ }
1814
+ },
1815
+
1816
+ closeClaudeConfigModal() {
1817
+ this.showClaudeConfigModal = false;
1818
+ this.newClaudeConfig = {
1819
+ name: '',
1820
+ apiKey: '',
1821
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
1822
+ model: 'glm-4.7'
1823
+ };
1824
+ },
1825
+
1826
+ getOpenclawParser() {
1827
+ if (window.JSON5 && typeof window.JSON5.parse === 'function' && typeof window.JSON5.stringify === 'function') {
1828
+ return {
1829
+ parse: window.JSON5.parse,
1830
+ stringify: window.JSON5.stringify
1831
+ };
1832
+ }
1833
+ return {
1834
+ parse: JSON.parse,
1835
+ stringify: JSON.stringify
1836
+ };
1837
+ },
1838
+
1839
+ parseOpenclawContent(content, options = {}) {
1840
+ const allowEmpty = !!options.allowEmpty;
1841
+ const raw = typeof content === 'string' ? content.trim() : '';
1842
+ if (!raw) {
1843
+ if (allowEmpty) {
1844
+ return { ok: true, data: {} };
1845
+ }
1846
+ return { ok: false, error: '配置内容为空' };
1847
+ }
1848
+ try {
1849
+ const parser = this.getOpenclawParser();
1850
+ const data = parser.parse(raw);
1851
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
1852
+ return { ok: false, error: '配置格式错误(根节点必须是对象)' };
1853
+ }
1854
+ return { ok: true, data };
1855
+ } catch (e) {
1856
+ return { ok: false, error: e.message || '解析失败' };
1857
+ }
1858
+ },
1859
+
1860
+ stringifyOpenclawConfig(data) {
1861
+ const parser = this.getOpenclawParser();
1862
+ try {
1863
+ return parser.stringify(data, null, 2);
1864
+ } catch (e) {
1865
+ return JSON.stringify(data, null, 2);
1866
+ }
1867
+ },
1868
+
1869
+ resetOpenclawStructured() {
1870
+ this.openclawStructured = {
1871
+ agentPrimary: '',
1872
+ agentFallbacks: [''],
1873
+ workspace: '',
1874
+ timeout: '',
1875
+ contextTokens: '',
1876
+ maxConcurrent: '',
1877
+ envItems: [{ key: '', value: '', show: false }],
1878
+ toolsProfile: 'default',
1879
+ toolsAllow: [''],
1880
+ toolsDeny: ['']
1881
+ };
1882
+ this.openclawAgentsList = [];
1883
+ this.openclawProviders = [];
1884
+ this.openclawMissingProviders = [];
1885
+ },
1886
+
1887
+ getOpenclawQuickDefaults() {
1888
+ return {
1889
+ providerName: '',
1890
+ baseUrl: '',
1891
+ apiKey: '',
1892
+ apiType: 'openai-responses',
1893
+ modelId: '',
1894
+ modelName: '',
1895
+ contextWindow: '',
1896
+ maxTokens: '',
1897
+ setPrimary: true,
1898
+ overrideProvider: true,
1899
+ overrideModels: true,
1900
+ showKey: false
1901
+ };
1902
+ },
1903
+
1904
+ resetOpenclawQuick() {
1905
+ this.openclawQuick = this.getOpenclawQuickDefaults();
1906
+ },
1907
+
1908
+ toggleOpenclawQuickKey() {
1909
+ this.openclawQuick.showKey = !this.openclawQuick.showKey;
1910
+ },
1911
+
1912
+ fillOpenclawQuickFromConfig(config) {
1913
+ const defaults = this.getOpenclawQuickDefaults();
1914
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
1915
+ this.openclawQuick = defaults;
1916
+ return;
1917
+ }
1918
+
1919
+ const agentDefaults = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
1920
+ && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
1921
+ ? config.agents.defaults
1922
+ : {};
1923
+ const modelConfig = agentDefaults.model;
1924
+ const legacyAgent = config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
1925
+ ? config.agent
1926
+ : {};
1927
+
1928
+ let primaryRef = '';
1929
+ if (modelConfig && typeof modelConfig === 'object' && !Array.isArray(modelConfig) && typeof modelConfig.primary === 'string') {
1930
+ primaryRef = modelConfig.primary;
1931
+ } else if (typeof modelConfig === 'string') {
1932
+ primaryRef = modelConfig;
1933
+ }
1934
+ if (!primaryRef) {
1935
+ if (typeof legacyAgent.model === 'string') {
1936
+ primaryRef = legacyAgent.model;
1937
+ } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
1938
+ primaryRef = legacyAgent.model.primary;
1939
+ }
1940
+ }
1941
+
1942
+ let providerName = '';
1943
+ let modelId = '';
1944
+ if (primaryRef) {
1945
+ const parts = primaryRef.split('/');
1946
+ if (parts.length >= 2) {
1947
+ providerName = parts.shift().trim();
1948
+ modelId = parts.join('/').trim();
1949
+ }
1950
+ }
1951
+
1952
+ const providers = config.models && typeof config.models === 'object' && !Array.isArray(config.models)
1953
+ && config.models.providers && typeof config.models.providers === 'object' && !Array.isArray(config.models.providers)
1954
+ ? config.models.providers
1955
+ : null;
1956
+ let providerConfig = providerName && providers ? providers[providerName] : null;
1957
+ if (!providerName && providers) {
1958
+ const providerKeys = Object.keys(providers);
1959
+ if (providerKeys.length === 1) {
1960
+ providerName = providerKeys[0];
1961
+ providerConfig = providers[providerName];
1962
+ }
1963
+ }
1964
+
1965
+ let modelEntry = null;
1966
+ if (providerConfig && typeof providerConfig === 'object' && Array.isArray(providerConfig.models)) {
1967
+ if (modelId) {
1968
+ modelEntry = providerConfig.models.find(item => item && item.id === modelId);
1969
+ }
1970
+ if (!modelEntry && providerConfig.models.length === 1) {
1971
+ modelEntry = providerConfig.models[0];
1972
+ if (!modelId && modelEntry && typeof modelEntry.id === 'string') {
1973
+ modelId = modelEntry.id;
1974
+ }
1975
+ }
1976
+ }
1977
+
1978
+ const baseUrl = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.baseUrl === 'string'
1979
+ ? providerConfig.baseUrl
1980
+ : '';
1981
+ const apiKey = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.apiKey === 'string'
1982
+ ? providerConfig.apiKey
1983
+ : '';
1984
+ const apiType = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.api === 'string'
1985
+ ? providerConfig.api
1986
+ : defaults.apiType;
1987
+
1988
+ this.openclawQuick = {
1989
+ ...defaults,
1990
+ providerName,
1991
+ baseUrl,
1992
+ apiKey,
1993
+ apiType,
1994
+ modelId: modelId || '',
1995
+ modelName: modelEntry && typeof modelEntry.name === 'string' ? modelEntry.name : '',
1996
+ contextWindow: modelEntry && typeof modelEntry.contextWindow === 'number'
1997
+ ? String(modelEntry.contextWindow)
1998
+ : '',
1999
+ maxTokens: modelEntry && typeof modelEntry.maxTokens === 'number'
2000
+ ? String(modelEntry.maxTokens)
2001
+ : ''
2002
+ };
2003
+ },
2004
+
2005
+ syncOpenclawQuickFromText(options = {}) {
2006
+ const silent = !!options.silent;
2007
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
2008
+ if (!parsed.ok) {
2009
+ this.resetOpenclawQuick();
2010
+ if (!silent) {
2011
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
2012
+ }
2013
+ return false;
2014
+ }
2015
+ this.fillOpenclawQuickFromConfig(parsed.data);
2016
+ if (!silent) {
2017
+ this.showMessage('已从编辑器读取快速配置', 'success');
2018
+ }
2019
+ return true;
2020
+ },
2021
+
2022
+ mergeOpenclawModelEntry(existing, incoming, overwrite = false) {
2023
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
2024
+ return { ...incoming };
2025
+ }
2026
+ if (overwrite) {
2027
+ return { ...incoming };
2028
+ }
2029
+ const merged = { ...existing };
2030
+ for (const [key, value] of Object.entries(incoming || {})) {
2031
+ if (merged[key] === undefined || merged[key] === null || merged[key] === '') {
2032
+ merged[key] = value;
2033
+ }
2034
+ }
2035
+ return merged;
2036
+ },
2037
+
2038
+ fillOpenclawStructured(config) {
2039
+ const defaults = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
2040
+ && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
2041
+ ? config.agents.defaults
2042
+ : {};
2043
+ const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
2044
+ ? defaults.model
2045
+ : {};
2046
+ const legacyAgent = config && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
2047
+ ? config.agent
2048
+ : {};
2049
+ const fallbackList = Array.isArray(model.fallbacks)
2050
+ ? model.fallbacks.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
2051
+ : [];
2052
+ const env = config && config.env && typeof config.env === 'object' && !Array.isArray(config.env)
2053
+ ? config.env
2054
+ : {};
2055
+ const envItems = Object.entries(env).map(([key, value]) => ({
2056
+ key,
2057
+ value: value == null ? '' : String(value),
2058
+ show: false
2059
+ }));
2060
+ const tools = config && config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
2061
+ ? config.tools
2062
+ : {};
2063
+
2064
+ let primary = typeof model.primary === 'string' ? model.primary : '';
2065
+ if (!primary) {
2066
+ if (typeof legacyAgent.model === 'string') {
2067
+ primary = legacyAgent.model;
2068
+ } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
2069
+ primary = legacyAgent.model.primary;
2070
+ }
2071
+ }
2072
+
2073
+ this.openclawStructured = {
2074
+ agentPrimary: primary,
2075
+ agentFallbacks: fallbackList.length ? fallbackList : [''],
2076
+ workspace: typeof defaults.workspace === 'string' ? defaults.workspace : '',
2077
+ timeout: typeof defaults.timeout === 'number' && Number.isFinite(defaults.timeout)
2078
+ ? String(defaults.timeout)
2079
+ : '',
2080
+ contextTokens: typeof defaults.contextTokens === 'number' && Number.isFinite(defaults.contextTokens)
2081
+ ? String(defaults.contextTokens)
2082
+ : '',
2083
+ maxConcurrent: typeof defaults.maxConcurrent === 'number' && Number.isFinite(defaults.maxConcurrent)
2084
+ ? String(defaults.maxConcurrent)
2085
+ : '',
2086
+ envItems: envItems.length ? envItems : [{ key: '', value: '', show: false }],
2087
+ toolsProfile: typeof tools.profile === 'string' && tools.profile.trim() ? tools.profile : 'default',
2088
+ toolsAllow: Array.isArray(tools.allow) && tools.allow.length
2089
+ ? tools.allow.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
2090
+ : [''],
2091
+ toolsDeny: Array.isArray(tools.deny) && tools.deny.length
2092
+ ? tools.deny.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
2093
+ : ['']
2094
+ };
2095
+ },
2096
+
2097
+ syncOpenclawStructuredFromText(options = {}) {
2098
+ const silent = !!options.silent;
2099
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
2100
+ if (!parsed.ok) {
2101
+ this.resetOpenclawStructured();
2102
+ this.resetOpenclawQuick();
2103
+ if (!silent) {
2104
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
2105
+ }
2106
+ return false;
2107
+ }
2108
+ this.fillOpenclawStructured(parsed.data);
2109
+ this.fillOpenclawQuickFromConfig(parsed.data);
2110
+ this.refreshOpenclawProviders(parsed.data);
2111
+ this.refreshOpenclawAgentsList(parsed.data);
2112
+ if (!silent) {
2113
+ this.showMessage('已从文本刷新结构化配置', 'success');
2114
+ }
2115
+ return true;
2116
+ },
2117
+
2118
+ getOpenclawActiveProviders(config) {
2119
+ const active = new Set();
2120
+ const addProvider = (ref) => {
2121
+ if (typeof ref !== 'string') return;
2122
+ const text = ref.trim();
2123
+ if (!text) return;
2124
+ const parts = text.split('/');
2125
+ if (parts.length < 2) return;
2126
+ const provider = parts[0].trim();
2127
+ if (provider) active.add(provider);
2128
+ };
2129
+ const defaults = config && config.agents && config.agents.defaults
2130
+ ? config.agents.defaults
2131
+ : {};
2132
+ const model = defaults && defaults.model;
2133
+ if (model && typeof model === 'object' && !Array.isArray(model)) {
2134
+ addProvider(model.primary);
2135
+ if (Array.isArray(model.fallbacks)) {
2136
+ for (const item of model.fallbacks) {
2137
+ addProvider(item);
2138
+ }
2139
+ }
2140
+ } else if (typeof model === 'string') {
2141
+ addProvider(model);
2142
+ }
2143
+ const modelsDefaults = config && config.models && config.models.defaults
2144
+ ? config.models.defaults
2145
+ : {};
2146
+ if (modelsDefaults && typeof modelsDefaults.provider === 'string' && modelsDefaults.provider.trim()) {
2147
+ active.add(modelsDefaults.provider.trim());
2148
+ }
2149
+ if (modelsDefaults && typeof modelsDefaults.model === 'string') {
2150
+ addProvider(modelsDefaults.model);
2151
+ }
2152
+ return active;
2153
+ },
2154
+
2155
+ maskProviderValue(value) {
2156
+ const text = value == null ? '' : String(value);
2157
+ if (!text) return '****';
2158
+ if (text.length <= 6) return '****';
2159
+ return `${text.slice(0, 3)}****${text.slice(-3)}`;
2160
+ },
2161
+
2162
+ formatProviderValue(key, value) {
2163
+ if (typeof value === 'undefined' || value === null) {
2164
+ return '';
2165
+ }
2166
+ let text = '';
2167
+ if (typeof value === 'string') {
2168
+ text = value;
2169
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
2170
+ text = String(value);
2171
+ } else {
2172
+ try {
2173
+ text = JSON.stringify(value);
2174
+ } catch (_) {
2175
+ text = String(value);
2176
+ }
2177
+ }
2178
+ if (!text) return '';
2179
+ if (/key|token|secret|password/i.test(key)) {
2180
+ return this.maskProviderValue(text);
2181
+ }
2182
+ if (text.length > 160) {
2183
+ return `${text.slice(0, 157)}...`;
2184
+ }
2185
+ return text;
2186
+ },
2187
+
2188
+ collectOpenclawProviders(source, providerMap, activeProviders, entries) {
2189
+ if (!providerMap || typeof providerMap !== 'object' || Array.isArray(providerMap)) {
2190
+ return;
2191
+ }
2192
+ const keys = Object.keys(providerMap).sort();
2193
+ for (const key of keys) {
2194
+ const value = providerMap[key];
2195
+ const fields = [];
2196
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
2197
+ const fieldKeys = Object.keys(value).sort();
2198
+ for (const fieldKey of fieldKeys) {
2199
+ const fieldValue = this.formatProviderValue(fieldKey, value[fieldKey]);
2200
+ if (fieldValue === '') continue;
2201
+ fields.push({ key: fieldKey, value: fieldValue });
2202
+ }
2203
+ } else {
2204
+ const fieldValue = this.formatProviderValue('value', value);
2205
+ if (fieldValue !== '') {
2206
+ fields.push({ key: 'value', value: fieldValue });
2207
+ }
2208
+ }
2209
+ entries.push({
2210
+ key,
2211
+ source,
2212
+ fields,
2213
+ isActive: activeProviders.has(key)
2214
+ });
2215
+ }
2216
+ },
2217
+
2218
+ refreshOpenclawProviders(config) {
2219
+ const activeProviders = this.getOpenclawActiveProviders(config || {});
2220
+ const entries = [];
2221
+ const modelsProviders = config && config.models ? config.models.providers : null;
2222
+ const rootProviders = config && config.providers ? config.providers : null;
2223
+ this.collectOpenclawProviders('models.providers', modelsProviders, activeProviders, entries);
2224
+ this.collectOpenclawProviders('providers', rootProviders, activeProviders, entries);
2225
+ const existing = new Set(entries.map(item => item.key));
2226
+ const missing = [];
2227
+ for (const provider of activeProviders) {
2228
+ if (!existing.has(provider)) {
2229
+ missing.push(provider);
2230
+ }
2231
+ }
2232
+ this.openclawProviders = entries;
2233
+ this.openclawMissingProviders = missing;
2234
+ },
2235
+
2236
+ refreshOpenclawAgentsList(config) {
2237
+ const list = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
2238
+ ? config.agents.list
2239
+ : null;
2240
+ if (!Array.isArray(list)) {
2241
+ this.openclawAgentsList = [];
2242
+ return;
2243
+ }
2244
+ const entries = [];
2245
+ list.forEach((item, index) => {
2246
+ if (!item || typeof item !== 'object') return;
2247
+ const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : `agent-${index + 1}`;
2248
+ const identity = item.identity && typeof item.identity === 'object' && !Array.isArray(item.identity)
2249
+ ? item.identity
2250
+ : {};
2251
+ const name = typeof identity.name === 'string' && identity.name.trim()
2252
+ ? identity.name.trim()
2253
+ : id;
2254
+ entries.push({
2255
+ key: `${id}-${index}`,
2256
+ id,
2257
+ name,
2258
+ theme: typeof identity.theme === 'string' ? identity.theme : '',
2259
+ emoji: typeof identity.emoji === 'string' ? identity.emoji : '',
2260
+ avatar: typeof identity.avatar === 'string' ? identity.avatar : ''
2261
+ });
2262
+ });
2263
+ this.openclawAgentsList = entries;
2264
+ },
2265
+
2266
+ normalizeStringList(list) {
2267
+ if (!Array.isArray(list)) return [];
2268
+ const result = [];
2269
+ const seen = new Set();
2270
+ for (const item of list) {
2271
+ const value = typeof item === 'string' ? item.trim() : String(item || '').trim();
2272
+ if (!value) continue;
2273
+ const key = value;
2274
+ if (seen.has(key)) continue;
2275
+ seen.add(key);
2276
+ result.push(value);
2277
+ }
2278
+ return result;
2279
+ },
2280
+
2281
+ normalizeEnvItems(items) {
2282
+ if (!Array.isArray(items)) {
2283
+ return { ok: true, items: {} };
2284
+ }
2285
+ const output = {};
2286
+ const seen = new Set();
2287
+ for (const item of items) {
2288
+ const key = item && typeof item.key === 'string' ? item.key.trim() : '';
2289
+ if (!key) continue;
2290
+ if (seen.has(key)) {
2291
+ return { ok: false, error: `环境变量重复: ${key}` };
2292
+ }
2293
+ seen.add(key);
2294
+ const value = item && typeof item.value !== 'undefined' ? String(item.value) : '';
2295
+ output[key] = value;
2296
+ }
2297
+ return { ok: true, items: output };
2298
+ },
2299
+
2300
+ parseOptionalNumber(value, label) {
2301
+ const text = typeof value === 'string' ? value.trim() : String(value || '').trim();
2302
+ if (!text) {
2303
+ return { ok: true, value: null };
2304
+ }
2305
+ const num = Number(text);
2306
+ if (!Number.isFinite(num) || num < 0) {
2307
+ return { ok: false, error: `${label} 请输入有效数字` };
2308
+ }
2309
+ return { ok: true, value: num };
2310
+ },
2311
+
2312
+ applyOpenclawStructuredToText() {
2313
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
2314
+ if (!parsed.ok) {
2315
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
2316
+ return;
2317
+ }
2318
+
2319
+ const config = parsed.data;
2320
+ const agents = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
2321
+ ? config.agents
2322
+ : {};
2323
+ const defaults = agents.defaults && typeof agents.defaults === 'object' && !Array.isArray(agents.defaults)
2324
+ ? agents.defaults
2325
+ : {};
2326
+ const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
2327
+ ? defaults.model
2328
+ : {};
2329
+
2330
+ const primary = (this.openclawStructured.agentPrimary || '').trim();
2331
+ const fallbacks = this.normalizeStringList(this.openclawStructured.agentFallbacks);
2332
+ if (primary) {
2333
+ model.primary = primary;
2334
+ }
2335
+ if (fallbacks.length) {
2336
+ model.fallbacks = fallbacks;
2337
+ }
2338
+ if (primary || fallbacks.length) {
2339
+ defaults.model = model;
2340
+ }
2341
+ if (primary && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
2342
+ config.agent.model = primary;
2343
+ }
2344
+
2345
+ const workspace = (this.openclawStructured.workspace || '').trim();
2346
+ if (workspace) {
2347
+ defaults.workspace = workspace;
2348
+ }
2349
+
2350
+ const timeout = this.parseOptionalNumber(this.openclawStructured.timeout, 'Timeout');
2351
+ if (!timeout.ok) {
2352
+ this.showMessage(timeout.error, 'error');
2353
+ return;
2354
+ }
2355
+ if (timeout.value !== null) {
2356
+ defaults.timeout = timeout.value;
2357
+ }
2358
+
2359
+ const contextTokens = this.parseOptionalNumber(this.openclawStructured.contextTokens, 'Context Tokens');
2360
+ if (!contextTokens.ok) {
2361
+ this.showMessage(contextTokens.error, 'error');
2362
+ return;
2363
+ }
2364
+ if (contextTokens.value !== null) {
2365
+ defaults.contextTokens = contextTokens.value;
2366
+ }
2367
+
2368
+ const maxConcurrent = this.parseOptionalNumber(this.openclawStructured.maxConcurrent, 'Max Concurrent');
2369
+ if (!maxConcurrent.ok) {
2370
+ this.showMessage(maxConcurrent.error, 'error');
2371
+ return;
2372
+ }
2373
+ if (maxConcurrent.value !== null) {
2374
+ defaults.maxConcurrent = maxConcurrent.value;
2375
+ }
2376
+
2377
+ if (Object.keys(defaults).length > 0) {
2378
+ config.agents = agents;
2379
+ config.agents.defaults = defaults;
2380
+ }
2381
+
2382
+ const envResult = this.normalizeEnvItems(this.openclawStructured.envItems);
2383
+ if (!envResult.ok) {
2384
+ this.showMessage(envResult.error, 'error');
2385
+ return;
2386
+ }
2387
+ if (Object.keys(envResult.items).length > 0) {
2388
+ config.env = envResult.items;
2389
+ } else if (config.env) {
2390
+ delete config.env;
2391
+ }
2392
+
2393
+ const profile = (this.openclawStructured.toolsProfile || '').trim();
2394
+ const allowList = this.normalizeStringList(this.openclawStructured.toolsAllow);
2395
+ const denyList = this.normalizeStringList(this.openclawStructured.toolsDeny);
2396
+ const hasTools = profile || allowList.length || denyList.length || (config.tools && typeof config.tools === 'object');
2397
+ if (hasTools) {
2398
+ const tools = config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
2399
+ ? config.tools
2400
+ : {};
2401
+ tools.profile = profile || tools.profile || 'default';
2402
+ tools.allow = allowList;
2403
+ tools.deny = denyList;
2404
+ config.tools = tools;
2405
+ }
2406
+
2407
+ this.openclawEditing.content = this.stringifyOpenclawConfig(config);
2408
+ this.refreshOpenclawProviders(config);
2409
+ this.refreshOpenclawAgentsList(config);
2410
+ this.fillOpenclawQuickFromConfig(config);
2411
+ this.showMessage('已写入编辑器', 'success');
2412
+ },
2413
+
2414
+ applyOpenclawQuickToText() {
2415
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
2416
+ if (!parsed.ok) {
2417
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
2418
+ return;
2419
+ }
2420
+
2421
+ const providerName = (this.openclawQuick.providerName || '').trim();
2422
+ const modelId = (this.openclawQuick.modelId || '').trim();
2423
+ if (!providerName) {
2424
+ this.showMessage('请填写 Provider 名称', 'error');
2425
+ return;
2426
+ }
2427
+ if (providerName.includes('/')) {
2428
+ this.showMessage('Provider 名称不能包含 "/"', 'error');
2429
+ return;
2430
+ }
2431
+ if (!modelId) {
2432
+ this.showMessage('请填写模型 ID', 'error');
2433
+ return;
2434
+ }
2435
+
2436
+ const config = parsed.data;
2437
+ const ensureObject = (value) => (value && typeof value === 'object' && !Array.isArray(value)) ? value : {};
2438
+ const models = ensureObject(config.models);
2439
+ const providers = ensureObject(models.providers);
2440
+ const provider = ensureObject(providers[providerName]);
2441
+ const baseUrl = (this.openclawQuick.baseUrl || '').trim();
2442
+ if (!baseUrl && !provider.baseUrl) {
2443
+ this.showMessage('请填写 Base URL', 'error');
2444
+ return;
2445
+ }
2446
+
2447
+ const contextWindow = this.parseOptionalNumber(this.openclawQuick.contextWindow, '上下文长度');
2448
+ if (!contextWindow.ok) {
2449
+ this.showMessage(contextWindow.error, 'error');
2450
+ return;
2451
+ }
2452
+ const maxTokens = this.parseOptionalNumber(this.openclawQuick.maxTokens, '最大输出');
2453
+ if (!maxTokens.ok) {
2454
+ this.showMessage(maxTokens.error, 'error');
2455
+ return;
2456
+ }
2457
+
2458
+ const shouldOverrideProvider = !!this.openclawQuick.overrideProvider;
2459
+ const apiKey = (this.openclawQuick.apiKey || '').trim();
2460
+ const apiType = (this.openclawQuick.apiType || '').trim();
2461
+ const setProviderField = (key, value) => {
2462
+ if (!value) return;
2463
+ if (shouldOverrideProvider || provider[key] === undefined || provider[key] === null || provider[key] === '') {
2464
+ provider[key] = value;
2465
+ }
2466
+ };
2467
+ setProviderField('baseUrl', baseUrl);
2468
+ setProviderField('api', apiType);
2469
+ if (apiKey) {
2470
+ setProviderField('apiKey', apiKey);
2471
+ }
2472
+
2473
+ const modelName = (this.openclawQuick.modelName || '').trim() || modelId;
2474
+ const modelEntry = {
2475
+ id: modelId,
2476
+ name: modelName,
2477
+ reasoning: false,
2478
+ input: ['text'],
2479
+ cost: {
2480
+ input: 0,
2481
+ output: 0,
2482
+ cacheRead: 0,
2483
+ cacheWrite: 0
2484
+ }
2485
+ };
2486
+ if (contextWindow.value !== null) {
2487
+ modelEntry.contextWindow = contextWindow.value;
2488
+ }
2489
+ if (maxTokens.value !== null) {
2490
+ modelEntry.maxTokens = maxTokens.value;
2491
+ }
2492
+
2493
+ const existingModels = Array.isArray(provider.models) ? [...provider.models] : [];
2494
+ if (this.openclawQuick.overrideModels || existingModels.length === 0) {
2495
+ provider.models = [modelEntry];
2496
+ } else {
2497
+ const idx = existingModels.findIndex(item => item && item.id === modelId);
2498
+ if (idx >= 0) {
2499
+ existingModels[idx] = this.mergeOpenclawModelEntry(existingModels[idx], modelEntry, false);
2500
+ } else {
2501
+ existingModels.push(modelEntry);
2502
+ }
2503
+ provider.models = existingModels;
2504
+ }
2505
+
2506
+ providers[providerName] = provider;
2507
+ models.providers = providers;
2508
+ config.models = models;
2509
+
2510
+ if (this.openclawQuick.setPrimary) {
2511
+ const agents = ensureObject(config.agents);
2512
+ const defaults = ensureObject(agents.defaults);
2513
+ const modelConfig = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
2514
+ ? defaults.model
2515
+ : {};
2516
+ modelConfig.primary = `${providerName}/${modelId}`;
2517
+ defaults.model = modelConfig;
2518
+ agents.defaults = defaults;
2519
+ config.agents = agents;
2520
+ if (config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
2521
+ config.agent.model = modelConfig.primary;
2522
+ }
2523
+ }
2524
+
2525
+ this.openclawEditing.content = this.stringifyOpenclawConfig(config);
2526
+ this.fillOpenclawStructured(config);
2527
+ this.refreshOpenclawProviders(config);
2528
+ this.refreshOpenclawAgentsList(config);
2529
+ this.showMessage('快速配置已写入编辑器', 'success');
2530
+ },
2531
+
2532
+ addOpenclawFallback() {
2533
+ this.openclawStructured.agentFallbacks.push('');
2534
+ },
2535
+
2536
+ removeOpenclawFallback(index) {
2537
+ this.openclawStructured.agentFallbacks.splice(index, 1);
2538
+ if (this.openclawStructured.agentFallbacks.length === 0) {
2539
+ this.openclawStructured.agentFallbacks.push('');
2540
+ }
2541
+ },
2542
+
2543
+ addOpenclawEnvItem() {
2544
+ this.openclawStructured.envItems.push({ key: '', value: '', show: false });
2545
+ },
2546
+
2547
+ removeOpenclawEnvItem(index) {
2548
+ this.openclawStructured.envItems.splice(index, 1);
2549
+ if (this.openclawStructured.envItems.length === 0) {
2550
+ this.openclawStructured.envItems.push({ key: '', value: '', show: false });
2551
+ }
2552
+ },
2553
+
2554
+ toggleOpenclawEnvItem(index) {
2555
+ const item = this.openclawStructured.envItems[index];
2556
+ if (item) {
2557
+ item.show = !item.show;
2558
+ }
2559
+ },
2560
+
2561
+ addOpenclawToolsAllow() {
2562
+ this.openclawStructured.toolsAllow.push('');
2563
+ },
2564
+
2565
+ removeOpenclawToolsAllow(index) {
2566
+ this.openclawStructured.toolsAllow.splice(index, 1);
2567
+ if (this.openclawStructured.toolsAllow.length === 0) {
2568
+ this.openclawStructured.toolsAllow.push('');
2569
+ }
2570
+ },
2571
+
2572
+ addOpenclawToolsDeny() {
2573
+ this.openclawStructured.toolsDeny.push('');
2574
+ },
2575
+
2576
+ removeOpenclawToolsDeny(index) {
2577
+ this.openclawStructured.toolsDeny.splice(index, 1);
2578
+ if (this.openclawStructured.toolsDeny.length === 0) {
2579
+ this.openclawStructured.toolsDeny.push('');
2580
+ }
2581
+ },
2582
+
2583
+ openclawHasContent(config) {
2584
+ return !!(config && typeof config.content === 'string' && config.content.trim());
2585
+ },
2586
+
2587
+ openclawSubtitle(config) {
2588
+ if (!this.openclawHasContent(config)) {
2589
+ return '未设置配置';
2590
+ }
2591
+ const length = config.content.trim().length;
2592
+ return `已保存 ${length} 字符`;
2593
+ },
2594
+
2595
+ saveOpenclawConfigs() {
2596
+ localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
2597
+ },
2598
+
2599
+ openOpenclawAddModal() {
2600
+ this.openclawEditorTitle = '添加 OpenClaw 配置';
2601
+ this.openclawEditing = {
2602
+ name: '',
2603
+ content: '',
2604
+ lockName: false
2605
+ };
2606
+ this.openclawConfigPath = '';
2607
+ this.openclawConfigExists = false;
2608
+ this.openclawLineEnding = '\n';
2609
+ void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
2610
+ this.showOpenclawConfigModal = true;
2611
+ },
2612
+
2613
+ openOpenclawEditModal(name) {
2614
+ this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
2615
+ this.openclawEditing = {
2616
+ name,
2617
+ content: '',
2618
+ lockName: true
2619
+ };
2620
+ void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
2621
+ this.showOpenclawConfigModal = true;
2622
+ },
2623
+
2624
+ closeOpenclawConfigModal() {
2625
+ this.showOpenclawConfigModal = false;
2626
+ this.openclawEditing = { name: '', content: '', lockName: false };
2627
+ this.openclawSaving = false;
2628
+ this.openclawApplying = false;
2629
+ this.resetOpenclawStructured();
2630
+ this.resetOpenclawQuick();
2631
+ },
2632
+
2633
+ async loadOpenclawConfigFromFile(options = {}) {
2634
+ const silent = !!options.silent;
2635
+ const force = !!options.force;
2636
+ const fallbackToTemplate = options.fallbackToTemplate !== false;
2637
+ this.openclawFileLoading = true;
2638
+ try {
2639
+ const res = await api('get-openclaw-config');
2640
+ if (res.error) {
2641
+ if (!silent) {
2642
+ this.showMessage(res.error, 'error');
2643
+ }
2644
+ return;
2645
+ }
2646
+ this.openclawConfigPath = res.path || '';
2647
+ this.openclawConfigExists = !!res.exists;
2648
+ this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
2649
+ const hasContent = !!(res.content && res.content.trim());
2650
+ const shouldOverride = force || !this.openclawEditing.content || !this.openclawEditing.content.trim();
2651
+ if (hasContent && shouldOverride) {
2652
+ this.openclawEditing.content = res.content;
2653
+ } else if (!hasContent && shouldOverride && fallbackToTemplate) {
2654
+ this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
2655
+ }
2656
+ this.syncOpenclawStructuredFromText({ silent: true });
2657
+ if (!silent) {
2658
+ this.showMessage('已加载当前 OpenClaw 配置', 'success');
2659
+ }
2660
+ } catch (e) {
2661
+ if (!silent) {
2662
+ this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
2663
+ }
2664
+ } finally {
2665
+ this.openclawFileLoading = false;
2666
+ }
2667
+ },
2668
+
2669
+ persistOpenclawConfig({ closeModal = true } = {}) {
2670
+ if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
2671
+ this.showMessage('请输入配置名称', 'error');
2672
+ return '';
2673
+ }
2674
+ const name = this.openclawEditing.name.trim();
2675
+ if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
2676
+ this.showMessage('配置名称已存在', 'error');
2677
+ return '';
2678
+ }
2679
+ if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
2680
+ this.showMessage('配置内容不能为空', 'error');
2681
+ return '';
2682
+ }
2683
+
2684
+ this.openclawConfigs[name] = {
2685
+ content: this.openclawEditing.content
2686
+ };
2687
+ this.currentOpenclawConfig = name;
2688
+ this.saveOpenclawConfigs();
2689
+ if (closeModal) {
2690
+ this.closeOpenclawConfigModal();
2691
+ }
2692
+ return name;
2693
+ },
2694
+
2695
+ async saveOpenclawConfig() {
2696
+ this.openclawSaving = true;
2697
+ try {
2698
+ const name = this.persistOpenclawConfig();
2699
+ if (!name) return;
2700
+ this.showMessage('OpenClaw 配置已保存', 'success');
2701
+ } finally {
2702
+ this.openclawSaving = false;
2703
+ }
2704
+ },
2705
+
2706
+ async saveAndApplyOpenclawConfig() {
2707
+ this.openclawApplying = true;
2708
+ try {
2709
+ const name = this.persistOpenclawConfig({ closeModal: false });
2710
+ if (!name) return;
2711
+ const config = this.openclawConfigs[name];
2712
+ const res = await api('apply-openclaw-config', {
2713
+ content: config.content,
2714
+ lineEnding: this.openclawLineEnding
2715
+ });
2716
+ if (res.error || res.success === false) {
2717
+ this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
2718
+ return;
2719
+ }
2720
+ this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
2721
+ this.openclawConfigExists = true;
2722
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
2723
+ this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
2724
+ this.closeOpenclawConfigModal();
2725
+ } catch (e) {
2726
+ this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
2727
+ } finally {
2728
+ this.openclawApplying = false;
2729
+ }
2730
+ },
2731
+
2732
+ deleteOpenclawConfig(name) {
2733
+ if (Object.keys(this.openclawConfigs).length <= 1) {
2734
+ return this.showMessage('至少保留一个配置', 'error');
2735
+ }
2736
+ if (!confirm(`确定删除配置 "${name}"?`)) return;
2737
+ delete this.openclawConfigs[name];
2738
+ if (this.currentOpenclawConfig === name) {
2739
+ this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
2740
+ }
2741
+ this.saveOpenclawConfigs();
2742
+ this.showMessage('OpenClaw 配置已删除', 'success');
2743
+ },
2744
+
2745
+ async applyOpenclawConfig(name) {
2746
+ this.currentOpenclawConfig = name;
2747
+ const config = this.openclawConfigs[name];
2748
+ if (!this.openclawHasContent(config)) {
2749
+ return this.showMessage('该配置为空,请先编辑', 'error');
2750
+ }
2751
+ const res = await api('apply-openclaw-config', {
2752
+ content: config.content,
2753
+ lineEnding: this.openclawLineEnding
2754
+ });
2755
+ if (res.error || res.success === false) {
2756
+ this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
2757
+ } else {
2758
+ this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
2759
+ this.openclawConfigExists = true;
2760
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
2761
+ this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
2762
+ }
2763
+ },
2764
+
2765
+ formatLatency,
2766
+
2767
+ buildSpeedTestIssue(name, result) {
2768
+ return buildSpeedTestIssue(name, result);
2769
+ },
2770
+
2771
+ async runSpeedTest(name, options = {}) {
2772
+ if (!name || this.speedLoading[name]) return null;
2773
+ const silent = !!options.silent;
2774
+ this.speedLoading[name] = true;
2775
+ try {
2776
+ const res = await api('speed-test', { name });
2777
+ if (res.error) {
2778
+ this.speedResults[name] = { ok: false, error: res.error };
2779
+ if (!silent) {
2780
+ this.showMessage(res.error, 'error');
2781
+ }
2782
+ return { ok: false, error: res.error };
2783
+ }
2784
+ this.speedResults[name] = res;
2785
+ if (!silent) {
2786
+ const status = res.status ? ` (${res.status})` : '';
2787
+ this.showMessage(`Speed ${name}: ${this.formatLatency(res)}${status}`, 'success');
2788
+ }
2789
+ return res;
2790
+ } catch (e) {
2791
+ const message = e && e.message ? e.message : 'Speed test failed';
2792
+ this.speedResults[name] = { ok: false, error: message };
2793
+ if (!silent) {
2794
+ this.showMessage(message, 'error');
2795
+ }
2796
+ return { ok: false, error: message };
2797
+ } finally {
2798
+ this.speedLoading[name] = false;
2799
+ }
2800
+ },
2801
+
2802
+ async runClaudeSpeedTest(name, config) {
2803
+ if (!name || this.claudeSpeedLoading[name]) return null;
2804
+ const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
2805
+ this.claudeSpeedLoading[name] = true;
2806
+ try {
2807
+ if (!baseUrl) {
2808
+ const res = { ok: false, error: 'Missing base URL' };
2809
+ this.claudeSpeedResults[name] = res;
2810
+ return res;
2811
+ }
2812
+ const res = await api('speed-test', { url: baseUrl });
2813
+ if (res.error) {
2814
+ this.claudeSpeedResults[name] = { ok: false, error: res.error };
2815
+ return { ok: false, error: res.error };
2816
+ }
2817
+ this.claudeSpeedResults[name] = res;
2818
+ return res;
2819
+ } catch (e) {
2820
+ const message = e && e.message ? e.message : 'Speed test failed';
2821
+ const res = { ok: false, error: message };
2822
+ this.claudeSpeedResults[name] = res;
2823
+ return res;
2824
+ } finally {
2825
+ this.claudeSpeedLoading[name] = false;
2826
+ }
2827
+ },
2828
+
2829
+ showMessage(text, type) {
2830
+ this.message = text;
2831
+ this.messageType = type || 'info';
2832
+ setTimeout(() => {
2833
+ this.message = '';
2834
+ }, 3000);
2835
+ }
2836
+ }
2837
+ });
2838
+
2839
+ app.mount('#app');
2840
+ });
2841
+