codexmate 0.0.18 → 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 (73) hide show
  1. package/README.en.md +34 -17
  2. package/README.md +34 -25
  3. package/cli/config-health.js +338 -0
  4. package/cli.js +1570 -839
  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 +379 -5754
  9. package/web-ui/index.html +15 -2079
  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/modules/config-mode.computed.mjs +1 -0
  37. package/web-ui/modules/skills.computed.mjs +26 -1
  38. package/web-ui/modules/skills.methods.mjs +154 -23
  39. package/web-ui/partials/index/layout-footer.html +69 -0
  40. package/web-ui/partials/index/layout-header.html +337 -0
  41. package/web-ui/partials/index/modal-config-template-agents.html +125 -0
  42. package/web-ui/partials/index/modal-confirm-toast.html +32 -0
  43. package/web-ui/partials/index/modal-health-check.html +72 -0
  44. package/web-ui/partials/index/modal-openclaw-config.html +275 -0
  45. package/web-ui/partials/index/modal-skills.html +184 -0
  46. package/web-ui/partials/index/modals-basic.html +196 -0
  47. package/web-ui/partials/index/panel-config-claude.html +100 -0
  48. package/web-ui/partials/index/panel-config-codex.html +237 -0
  49. package/web-ui/partials/index/panel-config-openclaw.html +84 -0
  50. package/web-ui/partials/index/panel-market.html +174 -0
  51. package/web-ui/partials/index/panel-sessions.html +387 -0
  52. package/web-ui/partials/index/panel-settings.html +166 -0
  53. package/web-ui/session-helpers.mjs +12 -0
  54. package/web-ui/source-bundle.cjs +233 -0
  55. package/web-ui/styles/base-theme.css +373 -0
  56. package/web-ui/styles/controls-forms.css +354 -0
  57. package/web-ui/styles/feedback.css +108 -0
  58. package/web-ui/styles/health-check-dialog.css +144 -0
  59. package/web-ui/styles/layout-shell.css +330 -0
  60. package/web-ui/styles/modals-core.css +449 -0
  61. package/web-ui/styles/navigation-panels.css +381 -0
  62. package/web-ui/styles/openclaw-structured.css +266 -0
  63. package/web-ui/styles/responsive.css +416 -0
  64. package/web-ui/styles/sessions-list.css +414 -0
  65. package/web-ui/styles/sessions-preview.css +405 -0
  66. package/web-ui/styles/sessions-toolbar-trash.css +243 -0
  67. package/web-ui/styles/sessions-usage.css +276 -0
  68. package/web-ui/styles/skills-list.css +298 -0
  69. package/web-ui/styles/skills-market.css +335 -0
  70. package/web-ui/styles/titles-cards.css +407 -0
  71. package/web-ui/styles.css +16 -4499
  72. package/doc/CHANGELOG.md +0 -32
  73. package/doc/CHANGELOG.zh-CN.md +0 -34
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,9 +65,17 @@ 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
- const DEFAULT_WEB_HOST = '127.0.0.1';
77
+ const DEFAULT_WEB_HOST = '0.0.0.0';
78
+ const DEFAULT_WEB_OPEN_HOST = '127.0.0.1';
68
79
 
69
80
  // ============================================================================
70
81
  // 配置
@@ -92,11 +103,12 @@ const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
92
103
  const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
93
104
  const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
94
105
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
106
+ const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
107
+ const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
95
108
  const CODEX_BACKUP_NAME = 'codex-config';
96
109
 
97
110
  const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
98
111
  const SPEED_TEST_TIMEOUT_MS = 8000;
99
- const HEALTH_CHECK_TIMEOUT_MS = 6000;
100
112
  const MAX_SESSION_LIST_SIZE = 300;
101
113
  const MAX_SESSION_TRASH_LIST_SIZE = 500;
102
114
  const MAX_EXPORT_MESSAGES = 1000;
@@ -116,9 +128,13 @@ const AGENTS_FILE_NAME = 'AGENTS.md';
116
128
  const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
117
129
  const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
118
130
  const AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
131
+ const SKILL_TARGETS = Object.freeze([
132
+ Object.freeze({ app: 'codex', label: 'Codex', dir: getCodexSkillsDir() }),
133
+ Object.freeze({ app: 'claude', label: 'Claude Code', dir: getClaudeSkillsDir() })
134
+ ]);
119
135
  const SKILL_IMPORT_SOURCES = Object.freeze([
120
- { app: 'claude', label: 'Claude Code', dir: CLAUDE_SKILLS_DIR },
121
- { app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR }
136
+ ...SKILL_TARGETS,
137
+ Object.freeze({ app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR })
122
138
  ]);
123
139
  const MODELS_CACHE_TTL_MS = 60 * 1000;
124
140
  const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
