codexmate 0.0.19 → 0.0.20

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 (67) hide show
  1. package/README.en.md +8 -4
  2. package/README.md +8 -4
  3. package/cli/config-health.js +338 -0
  4. package/cli.js +1136 -584
  5. package/lib/cli-models-utils.js +186 -27
  6. package/lib/cli-network-utils.js +117 -101
  7. package/package.json +8 -1
  8. package/web-ui/app.js +381 -5532
  9. package/web-ui/index.html +15 -2231
  10. package/web-ui/logic.agents-diff.mjs +386 -0
  11. package/web-ui/logic.claude.mjs +108 -0
  12. package/web-ui/logic.mjs +5 -793
  13. package/web-ui/logic.runtime.mjs +124 -0
  14. package/web-ui/logic.sessions.mjs +263 -0
  15. package/web-ui/modules/api.mjs +69 -0
  16. package/web-ui/modules/app.computed.dashboard.mjs +113 -0
  17. package/web-ui/modules/app.computed.index.mjs +13 -0
  18. package/web-ui/modules/app.computed.session.mjs +141 -0
  19. package/web-ui/modules/app.constants.mjs +15 -0
  20. package/web-ui/modules/app.methods.agents.mjs +493 -0
  21. package/web-ui/modules/app.methods.claude-config.mjs +174 -0
  22. package/web-ui/modules/app.methods.codex-config.mjs +640 -0
  23. package/web-ui/modules/app.methods.index.mjs +86 -0
  24. package/web-ui/modules/app.methods.install.mjs +157 -0
  25. package/web-ui/modules/app.methods.navigation.mjs +478 -0
  26. package/web-ui/modules/app.methods.openclaw-core.mjs +514 -0
  27. package/web-ui/modules/app.methods.openclaw-editing.mjs +337 -0
  28. package/web-ui/modules/app.methods.openclaw-persist.mjs +251 -0
  29. package/web-ui/modules/app.methods.providers.mjs +265 -0
  30. package/web-ui/modules/app.methods.runtime.mjs +323 -0
  31. package/web-ui/modules/app.methods.session-actions.mjs +457 -0
  32. package/web-ui/modules/app.methods.session-browser.mjs +435 -0
  33. package/web-ui/modules/app.methods.session-timeline.mjs +441 -0
  34. package/web-ui/modules/app.methods.session-trash.mjs +419 -0
  35. package/web-ui/modules/app.methods.startup-claude.mjs +406 -0
  36. package/web-ui/partials/index/layout-footer.html +69 -0
  37. package/web-ui/partials/index/layout-header.html +337 -0
  38. package/web-ui/partials/index/modal-config-template-agents.html +125 -0
  39. package/web-ui/partials/index/modal-confirm-toast.html +32 -0
  40. package/web-ui/partials/index/modal-health-check.html +72 -0
  41. package/web-ui/partials/index/modal-openclaw-config.html +275 -0
  42. package/web-ui/partials/index/modal-skills.html +184 -0
  43. package/web-ui/partials/index/modals-basic.html +196 -0
  44. package/web-ui/partials/index/panel-config-claude.html +100 -0
  45. package/web-ui/partials/index/panel-config-codex.html +237 -0
  46. package/web-ui/partials/index/panel-config-openclaw.html +84 -0
  47. package/web-ui/partials/index/panel-market.html +174 -0
  48. package/web-ui/partials/index/panel-sessions.html +387 -0
  49. package/web-ui/partials/index/panel-settings.html +166 -0
  50. package/web-ui/source-bundle.cjs +233 -0
  51. package/web-ui/styles/base-theme.css +373 -0
  52. package/web-ui/styles/controls-forms.css +354 -0
  53. package/web-ui/styles/feedback.css +108 -0
  54. package/web-ui/styles/health-check-dialog.css +144 -0
  55. package/web-ui/styles/layout-shell.css +330 -0
  56. package/web-ui/styles/modals-core.css +449 -0
  57. package/web-ui/styles/navigation-panels.css +381 -0
  58. package/web-ui/styles/openclaw-structured.css +266 -0
  59. package/web-ui/styles/responsive.css +416 -0
  60. package/web-ui/styles/sessions-list.css +414 -0
  61. package/web-ui/styles/sessions-preview.css +405 -0
  62. package/web-ui/styles/sessions-toolbar-trash.css +243 -0
  63. package/web-ui/styles/sessions-usage.css +276 -0
  64. package/web-ui/styles/skills-list.css +298 -0
  65. package/web-ui/styles/skills-market.css +335 -0
  66. package/web-ui/styles/titles-cards.css +407 -0
  67. package/web-ui/styles.css +16 -4668
package/cli.js CHANGED
@@ -42,12 +42,15 @@ const { buildLineDiff } = require('./lib/text-diff');
42
42
  const {
43
43
  extractModelNames,
44
44
  hasModelsListPayload,
45
- extractModelIds,
46
- buildModelsProbeUrl,
45
+ buildModelsCacheKey,
47
46
  buildModelProbeSpec,
48
- buildModelsCacheKey
47
+ buildModelConversationSpecs,
48
+ extractModelResponseText
49
49
  } = require('./lib/cli-models-utils');
