codexmate 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +389 -284
  2. package/README.zh.md +321 -0
  3. package/cli/agents-files.js +224 -162
  4. package/cli/archive-helpers.js +446 -446
  5. package/cli/auth-profiles.js +359 -359
  6. package/cli/builtin-proxy.js +1044 -580
  7. package/cli/claude-proxy.js +998 -998
  8. package/cli/config-bootstrap.js +384 -384
  9. package/cli/config-health.js +338 -338
  10. package/cli/openai-bridge.js +950 -0
  11. package/cli/openclaw-config.js +629 -629
  12. package/cli/session-usage.concurrent.js +28 -0
  13. package/cli/session-usage.js +112 -0
  14. package/cli/session-usage.models.js +176 -0
  15. package/cli/skills.js +1141 -1141
  16. package/cli/zip-commands.js +510 -510
  17. package/cli.js +13214 -13129
  18. package/lib/cli-file-utils.js +151 -151
  19. package/lib/cli-models-utils.js +419 -419
  20. package/lib/cli-network-utils.js +164 -164
  21. package/lib/cli-path-utils.js +69 -69
  22. package/lib/cli-session-utils.js +121 -121
  23. package/lib/cli-sessions.js +386 -386
  24. package/lib/cli-utils.js +155 -155
  25. package/lib/download-artifacts.js +77 -77
  26. package/lib/mcp-stdio.js +440 -440
  27. package/lib/task-orchestrator.js +869 -869
  28. package/lib/text-diff.js +303 -303
  29. package/lib/workflow-engine.js +340 -340
  30. package/package.json +74 -74
  31. package/res/json5.min.js +1 -1
  32. package/res/logo.png +0 -0
  33. package/res/vue.global.prod.js +13 -13
  34. package/web-ui/app.js +575 -530
  35. package/web-ui/index.html +34 -33
  36. package/web-ui/logic.agents-diff.mjs +386 -386
  37. package/web-ui/logic.claude.mjs +168 -168
  38. package/web-ui/logic.mjs +5 -5
  39. package/web-ui/logic.runtime.mjs +128 -124
  40. package/web-ui/logic.sessions.mjs +614 -581
  41. package/web-ui/modules/api.mjs +90 -90
  42. package/web-ui/modules/app.computed.dashboard.mjs +126 -113
  43. package/web-ui/modules/app.computed.index.mjs +17 -15
  44. package/web-ui/modules/app.computed.main-tabs.mjs +198 -195
  45. package/web-ui/modules/app.computed.session.mjs +653 -507
  46. package/web-ui/modules/app.constants.mjs +15 -15
  47. package/web-ui/modules/app.methods.agents.mjs +544 -493
  48. package/web-ui/modules/app.methods.claude-config.mjs +174 -174
  49. package/web-ui/modules/app.methods.codex-config.mjs +795 -640
  50. package/web-ui/modules/app.methods.index.mjs +92 -88
  51. package/web-ui/modules/app.methods.install.mjs +161 -149
  52. package/web-ui/modules/app.methods.navigation.mjs +619 -619
  53. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  54. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  55. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  56. package/web-ui/modules/app.methods.providers.mjs +404 -363
  57. package/web-ui/modules/app.methods.runtime.mjs +323 -323
  58. package/web-ui/modules/app.methods.session-actions.mjs +537 -520
  59. package/web-ui/modules/app.methods.session-browser.mjs +626 -626
  60. package/web-ui/modules/app.methods.session-timeline.mjs +448 -448
  61. package/web-ui/modules/app.methods.session-trash.mjs +422 -422
  62. package/web-ui/modules/app.methods.startup-claude.mjs +405 -412
  63. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -471
  64. package/web-ui/modules/config-mode.computed.mjs +126 -126
  65. package/web-ui/modules/config-template-confirm-pref.mjs +33 -0
  66. package/web-ui/modules/i18n.mjs +1609 -0
  67. package/web-ui/modules/plugins.computed.mjs +220 -0
  68. package/web-ui/modules/plugins.methods.mjs +620 -0
  69. package/web-ui/modules/plugins.storage.mjs +37 -0
  70. package/web-ui/modules/skills.computed.mjs +107 -107
  71. package/web-ui/modules/skills.methods.mjs +481 -481
  72. package/web-ui/partials/index/layout-footer.html +13 -13
  73. package/web-ui/partials/index/layout-header.html +461 -402
  74. package/web-ui/partials/index/modal-config-template-agents.html +175 -125
  75. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  76. package/web-ui/partials/index/modal-health-check.html +72 -72
  77. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  78. package/web-ui/partials/index/modal-skills.html +200 -184
  79. package/web-ui/partials/index/modals-basic.html +165 -156
  80. package/web-ui/partials/index/panel-config-claude.html +138 -126
  81. package/web-ui/partials/index/panel-config-codex.html +234 -237
  82. package/web-ui/partials/index/panel-config-openclaw.html +78 -78
  83. package/web-ui/partials/index/panel-docs.html +147 -130
  84. package/web-ui/partials/index/panel-market.html +174 -174
  85. package/web-ui/partials/index/panel-orchestration.html +397 -397
  86. package/web-ui/partials/index/panel-plugins.html +243 -0
  87. package/web-ui/partials/index/panel-sessions.html +292 -292
  88. package/web-ui/partials/index/panel-settings.html +258 -190
  89. package/web-ui/partials/index/panel-usage.html +353 -213
  90. package/web-ui/session-helpers.mjs +573 -559
  91. package/web-ui/source-bundle.cjs +233 -233
  92. package/web-ui/styles/base-theme.css +264 -271
  93. package/web-ui/styles/controls-forms.css +362 -360
  94. package/web-ui/styles/docs-panel.css +247 -182
  95. package/web-ui/styles/feedback.css +108 -108
  96. package/web-ui/styles/health-check-dialog.css +144 -144
  97. package/web-ui/styles/layout-shell.css +596 -376
  98. package/web-ui/styles/modals-core.css +464 -464
  99. package/web-ui/styles/navigation-panels.css +382 -348
  100. package/web-ui/styles/openclaw-structured.css +266 -266
  101. package/web-ui/styles/plugins-panel.css +518 -0
  102. package/web-ui/styles/responsive.css +456 -450
  103. package/web-ui/styles/sessions-list.css +400 -400
  104. package/web-ui/styles/sessions-preview.css +411 -411
  105. package/web-ui/styles/sessions-toolbar-trash.css +268 -243
  106. package/web-ui/styles/sessions-usage.css +851 -628
  107. package/web-ui/styles/settings-panel.css +166 -0
  108. package/web-ui/styles/skills-list.css +303 -296
  109. package/web-ui/styles/skills-market.css +396 -335
  110. package/web-ui/styles/task-orchestration.css +776 -776
  111. package/web-ui/styles/titles-cards.css +408 -408
  112. package/web-ui/styles.css +20 -18
  113. package/web-ui.html +17 -17
  114. package/README.en.md +0 -349