@@ -167,6 +183,48 @@ const CLI_INSTALL_TARGETS = Object.freeze([
167
183
  const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
168
184
  const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
169
185
 
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
+ }
227
+
170
228
  function resolveWebPort() {
171
229
  const raw = process.env.CODEXMATE_PORT;
172
230
  if (!raw) return DEFAULT_WEB_PORT;
@@ -175,6 +233,239 @@ function resolveWebPort() {
175
233
  return parsed;
176
234
  }
177
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
+
178
469
  function resolveWebHost(options = {}) {
179
470
  const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
180
471
  if (optionHost) {
@@ -188,7 +479,8 @@ function resolveWebHost(options = {}) {
188
479
  }
189
480
 
190
481
  const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
191
- model_reasoning_effort = "high"
482
+ model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
483
+ model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
192
484
  disable_response_storage = true
193
485
  approval_policy = "never"
194
486
  sandbox_mode = "danger-full-access"
@@ -216,7 +508,7 @@ let g_builtinProxyRuntime = null;
216
508
  const DEFAULT_LOCAL_PROVIDER_NAME = 'local';
217
509
 
218
510
  function isBuiltinProxyProvider(providerName) {
219
- return typeof providerName === 'string' && providerName.trim() === BUILTIN_PROXY_PROVIDER_NAME;
511
+ return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_PROXY_PROVIDER_NAME.toLowerCase();
220
512
  }
221
513
 
222
514
  function isReservedProviderNameForCreation(providerName) {
@@ -889,12 +1181,18 @@ function normalizeAuthRegistry(raw) {
889
1181
  };
890
1182
  }
891
1183
 
1184
+ function ensureAuthProfileStoragePrepared() {
1185
+ ensureDir(AUTH_PROFILES_DIR);
1186
+ }
1187
+
892
1188
  function readAuthRegistry() {
1189
+ ensureAuthProfileStoragePrepared();
893
1190
  const parsed = readJsonFile(AUTH_REGISTRY_FILE, null);
894
1191
  return normalizeAuthRegistry(parsed);
895
1192
  }
896
1193
 
897
1194
  function writeAuthRegistry(registry) {
1195
+ ensureAuthProfileStoragePrepared();
898
1196
  writeJsonAtomic(AUTH_REGISTRY_FILE, normalizeAuthRegistry(registry));
899
1197
  }
900
1198
 
@@ -953,6 +1251,7 @@ function listAuthProfilesInfo() {
953
1251
  }
954
1252
 
955
1253
  function upsertAuthProfile(payload, options = {}) {
1254
+ ensureAuthProfileStoragePrepared();
956
1255
  const safePayload = parseAuthProfileJson(JSON.stringify(payload || {}));
957
1256
  const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : '';
958
1257
  const preferredName = normalizeAuthProfileName(options.name || '');
@@ -1042,6 +1341,7 @@ function importAuthProfileFromUpload(payload = {}) {
1042
1341
  }
1043
1342
 
1044
1343
  function switchAuthProfile(name, options = {}) {
1344
+ ensureAuthProfileStoragePrepared();
1045
1345
  const profileName = normalizeAuthProfileName(name);
1046
1346
  if (!profileName) {
1047
1347
  throw new Error('认证名称不能为空');
@@ -1087,6 +1387,7 @@ function switchAuthProfile(name, options = {}) {
1087
1387
  }
1088
1388
 
1089
1389
  function deleteAuthProfile(name) {
1390
+ ensureAuthProfileStoragePrepared();
1090
1391
  const profileName = normalizeAuthProfileName(name);
1091
1392
  if (!profileName) {
1092
1393
  return { error: '认证名称不能为空' };
@@ -1135,6 +1436,7 @@ function deleteAuthProfile(name) {
1135
1436
  }
1136
1437
 
1137
1438
  function resolveAuthTokenFromCurrentProfile() {
1439
+ ensureAuthProfileStoragePrepared();
1138
1440
  const registry = readAuthRegistry();
1139
1441
  if (!registry.current) return '';
1140
1442
  const profile = registry.items.find((item) => item.name === registry.current);
@@ -1390,8 +1692,40 @@ function normalizeCodexSkillName(name) {
1390
1692
  return { name: value };
1391
1693
  }
1392
1694
 
1393
- function isSkillDirectoryEntry(entryName) {
1394
- const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
1695
+ function normalizeSkillTargetApp(app) {
1696
+ const value = typeof app === 'string' ? app.trim().toLowerCase() : '';
1697
+ return SKILL_TARGETS.some((item) => item.app === value) ? value : '';
1698
+ }
1699
+
1700
+ function getSkillTargetByApp(app) {
1701
+ const normalizedApp = normalizeSkillTargetApp(app);
1702
+ if (!normalizedApp) return null;
1703
+ return SKILL_TARGETS.find((item) => item.app === normalizedApp) || null;
1704
+ }
1705
+
1706
+ function resolveSkillTarget(params = {}, defaultApp = 'codex') {
1707
+ const hasExplicitTargetApp = !!(params && typeof params === 'object'
1708
+ && Object.prototype.hasOwnProperty.call(params, 'targetApp'));
1709
+ const hasExplicitTarget = !!(params && typeof params === 'object'
1710
+ && Object.prototype.hasOwnProperty.call(params, 'target'));
1711
+ const hasAnyExplicitTarget = hasExplicitTargetApp || hasExplicitTarget;
1712
+ const rawTargetApp = hasExplicitTargetApp ? params.targetApp : '';
1713
+ const rawTarget = hasExplicitTarget ? params.target : '';
1714
+ const raw = rawTargetApp || rawTarget || '';
1715
+ if (hasAnyExplicitTarget && raw === '') {
1716
+ return null;
1717
+ }
1718
+ if (hasAnyExplicitTarget && !getSkillTargetByApp(raw)) {
1719
+ return null;
1720
+ }
1721
+ return getSkillTargetByApp(raw)
1722
+ || getSkillTargetByApp(defaultApp)
1723
+ || SKILL_TARGETS[0]
1724
+ || null;
1725
+ }
1726
+
1727
+ function isSkillDirectoryEntryAtRoot(rootDir, entryName) {
1728
+ const targetPath = path.join(rootDir, entryName);
1395
1729
  try {
1396
1730
  const stat = fs.statSync(targetPath);
1397
1731
  return stat.isDirectory();
@@ -1560,13 +1894,13 @@ function readCodexSkillMetadata(skillPath) {
1560
1894
  }
1561
1895
  }
1562
1896
 
1563
- function getCodexSkillEntryInfoByName(entryName) {
1564
- const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
1897
+ function getSkillEntryInfoByName(rootDir, entryName) {
1898
+ const targetPath = path.join(rootDir, entryName);
1565
1899
  const normalized = normalizeCodexSkillName(entryName);
1566
1900
  if (normalized.error) {
1567
1901
  return null;
1568
1902
  }
1569
- const relativePath = path.relative(CODEX_SKILLS_DIR, targetPath);
1903
+ const relativePath = path.relative(rootDir, targetPath);
1570
1904
  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
1571
1905
  return null;
1572
1906
  }
@@ -1577,7 +1911,7 @@ function getCodexSkillEntryInfoByName(entryName) {
1577
1911
  if (!lstat.isDirectory() && !isSymbolicLink) {
1578
1912
  return null;
1579
1913
  }
1580
- if (isSymbolicLink && !isSkillDirectoryEntry(entryName)) {
1914
+ if (isSymbolicLink && !isSkillDirectoryEntryAtRoot(rootDir, entryName)) {
1581
1915
  return null;
1582
1916
  }
1583
1917
  const metadata = readCodexSkillMetadata(targetPath);
@@ -1595,26 +1929,34 @@ function getCodexSkillEntryInfoByName(entryName) {
1595
1929
  }
1596
1930
  }
1597
1931
 
1598
- function listCodexSkills() {
1599
- if (!fs.existsSync(CODEX_SKILLS_DIR)) {
1932
+ function listSkills(params = {}) {
1933
+ const target = resolveSkillTarget(params);
1934
+ if (!target) {
1935
+ return { error: '目标宿主不支持' };
1936
+ }
1937
+ if (!fs.existsSync(target.dir)) {
1600
1938
  return {
1601
- root: CODEX_SKILLS_DIR,
1939
+ targetApp: target.app,
1940
+ targetLabel: target.label,
1941
+ root: target.dir,
1602
1942
  exists: false,
1603
1943
  items: []
1604
1944
  };
1605
1945
  }
1606
1946
  try {
1607
- const entries = fs.readdirSync(CODEX_SKILLS_DIR, { withFileTypes: true });
1947
+ const entries = fs.readdirSync(target.dir, { withFileTypes: true });
1608
1948
  const items = entries
1609
1949
  .map((entry) => {
1610
1950
  const name = entry && entry.name ? entry.name : '';
1611
1951
  if (!name || name.startsWith('.')) return null;
1612
- return getCodexSkillEntryInfoByName(name);
1952
+ return getSkillEntryInfoByName(target.dir, name);
1613
1953
  })
1614
1954
  .filter(Boolean)
1615
1955
  .sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
1616
1956
  return {
1617
- root: CODEX_SKILLS_DIR,
1957
+ targetApp: target.app,
1958
+ targetLabel: target.label,
1959
+ root: target.dir,
1618
1960
  exists: true,
1619
1961
  items
1620
1962
  };
@@ -1623,6 +1965,10 @@ function listCodexSkills() {
1623
1965
  }
1624
1966
  }
1625
1967
 
1968
+ function listCodexSkills() {
1969
+ return listSkills({ targetApp: 'codex' });
1970
+ }
1971
+
1626
1972
  function listSkillEntriesByRoot(rootDir) {
1627
1973
  if (!rootDir || !fs.existsSync(rootDir)) {
1628
1974
  return [];
@@ -1668,8 +2014,18 @@ function listSkillEntriesByRoot(rootDir) {
1668
2014
  }
1669
2015
  }
1670
2016
 
1671
- function scanUnmanagedCodexSkills() {
1672
- const existing = listCodexSkills();
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 });
1673
2029
  if (existing.error) {
1674
2030
  return { error: existing.error };
1675
2031
  }
@@ -1677,10 +2033,15 @@ function scanUnmanagedCodexSkills() {
1677
2033
  .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
1678
2034
  .filter(Boolean));
1679
2035
 
1680
- const items = [];
1681
- for (const source of SKILL_IMPORT_SOURCES) {
1682
- const sourceEntries = listSkillEntriesByRoot(source.dir);
1683
- for (const entry of sourceEntries) {
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
+ }
1684
2045
  if (existingNames.has(entry.name)) {
1685
2046
  continue;
1686
2047
  }
@@ -1706,9 +2067,11 @@ function scanUnmanagedCodexSkills() {
1706
2067
  });
1707
2068
 
1708
2069
  return {
1709
- root: CODEX_SKILLS_DIR,
2070
+ targetApp: target.app,
2071
+ targetLabel: target.label,
2072
+ root: target.dir,
1710
2073
  items,
1711
- sources: SKILL_IMPORT_SOURCES.map((source) => ({
2074
+ sources: sources.map((source) => ({
1712
2075
  app: source.app,
1713
2076
  label: source.label,
1714
2077
  path: source.dir,
@@ -1717,14 +2080,26 @@ function scanUnmanagedCodexSkills() {
1717
2080
  };
1718
2081
  }
1719
2082
 
1720
- function importCodexSkills(params = {}) {
1721
- const rawItems = Array.isArray(params.items) ? params.items : [];
2083
+ function scanUnmanagedCodexSkills() {
2084
+ return scanUnmanagedSkills({ targetApp: 'codex' });
2085
+ }
2086
+
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 : [];
1722
2099
  if (!rawItems.length) {
1723
2100
  return { error: '请先选择要导入的 skill' };
1724
2101
  }
1725
2102
 
1726
- ensureDir(CODEX_SKILLS_DIR);
1727
-
1728
2103
  const imported = [];
1729
2104
  const failed = [];
1730
2105
  const dedup = new Set();
@@ -1749,18 +2124,27 @@ function importCodexSkills(params = {}) {
1749
2124
  });
1750
2125
  continue;
1751
2126
  }
2127
+ if (source.app === target.app) {
2128
+ failed.push({
2129
+ name: normalizedName.name,
2130
+ sourceApp: source.app,
2131
+ error: '来源与目标相同,无需导入'
2132
+ });
2133
+ continue;
2134
+ }
1752
2135
  const dedupKey = `${source.app}:${normalizedName.name}`;
1753
2136
  if (dedup.has(dedupKey)) {
1754
2137
  continue;
1755
2138
  }
1756
2139
  dedup.add(dedupKey);
1757
2140
 
1758
- const sourcePath = path.join(source.dir, normalizedName.name);
1759
- const sourceRelative = path.relative(source.dir, sourcePath);
1760
- if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
1761
- failed.push({
1762
- name: normalizedName.name,
1763
- 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,
1764
2148
  error: '来源路径非法'
1765
2149
  });
1766
2150
  continue;
@@ -1774,12 +2158,12 @@ function importCodexSkills(params = {}) {
1774
2158
  continue;
1775
2159
  }
1776
2160
 
1777
- const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
1778
- const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
1779
- if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
1780
- failed.push({
1781
- name: normalizedName.name,
1782
- 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,
1783
2167
  error: '目标路径非法'
1784
2168
  });
1785
2169
  continue;
@@ -1788,7 +2172,7 @@ function importCodexSkills(params = {}) {
1788
2172
  failed.push({
1789
2173
  name: normalizedName.name,
1790
2174
  sourceApp: source.app,
1791
- error: 'Codex 中已存在同名 skill'
2175
+ error: `${target.label} 中已存在同名 skill`
1792
2176
  });
1793
2177
  continue;
1794
2178
  }
@@ -1814,6 +2198,15 @@ function importCodexSkills(params = {}) {
1814
2198
  });
1815
2199
  continue;
1816
2200
  }
2201
+ if (isPathInside(targetRoot, sourceDirForCopy)) {
2202
+ failed.push({
2203
+ name: normalizedName.name,
2204
+ sourceApp: source.app,
2205
+ error: '目标路径不能位于来源 skill 目录内'
2206
+ });
2207
+ continue;
2208
+ }
2209
+ ensureDir(targetRoot);
1817
2210
  const visitedRealPaths = new Set([sourceDirForCopy]);
1818
2211
  copyDirRecursive(sourceDirForCopy, targetPath, {
1819
2212
  dereferenceSymlinks: true,
@@ -1825,6 +2218,8 @@ function importCodexSkills(params = {}) {
1825
2218
  name: normalizedName.name,
1826
2219
  sourceApp: source.app,
1827
2220
  sourceLabel: source.label,
2221
+ targetApp: target.app,
2222
+ targetLabel: target.label,
1828
2223
  path: targetPath
1829
2224
  });
1830
2225
  } catch (e) {
@@ -1845,10 +2240,16 @@ function importCodexSkills(params = {}) {
1845
2240
  success: failed.length === 0,
1846
2241
  imported,
1847
2242
  failed,
1848
- root: CODEX_SKILLS_DIR
2243
+ targetApp: target.app,
2244
+ targetLabel: target.label,
2245
+ root: targetRoot
1849
2246
  };
1850
2247
  }
1851
2248
 
2249
+ function importCodexSkills(params = {}) {
2250
+ return importSkills({ ...(params || {}), targetApp: 'codex' });
2251
+ }
2252
+
1852
2253
  function collectSkillDirectoriesFromRoot(rootDir, limit = MAX_SKILLS_ZIP_ENTRY_COUNT) {
1853
2254
  const results = [];
1854
2255
  let truncated = false;
@@ -1902,18 +2303,32 @@ function resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbac
1902
2303
  return normalizeCodexSkillName(candidate);
1903
2304
  }
1904
2305
 
1905
- async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1906
- const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
1907
- const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
1908
- const imported = [];
1909
- const failed = [];
1910
- const dedupNames = new Set();
1911
- 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');
2319
+ let target = null;
2320
+ let targetRoot = '';
1912
2321
 
1913
2322
  try {
1914
- await inspectZipArchiveLimits(zipPath, {
1915
- maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
1916
- maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
2323
+ target = resolveSkillTarget(options, 'codex');
2324
+ if (!target) {
2325
+ return { error: '目标宿主不支持' };
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
1917
2332
  });
1918
2333
 
1919
2334
  await extractUploadZip(zipPath, extractionRoot);
@@ -1926,7 +2341,6 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1926
2341
  return { error: '压缩包中的技能目录数量超出导入上限' };
1927
2342
  }
1928
2343
 
1929
- ensureDir(CODEX_SKILLS_DIR);
1930
2344
  for (const skillDir of discoveredDirs) {
1931
2345
  const normalizedName = resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName);
1932
2346
  if (normalizedName.error) {
@@ -1942,19 +2356,19 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1942
2356
  }
1943
2357
  dedupNames.add(dedupKey);
1944
2358
 
1945
- const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
1946
- const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
1947
- if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
1948
- failed.push({
1949
- name: normalizedName.name,
1950
- 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: '目标路径非法'
1951
2365
  });
1952
2366
  continue;
1953
2367
  }
1954
2368
  if (fs.existsSync(targetPath)) {
1955
2369
  failed.push({
1956
2370
  name: normalizedName.name,
1957
- error: 'Codex 中已存在同名 skill'
2371
+ error: `${target.label} 中已存在同名 skill`
1958
2372
  });
1959
2373
  continue;
1960
2374
  }
@@ -1970,6 +2384,14 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1970
2384
  });
1971
2385
  continue;
1972
2386
  }
2387
+ if (isPathInside(targetRoot, sourceRealPath)) {
2388
+ failed.push({
2389
+ name: normalizedName.name,
2390
+ error: '目标路径不能位于来源 skill 目录内'
2391
+ });
2392
+ continue;
2393
+ }
2394
+ ensureDir(targetRoot);
1973
2395
  const visitedRealPaths = new Set([sourceRealPath]);
1974
2396
  copyDirRecursive(sourceRealPath, targetPath, {
1975
2397
  dereferenceSymlinks: true,
@@ -1979,6 +2401,8 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1979
2401
  copiedToTarget = true;
1980
2402
  imported.push({
1981
2403
  name: normalizedName.name,
2404
+ targetApp: target.app,
2405
+ targetLabel: target.label,
1982
2406
  path: targetPath
1983
2407
  });
1984
2408
  } catch (e) {
@@ -1999,7 +2423,9 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1999
2423
  error: failed[0].error || '导入失败',
2000
2424
  imported,
2001
2425
  failed,
2002
- root: CODEX_SKILLS_DIR
2426
+ targetApp: target.app,
2427
+ targetLabel: target.label,
2428
+ root: targetRoot
2003
2429
  };
2004
2430
  }
2005
2431
 
@@ -2007,7 +2433,9 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
2007
2433
  success: failed.length === 0,
2008
2434
  imported,
2009
2435
  failed,
2010
- root: CODEX_SKILLS_DIR
2436
+ targetApp: target.app,
2437
+ targetLabel: target.label,
2438
+ root: targetRoot
2011
2439
  };
2012
2440
  } catch (e) {
2013
2441
  return {
@@ -2026,21 +2454,40 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
2026
2454
  }
2027
2455
  }
2028
2456
 
2029
- async function importCodexSkillsFromZip(payload = {}) {
2457
+ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
2458
+ return importSkillsFromZipFile(zipPath, { ...(options || {}), targetApp: 'codex' });
2459
+ }
2460
+
2461
+ async function importSkillsFromZip(payload = {}) {
2030
2462
  if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
2031
2463
  return { error: '缺少技能压缩包内容' };
2032
2464
  }
2033
- const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', payload.fileName || 'codex-skills.zip');
2465
+ const fallbackTarget = resolveSkillTarget(payload, 'codex');
2466
+ const fallbackTargetApp = fallbackTarget ? fallbackTarget.app : 'codex';
2467
+ const fallbackName = payload.fileName || `${fallbackTargetApp}-skills.zip`;
2468
+ const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', fallbackName);
2034
2469
  if (upload.error) {
2035
2470
  return { error: upload.error };
2036
2471
  }
2037
- return importCodexSkillsFromZipFile(upload.zipPath, {
2038
- tempDir: upload.tempDir,
2039
- fallbackName: payload.fileName || ''
2040
- });
2472
+ const importOptions = { tempDir: upload.tempDir, fallbackName };
2473
+ if (Object.prototype.hasOwnProperty.call(payload, 'targetApp')) {
2474
+ importOptions.targetApp = payload.targetApp;
2475
+ }
2476
+ if (Object.prototype.hasOwnProperty.call(payload, 'target')) {
2477
+ importOptions.target = payload.target;
2478
+ }
2479
+ return importSkillsFromZipFile(upload.zipPath, importOptions);
2041
2480
  }
2042
2481
 
2043
- async function exportCodexSkills(params = {}) {
2482
+ async function importCodexSkillsFromZip(payload = {}) {
2483
+ return importSkillsFromZip({ ...(payload || {}), targetApp: 'codex' });
2484
+ }
2485
+
2486
+ async function exportSkills(params = {}) {
2487
+ const target = resolveSkillTarget(params);
2488
+ if (!target) {
2489
+ return { error: '目标宿主不支持' };
2490
+ }
2044
2491
  const rawNames = Array.isArray(params.names) ? params.names : [];
2045
2492
  const uniqueNames = Array.from(new Set(rawNames
2046
2493
  .map((item) => (typeof item === 'string' ? item.trim() : ''))
@@ -2062,8 +2509,8 @@ async function exportCodexSkills(params = {}) {
2062
2509
  failed.push({ name: rawName, error: normalizedName.error });
2063
2510
  continue;
2064
2511
  }
2065
- const sourcePath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
2066
- const sourceRelative = path.relative(CODEX_SKILLS_DIR, sourcePath);
2512
+ const sourcePath = path.join(target.dir, normalizedName.name);
2513
+ const sourceRelative = path.relative(target.dir, sourcePath);
2067
2514
  if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
2068
2515
  failed.push({ name: normalizedName.name, error: '来源路径非法' });
2069
2516
  continue;
@@ -2109,12 +2556,14 @@ async function exportCodexSkills(params = {}) {
2109
2556
  error: failed[0] && failed[0].error ? failed[0].error : '无可导出的 skill',
2110
2557
  exported,
2111
2558
  failed,
2112
- root: CODEX_SKILLS_DIR
2559
+ targetApp: target.app,
2560
+ targetLabel: target.label,
2561
+ root: target.dir
2113
2562
  };
2114
2563
  }
2115
2564
 
2116
2565
  const randomToken = crypto.randomBytes(12).toString('hex');
2117
- const zipFileName = `codex-skills-${randomToken}.zip`;
2566
+ const zipFileName = `${target.app}-skills-${randomToken}.zip`;
2118
2567
  const zipFilePath = path.join(os.tmpdir(), zipFileName);
2119
2568
  if (fs.existsSync(zipFilePath)) {
2120
2569
  try {
@@ -2133,14 +2582,18 @@ async function exportCodexSkills(params = {}) {
2133
2582
  downloadPath: artifact.downloadPath,
2134
2583
  exported,
2135
2584
  failed,
2136
- root: CODEX_SKILLS_DIR
2585
+ targetApp: target.app,
2586
+ targetLabel: target.label,
2587
+ root: target.dir
2137
2588
  };
2138
2589
  } catch (e) {
2139
2590
  return {
2140
2591
  error: `导出失败:${e && e.message ? e.message : '未知错误'}`,
2141
2592
  exported,
2142
2593
  failed,
2143
- root: CODEX_SKILLS_DIR
2594
+ targetApp: target.app,
2595
+ targetLabel: target.label,
2596
+ root: target.dir
2144
2597
  };
2145
2598
  } finally {
2146
2599
  try {
@@ -2149,6 +2602,10 @@ async function exportCodexSkills(params = {}) {
2149
2602
  }
2150
2603
  }
2151
2604
 
2605
+ async function exportCodexSkills(params = {}) {
2606
+ return exportSkills({ ...(params || {}), targetApp: 'codex' });
2607
+ }
2608
+
2152
2609
  function removeDirectoryRecursive(targetPath) {
2153
2610
  if (typeof fs.rmSync === 'function') {
2154
2611
  fs.rmSync(targetPath, { recursive: true, force: false });
@@ -2157,7 +2614,11 @@ function removeDirectoryRecursive(targetPath) {
2157
2614
  fs.rmdirSync(targetPath, { recursive: true });
2158
2615
  }
2159
2616
 
2160
- function deleteCodexSkills(params = {}) {
2617
+ function deleteSkills(params = {}) {
2618
+ const target = resolveSkillTarget(params);
2619
+ if (!target) {
2620
+ return { error: '目标宿主不支持' };
2621
+ }
2161
2622
  const rawList = Array.isArray(params.names) ? params.names : [];
2162
2623
  const uniqueNames = Array.from(new Set(rawList
2163
2624
  .map((item) => (typeof item === 'string' ? item.trim() : ''))
@@ -2175,8 +2636,8 @@ function deleteCodexSkills(params = {}) {
2175
2636
  continue;
2176
2637
  }
2177
2638
 
2178
- const skillPath = path.join(CODEX_SKILLS_DIR, normalized.name);
2179
- const relativePath = path.relative(CODEX_SKILLS_DIR, skillPath);
2639
+ const skillPath = path.join(target.dir, normalized.name);
2640
+ const relativePath = path.relative(target.dir, skillPath);
2180
2641
  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
2181
2642
  failed.push({ name: normalized.name, error: '技能路径非法' });
2182
2643
  continue;
@@ -2206,10 +2667,16 @@ function deleteCodexSkills(params = {}) {
2206
2667
  success: failed.length === 0,
2207
2668
  deleted,
2208
2669
  failed,
2209
- root: CODEX_SKILLS_DIR
2670
+ targetApp: target.app,
2671
+ targetLabel: target.label,
2672
+ root: target.dir
2210
2673
  };
2211
2674
  }
2212
2675
 
2676
+ function deleteCodexSkills(params = {}) {
2677
+ return deleteSkills({ ...(params || {}), targetApp: 'codex' });
2678
+ }
2679
+
2213
2680
  function readAgentsFile(params = {}) {
2214
2681
  const filePath = resolveAgentsFilePath(params);
2215
2682
  const dirCheck = validateAgentsBaseDir(filePath);
@@ -2613,393 +3080,77 @@ function recordRecentConfig(provider, model) {
2613
3080
  writeRecentConfigs(trimmed);
2614
3081
  }
2615
3082
 
2616
- async function runRemoteHealthCheck(provider, modelName, options = {}) {
2617
- const issues = [];
2618
- const results = {};
2619
- const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
2620
- if (!baseUrl) {
2621
- issues.push({
2622
- code: 'remote-skip-base-url',
2623
- message: '无法进行远程探测:base_url 为空',
2624
- suggestion: '补全 base_url 或关闭远程探测'
2625
- });
2626
- return { issues, results };
2627
- }
3083
+ async function buildConfigHealthReport(params = {}) {
3084
+ return buildConfigHealthReportCore(params, {
3085
+ readConfigOrVirtualDefault,
3086
+ readModels
3087
+ });
3088
+ }
2628
3089
 
2629
- const requiresAuth = provider && provider.requires_openai_auth !== false;
2630
- const apiKey = typeof provider.preferred_auth_method === 'string'
2631
- ? provider.preferred_auth_method.trim()
2632
- : '';
2633
- const authValue = requiresAuth ? apiKey : (apiKey || '');
2634
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
2635
-
2636
- const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
2637
- results.base = {
2638
- url: baseUrl,
2639
- status: baseProbe.status || 0,
2640
- ok: baseProbe.ok,
2641
- durationMs: baseProbe.durationMs || 0
2642
- };
3090
+ function buildDefaultConfigContent(initializedAt) {
3091
+ const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
3092
+ return `${CODEXMATE_MANAGED_MARKER}
3093
+ # codexmate-initialized-at: ${initializedAt}
2643
3094
 
2644
- if (!baseProbe.ok) {
2645
- issues.push({
2646
- code: 'remote-unreachable',
2647
- message: `远程探测失败:${baseProbe.error || '无法连接'}`,
2648
- suggestion: '检查网络与 base_url 可达性'
2649
- });
2650
- return { issues, results };
2651
- }
3095
+ model_provider = "openai"
3096
+ model = "${defaultModel}"
3097
+ model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
3098
+ model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
2652
3099
 
2653
- if (baseProbe.status === 401 || baseProbe.status === 403) {
2654
- issues.push({
2655
- code: 'remote-auth-failed',
2656
- message: '远程探测鉴权失败(401/403)',
2657
- suggestion: '检查 API Key 或认证方式'
2658
- });
2659
- } else if (baseProbe.status >= 400) {
2660
- issues.push({
2661
- code: 'remote-http-error',
2662
- message: `远程探测返回异常状态: ${baseProbe.status}`,
2663
- suggestion: '检查 base_url 是否正确'
2664
- });
2665
- }
3100
+ [model_providers.openai]
3101
+ name = "openai"
3102
+ base_url = "https://api.openai.com/v1"
3103
+ wire_api = "responses"
3104
+ requires_openai_auth = false
3105
+ preferred_auth_method = ""
3106
+ request_max_retries = 4
3107
+ stream_max_retries = 10
3108
+ stream_idle_timeout_ms = 300000
3109
+ `;
3110
+ }
2666
3111
 
2667
- const modelsUrl = buildModelsProbeUrl(baseUrl);
2668
- if (modelsUrl) {
2669
- const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
2670
- results.models = {
2671
- url: modelsUrl,
2672
- status: modelsProbe.status || 0,
2673
- ok: modelsProbe.ok,
2674
- durationMs: modelsProbe.durationMs || 0
2675
- };
3112
+ function buildVirtualDefaultConfig() {
3113
+ return toml.parse(EMPTY_CONFIG_FALLBACK_TEMPLATE);
3114
+ }
2676
3115
 
2677
- if (!modelsProbe.ok) {
2678
- issues.push({
2679
- code: 'remote-models-unreachable',
2680
- message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
2681
- suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
2682
- });
2683
- } else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
2684
- issues.push({
2685
- code: 'remote-models-auth-failed',
2686
- message: '模型列表鉴权失败(401/403)',
2687
- suggestion: '检查 API Key 或认证方式'
2688
- });
2689
- } else if (modelsProbe.status >= 400) {
2690
- issues.push({
2691
- code: 'remote-models-http-error',
2692
- message: `模型列表返回异常状态: ${modelsProbe.status}`,
2693
- suggestion: '确认 /v1/models 可用'
2694
- });
2695
- } else {
2696
- let payload = null;
2697
- try {
2698
- payload = modelsProbe.body ? JSON.parse(modelsProbe.body) : null;
2699
- } catch (e) {
2700
- issues.push({
2701
- code: 'remote-models-parse',
2702
- message: '模型列表解析失败(非 JSON)',
2703
- suggestion: '确认 /v1/models 返回 JSON'
2704
- });
2705
- }
3116
+ function sanitizeRemovedBuiltinProxyProvider(config) {
3117
+ const safeConfig = isPlainObject(config) ? config : {};
3118
+ const providers = isPlainObject(safeConfig.model_providers) ? safeConfig.model_providers : null;
3119
+ const currentProvider = typeof safeConfig.model_provider === 'string' ? safeConfig.model_provider.trim() : '';
3120
+ const hasRemovedBuiltin = !!(providers && providers[BUILTIN_PROXY_PROVIDER_NAME]);
3121
+ const currentIsRemovedBuiltin = currentProvider === BUILTIN_PROXY_PROVIDER_NAME;
2706
3122
 
2707
- if (payload) {
2708
- const ids = extractModelIds(payload);
2709
- if (ids.length === 0) {
2710
- issues.push({
2711
- code: 'remote-models-empty',
2712
- message: '模型列表为空或结构无法识别',
2713
- suggestion: '确认 provider 是否兼容 /v1/models'
2714
- });
2715
- } else if (modelName && !ids.includes(modelName)) {
2716
- issues.push({
2717
- code: 'remote-model-unavailable',
2718
- message: `远程模型列表中未找到: ${modelName}`,
2719
- suggestion: '切换模型或确认模型名称'
2720
- });
2721
- }
2722
- }
2723
- }
3123
+ if (!hasRemovedBuiltin && !currentIsRemovedBuiltin) {
3124
+ return safeConfig;
2724
3125
  }
2725
3126
 
2726
- const modelProbeSpec = buildModelProbeSpec(provider, modelName, baseUrl);
2727
- if (modelProbeSpec && modelProbeSpec.url) {
2728
- const modelProbe = await probeJsonPost(modelProbeSpec.url, modelProbeSpec.body, {
2729
- apiKey: authValue,
2730
- timeoutMs,
2731
- maxBytes: 256 * 1024
2732
- });
2733
-
2734
- results.modelProbe = {
2735
- url: modelProbeSpec.url,
2736
- status: modelProbe.status || 0,
2737
- ok: modelProbe.ok,
2738
- durationMs: modelProbe.durationMs || 0
2739
- };
2740
-
2741
- if (!modelProbe.ok) {
2742
- issues.push({
2743
- code: 'remote-model-probe-unreachable',
2744
- message: `模型可用性探测失败:${modelProbe.error || '无法连接'}`,
2745
- suggestion: '检查网络或模型接口是否可用'
2746
- });
2747
- } else if (modelProbe.status === 401 || modelProbe.status === 403) {
2748
- issues.push({
2749
- code: 'remote-model-probe-auth-failed',
2750
- message: '模型可用性探测鉴权失败(401/403)',
2751
- suggestion: '检查 API Key 或认证方式'
2752
- });
2753
- } else if (modelProbe.status >= 400) {
2754
- issues.push({
2755
- code: 'remote-model-probe-http-error',
2756
- message: `模型可用性探测返回异常状态: ${modelProbe.status}`,
2757
- suggestion: '检查模型或接口路径'
2758
- });
2759
- } else {
2760
- let payload = null;
2761
- try {
2762
- payload = modelProbe.body ? JSON.parse(modelProbe.body) : null;
2763
- } catch (e) {
2764
- issues.push({
2765
- code: 'remote-model-probe-parse',
2766
- message: '模型可用性探测解析失败(非 JSON)',
2767
- suggestion: '确认模型接口返回 JSON'
2768
- });
2769
- }
2770
- if (payload && payload.error) {
2771
- const message = typeof payload.error.message === 'string'
2772
- ? payload.error.message
2773
- : '模型接口返回错误';
2774
- issues.push({
2775
- code: 'remote-model-probe-error',
2776
- message: `模型可用性探测失败:${message}`,
2777
- suggestion: '检查模型名与权限'
2778
- });
2779
- }
2780
- }
2781
- }
3127
+ const nextProviders = providers ? { ...providers } : {};
3128
+ delete nextProviders[BUILTIN_PROXY_PROVIDER_NAME];
3129
+ const providerNames = Object.keys(nextProviders);
3130
+ const fallbackProvider = providerNames[0] || '';
3131
+ const currentModels = readCurrentModels();
3132
+ const fallbackModel = fallbackProvider
3133
+ ? (currentModels[fallbackProvider] || (typeof safeConfig.model === 'string' ? safeConfig.model : ''))
3134
+ : '';
2782
3135
 
2783
- return { issues, results };
3136
+ return {
3137
+ ...safeConfig,
3138
+ model_providers: nextProviders,
3139
+ model_provider: currentIsRemovedBuiltin ? fallbackProvider : safeConfig.model_provider,
3140
+ model: currentIsRemovedBuiltin ? fallbackModel : safeConfig.model
3141
+ };
2784
3142
  }
2785
3143
 
2786
- async function buildConfigHealthReport(params = {}) {
2787
- const issues = [];
2788
- const status = readConfigOrVirtualDefault();
2789
- const config = status.config || {};
2790
-
2791
- if (status.isVirtual) {
2792
- const parseFailed = status.errorType === 'parse';
2793
- const readFailed = status.errorType === 'read';
2794
- issues.push({
2795
- code: parseFailed ? 'config-parse-failed' : (readFailed ? 'config-read-failed' : 'config-missing'),
2796
- message: status.reason || (parseFailed
2797
- ? 'config.toml 解析失败'
2798
- : (readFailed ? '读取 config.toml 失败' : '未检测到 config.toml')),
2799
- suggestion: parseFailed
2800
- ? '修复 config.toml 语法错误后重试'
2801
- : (readFailed ? '检查文件权限后重试' : '在模板编辑器中确认应用配置,生成可用的 config.toml')
2802
- });
2803
- if (parseFailed || readFailed) {
3144
+ function readConfigOrVirtualDefault() {
3145
+ if (fs.existsSync(CONFIG_FILE)) {
3146
+ try {
3147
+ removePersistedBuiltinProxyProviderFromConfig();
2804
3148
  return {
2805
- ok: false,
2806
- issues,
2807
- summary: {
2808
- currentProvider: '',
2809
- currentModel: ''
2810
- },
2811
- remote: null
2812
- };
2813
- }
2814
- }
2815
-
2816
- const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
2817
- const modelName = typeof config.model === 'string' ? config.model.trim() : '';
2818
- if (!providerName) {
2819
- issues.push({
2820
- code: 'provider-missing',
2821
- message: '当前 provider 未设置',
2822
- suggestion: '在模板中设置 model_provider'
2823
- });
2824
- }
2825
-
2826
- if (!modelName) {
2827
- issues.push({
2828
- code: 'model-missing',
2829
- message: '当前模型未设置',
2830
- suggestion: '在模板中设置 model'
2831
- });
2832
- }
2833
-
2834
- const providers = config.model_providers && typeof config.model_providers === 'object'
2835
- ? config.model_providers
2836
- : {};
2837
- const provider = providerName ? providers[providerName] : null;
2838
- if (providerName && !provider) {
2839
- issues.push({
2840
- code: 'provider-not-found',
2841
- message: `当前 provider 未在配置中找到: ${providerName}`,
2842
- suggestion: '检查 model_providers 是否包含该 provider 配置块'
2843
- });
2844
- }
2845
-
2846
- if (provider && typeof provider === 'object') {
2847
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
2848
- if (!isValidHttpUrl(baseUrl)) {
2849
- issues.push({
2850
- code: 'base-url-invalid',
2851
- message: '当前 provider 的 base_url 无效',
2852
- suggestion: '请设置为 http/https 的完整 URL'
2853
- });
2854
- }
2855
-
2856
- const requiresAuth = provider.requires_openai_auth;
2857
- if (requiresAuth !== false) {
2858
- const apiKey = typeof provider.preferred_auth_method === 'string'
2859
- ? provider.preferred_auth_method.trim()
2860
- : '';
2861
- if (!apiKey) {
2862
- issues.push({
2863
- code: 'api-key-missing',
2864
- message: '当前 provider 未配置 API Key',
2865
- suggestion: '在模板中设置 preferred_auth_method'
2866
- });
2867
- }
2868
- }
2869
- }
2870
-
2871
- if (modelName) {
2872
- const models = readModels();
2873
- if (!models.includes(modelName)) {
2874
- issues.push({
2875
- code: 'model-unavailable',
2876
- message: `模型未在可用列表中找到: ${modelName}`,
2877
- suggestion: '在模型列表中添加该模型或切换到已有模型'
2878
- });
2879
- }
2880
- }
2881
-
2882
- const remoteEnabled = !!params.remote;
2883
- let remote = null;
2884
- if (remoteEnabled) {
2885
- const baseUrl = provider && typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
2886
- if (!provider) {
2887
- issues.push({
2888
- code: 'remote-skip-provider',
2889
- message: '无法进行远程探测:provider 未找到',
2890
- suggestion: '检查 model_provider 配置或关闭远程探测'
2891
- });
2892
- } else if (!isValidHttpUrl(baseUrl)) {
2893
- issues.push({
2894
- code: 'remote-skip-base-url',
2895
- message: '无法进行远程探测:base_url 无效',
2896
- suggestion: '补全 base_url 或关闭远程探测'
2897
- });
2898
- } else {
2899
- const timeoutMs = Number.isFinite(params.timeoutMs)
2900
- ? Math.max(1000, Number(params.timeoutMs))
2901
- : undefined;
2902
- const apiKey = typeof provider.preferred_auth_method === 'string'
2903
- ? provider.preferred_auth_method.trim()
2904
- : '';
2905
- const speedResult = await runSpeedTest(baseUrl, apiKey, { timeoutMs });
2906
- const status = speedResult && typeof speedResult.status === 'number'
2907
- ? speedResult.status
2908
- : 0;
2909
- const durationMs = speedResult && typeof speedResult.durationMs === 'number'
2910
- ? speedResult.durationMs
2911
- : 0;
2912
- const error = speedResult && speedResult.error ? String(speedResult.error) : '';
2913
- remote = {
2914
- type: 'speed-test',
2915
- url: baseUrl,
2916
- ok: !!speedResult.ok,
2917
- status,
2918
- durationMs,
2919
- error
2920
- };
2921
-
2922
- if (!speedResult.ok) {
2923
- const errorLower = error.toLowerCase();
2924
- if (errorLower.includes('timeout')) {
2925
- issues.push({
2926
- code: 'remote-speedtest-timeout',
2927
- message: '远程测速超时',
2928
- suggestion: '检查网络或 base_url 是否可达'
2929
- });
2930
- } else if (errorLower.includes('invalid url')) {
2931
- issues.push({
2932
- code: 'remote-speedtest-invalid-url',
2933
- message: '远程测速失败:base_url 无效',
2934
- suggestion: '请设置为 http/https 的完整 URL'
2935
- });
2936
- } else {
2937
- issues.push({
2938
- code: 'remote-speedtest-unreachable',
2939
- message: `远程测速失败:${error || '无法连接'}`,
2940
- suggestion: '检查网络或 base_url 是否可用'
2941
- });
2942
- }
2943
- } else if (status === 401 || status === 403) {
2944
- issues.push({
2945
- code: 'remote-speedtest-auth-failed',
2946
- message: '远程测速鉴权失败(401/403)',
2947
- suggestion: '检查 API Key 或认证方式'
2948
- });
2949
- } else if (status >= 400) {
2950
- issues.push({
2951
- code: 'remote-speedtest-http-error',
2952
- message: `远程测速返回异常状态: ${status}`,
2953
- suggestion: '检查 base_url 或服务状态'
2954
- });
2955
- }
2956
- }
2957
- }
2958
-
2959
- return {
2960
- ok: issues.length === 0,
2961
- issues,
2962
- summary: {
2963
- currentProvider: providerName,
2964
- currentModel: modelName
2965
- },
2966
- remote
2967
- };
2968
- }
2969
-
2970
- function buildDefaultConfigContent(initializedAt) {
2971
- const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
2972
- return `${CODEXMATE_MANAGED_MARKER}
2973
- # codexmate-initialized-at: ${initializedAt}
2974
-
2975
- model_provider = "openai"
2976
- model = "${defaultModel}"
2977
-
2978
- [model_providers.openai]
2979
- name = "openai"
2980
- base_url = "https://api.openai.com/v1"
2981
- wire_api = "responses"
2982
- requires_openai_auth = false
2983
- preferred_auth_method = ""
2984
- request_max_retries = 4
2985
- stream_max_retries = 10
2986
- stream_idle_timeout_ms = 300000
2987
- `;
2988
- }
2989
-
2990
- function buildVirtualDefaultConfig() {
2991
- return toml.parse(EMPTY_CONFIG_FALLBACK_TEMPLATE);
2992
- }
2993
-
2994
- function readConfigOrVirtualDefault() {
2995
- if (fs.existsSync(CONFIG_FILE)) {
2996
- try {
2997
- return {
2998
- config: readConfig(),
2999
- isVirtual: false,
3000
- reason: '',
3001
- detail: '',
3002
- errorType: ''
3149
+ config: sanitizeRemovedBuiltinProxyProvider(readConfig()),
3150
+ isVirtual: false,
3151
+ reason: '',
3152
+ detail: '',
3153
+ errorType: ''
3003
3154
  };
3004
3155
  } catch (e) {
3005
3156
  const errorType = typeof e.configErrorType === 'string' && e.configErrorType.trim()
@@ -3012,7 +3163,9 @@ function readConfigOrVirtualDefault() {
3012
3163
  ? e.configDetail.trim()
3013
3164
  : (e && e.message ? e.message : publicReason);
3014
3165
  return {
3015
- config: errorType === 'missing' ? buildVirtualDefaultConfig() : {},
3166
+ config: errorType === 'missing'
3167
+ ? sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig())
3168
+ : {},
3016
3169
  isVirtual: true,
3017
3170
  reason: publicReason,
3018
3171
  detail,
@@ -3022,7 +3175,7 @@ function readConfigOrVirtualDefault() {
3022
3175
  }
3023
3176
 
3024
3177
  return {
3025
- config: buildVirtualDefaultConfig(),
3178
+ config: sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig()),
3026
3179
  isVirtual: true,
3027
3180
  reason: '未检测到 config.toml',
3028
3181
  detail: `配置文件不存在: ${CONFIG_FILE}`,
@@ -3108,6 +3261,45 @@ function applyReasoningEffortToTemplate(template, reasoningEffort) {
3108
3261
  return content;
3109
3262
  }
3110
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
+
3111
3303
  function getConfigTemplate(params = {}) {
3112
3304
  let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
3113
3305
  if (fs.existsSync(CONFIG_FILE)) {
@@ -3118,8 +3310,22 @@ function getConfigTemplate(params = {}) {
3118
3310
  }
3119
3311
  } catch (e) {}
3120
3312
  }
3121
- const selectedProvider = params.provider || '';
3122
- const selectedModel = params.model || '';
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
+ }
3327
+ const selectedProvider = typeof params.provider === 'string' ? params.provider.trim() : '';
3328
+ const selectedModel = typeof params.model === 'string' ? params.model.trim() : '';
3123
3329
  let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel);
3124
3330
  if (typeof params.serviceTier === 'string') {
3125
3331
  template = applyServiceTierToTemplate(template, params.serviceTier);
@@ -3127,11 +3333,54 @@ function getConfigTemplate(params = {}) {
3127
3333
  if (typeof params.reasoningEffort === 'string') {
3128
3334
  template = applyReasoningEffortToTemplate(template, params.reasoningEffort);
3129
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
+ }
3130
3364
  return {
3131
3365
  template
3132
3366
  };
3133
3367
  }
3134
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
+
3135
3384
  function applyConfigTemplate(params = {}) {
3136
3385
  const template = typeof params.template === 'string' ? params.template : '';
3137
3386
  if (!template.trim()) {
@@ -3145,6 +3394,20 @@ function applyConfigTemplate(params = {}) {
3145
3394
  return { error: `模板 TOML 解析失败: ${e.message}` };
3146
3395
  }
3147
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
+
3148
3411
  if (!parsed.model_provider || typeof parsed.model_provider !== 'string') {
3149
3412
  return { error: '模板缺少 model_provider' };
3150
3413
  }
@@ -3196,7 +3459,7 @@ function addProviderToConfig(params = {}) {
3196
3459
  return { error: 'local provider 为系统保留名称,不可新增' };
3197
3460
  }
3198
3461
  if (isBuiltinProxyProvider(name) && !allowManaged) {
3199
- return { error: '本地代理配置为系统内建项,不可手动添加' };
3462
+ return { error: 'codexmate-proxy 为保留名称,不可手动添加' };
3200
3463
  }
3201
3464
 
3202
3465
  ensureConfigDir();
@@ -3274,7 +3537,7 @@ function updateProviderInConfig(params = {}) {
3274
3537
  if (isDefaultLocalProvider(name)) {
3275
3538
  return { error: 'local provider 为系统保留项,不可编辑' };
3276
3539
  }
3277
- return { error: '本地代理配置为系统内建项,不可编辑' };
3540
+ return { error: 'codexmate-proxy 为保留名称,不可编辑' };
3278
3541
  }
3279
3542
 
3280
3543
  try {
@@ -3292,7 +3555,7 @@ function deleteProviderFromConfig(params = {}) {
3292
3555
  if (isDefaultLocalProvider(name)) {
3293
3556
  return { error: 'local provider 为系统保留项,不可删除' };
3294
3557
  }
3295
- return { error: '本地代理配置为系统内建项,不可删除' };
3558
+ return { error: 'codexmate-proxy 为保留名称,不可删除' };
3296
3559
  }
3297
3560
  if (!fs.existsSync(CONFIG_FILE)) {
3298
3561
  return { error: 'config.toml 不存在' };
@@ -3322,7 +3585,7 @@ function performProviderDeletion(name, options = {}) {
3322
3585
  if (isNonDeletableProvider(name)) {
3323
3586
  const msg = isDefaultLocalProvider(name)
3324
3587
  ? 'local provider 为系统保留项,不可删除'
3325
- : '本地代理配置为系统内建项,不可删除';
3588
+ : 'codexmate-proxy 为保留名称,不可删除';
3326
3589
  if (!silent) console.error('错误:', msg);
3327
3590
  return { error: msg };
3328
3591
  }
@@ -3611,7 +3874,7 @@ function normalizePathForCompare(targetPath, options = {}) {
3611
3874
  return ignoreCase ? resolved.toLowerCase() : resolved;
3612
3875
  }
3613
3876
 
3614
- function isPathInside(targetPath, rootPath) {
3877
+ function isPathInside(targetPath, rootPath) {
3615
3878
  if (!targetPath || !rootPath) {
3616
3879
  return false;
3617
3880
  }
@@ -3621,9 +3884,33 @@ function isPathInside(targetPath, rootPath) {
3621
3884
  if (resolvedTarget === resolvedRoot) {
3622
3885
  return true;
3623
3886
  }
3624
- const rootWithSlash = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
3625
- return resolvedTarget.startsWith(rootWithSlash);
3626
- }
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
+ }
3627
3914
 
3628
3915
  function collectJsonlFiles(rootDir, maxFiles = 5000) {
3629
3916
  if (!fs.existsSync(rootDir)) {
@@ -4408,25 +4695,7 @@ function extractMessageFromRecord(record, source) {
4408
4695
  return { role, text };
4409
4696
  }
4410
4697
 
4411
- function scanSessionContentForQuery(session, tokens, options = {}) {
4412
- if (!session || !Array.isArray(tokens) || tokens.length === 0) {
4413
- return { hit: false, count: 0, snippets: [] };
4414
- }
4415
-
4416
- const filePath = resolveSessionFilePath(session.source, session.filePath, session.sessionId);
4417
- if (!filePath) {
4418
- return { hit: false, count: 0, snippets: [] };
4419
- }
4420
-
4421
- const maxBytes = Number.isFinite(Number(options.maxBytes))
4422
- ? Math.max(1024, Number(options.maxBytes))
4423
- : SESSION_CONTENT_READ_BYTES;
4424
- const headText = getFileHeadText(filePath, maxBytes);
4425
- if (!headText) {
4426
- return { hit: false, count: 0, snippets: [] };
4427
- }
4428
-
4429
- const records = parseJsonlContent(headText);
4698
+ function createSessionQueryScanState(tokens, options = {}) {
4430
4699
  const mode = normalizeQueryMode(options.mode);
4431
4700
  const roleFilter = normalizeRoleFilter(options.roleFilter);
4432
4701
  const maxMatches = Number.isFinite(Number(options.maxMatches))
@@ -4436,43 +4705,137 @@ function scanSessionContentForQuery(session, tokens, options = {}) {
4436
4705
  ? Math.max(0, Number(options.snippetLimit))
4437
4706
  : 0;
4438
4707
 
4439
- const messages = [];
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));
4746
+ }
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);
4761
+ }
4762
+
4440
4763
  for (const record of records) {
4441
- const message = extractMessageFromRecord(record, session.source);
4442
- if (!message || !message.text) {
4764
+ const message = extractMessageFromRecord(record, source);
4765
+ if (!message) {
4443
4766
  continue;
4444
4767
  }
4445
- messages.push(message);
4768
+ if (consumeSessionQueryMessage(state, message)) {
4769
+ break;
4770
+ }
4446
4771
  }
4447
4772
 
4448
- const filteredMessages = roleFilter === 'system'
4449
- ? messages
4450
- : removeLeadingSystemMessage(messages);
4773
+ return buildSessionQueryScanResult(state);
4774
+ }
4451
4775
 
4452
- let count = 0;
4453
- const snippets = [];
4776
+ async function scanSessionContentForQuery(session, tokens, options = {}) {
4777
+ if (!session || !Array.isArray(tokens) || tokens.length === 0) {
4778
+ return { hit: false, count: 0, snippets: [] };
4779
+ }
4454
4780
 
4455
- for (const message of filteredMessages) {
4456
- if (roleFilter !== 'all' && message.role !== roleFilter) {
4457
- continue;
4458
- }
4459
- if (!matchTokensInText(message.text, tokens, mode)) {
4460
- continue;
4781
+ const filePath = resolveSessionFilePath(session.source, session.filePath, session.sessionId);
4782
+ if (!filePath) {
4783
+ return { hit: false, count: 0, snippets: [] };
4784
+ }
4785
+
4786
+ const rawMaxBytes = Number(options.maxBytes);
4787
+ const maxBytes = Number.isFinite(rawMaxBytes) && rawMaxBytes > 0
4788
+ ? Math.max(1024, rawMaxBytes)
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 });
4796
+
4797
+ let bytesRead = 0;
4798
+ for await (const line of rl) {
4799
+ if (maxBytes > 0 && bytesRead >= maxBytes) {
4800
+ break;
4801
+ }
4802
+
4803
+ bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
4804
+ const trimmed = line.trim();
4805
+ if (!trimmed) {
4806
+ continue;
4807
+ }
4808
+
4809
+ let record;
4810
+ try {
4811
+ record = JSON.parse(trimmed);
4812
+ } catch (e) {
4813
+ continue;
4814
+ }
4815
+
4816
+ const message = extractMessageFromRecord(record, session.source);
4817
+ if (!message) {
4818
+ continue;
4819
+ }
4820
+ if (consumeSessionQueryMessage(state, message)) {
4821
+ break;
4822
+ }
4461
4823
  }
4462
4824
 
4463
- count += 1;
4464
- if (snippetLimit > 0 && snippets.length < snippetLimit) {
4465
- 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) {}
4466
4831
  }
4467
- if (count >= maxMatches) {
4468
- break;
4832
+ if (stream && !stream.destroyed && stream.destroy) {
4833
+ try { stream.destroy(); } catch (e) {}
4469
4834
  }
4470
4835
  }
4471
-
4472
- return { hit: count > 0, count, snippets };
4473
4836
  }
4474
4837
 
4475
- function applySessionQueryFilter(sessions, options = {}) {
4838
+ async function applySessionQueryFilter(sessions, options = {}) {
4476
4839
  const tokens = Array.isArray(options.tokens) ? options.tokens : [];
4477
4840
  if (tokens.length === 0) {
4478
4841
  return sessions;
@@ -4486,7 +4849,7 @@ function applySessionQueryFilter(sessions, options = {}) {
4486
4849
  : DEFAULT_CONTENT_SCAN_LIMIT;
4487
4850
  const contentScanBytes = Number.isFinite(Number(options.contentScanBytes))
4488
4851
  ? Math.max(1024, Number(options.contentScanBytes))
4489
- : SESSION_CONTENT_READ_BYTES;
4852
+ : 0;
4490
4853
 
4491
4854
  let scanned = 0;
4492
4855
  const results = [];
@@ -4504,7 +4867,7 @@ function applySessionQueryFilter(sessions, options = {}) {
4504
4867
  const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
4505
4868
  if (shouldScanContent && scanned < contentScanLimit) {
4506
4869
  scanned += 1;
4507
- contentInfo = scanSessionContentForQuery(session, tokens, {
4870
+ contentInfo = await scanSessionContentForQuery(session, tokens, {
4508
4871
  mode,
4509
4872
  roleFilter,
4510
4873
  maxBytes: contentScanBytes,
@@ -4979,7 +5342,7 @@ function listClaudeSessions(limit, options = {}) {
4979
5342
  return mergeAndLimitSessions(sessions, limit);
4980
5343
  }
4981
5344
 
4982
- function listAllSessions(params = {}) {
5345
+ async function listAllSessions(params = {}) {
4983
5346
  const source = params.source === 'codex' || params.source === 'claude'
4984
5347
  ? params.source
4985
5348
  : 'all';
@@ -5021,7 +5384,7 @@ function listAllSessions(params = {}) {
5021
5384
 
5022
5385
  let result = sessions;
5023
5386
  if (hasQuery) {
5024
- result = applySessionQueryFilter(result, {
5387
+ result = await applySessionQueryFilter(result, {
5025
5388
  tokens: queryTokens,
5026
5389
  queryMode: params.queryMode,
5027
5390
  queryScope: params.queryScope,
@@ -5057,7 +5420,7 @@ async function listAllSessionsData(params = {}) {
5057
5420
  }
5058
5421
  }
5059
5422
 
5060
- const sessions = listAllSessions(params);
5423
+ const sessions = await listAllSessions(params);
5061
5424
  const hydratedSessions = await hydrateSessionItemsExactMessageCount(sessions);
5062
5425
  const result = hydratedSessions.map((item) => {
5063
5426
  if (!item || typeof item !== 'object' || Array.isArray(item)) {
@@ -5306,6 +5669,124 @@ function buildProxyListenUrl(settings) {
5306
5669
  return `http://${host}:${settings.port}`;
5307
5670
  }
5308
5671
 
5672
+ function buildBuiltinProxyProviderBaseUrl(settings) {
5673
+ return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
5674
+ }
5675
+
5676
+ function buildBuiltinProxyProviderConfig(settings) {
5677
+ return {
5678
+ name: BUILTIN_PROXY_PROVIDER_NAME,
5679
+ base_url: buildBuiltinProxyProviderBaseUrl(settings),
5680
+ wire_api: 'responses',
5681
+ requires_openai_auth: false,
5682
+ preferred_auth_method: '',
5683
+ request_max_retries: 4,
5684
+ stream_max_retries: 10,
5685
+ stream_idle_timeout_ms: 300000
5686
+ };
5687
+ }
5688
+
5689
+ function injectBuiltinProxyProvider(config) {
5690
+ return isPlainObject(config) ? config : {};
5691
+ }
5692
+
5693
+ function removePersistedBuiltinProxyProviderFromConfig() {
5694
+ if (!fs.existsSync(CONFIG_FILE)) {
5695
+ return { success: true, removed: false };
5696
+ }
5697
+
5698
+ let config;
5699
+ try {
5700
+ config = readConfig();
5701
+ } catch (e) {
5702
+ return { error: e.message || '读取 config.toml 失败' };
5703
+ }
5704
+
5705
+ if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
5706
+ return { success: true, removed: false };
5707
+ }
5708
+
5709
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
5710
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
5711
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
5712
+ const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
5713
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
5714
+ ? providerConfig.__codexmate_legacy_segments
5715
+ : null;
5716
+ const providerSegmentVariants = (() => {
5717
+ const variants = [];
5718
+ const seen = new Set();
5719
+ const pushVariant = (segments) => {
5720
+ const normalized = normalizeLegacySegments(segments);
5721
+ const key = buildLegacySegmentsKey(normalized);
5722
+ if (!key || seen.has(key)) return;
5723
+ seen.add(key);
5724
+ variants.push(normalized);
5725
+ };
5726
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
5727
+ pushVariant(providerConfig.__codexmate_legacy_segments);
5728
+ }
5729
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
5730
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
5731
+ pushVariant(segments);
5732
+ }
5733
+ }
5734
+ if (providerSegments) {
5735
+ pushVariant(providerSegments);
5736
+ }
5737
+ if (variants.length === 0) {
5738
+ pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
5739
+ }
5740
+ return variants;
5741
+ })();
5742
+
5743
+ let updatedContent = null;
5744
+ const combinedRanges = [];
5745
+ for (const segments of providerSegmentVariants) {
5746
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
5747
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
5748
+ }
5749
+ if (combinedRanges.length === 0) {
5750
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
5751
+ }
5752
+
5753
+ if (combinedRanges.length > 0) {
5754
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
5755
+ const seen = new Set();
5756
+ let removedContent = content;
5757
+ for (const range of sorted) {
5758
+ const rangeKey = `${range.start}:${range.end}`;
5759
+ if (seen.has(rangeKey)) continue;
5760
+ seen.add(rangeKey);
5761
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
5762
+ }
5763
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
5764
+ }
5765
+
5766
+ if (!updatedContent) {
5767
+ const rebuilt = JSON.parse(JSON.stringify(config));
5768
+ delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
5769
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
5770
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
5771
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
5772
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
5773
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
5774
+ }
5775
+ updatedContent = rebuiltToml + lineEnding;
5776
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
5777
+ updatedContent = '\uFEFF' + updatedContent;
5778
+ }
5779
+ }
5780
+
5781
+ try {
5782
+ writeConfig(updatedContent.trimEnd() + lineEnding);
5783
+ } catch (e) {
5784
+ return { error: e.message || '写入 config.toml 失败' };
5785
+ }
5786
+
5787
+ return { success: true, removed: true };
5788
+ }
5789
+
5309
5790
  function hasCodexConfigReadyForProxy() {
5310
5791
  const result = readConfigOrVirtualDefault();
5311
5792
  if (!result || result.isVirtual) {
@@ -5582,131 +6063,11 @@ function getBuiltinProxyStatus() {
5582
6063
  }
5583
6064
 
5584
6065
  function applyBuiltinProxyProvider(params = {}) {
5585
- const settings = readBuiltinProxySettings();
5586
- const hostForUrl = formatHostForUrl(settings.host);
5587
- const baseUrl = `http://${hostForUrl}:${settings.port}`;
5588
-
5589
- const { config } = readConfigOrVirtualDefault();
5590
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
5591
- const exists = !!providers[BUILTIN_PROXY_PROVIDER_NAME];
5592
- const saveResult = exists
5593
- ? updateProviderInConfig({
5594
- name: BUILTIN_PROXY_PROVIDER_NAME,
5595
- url: baseUrl,
5596
- key: '',
5597
- allowManaged: true
5598
- })
5599
- : addProviderToConfig({
5600
- name: BUILTIN_PROXY_PROVIDER_NAME,
5601
- url: baseUrl,
5602
- key: '',
5603
- allowManaged: true
5604
- });
5605
-
5606
- if (saveResult && saveResult.error) {
5607
- return saveResult;
5608
- }
5609
-
5610
- const switchToProxy = params.switchToProxy !== false;
5611
- let targetModel = '';
5612
- if (switchToProxy) {
5613
- try {
5614
- targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || '';
5615
- } catch (e) {
5616
- return { error: `写入代理 provider 成功,但切换失败: ${e.message}` };
5617
- }
5618
- }
5619
-
5620
- return {
5621
- success: true,
5622
- provider: BUILTIN_PROXY_PROVIDER_NAME,
5623
- baseUrl,
5624
- switched: switchToProxy,
5625
- model: targetModel
5626
- };
6066
+ return { error: '该功能已移除' };
5627
6067
  }
5628
6068
 
5629
- async function ensureBuiltinProxyForCodexDefault(params = {}) {
5630
- const payload = isPlainObject(params) ? { ...params } : {};
5631
- const switchToProxy = payload.switchToProxy !== false;
5632
- delete payload.switchToProxy;
5633
- payload.enabled = true;
5634
-
5635
- const saveResult = saveBuiltinProxySettings(payload);
5636
- if (saveResult.error) {
5637
- return { error: saveResult.error };
5638
- }
5639
- let nextSettings = saveResult.settings;
5640
-
5641
- let upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
5642
- if (upstreamResult.error) {
5643
- return { error: upstreamResult.error };
5644
- }
5645
-
5646
- const runtime = g_builtinProxyRuntime;
5647
- const shouldRestart = !!runtime && (
5648
- runtime.settings.host !== nextSettings.host
5649
- || runtime.settings.port !== nextSettings.port
5650
- || runtime.settings.authSource !== nextSettings.authSource
5651
- || runtime.settings.timeoutMs !== nextSettings.timeoutMs
5652
- || runtime.upstream.providerName !== upstreamResult.providerName
5653
- || runtime.upstream.baseUrl !== upstreamResult.baseUrl
5654
- || runtime.upstream.authHeader !== upstreamResult.authHeader
5655
- );
5656
-
5657
- if (shouldRestart) {
5658
- await stopBuiltinProxyRuntime();
5659
- }
5660
-
5661
- if (!g_builtinProxyRuntime) {
5662
- let startRes = await startBuiltinProxyRuntime(nextSettings);
5663
- if (!startRes.success && /EADDRINUSE/i.test(String(startRes.error || ''))) {
5664
- const fallbackPort = await findAvailablePort(nextSettings.host, nextSettings.port + 1, 30);
5665
- if (fallbackPort > 0) {
5666
- const retrySave = saveBuiltinProxySettings({
5667
- ...nextSettings,
5668
- port: fallbackPort,
5669
- enabled: true
5670
- });
5671
- if (retrySave.success) {
5672
- nextSettings = retrySave.settings;
5673
- upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
5674
- if (upstreamResult.error) {
5675
- return { error: upstreamResult.error };
5676
- }
5677
- startRes = await startBuiltinProxyRuntime(nextSettings);
5678
- }
5679
- }
5680
- }
5681
- if (!startRes.success) {
5682
- return { error: startRes.error || '启动内建代理失败' };
5683
- }
5684
- }
5685
-
5686
- let applyRes = {
5687
- success: true,
5688
- provider: BUILTIN_PROXY_PROVIDER_NAME,
5689
- baseUrl: buildProxyListenUrl(nextSettings),
5690
- switched: false,
5691
- model: ''
5692
- };
5693
- if (switchToProxy) {
5694
- applyRes = applyBuiltinProxyProvider({ switchToProxy: true });
5695
- if (applyRes.error) {
5696
- return applyRes;
5697
- }
5698
- }
5699
-
5700
- const status = getBuiltinProxyStatus();
5701
- return {
5702
- success: true,
5703
- provider: applyRes.provider,
5704
- baseUrl: applyRes.baseUrl,
5705
- switched: applyRes.switched,
5706
- model: applyRes.model || '',
5707
- settings: status.settings,
5708
- runtime: status.runtime
5709
- };
6069
+ async function ensureBuiltinProxyForCodexDefault(params = {}) {
6070
+ return { error: '该功能已移除' };
5710
6071
  }
5711
6072
 
5712
6073
  function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) {
@@ -7236,63 +7597,210 @@ function resolveSpeedTestTarget(params) {
7236
7597
  if (!provider.base_url) {
7237
7598
  return { error: 'Provider missing URL' };
7238
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
+ }
7239
7610
  return {
7611
+ method: 'GET',
7240
7612
  url: provider.base_url,
7241
7613
  apiKey: provider.preferred_auth_method || ''
7242
7614
  };
7243
7615
  }
7244
7616
 
7245
7617
  if (params.url) {
7246
- return { url: params.url, apiKey: '' };
7618
+ return {
7619
+ method: 'GET',
7620
+ url: params.url,
7621
+ apiKey: typeof params.apiKey === 'string' ? params.apiKey : ''
7622
+ };
7247
7623
  }
7248
7624
 
7249
7625
  return { error: 'Missing name or url' };
7250
7626
  }
7251
7627
 
7252
- function runSpeedTest(targetUrl, apiKey, options = {}) {
7253
- return new Promise((resolve) => {
7254
- let parsed;
7255
- try {
7256
- parsed = new URL(targetUrl);
7257
- } catch (e) {
7258
- return resolve({ ok: false, error: 'Invalid URL' });
7259
- }
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
+ }
7260
7646
 
7261
- const timeoutMs = Number.isFinite(options.timeoutMs)
7262
- ? Math.max(1000, Number(options.timeoutMs))
7263
- : SPEED_TEST_TIMEOUT_MS;
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
+ }
7264
7656
 
7265
- const transport = parsed.protocol === 'https:' ? https : http;
7266
- const headers = {
7267
- 'User-Agent': 'codexmate-speed-test',
7268
- 'Accept': 'application/json'
7269
- };
7270
- if (apiKey) {
7271
- headers['Authorization'] = `Bearer ${apiKey}`;
7272
- }
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
+ }
7273
7663
 
7274
- const start = Date.now();
7275
- const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
7276
- res.on('data', () => {});
7277
- res.on('end', () => {
7278
- resolve({
7279
- ok: true,
7280
- status: res.statusCode || 0,
7281
- durationMs: Date.now() - start
7282
- });
7283
- });
7284
- });
7664
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
7665
+ if (!baseUrl) {
7666
+ return { error: `Provider ${providerName} missing URL` };
7667
+ }
7285
7668
 
7286
- req.setTimeout(timeoutMs, () => {
7287
- req.destroy(new Error('timeout'));
7288
- });
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
+ }
7679
+
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
+ }
7289
7704
 
7290
- req.on('error', (err) => {
7291
- resolve({ ok: false, error: err.message, durationMs: Date.now() - start });
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;
7710
+
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
7292
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
+ }
7293
7726
 
7294
- req.end();
7295
- });
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
+ }));
7296
7804
  }
7297
7805
 
7298
7806
  // ============================================================================
@@ -7348,7 +7856,8 @@ async function cmdSetup() {
7348
7856
  const { config } = readConfigOrVirtualDefault();
7349
7857
  const providers = config.model_providers || {};
7350
7858
  const providerNames = Object.keys(providers);
7351
- const defaultProvider = config.model_provider || providerNames[0] || '';
7859
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
7860
+ const defaultProvider = currentProvider || providerNames[0] || '';
7352
7861
  let availableModels = [];
7353
7862
  let defaultModel = config.model || '';
7354
7863
  let modelFetchUnlimited = false;
@@ -7634,7 +8143,7 @@ async function cmdModels() {
7634
8143
 
7635
8144
  // 切换提供商
7636
8145
  function cmdSwitch(providerName, silent = false) {
7637
- const config = readConfig();
8146
+ const config = sanitizeRemovedBuiltinProxyProvider(readConfig());
7638
8147
  const providers = config.model_providers || {};
7639
8148
 
7640
8149
  if (!providers[providerName]) {
@@ -7737,6 +8246,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
7737
8246
  if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
7738
8247
  throw new Error('local provider 为系统保留名称,不可新增');
7739
8248
  }
8249
+ if (isBuiltinProxyProvider(providerName)) {
8250
+ if (!silent) console.error('错误: codexmate-proxy 为保留名称,不可手动添加');
8251
+ throw new Error('codexmate-proxy 为保留名称,不可手动添加');
8252
+ }
7740
8253
 
7741
8254
  const config = readConfig();
7742
8255
  if (config.model_providers && config.model_providers[providerName]) {
@@ -7801,7 +8314,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
7801
8314
  if (isNonEditableProvider(name) && !allowManaged) {
7802
8315
  const msg = isDefaultLocalProvider(name)
7803
8316
  ? 'local provider 为系统保留项,不可编辑'
7804
- : '本地代理配置为系统内建项,不可编辑';
8317
+ : 'codexmate-proxy 为保留名称,不可编辑';
7805
8318
  if (!silent) console.error(`错误: ${msg}`);
7806
8319
  throw new Error(msg);
7807
8320
  }
@@ -9504,10 +10017,19 @@ function formatHostForUrl(host) {
9504
10017
  return value;
9505
10018
  }
9506
10019
 
9507
- function watchPathsForRestart(targets, onChange) {
9508
- const disposers = [];
9509
- const debounceMs = 300;
9510
- 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
+ };
9511
10033
 
9512
10034
  const trigger = (info) => {
9513
10035
  if (timer) clearTimeout(timer);
@@ -9517,35 +10039,205 @@ function watchPathsForRestart(targets, onChange) {
9517
10039
  }, debounceMs);
9518
10040
  };
9519
10041
 
9520
- const addWatcher = (target, recursive) => {
9521
- if (!fs.existsSync(target)) return;
10042
+ const closeWatcher = (watchKey) => {
10043
+ const entry = watcherEntries.get(watchKey);
10044
+ if (!entry) return;
10045
+ watcherEntries.delete(watchKey);
9522
10046
  try {
9523
- 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
+ }
9524
10106
  if (!filename) return;
9525
- const lower = filename.toLowerCase();
9526
- if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
9527
- 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
9528
10137
  });
9529
- disposers.push(() => watcher.close());
9530
10138
  return true;
9531
10139
  } catch (e) {
9532
10140
  return false;
9533
10141
  }
9534
10142
  };
9535
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
+
9536
10209
  for (const target of targets) {
9537
- 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);
9538
10225
  if (!ok) {
9539
- addWatcher(target, false);
10226
+ addWatcher(target, false, false);
9540
10227
  }
9541
10228
  }
9542
10229
 
9543
10230
  return () => {
9544
- for (const dispose of disposers) {
9545
- 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);
9546
10237
  }
9547
10238
  };
9548
10239
  }
10240
+ // #endregion watchPathsForRestart
9549
10241
 
9550
10242
  function writeJsonResponse(res, statusCode, payload) {
9551
10243
  const body = JSON.stringify(payload, null, 2);
@@ -9633,22 +10325,55 @@ function resolveUploadFileNameFromRequest(req, fallbackName = 'codex-skills.zip'
9633
10325
  return normalized || fallback;
9634
10326
  }
9635
10327
 
9636
- async function handleImportCodexSkillsZipUpload(req, res) {
10328
+ function resolveSkillTargetAppFromRequest(req, fallbackApp = 'codex') {
10329
+ const fallbackTarget = resolveSkillTarget({}, fallbackApp);
10330
+ const fallback = fallbackTarget ? fallbackTarget.app : 'codex';
10331
+ try {
10332
+ const parsed = new URL(req.url || '/', 'http://localhost');
10333
+ const hasTargetApp = parsed.searchParams.has('targetApp');
10334
+ const hasTarget = parsed.searchParams.has('target');
10335
+ if (hasTargetApp || hasTarget) {
10336
+ const target = resolveSkillTarget({
10337
+ ...(hasTargetApp ? { targetApp: parsed.searchParams.get('targetApp') } : {}),
10338
+ ...(hasTarget ? { target: parsed.searchParams.get('target') } : {})
10339
+ }, fallback);
10340
+ return target ? target.app : null;
10341
+ }
10342
+ return fallback;
10343
+ } catch (_) {
10344
+ return fallback;
10345
+ }
10346
+ }
10347
+
10348
+ async function handleImportSkillsZipUpload(req, res, options = {}) {
9637
10349
  if (req.method !== 'POST') {
10350
+ if (req && typeof req.resume === 'function') {
10351
+ req.resume();
10352
+ }
9638
10353
  writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
9639
10354
  return;
9640
10355
  }
9641
10356
  try {
9642
- const fileName = resolveUploadFileNameFromRequest(req, 'codex-skills.zip');
10357
+ const forcedTargetApp = normalizeSkillTargetApp(options && options.targetApp ? options.targetApp : '');
10358
+ const targetApp = forcedTargetApp || resolveSkillTargetAppFromRequest(req, 'codex');
10359
+ if (!targetApp) {
10360
+ if (req && typeof req.resume === 'function') {
10361
+ req.resume();
10362
+ }
10363
+ writeJsonResponse(res, 400, { error: '目标宿主不支持' });
10364
+ return;
10365
+ }
10366
+ const fileName = resolveUploadFileNameFromRequest(req, `${targetApp}-skills.zip`);
9643
10367
  const upload = await writeUploadZipStream(
9644
10368
  req,
9645
10369
  'codex-skills-import',
9646
10370
  fileName,
9647
10371
  MAX_SKILLS_ZIP_UPLOAD_SIZE
9648
10372
  );
9649
- const result = await importCodexSkillsFromZipFile(upload.zipPath, {
10373
+ const result = await importSkillsFromZipFile(upload.zipPath, {
9650
10374
  tempDir: upload.tempDir,
9651
- fallbackName: fileName
10375
+ fallbackName: fileName,
10376
+ targetApp
9652
10377
  });
9653
10378
  writeJsonResponse(res, 200, result || {});
9654
10379
  } catch (e) {
@@ -9657,13 +10382,55 @@ async function handleImportCodexSkillsZipUpload(req, res) {
9657
10382
  }
9658
10383
  }
9659
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
+
9660
10411
  function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
9661
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
+ };
9662
10425
 
9663
10426
  const server = http.createServer((req, res) => {
9664
10427
  const requestPath = (req.url || '/').split('?')[0];
10428
+ if (requestPath === '/api/import-skills-zip') {
10429
+ void handleImportSkillsZipUpload(req, res);
10430
+ return;
10431
+ }
9665
10432
  if (requestPath === '/api/import-codex-skills-zip') {
9666
- void handleImportCodexSkillsZipUpload(req, res);
10433
+ void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
9667
10434
  return;
9668
10435
  }
9669
10436
  if (requestPath === '/api') {
@@ -9690,22 +10457,38 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
9690
10457
  let result;
9691
10458
 
9692
10459
  switch (action) {
9693
- case 'status':
10460
+ case 'status': {
9694
10461
  const statusConfigResult = readConfigOrVirtualDefault();
9695
10462
  const config = statusConfigResult.config;
9696
10463
  const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
9697
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
+ );
9698
10478
  result = {
9699
10479
  provider: config.model_provider || '未设置',
9700
10480
  model: config.model || '未设置',
9701
10481
  serviceTier,
9702
10482
  modelReasoningEffort,
10483
+ modelContextWindow,
10484
+ modelAutoCompactTokenLimit,
9703
10485
  configReady: !statusConfigResult.isVirtual,
9704
10486
  configErrorType: statusConfigResult.errorType || '',
9705
10487
  configNotice: statusConfigResult.reason || '',
9706
10488
  initNotice: consumeInitNotice()
9707
10489
  };
9708
10490
  break;
10491
+ }
9709
10492
  case 'install-status':
9710
10493
  result = buildInstallStatusReport();
9711
10494
  break;
@@ -9795,6 +10578,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
9795
10578
  case 'preview-agents-diff':
9796
10579
  result = buildAgentsDiff(params || {});
9797
10580
  break;
10581
+ case 'list-skills':
10582
+ result = listSkills(params || {});
10583
+ break;
10584
+ case 'delete-skills':
10585
+ result = deleteSkills(params || {});
10586
+ break;
10587
+ case 'scan-unmanaged-skills':
10588
+ result = scanUnmanagedSkills(params || {});
10589
+ break;
10590
+ case 'import-skills':
10591
+ result = importSkills(params || {});
10592
+ break;
10593
+ case 'export-skills':
10594
+ result = await exportSkills(params || {});
10595
+ break;
9798
10596
  case 'list-codex-skills':
9799
10597
  result = listCodexSkills();
9800
10598
  break;
@@ -9872,7 +10670,11 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
9872
10670
  result = { error: target.error };
9873
10671
  break;
9874
10672
  }
9875
- 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 || {});
9876
10678
  break;
9877
10679
  }
9878
10680
  case 'list-sessions':
@@ -10053,6 +10855,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10053
10855
  res.end(errorBody, 'utf-8');
10054
10856
  }
10055
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
+ }
10056
10866
  } else if (requestPath.startsWith('/web-ui/')) {
10057
10867
  const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
10058
10868
  const filePath = path.join(__dirname, normalized);
@@ -10061,6 +10871,23 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10061
10871
  res.end('Forbidden');
10062
10872
  return;
10063
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
+ }
10064
10891
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
10065
10892
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
10066
10893
  res.end('Not Found');
@@ -10133,9 +10960,13 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10133
10960
  res.writeHead(200, { 'Content-Type': mime });
10134
10961
  fs.createReadStream(filePath).pipe(res);
10135
10962
  } else {
10136
- const html = fs.readFileSync(htmlPath, 'utf-8');
10137
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
10138
- 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
+ }
10139
10970
  }
10140
10971
  });
10141
10972
 
@@ -10154,7 +10985,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10154
10985
  process.exit(1);
10155
10986
  });
10156
10987
 
10157
- const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
10988
+ const openHost = host === '::'
10989
+ ? '::1'
10990
+ : (host === '0.0.0.0' ? DEFAULT_WEB_OPEN_HOST : host);
10158
10991
  const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
10159
10992
  server.listen(port, host, () => {
10160
10993
  console.log('\n✓ Web UI 已启动:', openUrl);
@@ -10287,6 +11120,7 @@ function cmdStart(options = {}) {
10287
11120
 
10288
11121
  const port = resolveWebPort();
10289
11122
  const host = resolveWebHost(options);
11123
+ releaseRunPortIfNeeded(port, host);
10290
11124
 
10291
11125
  let serverHandle = createWebServer({
10292
11126
  htmlPath,
@@ -10297,25 +11131,6 @@ function cmdStart(options = {}) {
10297
11131
  openBrowser: !options.noBrowser
10298
11132
  });
10299
11133
 
10300
- const proxySettings = readBuiltinProxySettings();
10301
- const shouldAutoStartProxy = proxySettings.enabled || hasCodexConfigReadyForProxy();
10302
- if (shouldAutoStartProxy) {
10303
- ensureBuiltinProxyForCodexDefault({
10304
- ...proxySettings,
10305
- switchToProxy: false
10306
- }).then((res) => {
10307
- if (res && res.success && res.runtime && res.runtime.listenUrl) {
10308
- const entryProvider = res.runtime.provider || DEFAULT_LOCAL_PROVIDER_NAME;
10309
- const upstreamLabel = res.runtime.upstreamProvider ? `(上游: ${res.runtime.upstreamProvider})` : '';
10310
- console.log(`~ 内建代理已启动(${entryProvider}): ${res.runtime.listenUrl}${upstreamLabel}`);
10311
- } else if (res && res.error) {
10312
- console.warn(`! 内建代理启动失败: ${res.error}`);
10313
- }
10314
- }).catch((err) => {
10315
- console.warn(`! 内建代理启动失败: ${err && err.message ? err.message : err}`);
10316
- });
10317
- }
10318
-
10319
11134
  const requestWebUiRestart = createSerializedWebUiRestartHandler(async (info) => {
10320
11135
  const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
10321
11136
  console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
@@ -10507,111 +11322,8 @@ function parseProxyCliOptions(args = []) {
10507
11322
  }
10508
11323
 
10509
11324
  async function cmdProxy(args = []) {
10510
- const subcommand = (args[0] || 'status').toLowerCase();
10511
- const optionResult = parseProxyCliOptions(args.slice(1));
10512
- if (optionResult.error) {
10513
- throw new Error(optionResult.error);
10514
- }
10515
- const options = optionResult.payload || {};
10516
-
10517
- if (subcommand === 'status') {
10518
- const status = getBuiltinProxyStatus();
10519
- const settings = status.settings || DEFAULT_BUILTIN_PROXY_SETTINGS;
10520
- console.log('\n内建代理状态:');
10521
- console.log(' 运行中:', status.running ? '是' : '否');
10522
- console.log(' 启用:', settings.enabled ? '是' : '否');
10523
- console.log(' 监听:', buildProxyListenUrl(settings));
10524
- console.log(' 上游 provider:', settings.provider || '(自动)');
10525
- console.log(' 鉴权来源:', settings.authSource);
10526
- if (status.runtime) {
10527
- console.log(' 实际上游:', status.runtime.upstreamProvider);
10528
- console.log(' 启动时间:', status.runtime.startedAt);
10529
- }
10530
- console.log();
10531
- return;
10532
- }
10533
-
10534
- if (subcommand === 'set' || subcommand === 'config') {
10535
- const result = saveBuiltinProxySettings(options);
10536
- if (result.error) {
10537
- throw new Error(result.error);
10538
- }
10539
- const settings = result.settings;
10540
- console.log('✓ 内建代理配置已保存');
10541
- console.log(' 监听:', buildProxyListenUrl(settings));
10542
- console.log(' 上游 provider:', settings.provider || '(自动)');
10543
- console.log(' 鉴权来源:', settings.authSource);
10544
- console.log();
10545
- return;
10546
- }
10547
-
10548
- if (subcommand === 'apply' || subcommand === 'apply-provider') {
10549
- const result = applyBuiltinProxyProvider({
10550
- switchToProxy: options.switchToProxy !== false
10551
- });
10552
- if (result.error) {
10553
- throw new Error(result.error);
10554
- }
10555
- console.log(`✓ 已写入本地代理 provider: ${result.provider}`);
10556
- console.log(` URL: ${result.baseUrl}`);
10557
- if (result.switched) {
10558
- console.log(` 已切换到 ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
10559
- }
10560
- console.log();
10561
- return;
10562
- }
10563
-
10564
- if (subcommand === 'enable' || subcommand === 'default-codex') {
10565
- const result = await ensureBuiltinProxyForCodexDefault(options);
10566
- if (result.error) {
10567
- throw new Error(result.error);
10568
- }
10569
- const listenUrl = result.runtime && result.runtime.listenUrl
10570
- ? result.runtime.listenUrl
10571
- : buildProxyListenUrl(result.settings || DEFAULT_BUILTIN_PROXY_SETTINGS);
10572
- console.log('✓ 已启用 Codex 内建代理默认模式');
10573
- console.log(` 监听: ${listenUrl}`);
10574
- if (result.runtime && result.runtime.upstreamProvider) {
10575
- console.log(` 上游 provider: ${result.runtime.upstreamProvider}`);
10576
- }
10577
- console.log(` 当前 provider: ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
10578
- console.log();
10579
- return;
10580
- }
10581
-
10582
- if (subcommand === 'start') {
10583
- const result = await startBuiltinProxyRuntime({
10584
- ...options,
10585
- enabled: true
10586
- });
10587
- if (result.error) {
10588
- throw new Error(result.error);
10589
- }
10590
- console.log(`✓ 内建代理已启动: ${result.listenUrl}`);
10591
- console.log(` 上游 provider: ${result.upstreamProvider}`);
10592
- console.log(' 按 Ctrl+C 停止代理\n');
10593
-
10594
- await new Promise((resolve) => {
10595
- let stopping = false;
10596
- const gracefulStop = async () => {
10597
- if (stopping) return;
10598
- stopping = true;
10599
- await stopBuiltinProxyRuntime();
10600
- resolve();
10601
- };
10602
- process.once('SIGINT', gracefulStop);
10603
- process.once('SIGTERM', gracefulStop);
10604
- });
10605
- return;
10606
- }
10607
-
10608
- if (subcommand === 'stop') {
10609
- await stopBuiltinProxyRuntime();
10610
- console.log('✓ 内建代理已停止\n');
10611
- return;
10612
- }
10613
-
10614
- throw new Error(`未知 proxy 子命令: ${subcommand}`);
11325
+ void args;
11326
+ throw new Error('该功能已移除');
10615
11327
  }
10616
11328
 
10617
11329
  function parseWorkflowInputArg(rawInput) {
@@ -11283,11 +11995,26 @@ function buildMcpStatusPayload() {
11283
11995
  const config = statusConfigResult.config;
11284
11996
  const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
11285
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
+ );
11286
12011
  return {
11287
12012
  provider: config.model_provider || '未设置',
11288
12013
  model: config.model || '未设置',
11289
12014
  serviceTier,
11290
12015
  modelReasoningEffort,
12016
+ modelContextWindow,
12017
+ modelAutoCompactTokenLimit,
11291
12018
  configReady: !statusConfigResult.isVirtual,
11292
12019
  configErrorType: statusConfigResult.errorType || '',
11293
12020
  configNotice: statusConfigResult.reason || '',
@@ -11385,6 +12112,8 @@ const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
11385
12112
  model: { type: 'string' },
11386
12113
  serviceTier: { type: 'string' },
11387
12114
  reasoningEffort: { type: 'string' },
12115
+ modelContextWindow: { type: ['string', 'number'] },
12116
+ modelAutoCompactTokenLimit: { type: ['string', 'number'] },
11388
12117
  apply: { type: 'boolean' }
11389
12118
  },
11390
12119
  required: ['provider'],
@@ -11399,7 +12128,9 @@ const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
11399
12128
  provider: '{{input.provider}}',
11400
12129
  model: '{{input.model}}',
11401
12130
  serviceTier: '{{input.serviceTier}}',
11402
- reasoningEffort: '{{input.reasoningEffort}}'
12131
+ reasoningEffort: '{{input.reasoningEffort}}',
12132
+ modelContextWindow: '{{input.modelContextWindow}}',
12133
+ modelAutoCompactTokenLimit: '{{input.modelAutoCompactTokenLimit}}'
11403
12134
  }
11404
12135
  },
11405
12136
  {
@@ -11968,7 +12699,7 @@ function createMcpTools(options = {}) {
11968
12699
 
11969
12700
  pushTool({
11970
12701
  name: 'codexmate.config.template.get',
11971
- 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.',
11972
12703
  readOnly: true,
11973
12704
  inputSchema: {
11974
12705
  type: 'object',
@@ -11976,7 +12707,9 @@ function createMcpTools(options = {}) {
11976
12707
  provider: { type: 'string' },
11977
12708
  model: { type: 'string' },
11978
12709
  serviceTier: { type: 'string' },
11979
- reasoningEffort: { type: 'string' }
12710
+ reasoningEffort: { type: 'string' },
12711
+ modelContextWindow: { type: ['string', 'number'] },
12712
+ modelAutoCompactTokenLimit: { type: ['string', 'number'] }
11980
12713
  },
11981
12714
  additionalProperties: false
11982
12715
  },
@@ -12685,11 +13418,9 @@ async function main() {
12685
13418
  console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
12686
13419
  console.log(' codexmate add-model <模型> 添加模型');
12687
13420
  console.log(' codexmate delete-model <模型> 删除模型');
12688
- console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
12689
- console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
12690
13421
  console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
12691
13422
  console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
12692
- console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)');
13423
+ console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
12693
13424
  console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
12694
13425
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
12695
13426
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');