50
- const { probeUrl, probeJsonPost } = require('./lib/cli-network-utils');
50
+ const {
51
+ probeUrl,
52
+ probeJsonPost
53
+ } = require('./lib/cli-network-utils');
51
54
  const {
52
55
  toIsoTime,
53
56
  updateLatestIso,
@@ -62,6 +65,13 @@ const {
62
65
  validateWorkflowDefinition,
63
66
  executeWorkflowDefinition
64
67
  } = require('./lib/workflow-engine');
68
+ const { buildConfigHealthReport: buildConfigHealthReportCore } = require('./cli/config-health');
69
+ const {
70
+ readBundledWebUiCss,
71
+ readBundledWebUiHtml,
72
+ readExecutableBundledJavaScriptModule,
73
+ readExecutableBundledWebUiScript
74
+ } = require('./web-ui/source-bundle.cjs');
65
75
 
66
76
  const DEFAULT_WEB_PORT = 3737;
67
77
  const DEFAULT_WEB_HOST = '0.0.0.0';
@@ -93,11 +103,12 @@ const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
93
103
  const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
94
104
  const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
95
105
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
106
+ const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
107
+ const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
96
108
  const CODEX_BACKUP_NAME = 'codex-config';
97
109
 
98
110
  const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
99
111
  const SPEED_TEST_TIMEOUT_MS = 8000;
100
- const HEALTH_CHECK_TIMEOUT_MS = 6000;
101
112
  const MAX_SESSION_LIST_SIZE = 300;
102
113
  const MAX_SESSION_TRASH_LIST_SIZE = 500;
103
114
  const MAX_EXPORT_MESSAGES = 1000;
@@ -172,37 +183,47 @@ const CLI_INSTALL_TARGETS = Object.freeze([
172
183
  const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
173
184
  const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
174
185
 
175
- function getCodexSkillsDir() {
176
- const envCodexHome = typeof process.env.CODEX_HOME === 'string' ? process.env.CODEX_HOME.trim() : '';
177
- if (envCodexHome) {
178
- const target = path.join(envCodexHome, 'skills');
179
- return resolveExistingDir([target], target);
180
- }
181
- const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
182
- if (xdgConfig) {
183
- const target = path.join(xdgConfig, 'codex', 'skills');
184
- return resolveExistingDir([target], target);
185
- }
186
- const homeConfigDir = path.join(os.homedir(), '.config', 'codex', 'skills');
187
- return resolveExistingDir([homeConfigDir], CODEX_SKILLS_DIR);
188
- }
189
-
190
- function getClaudeSkillsDir() {
191
- const envClaudeHome = typeof process.env.CLAUDE_HOME === 'string' && process.env.CLAUDE_HOME.trim()
192
- ? process.env.CLAUDE_HOME.trim()
193
- : (typeof process.env.CLAUDE_CONFIG_DIR === 'string' ? process.env.CLAUDE_CONFIG_DIR.trim() : '');
194
- if (envClaudeHome) {
195
- const target = path.join(envClaudeHome, 'skills');
196
- return resolveExistingDir([target], target);
197
- }
198
- const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
199
- if (xdgConfig) {
200
- const target = path.join(xdgConfig, 'claude', 'skills');
201
- return resolveExistingDir([target], target);
202
- }
203
- const homeConfigDir = path.join(os.homedir(), '.config', 'claude', 'skills');
204
- return resolveExistingDir([homeConfigDir], CLAUDE_SKILLS_DIR);
205
- }
186
+ function getCodexSkillsDir() {
187
+ const joinPath = (basePath, ...segments) => {
188
+ const base = typeof basePath === 'string' ? basePath.trim() : '';
189
+ const pathApi = base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
190
+ return pathApi.join(base, ...segments);
191
+ };
192
+ const envCodexHome = typeof process.env.CODEX_HOME === 'string' ? process.env.CODEX_HOME.trim() : '';
193
+ if (envCodexHome) {
194
+ const target = joinPath(envCodexHome, 'skills');
195
+ return resolveExistingDir([target], target);
196
+ }
197
+ const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
198
+ if (xdgConfig) {
199
+ const target = joinPath(xdgConfig, 'codex', 'skills');
200
+ return resolveExistingDir([target], target);
201
+ }
202
+ const homeConfigDir = joinPath(os.homedir(), '.config', 'codex', 'skills');
203
+ return resolveExistingDir([homeConfigDir], CODEX_SKILLS_DIR);
204
+ }
205
+
206
+ function getClaudeSkillsDir() {
207
+ const joinPath = (basePath, ...segments) => {
208
+ const base = typeof basePath === 'string' ? basePath.trim() : '';
209
+ const pathApi = base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
210
+ return pathApi.join(base, ...segments);
211
+ };
212
+ const envClaudeHome = typeof process.env.CLAUDE_HOME === 'string' && process.env.CLAUDE_HOME.trim()
213
+ ? process.env.CLAUDE_HOME.trim()
214
+ : (typeof process.env.CLAUDE_CONFIG_DIR === 'string' ? process.env.CLAUDE_CONFIG_DIR.trim() : '');
215
+ if (envClaudeHome) {
216
+ const target = joinPath(envClaudeHome, 'skills');
217
+ return resolveExistingDir([target], target);
218
+ }
219
+ const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
220
+ if (xdgConfig) {
221
+ const target = joinPath(xdgConfig, 'claude', 'skills');
222
+ return resolveExistingDir([target], target);
223
+ }
224
+ const homeConfigDir = joinPath(os.homedir(), '.config', 'claude', 'skills');
225
+ return resolveExistingDir([homeConfigDir], CLAUDE_SKILLS_DIR);
226
+ }
206
227
 
207
228
  function resolveWebPort() {
208
229
  const raw = process.env.CODEXMATE_PORT;
@@ -212,6 +233,239 @@ function resolveWebPort() {
212
233
  return parsed;
213
234
  }
214
235
 
236
+ // #region releaseRunPortIfNeeded
237
+ function releaseRunPortIfNeeded(port, host, deps = {}) {
238
+ const numericPort = parseInt(String(port), 10);
239
+ if (numericPort !== DEFAULT_WEB_PORT) {
240
+ return { attempted: false, released: false, pids: [], reason: 'non-default-port' };
241
+ }
242
+
243
+ const processRef = deps.process || process;
244
+ const runSpawnSync = deps.spawnSync || spawnSync;
245
+ const logger = deps.logger || console;
246
+ const killProcess = typeof deps.kill === 'function'
247
+ ? deps.kill
248
+ : (typeof processRef.kill === 'function' ? processRef.kill.bind(processRef) : null);
249
+ const seenPids = new Set();
250
+ const candidatePids = new Set();
251
+ const currentPid = Number(processRef.pid);
252
+ const normalizedHost = typeof host === 'string' ? host.trim().toLowerCase() : '';
253
+ let released = false;
254
+ const windowsCommandLineCache = new Map();
255
+
256
+ const isManagedRunCommand = (commandLine) => {
257
+ const normalizedLine = ` ${String(commandLine || '').replace(/\s+/g, ' ').trim()} `;
258
+ return /(^|[\/\\\s])codexmate(?:\.cmd|\.exe)? run(\s|$)/i.test(normalizedLine)
259
+ || /(^|[\/\\\s])cli\.js run(\s|$)/i.test(normalizedLine);
260
+ };
261
+
262
+ const normalizeListenerHost = (value) => {
263
+ const trimmed = String(value || '').trim().toLowerCase();
264
+ if (!trimmed) {
265
+ return '';
266
+ }
267
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
268
+ return trimmed.slice(1, -1);
269
+ }
270
+ return trimmed.startsWith('::ffff:') ? trimmed.slice('::ffff:'.length) : trimmed;
271
+ };
272
+
273
+ const extractListenerHost = (localAddress) => {
274
+ const trimmed = String(localAddress || '').trim();
275
+ if (!trimmed) {
276
+ return '';
277
+ }
278
+ if (trimmed.startsWith('[')) {
279
+ const closingBracket = trimmed.indexOf(']');
280
+ if (closingBracket > 0) {
281
+ return normalizeListenerHost(trimmed.slice(1, closingBracket));
282
+ }
283
+ }
284
+ const lastColon = trimmed.lastIndexOf(':');
285
+ if (lastColon <= 0) {
286
+ return normalizeListenerHost(trimmed);
287
+ }
288
+ return normalizeListenerHost(trimmed.slice(0, lastColon));
289
+ };
290
+
291
+ const isMatchingWindowsListenerAddress = (localAddress) => {
292
+ const listenerHost = extractListenerHost(localAddress);
293
+ if (!listenerHost || !normalizedHost) {
294
+ return false;
295
+ }
296
+ if (normalizedHost === 'localhost') {
297
+ return listenerHost === '127.0.0.1' || listenerHost === '::1';
298
+ }
299
+ if (normalizedHost === '0.0.0.0' || normalizedHost === '::') {
300
+ return listenerHost === normalizedHost;
301
+ }
302
+ return listenerHost === normalizeListenerHost(normalizedHost);
303
+ };
304
+
305
+ const addPidsFromText = (text, targetSet = seenPids) => {
306
+ if (!targetSet) {
307
+ return;
308
+ }
309
+ const lines = String(text || '').split(/\r?\n/);
310
+ for (const line of lines) {
311
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
312
+ for (const token of tokens) {
313
+ if (!/^\d+$/.test(token)) {
314
+ continue;
315
+ }
316
+ targetSet.add(Number(token));
317
+ }
318
+ }
319
+ };
320
+
321
+ const runCommand = (command, args, options = {}) => {
322
+ const {
323
+ stdoutPidSet = seenPids,
324
+ stderrPidSet = seenPids
325
+ } = options;
326
+ const result = runSpawnSync(command, args, { encoding: 'utf8' });
327
+ if (result && result.stdout) addPidsFromText(result.stdout, stdoutPidSet);
328
+ if (result && result.stderr) addPidsFromText(result.stderr, stderrPidSet);
329
+ return result || {};
330
+ };
331
+
332
+ const addManagedRunPidsFromPs = (text, allowedPids = null) => {
333
+ const lines = String(text || '').split(/\r?\n/);
334
+ for (const line of lines) {
335
+ const normalizedLine = ` ${line.replace(/\s+/g, ' ').trim()} `;
336
+ if (!/(^|[\/\s])codexmate run(\s|$)/.test(normalizedLine) && !/(^|[\/\s])cli\.js run(\s|$)/.test(normalizedLine)) {
337
+ continue;
338
+ }
339
+ const pidMatch = line.match(/^\S+\s+(\d+)\s+/);
340
+ if (!pidMatch) {
341
+ continue;
342
+ }
343
+ const pid = Number(pidMatch[1]);
344
+ if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) {
345
+ continue;
346
+ }
347
+ if (allowedPids && !allowedPids.has(pid)) {
348
+ continue;
349
+ }
350
+ seenPids.add(pid);
351
+ }
352
+ };
353
+
354
+ const getWindowsProcessCommandLine = (pid) => {
355
+ if (windowsCommandLineCache.has(pid)) {
356
+ return windowsCommandLineCache.get(pid);
357
+ }
358
+ const result = runCommand(
359
+ 'powershell',
360
+ [
361
+ '-NoProfile',
362
+ '-Command',
363
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($p) { $p.CommandLine }`
364
+ ],
365
+ { stdoutPidSet: null, stderrPidSet: null }
366
+ );
367
+ const commandLine = !result.error && result.status === 0
368
+ ? String(result.stdout || '').trim()
369
+ : '';
370
+ windowsCommandLineCache.set(pid, commandLine);
371
+ return commandLine;
372
+ };
373
+
374
+ if (processRef.platform === 'win32') {
375
+ const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp'], { stdoutPidSet: null, stderrPidSet: null });
376
+ if (!(netstatResult && netstatResult.error)) {
377
+ const lines = String(netstatResult.stdout || '').split(/\r?\n/);
378
+ for (const line of lines) {
379
+ const parts = line.trim().split(/\s+/);
380
+ if (parts.length < 5) {
381
+ continue;
382
+ }
383
+ const localAddress = parts[1];
384
+ const state = parts[3];
385
+ const pidText = parts[4];
386
+ if (state !== 'LISTENING' || !localAddress.endsWith(`:${numericPort}`) || !/^\d+$/.test(pidText)) {
387
+ continue;
388
+ }
389
+ if (!isMatchingWindowsListenerAddress(localAddress)) {
390
+ continue;
391
+ }
392
+ candidatePids.add(Number(pidText));
393
+ }
394
+ for (const pid of candidatePids) {
395
+ if (pid === currentPid) {
396
+ continue;
397
+ }
398
+ if (!isManagedRunCommand(getWindowsProcessCommandLine(pid))) {
399
+ continue;
400
+ }
401
+ seenPids.add(pid);
402
+ const taskkillResult = runCommand(
403
+ 'taskkill',
404
+ ['/PID', String(pid), '/F'],
405
+ { stdoutPidSet: null, stderrPidSet: null }
406
+ );
407
+ if (!taskkillResult.error && taskkillResult.status === 0) {
408
+ released = true;
409
+ }
410
+ }
411
+ }
412
+ } else {
413
+ let psResult = null;
414
+ const readPsResult = () => {
415
+ if (psResult) {
416
+ return psResult;
417
+ }
418
+ psResult = runCommand('ps', ['-ef'], { stdoutPidSet: null, stderrPidSet: null });
419
+ return psResult;
420
+ };
421
+
422
+ const lsofResult = runCommand(
423
+ 'lsof',
424
+ ['-ti', `tcp:${numericPort}`],
425
+ { stdoutPidSet: candidatePids, stderrPidSet: null }
426
+ );
427
+ const shouldTryFuser = !!(lsofResult && lsofResult.error && lsofResult.error.code === 'ENOENT');
428
+ if (shouldTryFuser && candidatePids.size === 0) {
429
+ runCommand(
430
+ 'fuser',
431
+ [`${numericPort}/tcp`],
432
+ { stdoutPidSet: candidatePids, stderrPidSet: candidatePids }
433
+ );
434
+ }
435
+ if (candidatePids.size > 0) {
436
+ const managedPsResult = readPsResult();
437
+ if (!(managedPsResult && managedPsResult.error)) {
438
+ addManagedRunPidsFromPs(managedPsResult.stdout, candidatePids);
439
+ }
440
+ }
441
+ }
442
+
443
+ if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size > 0) {
444
+ for (const pid of seenPids) {
445
+ if (pid === currentPid) {
446
+ continue;
447
+ }
448
+ try {
449
+ killProcess(pid, 'SIGKILL');
450
+ released = true;
451
+ } catch (_) {}
452
+ }
453
+ }
454
+
455
+ if (released) {
456
+ logger.log(`~ 已释放端口 ${numericPort} 占用`);
457
+ }
458
+
459
+ return {
460
+ attempted: true,
461
+ released,
462
+ pids: Array.from(seenPids)
463
+ .filter((pid) => pid !== currentPid)
464
+ .sort((a, b) => a - b)
465
+ };
466
+ }
467
+ // #endregion releaseRunPortIfNeeded
468
+
215
469
  function resolveWebHost(options = {}) {
216
470
  const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
217
471
  if (optionHost) {
@@ -225,7 +479,8 @@ function resolveWebHost(options = {}) {
225
479
  }
226
480
 
227
481
  const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
228
- model_reasoning_effort = "high"
482
+ model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
483
+ model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
229
484
  disable_response_storage = true
230
485
  approval_policy = "never"
231
486
  sandbox_mode = "danger-full-access"
@@ -1759,13 +2014,18 @@ function listSkillEntriesByRoot(rootDir) {
1759
2014
  }
1760
2015
  }
1761
2016
 
1762
- function scanUnmanagedSkills(params = {}) {
1763
- const target = resolveSkillTarget(params);
1764
- if (!target) {
1765
- return { error: '目标宿主不支持' };
1766
- }
1767
- const targetRoot = resolveCopyTargetRoot(target.dir);
1768
- const existing = listSkills({ targetApp: target.app });
2017
+ function scanUnmanagedSkills(params = {}) {
2018
+ const getPathApi = (basePath) => {
2019
+ const base = typeof basePath === 'string' ? basePath.trim() : '';
2020
+ return base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
2021
+ };
2022
+ const target = resolveSkillTarget(params);
2023
+ if (!target) {
2024
+ return { error: '目标宿主不支持' };
2025
+ }
2026
+ const targetRoot = resolveCopyTargetRoot(target.dir);
2027
+ const targetPathApi = getPathApi(targetRoot);
2028
+ const existing = listSkills({ targetApp: target.app });
1769
2029
  if (existing.error) {
1770
2030
  return { error: existing.error };
1771
2031
  }
@@ -1773,15 +2033,15 @@ function scanUnmanagedSkills(params = {}) {
1773
2033
  .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
1774
2034
  .filter(Boolean));
1775
2035
 
1776
- const items = [];
1777
- const sources = SKILL_IMPORT_SOURCES.filter((source) => source.app !== target.app);
1778
- for (const source of sources) {
1779
- const sourceEntries = listSkillEntriesByRoot(source.dir);
1780
- for (const entry of sourceEntries) {
1781
- const targetCandidate = path.join(targetRoot, entry.name);
1782
- if (fs.existsSync(targetCandidate)) {
1783
- continue;
1784
- }
2036
+ const items = [];
2037
+ const sources = SKILL_IMPORT_SOURCES.filter((source) => source.app !== target.app);
2038
+ for (const source of sources) {
2039
+ const sourceEntries = listSkillEntriesByRoot(source.dir);
2040
+ for (const entry of sourceEntries) {
2041
+ const targetCandidate = targetPathApi.join(targetRoot, entry.name);
2042
+ if (fs.existsSync(targetCandidate)) {
2043
+ continue;
2044
+ }
1785
2045
  if (existingNames.has(entry.name)) {
1786
2046
  continue;
1787
2047
  }
@@ -1824,13 +2084,18 @@ function scanUnmanagedCodexSkills() {
1824
2084
  return scanUnmanagedSkills({ targetApp: 'codex' });
1825
2085
  }
1826
2086
 
1827
- function importSkills(params = {}) {
1828
- const target = resolveSkillTarget(params);
1829
- if (!target) {
1830
- return { error: '目标宿主不支持' };
1831
- }
1832
- const targetRoot = resolveCopyTargetRoot(target.dir);
1833
- const rawItems = Array.isArray(params.items) ? params.items : [];
2087
+ function importSkills(params = {}) {
2088
+ const getPathApi = (basePath) => {
2089
+ const base = typeof basePath === 'string' ? basePath.trim() : '';
2090
+ return base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
2091
+ };
2092
+ const target = resolveSkillTarget(params);
2093
+ if (!target) {
2094
+ return { error: '目标宿主不支持' };
2095
+ }
2096
+ const targetRoot = resolveCopyTargetRoot(target.dir);
2097
+ const targetPathApi = getPathApi(targetRoot);
2098
+ const rawItems = Array.isArray(params.items) ? params.items : [];
1834
2099
  if (!rawItems.length) {
1835
2100
  return { error: '请先选择要导入的 skill' };
1836
2101
  }
@@ -1873,12 +2138,13 @@ function importSkills(params = {}) {
1873
2138
  }
1874
2139
  dedup.add(dedupKey);
1875
2140
 
1876
- const sourcePath = path.join(source.dir, normalizedName.name);
1877
- const sourceRelative = path.relative(source.dir, sourcePath);
1878
- if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
1879
- failed.push({
1880
- name: normalizedName.name,
1881
- sourceApp: source.app,
2141
+ const sourcePathApi = getPathApi(source.dir);
2142
+ const sourcePath = sourcePathApi.join(source.dir, normalizedName.name);
2143
+ const sourceRelative = sourcePathApi.relative(source.dir, sourcePath);
2144
+ if (sourceRelative.startsWith('..') || sourcePathApi.isAbsolute(sourceRelative)) {
2145
+ failed.push({
2146
+ name: normalizedName.name,
2147
+ sourceApp: source.app,
1882
2148
  error: '来源路径非法'
1883
2149
  });
1884
2150
  continue;
@@ -1892,12 +2158,12 @@ function importSkills(params = {}) {
1892
2158
  continue;
1893
2159
  }
1894
2160
 
1895
- const targetPath = path.join(targetRoot, normalizedName.name);
1896
- const targetRelative = path.relative(targetRoot, targetPath);
1897
- if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
1898
- failed.push({
1899
- name: normalizedName.name,
1900
- sourceApp: source.app,
2161
+ const targetPath = targetPathApi.join(targetRoot, normalizedName.name);
2162
+ const targetRelative = targetPathApi.relative(targetRoot, targetPath);
2163
+ if (targetRelative.startsWith('..') || targetPathApi.isAbsolute(targetRelative)) {
2164
+ failed.push({
2165
+ name: normalizedName.name,
2166
+ sourceApp: source.app,
1901
2167
  error: '目标路径非法'
1902
2168
  });
1903
2169
  continue;
@@ -2037,13 +2303,19 @@ function resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbac
2037
2303
  return normalizeCodexSkillName(candidate);
2038
2304
  }
2039
2305
 
2040
- async function importSkillsFromZipFile(zipPath, options = {}) {
2041
- const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
2042
- const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
2043
- const imported = [];
2044
- const failed = [];
2045
- const dedupNames = new Set();
2046
- const extractionRoot = path.join(tempDir || path.dirname(zipPath), 'extract');
2306
+ async function importSkillsFromZipFile(zipPath, options = {}) {
2307
+ const getPathApi = (basePath) => {
2308
+ const base = typeof basePath === 'string' ? basePath.trim() : '';
2309
+ return base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
2310
+ };
2311
+ const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
2312
+ const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
2313
+ const imported = [];
2314
+ const failed = [];
2315
+ const dedupNames = new Set();
2316
+ const extractionPathApi = getPathApi(tempDir || zipPath);
2317
+ const extractionBaseDir = tempDir || extractionPathApi.dirname(zipPath);
2318
+ const extractionRoot = extractionPathApi.join(extractionBaseDir, 'extract');
2047
2319
  let target = null;
2048
2320
  let targetRoot = '';
2049
2321
 
@@ -2051,11 +2323,12 @@ async function importSkillsFromZipFile(zipPath, options = {}) {
2051
2323
  target = resolveSkillTarget(options, 'codex');
2052
2324
  if (!target) {
2053
2325
  return { error: '目标宿主不支持' };
2054
- }
2055
- targetRoot = resolveCopyTargetRoot(target.dir);
2056
- await inspectZipArchiveLimits(zipPath, {
2057
- maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
2058
- maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
2326
+ }
2327
+ targetRoot = resolveCopyTargetRoot(target.dir);
2328
+ const targetPathApi = getPathApi(targetRoot);
2329
+ await inspectZipArchiveLimits(zipPath, {
2330
+ maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
2331
+ maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
2059
2332
  });
2060
2333
 
2061
2334
  await extractUploadZip(zipPath, extractionRoot);
@@ -2083,12 +2356,12 @@ async function importSkillsFromZipFile(zipPath, options = {}) {
2083
2356
  }
2084
2357
  dedupNames.add(dedupKey);
2085
2358
 
2086
- const targetPath = path.join(targetRoot, normalizedName.name);
2087
- const targetRelative = path.relative(targetRoot, targetPath);
2088
- if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
2089
- failed.push({
2090
- name: normalizedName.name,
2091
- error: '目标路径非法'
2359
+ const targetPath = targetPathApi.join(targetRoot, normalizedName.name);
2360
+ const targetRelative = targetPathApi.relative(targetRoot, targetPath);
2361
+ if (targetRelative.startsWith('..') || targetPathApi.isAbsolute(targetRelative)) {
2362
+ failed.push({
2363
+ name: normalizedName.name,
2364
+ error: '目标路径非法'
2092
2365
  });
2093
2366
  continue;
2094
2367
  }
@@ -2807,358 +3080,11 @@ function recordRecentConfig(provider, model) {
2807
3080
  writeRecentConfigs(trimmed);
2808
3081
  }
2809
3082
 
2810
- async function runRemoteHealthCheck(provider, modelName, options = {}) {
2811
- const issues = [];
2812
- const results = {};
2813
- const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
2814
- if (!baseUrl) {
2815
- issues.push({
2816
- code: 'remote-skip-base-url',
2817
- message: '无法进行远程探测:base_url 为空',
2818
- suggestion: '补全 base_url 或关闭远程探测'
2819
- });
2820
- return { issues, results };
2821
- }
2822
-
2823
- const requiresAuth = provider && provider.requires_openai_auth !== false;
2824
- const apiKey = typeof provider.preferred_auth_method === 'string'
2825
- ? provider.preferred_auth_method.trim()
2826
- : '';
2827
- const authValue = requiresAuth ? apiKey : (apiKey || '');
2828
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
2829
-
2830
- const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
2831
- results.base = {
2832
- url: baseUrl,
2833
- status: baseProbe.status || 0,
2834
- ok: baseProbe.ok,
2835
- durationMs: baseProbe.durationMs || 0
2836
- };
2837
-
2838
- if (!baseProbe.ok) {
2839
- issues.push({
2840
- code: 'remote-unreachable',
2841
- message: `远程探测失败:${baseProbe.error || '无法连接'}`,
2842
- suggestion: '检查网络与 base_url 可达性'
2843
- });
2844
- return { issues, results };
2845
- }
2846
-
2847
- if (baseProbe.status === 401 || baseProbe.status === 403) {
2848
- issues.push({
2849
- code: 'remote-auth-failed',
2850
- message: '远程探测鉴权失败(401/403)',
2851
- suggestion: '检查 API Key 或认证方式'
2852
- });
2853
- } else if (baseProbe.status >= 400) {
2854
- issues.push({
2855
- code: 'remote-http-error',
2856
- message: `远程探测返回异常状态: ${baseProbe.status}`,
2857
- suggestion: '检查 base_url 是否正确'
2858
- });
2859
- }
2860
-
2861
- const modelsUrl = buildModelsProbeUrl(baseUrl);
2862
- if (modelsUrl) {
2863
- const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
2864
- results.models = {
2865
- url: modelsUrl,
2866
- status: modelsProbe.status || 0,
2867
- ok: modelsProbe.ok,
2868
- durationMs: modelsProbe.durationMs || 0
2869
- };
2870
-
2871
- if (!modelsProbe.ok) {
2872
- issues.push({
2873
- code: 'remote-models-unreachable',
2874
- message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
2875
- suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
2876
- });
2877
- } else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
2878
- issues.push({
2879
- code: 'remote-models-auth-failed',
2880
- message: '模型列表鉴权失败(401/403)',
2881
- suggestion: '检查 API Key 或认证方式'
2882
- });
2883
- } else if (modelsProbe.status >= 400) {
2884
- issues.push({
2885
- code: 'remote-models-http-error',
2886
- message: `模型列表返回异常状态: ${modelsProbe.status}`,
2887
- suggestion: '确认 /v1/models 可用'
2888
- });
2889
- } else {
2890
- let payload = null;
2891
- try {
2892
- payload = modelsProbe.body ? JSON.parse(modelsProbe.body) : null;
2893
- } catch (e) {
2894
- issues.push({
2895
- code: 'remote-models-parse',
2896
- message: '模型列表解析失败(非 JSON)',
2897
- suggestion: '确认 /v1/models 返回 JSON'
2898
- });
2899
- }
2900
-
2901
- if (payload) {
2902
- const ids = extractModelIds(payload);
2903
- if (ids.length === 0) {
2904
- issues.push({
2905
- code: 'remote-models-empty',
2906
- message: '模型列表为空或结构无法识别',
2907
- suggestion: '确认 provider 是否兼容 /v1/models'
2908
- });
2909
- } else if (modelName && !ids.includes(modelName)) {
2910
- issues.push({
2911
- code: 'remote-model-unavailable',
2912
- message: `远程模型列表中未找到: ${modelName}`,
2913
- suggestion: '切换模型或确认模型名称'
2914
- });
2915
- }
2916
- }
2917
- }
2918
- }
2919
-
2920
- const modelProbeSpec = buildModelProbeSpec(provider, modelName, baseUrl);
2921
- if (modelProbeSpec && modelProbeSpec.url) {
2922
- const modelProbe = await probeJsonPost(modelProbeSpec.url, modelProbeSpec.body, {
2923
- apiKey: authValue,
2924
- timeoutMs,
2925
- maxBytes: 256 * 1024
2926
- });
2927
-
2928
- results.modelProbe = {
2929
- url: modelProbeSpec.url,
2930
- status: modelProbe.status || 0,
2931
- ok: modelProbe.ok,
2932
- durationMs: modelProbe.durationMs || 0
2933
- };
2934
-
2935
- if (!modelProbe.ok) {
2936
- issues.push({
2937
- code: 'remote-model-probe-unreachable',
2938
- message: `模型可用性探测失败:${modelProbe.error || '无法连接'}`,
2939
- suggestion: '检查网络或模型接口是否可用'
2940
- });
2941
- } else if (modelProbe.status === 401 || modelProbe.status === 403) {
2942
- issues.push({
2943
- code: 'remote-model-probe-auth-failed',
2944
- message: '模型可用性探测鉴权失败(401/403)',
2945
- suggestion: '检查 API Key 或认证方式'
2946
- });
2947
- } else if (modelProbe.status >= 400) {
2948
- issues.push({
2949
- code: 'remote-model-probe-http-error',
2950
- message: `模型可用性探测返回异常状态: ${modelProbe.status}`,
2951
- suggestion: '检查模型或接口路径'
2952
- });
2953
- } else {
2954
- let payload = null;
2955
- try {
2956
- payload = modelProbe.body ? JSON.parse(modelProbe.body) : null;
2957
- } catch (e) {
2958
- issues.push({
2959
- code: 'remote-model-probe-parse',
2960
- message: '模型可用性探测解析失败(非 JSON)',
2961
- suggestion: '确认模型接口返回 JSON'
2962
- });
2963
- }
2964
- if (payload && payload.error) {
2965
- const message = typeof payload.error.message === 'string'
2966
- ? payload.error.message
2967
- : '模型接口返回错误';
2968
- issues.push({
2969
- code: 'remote-model-probe-error',
2970
- message: `模型可用性探测失败:${message}`,
2971
- suggestion: '检查模型名与权限'
2972
- });
2973
- }
2974
- }
2975
- }
2976
-
2977
- return { issues, results };
2978
- }
2979
-
2980
3083
  async function buildConfigHealthReport(params = {}) {
2981
- const issues = [];
2982
- const status = readConfigOrVirtualDefault();
2983
- const config = status.config || {};
2984
-
2985
- if (status.isVirtual) {
2986
- const parseFailed = status.errorType === 'parse';
2987
- const readFailed = status.errorType === 'read';
2988
- issues.push({
2989
- code: parseFailed ? 'config-parse-failed' : (readFailed ? 'config-read-failed' : 'config-missing'),
2990
- message: status.reason || (parseFailed
2991
- ? 'config.toml 解析失败'
2992
- : (readFailed ? '读取 config.toml 失败' : '未检测到 config.toml')),
2993
- suggestion: parseFailed
2994
- ? '修复 config.toml 语法错误后重试'
2995
- : (readFailed ? '检查文件权限后重试' : '在模板编辑器中确认应用配置,生成可用的 config.toml')
2996
- });
2997
- if (parseFailed || readFailed) {
2998
- return {
2999
- ok: false,
3000
- issues,
3001
- summary: {
3002
- currentProvider: '',
3003
- currentModel: ''
3004
- },
3005
- remote: null
3006
- };
3007
- }
3008
- }
3009
-
3010
- const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
3011
- const modelName = typeof config.model === 'string' ? config.model.trim() : '';
3012
- if (!providerName) {
3013
- issues.push({
3014
- code: 'provider-missing',
3015
- message: '当前 provider 未设置',
3016
- suggestion: '在模板中设置 model_provider'
3017
- });
3018
- }
3019
-
3020
- if (!modelName) {
3021
- issues.push({
3022
- code: 'model-missing',
3023
- message: '当前模型未设置',
3024
- suggestion: '在模板中设置 model'
3025
- });
3026
- }
3027
-
3028
- const providers = config.model_providers && typeof config.model_providers === 'object'
3029
- ? config.model_providers
3030
- : {};
3031
- const provider = providerName ? providers[providerName] : null;
3032
- if (providerName && !provider) {
3033
- issues.push({
3034
- code: 'provider-not-found',
3035
- message: `当前 provider 未在配置中找到: ${providerName}`,
3036
- suggestion: '检查 model_providers 是否包含该 provider 配置块'
3037
- });
3038
- }
3039
-
3040
- if (provider && typeof provider === 'object') {
3041
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3042
- if (!isValidHttpUrl(baseUrl)) {
3043
- issues.push({
3044
- code: 'base-url-invalid',
3045
- message: '当前 provider 的 base_url 无效',
3046
- suggestion: '请设置为 http/https 的完整 URL'
3047
- });
3048
- }
3049
-
3050
- const requiresAuth = provider.requires_openai_auth;
3051
- if (requiresAuth !== false) {
3052
- const apiKey = typeof provider.preferred_auth_method === 'string'
3053
- ? provider.preferred_auth_method.trim()
3054
- : '';
3055
- if (!apiKey) {
3056
- issues.push({
3057
- code: 'api-key-missing',
3058
- message: '当前 provider 未配置 API Key',
3059
- suggestion: '在模板中设置 preferred_auth_method'
3060
- });
3061
- }
3062
- }
3063
- }
3064
-
3065
- if (modelName) {
3066
- const models = readModels();
3067
- if (!models.includes(modelName)) {
3068
- issues.push({
3069
- code: 'model-unavailable',
3070
- message: `模型未在可用列表中找到: ${modelName}`,
3071
- suggestion: '在模型列表中添加该模型或切换到已有模型'
3072
- });
3073
- }
3074
- }
3075
-
3076
- const remoteEnabled = !!params.remote;
3077
- let remote = null;
3078
- if (remoteEnabled) {
3079
- const baseUrl = provider && typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3080
- if (!provider) {
3081
- issues.push({
3082
- code: 'remote-skip-provider',
3083
- message: '无法进行远程探测:provider 未找到',
3084
- suggestion: '检查 model_provider 配置或关闭远程探测'
3085
- });
3086
- } else if (!isValidHttpUrl(baseUrl)) {
3087
- issues.push({
3088
- code: 'remote-skip-base-url',
3089
- message: '无法进行远程探测:base_url 无效',
3090
- suggestion: '补全 base_url 或关闭远程探测'
3091
- });
3092
- } else {
3093
- const timeoutMs = Number.isFinite(params.timeoutMs)
3094
- ? Math.max(1000, Number(params.timeoutMs))
3095
- : undefined;
3096
- const apiKey = typeof provider.preferred_auth_method === 'string'
3097
- ? provider.preferred_auth_method.trim()
3098
- : '';
3099
- const speedResult = await runSpeedTest(baseUrl, apiKey, { timeoutMs });
3100
- const status = speedResult && typeof speedResult.status === 'number'
3101
- ? speedResult.status
3102
- : 0;
3103
- const durationMs = speedResult && typeof speedResult.durationMs === 'number'
3104
- ? speedResult.durationMs
3105
- : 0;
3106
- const error = speedResult && speedResult.error ? String(speedResult.error) : '';
3107
- remote = {
3108
- type: 'speed-test',
3109
- url: baseUrl,
3110
- ok: !!speedResult.ok,
3111
- status,
3112
- durationMs,
3113
- error
3114
- };
3115
-
3116
- if (!speedResult.ok) {
3117
- const errorLower = error.toLowerCase();
3118
- if (errorLower.includes('timeout')) {
3119
- issues.push({
3120
- code: 'remote-speedtest-timeout',
3121
- message: '远程测速超时',
3122
- suggestion: '检查网络或 base_url 是否可达'
3123
- });
3124
- } else if (errorLower.includes('invalid url')) {
3125
- issues.push({
3126
- code: 'remote-speedtest-invalid-url',
3127
- message: '远程测速失败:base_url 无效',
3128
- suggestion: '请设置为 http/https 的完整 URL'
3129
- });
3130
- } else {
3131
- issues.push({
3132
- code: 'remote-speedtest-unreachable',
3133
- message: `远程测速失败:${error || '无法连接'}`,
3134
- suggestion: '检查网络或 base_url 是否可用'
3135
- });
3136
- }
3137
- } else if (status === 401 || status === 403) {
3138
- issues.push({
3139
- code: 'remote-speedtest-auth-failed',
3140
- message: '远程测速鉴权失败(401/403)',
3141
- suggestion: '检查 API Key 或认证方式'
3142
- });
3143
- } else if (status >= 400) {
3144
- issues.push({
3145
- code: 'remote-speedtest-http-error',
3146
- message: `远程测速返回异常状态: ${status}`,
3147
- suggestion: '检查 base_url 或服务状态'
3148
- });
3149
- }
3150
- }
3151
- }
3152
-
3153
- return {
3154
- ok: issues.length === 0,
3155
- issues,
3156
- summary: {
3157
- currentProvider: providerName,
3158
- currentModel: modelName
3159
- },
3160
- remote
3161
- };
3084
+ return buildConfigHealthReportCore(params, {
3085
+ readConfigOrVirtualDefault,
3086
+ readModels
3087
+ });
3162
3088
  }
3163
3089
 
3164
3090
  function buildDefaultConfigContent(initializedAt) {
@@ -3168,6 +3094,8 @@ function buildDefaultConfigContent(initializedAt) {
3168
3094
 
3169
3095
  model_provider = "openai"
3170
3096
  model = "${defaultModel}"
3097
+ model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
3098
+ model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
3171
3099
 
3172
3100
  [model_providers.openai]
3173
3101
  name = "openai"
@@ -3333,6 +3261,45 @@ function applyReasoningEffortToTemplate(template, reasoningEffort) {
3333
3261
  return content;
3334
3262
  }
3335
3263
 
3264
+ function normalizePositiveIntegerParam(value) {
3265
+ if (value === undefined || value === null) {
3266
+ return null;
3267
+ }
3268
+ const text = typeof value === 'number'
3269
+ ? String(value)
3270
+ : (typeof value === 'string' ? value.trim() : String(value).trim());
3271
+ if (!text) {
3272
+ return null;
3273
+ }
3274
+ if (!/^\d+$/.test(text)) {
3275
+ return null;
3276
+ }
3277
+ const parsed = Number.parseInt(text, 10);
3278
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
3279
+ return null;
3280
+ }
3281
+ return parsed;
3282
+ }
3283
+
3284
+ function applyPositiveIntegerConfigToTemplate(template, key, value) {
3285
+ let content = typeof template === 'string' ? template : '';
3286
+ const normalized = normalizePositiveIntegerParam(value);
3287
+ if (!key || normalized === null) {
3288
+ return content;
3289
+ }
3290
+
3291
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
3292
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
3293
+ if (hasBom) {
3294
+ content = content.slice(1);
3295
+ }
3296
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3297
+ const pattern = new RegExp(`^\\s*${escapedKey}\\s*=\\s*[^\\n]*\\n?`, 'gmi');
3298
+ content = content.replace(pattern, '');
3299
+ content = content.replace(new RegExp(`^(?:[\\t ]*${lineEnding})+`), '');
3300
+ return `${hasBom ? '\uFEFF' : ''}${key} = ${normalized}${lineEnding}${content}`;
3301
+ }
3302
+
3336
3303
  function getConfigTemplate(params = {}) {
3337
3304
  let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
3338
3305
  if (fs.existsSync(CONFIG_FILE)) {
@@ -3343,6 +3310,20 @@ function getConfigTemplate(params = {}) {
3343
3310
  }
3344
3311
  } catch (e) {}
3345
3312
  }
3313
+ if (
3314
+ params.modelAutoCompactTokenLimit !== undefined
3315
+ && params.modelAutoCompactTokenLimit !== null
3316
+ && normalizePositiveIntegerParam(params.modelAutoCompactTokenLimit) === null
3317
+ ) {
3318
+ return { error: 'modelAutoCompactTokenLimit must be a positive integer' };
3319
+ }
3320
+ if (
3321
+ params.modelContextWindow !== undefined
3322
+ && params.modelContextWindow !== null
3323
+ && normalizePositiveIntegerParam(params.modelContextWindow) === null
3324
+ ) {
3325
+ return { error: 'modelContextWindow must be a positive integer' };
3326
+ }
3346
3327
  const selectedProvider = typeof params.provider === 'string' ? params.provider.trim() : '';
3347
3328
  const selectedModel = typeof params.model === 'string' ? params.model.trim() : '';
3348
3329
  let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel);
@@ -3352,11 +3333,54 @@ function getConfigTemplate(params = {}) {
3352
3333
  if (typeof params.reasoningEffort === 'string') {
3353
3334
  template = applyReasoningEffortToTemplate(template, params.reasoningEffort);
3354
3335
  }
3336
+ if (!/^\s*model_auto_compact_token_limit\s*=.*$/m.test(template)) {
3337
+ template = applyPositiveIntegerConfigToTemplate(
3338
+ template,
3339
+ 'model_auto_compact_token_limit',
3340
+ DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT
3341
+ );
3342
+ }
3343
+ if (!/^\s*model_context_window\s*=.*$/m.test(template)) {
3344
+ template = applyPositiveIntegerConfigToTemplate(
3345
+ template,
3346
+ 'model_context_window',
3347
+ DEFAULT_MODEL_CONTEXT_WINDOW
3348
+ );
3349
+ }
3350
+ if (params.modelAutoCompactTokenLimit !== undefined) {
3351
+ template = applyPositiveIntegerConfigToTemplate(
3352
+ template,
3353
+ 'model_auto_compact_token_limit',
3354
+ params.modelAutoCompactTokenLimit
3355
+ );
3356
+ }
3357
+ if (params.modelContextWindow !== undefined) {
3358
+ template = applyPositiveIntegerConfigToTemplate(
3359
+ template,
3360
+ 'model_context_window',
3361
+ params.modelContextWindow
3362
+ );
3363
+ }
3355
3364
  return {
3356
3365
  template
3357
3366
  };
3358
3367
  }
3359
3368
 
3369
+ function readPositiveIntegerConfigValue(config, key) {
3370
+ const options = arguments[2] && typeof arguments[2] === 'object' ? arguments[2] : {};
3371
+ const useDefaultsWhenMissing = options.useDefaultsWhenMissing !== false;
3372
+ if (!config || typeof config !== 'object' || !key) {
3373
+ return '';
3374
+ }
3375
+ const raw = config[key];
3376
+ if (raw === undefined && useDefaultsWhenMissing) {
3377
+ if (key === 'model_context_window') return DEFAULT_MODEL_CONTEXT_WINDOW;
3378
+ if (key === 'model_auto_compact_token_limit') return DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT;
3379
+ }
3380
+ const normalized = normalizePositiveIntegerParam(raw);
3381
+ return normalized === null ? '' : normalized;
3382
+ }
3383
+
3360
3384
  function applyConfigTemplate(params = {}) {
3361
3385
  const template = typeof params.template === 'string' ? params.template : '';
3362
3386
  if (!template.trim()) {
@@ -3370,6 +3394,20 @@ function applyConfigTemplate(params = {}) {
3370
3394
  return { error: `模板 TOML 解析失败: ${e.message}` };
3371
3395
  }
3372
3396
 
3397
+ if (
3398
+ Object.prototype.hasOwnProperty.call(parsed, 'model_context_window')
3399
+ && normalizePositiveIntegerParam(parsed.model_context_window) === null
3400
+ ) {
3401
+ return { error: '模板中的 model_context_window 必须是正整数' };
3402
+ }
3403
+
3404
+ if (
3405
+ Object.prototype.hasOwnProperty.call(parsed, 'model_auto_compact_token_limit')
3406
+ && normalizePositiveIntegerParam(parsed.model_auto_compact_token_limit) === null
3407
+ ) {
3408
+ return { error: '模板中的 model_auto_compact_token_limit 必须是正整数' };
3409
+ }
3410
+
3373
3411
  if (!parsed.model_provider || typeof parsed.model_provider !== 'string') {
3374
3412
  return { error: '模板缺少 model_provider' };
3375
3413
  }
@@ -3836,7 +3874,7 @@ function normalizePathForCompare(targetPath, options = {}) {
3836
3874
  return ignoreCase ? resolved.toLowerCase() : resolved;
3837
3875
  }
3838
3876
 
3839
- function isPathInside(targetPath, rootPath) {
3877
+ function isPathInside(targetPath, rootPath) {
3840
3878
  if (!targetPath || !rootPath) {
3841
3879
  return false;
3842
3880
  }
@@ -3846,30 +3884,33 @@ function isPathInside(targetPath, rootPath) {
3846
3884
  if (resolvedTarget === resolvedRoot) {
3847
3885
  return true;
3848
3886
  }
3849
- const rootWithSlash = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
3850
- return resolvedTarget.startsWith(rootWithSlash);
3851
- }
3852
-
3853
- function resolveCopyTargetRoot(targetDir) {
3854
- const suffixSegments = [];
3855
- let current = path.resolve(targetDir || '');
3856
- while (current && !fs.existsSync(current)) {
3857
- const parent = path.dirname(current);
3858
- if (!parent || parent === current) {
3859
- break;
3860
- }
3861
- suffixSegments.unshift(path.basename(current));
3862
- current = parent;
3863
- }
3864
- let resolvedRoot = normalizePathForCompare(current || targetDir);
3865
- if (!resolvedRoot) {
3866
- resolvedRoot = path.resolve(targetDir || '');
3867
- }
3868
- for (const segment of suffixSegments) {
3869
- resolvedRoot = path.join(resolvedRoot, segment);
3870
- }
3871
- return resolvedRoot;
3872
- }
3887
+ const separator = resolvedRoot.includes('/') && !resolvedRoot.includes('\\') ? '/' : path.sep;
3888
+ const rootWithSlash = resolvedRoot.endsWith(separator) ? resolvedRoot : resolvedRoot + separator;
3889
+ return resolvedTarget.startsWith(rootWithSlash);
3890
+ }
3891
+
3892
+ function resolveCopyTargetRoot(targetDir) {
3893
+ const base = typeof targetDir === 'string' ? targetDir.trim() : '';
3894
+ const pathApi = base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
3895
+ const suffixSegments = [];
3896
+ let current = pathApi.resolve(base || '');
3897
+ while (current && !fs.existsSync(current)) {
3898
+ const parent = pathApi.dirname(current);
3899
+ if (!parent || parent === current) {
3900
+ break;
3901
+ }
3902
+ suffixSegments.unshift(pathApi.basename(current));
3903
+ current = parent;
3904
+ }
3905
+ let resolvedRoot = normalizePathForCompare(current || base);
3906
+ if (!resolvedRoot) {
3907
+ resolvedRoot = pathApi.resolve(base || '');
3908
+ }
3909
+ for (const segment of suffixSegments) {
3910
+ resolvedRoot = pathApi.join(resolvedRoot, segment);
3911
+ }
3912
+ return resolvedRoot;
3913
+ }
3873
3914
 
3874
3915
  function collectJsonlFiles(rootDir, maxFiles = 5000) {
3875
3916
  if (!fs.existsSync(rootDir)) {
@@ -4642,19 +4683,97 @@ function extractMessageFromRecord(record, source) {
4642
4683
  return null;
4643
4684
  }
4644
4685
 
4645
- const role = normalizeRole(record.type);
4646
- if (!role) {
4647
- return null;
4686
+ const role = normalizeRole(record.type);
4687
+ if (!role) {
4688
+ return null;
4689
+ }
4690
+ const content = record.message ? record.message.content : '';
4691
+ const text = extractMessageText(content);
4692
+ if (!text) {
4693
+ return null;
4694
+ }
4695
+ return { role, text };
4696
+ }
4697
+
4698
+ function createSessionQueryScanState(tokens, options = {}) {
4699
+ const mode = normalizeQueryMode(options.mode);
4700
+ const roleFilter = normalizeRoleFilter(options.roleFilter);
4701
+ const maxMatches = Number.isFinite(Number(options.maxMatches))
4702
+ ? Math.max(1, Number(options.maxMatches))
4703
+ : 1;
4704
+ const snippetLimit = Number.isFinite(Number(options.snippetLimit))
4705
+ ? Math.max(0, Number(options.snippetLimit))
4706
+ : 0;
4707
+
4708
+ return {
4709
+ tokens,
4710
+ mode,
4711
+ roleFilter,
4712
+ maxMatches,
4713
+ snippetLimit,
4714
+ count: 0,
4715
+ snippets: [],
4716
+ leadingSystem: roleFilter !== 'system'
4717
+ };
4718
+ }
4719
+
4720
+ function consumeSessionQueryMessage(state, message) {
4721
+ if (!state || typeof state !== 'object' || !message) {
4722
+ return false;
4723
+ }
4724
+
4725
+ const role = normalizeRole(message.role);
4726
+ const text = typeof message.text === 'string' ? message.text : '';
4727
+ if (!role || !text) {
4728
+ return false;
4729
+ }
4730
+
4731
+ if (state.leadingSystem && (role === 'system' || isBootstrapLikeText(text))) {
4732
+ return false;
4733
+ }
4734
+ state.leadingSystem = false;
4735
+
4736
+ if (state.roleFilter !== 'all' && role !== state.roleFilter) {
4737
+ return false;
4738
+ }
4739
+ if (!matchTokensInText(text, state.tokens, state.mode)) {
4740
+ return false;
4741
+ }
4742
+
4743
+ state.count += 1;
4744
+ if (state.snippetLimit > 0 && state.snippets.length < state.snippetLimit) {
4745
+ state.snippets.push(truncateText(text));
4648
4746
  }
4649
- const content = record.message ? record.message.content : '';
4650
- const text = extractMessageText(content);
4651
- if (!text) {
4652
- return null;
4747
+ return state.count >= state.maxMatches;
4748
+ }
4749
+
4750
+ function buildSessionQueryScanResult(state) {
4751
+ return {
4752
+ hit: !!(state && state.count > 0),
4753
+ count: state && Number.isFinite(state.count) ? state.count : 0,
4754
+ snippets: state && Array.isArray(state.snippets) ? state.snippets : []
4755
+ };
4756
+ }
4757
+
4758
+ function scanSessionContentForQueryInRecords(records, source, state) {
4759
+ if (!Array.isArray(records) || !state) {
4760
+ return buildSessionQueryScanResult(state);
4653
4761
  }
4654
- return { role, text };
4762
+
4763
+ for (const record of records) {
4764
+ const message = extractMessageFromRecord(record, source);
4765
+ if (!message) {
4766
+ continue;
4767
+ }
4768
+ if (consumeSessionQueryMessage(state, message)) {
4769
+ break;
4770
+ }
4771
+ }
4772
+
4773
+ return buildSessionQueryScanResult(state);
4655
4774
  }
4656
4775
 
4657
- function scanSessionContentForQuery(session, tokens, options = {}) {
4776
+ async function scanSessionContentForQuery(session, tokens, options = {}) {
4658
4777
  if (!session || !Array.isArray(tokens) || tokens.length === 0) {
4659
4778
  return { hit: false, count: 0, snippets: [] };
4660
4779
  }
@@ -4664,61 +4783,59 @@ function scanSessionContentForQuery(session, tokens, options = {}) {
4664
4783
  return { hit: false, count: 0, snippets: [] };
4665
4784
  }
4666
4785
 
4667
- const maxBytes = Number.isFinite(Number(options.maxBytes))
4668
- ? Math.max(1024, Number(options.maxBytes))
4669
- : SESSION_CONTENT_READ_BYTES;
4670
- const headText = getFileHeadText(filePath, maxBytes);
4671
- if (!headText) {
4672
- return { hit: false, count: 0, snippets: [] };
4673
- }
4674
-
4675
- const records = parseJsonlContent(headText);
4676
- const mode = normalizeQueryMode(options.mode);
4677
- const roleFilter = normalizeRoleFilter(options.roleFilter);
4678
- const maxMatches = Number.isFinite(Number(options.maxMatches))
4679
- ? Math.max(1, Number(options.maxMatches))
4680
- : 1;
4681
- const snippetLimit = Number.isFinite(Number(options.snippetLimit))
4682
- ? Math.max(0, Number(options.snippetLimit))
4786
+ const rawMaxBytes = Number(options.maxBytes);
4787
+ const maxBytes = Number.isFinite(rawMaxBytes) && rawMaxBytes > 0
4788
+ ? Math.max(1024, rawMaxBytes)
4683
4789
  : 0;
4790
+ const state = createSessionQueryScanState(tokens, options);
4791
+ let stream;
4792
+ let rl;
4793
+ try {
4794
+ stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
4795
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4684
4796
 
4685
- const messages = [];
4686
- for (const record of records) {
4687
- const message = extractMessageFromRecord(record, session.source);
4688
- if (!message || !message.text) {
4689
- continue;
4690
- }
4691
- messages.push(message);
4692
- }
4797
+ let bytesRead = 0;
4798
+ for await (const line of rl) {
4799
+ if (maxBytes > 0 && bytesRead >= maxBytes) {
4800
+ break;
4801
+ }
4693
4802
 
4694
- const filteredMessages = roleFilter === 'system'
4695
- ? messages
4696
- : removeLeadingSystemMessage(messages);
4803
+ bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
4804
+ const trimmed = line.trim();
4805
+ if (!trimmed) {
4806
+ continue;
4807
+ }
4697
4808
 
4698
- let count = 0;
4699
- const snippets = [];
4809
+ let record;
4810
+ try {
4811
+ record = JSON.parse(trimmed);
4812
+ } catch (e) {
4813
+ continue;
4814
+ }
4700
4815
 
4701
- for (const message of filteredMessages) {
4702
- if (roleFilter !== 'all' && message.role !== roleFilter) {
4703
- continue;
4704
- }
4705
- if (!matchTokensInText(message.text, tokens, mode)) {
4706
- continue;
4816
+ const message = extractMessageFromRecord(record, session.source);
4817
+ if (!message) {
4818
+ continue;
4819
+ }
4820
+ if (consumeSessionQueryMessage(state, message)) {
4821
+ break;
4822
+ }
4707
4823
  }
4708
4824
 
4709
- count += 1;
4710
- if (snippetLimit > 0 && snippets.length < snippetLimit) {
4711
- snippets.push(truncateText(message.text));
4825
+ return buildSessionQueryScanResult(state);
4826
+ } catch (e) {
4827
+ return scanSessionContentForQueryInRecords(readJsonlRecords(filePath), session.source, state);
4828
+ } finally {
4829
+ if (rl) {
4830
+ try { rl.close(); } catch (e) {}
4712
4831
  }
4713
- if (count >= maxMatches) {
4714
- break;
4832
+ if (stream && !stream.destroyed && stream.destroy) {
4833
+ try { stream.destroy(); } catch (e) {}
4715
4834
  }
4716
4835
  }
4717
-
4718
- return { hit: count > 0, count, snippets };
4719
4836
  }
4720
4837
 
4721
- function applySessionQueryFilter(sessions, options = {}) {
4838
+ async function applySessionQueryFilter(sessions, options = {}) {
4722
4839
  const tokens = Array.isArray(options.tokens) ? options.tokens : [];
4723
4840
  if (tokens.length === 0) {
4724
4841
  return sessions;
@@ -4732,7 +4849,7 @@ function applySessionQueryFilter(sessions, options = {}) {
4732
4849
  : DEFAULT_CONTENT_SCAN_LIMIT;
4733
4850
  const contentScanBytes = Number.isFinite(Number(options.contentScanBytes))
4734
4851
  ? Math.max(1024, Number(options.contentScanBytes))
4735
- : SESSION_CONTENT_READ_BYTES;
4852
+ : 0;
4736
4853
 
4737
4854
  let scanned = 0;
4738
4855
  const results = [];
@@ -4750,7 +4867,7 @@ function applySessionQueryFilter(sessions, options = {}) {
4750
4867
  const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
4751
4868
  if (shouldScanContent && scanned < contentScanLimit) {
4752
4869
  scanned += 1;
4753
- contentInfo = scanSessionContentForQuery(session, tokens, {
4870
+ contentInfo = await scanSessionContentForQuery(session, tokens, {
4754
4871
  mode,
4755
4872
  roleFilter,
4756
4873
  maxBytes: contentScanBytes,
@@ -5225,7 +5342,7 @@ function listClaudeSessions(limit, options = {}) {
5225
5342
  return mergeAndLimitSessions(sessions, limit);
5226
5343
  }
5227
5344
 
5228
- function listAllSessions(params = {}) {
5345
+ async function listAllSessions(params = {}) {
5229
5346
  const source = params.source === 'codex' || params.source === 'claude'
5230
5347
  ? params.source
5231
5348
  : 'all';
@@ -5267,7 +5384,7 @@ function listAllSessions(params = {}) {
5267
5384
 
5268
5385
  let result = sessions;
5269
5386
  if (hasQuery) {
5270
- result = applySessionQueryFilter(result, {
5387
+ result = await applySessionQueryFilter(result, {
5271
5388
  tokens: queryTokens,
5272
5389
  queryMode: params.queryMode,
5273
5390
  queryScope: params.queryScope,
@@ -5303,7 +5420,7 @@ async function listAllSessionsData(params = {}) {
5303
5420
  }
5304
5421
  }
5305
5422
 
5306
- const sessions = listAllSessions(params);
5423
+ const sessions = await listAllSessions(params);
5307
5424
  const hydratedSessions = await hydrateSessionItemsExactMessageCount(sessions);
5308
5425
  const result = hydratedSessions.map((item) => {
5309
5426
  if (!item || typeof item !== 'object' || Array.isArray(item)) {
@@ -7480,63 +7597,210 @@ function resolveSpeedTestTarget(params) {
7480
7597
  if (!provider.base_url) {
7481
7598
  return { error: 'Provider missing URL' };
7482
7599
  }
7600
+ const currentModel = typeof config.model === 'string' ? config.model.trim() : '';
7601
+ const probeSpec = buildModelProbeSpec(provider, currentModel, provider.base_url);
7602
+ if (probeSpec && probeSpec.url) {
7603
+ return {
7604
+ method: 'POST',
7605
+ url: probeSpec.url,
7606
+ body: probeSpec.body,
7607
+ apiKey: provider.preferred_auth_method || ''
7608
+ };
7609
+ }
7483
7610
  return {
7611
+ method: 'GET',
7484
7612
  url: provider.base_url,
7485
7613
  apiKey: provider.preferred_auth_method || ''
7486
7614
  };
7487
7615
  }
7488
7616
 
7489
7617
  if (params.url) {
7490
- return { url: params.url, apiKey: '' };
7618
+ return {
7619
+ method: 'GET',
7620
+ url: params.url,
7621
+ apiKey: typeof params.apiKey === 'string' ? params.apiKey : ''
7622
+ };
7491
7623
  }
7492
7624
 
7493
7625
  return { error: 'Missing name or url' };
7494
7626
  }
7495
7627
 
7496
- function runSpeedTest(targetUrl, apiKey, options = {}) {
7497
- return new Promise((resolve) => {
7498
- let parsed;
7499
- try {
7500
- parsed = new URL(targetUrl);
7501
- } catch (e) {
7502
- return resolve({ ok: false, error: 'Invalid URL' });
7503
- }
7628
+ function extractApiPayloadErrorMessage(payload) {
7629
+ if (!payload || typeof payload !== 'object') {
7630
+ return '';
7631
+ }
7632
+ if (typeof payload.error === 'string' && payload.error.trim()) {
7633
+ return payload.error.trim();
7634
+ }
7635
+ if (!payload.error || typeof payload.error !== 'object') {
7636
+ return '';
7637
+ }
7638
+ if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
7639
+ return payload.error.message.trim();
7640
+ }
7641
+ if (typeof payload.error.code === 'string' && payload.error.code.trim()) {
7642
+ return payload.error.code.trim();
7643
+ }
7644
+ return '';
7645
+ }
7646
+
7647
+ function resolveProviderChatTarget(params) {
7648
+ const providerName = typeof (params && params.name) === 'string' ? params.name.trim() : '';
7649
+ const prompt = typeof (params && params.prompt) === 'string' ? params.prompt.trim() : '';
7650
+ if (!providerName) {
7651
+ return { error: 'Provider name is required' };
7652
+ }
7653
+ if (!prompt) {
7654
+ return { error: 'Prompt is required' };
7655
+ }
7504
7656
 
7505
- const timeoutMs = Number.isFinite(options.timeoutMs)
7506
- ? Math.max(1000, Number(options.timeoutMs))
7507
- : SPEED_TEST_TIMEOUT_MS;
7657
+ const { config } = readConfigOrVirtualDefault();
7658
+ const providers = config.model_providers || {};
7659
+ const provider = providers[providerName];
7660
+ if (!provider || typeof provider !== 'object') {
7661
+ return { error: `Provider not found: ${providerName}` };
7662
+ }
7508
7663
 
7509
- const transport = parsed.protocol === 'https:' ? https : http;
7510
- const headers = {
7511
- 'User-Agent': 'codexmate-speed-test',
7512
- 'Accept': 'application/json'
7513
- };
7514
- if (apiKey) {
7515
- headers['Authorization'] = `Bearer ${apiKey}`;
7516
- }
7664
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
7665
+ if (!baseUrl) {
7666
+ return { error: `Provider ${providerName} missing URL` };
7667
+ }
7517
7668
 
7518
- const start = Date.now();
7519
- const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
7520
- res.on('data', () => {});
7521
- res.on('end', () => {
7522
- resolve({
7523
- ok: true,
7524
- status: res.statusCode || 0,
7525
- durationMs: Date.now() - start
7526
- });
7527
- });
7528
- });
7669
+ const currentModels = readCurrentModels();
7670
+ const savedModel = currentModels && typeof currentModels[providerName] === 'string'
7671
+ ? currentModels[providerName].trim()
7672
+ : '';
7673
+ const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
7674
+ const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
7675
+ const model = savedModel || (activeProvider === providerName ? activeModel : '');
7676
+ if (!model) {
7677
+ return { error: `Provider ${providerName} missing current model` };
7678
+ }
7529
7679
 
7530
- req.setTimeout(timeoutMs, () => {
7531
- req.destroy(new Error('timeout'));
7532
- });
7680
+ const specs = buildModelConversationSpecs(provider, model, baseUrl, prompt, {
7681
+ maxOutputTokens: 256
7682
+ });
7683
+ if (!specs.length) {
7684
+ return { error: `Provider ${providerName} missing available conversation endpoint` };
7685
+ }
7686
+
7687
+ return {
7688
+ providerName,
7689
+ provider,
7690
+ model,
7691
+ prompt,
7692
+ specs,
7693
+ apiKey: typeof provider.preferred_auth_method === 'string'
7694
+ ? provider.preferred_auth_method.trim()
7695
+ : ''
7696
+ };
7697
+ }
7698
+
7699
+ async function runProviderChatCheck(params = {}) {
7700
+ const target = resolveProviderChatTarget(params);
7701
+ if (target.error) {
7702
+ return { ok: false, error: target.error };
7703
+ }
7704
+
7705
+ const timeoutMs = Number.isFinite(params.timeoutMs)
7706
+ ? Math.max(1000, Number(params.timeoutMs))
7707
+ : 30000;
7708
+ let finalSpec = target.specs[0];
7709
+ let result = null;
7533
7710
 
7534
- req.on('error', (err) => {
7535
- resolve({ ok: false, error: err.message, durationMs: Date.now() - start });
7711
+ for (let index = 0; index < target.specs.length; index += 1) {
7712
+ const candidate = target.specs[index];
7713
+ const probeResult = await probeJsonPost(candidate.url, candidate.body, {
7714
+ apiKey: target.apiKey,
7715
+ timeoutMs,
7716
+ maxBytes: 512 * 1024
7536
7717
  });
7718
+ finalSpec = candidate;
7719
+ result = probeResult;
7720
+ const shouldTryNextCandidate = index < target.specs.length - 1
7721
+ && (!probeResult.ok || probeResult.status === 404);
7722
+ if (!shouldTryNextCandidate) {
7723
+ break;
7724
+ }
7725
+ }
7537
7726
 
7538
- req.end();
7539
- });
7727
+ if (!result || !result.ok) {
7728
+ return {
7729
+ ok: false,
7730
+ provider: target.providerName,
7731
+ model: target.model,
7732
+ url: finalSpec.url,
7733
+ status: Number.isFinite(result && result.status) ? result.status : 0,
7734
+ durationMs: Number.isFinite(result && result.durationMs) ? result.durationMs : 0,
7735
+ reply: '',
7736
+ rawPreview: '',
7737
+ error: result && result.error ? result.error : 'request failed'
7738
+ };
7739
+ }
7740
+
7741
+ let payload = null;
7742
+ try {
7743
+ payload = result.body ? JSON.parse(result.body) : null;
7744
+ } catch (e) {
7745
+ payload = null;
7746
+ }
7747
+
7748
+ const payloadError = extractApiPayloadErrorMessage(payload);
7749
+ if (result.status >= 400 || payloadError) {
7750
+ return {
7751
+ ok: false,
7752
+ provider: target.providerName,
7753
+ model: target.model,
7754
+ url: finalSpec.url,
7755
+ status: Number.isFinite(result.status) ? result.status : 0,
7756
+ durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
7757
+ reply: '',
7758
+ rawPreview: result.body ? truncateText(result.body, 600) : '',
7759
+ error: payloadError || `HTTP ${result.status}`
7760
+ };
7761
+ }
7762
+
7763
+ const reply = extractModelResponseText(payload);
7764
+ return {
7765
+ ok: true,
7766
+ provider: target.providerName,
7767
+ model: target.model,
7768
+ url: finalSpec.url,
7769
+ status: Number.isFinite(result.status) ? result.status : 0,
7770
+ durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
7771
+ reply,
7772
+ rawPreview: reply ? '' : (result.body ? truncateText(result.body, 600) : ''),
7773
+ error: ''
7774
+ };
7775
+ }
7776
+
7777
+ function runSpeedTest(targetUrl, apiKey, options = {}) {
7778
+ const timeoutMs = Number.isFinite(options.timeoutMs)
7779
+ ? Math.max(1000, Number(options.timeoutMs))
7780
+ : SPEED_TEST_TIMEOUT_MS;
7781
+ const method = typeof options.method === 'string' ? options.method.toUpperCase() : 'GET';
7782
+ if (method === 'POST') {
7783
+ return probeJsonPost(targetUrl, options.body || {}, {
7784
+ apiKey,
7785
+ timeoutMs,
7786
+ maxBytes: 256 * 1024
7787
+ }).then((result) => ({
7788
+ ok: !!result.ok,
7789
+ status: Number.isFinite(result.status) ? result.status : 0,
7790
+ durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
7791
+ error: result.ok ? '' : (result.error || '')
7792
+ }));
7793
+ }
7794
+ return probeUrl(targetUrl, {
7795
+ apiKey,
7796
+ timeoutMs,
7797
+ maxBytes: 256 * 1024
7798
+ }).then((result) => ({
7799
+ ok: !!result.ok,
7800
+ status: Number.isFinite(result.status) ? result.status : 0,
7801
+ durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0,
7802
+ error: result.ok ? '' : (result.error || '')
7803
+ }));
7540
7804
  }
7541
7805
 
7542
7806
  // ============================================================================
@@ -9753,10 +10017,19 @@ function formatHostForUrl(host) {
9753
10017
  return value;
9754
10018
  }
9755
10019
 
9756
- function watchPathsForRestart(targets, onChange) {
9757
- const disposers = [];
9758
- const debounceMs = 300;
9759
- let timer = null;
10020
+ // #region watchPathsForRestart
10021
+ function watchPathsForRestart(targets, onChange) {
10022
+ const debounceMs = 300;
10023
+ let timer = null;
10024
+ const watcherEntries = new Map();
10025
+ const getPathApi = (targetPath) => {
10026
+ const value = typeof targetPath === 'string' ? targetPath.trim() : '';
10027
+ return value.includes('/') && !value.includes('\\') && path.posix ? path.posix : path;
10028
+ };
10029
+ const getPathSeparator = (targetPath) => {
10030
+ const pathApi = getPathApi(targetPath);
10031
+ return pathApi.sep || (pathApi === path.posix ? '/' : path.sep);
10032
+ };
9760
10033
 
9761
10034
  const trigger = (info) => {
9762
10035
  if (timer) clearTimeout(timer);
@@ -9766,35 +10039,205 @@ function watchPathsForRestart(targets, onChange) {
9766
10039
  }, debounceMs);
9767
10040
  };
9768
10041
 
9769
- const addWatcher = (target, recursive) => {
9770
- if (!fs.existsSync(target)) return;
10042
+ const closeWatcher = (watchKey) => {
10043
+ const entry = watcherEntries.get(watchKey);
10044
+ if (!entry) return;
10045
+ watcherEntries.delete(watchKey);
9771
10046
  try {
9772
- const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
10047
+ entry.watcher.close();
10048
+ } catch (_) {}
10049
+ };
10050
+
10051
+ const listDirectoryTree = (rootDir) => {
10052
+ const queue = [rootDir];
10053
+ const directories = [];
10054
+ const seen = new Set();
10055
+ const pathApi = getPathApi(rootDir);
10056
+ while (queue.length) {
10057
+ const current = queue.shift();
10058
+ if (!current || seen.has(current) || !fs.existsSync(current)) {
10059
+ continue;
10060
+ }
10061
+ seen.add(current);
10062
+ let stat = null;
10063
+ try {
10064
+ stat = fs.statSync(current);
10065
+ } catch (_) {
10066
+ continue;
10067
+ }
10068
+ if (!stat || !stat.isDirectory()) {
10069
+ continue;
10070
+ }
10071
+ directories.push(current);
10072
+ let entries = [];
10073
+ try {
10074
+ entries = fs.readdirSync(current, { withFileTypes: true });
10075
+ } catch (_) {
10076
+ continue;
10077
+ }
10078
+ for (const entry of entries) {
10079
+ if (entry && typeof entry.isDirectory === 'function' && entry.isDirectory()) {
10080
+ queue.push(pathApi.join(current, entry.name));
10081
+ }
10082
+ }
10083
+ }
10084
+ return directories;
10085
+ };
10086
+
10087
+ const isSameOrNestedPath = (candidate, rootDir) => {
10088
+ const separator = getPathSeparator(rootDir);
10089
+ return candidate === rootDir || candidate.startsWith(`${rootDir}${separator}`);
10090
+ };
10091
+
10092
+ const addWatcher = (target, recursive, isDirectory = false) => {
10093
+ if (!fs.existsSync(target)) return;
10094
+ const watchKey = `${recursive ? 'recursive' : 'plain'}:${target}`;
10095
+ if (watcherEntries.has(watchKey)) {
10096
+ return true;
10097
+ }
10098
+ try {
10099
+ const pathApi = getPathApi(target);
10100
+ const basename = isDirectory ? '' : pathApi.basename(target);
10101
+ const watchTarget = isDirectory ? target : pathApi.dirname(target);
10102
+ const watcher = fs.watch(watchTarget, { recursive }, (eventType, filename) => {
10103
+ if (isDirectory && !recursive && eventType === 'rename') {
10104
+ syncDirectoryTree(target);
10105
+ }
9773
10106
  if (!filename) return;
9774
- const lower = filename.toLowerCase();
9775
- if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
9776
- trigger({ target, eventType, filename });
10107
+ let normalizedFilename = String(filename).replace(/\\/g, '/');
10108
+ if (!isDirectory) {
10109
+ const fileNameOnly = normalizedFilename.split('/').pop();
10110
+ if (fileNameOnly !== basename) {
10111
+ return;
10112
+ }
10113
+ normalizedFilename = basename;
10114
+ }
10115
+ const lower = normalizedFilename.toLowerCase();
10116
+ if (!(/\.(html|js|mjs|cjs|css)$/.test(lower))) return;
10117
+ trigger({ target, eventType, filename: normalizedFilename });
10118
+ });
10119
+ watcher.on('error', () => {
10120
+ closeWatcher(watchKey);
10121
+ if (isDirectory && recursive && !fs.existsSync(target)) {
10122
+ syncDirectoryTree(target);
10123
+ addMissingDirectoryWatcher(target);
10124
+ return;
10125
+ }
10126
+ if (isDirectory && !recursive) {
10127
+ syncDirectoryTree(target);
10128
+ } else if (fs.existsSync(target)) {
10129
+ addWatcher(target, recursive, isDirectory);
10130
+ }
10131
+ });
10132
+ watcherEntries.set(watchKey, {
10133
+ watcher,
10134
+ target,
10135
+ recursive,
10136
+ isDirectory
9777
10137
  });
9778
- disposers.push(() => watcher.close());
9779
10138
  return true;
9780
10139
  } catch (e) {
9781
10140
  return false;
9782
10141
  }
9783
10142
  };
9784
10143
 
10144
+ const addMissingDirectoryWatcher = (target) => {
10145
+ const pathApi = getPathApi(target);
10146
+ const parentDir = pathApi.dirname(target);
10147
+ if (!parentDir || parentDir === target || !fs.existsSync(parentDir)) {
10148
+ return false;
10149
+ }
10150
+ const watchKey = `missing-dir:${target}`;
10151
+ if (watcherEntries.has(watchKey)) {
10152
+ return true;
10153
+ }
10154
+ const basename = path.basename(target);
10155
+ try {
10156
+ const watcher = fs.watch(parentDir, { recursive: false }, (_eventType, filename) => {
10157
+ if (!filename) return;
10158
+ const fileNameOnly = String(filename).replace(/\\/g, '/').split('/').pop();
10159
+ if (fileNameOnly !== basename) {
10160
+ return;
10161
+ }
10162
+ if (!fs.existsSync(target)) {
10163
+ syncDirectoryTree(target);
10164
+ return;
10165
+ }
10166
+ closeWatcher(watchKey);
10167
+ const ok = addWatcher(target, true, true);
10168
+ if (!ok) {
10169
+ syncDirectoryTree(target);
10170
+ }
10171
+ });
10172
+ watcher.on('error', () => {
10173
+ closeWatcher(watchKey);
10174
+ if (fs.existsSync(parentDir) && !fs.existsSync(target)) {
10175
+ addMissingDirectoryWatcher(target);
10176
+ }
10177
+ });
10178
+ watcherEntries.set(watchKey, {
10179
+ watcher,
10180
+ target: parentDir,
10181
+ recursive: false,
10182
+ isDirectory: false
10183
+ });
10184
+ return true;
10185
+ } catch (_) {
10186
+ return false;
10187
+ }
10188
+ };
10189
+
10190
+ const syncDirectoryTree = (rootDir) => {
10191
+ const directories = listDirectoryTree(rootDir);
10192
+ const existingDirectorySet = new Set(directories);
10193
+ for (const [watchKey, entry] of Array.from(watcherEntries.entries())) {
10194
+ if (!entry.isDirectory || entry.recursive) {
10195
+ continue;
10196
+ }
10197
+ if (!isSameOrNestedPath(entry.target, rootDir)) {
10198
+ continue;
10199
+ }
10200
+ if (!existingDirectorySet.has(entry.target)) {
10201
+ closeWatcher(watchKey);
10202
+ }
10203
+ }
10204
+ for (const directory of directories) {
10205
+ addWatcher(directory, false, true);
10206
+ }
10207
+ };
10208
+
9785
10209
  for (const target of targets) {
9786
- const ok = addWatcher(target, true);
10210
+ if (!fs.existsSync(target)) continue;
10211
+ let stat = null;
10212
+ try {
10213
+ stat = fs.statSync(target);
10214
+ } catch (_) {
10215
+ continue;
10216
+ }
10217
+ if (stat && stat.isDirectory()) {
10218
+ const ok = addWatcher(target, true, true);
10219
+ if (!ok) {
10220
+ syncDirectoryTree(target);
10221
+ }
10222
+ continue;
10223
+ }
10224
+ const ok = addWatcher(target, true, false);
9787
10225
  if (!ok) {
9788
- addWatcher(target, false);
10226
+ addWatcher(target, false, false);
9789
10227
  }
9790
10228
  }
9791
10229
 
9792
10230
  return () => {
9793
- for (const dispose of disposers) {
9794
- try { dispose(); } catch (_) {}
10231
+ if (timer) {
10232
+ clearTimeout(timer);
10233
+ timer = null;
10234
+ }
10235
+ for (const watchKey of Array.from(watcherEntries.keys())) {
10236
+ closeWatcher(watchKey);
9795
10237
  }
9796
10238
  };
9797
10239
  }
10240
+ // #endregion watchPathsForRestart
9798
10241
 
9799
10242
  function writeJsonResponse(res, statusCode, payload) {
9800
10243
  const body = JSON.stringify(payload, null, 2);
@@ -9939,8 +10382,46 @@ async function handleImportSkillsZipUpload(req, res, options = {}) {
9939
10382
  }
9940
10383
  }
9941
10384
 
10385
+ const PUBLIC_WEB_UI_DYNAMIC_ASSETS = new Map([
10386
+ ['app.js', {
10387
+ mime: 'application/javascript; charset=utf-8',
10388
+ reader: readExecutableBundledWebUiScript
10389
+ }],
10390
+ ['index.html', {
10391
+ mime: 'text/html; charset=utf-8',
10392
+ reader: readBundledWebUiHtml
10393
+ }],
10394
+ ['logic.mjs', {
10395
+ mime: 'application/javascript; charset=utf-8',
10396
+ reader: readExecutableBundledJavaScriptModule
10397
+ }],
10398
+ ['styles.css', {
10399
+ mime: 'text/css; charset=utf-8',
10400
+ reader: readBundledWebUiCss
10401
+ }]
10402
+ ]);
10403
+
10404
+ const PUBLIC_WEB_UI_STATIC_ASSETS = new Set([
10405
+ 'modules/config-mode.computed.mjs',
10406
+ 'modules/skills.computed.mjs',
10407
+ 'modules/skills.methods.mjs',
10408
+ 'session-helpers.mjs'
10409
+ ]);
10410
+
9942
10411
  function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
9943
10412
  const connections = new Set();
10413
+ const writeWebUiAssetError = (res, requestPath, error) => {
10414
+ const message = error && error.message ? error.message : String(error);
10415
+ console.error(`! Web UI 资源读取失败 [${requestPath}]:`, message);
10416
+ if (res.headersSent) {
10417
+ try {
10418
+ res.destroy(error);
10419
+ } catch (_) {}
10420
+ return;
10421
+ }
10422
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
10423
+ res.end('Internal Server Error');
10424
+ };
9944
10425
 
9945
10426
  const server = http.createServer((req, res) => {
9946
10427
  const requestPath = (req.url || '/').split('?')[0];
@@ -9976,22 +10457,38 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
9976
10457
  let result;
9977
10458
 
9978
10459
  switch (action) {
9979
- case 'status':
10460
+ case 'status': {
9980
10461
  const statusConfigResult = readConfigOrVirtualDefault();
9981
10462
  const config = statusConfigResult.config;
9982
10463
  const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
9983
10464
  const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
10465
+ const budgetReadOptions = {
10466
+ useDefaultsWhenMissing: !hasConfigLoadError(statusConfigResult)
10467
+ };
10468
+ const modelContextWindow = readPositiveIntegerConfigValue(
10469
+ config,
10470
+ 'model_context_window',
10471
+ budgetReadOptions
10472
+ );
10473
+ const modelAutoCompactTokenLimit = readPositiveIntegerConfigValue(
10474
+ config,
10475
+ 'model_auto_compact_token_limit',
10476
+ budgetReadOptions
10477
+ );
9984
10478
  result = {
9985
10479
  provider: config.model_provider || '未设置',
9986
10480
  model: config.model || '未设置',
9987
10481
  serviceTier,
9988
10482
  modelReasoningEffort,
10483
+ modelContextWindow,
10484
+ modelAutoCompactTokenLimit,
9989
10485
  configReady: !statusConfigResult.isVirtual,
9990
10486
  configErrorType: statusConfigResult.errorType || '',
9991
10487
  configNotice: statusConfigResult.reason || '',
9992
10488
  initNotice: consumeInitNotice()
9993
10489
  };
9994
10490
  break;
10491
+ }
9995
10492
  case 'install-status':
9996
10493
  result = buildInstallStatusReport();
9997
10494
  break;
@@ -10173,7 +10670,11 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10173
10670
  result = { error: target.error };
10174
10671
  break;
10175
10672
  }
10176
- result = await runSpeedTest(target.url, target.apiKey);
10673
+ result = await runSpeedTest(target.url, target.apiKey, target);
10674
+ break;
10675
+ }
10676
+ case 'provider-chat-check': {
10677
+ result = await runProviderChatCheck(params || {});
10177
10678
  break;
10178
10679
  }
10179
10680
  case 'list-sessions':
@@ -10354,6 +10855,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10354
10855
  res.end(errorBody, 'utf-8');
10355
10856
  }
10356
10857
  });
10858
+ } else if (requestPath === '/web-ui') {
10859
+ try {
10860
+ const html = readBundledWebUiHtml(htmlPath);
10861
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
10862
+ res.end(html);
10863
+ } catch (error) {
10864
+ writeWebUiAssetError(res, requestPath, error);
10865
+ }
10357
10866
  } else if (requestPath.startsWith('/web-ui/')) {
10358
10867
  const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
10359
10868
  const filePath = path.join(__dirname, normalized);
@@ -10362,6 +10871,23 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10362
10871
  res.end('Forbidden');
10363
10872
  return;
10364
10873
  }
10874
+ const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
10875
+ const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
10876
+ if (dynamicAsset) {
10877
+ try {
10878
+ const assetBody = dynamicAsset.reader(filePath);
10879
+ res.writeHead(200, { 'Content-Type': dynamicAsset.mime });
10880
+ res.end(assetBody, 'utf-8');
10881
+ } catch (error) {
10882
+ writeWebUiAssetError(res, requestPath, error);
10883
+ }
10884
+ return;
10885
+ }
10886
+ if (!PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) {
10887
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
10888
+ res.end('Not Found');
10889
+ return;
10890
+ }
10365
10891
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
10366
10892
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
10367
10893
  res.end('Not Found');
@@ -10434,9 +10960,13 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10434
10960
  res.writeHead(200, { 'Content-Type': mime });
10435
10961
  fs.createReadStream(filePath).pipe(res);
10436
10962
  } else {
10437
- const html = fs.readFileSync(htmlPath, 'utf-8');
10438
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
10439
- res.end(html);
10963
+ try {
10964
+ const html = readBundledWebUiHtml(htmlPath);
10965
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
10966
+ res.end(html);
10967
+ } catch (error) {
10968
+ writeWebUiAssetError(res, requestPath, error);
10969
+ }
10440
10970
  }
10441
10971
  });
10442
10972
 
@@ -10590,6 +11120,7 @@ function cmdStart(options = {}) {
10590
11120
 
10591
11121
  const port = resolveWebPort();
10592
11122
  const host = resolveWebHost(options);
11123
+ releaseRunPortIfNeeded(port, host);
10593
11124
 
10594
11125
  let serverHandle = createWebServer({
10595
11126
  htmlPath,
@@ -11464,11 +11995,26 @@ function buildMcpStatusPayload() {
11464
11995
  const config = statusConfigResult.config;
11465
11996
  const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
11466
11997
  const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
11998
+ const budgetReadOptions = {
11999
+ useDefaultsWhenMissing: !hasConfigLoadError(statusConfigResult)
12000
+ };
12001
+ const modelContextWindow = readPositiveIntegerConfigValue(
12002
+ config,
12003
+ 'model_context_window',
12004
+ budgetReadOptions
12005
+ );
12006
+ const modelAutoCompactTokenLimit = readPositiveIntegerConfigValue(
12007
+ config,
12008
+ 'model_auto_compact_token_limit',
12009
+ budgetReadOptions
12010
+ );
11467
12011
  return {
11468
12012
  provider: config.model_provider || '未设置',
11469
12013
  model: config.model || '未设置',
11470
12014
  serviceTier,
11471
12015
  modelReasoningEffort,
12016
+ modelContextWindow,
12017
+ modelAutoCompactTokenLimit,
11472
12018
  configReady: !statusConfigResult.isVirtual,
11473
12019
  configErrorType: statusConfigResult.errorType || '',
11474
12020
  configNotice: statusConfigResult.reason || '',
@@ -11566,6 +12112,8 @@ const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
11566
12112
  model: { type: 'string' },
11567
12113
  serviceTier: { type: 'string' },
11568
12114
  reasoningEffort: { type: 'string' },
12115
+ modelContextWindow: { type: ['string', 'number'] },
12116
+ modelAutoCompactTokenLimit: { type: ['string', 'number'] },
11569
12117
  apply: { type: 'boolean' }
11570
12118
  },
11571
12119
  required: ['provider'],
@@ -11580,7 +12128,9 @@ const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
11580
12128
  provider: '{{input.provider}}',
11581
12129
  model: '{{input.model}}',
11582
12130
  serviceTier: '{{input.serviceTier}}',
11583
- reasoningEffort: '{{input.reasoningEffort}}'
12131
+ reasoningEffort: '{{input.reasoningEffort}}',
12132
+ modelContextWindow: '{{input.modelContextWindow}}',
12133
+ modelAutoCompactTokenLimit: '{{input.modelAutoCompactTokenLimit}}'
11584
12134
  }
11585
12135
  },
11586
12136
  {
@@ -12149,7 +12699,7 @@ function createMcpTools(options = {}) {
12149
12699
 
12150
12700
  pushTool({
12151
12701
  name: 'codexmate.config.template.get',
12152
- description: 'Get Codex config template with optional provider/model/service tier/reasoning effort.',
12702
+ description: 'Get Codex config template with optional provider/model/service tier/reasoning effort/context budget.',
12153
12703
  readOnly: true,
12154
12704
  inputSchema: {
12155
12705
  type: 'object',
@@ -12157,7 +12707,9 @@ function createMcpTools(options = {}) {
12157
12707
  provider: { type: 'string' },
12158
12708
  model: { type: 'string' },
12159
12709
  serviceTier: { type: 'string' },
12160
- reasoningEffort: { type: 'string' }
12710
+ reasoningEffort: { type: 'string' },
12711
+ modelContextWindow: { type: ['string', 'number'] },
12712
+ modelAutoCompactTokenLimit: { type: ['string', 'number'] }
12161
12713
  },
12162
12714
  additionalProperties: false
12163
12715
  },