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.
- package/README.en.md +34 -17
- package/README.md +34 -25
- package/cli/config-health.js +338 -0
- package/cli.js +1570 -839
- package/lib/cli-models-utils.js +186 -27
- package/lib/cli-network-utils.js +117 -101
- package/package.json +8 -1
- package/web-ui/app.js +379 -5754
- package/web-ui/index.html +15 -2079
- package/web-ui/logic.agents-diff.mjs +386 -0
- package/web-ui/logic.claude.mjs +108 -0
- package/web-ui/logic.mjs +5 -793
- package/web-ui/logic.runtime.mjs +124 -0
- package/web-ui/logic.sessions.mjs +263 -0
- package/web-ui/modules/api.mjs +69 -0
- package/web-ui/modules/app.computed.dashboard.mjs +113 -0
- package/web-ui/modules/app.computed.index.mjs +13 -0
- package/web-ui/modules/app.computed.session.mjs +141 -0
- package/web-ui/modules/app.constants.mjs +15 -0
- package/web-ui/modules/app.methods.agents.mjs +493 -0
- package/web-ui/modules/app.methods.claude-config.mjs +174 -0
- package/web-ui/modules/app.methods.codex-config.mjs +640 -0
- package/web-ui/modules/app.methods.index.mjs +86 -0
- package/web-ui/modules/app.methods.install.mjs +157 -0
- package/web-ui/modules/app.methods.navigation.mjs +478 -0
- package/web-ui/modules/app.methods.openclaw-core.mjs +514 -0
- package/web-ui/modules/app.methods.openclaw-editing.mjs +337 -0
- package/web-ui/modules/app.methods.openclaw-persist.mjs +251 -0
- package/web-ui/modules/app.methods.providers.mjs +265 -0
- package/web-ui/modules/app.methods.runtime.mjs +323 -0
- package/web-ui/modules/app.methods.session-actions.mjs +457 -0
- package/web-ui/modules/app.methods.session-browser.mjs +435 -0
- package/web-ui/modules/app.methods.session-timeline.mjs +441 -0
- package/web-ui/modules/app.methods.session-trash.mjs +419 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +406 -0
- package/web-ui/modules/config-mode.computed.mjs +1 -0
- package/web-ui/modules/skills.computed.mjs +26 -1
- package/web-ui/modules/skills.methods.mjs +154 -23
- package/web-ui/partials/index/layout-footer.html +69 -0
- package/web-ui/partials/index/layout-header.html +337 -0
- package/web-ui/partials/index/modal-config-template-agents.html +125 -0
- package/web-ui/partials/index/modal-confirm-toast.html +32 -0
- package/web-ui/partials/index/modal-health-check.html +72 -0
- package/web-ui/partials/index/modal-openclaw-config.html +275 -0
- package/web-ui/partials/index/modal-skills.html +184 -0
- package/web-ui/partials/index/modals-basic.html +196 -0
- package/web-ui/partials/index/panel-config-claude.html +100 -0
- package/web-ui/partials/index/panel-config-codex.html +237 -0
- package/web-ui/partials/index/panel-config-openclaw.html +84 -0
- package/web-ui/partials/index/panel-market.html +174 -0
- package/web-ui/partials/index/panel-sessions.html +387 -0
- package/web-ui/partials/index/panel-settings.html +166 -0
- package/web-ui/session-helpers.mjs +12 -0
- package/web-ui/source-bundle.cjs +233 -0
- package/web-ui/styles/base-theme.css +373 -0
- package/web-ui/styles/controls-forms.css +354 -0
- package/web-ui/styles/feedback.css +108 -0
- package/web-ui/styles/health-check-dialog.css +144 -0
- package/web-ui/styles/layout-shell.css +330 -0
- package/web-ui/styles/modals-core.css +449 -0
- package/web-ui/styles/navigation-panels.css +381 -0
- package/web-ui/styles/openclaw-structured.css +266 -0
- package/web-ui/styles/responsive.css +416 -0
- package/web-ui/styles/sessions-list.css +414 -0
- package/web-ui/styles/sessions-preview.css +405 -0
- package/web-ui/styles/sessions-toolbar-trash.css +243 -0
- package/web-ui/styles/sessions-usage.css +276 -0
- package/web-ui/styles/skills-list.css +298 -0
- package/web-ui/styles/skills-market.css +335 -0
- package/web-ui/styles/titles-cards.css +407 -0
- package/web-ui/styles.css +16 -4499
- package/doc/CHANGELOG.md +0 -32
- 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
|
-
|
|
46
|
-
buildModelsProbeUrl,
|
|
45
|
+
buildModelsCacheKey,
|
|
47
46
|
buildModelProbeSpec,
|
|
48
|
-
|
|
47
|
+
buildModelConversationSpecs,
|
|
48
|
+
extractModelResponseText
|
|
49
49
|
} = require('./lib/cli-models-utils');
|
|
50
|
-
const {
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1394
|
-
const
|
|
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
|
|
1564
|
-
const targetPath = path.join(
|
|
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(
|
|
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 && !
|
|
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
|
|
1599
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
1672
|
-
const
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
2070
|
+
targetApp: target.app,
|
|
2071
|
+
targetLabel: target.label,
|
|
2072
|
+
root: target.dir,
|
|
1710
2073
|
items,
|
|
1711
|
-
sources:
|
|
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
|
|
1721
|
-
|
|
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
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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 =
|
|
1778
|
-
const targetRelative =
|
|
1779
|
-
if (targetRelative.startsWith('..') ||
|
|
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:
|
|
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
|
-
|
|
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
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
const
|
|
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
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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 =
|
|
1946
|
-
const targetRelative =
|
|
1947
|
-
if (targetRelative.startsWith('..') ||
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
|
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(
|
|
2066
|
-
const sourceRelative = path.relative(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
2179
|
-
const relativePath = path.relative(
|
|
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
|
-
|
|
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
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
-
|
|
2630
|
-
const
|
|
2631
|
-
|
|
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
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
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
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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
|
-
|
|
2708
|
-
|
|
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
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
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 {
|
|
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
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
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'
|
|
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
|
-
|
|
3122
|
-
|
|
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
|
|
3625
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
4442
|
-
if (!message
|
|
4764
|
+
const message = extractMessageFromRecord(record, source);
|
|
4765
|
+
if (!message) {
|
|
4443
4766
|
continue;
|
|
4444
4767
|
}
|
|
4445
|
-
|
|
4768
|
+
if (consumeSessionQueryMessage(state, message)) {
|
|
4769
|
+
break;
|
|
4770
|
+
}
|
|
4446
4771
|
}
|
|
4447
4772
|
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
: removeLeadingSystemMessage(messages);
|
|
4773
|
+
return buildSessionQueryScanResult(state);
|
|
4774
|
+
}
|
|
4451
4775
|
|
|
4452
|
-
|
|
4453
|
-
|
|
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
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
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
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
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 (
|
|
4468
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
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
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
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
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
};
|
|
7270
|
-
|
|
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
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
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
|
-
|
|
7287
|
-
|
|
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
|
-
|
|
7291
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9508
|
-
|
|
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
|
|
9521
|
-
|
|
10042
|
+
const closeWatcher = (watchKey) => {
|
|
10043
|
+
const entry = watcherEntries.get(watchKey);
|
|
10044
|
+
if (!entry) return;
|
|
10045
|
+
watcherEntries.delete(watchKey);
|
|
9522
10046
|
try {
|
|
9523
|
-
|
|
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
|
-
|
|
9526
|
-
if (!
|
|
9527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9545
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
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 =
|
|
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
|
-
|
|
10511
|
-
|
|
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]');
|