codexmate 0.0.12 → 0.0.14

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