@@ -1,640 +1,795 @@
1
- import { runLatestOnlyQueue } from '../logic.mjs';
2
-
3
- function hasResponseError(response) {
4
- if (!response || typeof response !== 'object') {
5
- return false;
6
- }
7
- if (typeof response.error === 'string') {
8
- return response.error.trim().length > 0;
9
- }
10
- return response.error !== undefined && response.error !== null && response.error !== false;
11
- }
12
-
13
- function getResponseMessage(response, fallback) {
14
- if (!response || typeof response !== 'object') {
15
- return fallback;
16
- }
17
- for (const key of ['error', 'message', 'detail']) {
18
- const value = response[key];
19
- if (typeof value === 'string' && value.trim()) {
20
- return value.trim();
21
- }
22
- }
23
- return fallback;
24
- }
25
-
26
- export function createCodexConfigMethods(options = {}) {
27
- const {
28
- api,
29
- defaultModelContextWindow = 190000,
30
- defaultModelAutoCompactTokenLimit = 185000,
31
- getProviderConfigModeMeta
32
- } = options;
33
-
34
- return {
35
- downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
36
- const BOM = '\uFEFF';
37
- const blob = new Blob([BOM + content], { type: mimeType });
38
- const url = URL.createObjectURL(blob);
39
- const link = document.createElement('a');
40
- link.href = url;
41
- link.download = fileName;
42
- link.click();
43
- URL.revokeObjectURL(url);
44
- },
45
-
46
- async exportSession(session) {
47
- const key = this.getSessionExportKey(session);
48
- if (this.sessionExporting[key]) return;
49
-
50
- this.sessionExporting[key] = true;
51
- try {
52
- const res = await api('export-session', {
53
- source: session.source,
54
- sessionId: session.sessionId,
55
- filePath: session.filePath
56
- });
57
- if (res.error) {
58
- this.showMessage(res.error, 'error');
59
- return;
60
- }
61
-
62
- const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
63
- this.downloadTextFile(fileName, res.content || '');
64
- if (res.truncated) {
65
- const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
66
- this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
67
- } else {
68
- this.showMessage('操作成功', 'success');
69
- }
70
- } catch (e) {
71
- this.showMessage('导出失败', 'error');
72
- } finally {
73
- this.sessionExporting[key] = false;
74
- }
75
- },
76
-
77
- async quickSwitchProvider(name) {
78
- const target = String(name || '').trim();
79
- const visualTarget = String(this.providerSwitchDisplayTarget || '').trim();
80
- if (!target || target === visualTarget || target === this.pendingProviderSwitch) {
81
- return;
82
- }
83
- if (!this.providerSwitchInProgress && target === this.currentProvider) {
84
- return;
85
- }
86
- await this.switchProvider(target);
87
- },
88
-
89
- async waitForCodexApplyIdle(maxWaitMs = 20000) {
90
- const startedAt = Date.now();
91
- while (this.codexApplying) {
92
- if ((Date.now() - startedAt) > maxWaitMs) {
93
- throw new Error('等待配置应用完成超时');
94
- }
95
- await new Promise((resolve) => setTimeout(resolve, 50));
96
- }
97
- },
98
-
99
- async performProviderSwitch(name) {
100
- await this.waitForCodexApplyIdle();
101
- const previousProvider = this.currentProvider;
102
- const previousModel = this.currentModel;
103
- const previousModels = Array.isArray(this.models) ? [...this.models] : [];
104
- const previousModelsSource = this.modelsSource;
105
- const previousModelsHasCurrent = this.modelsHasCurrent;
106
- this.currentProvider = name;
107
- await this.loadModelsForProvider(name);
108
- if (this.modelsSource === 'error') {
109
- this.currentProvider = previousProvider;
110
- this.currentModel = previousModel;
111
- this.models = previousModels;
112
- this.modelsSource = previousModelsSource;
113
- this.modelsHasCurrent = previousModelsHasCurrent;
114
- return;
115
- }
116
- if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
117
- this.currentModel = this.models[0];
118
- this.modelsHasCurrent = true;
119
- }
120
- if (getProviderConfigModeMeta(this.configMode)) {
121
- await this.waitForCodexApplyIdle();
122
- await this.applyCodexConfigDirect({ silent: true });
123
- }
124
- },
125
-
126
- async switchProvider(name) {
127
- const target = String(name || '').trim();
128
- if (!target) {
129
- return;
130
- }
131
- if (target === String(this.providerSwitchDisplayTarget || '').trim()) {
132
- return;
133
- }
134
- this.providerSwitchDisplayTarget = target;
135
- if (this.providerSwitchInProgress) {
136
- this.pendingProviderSwitch = target;
137
- return;
138
- }
139
- this.providerSwitchInProgress = true;
140
- let lastError = '';
141
- try {
142
- this.pendingProviderSwitch = '';
143
- const result = await runLatestOnlyQueue(target, {
144
- perform: async (queuedTarget) => {
145
- this.providerSwitchDisplayTarget = queuedTarget;
146
- await this.performProviderSwitch(queuedTarget);
147
- },
148
- consumePending: () => {
149
- const queued = this.pendingProviderSwitch;
150
- this.pendingProviderSwitch = '';
151
- return queued;
152
- }
153
- });
154
- if (result && typeof result.lastError === 'string') {
155
- lastError = result.lastError;
156
- }
157
- } finally {
158
- this.providerSwitchInProgress = false;
159
- this.pendingProviderSwitch = '';
160
- this.providerSwitchDisplayTarget = '';
161
- }
162
- if (lastError) {
163
- this.showMessage(lastError, 'error');
164
- }
165
- },
166
-
167
- async onModelChange() {
168
- await this.applyCodexConfigDirect();
169
- },
170
-
171
- async onServiceTierChange() {
172
- await this.applyCodexConfigDirect({ silent: true });
173
- },
174
-
175
- async onReasoningEffortChange() {
176
- await this.applyCodexConfigDirect({ silent: true });
177
- },
178
-
179
- sanitizePositiveIntegerDraft(field) {
180
- if (!field || typeof this[field] === 'undefined') return;
181
- const current = typeof this[field] === 'string'
182
- ? this[field]
183
- : String(this[field] || '');
184
- const sanitized = current.replace(/[^\d]/g, '');
185
- if (sanitized !== current) {
186
- this[field] = sanitized;
187
- }
188
- },
189
-
190
- normalizePositiveIntegerInput(value, label, fallback = '') {
191
- const fallbackText = fallback === '' ? '' : String(fallback).trim();
192
- const raw = typeof value === 'string'
193
- ? value.trim()
194
- : String(value ?? '').trim();
195
- const text = raw || fallbackText;
196
- if (!text) {
197
- return { ok: true, value: null, text: '' };
198
- }
199
- if (!/^\d+$/.test(text)) {
200
- return { ok: false, error: `${label} 请输入正整数` };
201
- }
202
- const num = Number.parseInt(text, 10);
203
- if (!Number.isSafeInteger(num) || num <= 0) {
204
- return { ok: false, error: `${label} 请输入正整数` };
205
- }
206
- return { ok: true, value: num, text: String(num) };
207
- },
208
-
209
- async onModelContextWindowBlur() {
210
- this.editingCodexBudgetField = '';
211
- const normalized = this.normalizePositiveIntegerInput(
212
- this.modelContextWindowInput,
213
- 'model_context_window',
214
- defaultModelContextWindow
215
- );
216
- if (!normalized.ok) {
217
- this.showMessage(normalized.error, 'error');
218
- return;
219
- }
220
- this.modelContextWindowInput = normalized.text;
221
- await this.applyCodexConfigDirect({
222
- silent: true,
223
- modelContextWindow: normalized.value
224
- });
225
- },
226
-
227
- async onModelAutoCompactTokenLimitBlur() {
228
- this.editingCodexBudgetField = '';
229
- const normalized = this.normalizePositiveIntegerInput(
230
- this.modelAutoCompactTokenLimitInput,
231
- 'model_auto_compact_token_limit',
232
- defaultModelAutoCompactTokenLimit
233
- );
234
- if (!normalized.ok) {
235
- this.showMessage(normalized.error, 'error');
236
- return;
237
- }
238
- this.modelAutoCompactTokenLimitInput = normalized.text;
239
- await this.applyCodexConfigDirect({
240
- silent: true,
241
- modelAutoCompactTokenLimit: normalized.value
242
- });
243
- },
244
-
245
- async resetCodexContextBudgetDefaults() {
246
- this.modelContextWindowInput = String(defaultModelContextWindow);
247
- this.modelAutoCompactTokenLimitInput = String(defaultModelAutoCompactTokenLimit);
248
- await this.applyCodexConfigDirect({
249
- modelContextWindow: defaultModelContextWindow,
250
- modelAutoCompactTokenLimit: defaultModelAutoCompactTokenLimit
251
- });
252
- },
253
-
254
- async runHealthCheck() {
255
- this.healthCheckLoading = true;
256
- this.healthCheckResult = null;
257
- let shouldRunClaudeSpeedTests = false;
258
- try {
259
- const res = await api('config-health-check', {
260
- remote: this.configMode === 'codex'
261
- });
262
- if (hasResponseError(res)) {
263
- this.healthCheckResult = null;
264
- this.showMessage(getResponseMessage(res, '检查失败'), 'error');
265
- } else if (res && typeof res === 'object') {
266
- shouldRunClaudeSpeedTests = true;
267
- const issues = Array.isArray(res.issues) ? [...res.issues] : [];
268
- let remote = res.remote || null;
269
- {
270
- const providers = (this.providersList || [])
271
- .map((provider) => typeof provider === 'string'
272
- ? provider.trim()
273
- : String((provider && provider.name) || '').trim())
274
- .filter(Boolean);
275
- const tasks = providers.map(provider =>
276
- this.runSpeedTest(provider, { silent: true })
277
- .then(result => ({ name: provider, result }))
278
- .catch(err => ({
279
- name: provider,
280
- result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
281
- }))
282
- );
283
- const pairs = await Promise.all(tasks);
284
- const results = {};
285
- for (const pair of pairs) {
286
- results[pair.name] = pair.result || null;
287
- const issue = this.buildSpeedTestIssue(pair.name, pair.result);
288
- if (issue) issues.push(issue);
289
- }
290
- if (remote && typeof remote === 'object') {
291
- remote = {
292
- ...remote,
293
- speedTests: results
294
- };
295
- } else {
296
- remote = {
297
- type: 'speed-test',
298
- speedTests: results
299
- };
300
- }
301
- }
302
-
303
- const ok = issues.length === 0;
304
- this.healthCheckResult = {
305
- ...res,
306
- ok,
307
- issues,
308
- remote
309
- };
310
- if (ok) {
311
- this.showMessage('检查通过', 'success');
312
- }
313
- } else {
314
- this.healthCheckResult = null;
315
- this.showMessage('检查失败', 'error');
316
- }
317
- } catch (e) {
318
- this.healthCheckResult = null;
319
- this.showMessage('检查失败', 'error');
320
- } finally {
321
- if (shouldRunClaudeSpeedTests && this.configMode === 'claude') {
322
- try {
323
- const entries = Object.entries(this.claudeConfigs || {});
324
- await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)));
325
- } catch (e) {}
326
- }
327
- this.healthCheckLoading = false;
328
- }
329
- },
330
-
331
- buildDefaultHealthCheckPrompt() {
332
- return '请简短回复:当前提供商连接正常。';
333
- },
334
-
335
- openHealthCheckDialog(options = {}) {
336
- const providerName = typeof options.providerName === 'string'
337
- ? options.providerName.trim()
338
- : '';
339
- const locked = !!options.locked && !!providerName;
340
- const nextProvider = providerName
341
- || String(this.healthCheckDialogSelectedProvider || '').trim()
342
- || String(this.currentProvider || '').trim()
343
- || String(((this.displayProvidersList || [])[0] || {}).name || '').trim();
344
-
345
- this.showHealthCheckDialog = true;
346
- this.healthCheckDialogLockedProvider = locked ? nextProvider : '';
347
- this.healthCheckDialogSelectedProvider = nextProvider;
348
- this.healthCheckDialogPrompt = this.buildDefaultHealthCheckPrompt();
349
- this.healthCheckDialogMessages = [];
350
- this.healthCheckDialogLastResult = null;
351
- },
352
-
353
- closeHealthCheckDialog(options = {}) {
354
- if (this.healthCheckDialogSending && !options.force) {
355
- return;
356
- }
357
- this.showHealthCheckDialog = false;
358
- this.healthCheckDialogLockedProvider = '';
359
- this.healthCheckDialogSelectedProvider = '';
360
- this.healthCheckDialogPrompt = this.buildDefaultHealthCheckPrompt();
361
- this.healthCheckDialogMessages = [];
362
- this.healthCheckDialogLastResult = null;
363
- },
364
-
365
- async sendHealthCheckDialogMessage() {
366
- if (this.healthCheckDialogSending) {
367
- return;
368
- }
369
-
370
- const provider = String(
371
- this.healthCheckDialogLockedProvider || this.healthCheckDialogSelectedProvider || ''
372
- ).trim();
373
- const prompt = String(this.healthCheckDialogPrompt || '').trim();
374
- if (!provider) {
375
- this.showMessage('请先选择提供商', 'error');
376
- return;
377
- }
378
- if (!prompt) {
379
- this.showMessage('请输入对话内容', 'error');
380
- return;
381
- }
382
-
383
- this.healthCheckDialogMessages.push({
384
- id: `user-${Date.now()}`,
385
- role: 'user',
386
- text: prompt
387
- });
388
- this.healthCheckDialogSending = true;
389
- this.healthCheckDialogLastResult = null;
390
-
391
- try {
392
- const res = await api('provider-chat-check', {
393
- name: provider,
394
- prompt
395
- });
396
- this.healthCheckDialogLastResult = res;
397
-
398
- if (hasResponseError(res) || res.ok === false) {
399
- const message = getResponseMessage(res, '健康检测失败');
400
- this.healthCheckDialogMessages.push({
401
- id: `assistant-${Date.now()}`,
402
- role: 'assistant',
403
- text: message,
404
- ok: false,
405
- status: Number.isFinite(res && res.status) ? res.status : 0,
406
- durationMs: Number.isFinite(res && res.durationMs) ? res.durationMs : 0,
407
- model: typeof (res && res.model) === 'string' ? res.model : '',
408
- rawPreview: typeof (res && res.rawPreview) === 'string' ? res.rawPreview : ''
409
- });
410
- this.showMessage(message, 'error');
411
- return;
412
- }
413
-
414
- const reply = typeof res.reply === 'string' && res.reply.trim()
415
- ? res.reply.trim()
416
- : '已收到响应,但未解析到可展示文本。';
417
- this.healthCheckDialogMessages.push({
418
- id: `assistant-${Date.now()}`,
419
- role: 'assistant',
420
- text: reply,
421
- ok: true,
422
- status: Number.isFinite(res.status) ? res.status : 0,
423
- durationMs: Number.isFinite(res.durationMs) ? res.durationMs : 0,
424
- model: typeof res.model === 'string' ? res.model : '',
425
- rawPreview: typeof res.rawPreview === 'string' ? res.rawPreview : ''
426
- });
427
- this.healthCheckDialogPrompt = '';
428
- } catch (e) {
429
- const message = e && e.message ? e.message : '健康检测失败';
430
- this.healthCheckDialogMessages.push({
431
- id: `assistant-${Date.now()}`,
432
- role: 'assistant',
433
- text: message,
434
- ok: false,
435
- status: 0,
436
- durationMs: 0,
437
- model: '',
438
- rawPreview: ''
439
- });
440
- this.healthCheckDialogLastResult = { ok: false, error: message };
441
- this.showMessage(message, 'error');
442
- } finally {
443
- this.healthCheckDialogSending = false;
444
- }
445
- },
446
-
447
- escapeTomlString(value) {
448
- return String(value || '')
449
- .replace(/\\/g, '\\\\')
450
- .replace(/"/g, '\\"');
451
- },
452
-
453
- async openConfigTemplateEditor(options = {}) {
454
- const modelContextWindow = this.normalizePositiveIntegerInput(
455
- this.modelContextWindowInput,
456
- 'model_context_window',
457
- defaultModelContextWindow
458
- );
459
- if (!modelContextWindow.ok) {
460
- this.showMessage(modelContextWindow.error, 'error');
461
- return;
462
- }
463
- const modelAutoCompactTokenLimit = this.normalizePositiveIntegerInput(
464
- this.modelAutoCompactTokenLimitInput,
465
- 'model_auto_compact_token_limit',
466
- defaultModelAutoCompactTokenLimit
467
- );
468
- if (!modelAutoCompactTokenLimit.ok) {
469
- this.showMessage(modelAutoCompactTokenLimit.error, 'error');
470
- return;
471
- }
472
- try {
473
- const res = await api('get-config-template', {
474
- provider: this.currentProvider,
475
- model: this.currentModel,
476
- serviceTier: this.serviceTier,
477
- reasoningEffort: this.modelReasoningEffort,
478
- modelContextWindow: modelContextWindow.value,
479
- modelAutoCompactTokenLimit: modelAutoCompactTokenLimit.value
480
- });
481
- if (res.error) {
482
- this.showMessage(res.error, 'error');
483
- return;
484
- }
485
- let template = res.template || '';
486
- const appendHint = typeof options.appendHint === 'string' ? options.appendHint.trim() : '';
487
- const appendBlock = typeof options.appendBlock === 'string' ? options.appendBlock.trim() : '';
488
- if (appendHint) {
489
- template = `${template.trimEnd()}\n\n# -------------------------------\n# ${appendHint}\n# -------------------------------\n`;
490
- }
491
- if (appendBlock) {
492
- template = `${template.trimEnd()}\n\n${appendBlock}\n`;
493
- }
494
- this.configTemplateContent = template;
495
- this.showConfigTemplateModal = true;
496
- } catch (e) {
497
- this.showMessage('加载模板失败', 'error');
498
- }
499
- },
500
-
501
- async applyCodexConfigDirect(options = {}) {
502
- if (this.codexApplying) {
503
- this._pendingCodexApplyOptions = {
504
- ...(this._pendingCodexApplyOptions || {}),
505
- ...options
506
- };
507
- return;
508
- }
509
-
510
- const provider = (this.currentProvider || '').trim();
511
- const model = (this.currentModel || '').trim();
512
- if (!provider || !model) {
513
- this.showMessage('请选择提供商和模型', 'error');
514
- return;
515
- }
516
-
517
- const modelContextWindow = this.normalizePositiveIntegerInput(
518
- options.modelContextWindow !== undefined ? options.modelContextWindow : this.modelContextWindowInput,
519
- 'model_context_window',
520
- defaultModelContextWindow
521
- );
522
- if (!modelContextWindow.ok) {
523
- this.showMessage(modelContextWindow.error, 'error');
524
- return;
525
- }
526
- const modelAutoCompactTokenLimit = this.normalizePositiveIntegerInput(
527
- options.modelAutoCompactTokenLimit !== undefined
528
- ? options.modelAutoCompactTokenLimit
529
- : this.modelAutoCompactTokenLimitInput,
530
- 'model_auto_compact_token_limit',
531
- defaultModelAutoCompactTokenLimit
532
- );
533
- if (!modelAutoCompactTokenLimit.ok) {
534
- this.showMessage(modelAutoCompactTokenLimit.error, 'error');
535
- return;
536
- }
537
- this.modelContextWindowInput = modelContextWindow.text;
538
- this.modelAutoCompactTokenLimitInput = modelAutoCompactTokenLimit.text;
539
-
540
- this.codexApplying = true;
541
- try {
542
- const tplRes = await api('get-config-template', {
543
- provider,
544
- model,
545
- serviceTier: this.serviceTier,
546
- reasoningEffort: this.modelReasoningEffort,
547
- modelContextWindow: modelContextWindow.value,
548
- modelAutoCompactTokenLimit: modelAutoCompactTokenLimit.value
549
- });
550
- if (tplRes.error) {
551
- this.showMessage(
552
- (typeof tplRes.error === 'string' && tplRes.error.trim())
553
- || (typeof tplRes.message === 'string' && tplRes.message.trim())
554
- || (typeof tplRes.detail === 'string' && tplRes.detail.trim())
555
- || '获取模板失败',
556
- 'error'
557
- );
558
- return;
559
- }
560
-
561
- const applyRes = await api('apply-config-template', {
562
- template: tplRes.template
563
- });
564
- if (applyRes.error) {
565
- this.showMessage(
566
- (typeof applyRes.error === 'string' && applyRes.error.trim())
567
- || (typeof applyRes.message === 'string' && applyRes.message.trim())
568
- || (typeof applyRes.detail === 'string' && applyRes.detail.trim())
569
- || '应用模板失败',
570
- 'error'
571
- );
572
- return;
573
- }
574
-
575
- if (options.silent !== true) {
576
- this.showMessage('配置已应用', 'success');
577
- }
578
-
579
- const refreshOptions = options.silent === true
580
- ? { preserveLoading: true }
581
- : {};
582
- try {
583
- await this.loadAll(refreshOptions);
584
- } catch (_) {
585
- this.showMessage('配置已应用,但界面刷新失败,请手动刷新', 'error');
586
- }
587
- } catch (e) {
588
- this.showMessage('应用失败', 'error');
589
- } finally {
590
- this.codexApplying = false;
591
- const pendingOptions = this._pendingCodexApplyOptions;
592
- this._pendingCodexApplyOptions = null;
593
- if (pendingOptions) {
594
- await this.applyCodexConfigDirect(pendingOptions);
595
- }
596
- }
597
- },
598
-
599
- closeConfigTemplateModal(options = {}) {
600
- const force = !!options.force;
601
- if (!force && this.configTemplateApplying) {
602
- return;
603
- }
604
- this.showConfigTemplateModal = false;
605
- this.configTemplateContent = '';
606
- },
607
-
608
- async applyConfigTemplate() {
609
- if (this.configTemplateApplying) {
610
- return;
611
- }
612
- if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
613
- this.showMessage('模板不能为空', 'error');
614
- return;
615
- }
616
-
617
- this.configTemplateApplying = true;
618
- try {
619
- const res = await api('apply-config-template', {
620
- template: this.configTemplateContent
621
- });
622
- if (res.error) {
623
- this.showMessage(res.error, 'error');
624
- return;
625
- }
626
- this.showMessage('模板已应用', 'success');
627
- this.closeConfigTemplateModal({ force: true });
628
- try {
629
- await this.loadAll();
630
- } catch (_) {
631
- this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
632
- }
633
- } catch (e) {
634
- this.showMessage('应用模板失败', 'error');
635
- } finally {
636
- this.configTemplateApplying = false;
637
- }
638
- }
639
- };
640
- }
1
+ import { runLatestOnlyQueue } from '../logic.mjs';
2
+ import { normalizeConfigTemplateDiffConfirmEnabled } from './config-template-confirm-pref.mjs';
3
+
4
+ function hasResponseError(response) {
5
+ if (!response || typeof response !== 'object') {
6
+ return false;
7
+ }
8
+ if (typeof response.error === 'string') {
9
+ return response.error.trim().length > 0;
10
+ }
11
+ return response.error !== undefined && response.error !== null && response.error !== false;
12
+ }
13
+
14
+ function getResponseMessage(response, fallback) {
15
+ if (!response || typeof response !== 'object') {
16
+ return fallback;
17
+ }
18
+ for (const key of ['error', 'message', 'detail']) {
19
+ const value = response[key];
20
+ if (typeof value === 'string' && value.trim()) {
21
+ return value.trim();
22
+ }
23
+ }
24
+ return fallback;
25
+ }
26
+
27
+ export function createCodexConfigMethods(options = {}) {
28
+ const {
29
+ api,
30
+ defaultModelContextWindow = 190000,
31
+ defaultModelAutoCompactTokenLimit = 185000,
32
+ getProviderConfigModeMeta
33
+ } = options;
34
+
35
+ return {
36
+ downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
37
+ const BOM = '\uFEFF';
38
+ const blob = new Blob([BOM + content], { type: mimeType });
39
+ const url = URL.createObjectURL(blob);
40
+ const link = document.createElement('a');
41
+ link.href = url;
42
+ link.download = fileName;
43
+ link.click();
44
+ URL.revokeObjectURL(url);
45
+ },
46
+
47
+ async exportSession(session) {
48
+ const key = this.getSessionExportKey(session);
49
+ if (this.sessionExporting[key]) return;
50
+
51
+ this.sessionExporting[key] = true;
52
+ try {
53
+ const res = await api('export-session', {
54
+ source: session.source,
55
+ sessionId: session.sessionId,
56
+ filePath: session.filePath
57
+ });
58
+ if (res.error) {
59
+ this.showMessage(res.error, 'error');
60
+ return;
61
+ }
62
+
63
+ const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
64
+ this.downloadTextFile(fileName, res.content || '');
65
+ if (res.truncated) {
66
+ const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
67
+ this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
68
+ } else {
69
+ this.showMessage('操作成功', 'success');
70
+ }
71
+ } catch (e) {
72
+ this.showMessage('导出失败', 'error');
73
+ } finally {
74
+ this.sessionExporting[key] = false;
75
+ }
76
+ },
77
+
78
+ async quickSwitchProvider(name) {
79
+ const target = String(name || '').trim();
80
+ const visualTarget = String(this.providerSwitchDisplayTarget || '').trim();
81
+ if (!target || target === visualTarget || target === this.pendingProviderSwitch) {
82
+ return;
83
+ }
84
+ if (!this.providerSwitchInProgress && target === this.currentProvider) {
85
+ return;
86
+ }
87
+ await this.switchProvider(target);
88
+ },
89
+
90
+ async waitForCodexApplyIdle(maxWaitMs = 20000) {
91
+ const startedAt = Date.now();
92
+ while (this.codexApplying) {
93
+ if ((Date.now() - startedAt) > maxWaitMs) {
94
+ throw new Error('等待配置应用完成超时');
95
+ }
96
+ await new Promise((resolve) => setTimeout(resolve, 50));
97
+ }
98
+ },
99
+
100
+ async performProviderSwitch(name) {
101
+ await this.waitForCodexApplyIdle();
102
+ const previousProvider = this.currentProvider;
103
+ const previousModel = this.currentModel;
104
+ const previousModels = Array.isArray(this.models) ? [...this.models] : [];
105
+ const previousModelsSource = this.modelsSource;
106
+ const previousModelsHasCurrent = this.modelsHasCurrent;
107
+ this.currentProvider = name;
108
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
109
+
110
+ // 不要把“切换提供商”强绑定到 /models 成功与否:
111
+ // 部分 OpenAI 兼容服务 /models 不可用或很慢,但用户仍希望一次点击即可完成切换。
112
+ // 这里做“短等待 + 后台补齐”:
113
+ // 1) 先启动 models 拉取(静默)
114
+ // 2) 给一个很短的窗口等待它完成,以便能立即选到第一个模型
115
+ // 3) 无论 models 是否成功,先应用 provider 切换
116
+ // 4) models 后续若补齐并发现当前 model 不在列表,则自动切到首个 model 并再应用一次
117
+ const modelsTask = this.loadModelsForProvider(name, { silentError: true })
118
+ .catch(() => {});
119
+
120
+ await Promise.race([modelsTask, delay(250)]);
121
+
122
+ if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
123
+ this.currentModel = this.models[0];
124
+ this.modelsHasCurrent = true;
125
+ }
126
+
127
+ if (getProviderConfigModeMeta(this.configMode)) {
128
+ await this.waitForCodexApplyIdle();
129
+ await this.applyCodexConfigDirect({ silent: true });
130
+ }
131
+
132
+ await modelsTask;
133
+
134
+ if (this.currentProvider === name) {
135
+ if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
136
+ this.currentModel = this.models[0];
137
+ this.modelsHasCurrent = true;
138
+ if (getProviderConfigModeMeta(this.configMode)) {
139
+ await this.waitForCodexApplyIdle();
140
+ await this.applyCodexConfigDirect({ silent: true });
141
+ }
142
+ }
143
+ }
144
+ },
145
+
146
+ async switchProvider(name) {
147
+ const target = String(name || '').trim();
148
+ if (!target) {
149
+ return;
150
+ }
151
+ if (target === String(this.providerSwitchDisplayTarget || '').trim()) {
152
+ return;
153
+ }
154
+ this.providerSwitchDisplayTarget = target;
155
+ if (this.providerSwitchInProgress) {
156
+ this.pendingProviderSwitch = target;
157
+ return;
158
+ }
159
+ this.providerSwitchInProgress = true;
160
+ let lastError = '';
161
+ try {
162
+ this.pendingProviderSwitch = '';
163
+ const result = await runLatestOnlyQueue(target, {
164
+ perform: async (queuedTarget) => {
165
+ this.providerSwitchDisplayTarget = queuedTarget;
166
+ await this.performProviderSwitch(queuedTarget);
167
+ },
168
+ consumePending: () => {
169
+ const queued = this.pendingProviderSwitch;
170
+ this.pendingProviderSwitch = '';
171
+ return queued;
172
+ }
173
+ });
174
+ if (result && typeof result.lastError === 'string') {
175
+ lastError = result.lastError;
176
+ }
177
+ } finally {
178
+ this.providerSwitchInProgress = false;
179
+ this.pendingProviderSwitch = '';
180
+ this.providerSwitchDisplayTarget = '';
181
+ }
182
+ if (lastError) {
183
+ this.showMessage(lastError, 'error');
184
+ }
185
+ },
186
+
187
+ async onModelChange() {
188
+ await this.applyCodexConfigDirect();
189
+ },
190
+
191
+ async onServiceTierChange() {
192
+ await this.applyCodexConfigDirect({ silent: true });
193
+ },
194
+
195
+ async onReasoningEffortChange() {
196
+ await this.applyCodexConfigDirect({ silent: true });
197
+ },
198
+
199
+ sanitizePositiveIntegerDraft(field) {
200
+ if (!field || typeof this[field] === 'undefined') return;
201
+ const current = typeof this[field] === 'string'
202
+ ? this[field]
203
+ : String(this[field] || '');
204
+ const sanitized = current.replace(/[^\d]/g, '');
205
+ if (sanitized !== current) {
206
+ this[field] = sanitized;
207
+ }
208
+ },
209
+
210
+ normalizePositiveIntegerInput(value, label, fallback = '') {
211
+ const fallbackText = fallback === '' ? '' : String(fallback).trim();
212
+ const raw = typeof value === 'string'
213
+ ? value.trim()
214
+ : String(value == null ? '' : value).trim();
215
+ const text = raw || fallbackText;
216
+ if (!text) {
217
+ return { ok: true, value: null, text: '' };
218
+ }
219
+ if (!/^\d+$/.test(text)) {
220
+ return { ok: false, error: `${label} 请输入正整数` };
221
+ }
222
+ const num = Number.parseInt(text, 10);
223
+ if (!Number.isSafeInteger(num) || num <= 0) {
224
+ return { ok: false, error: `${label} 请输入正整数` };
225
+ }
226
+ return { ok: true, value: num, text: String(num) };
227
+ },
228
+
229
+ async onModelContextWindowBlur() {
230
+ this.editingCodexBudgetField = '';
231
+ const normalized = this.normalizePositiveIntegerInput(
232
+ this.modelContextWindowInput,
233
+ 'model_context_window',
234
+ defaultModelContextWindow
235
+ );
236
+ if (!normalized.ok) {
237
+ this.showMessage(normalized.error, 'error');
238
+ return;
239
+ }
240
+ this.modelContextWindowInput = normalized.text;
241
+ await this.applyCodexConfigDirect({
242
+ silent: true,
243
+ modelContextWindow: normalized.value
244
+ });
245
+ },
246
+
247
+ async onModelAutoCompactTokenLimitBlur() {
248
+ this.editingCodexBudgetField = '';
249
+ const normalized = this.normalizePositiveIntegerInput(
250
+ this.modelAutoCompactTokenLimitInput,
251
+ 'model_auto_compact_token_limit',
252
+ defaultModelAutoCompactTokenLimit
253
+ );
254
+ if (!normalized.ok) {
255
+ this.showMessage(normalized.error, 'error');
256
+ return;
257
+ }
258
+ this.modelAutoCompactTokenLimitInput = normalized.text;
259
+ await this.applyCodexConfigDirect({
260
+ silent: true,
261
+ modelAutoCompactTokenLimit: normalized.value
262
+ });
263
+ },
264
+
265
+ async resetCodexContextBudgetDefaults() {
266
+ this.modelContextWindowInput = String(defaultModelContextWindow);
267
+ this.modelAutoCompactTokenLimitInput = String(defaultModelAutoCompactTokenLimit);
268
+ await this.applyCodexConfigDirect({
269
+ modelContextWindow: defaultModelContextWindow,
270
+ modelAutoCompactTokenLimit: defaultModelAutoCompactTokenLimit
271
+ });
272
+ },
273
+
274
+ async runHealthCheck() {
275
+ this.healthCheckLoading = true;
276
+ this.healthCheckResult = null;
277
+ let shouldRunClaudeSpeedTests = false;
278
+ try {
279
+ const res = await api('config-health-check', {
280
+ remote: this.configMode === 'codex'
281
+ });
282
+ if (hasResponseError(res)) {
283
+ this.healthCheckResult = null;
284
+ this.showMessage(getResponseMessage(res, '检查失败'), 'error');
285
+ } else if (res && typeof res === 'object') {
286
+ shouldRunClaudeSpeedTests = true;
287
+ const issues = Array.isArray(res.issues) ? [...res.issues] : [];
288
+ let remote = res.remote || null;
289
+ {
290
+ const providers = (this.providersList || [])
291
+ .map((provider) => typeof provider === 'string'
292
+ ? provider.trim()
293
+ : String((provider && provider.name) || '').trim())
294
+ .filter(Boolean);
295
+ const tasks = providers.map(provider =>
296
+ this.runSpeedTest(provider, { silent: true })
297
+ .then(result => ({ name: provider, result }))
298
+ .catch(err => ({
299
+ name: provider,
300
+ result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
301
+ }))
302
+ );
303
+ const pairs = await Promise.all(tasks);
304
+ const results = {};
305
+ for (const pair of pairs) {
306
+ results[pair.name] = pair.result || null;
307
+ const issue = this.buildSpeedTestIssue(pair.name, pair.result);
308
+ if (issue) issues.push(issue);
309
+ }
310
+ if (remote && typeof remote === 'object') {
311
+ remote = {
312
+ ...remote,
313
+ speedTests: results
314
+ };
315
+ } else {
316
+ remote = {
317
+ type: 'speed-test',
318
+ speedTests: results
319
+ };
320
+ }
321
+ }
322
+
323
+ const ok = issues.length === 0;
324
+ this.healthCheckResult = {
325
+ ...res,
326
+ ok,
327
+ issues,
328
+ remote
329
+ };
330
+ if (ok) {
331
+ this.showMessage('检查通过', 'success');
332
+ }
333
+ } else {
334
+ this.healthCheckResult = null;
335
+ this.showMessage('检查失败', 'error');
336
+ }
337
+ } catch (e) {
338
+ this.healthCheckResult = null;
339
+ this.showMessage('检查失败', 'error');
340
+ } finally {
341
+ if (shouldRunClaudeSpeedTests && this.configMode === 'claude') {
342
+ try {
343
+ const entries = Object.entries(this.claudeConfigs || {});
344
+ await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)));
345
+ } catch (e) {}
346
+ }
347
+ this.healthCheckLoading = false;
348
+ }
349
+ },
350
+
351
+ buildDefaultHealthCheckPrompt() {
352
+ return '请简短回复:连接正常。';
353
+ },
354
+
355
+ openHealthCheckDialog(options = {}) {
356
+ const providerName = typeof options.providerName === 'string'
357
+ ? options.providerName.trim()
358
+ : '';
359
+ const locked = !!options.locked && !!providerName;
360
+ if (locked && providerName && providerName !== String(this.currentProvider || '').trim()) {
361
+ if (typeof this.showMessage === 'function') {
362
+ this.showMessage('请先切换到该提供商再进行健康聊天测试', 'info');
363
+ }
364
+ return;
365
+ }
366
+ const nextProvider = providerName
367
+ || String(this.healthCheckDialogSelectedProvider || '').trim()
368
+ || String(this.currentProvider || '').trim()
369
+ || String(((this.displayProvidersList || [])[0] || {}).name || '').trim();
370
+
371
+ this.showHealthCheckDialog = true;
372
+ this.healthCheckDialogLockedProvider = locked ? nextProvider : '';
373
+ this.healthCheckDialogSelectedProvider = nextProvider;
374
+ this.healthCheckDialogPrompt = this.buildDefaultHealthCheckPrompt();
375
+ this.healthCheckDialogMessages = [];
376
+ this.healthCheckDialogLastResult = null;
377
+ },
378
+
379
+ closeHealthCheckDialog(options = {}) {
380
+ if (this.healthCheckDialogSending && !options.force) {
381
+ return;
382
+ }
383
+ this.showHealthCheckDialog = false;
384
+ this.healthCheckDialogLockedProvider = '';
385
+ this.healthCheckDialogSelectedProvider = '';
386
+ this.healthCheckDialogPrompt = this.buildDefaultHealthCheckPrompt();
387
+ this.healthCheckDialogMessages = [];
388
+ this.healthCheckDialogLastResult = null;
389
+ },
390
+
391
+ async sendHealthCheckDialogMessage() {
392
+ if (this.healthCheckDialogSending) {
393
+ return;
394
+ }
395
+
396
+ const provider = String(
397
+ this.healthCheckDialogLockedProvider || this.healthCheckDialogSelectedProvider || ''
398
+ ).trim();
399
+ const prompt = String(this.healthCheckDialogPrompt || '').trim();
400
+ if (!provider) {
401
+ this.showMessage('请先选择提供商', 'error');
402
+ return;
403
+ }
404
+ if (!prompt) {
405
+ this.showMessage('请输入消息内容', 'error');
406
+ return;
407
+ }
408
+
409
+ this.healthCheckDialogMessages.push({
410
+ id: `user-${Date.now()}`,
411
+ role: 'user',
412
+ text: prompt
413
+ });
414
+ this.healthCheckDialogSending = true;
415
+ this.healthCheckDialogLastResult = null;
416
+
417
+ try {
418
+ const res = await api('provider-chat-check', {
419
+ name: provider,
420
+ prompt
421
+ });
422
+ this.healthCheckDialogLastResult = res;
423
+
424
+ if (hasResponseError(res) || res.ok === false) {
425
+ const message = getResponseMessage(res, '健康聊天测试失败');
426
+ this.healthCheckDialogMessages.push({
427
+ id: `assistant-${Date.now()}`,
428
+ role: 'assistant',
429
+ text: message,
430
+ ok: false,
431
+ status: Number.isFinite(res && res.status) ? res.status : 0,
432
+ durationMs: Number.isFinite(res && res.durationMs) ? res.durationMs : 0,
433
+ model: typeof (res && res.model) === 'string' ? res.model : '',
434
+ rawPreview: typeof (res && res.rawPreview) === 'string' ? res.rawPreview : ''
435
+ });
436
+ this.showMessage(message, 'error');
437
+ return;
438
+ }
439
+
440
+ const reply = typeof res.reply === 'string' && res.reply.trim()
441
+ ? res.reply.trim()
442
+ : '已收到回复,但未解析到可展示文本。';
443
+ this.healthCheckDialogMessages.push({
444
+ id: `assistant-${Date.now()}`,
445
+ role: 'assistant',
446
+ text: reply,
447
+ ok: true,
448
+ status: Number.isFinite(res.status) ? res.status : 0,
449
+ durationMs: Number.isFinite(res.durationMs) ? res.durationMs : 0,
450
+ model: typeof res.model === 'string' ? res.model : '',
451
+ rawPreview: typeof res.rawPreview === 'string' ? res.rawPreview : ''
452
+ });
453
+ this.healthCheckDialogPrompt = '';
454
+ } catch (e) {
455
+ const message = e && e.message ? e.message : '健康聊天测试失败';
456
+ this.healthCheckDialogMessages.push({
457
+ id: `assistant-${Date.now()}`,
458
+ role: 'assistant',
459
+ text: message,
460
+ ok: false,
461
+ status: 0,
462
+ durationMs: 0,
463
+ model: '',
464
+ rawPreview: ''
465
+ });
466
+ this.healthCheckDialogLastResult = { ok: false, error: message };
467
+ this.showMessage(message, 'error');
468
+ } finally {
469
+ this.healthCheckDialogSending = false;
470
+ }
471
+ },
472
+
473
+ escapeTomlString(value) {
474
+ return String(value || '')
475
+ .replace(/\\/g, '\\\\')
476
+ .replace(/"/g, '\\"');
477
+ },
478
+
479
+ async openConfigTemplateEditor(options = {}) {
480
+ this.resetConfigTemplateDiffState();
481
+ const modelContextWindow = this.normalizePositiveIntegerInput(
482
+ this.modelContextWindowInput,
483
+ 'model_context_window',
484
+ defaultModelContextWindow
485
+ );
486
+ if (!modelContextWindow.ok) {
487
+ this.showMessage(modelContextWindow.error, 'error');
488
+ return;
489
+ }
490
+ const modelAutoCompactTokenLimit = this.normalizePositiveIntegerInput(
491
+ this.modelAutoCompactTokenLimitInput,
492
+ 'model_auto_compact_token_limit',
493
+ defaultModelAutoCompactTokenLimit
494
+ );
495
+ if (!modelAutoCompactTokenLimit.ok) {
496
+ this.showMessage(modelAutoCompactTokenLimit.error, 'error');
497
+ return;
498
+ }
499
+ try {
500
+ const res = await api('get-config-template', {
501
+ provider: this.currentProvider,
502
+ model: this.currentModel,
503
+ serviceTier: this.serviceTier,
504
+ reasoningEffort: this.modelReasoningEffort,
505
+ modelContextWindow: modelContextWindow.value,
506
+ modelAutoCompactTokenLimit: modelAutoCompactTokenLimit.value
507
+ });
508
+ if (res.error) {
509
+ this.showMessage(res.error, 'error');
510
+ return;
511
+ }
512
+ let template = res.template || '';
513
+ const appendHint = typeof options.appendHint === 'string' ? options.appendHint.trim() : '';
514
+ const appendBlock = typeof options.appendBlock === 'string' ? options.appendBlock.trim() : '';
515
+ if (appendHint) {
516
+ template = `${template.trimEnd()}\n\n# -------------------------------\n# ${appendHint}\n# -------------------------------\n`;
517
+ }
518
+ if (appendBlock) {
519
+ template = `${template.trimEnd()}\n\n${appendBlock}\n`;
520
+ }
521
+ this.configTemplateContent = template;
522
+ this.showConfigTemplateModal = true;
523
+ } catch (e) {
524
+ this.showMessage('加载模板失败', 'error');
525
+ }
526
+ },
527
+
528
+ async applyCodexConfigDirect(options = {}) {
529
+ if (this.codexApplying) {
530
+ this._pendingCodexApplyOptions = {
531
+ ...(this._pendingCodexApplyOptions || {}),
532
+ ...options
533
+ };
534
+ return;
535
+ }
536
+
537
+ const provider = (this.currentProvider || '').trim();
538
+ const model = (this.currentModel || '').trim();
539
+ if (!provider || !model) {
540
+ this.showMessage('请选择提供商和模型', 'error');
541
+ return;
542
+ }
543
+
544
+ const modelContextWindow = this.normalizePositiveIntegerInput(
545
+ options.modelContextWindow !== undefined ? options.modelContextWindow : this.modelContextWindowInput,
546
+ 'model_context_window',
547
+ defaultModelContextWindow
548
+ );
549
+ if (!modelContextWindow.ok) {
550
+ this.showMessage(modelContextWindow.error, 'error');
551
+ return;
552
+ }
553
+ const modelAutoCompactTokenLimit = this.normalizePositiveIntegerInput(
554
+ options.modelAutoCompactTokenLimit !== undefined
555
+ ? options.modelAutoCompactTokenLimit
556
+ : this.modelAutoCompactTokenLimitInput,
557
+ 'model_auto_compact_token_limit',
558
+ defaultModelAutoCompactTokenLimit
559
+ );
560
+ if (!modelAutoCompactTokenLimit.ok) {
561
+ this.showMessage(modelAutoCompactTokenLimit.error, 'error');
562
+ return;
563
+ }
564
+ this.modelContextWindowInput = modelContextWindow.text;
565
+ this.modelAutoCompactTokenLimitInput = modelAutoCompactTokenLimit.text;
566
+
567
+ this.codexApplying = true;
568
+ try {
569
+ const tplRes = await api('get-config-template', {
570
+ provider,
571
+ model,
572
+ serviceTier: this.serviceTier,
573
+ reasoningEffort: this.modelReasoningEffort,
574
+ modelContextWindow: modelContextWindow.value,
575
+ modelAutoCompactTokenLimit: modelAutoCompactTokenLimit.value
576
+ });
577
+ if (tplRes.error) {
578
+ this.showMessage(
579
+ (typeof tplRes.error === 'string' && tplRes.error.trim())
580
+ || (typeof tplRes.message === 'string' && tplRes.message.trim())
581
+ || (typeof tplRes.detail === 'string' && tplRes.detail.trim())
582
+ || '获取模板失败',
583
+ 'error'
584
+ );
585
+ return;
586
+ }
587
+
588
+ const applyRes = await api('apply-config-template', {
589
+ template: tplRes.template
590
+ });
591
+ if (applyRes.error) {
592
+ this.showMessage(
593
+ (typeof applyRes.error === 'string' && applyRes.error.trim())
594
+ || (typeof applyRes.message === 'string' && applyRes.message.trim())
595
+ || (typeof applyRes.detail === 'string' && applyRes.detail.trim())
596
+ || '应用模板失败',
597
+ 'error'
598
+ );
599
+ return;
600
+ }
601
+
602
+ if (options.silent !== true) {
603
+ this.showMessage('配置已应用', 'success');
604
+ }
605
+
606
+ const refreshOptions = options.silent === true
607
+ ? { preserveLoading: true }
608
+ : {};
609
+ try {
610
+ await this.loadAll(refreshOptions);
611
+ } catch (_) {
612
+ this.showMessage('配置已应用,但界面刷新失败,请手动刷新', 'error');
613
+ }
614
+ } catch (e) {
615
+ this.showMessage('应用失败', 'error');
616
+ } finally {
617
+ this.codexApplying = false;
618
+ const pendingOptions = this._pendingCodexApplyOptions;
619
+ this._pendingCodexApplyOptions = null;
620
+ if (pendingOptions) {
621
+ await this.applyCodexConfigDirect(pendingOptions);
622
+ }
623
+ }
624
+ },
625
+
626
+ closeConfigTemplateModal(options = {}) {
627
+ const force = !!options.force;
628
+ if (!force && (this.configTemplateApplying || this.configTemplateDiffLoading)) {
629
+ return;
630
+ }
631
+ this.showConfigTemplateModal = false;
632
+ this.configTemplateContent = '';
633
+ this.resetConfigTemplateDiffState();
634
+ },
635
+
636
+ resetConfigTemplateDiffState() {
637
+ this.configTemplateDiffVisible = false;
638
+ this.configTemplateDiffLoading = false;
639
+ this.configTemplateDiffError = '';
640
+ this.configTemplateDiffLines = [];
641
+ this.configTemplateDiffStats = { added: 0, removed: 0, unchanged: 0 };
642
+ this.configTemplateDiffHasChangesValue = false;
643
+ this.configTemplateDiffFingerprint = '';
644
+ this._configTemplateDiffPreviewRequestToken = null;
645
+ },
646
+
647
+ onConfigTemplateContentInput() {
648
+ if (this.configTemplateDiffVisible || (this.configTemplateDiffLines && this.configTemplateDiffLines.length)) {
649
+ this.resetConfigTemplateDiffState();
650
+ }
651
+ },
652
+
653
+ buildConfigTemplateDiffFingerprint() {
654
+ const content = typeof this.configTemplateContent === 'string' ? this.configTemplateContent : '';
655
+ return `${content.length}::${content}`;
656
+ },
657
+
658
+ hasConfigTemplateDiffChanges() {
659
+ if (this.configTemplateDiffHasChangesValue !== undefined && this.configTemplateDiffHasChangesValue !== null) {
660
+ return !!this.configTemplateDiffHasChangesValue;
661
+ }
662
+ const stats = this.configTemplateDiffStats && typeof this.configTemplateDiffStats === 'object'
663
+ ? this.configTemplateDiffStats
664
+ : {};
665
+ const added = Number(stats.added || 0);
666
+ const removed = Number(stats.removed || 0);
667
+ return added > 0 || removed > 0;
668
+ },
669
+
670
+ async prepareConfigTemplateDiff() {
671
+ const requestFingerprint = this.buildConfigTemplateDiffFingerprint();
672
+ const requestToken = Symbol('config-template-diff-preview');
673
+ this._configTemplateDiffPreviewRequestToken = requestToken;
674
+ this.configTemplateDiffVisible = true;
675
+ this.configTemplateDiffLoading = true;
676
+ this.configTemplateDiffError = '';
677
+ this.configTemplateDiffLines = [];
678
+ this.configTemplateDiffStats = { added: 0, removed: 0, unchanged: 0 };
679
+ this.configTemplateDiffHasChangesValue = false;
680
+ try {
681
+ const shouldApply = () => (
682
+ this.configTemplateDiffVisible
683
+ && this._configTemplateDiffPreviewRequestToken === requestToken
684
+ && this.buildConfigTemplateDiffFingerprint() === requestFingerprint
685
+ );
686
+ const res = await api('preview-config-template-diff', {
687
+ template: this.configTemplateContent
688
+ });
689
+ if (!shouldApply()) {
690
+ return;
691
+ }
692
+ if (res.error) {
693
+ this.configTemplateDiffError = res.error;
694
+ return;
695
+ }
696
+ const diff = res.diff && typeof res.diff === 'object' ? res.diff : {};
697
+ const lines = Array.isArray(diff.lines) ? diff.lines : [];
698
+ this.configTemplateDiffLines = lines.filter(line => line && line.type);
699
+ const stats = diff.stats && typeof diff.stats === 'object' ? diff.stats : null;
700
+ if (stats) {
701
+ this.configTemplateDiffStats = {
702
+ added: Number(stats.added || 0),
703
+ removed: Number(stats.removed || 0),
704
+ unchanged: Number(stats.unchanged || 0)
705
+ };
706
+ } else {
707
+ const nextStats = { added: 0, removed: 0, unchanged: 0 };
708
+ for (const line of this.configTemplateDiffLines) {
709
+ if (line && line.type === 'add') nextStats.added += 1;
710
+ else if (line && line.type === 'del') nextStats.removed += 1;
711
+ else nextStats.unchanged += 1;
712
+ }
713
+ this.configTemplateDiffStats = nextStats;
714
+ }
715
+ this.configTemplateDiffHasChangesValue = !!diff.hasChanges;
716
+ this.configTemplateDiffFingerprint = requestFingerprint;
717
+ } catch (_) {
718
+ if (this._configTemplateDiffPreviewRequestToken === requestToken) {
719
+ this.configTemplateDiffError = '生成差异失败';
720
+ }
721
+ } finally {
722
+ if (this._configTemplateDiffPreviewRequestToken === requestToken) {
723
+ this.configTemplateDiffLoading = false;
724
+ }
725
+ }
726
+ },
727
+
728
+ async applyConfigTemplate() {
729
+ if (this.configTemplateApplying) {
730
+ return;
731
+ }
732
+ if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
733
+ this.showMessage('模板不能为空', 'error');
734
+ return;
735
+ }
736
+
737
+ // Default to two-step confirmation when the setting is unset.
738
+ // (The normalize helper lives in session-actions; keep a safe fallback here.)
739
+ const shouldUseTwoStepConfirm = normalizeConfigTemplateDiffConfirmEnabled(this.configTemplateDiffConfirmEnabled);
740
+
741
+ const performApply = async () => {
742
+ this.configTemplateApplying = true;
743
+ try {
744
+ const res = await api('apply-config-template', {
745
+ template: this.configTemplateContent
746
+ });
747
+ if (res.error) {
748
+ this.showMessage(res.error, 'error');
749
+ return;
750
+ }
751
+ this.showMessage('模板已应用', 'success');
752
+ this.closeConfigTemplateModal({ force: true });
753
+ try {
754
+ await this.loadAll();
755
+ } catch (_) {
756
+ this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
757
+ }
758
+ } catch (e) {
759
+ this.showMessage('应用模板失败', 'error');
760
+ } finally {
761
+ this.configTemplateApplying = false;
762
+ }
763
+ };
764
+
765
+ // One-step mode: apply immediately unless user explicitly entered the diff preview state.
766
+ if (!shouldUseTwoStepConfirm && !this.configTemplateDiffVisible) {
767
+ await performApply();
768
+ return;
769
+ }
770
+
771
+ if (!this.configTemplateDiffVisible) {
772
+ await this.prepareConfigTemplateDiff();
773
+ return;
774
+ }
775
+ if (this.configTemplateDiffLoading) {
776
+ return;
777
+ }
778
+ if (this.configTemplateDiffError) {
779
+ this.showMessage(this.configTemplateDiffError, 'error');
780
+ return;
781
+ }
782
+ const fingerprint = this.buildConfigTemplateDiffFingerprint();
783
+ if (this.configTemplateDiffFingerprint !== fingerprint) {
784
+ await this.prepareConfigTemplateDiff();
785
+ return;
786
+ }
787
+ if (!this.hasConfigTemplateDiffChanges()) {
788
+ this.showMessage('未检测到改动', 'info');
789
+ return;
790
+ }
791
+
792
+ await performApply();
793
+ }
794
+ };
795
+ }