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