codexmate 0.0.13 → 0.0.15
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 +213 -0
- package/README.md +147 -346
- package/cli.js +3163 -224
- package/doc/CHANGELOG.md +13 -1
- package/doc/CHANGELOG.zh-CN.md +14 -0
- package/lib/cli-utils.js +16 -0
- package/lib/workflow-engine.js +340 -0
- package/package.json +10 -3
- package/web-ui/app.js +275 -65
- package/web-ui/index.html +279 -33
- package/web-ui/logic.mjs +147 -1
- package/web-ui/modules/config-mode.computed.mjs +123 -0
- package/web-ui/modules/skills.computed.mjs +82 -0
- package/web-ui/modules/skills.methods.mjs +344 -0
- package/web-ui/styles.css +648 -10
- package/README.zh-CN.md +0 -419
package/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const crypto = require('crypto');
|
|
|
6
6
|
const toml = require('@iarna/toml');
|
|
7
7
|
const JSON5 = require('json5');
|
|
8
8
|
const zipLib = require('zip-lib');
|
|
9
|
+
const yauzl = require('yauzl');
|
|
9
10
|
const { exec, execSync, spawn, spawnSync } = require('child_process');
|
|
10
11
|
const http = require('http');
|
|
11
12
|
const https = require('https');
|
|
@@ -21,6 +22,8 @@ const {
|
|
|
21
22
|
detectLineEnding,
|
|
22
23
|
normalizeLineEnding,
|
|
23
24
|
isValidProviderName,
|
|
25
|
+
escapeTomlBasicString,
|
|
26
|
+
buildModelProviderTableHeader,
|
|
24
27
|
buildModelsCandidates,
|
|
25
28
|
isValidHttpUrl,
|
|
26
29
|
normalizeBaseUrl,
|
|
@@ -54,6 +57,10 @@ const {
|
|
|
54
57
|
resolveMaxMessagesValue
|
|
55
58
|
} = require('./lib/cli-session-utils');
|
|
56
59
|
const { createMcpStdioServer } = require('./lib/mcp-stdio');
|
|
60
|
+
const {
|
|
61
|
+
validateWorkflowDefinition,
|
|
62
|
+
executeWorkflowDefinition
|
|
63
|
+
} = require('./lib/workflow-engine');
|
|
57
64
|
|
|
58
65
|
const DEFAULT_WEB_PORT = 3737;
|
|
59
66
|
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
@@ -78,6 +85,8 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
78
85
|
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
79
86
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
80
87
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
88
|
+
const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
|
|
89
|
+
const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
|
|
81
90
|
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
82
91
|
const CODEX_BACKUP_NAME = 'codex-config';
|
|
83
92
|
|
|
@@ -98,12 +107,24 @@ const SESSION_SCAN_FACTOR = 4;
|
|
|
98
107
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
99
108
|
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
100
109
|
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
110
|
+
const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
|
|
111
|
+
const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
|
|
112
|
+
const AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
|
|
113
|
+
const SKILL_IMPORT_SOURCES = Object.freeze([
|
|
114
|
+
{ app: 'claude', label: 'Claude Code', dir: CLAUDE_SKILLS_DIR },
|
|
115
|
+
{ app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR }
|
|
116
|
+
]);
|
|
101
117
|
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
102
118
|
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
103
119
|
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
104
120
|
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
105
121
|
const MAX_RECENT_CONFIGS = 3;
|
|
106
122
|
const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
|
|
123
|
+
const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
|
|
124
|
+
const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
|
|
125
|
+
const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
126
|
+
const DOWNLOAD_ARTIFACT_TTL_MS = 10 * 60 * 1000;
|
|
127
|
+
const g_downloadArtifacts = new Map();
|
|
107
128
|
const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
|
|
108
129
|
const DEFAULT_BUILTIN_PROXY_SETTINGS = Object.freeze({
|
|
109
130
|
enabled: false,
|
|
@@ -215,16 +236,50 @@ function ensureConfigDir() {
|
|
|
215
236
|
}
|
|
216
237
|
}
|
|
217
238
|
|
|
239
|
+
function createConfigLoadError(type, message, detail) {
|
|
240
|
+
const err = new Error(detail || message);
|
|
241
|
+
err.configErrorType = type || 'read';
|
|
242
|
+
err.configPublicReason = message || '读取 config.toml 失败';
|
|
243
|
+
err.configDetail = detail || message || '';
|
|
244
|
+
return err;
|
|
245
|
+
}
|
|
246
|
+
|
|
218
247
|
function readConfig() {
|
|
219
248
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
220
|
-
throw
|
|
249
|
+
throw createConfigLoadError(
|
|
250
|
+
'missing',
|
|
251
|
+
'未检测到 config.toml',
|
|
252
|
+
`配置文件不存在: ${CONFIG_FILE}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let content = '';
|
|
257
|
+
try {
|
|
258
|
+
content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw createConfigLoadError(
|
|
261
|
+
'read',
|
|
262
|
+
'读取 config.toml 失败',
|
|
263
|
+
`读取配置文件失败: ${e && e.message ? e.message : e}`
|
|
264
|
+
);
|
|
221
265
|
}
|
|
266
|
+
|
|
267
|
+
let parsed;
|
|
222
268
|
try {
|
|
223
|
-
|
|
224
|
-
return toml.parse(content);
|
|
269
|
+
parsed = toml.parse(content);
|
|
225
270
|
} catch (e) {
|
|
226
|
-
throw
|
|
271
|
+
throw createConfigLoadError(
|
|
272
|
+
'parse',
|
|
273
|
+
'config.toml 解析失败',
|
|
274
|
+
`配置文件解析失败: ${e && e.message ? e.message : e}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (isPlainObject(parsed) && isPlainObject(parsed.model_providers)) {
|
|
279
|
+
const providerHeaderSegmentKeySet = collectModelProviderHeaderSegmentKeySet(content);
|
|
280
|
+
parsed.model_providers = normalizeLegacyModelProviders(parsed.model_providers, providerHeaderSegmentKeySet);
|
|
227
281
|
}
|
|
282
|
+
return parsed;
|
|
228
283
|
}
|
|
229
284
|
|
|
230
285
|
function writeConfig(content) {
|
|
@@ -277,6 +332,519 @@ function isPlainObject(value) {
|
|
|
277
332
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
278
333
|
}
|
|
279
334
|
|
|
335
|
+
const PROVIDER_CONFIG_KEYS = new Set([
|
|
336
|
+
'name',
|
|
337
|
+
'base_url',
|
|
338
|
+
'wire_api',
|
|
339
|
+
'requires_openai_auth',
|
|
340
|
+
'preferred_auth_method',
|
|
341
|
+
'request_max_retries',
|
|
342
|
+
'stream_max_retries',
|
|
343
|
+
'stream_idle_timeout_ms'
|
|
344
|
+
]);
|
|
345
|
+
const RECOVERABLE_PROVIDER_SIGNAL_KEYS = [...PROVIDER_CONFIG_KEYS].filter((key) => key !== 'name' && key !== 'base_url');
|
|
346
|
+
|
|
347
|
+
function looksLikeProviderConfig(value) {
|
|
348
|
+
if (!isPlainObject(value)) return false;
|
|
349
|
+
return Object.keys(value).some((key) => PROVIDER_CONFIG_KEYS.has(key));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isRecoverableNestedProviderConfig(value) {
|
|
353
|
+
if (!isPlainObject(value)) return false;
|
|
354
|
+
const hasBaseUrl = typeof value.base_url === 'string' && value.base_url.trim() !== '';
|
|
355
|
+
if (!hasBaseUrl) return false;
|
|
356
|
+
const hasName = typeof value.name === 'string' && value.name.trim() !== '';
|
|
357
|
+
const hasProviderSignals = RECOVERABLE_PROVIDER_SIGNAL_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
358
|
+
return hasName || hasProviderSignals;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function collectNestedProviderConfigs(node, pathSegments, collector) {
|
|
362
|
+
if (!isPlainObject(node)) return;
|
|
363
|
+
const segments = Array.isArray(pathSegments) ? pathSegments : [String(pathSegments || '')];
|
|
364
|
+
const lastSegment = segments.length > 0 ? segments[segments.length - 1] : '';
|
|
365
|
+
if (segments.length > 1 && lastSegment === 'metadata') {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (isRecoverableNestedProviderConfig(node)) {
|
|
369
|
+
collector.push({
|
|
370
|
+
name: segments.join('.'),
|
|
371
|
+
segments: segments.slice(),
|
|
372
|
+
provider: node
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
for (const [childKey, childValue] of Object.entries(node)) {
|
|
376
|
+
if (!isPlainObject(childValue)) continue;
|
|
377
|
+
collectNestedProviderConfigs(childValue, [...segments, childKey], collector);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeLegacySegments(segments) {
|
|
382
|
+
if (!Array.isArray(segments) || segments.length === 0) return null;
|
|
383
|
+
return segments.map((item) => String(item));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildLegacySegmentsKey(segments) {
|
|
387
|
+
const normalized = normalizeLegacySegments(segments);
|
|
388
|
+
return normalized ? JSON.stringify(normalized) : '';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function appendLegacySegmentsVariant(provider, segments) {
|
|
392
|
+
if (!isPlainObject(provider)) return;
|
|
393
|
+
const normalized = normalizeLegacySegments(segments);
|
|
394
|
+
if (!normalized) return;
|
|
395
|
+
|
|
396
|
+
const variants = [];
|
|
397
|
+
const seen = new Set();
|
|
398
|
+
const pushVariant = (candidate) => {
|
|
399
|
+
const key = buildLegacySegmentsKey(candidate);
|
|
400
|
+
if (!key || seen.has(key)) return;
|
|
401
|
+
seen.add(key);
|
|
402
|
+
variants.push(normalizeLegacySegments(candidate));
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (Array.isArray(provider.__codexmate_legacy_segments)) {
|
|
406
|
+
pushVariant(provider.__codexmate_legacy_segments);
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(provider.__codexmate_legacy_segment_variants)) {
|
|
409
|
+
for (const candidate of provider.__codexmate_legacy_segment_variants) {
|
|
410
|
+
pushVariant(candidate);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
pushVariant(normalized);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
if (!Array.isArray(provider.__codexmate_legacy_segments)) {
|
|
417
|
+
Object.defineProperty(provider, '__codexmate_legacy_segments', {
|
|
418
|
+
value: normalized,
|
|
419
|
+
enumerable: false,
|
|
420
|
+
configurable: true,
|
|
421
|
+
writable: true
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
Object.defineProperty(provider, '__codexmate_legacy_segment_variants', {
|
|
425
|
+
value: variants,
|
|
426
|
+
enumerable: false,
|
|
427
|
+
configurable: true,
|
|
428
|
+
writable: true
|
|
429
|
+
});
|
|
430
|
+
} catch (e) {}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function setLegacySegmentsMetadata(provider, segments) {
|
|
434
|
+
appendLegacySegmentsVariant(provider, segments);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeLegacyModelProviders(modelProviders, providerHeaderSegmentKeySet = null) {
|
|
438
|
+
if (!isPlainObject(modelProviders)) {
|
|
439
|
+
return modelProviders;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let changed = false;
|
|
443
|
+
const normalized = {};
|
|
444
|
+
const addRecovered = (entry) => {
|
|
445
|
+
const name = entry && typeof entry.name === 'string' ? entry.name : '';
|
|
446
|
+
const segments = entry && Array.isArray(entry.segments) ? entry.segments.slice() : null;
|
|
447
|
+
const provider = entry ? entry.provider : null;
|
|
448
|
+
if (!name || !isPlainObject(provider)) return;
|
|
449
|
+
const segmentKey = buildLegacySegmentsKey(segments);
|
|
450
|
+
if (providerHeaderSegmentKeySet instanceof Set && segmentKey && !providerHeaderSegmentKeySet.has(segmentKey)) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const existing = Object.prototype.hasOwnProperty.call(normalized, name)
|
|
454
|
+
? normalized[name]
|
|
455
|
+
: (Object.prototype.hasOwnProperty.call(modelProviders, name) ? modelProviders[name] : null);
|
|
456
|
+
if (isPlainObject(existing)) {
|
|
457
|
+
if (!Array.isArray(existing.__codexmate_legacy_segments)) {
|
|
458
|
+
setLegacySegmentsMetadata(existing, [name]);
|
|
459
|
+
}
|
|
460
|
+
appendLegacySegmentsVariant(existing, segments);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (Object.prototype.hasOwnProperty.call(modelProviders, name)) return;
|
|
464
|
+
if (Object.prototype.hasOwnProperty.call(normalized, name)) return;
|
|
465
|
+
setLegacySegmentsMetadata(provider, segments);
|
|
466
|
+
normalized[name] = provider;
|
|
467
|
+
changed = true;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
for (const [name, provider] of Object.entries(modelProviders)) {
|
|
471
|
+
normalized[name] = provider;
|
|
472
|
+
if (!isPlainObject(provider)) continue;
|
|
473
|
+
|
|
474
|
+
if (looksLikeProviderConfig(provider)) {
|
|
475
|
+
setLegacySegmentsMetadata(provider, [name]);
|
|
476
|
+
for (const [childKey, childValue] of Object.entries(provider)) {
|
|
477
|
+
if (!isPlainObject(childValue)) continue;
|
|
478
|
+
const recovered = [];
|
|
479
|
+
collectNestedProviderConfigs(childValue, [name, childKey], recovered);
|
|
480
|
+
for (const recoveredEntry of recovered) {
|
|
481
|
+
addRecovered(recoveredEntry);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const recovered = [];
|
|
488
|
+
collectNestedProviderConfigs(provider, [name], recovered);
|
|
489
|
+
delete normalized[name];
|
|
490
|
+
changed = true;
|
|
491
|
+
for (const recoveredEntry of recovered) {
|
|
492
|
+
addRecovered(recoveredEntry);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return changed ? normalized : modelProviders;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function escapeRegex(value) {
|
|
500
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function areStringArraysEqual(a, b) {
|
|
504
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
505
|
+
for (let i = 0; i < a.length; i++) {
|
|
506
|
+
if (String(a[i]) !== String(b[i])) return false;
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function parseTomlDottedKeyExpression(expression) {
|
|
512
|
+
const text = String(expression || '');
|
|
513
|
+
let index = 0;
|
|
514
|
+
const segments = [];
|
|
515
|
+
const skipWhitespace = () => {
|
|
516
|
+
while (index < text.length && /\s/.test(text[index])) index++;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
while (index < text.length) {
|
|
520
|
+
skipWhitespace();
|
|
521
|
+
if (index >= text.length) break;
|
|
522
|
+
|
|
523
|
+
const ch = text[index];
|
|
524
|
+
if (ch === "'") {
|
|
525
|
+
const end = text.indexOf("'", index + 1);
|
|
526
|
+
if (end === -1) return null;
|
|
527
|
+
segments.push(text.slice(index + 1, end));
|
|
528
|
+
index = end + 1;
|
|
529
|
+
} else if (ch === '"') {
|
|
530
|
+
index += 1;
|
|
531
|
+
let value = '';
|
|
532
|
+
let closed = false;
|
|
533
|
+
while (index < text.length) {
|
|
534
|
+
const cur = text[index];
|
|
535
|
+
if (cur === '"') {
|
|
536
|
+
index += 1;
|
|
537
|
+
closed = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
if (cur !== '\\') {
|
|
541
|
+
value += cur;
|
|
542
|
+
index += 1;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (index + 1 >= text.length) return null;
|
|
546
|
+
const esc = text[index + 1];
|
|
547
|
+
if (esc === 'u' || esc === 'U') {
|
|
548
|
+
const hexLen = esc === 'u' ? 4 : 8;
|
|
549
|
+
const hex = text.slice(index + 2, index + 2 + hexLen);
|
|
550
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
551
|
+
try {
|
|
552
|
+
value += String.fromCodePoint(parseInt(hex, 16));
|
|
553
|
+
} catch (e) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
index += 2 + hexLen;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const unescaped = {
|
|
560
|
+
b: '\b',
|
|
561
|
+
t: '\t',
|
|
562
|
+
n: '\n',
|
|
563
|
+
f: '\f',
|
|
564
|
+
r: '\r',
|
|
565
|
+
'"': '"',
|
|
566
|
+
'\\': '\\'
|
|
567
|
+
}[esc];
|
|
568
|
+
if (unescaped === undefined) return null;
|
|
569
|
+
value += unescaped;
|
|
570
|
+
index += 2;
|
|
571
|
+
}
|
|
572
|
+
if (!closed) return null;
|
|
573
|
+
segments.push(value);
|
|
574
|
+
} else {
|
|
575
|
+
const start = index;
|
|
576
|
+
while (index < text.length && !/\s|\./.test(text[index])) index++;
|
|
577
|
+
const bare = text.slice(start, index);
|
|
578
|
+
if (!bare) return null;
|
|
579
|
+
segments.push(bare);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
skipWhitespace();
|
|
583
|
+
if (index >= text.length) break;
|
|
584
|
+
if (text[index] !== '.') return null;
|
|
585
|
+
index += 1;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return segments.length > 0 ? segments : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function collectTomlMultilineStringRanges(text) {
|
|
592
|
+
const source = typeof text === 'string' ? text : '';
|
|
593
|
+
const ranges = [];
|
|
594
|
+
let i = 0;
|
|
595
|
+
let inMultilineBasic = false;
|
|
596
|
+
let inMultilineLiteral = false;
|
|
597
|
+
let rangeStart = -1;
|
|
598
|
+
|
|
599
|
+
while (i < source.length) {
|
|
600
|
+
if (inMultilineBasic) {
|
|
601
|
+
if (source.slice(i, i + 3) === '"""') {
|
|
602
|
+
let slashCount = 0;
|
|
603
|
+
for (let j = i - 1; j >= 0 && source[j] === '\\'; j--) {
|
|
604
|
+
slashCount++;
|
|
605
|
+
}
|
|
606
|
+
if (slashCount % 2 === 0) {
|
|
607
|
+
let runEnd = i + 3;
|
|
608
|
+
while (runEnd < source.length && source[runEnd] === '"') runEnd++;
|
|
609
|
+
ranges.push({ start: rangeStart, end: runEnd });
|
|
610
|
+
inMultilineBasic = false;
|
|
611
|
+
rangeStart = -1;
|
|
612
|
+
i = runEnd;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
i++;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (inMultilineLiteral) {
|
|
621
|
+
if (source.slice(i, i + 3) === "'''") {
|
|
622
|
+
let runEnd = i + 3;
|
|
623
|
+
while (runEnd < source.length && source[runEnd] === '\'') runEnd++;
|
|
624
|
+
ranges.push({ start: rangeStart, end: runEnd });
|
|
625
|
+
inMultilineLiteral = false;
|
|
626
|
+
rangeStart = -1;
|
|
627
|
+
i = runEnd;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
i++;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const ch = source[i];
|
|
635
|
+
if (ch === '#') {
|
|
636
|
+
while (i < source.length && source[i] !== '\n') i++;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (source.slice(i, i + 3) === '"""') {
|
|
641
|
+
inMultilineBasic = true;
|
|
642
|
+
rangeStart = i;
|
|
643
|
+
i += 3;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (source.slice(i, i + 3) === "'''") {
|
|
648
|
+
inMultilineLiteral = true;
|
|
649
|
+
rangeStart = i;
|
|
650
|
+
i += 3;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (ch === '"') {
|
|
655
|
+
i++;
|
|
656
|
+
while (i < source.length) {
|
|
657
|
+
if (source[i] === '\\') {
|
|
658
|
+
i += 2;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (source[i] === '"' || source[i] === '\n') {
|
|
662
|
+
i++;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
i++;
|
|
666
|
+
}
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (ch === '\'') {
|
|
671
|
+
i++;
|
|
672
|
+
while (i < source.length) {
|
|
673
|
+
if (source[i] === '\'' || source[i] === '\n') {
|
|
674
|
+
i++;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
i++;
|
|
678
|
+
}
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
i++;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (rangeStart >= 0) {
|
|
686
|
+
ranges.push({ start: rangeStart, end: source.length });
|
|
687
|
+
}
|
|
688
|
+
return ranges;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isIndexInRanges(index, ranges) {
|
|
692
|
+
for (const range of ranges) {
|
|
693
|
+
if (index < range.start) return false;
|
|
694
|
+
if (index >= range.start && index < range.end) return true;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function findProviderSectionRanges(content, providerName, exactSegments = null) {
|
|
700
|
+
const text = typeof content === 'string' ? content : '';
|
|
701
|
+
const name = typeof providerName === 'string' ? providerName.trim() : '';
|
|
702
|
+
const targetSegments = Array.isArray(exactSegments) ? exactSegments.map((item) => String(item)) : null;
|
|
703
|
+
if (!text || !name) return [];
|
|
704
|
+
|
|
705
|
+
const safeName = escapeRegex(name);
|
|
706
|
+
const headerPatterns = [
|
|
707
|
+
{ priority: 0, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*"${safeName}"\\s*$`) },
|
|
708
|
+
{ priority: 1, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*'${safeName}'\\s*$`) },
|
|
709
|
+
{ priority: 2, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*${safeName}\\s*$`) }
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
const allHeaders = [];
|
|
713
|
+
const targetPriorityByStart = new Map();
|
|
714
|
+
const multilineStringRanges = collectTomlMultilineStringRanges(text);
|
|
715
|
+
const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
716
|
+
let match;
|
|
717
|
+
while ((match = sectionLineRegex.exec(text)) !== null) {
|
|
718
|
+
const start = match.index;
|
|
719
|
+
if (isIndexInRanges(start, multilineStringRanges)) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
allHeaders.push(start);
|
|
723
|
+
const headerExpr = String(match[1] || '').trim();
|
|
724
|
+
|
|
725
|
+
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
|
|
726
|
+
if (Array.isArray(parsedSegments) && parsedSegments.length >= 2 && parsedSegments[0] === 'model_providers') {
|
|
727
|
+
const providerSegments = parsedSegments.slice(1);
|
|
728
|
+
if (targetSegments && targetSegments.length > 0 && areStringArraysEqual(providerSegments, targetSegments)) {
|
|
729
|
+
const prev = targetPriorityByStart.get(start);
|
|
730
|
+
if (prev === undefined || -3 < prev) {
|
|
731
|
+
targetPriorityByStart.set(start, -3);
|
|
732
|
+
}
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (!targetSegments || targetSegments.length === 0) {
|
|
736
|
+
const parsedName = providerSegments.join('.');
|
|
737
|
+
if (parsedName === name) {
|
|
738
|
+
const prev = targetPriorityByStart.get(start);
|
|
739
|
+
if (prev === undefined || -2 < prev) {
|
|
740
|
+
targetPriorityByStart.set(start, -2);
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
for (const pattern of headerPatterns) {
|
|
748
|
+
if (pattern.regex.test(headerExpr)) {
|
|
749
|
+
const prev = targetPriorityByStart.get(start);
|
|
750
|
+
if (prev === undefined || pattern.priority < prev) {
|
|
751
|
+
targetPriorityByStart.set(start, pattern.priority);
|
|
752
|
+
}
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (targetPriorityByStart.size === 0) {
|
|
759
|
+
return [];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const ranges = [];
|
|
763
|
+
for (let i = 0; i < allHeaders.length; i++) {
|
|
764
|
+
const start = allHeaders[i];
|
|
765
|
+
if (!targetPriorityByStart.has(start)) continue;
|
|
766
|
+
const end = i + 1 < allHeaders.length ? allHeaders[i + 1] : text.length;
|
|
767
|
+
ranges.push({
|
|
768
|
+
start,
|
|
769
|
+
end,
|
|
770
|
+
priority: targetPriorityByStart.get(start)
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const exactMatches = ranges.filter((range) => range.priority === -3);
|
|
774
|
+
return exactMatches.length > 0 ? exactMatches : ranges;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function doesSegmentsStartWith(segments, prefix) {
|
|
778
|
+
if (!Array.isArray(segments) || !Array.isArray(prefix) || prefix.length === 0 || segments.length < prefix.length) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
782
|
+
if (String(segments[i]) !== String(prefix[i])) return false;
|
|
783
|
+
}
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function findProviderDescendantSectionRanges(content, prefixSegments) {
|
|
788
|
+
const text = typeof content === 'string' ? content : '';
|
|
789
|
+
const prefix = Array.isArray(prefixSegments) ? prefixSegments.map((item) => String(item)) : [];
|
|
790
|
+
if (!text || prefix.length === 0) return [];
|
|
791
|
+
|
|
792
|
+
const allHeaders = [];
|
|
793
|
+
const parsedProviderSegmentsByStart = new Map();
|
|
794
|
+
const multilineStringRanges = collectTomlMultilineStringRanges(text);
|
|
795
|
+
const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
796
|
+
let match;
|
|
797
|
+
while ((match = sectionLineRegex.exec(text)) !== null) {
|
|
798
|
+
const start = match.index;
|
|
799
|
+
if (isIndexInRanges(start, multilineStringRanges)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
allHeaders.push(start);
|
|
803
|
+
const headerExpr = String(match[1] || '').trim();
|
|
804
|
+
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
|
|
805
|
+
if (!Array.isArray(parsedSegments) || parsedSegments.length < 2 || parsedSegments[0] !== 'model_providers') {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
parsedProviderSegmentsByStart.set(start, parsedSegments.slice(1));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const ranges = [];
|
|
812
|
+
for (let i = 0; i < allHeaders.length; i++) {
|
|
813
|
+
const start = allHeaders[i];
|
|
814
|
+
const providerSegments = parsedProviderSegmentsByStart.get(start);
|
|
815
|
+
if (!providerSegments) continue;
|
|
816
|
+
if (!doesSegmentsStartWith(providerSegments, prefix)) continue;
|
|
817
|
+
if (providerSegments.length <= prefix.length) continue;
|
|
818
|
+
const end = i + 1 < allHeaders.length ? allHeaders[i + 1] : text.length;
|
|
819
|
+
ranges.push({ start, end, priority: 0 });
|
|
820
|
+
}
|
|
821
|
+
return ranges;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function collectModelProviderHeaderSegmentKeySet(content) {
|
|
825
|
+
const text = typeof content === 'string' ? content : '';
|
|
826
|
+
const keys = new Set();
|
|
827
|
+
if (!text) return keys;
|
|
828
|
+
|
|
829
|
+
const multilineStringRanges = collectTomlMultilineStringRanges(text);
|
|
830
|
+
const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
831
|
+
let match;
|
|
832
|
+
while ((match = sectionLineRegex.exec(text)) !== null) {
|
|
833
|
+
const start = match.index;
|
|
834
|
+
if (isIndexInRanges(start, multilineStringRanges)) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const headerExpr = String(match[1] || '').trim();
|
|
838
|
+
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
|
|
839
|
+
if (!Array.isArray(parsedSegments) || parsedSegments.length < 2 || parsedSegments[0] !== 'model_providers') {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const key = buildLegacySegmentsKey(parsedSegments.slice(1));
|
|
843
|
+
if (key) keys.add(key);
|
|
844
|
+
}
|
|
845
|
+
return keys;
|
|
846
|
+
}
|
|
847
|
+
|
|
280
848
|
function normalizeAuthProfileName(value) {
|
|
281
849
|
const raw = typeof value === 'string' ? value.trim() : '';
|
|
282
850
|
if (!raw) return '';
|
|
@@ -656,138 +1224,981 @@ async function fetchModelsFromBaseUrl(baseUrl, apiKey) {
|
|
|
656
1224
|
promise.finally(() => {
|
|
657
1225
|
g_modelsInFlight.delete(cacheKey);
|
|
658
1226
|
});
|
|
659
|
-
return promise;
|
|
1227
|
+
return promise;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async function fetchModelsFromBaseUrlCore(baseUrl, apiKey) {
|
|
1231
|
+
const candidates = buildModelsCandidates(baseUrl);
|
|
1232
|
+
if (candidates.length === 0) return { error: 'Provider missing URL' };
|
|
1233
|
+
|
|
1234
|
+
let lastError = '';
|
|
1235
|
+
for (const modelsUrl of candidates) {
|
|
1236
|
+
let parsed;
|
|
1237
|
+
try {
|
|
1238
|
+
parsed = new URL(modelsUrl);
|
|
1239
|
+
} catch (e) {
|
|
1240
|
+
lastError = 'Invalid URL';
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1245
|
+
const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT;
|
|
1246
|
+
const headers = {
|
|
1247
|
+
'User-Agent': 'codexmate-models',
|
|
1248
|
+
'Accept': 'application/json'
|
|
1249
|
+
};
|
|
1250
|
+
if (apiKey) {
|
|
1251
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
1252
|
+
headers['x-api-key'] = apiKey;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const result = await new Promise((innerResolve) => {
|
|
1256
|
+
let settled = false;
|
|
1257
|
+
const finish = (payload) => {
|
|
1258
|
+
if (settled) return;
|
|
1259
|
+
settled = true;
|
|
1260
|
+
innerResolve(payload);
|
|
1261
|
+
};
|
|
1262
|
+
const req = transport.request(parsed, { method: 'GET', headers, agent }, (res) => {
|
|
1263
|
+
const status = res.statusCode || 0;
|
|
1264
|
+
const contentType = String(res.headers['content-type'] || '').toLowerCase();
|
|
1265
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
1266
|
+
res.resume();
|
|
1267
|
+
return finish({ unavailable: true });
|
|
1268
|
+
}
|
|
1269
|
+
let body = '';
|
|
1270
|
+
let receivedBytes = 0;
|
|
1271
|
+
res.on('data', chunk => {
|
|
1272
|
+
receivedBytes += chunk.length || 0;
|
|
1273
|
+
if (receivedBytes > MODELS_RESPONSE_MAX_BYTES) {
|
|
1274
|
+
res.destroy();
|
|
1275
|
+
return finish({ unavailable: true });
|
|
1276
|
+
}
|
|
1277
|
+
body += chunk;
|
|
1278
|
+
});
|
|
1279
|
+
res.on('end', () => {
|
|
1280
|
+
if (settled) return;
|
|
1281
|
+
if (status >= 400) {
|
|
1282
|
+
return finish({ error: `Request failed: ${status}` });
|
|
1283
|
+
}
|
|
1284
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
1285
|
+
return finish({ unavailable: true });
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
const payload = JSON.parse(body || '{}');
|
|
1289
|
+
if (!hasModelsListPayload(payload)) {
|
|
1290
|
+
return finish({ unavailable: true });
|
|
1291
|
+
}
|
|
1292
|
+
const models = extractModelNames(payload);
|
|
1293
|
+
return finish({ models });
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
return finish({ unavailable: true });
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
req.setTimeout(SPEED_TEST_TIMEOUT_MS, () => {
|
|
1301
|
+
req.destroy(new Error('timeout'));
|
|
1302
|
+
});
|
|
1303
|
+
req.on('error', (err) => {
|
|
1304
|
+
finish({ error: err.message || 'Request failed' });
|
|
1305
|
+
});
|
|
1306
|
+
req.end();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
if (result && Array.isArray(result.models)) {
|
|
1310
|
+
return { models: result.models };
|
|
1311
|
+
}
|
|
1312
|
+
if (result && result.error) {
|
|
1313
|
+
lastError = result.error;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (lastError) {
|
|
1319
|
+
return { error: lastError };
|
|
1320
|
+
}
|
|
1321
|
+
return { unlimited: true };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function fetchProviderModels(providerName, overrides = {}) {
|
|
1325
|
+
const { config } = readConfigOrVirtualDefault();
|
|
1326
|
+
const targetProvider = providerName || config.model_provider || '';
|
|
1327
|
+
if (!targetProvider) return { error: '未设置当前提供商' };
|
|
1328
|
+
|
|
1329
|
+
const providers = config.model_providers || {};
|
|
1330
|
+
const provider = providers[targetProvider];
|
|
1331
|
+
if (!provider) return { error: `提供商不存在: ${targetProvider}` };
|
|
1332
|
+
|
|
1333
|
+
const baseUrl = overrides.baseUrl || provider.base_url || '';
|
|
1334
|
+
const apiKey = overrides.apiKey ?? provider.preferred_auth_method ?? '';
|
|
1335
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
1336
|
+
if (res.unlimited) return { models: [], provider: targetProvider, unlimited: true };
|
|
1337
|
+
if (res.error) return { error: res.error };
|
|
1338
|
+
return { models: res.models || [], provider: targetProvider, unlimited: false };
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function resolveAgentsFilePath(params = {}) {
|
|
1342
|
+
const baseDir = typeof params.baseDir === 'string' && params.baseDir.trim()
|
|
1343
|
+
? params.baseDir.trim()
|
|
1344
|
+
: CONFIG_DIR;
|
|
1345
|
+
return path.join(baseDir, AGENTS_FILE_NAME);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function validateAgentsBaseDir(filePath) {
|
|
1349
|
+
const dirPath = path.dirname(filePath);
|
|
1350
|
+
try {
|
|
1351
|
+
const stat = fs.statSync(dirPath);
|
|
1352
|
+
if (!stat.isDirectory()) {
|
|
1353
|
+
return { error: `目标不是目录: ${dirPath}` };
|
|
1354
|
+
}
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
return { error: `目标目录不存在: ${dirPath}` };
|
|
1357
|
+
}
|
|
1358
|
+
return { ok: true, dirPath };
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function normalizeCodexSkillName(name) {
|
|
1362
|
+
const value = typeof name === 'string' ? name.trim() : '';
|
|
1363
|
+
if (!value) {
|
|
1364
|
+
return { error: '技能名称不能为空' };
|
|
1365
|
+
}
|
|
1366
|
+
if (value.includes('\0')) {
|
|
1367
|
+
return { error: '技能名称非法' };
|
|
1368
|
+
}
|
|
1369
|
+
if (value === '.' || value === '..') {
|
|
1370
|
+
return { error: '技能名称非法' };
|
|
1371
|
+
}
|
|
1372
|
+
if (value.includes('/') || value.includes('\\')) {
|
|
1373
|
+
return { error: '技能名称非法' };
|
|
1374
|
+
}
|
|
1375
|
+
if (path.basename(value) !== value) {
|
|
1376
|
+
return { error: '技能名称非法' };
|
|
1377
|
+
}
|
|
1378
|
+
if (value.startsWith('.')) {
|
|
1379
|
+
return { error: '系统技能不可删除' };
|
|
1380
|
+
}
|
|
1381
|
+
return { name: value };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function isSkillDirectoryEntry(entryName) {
|
|
1385
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
|
|
1386
|
+
try {
|
|
1387
|
+
const stat = fs.statSync(targetPath);
|
|
1388
|
+
return stat.isDirectory();
|
|
1389
|
+
} catch (e) {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function normalizeSkillImportSourceApp(app) {
|
|
1395
|
+
const value = typeof app === 'string' ? app.trim().toLowerCase() : '';
|
|
1396
|
+
return SKILL_IMPORT_SOURCES.some((item) => item.app === value) ? value : '';
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function getSkillImportSourceByApp(app) {
|
|
1400
|
+
const normalizedApp = normalizeSkillImportSourceApp(app);
|
|
1401
|
+
if (!normalizedApp) return null;
|
|
1402
|
+
return SKILL_IMPORT_SOURCES.find((item) => item.app === normalizedApp) || null;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function parseSimpleSkillFrontmatter(content = '') {
|
|
1406
|
+
const normalized = String(content || '').replace(/\r\n/g, '\n');
|
|
1407
|
+
if (!normalized.startsWith('---\n')) {
|
|
1408
|
+
return {};
|
|
1409
|
+
}
|
|
1410
|
+
const endIndex = normalized.indexOf('\n---\n', 4);
|
|
1411
|
+
if (endIndex <= 4) {
|
|
1412
|
+
return {};
|
|
1413
|
+
}
|
|
1414
|
+
const frontmatterRaw = normalized.slice(4, endIndex);
|
|
1415
|
+
const result = {};
|
|
1416
|
+
const lines = frontmatterRaw.split('\n');
|
|
1417
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
1418
|
+
const line = lines[lineIndex];
|
|
1419
|
+
const trimmed = line.trim();
|
|
1420
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
1421
|
+
const matched = trimmed.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
1422
|
+
if (!matched) continue;
|
|
1423
|
+
const key = matched[1];
|
|
1424
|
+
let value = matched[2] || '';
|
|
1425
|
+
const indicator = value.trim();
|
|
1426
|
+
if (/^[>|]/.test(indicator)) {
|
|
1427
|
+
const blockLines = [];
|
|
1428
|
+
let cursor = lineIndex + 1;
|
|
1429
|
+
while (cursor < lines.length) {
|
|
1430
|
+
const candidateLine = lines[cursor];
|
|
1431
|
+
if (!candidateLine.trim()) {
|
|
1432
|
+
blockLines.push('');
|
|
1433
|
+
cursor += 1;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (/^\s/.test(candidateLine)) {
|
|
1437
|
+
blockLines.push(candidateLine);
|
|
1438
|
+
cursor += 1;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
lineIndex = cursor - 1;
|
|
1444
|
+
const indents = blockLines
|
|
1445
|
+
.filter((item) => item.trim())
|
|
1446
|
+
.map((item) => {
|
|
1447
|
+
const indentMatch = item.match(/^[ \t]*/);
|
|
1448
|
+
return indentMatch ? indentMatch[0].length : 0;
|
|
1449
|
+
});
|
|
1450
|
+
const commonIndent = indents.length ? Math.min(...indents) : 0;
|
|
1451
|
+
const deindented = blockLines.map((item) => {
|
|
1452
|
+
if (!item.trim()) return '';
|
|
1453
|
+
return item.slice(commonIndent);
|
|
1454
|
+
});
|
|
1455
|
+
if (indicator.startsWith('>')) {
|
|
1456
|
+
const paragraphs = [];
|
|
1457
|
+
let paragraphLines = [];
|
|
1458
|
+
for (const blockLine of deindented) {
|
|
1459
|
+
const blockTrimmed = blockLine.trim();
|
|
1460
|
+
if (!blockTrimmed) {
|
|
1461
|
+
if (paragraphLines.length) {
|
|
1462
|
+
paragraphs.push(paragraphLines.join(' '));
|
|
1463
|
+
paragraphLines = [];
|
|
1464
|
+
}
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
paragraphLines.push(blockTrimmed);
|
|
1468
|
+
}
|
|
1469
|
+
if (paragraphLines.length) {
|
|
1470
|
+
paragraphs.push(paragraphLines.join(' '));
|
|
1471
|
+
}
|
|
1472
|
+
value = paragraphs.join('\n');
|
|
1473
|
+
} else {
|
|
1474
|
+
value = deindented.join('\n');
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
1478
|
+
value = value.slice(1, -1);
|
|
1479
|
+
}
|
|
1480
|
+
result[key] = value.trim();
|
|
1481
|
+
}
|
|
1482
|
+
return result;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function stripMarkdownFrontmatter(content = '') {
|
|
1486
|
+
const normalized = String(content || '').replace(/\r\n/g, '\n');
|
|
1487
|
+
if (!normalized.startsWith('---\n')) {
|
|
1488
|
+
return normalized;
|
|
1489
|
+
}
|
|
1490
|
+
const endIndex = normalized.indexOf('\n---\n', 4);
|
|
1491
|
+
if (endIndex <= 4) {
|
|
1492
|
+
return normalized;
|
|
1493
|
+
}
|
|
1494
|
+
return normalized.slice(endIndex + 5);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function extractSkillDescriptionFromMarkdown(content = '') {
|
|
1498
|
+
const normalized = String(content || '').replace(/\r\n/g, '\n');
|
|
1499
|
+
const lines = normalized.split('\n');
|
|
1500
|
+
let inFence = false;
|
|
1501
|
+
for (const line of lines) {
|
|
1502
|
+
const trimmedStart = line.trimStart();
|
|
1503
|
+
if (trimmedStart.startsWith('```')) {
|
|
1504
|
+
inFence = !inFence;
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (inFence) continue;
|
|
1508
|
+
if (/^( {4}|\t)/.test(line)) continue;
|
|
1509
|
+
const trimmed = line.trim();
|
|
1510
|
+
if (!trimmed) continue;
|
|
1511
|
+
if (trimmed.startsWith('#')) continue;
|
|
1512
|
+
if (trimmed.startsWith('---')) continue;
|
|
1513
|
+
if (/^([A-Za-z0-9_-]+)\s*:\s*/.test(trimmed)) continue;
|
|
1514
|
+
return trimmed.slice(0, 200);
|
|
1515
|
+
}
|
|
1516
|
+
return '';
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function readCodexSkillMetadata(skillPath) {
|
|
1520
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
1521
|
+
if (!fs.existsSync(skillFile)) {
|
|
1522
|
+
return {
|
|
1523
|
+
hasSkillFile: false,
|
|
1524
|
+
displayName: '',
|
|
1525
|
+
description: ''
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const raw = fs.readFileSync(skillFile, 'utf-8');
|
|
1530
|
+
const content = stripUtf8Bom(raw);
|
|
1531
|
+
const frontmatter = parseSimpleSkillFrontmatter(content);
|
|
1532
|
+
const contentWithoutFrontmatter = stripMarkdownFrontmatter(content);
|
|
1533
|
+
const heading = contentWithoutFrontmatter.match(/^\s*#\s+(.+)$/m);
|
|
1534
|
+
const displayName = typeof frontmatter.name === 'string' && frontmatter.name.trim()
|
|
1535
|
+
? frontmatter.name.trim()
|
|
1536
|
+
: (heading && heading[1] ? heading[1].trim() : '');
|
|
1537
|
+
const description = typeof frontmatter.description === 'string' && frontmatter.description.trim()
|
|
1538
|
+
? frontmatter.description.trim().slice(0, 200)
|
|
1539
|
+
: extractSkillDescriptionFromMarkdown(contentWithoutFrontmatter);
|
|
1540
|
+
return {
|
|
1541
|
+
hasSkillFile: true,
|
|
1542
|
+
displayName,
|
|
1543
|
+
description
|
|
1544
|
+
};
|
|
1545
|
+
} catch (e) {
|
|
1546
|
+
return {
|
|
1547
|
+
hasSkillFile: false,
|
|
1548
|
+
displayName: '',
|
|
1549
|
+
description: ''
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function getCodexSkillEntryInfoByName(entryName) {
|
|
1555
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
|
|
1556
|
+
const normalized = normalizeCodexSkillName(entryName);
|
|
1557
|
+
if (normalized.error) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
const relativePath = path.relative(CODEX_SKILLS_DIR, targetPath);
|
|
1561
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
try {
|
|
1566
|
+
const lstat = fs.lstatSync(targetPath);
|
|
1567
|
+
const isSymbolicLink = lstat.isSymbolicLink();
|
|
1568
|
+
if (!lstat.isDirectory() && !isSymbolicLink) {
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
if (isSymbolicLink && !isSkillDirectoryEntry(entryName)) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
const metadata = readCodexSkillMetadata(targetPath);
|
|
1575
|
+
return {
|
|
1576
|
+
name: entryName,
|
|
1577
|
+
path: targetPath,
|
|
1578
|
+
hasSkillFile: !!metadata.hasSkillFile,
|
|
1579
|
+
displayName: metadata.displayName || entryName,
|
|
1580
|
+
description: metadata.description || '',
|
|
1581
|
+
sourceType: isSymbolicLink ? 'symlink' : 'directory',
|
|
1582
|
+
updatedAt: Number.isFinite(lstat.mtimeMs) ? Math.floor(lstat.mtimeMs) : 0
|
|
1583
|
+
};
|
|
1584
|
+
} catch (e) {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function listCodexSkills() {
|
|
1590
|
+
if (!fs.existsSync(CODEX_SKILLS_DIR)) {
|
|
1591
|
+
return {
|
|
1592
|
+
root: CODEX_SKILLS_DIR,
|
|
1593
|
+
exists: false,
|
|
1594
|
+
items: []
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
const entries = fs.readdirSync(CODEX_SKILLS_DIR, { withFileTypes: true });
|
|
1599
|
+
const items = entries
|
|
1600
|
+
.map((entry) => {
|
|
1601
|
+
const name = entry && entry.name ? entry.name : '';
|
|
1602
|
+
if (!name || name.startsWith('.')) return null;
|
|
1603
|
+
return getCodexSkillEntryInfoByName(name);
|
|
1604
|
+
})
|
|
1605
|
+
.filter(Boolean)
|
|
1606
|
+
.sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
|
|
1607
|
+
return {
|
|
1608
|
+
root: CODEX_SKILLS_DIR,
|
|
1609
|
+
exists: true,
|
|
1610
|
+
items
|
|
1611
|
+
};
|
|
1612
|
+
} catch (e) {
|
|
1613
|
+
return { error: `读取 skills 目录失败: ${e.message}` };
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function listSkillEntriesByRoot(rootDir) {
|
|
1618
|
+
if (!rootDir || !fs.existsSync(rootDir)) {
|
|
1619
|
+
return [];
|
|
1620
|
+
}
|
|
1621
|
+
try {
|
|
1622
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
1623
|
+
return entries
|
|
1624
|
+
.map((entry) => {
|
|
1625
|
+
const name = entry && entry.name ? entry.name : '';
|
|
1626
|
+
if (!name || name.startsWith('.')) return null;
|
|
1627
|
+
const normalized = normalizeCodexSkillName(name);
|
|
1628
|
+
if (normalized.error) return null;
|
|
1629
|
+
const skillPath = path.join(rootDir, name);
|
|
1630
|
+
const relativePath = path.relative(rootDir, skillPath);
|
|
1631
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const lstat = fs.lstatSync(skillPath);
|
|
1636
|
+
const isSymbolicLink = lstat.isSymbolicLink();
|
|
1637
|
+
if (!lstat.isDirectory() && !isSymbolicLink) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
if (isSymbolicLink) {
|
|
1641
|
+
const realPath = fs.realpathSync(skillPath);
|
|
1642
|
+
const realStat = fs.statSync(realPath);
|
|
1643
|
+
if (!realStat.isDirectory()) {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
name,
|
|
1649
|
+
path: skillPath,
|
|
1650
|
+
sourceType: isSymbolicLink ? 'symlink' : 'directory'
|
|
1651
|
+
};
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
})
|
|
1656
|
+
.filter(Boolean);
|
|
1657
|
+
} catch (e) {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function scanUnmanagedCodexSkills() {
|
|
1663
|
+
const existing = listCodexSkills();
|
|
1664
|
+
if (existing.error) {
|
|
1665
|
+
return { error: existing.error };
|
|
1666
|
+
}
|
|
1667
|
+
const existingNames = new Set((Array.isArray(existing.items) ? existing.items : [])
|
|
1668
|
+
.map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
|
|
1669
|
+
.filter(Boolean));
|
|
1670
|
+
|
|
1671
|
+
const items = [];
|
|
1672
|
+
for (const source of SKILL_IMPORT_SOURCES) {
|
|
1673
|
+
const sourceEntries = listSkillEntriesByRoot(source.dir);
|
|
1674
|
+
for (const entry of sourceEntries) {
|
|
1675
|
+
if (existingNames.has(entry.name)) {
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
const metadata = readCodexSkillMetadata(entry.path);
|
|
1679
|
+
items.push({
|
|
1680
|
+
key: `${source.app}:${entry.name}`,
|
|
1681
|
+
name: entry.name,
|
|
1682
|
+
displayName: metadata.displayName || entry.name,
|
|
1683
|
+
description: metadata.description || '',
|
|
1684
|
+
sourceApp: source.app,
|
|
1685
|
+
sourceLabel: source.label,
|
|
1686
|
+
sourcePath: entry.path,
|
|
1687
|
+
sourceType: entry.sourceType,
|
|
1688
|
+
hasSkillFile: !!metadata.hasSkillFile
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
items.sort((a, b) => {
|
|
1694
|
+
const nameCompare = a.displayName.localeCompare(b.displayName, 'zh-Hans-CN');
|
|
1695
|
+
if (nameCompare !== 0) return nameCompare;
|
|
1696
|
+
return a.sourceLabel.localeCompare(b.sourceLabel, 'zh-Hans-CN');
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
return {
|
|
1700
|
+
root: CODEX_SKILLS_DIR,
|
|
1701
|
+
items,
|
|
1702
|
+
sources: SKILL_IMPORT_SOURCES.map((source) => ({
|
|
1703
|
+
app: source.app,
|
|
1704
|
+
label: source.label,
|
|
1705
|
+
path: source.dir,
|
|
1706
|
+
exists: fs.existsSync(source.dir)
|
|
1707
|
+
}))
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function importCodexSkills(params = {}) {
|
|
1712
|
+
const rawItems = Array.isArray(params.items) ? params.items : [];
|
|
1713
|
+
if (!rawItems.length) {
|
|
1714
|
+
return { error: '请先选择要导入的 skill' };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
ensureDir(CODEX_SKILLS_DIR);
|
|
1718
|
+
|
|
1719
|
+
const imported = [];
|
|
1720
|
+
const failed = [];
|
|
1721
|
+
const dedup = new Set();
|
|
1722
|
+
|
|
1723
|
+
for (const rawItem of rawItems) {
|
|
1724
|
+
const safeItem = rawItem && typeof rawItem === 'object' ? rawItem : {};
|
|
1725
|
+
const normalizedName = normalizeCodexSkillName(safeItem.name);
|
|
1726
|
+
if (normalizedName.error) {
|
|
1727
|
+
failed.push({
|
|
1728
|
+
name: safeItem && safeItem.name ? String(safeItem.name) : '',
|
|
1729
|
+
sourceApp: safeItem && safeItem.sourceApp ? String(safeItem.sourceApp) : '',
|
|
1730
|
+
error: normalizedName.error
|
|
1731
|
+
});
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
const source = getSkillImportSourceByApp(safeItem.sourceApp);
|
|
1735
|
+
if (!source) {
|
|
1736
|
+
failed.push({
|
|
1737
|
+
name: normalizedName.name,
|
|
1738
|
+
sourceApp: safeItem && safeItem.sourceApp ? String(safeItem.sourceApp) : '',
|
|
1739
|
+
error: '来源应用不支持'
|
|
1740
|
+
});
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
const dedupKey = `${source.app}:${normalizedName.name}`;
|
|
1744
|
+
if (dedup.has(dedupKey)) {
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
dedup.add(dedupKey);
|
|
1748
|
+
|
|
1749
|
+
const sourcePath = path.join(source.dir, normalizedName.name);
|
|
1750
|
+
const sourceRelative = path.relative(source.dir, sourcePath);
|
|
1751
|
+
if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
|
|
1752
|
+
failed.push({
|
|
1753
|
+
name: normalizedName.name,
|
|
1754
|
+
sourceApp: source.app,
|
|
1755
|
+
error: '来源路径非法'
|
|
1756
|
+
});
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1760
|
+
failed.push({
|
|
1761
|
+
name: normalizedName.name,
|
|
1762
|
+
sourceApp: source.app,
|
|
1763
|
+
error: '来源 skill 不存在'
|
|
1764
|
+
});
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
|
|
1769
|
+
const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
|
|
1770
|
+
if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
|
|
1771
|
+
failed.push({
|
|
1772
|
+
name: normalizedName.name,
|
|
1773
|
+
sourceApp: source.app,
|
|
1774
|
+
error: '目标路径非法'
|
|
1775
|
+
});
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
if (fs.existsSync(targetPath)) {
|
|
1779
|
+
failed.push({
|
|
1780
|
+
name: normalizedName.name,
|
|
1781
|
+
sourceApp: source.app,
|
|
1782
|
+
error: 'Codex 中已存在同名 skill'
|
|
1783
|
+
});
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
let copiedToTarget = false;
|
|
1788
|
+
try {
|
|
1789
|
+
const lstat = fs.lstatSync(sourcePath);
|
|
1790
|
+
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) {
|
|
1791
|
+
failed.push({
|
|
1792
|
+
name: normalizedName.name,
|
|
1793
|
+
sourceApp: source.app,
|
|
1794
|
+
error: '来源不是技能目录'
|
|
1795
|
+
});
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
const sourceDirForCopy = lstat.isSymbolicLink() ? fs.realpathSync(sourcePath) : sourcePath;
|
|
1799
|
+
const sourceStat = fs.statSync(sourceDirForCopy);
|
|
1800
|
+
if (!sourceStat.isDirectory()) {
|
|
1801
|
+
failed.push({
|
|
1802
|
+
name: normalizedName.name,
|
|
1803
|
+
sourceApp: source.app,
|
|
1804
|
+
error: '来源 skill 无法读取'
|
|
1805
|
+
});
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
const visitedRealPaths = new Set([sourceDirForCopy]);
|
|
1809
|
+
copyDirRecursive(sourceDirForCopy, targetPath, {
|
|
1810
|
+
dereferenceSymlinks: true,
|
|
1811
|
+
allowedRootRealPath: sourceDirForCopy,
|
|
1812
|
+
visitedRealPaths
|
|
1813
|
+
});
|
|
1814
|
+
copiedToTarget = true;
|
|
1815
|
+
imported.push({
|
|
1816
|
+
name: normalizedName.name,
|
|
1817
|
+
sourceApp: source.app,
|
|
1818
|
+
sourceLabel: source.label,
|
|
1819
|
+
path: targetPath
|
|
1820
|
+
});
|
|
1821
|
+
} catch (e) {
|
|
1822
|
+
if (!copiedToTarget && fs.existsSync(targetPath)) {
|
|
1823
|
+
try {
|
|
1824
|
+
removeDirectoryRecursive(targetPath);
|
|
1825
|
+
} catch (_) {}
|
|
1826
|
+
}
|
|
1827
|
+
failed.push({
|
|
1828
|
+
name: normalizedName.name,
|
|
1829
|
+
sourceApp: source.app,
|
|
1830
|
+
error: e && e.message ? e.message : '导入失败'
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
return {
|
|
1836
|
+
success: failed.length === 0,
|
|
1837
|
+
imported,
|
|
1838
|
+
failed,
|
|
1839
|
+
root: CODEX_SKILLS_DIR
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function collectSkillDirectoriesFromRoot(rootDir, limit = MAX_SKILLS_ZIP_ENTRY_COUNT) {
|
|
1844
|
+
const results = [];
|
|
1845
|
+
let truncated = false;
|
|
1846
|
+
if (!rootDir || !fs.existsSync(rootDir)) {
|
|
1847
|
+
return { results, truncated };
|
|
1848
|
+
}
|
|
1849
|
+
const normalizedLimit = Number.isFinite(limit) && limit > 0
|
|
1850
|
+
? Math.floor(limit)
|
|
1851
|
+
: MAX_SKILLS_ZIP_ENTRY_COUNT;
|
|
1852
|
+
const stack = [rootDir];
|
|
1853
|
+
while (stack.length > 0) {
|
|
1854
|
+
if (results.length >= normalizedLimit) {
|
|
1855
|
+
truncated = true;
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
const currentDir = stack.pop();
|
|
1859
|
+
let entries = [];
|
|
1860
|
+
try {
|
|
1861
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1862
|
+
} catch (e) {
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const hasSkillFile = entries.some((entry) => entry && entry.isFile() && String(entry.name || '') === 'SKILL.md');
|
|
1867
|
+
if (hasSkillFile) {
|
|
1868
|
+
results.push(currentDir);
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
for (const entry of entries) {
|
|
1873
|
+
if (!entry || !entry.isDirectory()) continue;
|
|
1874
|
+
const entryName = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
1875
|
+
if (!entryName || entryName.startsWith('.')) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
stack.push(path.join(currentDir, entryName));
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return { results, truncated };
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName = '') {
|
|
1885
|
+
const directoryBaseName = path.basename(skillDir || '');
|
|
1886
|
+
const extractionBaseName = path.basename(extractionRoot || '');
|
|
1887
|
+
let candidate = directoryBaseName;
|
|
1888
|
+
if (!candidate || candidate === extractionBaseName || candidate.startsWith('.')) {
|
|
1889
|
+
const fallback = typeof fallbackName === 'string' ? fallbackName.trim() : '';
|
|
1890
|
+
const fallbackBase = fallback ? path.basename(fallback, path.extname(fallback)) : '';
|
|
1891
|
+
candidate = fallbackBase || candidate;
|
|
1892
|
+
}
|
|
1893
|
+
return normalizeCodexSkillName(candidate);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
async function importCodexSkillsFromZipFile(zipPath, options = {}) {
|
|
1897
|
+
const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
|
|
1898
|
+
const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
|
|
1899
|
+
const imported = [];
|
|
1900
|
+
const failed = [];
|
|
1901
|
+
const dedupNames = new Set();
|
|
1902
|
+
const extractionRoot = path.join(tempDir || path.dirname(zipPath), 'extract');
|
|
1903
|
+
|
|
1904
|
+
try {
|
|
1905
|
+
await inspectZipArchiveLimits(zipPath, {
|
|
1906
|
+
maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
|
|
1907
|
+
maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
await extractUploadZip(zipPath, extractionRoot);
|
|
1911
|
+
const discovery = collectSkillDirectoriesFromRoot(extractionRoot, MAX_SKILLS_ZIP_ENTRY_COUNT);
|
|
1912
|
+
const discoveredDirs = discovery.results;
|
|
1913
|
+
if (discoveredDirs.length === 0) {
|
|
1914
|
+
return { error: '压缩包中未发现包含 SKILL.md 的技能目录' };
|
|
1915
|
+
}
|
|
1916
|
+
if (discovery.truncated) {
|
|
1917
|
+
return { error: '压缩包中的技能目录数量超出导入上限' };
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
ensureDir(CODEX_SKILLS_DIR);
|
|
1921
|
+
for (const skillDir of discoveredDirs) {
|
|
1922
|
+
const normalizedName = resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName);
|
|
1923
|
+
if (normalizedName.error) {
|
|
1924
|
+
failed.push({
|
|
1925
|
+
name: path.basename(skillDir || ''),
|
|
1926
|
+
error: normalizedName.error
|
|
1927
|
+
});
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
const dedupKey = normalizedName.name.toLowerCase();
|
|
1931
|
+
if (dedupNames.has(dedupKey)) {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
dedupNames.add(dedupKey);
|
|
1935
|
+
|
|
1936
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
|
|
1937
|
+
const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
|
|
1938
|
+
if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
|
|
1939
|
+
failed.push({
|
|
1940
|
+
name: normalizedName.name,
|
|
1941
|
+
error: '目标路径非法'
|
|
1942
|
+
});
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (fs.existsSync(targetPath)) {
|
|
1946
|
+
failed.push({
|
|
1947
|
+
name: normalizedName.name,
|
|
1948
|
+
error: 'Codex 中已存在同名 skill'
|
|
1949
|
+
});
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
let copiedToTarget = false;
|
|
1954
|
+
try {
|
|
1955
|
+
const sourceRealPath = fs.realpathSync(skillDir);
|
|
1956
|
+
const sourceStat = fs.statSync(sourceRealPath);
|
|
1957
|
+
if (!sourceStat.isDirectory()) {
|
|
1958
|
+
failed.push({
|
|
1959
|
+
name: normalizedName.name,
|
|
1960
|
+
error: '来源 skill 无法读取'
|
|
1961
|
+
});
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
const visitedRealPaths = new Set([sourceRealPath]);
|
|
1965
|
+
copyDirRecursive(sourceRealPath, targetPath, {
|
|
1966
|
+
dereferenceSymlinks: true,
|
|
1967
|
+
allowedRootRealPath: sourceRealPath,
|
|
1968
|
+
visitedRealPaths
|
|
1969
|
+
});
|
|
1970
|
+
copiedToTarget = true;
|
|
1971
|
+
imported.push({
|
|
1972
|
+
name: normalizedName.name,
|
|
1973
|
+
path: targetPath
|
|
1974
|
+
});
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
if (!copiedToTarget && fs.existsSync(targetPath)) {
|
|
1977
|
+
try {
|
|
1978
|
+
removeDirectoryRecursive(targetPath);
|
|
1979
|
+
} catch (_) {}
|
|
1980
|
+
}
|
|
1981
|
+
failed.push({
|
|
1982
|
+
name: normalizedName.name,
|
|
1983
|
+
error: e && e.message ? e.message : '导入失败'
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (imported.length === 0 && failed.length > 0) {
|
|
1989
|
+
return {
|
|
1990
|
+
error: failed[0].error || '导入失败',
|
|
1991
|
+
imported,
|
|
1992
|
+
failed,
|
|
1993
|
+
root: CODEX_SKILLS_DIR
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
return {
|
|
1998
|
+
success: failed.length === 0,
|
|
1999
|
+
imported,
|
|
2000
|
+
failed,
|
|
2001
|
+
root: CODEX_SKILLS_DIR
|
|
2002
|
+
};
|
|
2003
|
+
} catch (e) {
|
|
2004
|
+
return {
|
|
2005
|
+
error: `导入失败:${e && e.message ? e.message : '未知错误'}`
|
|
2006
|
+
};
|
|
2007
|
+
} finally {
|
|
2008
|
+
if (tempDir) {
|
|
2009
|
+
try {
|
|
2010
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
2011
|
+
} catch (_) {}
|
|
2012
|
+
} else if (fs.existsSync(extractionRoot)) {
|
|
2013
|
+
try {
|
|
2014
|
+
fs.rmSync(extractionRoot, { recursive: true, force: true });
|
|
2015
|
+
} catch (_) {}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async function importCodexSkillsFromZip(payload = {}) {
|
|
2021
|
+
if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
|
|
2022
|
+
return { error: '缺少技能压缩包内容' };
|
|
2023
|
+
}
|
|
2024
|
+
const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', payload.fileName || 'codex-skills.zip');
|
|
2025
|
+
if (upload.error) {
|
|
2026
|
+
return { error: upload.error };
|
|
2027
|
+
}
|
|
2028
|
+
return importCodexSkillsFromZipFile(upload.zipPath, {
|
|
2029
|
+
tempDir: upload.tempDir,
|
|
2030
|
+
fallbackName: payload.fileName || ''
|
|
2031
|
+
});
|
|
660
2032
|
}
|
|
661
2033
|
|
|
662
|
-
async function
|
|
663
|
-
const
|
|
664
|
-
|
|
2034
|
+
async function exportCodexSkills(params = {}) {
|
|
2035
|
+
const rawNames = Array.isArray(params.names) ? params.names : [];
|
|
2036
|
+
const uniqueNames = Array.from(new Set(rawNames
|
|
2037
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
2038
|
+
.filter(Boolean)));
|
|
2039
|
+
if (uniqueNames.length === 0) {
|
|
2040
|
+
return { error: '请先选择要导出的 skill' };
|
|
2041
|
+
}
|
|
665
2042
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
} catch (e) {
|
|
672
|
-
lastError = 'Invalid URL';
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
2043
|
+
const exported = [];
|
|
2044
|
+
const failed = [];
|
|
2045
|
+
const stagingTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-skills-export-'));
|
|
2046
|
+
const stagingRoot = path.join(stagingTempDir, 'skills');
|
|
2047
|
+
ensureDir(stagingRoot);
|
|
675
2048
|
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
2049
|
+
try {
|
|
2050
|
+
for (const rawName of uniqueNames) {
|
|
2051
|
+
const normalizedName = normalizeCodexSkillName(rawName);
|
|
2052
|
+
if (normalizedName.error) {
|
|
2053
|
+
failed.push({ name: rawName, error: normalizedName.error });
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
const sourcePath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
|
|
2057
|
+
const sourceRelative = path.relative(CODEX_SKILLS_DIR, sourcePath);
|
|
2058
|
+
if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
|
|
2059
|
+
failed.push({ name: normalizedName.name, error: '来源路径非法' });
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
if (!fs.existsSync(sourcePath)) {
|
|
2063
|
+
failed.push({ name: normalizedName.name, error: 'skill 不存在' });
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
686
2066
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
innerResolve(payload);
|
|
693
|
-
};
|
|
694
|
-
const req = transport.request(parsed, { method: 'GET', headers, agent }, (res) => {
|
|
695
|
-
const status = res.statusCode || 0;
|
|
696
|
-
const contentType = String(res.headers['content-type'] || '').toLowerCase();
|
|
697
|
-
if (status === 404 || status === 405 || status === 501) {
|
|
698
|
-
res.resume();
|
|
699
|
-
return finish({ unavailable: true });
|
|
2067
|
+
try {
|
|
2068
|
+
const lstat = fs.lstatSync(sourcePath);
|
|
2069
|
+
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) {
|
|
2070
|
+
failed.push({ name: normalizedName.name, error: '来源不是技能目录' });
|
|
2071
|
+
continue;
|
|
700
2072
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
2073
|
+
const sourceDirForCopy = lstat.isSymbolicLink() ? fs.realpathSync(sourcePath) : sourcePath;
|
|
2074
|
+
const sourceStat = fs.statSync(sourceDirForCopy);
|
|
2075
|
+
if (!sourceStat.isDirectory()) {
|
|
2076
|
+
failed.push({ name: normalizedName.name, error: '来源 skill 无法读取' });
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const targetPath = path.join(stagingRoot, normalizedName.name);
|
|
2080
|
+
const visitedRealPaths = new Set([sourceDirForCopy]);
|
|
2081
|
+
copyDirRecursive(sourceDirForCopy, targetPath, {
|
|
2082
|
+
dereferenceSymlinks: true,
|
|
2083
|
+
allowedRootRealPath: sourceDirForCopy,
|
|
2084
|
+
visitedRealPaths
|
|
710
2085
|
});
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return finish({ error: `Request failed: ${status}` });
|
|
715
|
-
}
|
|
716
|
-
if (contentType && !contentType.includes('application/json')) {
|
|
717
|
-
return finish({ unavailable: true });
|
|
718
|
-
}
|
|
719
|
-
try {
|
|
720
|
-
const payload = JSON.parse(body || '{}');
|
|
721
|
-
if (!hasModelsListPayload(payload)) {
|
|
722
|
-
return finish({ unavailable: true });
|
|
723
|
-
}
|
|
724
|
-
const models = extractModelNames(payload);
|
|
725
|
-
return finish({ models });
|
|
726
|
-
} catch (e) {
|
|
727
|
-
return finish({ unavailable: true });
|
|
728
|
-
}
|
|
2086
|
+
exported.push({
|
|
2087
|
+
name: normalizedName.name,
|
|
2088
|
+
path: sourcePath
|
|
729
2089
|
});
|
|
730
|
-
})
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
});
|
|
738
|
-
req.end();
|
|
739
|
-
});
|
|
2090
|
+
} catch (e) {
|
|
2091
|
+
failed.push({
|
|
2092
|
+
name: normalizedName.name,
|
|
2093
|
+
error: e && e.message ? e.message : '导出失败'
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
740
2097
|
|
|
741
|
-
if (
|
|
742
|
-
return {
|
|
2098
|
+
if (exported.length === 0) {
|
|
2099
|
+
return {
|
|
2100
|
+
error: failed[0] && failed[0].error ? failed[0].error : '无可导出的 skill',
|
|
2101
|
+
exported,
|
|
2102
|
+
failed,
|
|
2103
|
+
root: CODEX_SKILLS_DIR
|
|
2104
|
+
};
|
|
743
2105
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
2106
|
+
|
|
2107
|
+
const randomToken = crypto.randomBytes(12).toString('hex');
|
|
2108
|
+
const zipFileName = `codex-skills-${randomToken}.zip`;
|
|
2109
|
+
const zipFilePath = path.join(os.tmpdir(), zipFileName);
|
|
2110
|
+
if (fs.existsSync(zipFilePath)) {
|
|
2111
|
+
try {
|
|
2112
|
+
fs.unlinkSync(zipFilePath);
|
|
2113
|
+
} catch (_) {}
|
|
747
2114
|
}
|
|
748
|
-
|
|
2115
|
+
await zipLib.archiveFolder(stagingRoot, zipFilePath);
|
|
2116
|
+
const artifact = registerDownloadArtifact(zipFilePath, {
|
|
2117
|
+
fileName: zipFileName,
|
|
2118
|
+
deleteAfterDownload: true
|
|
2119
|
+
});
|
|
749
2120
|
|
|
750
|
-
|
|
751
|
-
|
|
2121
|
+
return {
|
|
2122
|
+
success: failed.length === 0,
|
|
2123
|
+
fileName: zipFileName,
|
|
2124
|
+
downloadPath: artifact.downloadPath,
|
|
2125
|
+
exported,
|
|
2126
|
+
failed,
|
|
2127
|
+
root: CODEX_SKILLS_DIR
|
|
2128
|
+
};
|
|
2129
|
+
} catch (e) {
|
|
2130
|
+
return {
|
|
2131
|
+
error: `导出失败:${e && e.message ? e.message : '未知错误'}`,
|
|
2132
|
+
exported,
|
|
2133
|
+
failed,
|
|
2134
|
+
root: CODEX_SKILLS_DIR
|
|
2135
|
+
};
|
|
2136
|
+
} finally {
|
|
2137
|
+
try {
|
|
2138
|
+
fs.rmSync(stagingTempDir, { recursive: true, force: true });
|
|
2139
|
+
} catch (_) {}
|
|
752
2140
|
}
|
|
753
|
-
return { unlimited: true };
|
|
754
2141
|
}
|
|
755
2142
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
2143
|
+
function removeDirectoryRecursive(targetPath) {
|
|
2144
|
+
if (typeof fs.rmSync === 'function') {
|
|
2145
|
+
fs.rmSync(targetPath, { recursive: true, force: false });
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
fs.rmdirSync(targetPath, { recursive: true });
|
|
2149
|
+
}
|
|
760
2150
|
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
2151
|
+
function deleteCodexSkills(params = {}) {
|
|
2152
|
+
const rawList = Array.isArray(params.names) ? params.names : [];
|
|
2153
|
+
const uniqueNames = Array.from(new Set(rawList
|
|
2154
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
2155
|
+
.filter(Boolean)));
|
|
2156
|
+
if (!uniqueNames.length) {
|
|
2157
|
+
return { error: '请先选择要删除的 skill' };
|
|
2158
|
+
}
|
|
764
2159
|
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
2160
|
+
const deleted = [];
|
|
2161
|
+
const failed = [];
|
|
2162
|
+
for (const rawName of uniqueNames) {
|
|
2163
|
+
const normalized = normalizeCodexSkillName(rawName);
|
|
2164
|
+
if (normalized.error) {
|
|
2165
|
+
failed.push({ name: rawName, error: normalized.error });
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
772
2168
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
2169
|
+
const skillPath = path.join(CODEX_SKILLS_DIR, normalized.name);
|
|
2170
|
+
const relativePath = path.relative(CODEX_SKILLS_DIR, skillPath);
|
|
2171
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
2172
|
+
failed.push({ name: normalized.name, error: '技能路径非法' });
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
if (!fs.existsSync(skillPath)) {
|
|
2176
|
+
failed.push({ name: normalized.name, error: 'skill 不存在' });
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
779
2179
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
2180
|
+
try {
|
|
2181
|
+
const stat = fs.lstatSync(skillPath);
|
|
2182
|
+
if (!stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
2183
|
+
failed.push({ name: normalized.name, error: '仅支持删除技能目录' });
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
removeDirectoryRecursive(skillPath);
|
|
2187
|
+
deleted.push(normalized.name);
|
|
2188
|
+
} catch (e) {
|
|
2189
|
+
failed.push({
|
|
2190
|
+
name: normalized.name,
|
|
2191
|
+
error: e && e.message ? e.message : '删除失败'
|
|
2192
|
+
});
|
|
786
2193
|
}
|
|
787
|
-
} catch (e) {
|
|
788
|
-
return { error: `目标目录不存在: ${dirPath}` };
|
|
789
2194
|
}
|
|
790
|
-
|
|
2195
|
+
|
|
2196
|
+
return {
|
|
2197
|
+
success: failed.length === 0,
|
|
2198
|
+
deleted,
|
|
2199
|
+
failed,
|
|
2200
|
+
root: CODEX_SKILLS_DIR
|
|
2201
|
+
};
|
|
791
2202
|
}
|
|
792
2203
|
|
|
793
2204
|
function readAgentsFile(params = {}) {
|
|
@@ -1307,11 +2718,28 @@ async function buildConfigHealthReport(params = {}) {
|
|
|
1307
2718
|
const config = status.config || {};
|
|
1308
2719
|
|
|
1309
2720
|
if (status.isVirtual) {
|
|
2721
|
+
const parseFailed = status.errorType === 'parse';
|
|
2722
|
+
const readFailed = status.errorType === 'read';
|
|
1310
2723
|
issues.push({
|
|
1311
|
-
code: 'config-missing',
|
|
1312
|
-
message: status.reason ||
|
|
1313
|
-
|
|
2724
|
+
code: parseFailed ? 'config-parse-failed' : (readFailed ? 'config-read-failed' : 'config-missing'),
|
|
2725
|
+
message: status.reason || (parseFailed
|
|
2726
|
+
? 'config.toml 解析失败'
|
|
2727
|
+
: (readFailed ? '读取 config.toml 失败' : '未检测到 config.toml')),
|
|
2728
|
+
suggestion: parseFailed
|
|
2729
|
+
? '修复 config.toml 语法错误后重试'
|
|
2730
|
+
: (readFailed ? '检查文件权限后重试' : '在模板编辑器中确认应用配置,生成可用的 config.toml')
|
|
1314
2731
|
});
|
|
2732
|
+
if (parseFailed || readFailed) {
|
|
2733
|
+
return {
|
|
2734
|
+
ok: false,
|
|
2735
|
+
issues,
|
|
2736
|
+
summary: {
|
|
2737
|
+
currentProvider: '',
|
|
2738
|
+
currentModel: ''
|
|
2739
|
+
},
|
|
2740
|
+
remote: null
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
1315
2743
|
}
|
|
1316
2744
|
|
|
1317
2745
|
const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
@@ -1498,13 +2926,26 @@ function readConfigOrVirtualDefault() {
|
|
|
1498
2926
|
return {
|
|
1499
2927
|
config: readConfig(),
|
|
1500
2928
|
isVirtual: false,
|
|
1501
|
-
reason: ''
|
|
2929
|
+
reason: '',
|
|
2930
|
+
detail: '',
|
|
2931
|
+
errorType: ''
|
|
1502
2932
|
};
|
|
1503
2933
|
} catch (e) {
|
|
2934
|
+
const errorType = typeof e.configErrorType === 'string' && e.configErrorType.trim()
|
|
2935
|
+
? e.configErrorType.trim()
|
|
2936
|
+
: 'read';
|
|
2937
|
+
const publicReason = typeof e.configPublicReason === 'string' && e.configPublicReason.trim()
|
|
2938
|
+
? e.configPublicReason.trim()
|
|
2939
|
+
: (errorType === 'parse' ? 'config.toml 解析失败' : '读取 config.toml 失败');
|
|
2940
|
+
const detail = typeof e.configDetail === 'string' && e.configDetail.trim()
|
|
2941
|
+
? e.configDetail.trim()
|
|
2942
|
+
: (e && e.message ? e.message : publicReason);
|
|
1504
2943
|
return {
|
|
1505
|
-
config: buildVirtualDefaultConfig(),
|
|
2944
|
+
config: errorType === 'missing' ? buildVirtualDefaultConfig() : {},
|
|
1506
2945
|
isVirtual: true,
|
|
1507
|
-
reason:
|
|
2946
|
+
reason: publicReason,
|
|
2947
|
+
detail,
|
|
2948
|
+
errorType
|
|
1508
2949
|
};
|
|
1509
2950
|
}
|
|
1510
2951
|
}
|
|
@@ -1512,10 +2953,31 @@ function readConfigOrVirtualDefault() {
|
|
|
1512
2953
|
return {
|
|
1513
2954
|
config: buildVirtualDefaultConfig(),
|
|
1514
2955
|
isVirtual: true,
|
|
1515
|
-
reason:
|
|
2956
|
+
reason: '未检测到 config.toml',
|
|
2957
|
+
detail: `配置文件不存在: ${CONFIG_FILE}`,
|
|
2958
|
+
errorType: 'missing'
|
|
1516
2959
|
};
|
|
1517
2960
|
}
|
|
1518
2961
|
|
|
2962
|
+
function hasConfigLoadError(result) {
|
|
2963
|
+
return !!(result
|
|
2964
|
+
&& result.isVirtual
|
|
2965
|
+
&& (result.errorType === 'parse' || result.errorType === 'read'));
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function printConfigLoadErrorAndMarkExit(result) {
|
|
2969
|
+
const isReadError = result && result.errorType === 'read';
|
|
2970
|
+
const detail = result && typeof result.detail === 'string' && result.detail.trim()
|
|
2971
|
+
? result.detail.trim()
|
|
2972
|
+
: (isReadError ? '读取配置文件失败' : '配置文件解析失败');
|
|
2973
|
+
console.error(`\n错误: ${isReadError ? '读取 config.toml 失败' : '配置文件解析失败'}`);
|
|
2974
|
+
console.error(` 详情: ${detail}`);
|
|
2975
|
+
console.error(` 路径: ${CONFIG_FILE}`);
|
|
2976
|
+
console.error(` 建议: ${isReadError ? '检查文件权限后重试' : '修复 config.toml 语法后重试'}`);
|
|
2977
|
+
console.error();
|
|
2978
|
+
process.exitCode = 1;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
1519
2981
|
function normalizeTopLevelConfigWithTemplate(template, selectedProvider, selectedModel) {
|
|
1520
2982
|
let content = typeof template === 'string' ? template : '';
|
|
1521
2983
|
if (!content.trim()) {
|
|
@@ -1656,6 +3118,9 @@ function addProviderToConfig(params = {}) {
|
|
|
1656
3118
|
|
|
1657
3119
|
if (!name) return { error: '名称不能为空' };
|
|
1658
3120
|
if (!url) return { error: 'URL 不能为空' };
|
|
3121
|
+
if (!isValidProviderName(name)) {
|
|
3122
|
+
return { error: '名称仅支持字母/数字/._-' };
|
|
3123
|
+
}
|
|
1659
3124
|
if (isReservedProviderNameForCreation(name)) {
|
|
1660
3125
|
return { error: 'local provider 为系统保留名称,不可新增' };
|
|
1661
3126
|
}
|
|
@@ -1687,24 +3152,20 @@ function addProviderToConfig(params = {}) {
|
|
|
1687
3152
|
return { error: `config.toml 解析失败: ${e.message}` };
|
|
1688
3153
|
}
|
|
1689
3154
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if (
|
|
3155
|
+
const providerHeaderSegmentKeySet = collectModelProviderHeaderSegmentKeySet(content);
|
|
3156
|
+
const normalizedProviders = isPlainObject(parsed.model_providers)
|
|
3157
|
+
? normalizeLegacyModelProviders(parsed.model_providers, providerHeaderSegmentKeySet)
|
|
3158
|
+
: {};
|
|
3159
|
+
if (normalizedProviders && normalizedProviders[name]) {
|
|
1695
3160
|
return { error: '提供商已存在' };
|
|
1696
3161
|
}
|
|
1697
3162
|
|
|
1698
|
-
const escapeTomlString = (value) => String(value || '')
|
|
1699
|
-
.replace(/\\/g, '\\\\')
|
|
1700
|
-
.replace(/"/g, '\\"');
|
|
1701
|
-
|
|
1702
3163
|
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
1703
|
-
const safeName =
|
|
1704
|
-
const safeUrl =
|
|
1705
|
-
const safeKey =
|
|
3164
|
+
const safeName = escapeTomlBasicString(name);
|
|
3165
|
+
const safeUrl = escapeTomlBasicString(url);
|
|
3166
|
+
const safeKey = escapeTomlBasicString(key);
|
|
1706
3167
|
const block = [
|
|
1707
|
-
|
|
3168
|
+
buildModelProviderTableHeader(name),
|
|
1708
3169
|
`name = "${safeName}"`,
|
|
1709
3170
|
`base_url = "${safeUrl}"`,
|
|
1710
3171
|
`wire_api = "responses"`,
|
|
@@ -1804,8 +3265,36 @@ function performProviderDeletion(name, options = {}) {
|
|
|
1804
3265
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1805
3266
|
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
1806
3267
|
const hasBom = content.charCodeAt(0) === 0xFEFF;
|
|
1807
|
-
const
|
|
1808
|
-
const
|
|
3268
|
+
const providerConfig = config.model_providers[name];
|
|
3269
|
+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
|
|
3270
|
+
? providerConfig.__codexmate_legacy_segments
|
|
3271
|
+
: null;
|
|
3272
|
+
const providerSegmentVariants = (() => {
|
|
3273
|
+
const variants = [];
|
|
3274
|
+
const seen = new Set();
|
|
3275
|
+
const pushVariant = (segments) => {
|
|
3276
|
+
const normalized = normalizeLegacySegments(segments);
|
|
3277
|
+
const key = buildLegacySegmentsKey(normalized);
|
|
3278
|
+
if (!key || seen.has(key)) return;
|
|
3279
|
+
seen.add(key);
|
|
3280
|
+
variants.push(normalized);
|
|
3281
|
+
};
|
|
3282
|
+
if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
|
|
3283
|
+
pushVariant(providerConfig.__codexmate_legacy_segments);
|
|
3284
|
+
}
|
|
3285
|
+
if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
|
|
3286
|
+
for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
|
|
3287
|
+
pushVariant(segments);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
if (providerSegments) {
|
|
3291
|
+
pushVariant(providerSegments);
|
|
3292
|
+
}
|
|
3293
|
+
if (variants.length === 0) {
|
|
3294
|
+
pushVariant(String(name || '').split('.').filter((item) => item));
|
|
3295
|
+
}
|
|
3296
|
+
return variants;
|
|
3297
|
+
})();
|
|
1809
3298
|
|
|
1810
3299
|
const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
|
|
1811
3300
|
if (remainingProviders.length === 0) {
|
|
@@ -1844,17 +3333,25 @@ function performProviderDeletion(name, options = {}) {
|
|
|
1844
3333
|
};
|
|
1845
3334
|
|
|
1846
3335
|
let updatedContent = null;
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
3336
|
+
const combinedRanges = [];
|
|
3337
|
+
for (const segments of providerSegmentVariants) {
|
|
3338
|
+
combinedRanges.push(...findProviderSectionRanges(content, name, segments));
|
|
3339
|
+
combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
|
|
3340
|
+
}
|
|
3341
|
+
if (combinedRanges.length === 0) {
|
|
3342
|
+
combinedRanges.push(...findProviderSectionRanges(content, name, providerSegments));
|
|
3343
|
+
}
|
|
3344
|
+
if (combinedRanges.length > 0) {
|
|
3345
|
+
const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
|
|
3346
|
+
const seen = new Set();
|
|
3347
|
+
let removedContent = content;
|
|
3348
|
+
for (const range of sorted) {
|
|
3349
|
+
const rangeKey = `${range.start}:${range.end}`;
|
|
3350
|
+
if (seen.has(rangeKey)) continue;
|
|
3351
|
+
seen.add(rangeKey);
|
|
3352
|
+
removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
|
|
3353
|
+
}
|
|
3354
|
+
updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
|
|
1858
3355
|
}
|
|
1859
3356
|
|
|
1860
3357
|
if (updatedContent) {
|
|
@@ -4857,7 +6354,12 @@ async function cmdSetup() {
|
|
|
4857
6354
|
|
|
4858
6355
|
// 显示当前状态
|
|
4859
6356
|
function cmdStatus() {
|
|
4860
|
-
const
|
|
6357
|
+
const configResult = readConfigOrVirtualDefault();
|
|
6358
|
+
if (hasConfigLoadError(configResult)) {
|
|
6359
|
+
printConfigLoadErrorAndMarkExit(configResult);
|
|
6360
|
+
return;
|
|
6361
|
+
}
|
|
6362
|
+
const { config, isVirtual } = configResult;
|
|
4861
6363
|
const current = config.model_provider || '未设置';
|
|
4862
6364
|
const currentModel = config.model || '未设置';
|
|
4863
6365
|
|
|
@@ -4873,7 +6375,12 @@ function cmdStatus() {
|
|
|
4873
6375
|
|
|
4874
6376
|
// 列出所有提供商
|
|
4875
6377
|
function cmdList() {
|
|
4876
|
-
const
|
|
6378
|
+
const configResult = readConfigOrVirtualDefault();
|
|
6379
|
+
if (hasConfigLoadError(configResult)) {
|
|
6380
|
+
printConfigLoadErrorAndMarkExit(configResult);
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
const { config, isVirtual } = configResult;
|
|
4877
6384
|
const providers = config.model_providers || {};
|
|
4878
6385
|
const current = config.model_provider;
|
|
4879
6386
|
|
|
@@ -5028,6 +6535,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
5028
6535
|
}
|
|
5029
6536
|
throw new Error('名称和URL必填');
|
|
5030
6537
|
}
|
|
6538
|
+
if (!isValidProviderName(providerName)) {
|
|
6539
|
+
if (!silent) console.error('错误: 名称仅支持字母/数字/._-');
|
|
6540
|
+
throw new Error('名称仅支持字母/数字/._-');
|
|
6541
|
+
}
|
|
5031
6542
|
if (isReservedProviderNameForCreation(providerName)) {
|
|
5032
6543
|
if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
|
|
5033
6544
|
throw new Error('local provider 为系统保留名称,不可新增');
|
|
@@ -5039,13 +6550,16 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
5039
6550
|
throw new Error('提供商已存在');
|
|
5040
6551
|
}
|
|
5041
6552
|
|
|
6553
|
+
const safeName = escapeTomlBasicString(providerName);
|
|
6554
|
+
const safeBaseUrl = escapeTomlBasicString(providerBaseUrl);
|
|
6555
|
+
const safeApiKey = escapeTomlBasicString(apiKey || '');
|
|
5042
6556
|
const newBlock = `
|
|
5043
|
-
|
|
5044
|
-
name = "${
|
|
5045
|
-
base_url = "${
|
|
6557
|
+
${buildModelProviderTableHeader(providerName)}
|
|
6558
|
+
name = "${safeName}"
|
|
6559
|
+
base_url = "${safeBaseUrl}"
|
|
5046
6560
|
wire_api = "responses"
|
|
5047
6561
|
requires_openai_auth = false
|
|
5048
|
-
preferred_auth_method = "${
|
|
6562
|
+
preferred_auth_method = "${safeApiKey}"
|
|
5049
6563
|
request_max_retries = 4
|
|
5050
6564
|
stream_max_retries = 10
|
|
5051
6565
|
stream_idle_timeout_ms = 300000
|
|
@@ -5105,42 +6619,156 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
|
5105
6619
|
}
|
|
5106
6620
|
|
|
5107
6621
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
5108
|
-
const
|
|
5109
|
-
const
|
|
5110
|
-
|
|
5111
|
-
|
|
6622
|
+
const providerConfig = config.model_providers[name];
|
|
6623
|
+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
|
|
6624
|
+
? providerConfig.__codexmate_legacy_segments
|
|
6625
|
+
: null;
|
|
6626
|
+
const ranges = findProviderSectionRanges(content, name, providerSegments);
|
|
6627
|
+
if (ranges.length === 0) {
|
|
5112
6628
|
if (!silent) console.error('错误: 无法找到提供商配置块');
|
|
5113
6629
|
throw new Error('无法找到提供商配置块');
|
|
5114
6630
|
}
|
|
5115
6631
|
|
|
5116
|
-
const
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
6632
|
+
const replaceTomlStringField = (block, fieldName, rawValue) => {
|
|
6633
|
+
const safeValue = escapeTomlBasicString(rawValue);
|
|
6634
|
+
const escapedFieldName = escapeRegex(fieldName);
|
|
6635
|
+
const multilineRanges = collectTomlMultilineStringRanges(block);
|
|
6636
|
+
const tripleStartRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)(\"\"\"|''')`, 'mg');
|
|
6637
|
+
let tripleStartMatch = null;
|
|
6638
|
+
let tripleCandidate;
|
|
6639
|
+
while ((tripleCandidate = tripleStartRegex.exec(block)) !== null) {
|
|
6640
|
+
if (isIndexInRanges(tripleCandidate.index, multilineRanges)) {
|
|
6641
|
+
continue;
|
|
6642
|
+
}
|
|
6643
|
+
tripleStartMatch = tripleCandidate;
|
|
6644
|
+
break;
|
|
6645
|
+
}
|
|
6646
|
+
if (tripleStartMatch) {
|
|
6647
|
+
const prefixStart = tripleStartMatch.index;
|
|
6648
|
+
const prefixEnd = prefixStart + tripleStartMatch[1].length;
|
|
6649
|
+
const tripleQuote = tripleStartMatch[2];
|
|
6650
|
+
const valueStart = prefixEnd + tripleQuote.length;
|
|
6651
|
+
const quoteChar = tripleQuote[0];
|
|
6652
|
+
let valueEnd = -1;
|
|
6653
|
+
let closingRunLength = 0;
|
|
6654
|
+
for (let i = valueStart; i < block.length; i++) {
|
|
6655
|
+
if (block[i] !== quoteChar) continue;
|
|
6656
|
+
let runEnd = i + 1;
|
|
6657
|
+
while (runEnd < block.length && block[runEnd] === quoteChar) {
|
|
6658
|
+
runEnd++;
|
|
6659
|
+
}
|
|
6660
|
+
const runLength = runEnd - i;
|
|
6661
|
+
if (runLength < tripleQuote.length) {
|
|
6662
|
+
i = runEnd - 1;
|
|
6663
|
+
continue;
|
|
6664
|
+
}
|
|
6665
|
+
if (tripleQuote === '"""') {
|
|
6666
|
+
let slashCount = 0;
|
|
6667
|
+
for (let j = i - 1; j >= valueStart && block[j] === '\\'; j--) {
|
|
6668
|
+
slashCount++;
|
|
6669
|
+
}
|
|
6670
|
+
if (slashCount % 2 !== 0) {
|
|
6671
|
+
continue;
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
valueEnd = i;
|
|
6675
|
+
closingRunLength = runLength;
|
|
6676
|
+
break;
|
|
6677
|
+
}
|
|
6678
|
+
if (valueEnd === -1) {
|
|
6679
|
+
throw new Error(`${fieldName} 使用了未闭合的多行 TOML 字符串,无法安全更新`);
|
|
6680
|
+
}
|
|
6681
|
+
const lineEndIndex = block.indexOf('\n', valueEnd + closingRunLength);
|
|
6682
|
+
let tailEnd = lineEndIndex === -1 ? block.length : lineEndIndex;
|
|
6683
|
+
if (lineEndIndex > 0 && block[lineEndIndex - 1] === '\r') {
|
|
6684
|
+
tailEnd = lineEndIndex - 1;
|
|
6685
|
+
}
|
|
6686
|
+
const tail = block.slice(valueEnd + closingRunLength, tailEnd);
|
|
6687
|
+
const tailMatch = tail.match(/^(\s+#.*)?\s*$/);
|
|
6688
|
+
if (!tailMatch) {
|
|
6689
|
+
throw new Error(`${fieldName} 多行字符串后的语法不受支持,无法安全更新`);
|
|
6690
|
+
}
|
|
6691
|
+
const commentSuffix = tailMatch[1] || '';
|
|
6692
|
+
const replacementLine = `${block.slice(prefixStart, prefixEnd)}"${safeValue}"${commentSuffix}`;
|
|
6693
|
+
return block.slice(0, prefixStart) + replacementLine + block.slice(tailEnd);
|
|
6694
|
+
}
|
|
5123
6695
|
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
updatedBlock = updatedBlock.replace(
|
|
5128
|
-
/^(base_url\s*=\s*)(["']).*?\2/m,
|
|
5129
|
-
`$1$2${baseUrl}$2`
|
|
6696
|
+
const withCommentRegex = new RegExp(
|
|
6697
|
+
`^(\\s*${escapedFieldName}\\s*=\\s*)(?:"(?:\\\\.|[^"\\\\])*"|'[^'\\n]*')(\\s+#.*)?$`,
|
|
6698
|
+
'mg'
|
|
5130
6699
|
);
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
6700
|
+
let replaced = false;
|
|
6701
|
+
let next = block.replace(
|
|
6702
|
+
withCommentRegex,
|
|
6703
|
+
(full, prefix, suffix = '', offset) => {
|
|
6704
|
+
if (replaced || isIndexInRanges(offset, multilineRanges)) {
|
|
6705
|
+
return full;
|
|
6706
|
+
}
|
|
6707
|
+
replaced = true;
|
|
6708
|
+
return `${prefix}"${safeValue}"${suffix}`;
|
|
6709
|
+
}
|
|
5138
6710
|
);
|
|
6711
|
+
if (!replaced) {
|
|
6712
|
+
const fallbackRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)(.*?)(\\s+#.*)?$`, 'mg');
|
|
6713
|
+
let fallbackReplaced = false;
|
|
6714
|
+
const multilineRangesForNext = collectTomlMultilineStringRanges(next);
|
|
6715
|
+
let fallbackMatch;
|
|
6716
|
+
let fallbackCandidate;
|
|
6717
|
+
while ((fallbackCandidate = fallbackRegex.exec(next)) !== null) {
|
|
6718
|
+
if (isIndexInRanges(fallbackCandidate.index, multilineRangesForNext)) {
|
|
6719
|
+
continue;
|
|
6720
|
+
}
|
|
6721
|
+
fallbackMatch = fallbackCandidate;
|
|
6722
|
+
break;
|
|
6723
|
+
}
|
|
6724
|
+
if (fallbackMatch) {
|
|
6725
|
+
const existingValue = String(fallbackMatch[2] || '').trim();
|
|
6726
|
+
const looksLikeMultilineArray = existingValue.startsWith('[') && !existingValue.endsWith(']');
|
|
6727
|
+
const looksLikeMultilineInlineTable = existingValue.startsWith('{') && !existingValue.endsWith('}');
|
|
6728
|
+
if (looksLikeMultilineArray || looksLikeMultilineInlineTable) {
|
|
6729
|
+
throw new Error(`${fieldName} 当前值是多行 TOML 结构,无法安全更新`);
|
|
6730
|
+
}
|
|
6731
|
+
const prefix = fallbackMatch[1];
|
|
6732
|
+
const suffix = fallbackMatch[3] || '';
|
|
6733
|
+
const replacement = `${prefix}"${safeValue}"${suffix}`;
|
|
6734
|
+
next = `${next.slice(0, fallbackMatch.index)}${replacement}${next.slice(fallbackMatch.index + fallbackMatch[0].length)}`;
|
|
6735
|
+
fallbackReplaced = true;
|
|
6736
|
+
}
|
|
6737
|
+
if (!fallbackReplaced) {
|
|
6738
|
+
const keyIndentMatch = block.match(/^(\s*)[A-Za-z0-9_.-]+\s*=/m);
|
|
6739
|
+
const indent = keyIndentMatch ? keyIndentMatch[1] : '';
|
|
6740
|
+
const lineEnding = block.includes('\r\n') ? '\r\n' : '\n';
|
|
6741
|
+
const tailMatch = block.match(/(\s*)$/);
|
|
6742
|
+
const tail = tailMatch ? tailMatch[1] : '';
|
|
6743
|
+
const body = block.slice(0, block.length - tail.length);
|
|
6744
|
+
const separator = body.endsWith('\n') || body.endsWith('\r') ? '' : lineEnding;
|
|
6745
|
+
next = `${body}${separator}${indent}${fieldName} = "${safeValue}"${tail}`;
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
return next;
|
|
6749
|
+
};
|
|
6750
|
+
|
|
6751
|
+
let newContent = content;
|
|
6752
|
+
const sorted = ranges.sort((a, b) => b.start - a.start);
|
|
6753
|
+
for (const range of sorted) {
|
|
6754
|
+
const providerBlock = newContent.slice(range.start, range.end);
|
|
6755
|
+
let updatedBlock = providerBlock;
|
|
6756
|
+
if (baseUrl) {
|
|
6757
|
+
updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', baseUrl);
|
|
6758
|
+
}
|
|
6759
|
+
if (apiKey !== undefined) {
|
|
6760
|
+
updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', apiKey);
|
|
6761
|
+
}
|
|
6762
|
+
newContent = newContent.slice(0, range.start) + updatedBlock + newContent.slice(range.end);
|
|
5139
6763
|
}
|
|
5140
6764
|
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
6765
|
+
const finalContent = newContent.trim();
|
|
6766
|
+
try {
|
|
6767
|
+
toml.parse(finalContent);
|
|
6768
|
+
} catch (e) {
|
|
6769
|
+
throw new Error(`更新后的 config.toml 无效: ${e.message}`);
|
|
6770
|
+
}
|
|
6771
|
+
writeConfig(finalContent);
|
|
5144
6772
|
|
|
5145
6773
|
// 如果更新了 API Key 且该提供商是当前激活的,同步更新 auth.json
|
|
5146
6774
|
const currentProvider = config.model_provider;
|
|
@@ -5297,12 +6925,76 @@ function readClaudeSettingsInfo() {
|
|
|
5297
6925
|
: {};
|
|
5298
6926
|
|
|
5299
6927
|
return {
|
|
5300
|
-
exists: !!readResult.exists,
|
|
5301
|
-
targetPath: CLAUDE_SETTINGS_FILE,
|
|
5302
|
-
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
5303
|
-
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
5304
|
-
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
5305
|
-
env
|
|
6928
|
+
exists: !!readResult.exists,
|
|
6929
|
+
targetPath: CLAUDE_SETTINGS_FILE,
|
|
6930
|
+
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
6931
|
+
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
6932
|
+
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
6933
|
+
env
|
|
6934
|
+
};
|
|
6935
|
+
}
|
|
6936
|
+
|
|
6937
|
+
function registerDownloadArtifact(filePath, options = {}) {
|
|
6938
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
6939
|
+
const fileName = typeof options.fileName === 'string' && options.fileName.trim()
|
|
6940
|
+
? options.fileName.trim()
|
|
6941
|
+
: path.basename(filePath || '');
|
|
6942
|
+
const ttlMs = Number.isFinite(options.ttlMs) && options.ttlMs > 0
|
|
6943
|
+
? Math.floor(options.ttlMs)
|
|
6944
|
+
: DOWNLOAD_ARTIFACT_TTL_MS;
|
|
6945
|
+
const expiresAt = Date.now() + ttlMs;
|
|
6946
|
+
const deleteAfterDownload = options.deleteAfterDownload !== false;
|
|
6947
|
+
|
|
6948
|
+
g_downloadArtifacts.set(token, {
|
|
6949
|
+
filePath,
|
|
6950
|
+
fileName,
|
|
6951
|
+
deleteAfterDownload,
|
|
6952
|
+
expiresAt
|
|
6953
|
+
});
|
|
6954
|
+
|
|
6955
|
+
setTimeout(() => {
|
|
6956
|
+
const artifact = g_downloadArtifacts.get(token);
|
|
6957
|
+
if (!artifact) return;
|
|
6958
|
+
if (Date.now() < artifact.expiresAt) return;
|
|
6959
|
+
g_downloadArtifacts.delete(token);
|
|
6960
|
+
if (artifact.deleteAfterDownload && artifact.filePath && fs.existsSync(artifact.filePath)) {
|
|
6961
|
+
try {
|
|
6962
|
+
fs.unlinkSync(artifact.filePath);
|
|
6963
|
+
} catch (_) {}
|
|
6964
|
+
}
|
|
6965
|
+
}, ttlMs + 2000);
|
|
6966
|
+
|
|
6967
|
+
return {
|
|
6968
|
+
token,
|
|
6969
|
+
fileName,
|
|
6970
|
+
downloadPath: `/download/${encodeURIComponent(token)}`
|
|
6971
|
+
};
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6974
|
+
function resolveDownloadArtifact(tokenOrFileName, options = {}) {
|
|
6975
|
+
if (!tokenOrFileName) return null;
|
|
6976
|
+
const token = typeof tokenOrFileName === 'string' ? tokenOrFileName.trim() : '';
|
|
6977
|
+
if (!token) return null;
|
|
6978
|
+
|
|
6979
|
+
const artifact = g_downloadArtifacts.get(token);
|
|
6980
|
+
if (!artifact) {
|
|
6981
|
+
return null;
|
|
6982
|
+
}
|
|
6983
|
+
if (Date.now() > artifact.expiresAt) {
|
|
6984
|
+
g_downloadArtifacts.delete(token);
|
|
6985
|
+
if (artifact.deleteAfterDownload && artifact.filePath && fs.existsSync(artifact.filePath)) {
|
|
6986
|
+
try {
|
|
6987
|
+
fs.unlinkSync(artifact.filePath);
|
|
6988
|
+
} catch (_) {}
|
|
6989
|
+
}
|
|
6990
|
+
return null;
|
|
6991
|
+
}
|
|
6992
|
+
if (options && options.consume === true) {
|
|
6993
|
+
g_downloadArtifacts.delete(token);
|
|
6994
|
+
}
|
|
6995
|
+
return {
|
|
6996
|
+
token,
|
|
6997
|
+
...artifact
|
|
5306
6998
|
};
|
|
5307
6999
|
}
|
|
5308
7000
|
|
|
@@ -5368,23 +7060,206 @@ async function prepareCodexDirDownload() {
|
|
|
5368
7060
|
}
|
|
5369
7061
|
}
|
|
5370
7062
|
|
|
5371
|
-
function copyDirRecursive(srcDir, destDir) {
|
|
7063
|
+
function copyDirRecursive(srcDir, destDir, options = {}) {
|
|
7064
|
+
const dereferenceSymlinks = !!(options && options.dereferenceSymlinks);
|
|
7065
|
+
const allowedRootRealPath = (options && typeof options.allowedRootRealPath === 'string')
|
|
7066
|
+
? options.allowedRootRealPath
|
|
7067
|
+
: '';
|
|
7068
|
+
const visitedRealPaths = options && options.visitedRealPaths instanceof Set
|
|
7069
|
+
? options.visitedRealPaths
|
|
7070
|
+
: new Set();
|
|
7071
|
+
const childOptions = {
|
|
7072
|
+
...options,
|
|
7073
|
+
dereferenceSymlinks,
|
|
7074
|
+
allowedRootRealPath,
|
|
7075
|
+
visitedRealPaths
|
|
7076
|
+
};
|
|
5372
7077
|
ensureDir(destDir);
|
|
5373
7078
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
5374
7079
|
for (const entry of entries) {
|
|
5375
7080
|
const srcPath = path.join(srcDir, entry.name);
|
|
5376
7081
|
const destPath = path.join(destDir, entry.name);
|
|
5377
7082
|
if (entry.isDirectory()) {
|
|
5378
|
-
|
|
7083
|
+
if (!dereferenceSymlinks) {
|
|
7084
|
+
copyDirRecursive(srcPath, destPath, childOptions);
|
|
7085
|
+
continue;
|
|
7086
|
+
}
|
|
7087
|
+
const realPath = fs.realpathSync(srcPath);
|
|
7088
|
+
if (allowedRootRealPath && !isPathInside(realPath, allowedRootRealPath)) {
|
|
7089
|
+
throw new Error(`symlink escapes skill root: ${srcPath}`);
|
|
7090
|
+
}
|
|
7091
|
+
if (visitedRealPaths.has(realPath)) {
|
|
7092
|
+
continue;
|
|
7093
|
+
}
|
|
7094
|
+
visitedRealPaths.add(realPath);
|
|
7095
|
+
try {
|
|
7096
|
+
copyDirRecursive(srcPath, destPath, childOptions);
|
|
7097
|
+
} finally {
|
|
7098
|
+
visitedRealPaths.delete(realPath);
|
|
7099
|
+
}
|
|
5379
7100
|
} else if (entry.isSymbolicLink()) {
|
|
5380
|
-
|
|
5381
|
-
|
|
7101
|
+
if (dereferenceSymlinks) {
|
|
7102
|
+
const realPath = fs.realpathSync(srcPath);
|
|
7103
|
+
if (allowedRootRealPath && !isPathInside(realPath, allowedRootRealPath)) {
|
|
7104
|
+
throw new Error(`symlink escapes skill root: ${srcPath}`);
|
|
7105
|
+
}
|
|
7106
|
+
const realStat = fs.statSync(realPath);
|
|
7107
|
+
if (realStat.isDirectory()) {
|
|
7108
|
+
if (visitedRealPaths.has(realPath)) {
|
|
7109
|
+
continue;
|
|
7110
|
+
}
|
|
7111
|
+
visitedRealPaths.add(realPath);
|
|
7112
|
+
try {
|
|
7113
|
+
copyDirRecursive(realPath, destPath, childOptions);
|
|
7114
|
+
} finally {
|
|
7115
|
+
visitedRealPaths.delete(realPath);
|
|
7116
|
+
}
|
|
7117
|
+
} else {
|
|
7118
|
+
fs.copyFileSync(realPath, destPath);
|
|
7119
|
+
}
|
|
7120
|
+
} else {
|
|
7121
|
+
const target = fs.readlinkSync(srcPath);
|
|
7122
|
+
fs.symlinkSync(target, destPath);
|
|
7123
|
+
}
|
|
5382
7124
|
} else {
|
|
5383
7125
|
fs.copyFileSync(srcPath, destPath);
|
|
5384
7126
|
}
|
|
5385
7127
|
}
|
|
5386
7128
|
}
|
|
5387
7129
|
|
|
7130
|
+
function inspectZipArchiveLimits(zipPath, options = {}) {
|
|
7131
|
+
const maxEntryCount = Number.isFinite(options.maxEntryCount) && options.maxEntryCount > 0
|
|
7132
|
+
? Math.floor(options.maxEntryCount)
|
|
7133
|
+
: MAX_SKILLS_ZIP_ENTRY_COUNT;
|
|
7134
|
+
const maxUncompressedBytes = Number.isFinite(options.maxUncompressedBytes) && options.maxUncompressedBytes > 0
|
|
7135
|
+
? Math.floor(options.maxUncompressedBytes)
|
|
7136
|
+
: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES;
|
|
7137
|
+
|
|
7138
|
+
return new Promise((resolve, reject) => {
|
|
7139
|
+
yauzl.open(zipPath, { lazyEntries: true, autoClose: true }, (openErr, zipFile) => {
|
|
7140
|
+
if (openErr) {
|
|
7141
|
+
reject(openErr);
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
7144
|
+
if (!zipFile) {
|
|
7145
|
+
reject(new Error('无法读取 ZIP 文件'));
|
|
7146
|
+
return;
|
|
7147
|
+
}
|
|
7148
|
+
let entryCount = 0;
|
|
7149
|
+
let totalUncompressedBytes = 0;
|
|
7150
|
+
let settled = false;
|
|
7151
|
+
const finish = (err, data) => {
|
|
7152
|
+
if (settled) return;
|
|
7153
|
+
settled = true;
|
|
7154
|
+
try {
|
|
7155
|
+
zipFile.close();
|
|
7156
|
+
} catch (_) {}
|
|
7157
|
+
if (err) {
|
|
7158
|
+
reject(err);
|
|
7159
|
+
} else {
|
|
7160
|
+
resolve(data);
|
|
7161
|
+
}
|
|
7162
|
+
};
|
|
7163
|
+
|
|
7164
|
+
zipFile.on('entry', (entry) => {
|
|
7165
|
+
if (settled) return;
|
|
7166
|
+
entryCount += 1;
|
|
7167
|
+
const entrySize = Number.isFinite(entry.uncompressedSize) ? entry.uncompressedSize : 0;
|
|
7168
|
+
totalUncompressedBytes += entrySize;
|
|
7169
|
+
if (entryCount > maxEntryCount) {
|
|
7170
|
+
finish(new Error(`压缩包条目过多(>${maxEntryCount})`));
|
|
7171
|
+
return;
|
|
7172
|
+
}
|
|
7173
|
+
if (totalUncompressedBytes > maxUncompressedBytes) {
|
|
7174
|
+
finish(new Error(`压缩包解压总大小超限(>${Math.floor(maxUncompressedBytes / 1024 / 1024)}MB)`));
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
7177
|
+
zipFile.readEntry();
|
|
7178
|
+
});
|
|
7179
|
+
|
|
7180
|
+
zipFile.on('end', () => {
|
|
7181
|
+
finish(null, { entryCount, totalUncompressedBytes });
|
|
7182
|
+
});
|
|
7183
|
+
|
|
7184
|
+
zipFile.on('error', (zipErr) => {
|
|
7185
|
+
finish(zipErr);
|
|
7186
|
+
});
|
|
7187
|
+
|
|
7188
|
+
zipFile.readEntry();
|
|
7189
|
+
});
|
|
7190
|
+
});
|
|
7191
|
+
}
|
|
7192
|
+
|
|
7193
|
+
function writeUploadZipStream(req, prefix, originalName = '', maxSize = MAX_SKILLS_ZIP_UPLOAD_SIZE) {
|
|
7194
|
+
return new Promise((resolve, reject) => {
|
|
7195
|
+
const lengthHeader = parseInt(req.headers['content-length'] || '0', 10);
|
|
7196
|
+
if (Number.isFinite(lengthHeader) && lengthHeader > maxSize) {
|
|
7197
|
+
reject(new Error(`备份文件过大(>${Math.floor(maxSize / 1024 / 1024)}MB)`));
|
|
7198
|
+
return;
|
|
7199
|
+
}
|
|
7200
|
+
|
|
7201
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
7202
|
+
const rawName = originalName && typeof originalName === 'string' ? originalName : `${prefix}.zip`;
|
|
7203
|
+
const fileName = path.basename(rawName);
|
|
7204
|
+
const zipPath = path.join(tempDir, fileName.toLowerCase().endsWith('.zip') ? fileName : `${fileName}.zip`);
|
|
7205
|
+
const stream = fs.createWriteStream(zipPath);
|
|
7206
|
+
let bytesWritten = 0;
|
|
7207
|
+
let settled = false;
|
|
7208
|
+
let hasContent = false;
|
|
7209
|
+
|
|
7210
|
+
const fail = (err) => {
|
|
7211
|
+
if (settled) return;
|
|
7212
|
+
settled = true;
|
|
7213
|
+
try {
|
|
7214
|
+
stream.destroy();
|
|
7215
|
+
} catch (_) {}
|
|
7216
|
+
try {
|
|
7217
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
7218
|
+
} catch (_) {}
|
|
7219
|
+
reject(err);
|
|
7220
|
+
};
|
|
7221
|
+
|
|
7222
|
+
const done = () => {
|
|
7223
|
+
if (settled) return;
|
|
7224
|
+
settled = true;
|
|
7225
|
+
if (!hasContent || bytesWritten <= 0) {
|
|
7226
|
+
try {
|
|
7227
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
7228
|
+
} catch (_) {}
|
|
7229
|
+
reject(new Error('备份文件为空'));
|
|
7230
|
+
return;
|
|
7231
|
+
}
|
|
7232
|
+
resolve({ tempDir, zipPath });
|
|
7233
|
+
};
|
|
7234
|
+
|
|
7235
|
+
req.on('error', (err) => fail(err));
|
|
7236
|
+
req.on('aborted', () => fail(new Error('上传已中断')));
|
|
7237
|
+
req.on('close', () => {
|
|
7238
|
+
if (!settled && !req.complete) {
|
|
7239
|
+
fail(new Error('上传已中断'));
|
|
7240
|
+
}
|
|
7241
|
+
});
|
|
7242
|
+
stream.on('error', (err) => fail(err));
|
|
7243
|
+
req.on('data', (chunk) => {
|
|
7244
|
+
if (settled) return;
|
|
7245
|
+
hasContent = true;
|
|
7246
|
+
bytesWritten += chunk.length;
|
|
7247
|
+
if (bytesWritten > maxSize) {
|
|
7248
|
+
fail(new Error(`备份文件过大(>${Math.floor(maxSize / 1024 / 1024)}MB)`));
|
|
7249
|
+
try {
|
|
7250
|
+
req.destroy();
|
|
7251
|
+
} catch (_) {}
|
|
7252
|
+
return;
|
|
7253
|
+
}
|
|
7254
|
+
stream.write(chunk);
|
|
7255
|
+
});
|
|
7256
|
+
req.on('end', () => {
|
|
7257
|
+
if (settled) return;
|
|
7258
|
+
stream.end(() => done());
|
|
7259
|
+
});
|
|
7260
|
+
});
|
|
7261
|
+
}
|
|
7262
|
+
|
|
5388
7263
|
function writeUploadZip(base64, prefix, originalName = '') {
|
|
5389
7264
|
let buffer;
|
|
5390
7265
|
try {
|
|
@@ -6080,7 +7955,7 @@ async function cmdExportSession(args = []) {
|
|
|
6080
7955
|
}
|
|
6081
7956
|
|
|
6082
7957
|
function parseStartOptions(args = []) {
|
|
6083
|
-
const options = { host: '' };
|
|
7958
|
+
const options = { host: '', noBrowser: false };
|
|
6084
7959
|
if (!Array.isArray(args)) {
|
|
6085
7960
|
return options;
|
|
6086
7961
|
}
|
|
@@ -6088,6 +7963,10 @@ function parseStartOptions(args = []) {
|
|
|
6088
7963
|
for (let i = 0; i < args.length; i++) {
|
|
6089
7964
|
const arg = args[i];
|
|
6090
7965
|
if (!arg) continue;
|
|
7966
|
+
if (arg === '--no-browser') {
|
|
7967
|
+
options.noBrowser = true;
|
|
7968
|
+
continue;
|
|
7969
|
+
}
|
|
6091
7970
|
if (arg.startsWith('--host=')) {
|
|
6092
7971
|
options.host = arg.slice('--host='.length);
|
|
6093
7972
|
continue;
|
|
@@ -6160,11 +8039,125 @@ function watchPathsForRestart(targets, onChange) {
|
|
|
6160
8039
|
};
|
|
6161
8040
|
}
|
|
6162
8041
|
|
|
8042
|
+
function writeJsonResponse(res, statusCode, payload) {
|
|
8043
|
+
const body = JSON.stringify(payload, null, 2);
|
|
8044
|
+
res.writeHead(statusCode, {
|
|
8045
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
8046
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
8047
|
+
});
|
|
8048
|
+
res.end(body, 'utf-8');
|
|
8049
|
+
}
|
|
8050
|
+
|
|
8051
|
+
function streamZipDownloadResponse(res, filePath, options = {}) {
|
|
8052
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
8053
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8054
|
+
res.end('File Not Found');
|
|
8055
|
+
return;
|
|
8056
|
+
}
|
|
8057
|
+
const stat = fs.statSync(filePath);
|
|
8058
|
+
if (!stat.isFile()) {
|
|
8059
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8060
|
+
res.end('Not a File');
|
|
8061
|
+
return;
|
|
8062
|
+
}
|
|
8063
|
+
const downloadName = typeof options.fileName === 'string' && options.fileName.trim()
|
|
8064
|
+
? options.fileName.trim()
|
|
8065
|
+
: path.basename(filePath);
|
|
8066
|
+
const deleteAfterDownload = !!options.deleteAfterDownload;
|
|
8067
|
+
const onAfterComplete = typeof options.onAfterComplete === 'function'
|
|
8068
|
+
? options.onAfterComplete
|
|
8069
|
+
: null;
|
|
8070
|
+
res.writeHead(200, {
|
|
8071
|
+
'Content-Type': 'application/zip',
|
|
8072
|
+
'Content-Disposition': `attachment; filename="${path.basename(downloadName)}"`,
|
|
8073
|
+
'Content-Length': stat.size
|
|
8074
|
+
});
|
|
8075
|
+
|
|
8076
|
+
const stream = fs.createReadStream(filePath);
|
|
8077
|
+
let finished = false;
|
|
8078
|
+
const finalize = () => {
|
|
8079
|
+
if (finished) return;
|
|
8080
|
+
finished = true;
|
|
8081
|
+
if (deleteAfterDownload && fs.existsSync(filePath)) {
|
|
8082
|
+
try {
|
|
8083
|
+
fs.unlinkSync(filePath);
|
|
8084
|
+
} catch (_) {}
|
|
8085
|
+
}
|
|
8086
|
+
if (onAfterComplete) {
|
|
8087
|
+
try {
|
|
8088
|
+
onAfterComplete();
|
|
8089
|
+
} catch (_) {}
|
|
8090
|
+
}
|
|
8091
|
+
};
|
|
8092
|
+
stream.on('error', () => {
|
|
8093
|
+
if (!res.headersSent) {
|
|
8094
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8095
|
+
res.end('Download Error');
|
|
8096
|
+
} else {
|
|
8097
|
+
try {
|
|
8098
|
+
res.destroy();
|
|
8099
|
+
} catch (_) {}
|
|
8100
|
+
}
|
|
8101
|
+
finalize();
|
|
8102
|
+
});
|
|
8103
|
+
res.on('finish', finalize);
|
|
8104
|
+
res.on('close', finalize);
|
|
8105
|
+
stream.pipe(res);
|
|
8106
|
+
}
|
|
8107
|
+
|
|
8108
|
+
function resolveUploadFileNameFromRequest(req, fallbackName = 'codex-skills.zip') {
|
|
8109
|
+
const rawHeader = req.headers['x-codexmate-file-name'];
|
|
8110
|
+
const source = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
8111
|
+
const fallback = typeof fallbackName === 'string' && fallbackName.trim()
|
|
8112
|
+
? fallbackName.trim()
|
|
8113
|
+
: 'codex-skills.zip';
|
|
8114
|
+
if (!source || typeof source !== 'string') {
|
|
8115
|
+
return fallback;
|
|
8116
|
+
}
|
|
8117
|
+
const decoded = (() => {
|
|
8118
|
+
try {
|
|
8119
|
+
return decodeURIComponent(source);
|
|
8120
|
+
} catch (_) {
|
|
8121
|
+
return source;
|
|
8122
|
+
}
|
|
8123
|
+
})();
|
|
8124
|
+
const normalized = path.basename(decoded.trim());
|
|
8125
|
+
return normalized || fallback;
|
|
8126
|
+
}
|
|
8127
|
+
|
|
8128
|
+
async function handleImportCodexSkillsZipUpload(req, res) {
|
|
8129
|
+
if (req.method !== 'POST') {
|
|
8130
|
+
writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
|
|
8131
|
+
return;
|
|
8132
|
+
}
|
|
8133
|
+
try {
|
|
8134
|
+
const fileName = resolveUploadFileNameFromRequest(req, 'codex-skills.zip');
|
|
8135
|
+
const upload = await writeUploadZipStream(
|
|
8136
|
+
req,
|
|
8137
|
+
'codex-skills-import',
|
|
8138
|
+
fileName,
|
|
8139
|
+
MAX_SKILLS_ZIP_UPLOAD_SIZE
|
|
8140
|
+
);
|
|
8141
|
+
const result = await importCodexSkillsFromZipFile(upload.zipPath, {
|
|
8142
|
+
tempDir: upload.tempDir,
|
|
8143
|
+
fallbackName: fileName
|
|
8144
|
+
});
|
|
8145
|
+
writeJsonResponse(res, 200, result || {});
|
|
8146
|
+
} catch (e) {
|
|
8147
|
+
const message = e && e.message ? e.message : '上传失败';
|
|
8148
|
+
writeJsonResponse(res, 400, { error: message });
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
|
|
6163
8152
|
function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
|
|
6164
8153
|
const connections = new Set();
|
|
6165
8154
|
|
|
6166
8155
|
const server = http.createServer((req, res) => {
|
|
6167
8156
|
const requestPath = (req.url || '/').split('?')[0];
|
|
8157
|
+
if (requestPath === '/api/import-codex-skills-zip') {
|
|
8158
|
+
void handleImportCodexSkillsZipUpload(req, res);
|
|
8159
|
+
return;
|
|
8160
|
+
}
|
|
6168
8161
|
if (requestPath === '/api') {
|
|
6169
8162
|
let body = '';
|
|
6170
8163
|
req.on('data', chunk => body += chunk);
|
|
@@ -6185,6 +8178,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6185
8178
|
serviceTier,
|
|
6186
8179
|
modelReasoningEffort,
|
|
6187
8180
|
configReady: !statusConfigResult.isVirtual,
|
|
8181
|
+
configErrorType: statusConfigResult.errorType || '',
|
|
6188
8182
|
configNotice: statusConfigResult.reason || '',
|
|
6189
8183
|
initNotice: consumeInitNotice()
|
|
6190
8184
|
};
|
|
@@ -6199,6 +8193,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6199
8193
|
const current = listConfig.model_provider;
|
|
6200
8194
|
result = {
|
|
6201
8195
|
configReady: !listConfigResult.isVirtual,
|
|
8196
|
+
configErrorType: listConfigResult.errorType || '',
|
|
8197
|
+
configNotice: listConfigResult.reason || '',
|
|
6202
8198
|
providers: Object.entries(providers).map(([name, p]) => ({
|
|
6203
8199
|
name,
|
|
6204
8200
|
url: p.base_url || '',
|
|
@@ -6273,6 +8269,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6273
8269
|
case 'apply-agents-file':
|
|
6274
8270
|
result = applyAgentsFile(params || {});
|
|
6275
8271
|
break;
|
|
8272
|
+
case 'list-codex-skills':
|
|
8273
|
+
result = listCodexSkills();
|
|
8274
|
+
break;
|
|
8275
|
+
case 'delete-codex-skills':
|
|
8276
|
+
result = deleteCodexSkills(params || {});
|
|
8277
|
+
break;
|
|
8278
|
+
case 'scan-unmanaged-codex-skills':
|
|
8279
|
+
result = scanUnmanagedCodexSkills();
|
|
8280
|
+
break;
|
|
8281
|
+
case 'import-codex-skills':
|
|
8282
|
+
result = importCodexSkills(params || {});
|
|
8283
|
+
break;
|
|
8284
|
+
case 'export-codex-skills':
|
|
8285
|
+
result = await exportCodexSkills(params || {});
|
|
8286
|
+
break;
|
|
6276
8287
|
case 'get-openclaw-config':
|
|
6277
8288
|
result = readOpenclawConfigFile();
|
|
6278
8289
|
break;
|
|
@@ -6433,6 +8444,58 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6433
8444
|
case 'proxy-apply-provider':
|
|
6434
8445
|
result = applyBuiltinProxyProvider(params || {});
|
|
6435
8446
|
break;
|
|
8447
|
+
case 'workflow-list':
|
|
8448
|
+
result = listWorkflowDefinitions();
|
|
8449
|
+
break;
|
|
8450
|
+
case 'workflow-get':
|
|
8451
|
+
{
|
|
8452
|
+
const id = params && typeof params.id === 'string' ? params.id.trim() : '';
|
|
8453
|
+
if (!id) {
|
|
8454
|
+
result = { error: 'workflow id is required' };
|
|
8455
|
+
} else {
|
|
8456
|
+
result = getWorkflowDefinitionById(id);
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
break;
|
|
8460
|
+
case 'workflow-validate':
|
|
8461
|
+
{
|
|
8462
|
+
const id = params && typeof params.id === 'string' ? params.id.trim() : '';
|
|
8463
|
+
if (!id) {
|
|
8464
|
+
result = { ok: false, error: 'workflow id is required' };
|
|
8465
|
+
break;
|
|
8466
|
+
}
|
|
8467
|
+
const input = params && params.input && typeof params.input === 'object' && !Array.isArray(params.input)
|
|
8468
|
+
? params.input
|
|
8469
|
+
: {};
|
|
8470
|
+
result = validateWorkflowById(id, input);
|
|
8471
|
+
}
|
|
8472
|
+
break;
|
|
8473
|
+
case 'workflow-run':
|
|
8474
|
+
{
|
|
8475
|
+
const id = params && typeof params.id === 'string' ? params.id.trim() : '';
|
|
8476
|
+
if (!id) {
|
|
8477
|
+
result = { error: 'workflow id is required' };
|
|
8478
|
+
break;
|
|
8479
|
+
}
|
|
8480
|
+
const input = params && params.input && typeof params.input === 'object' && !Array.isArray(params.input)
|
|
8481
|
+
? params.input
|
|
8482
|
+
: {};
|
|
8483
|
+
result = await runWorkflowById(id, input, {
|
|
8484
|
+
allowWrite: !!(params && params.allowWrite),
|
|
8485
|
+
dryRun: !!(params && params.dryRun)
|
|
8486
|
+
});
|
|
8487
|
+
}
|
|
8488
|
+
break;
|
|
8489
|
+
case 'workflow-runs':
|
|
8490
|
+
{
|
|
8491
|
+
const rawLimit = params && Number.isFinite(params.limit) ? params.limit : parseInt(params && params.limit, 10);
|
|
8492
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : 20;
|
|
8493
|
+
result = {
|
|
8494
|
+
runs: listWorkflowRunRecords(limit),
|
|
8495
|
+
limit
|
|
8496
|
+
};
|
|
8497
|
+
}
|
|
8498
|
+
break;
|
|
6436
8499
|
default:
|
|
6437
8500
|
result = { error: '未知操作' };
|
|
6438
8501
|
}
|
|
@@ -6479,32 +8542,35 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6479
8542
|
fs.createReadStream(filePath).pipe(res);
|
|
6480
8543
|
} else if (requestPath.startsWith('/download/')) {
|
|
6481
8544
|
const fileName = requestPath.slice('/download/'.length);
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
res.
|
|
6488
|
-
res.end('Forbidden');
|
|
8545
|
+
let decodedFileName = '';
|
|
8546
|
+
try {
|
|
8547
|
+
decodedFileName = decodeURIComponent(fileName);
|
|
8548
|
+
} catch (_) {
|
|
8549
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8550
|
+
res.end('Bad Request');
|
|
6489
8551
|
return;
|
|
6490
8552
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
8553
|
+
|
|
8554
|
+
const artifact = resolveDownloadArtifact(decodedFileName, { consume: true });
|
|
8555
|
+
if (artifact) {
|
|
8556
|
+
streamZipDownloadResponse(res, artifact.filePath, {
|
|
8557
|
+
fileName: artifact.fileName,
|
|
8558
|
+
deleteAfterDownload: artifact.deleteAfterDownload !== false
|
|
8559
|
+
});
|
|
6494
8560
|
return;
|
|
6495
8561
|
}
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
8562
|
+
|
|
8563
|
+
const tempDir = os.tmpdir();
|
|
8564
|
+
const legacyFilePath = path.join(tempDir, decodedFileName);
|
|
8565
|
+
if (!isPathInside(legacyFilePath, tempDir)) {
|
|
8566
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
8567
|
+
res.end('Forbidden');
|
|
6500
8568
|
return;
|
|
6501
8569
|
}
|
|
6502
|
-
res
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
'Content-Length': stat.size
|
|
8570
|
+
streamZipDownloadResponse(res, legacyFilePath, {
|
|
8571
|
+
fileName: path.basename(legacyFilePath),
|
|
8572
|
+
deleteAfterDownload: false
|
|
6506
8573
|
});
|
|
6507
|
-
fs.createReadStream(filePath).pipe(res);
|
|
6508
8574
|
} else if (requestPath.startsWith('/res/')) {
|
|
6509
8575
|
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
6510
8576
|
const filePath = path.join(__dirname, normalized);
|
|
@@ -6627,7 +8693,7 @@ function cmdStart(options = {}) {
|
|
|
6627
8693
|
webDir,
|
|
6628
8694
|
host,
|
|
6629
8695
|
port,
|
|
6630
|
-
openBrowser:
|
|
8696
|
+
openBrowser: !options.noBrowser
|
|
6631
8697
|
});
|
|
6632
8698
|
|
|
6633
8699
|
const proxySettings = readBuiltinProxySettings();
|
|
@@ -6948,7 +9014,223 @@ async function cmdProxy(args = []) {
|
|
|
6948
9014
|
return;
|
|
6949
9015
|
}
|
|
6950
9016
|
|
|
6951
|
-
throw new Error(`未知 proxy 子命令: ${subcommand}`);
|
|
9017
|
+
throw new Error(`未知 proxy 子命令: ${subcommand}`);
|
|
9018
|
+
}
|
|
9019
|
+
|
|
9020
|
+
function parseWorkflowInputArg(rawInput) {
|
|
9021
|
+
const raw = typeof rawInput === 'string' ? rawInput.trim() : '';
|
|
9022
|
+
if (!raw) {
|
|
9023
|
+
return {};
|
|
9024
|
+
}
|
|
9025
|
+
let content = raw;
|
|
9026
|
+
if (raw.startsWith('@')) {
|
|
9027
|
+
const filePath = path.resolve(raw.slice(1));
|
|
9028
|
+
if (!fs.existsSync(filePath)) {
|
|
9029
|
+
throw new Error(`工作流输入文件不存在: ${filePath}`);
|
|
9030
|
+
}
|
|
9031
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
9032
|
+
}
|
|
9033
|
+
let parsed;
|
|
9034
|
+
try {
|
|
9035
|
+
parsed = JSON.parse(content);
|
|
9036
|
+
} catch (e) {
|
|
9037
|
+
throw new Error(`工作流输入 JSON 解析失败: ${e.message}`);
|
|
9038
|
+
}
|
|
9039
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
9040
|
+
throw new Error('工作流输入必须是 JSON 对象');
|
|
9041
|
+
}
|
|
9042
|
+
return parsed;
|
|
9043
|
+
}
|
|
9044
|
+
|
|
9045
|
+
function printWorkflowHelp() {
|
|
9046
|
+
console.log('\n用法: codexmate workflow <list|get|validate|run|runs> [参数]');
|
|
9047
|
+
console.log(' codexmate workflow list');
|
|
9048
|
+
console.log(' codexmate workflow get diagnose-config');
|
|
9049
|
+
console.log(' codexmate workflow validate safe-provider-switch --input \'{"provider":"e2e"}\'');
|
|
9050
|
+
console.log(' codexmate workflow run diagnose-config --input \'{}\'');
|
|
9051
|
+
console.log(' codexmate workflow run safe-provider-switch --input \'{"provider":"e2e","apply":true}\' --allow-write');
|
|
9052
|
+
console.log(' codexmate workflow runs --limit 20');
|
|
9053
|
+
console.log('参数:');
|
|
9054
|
+
console.log(' --input <JSON|@file> 传入工作流输入');
|
|
9055
|
+
console.log(' --allow-write 允许执行写入步骤');
|
|
9056
|
+
console.log(' --dry-run 跳过写入步骤,仅预演');
|
|
9057
|
+
console.log(' --limit <N> 读取最近执行记录数量(runs)');
|
|
9058
|
+
console.log(' --json 以 JSON 输出');
|
|
9059
|
+
console.log();
|
|
9060
|
+
}
|
|
9061
|
+
|
|
9062
|
+
function parseWorkflowCliOptions(args = []) {
|
|
9063
|
+
const options = {
|
|
9064
|
+
inputRaw: '',
|
|
9065
|
+
allowWrite: false,
|
|
9066
|
+
dryRun: false,
|
|
9067
|
+
limit: 20,
|
|
9068
|
+
json: false
|
|
9069
|
+
};
|
|
9070
|
+
const rest = [];
|
|
9071
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
9072
|
+
const arg = args[i];
|
|
9073
|
+
if (arg === '--allow-write') {
|
|
9074
|
+
options.allowWrite = true;
|
|
9075
|
+
continue;
|
|
9076
|
+
}
|
|
9077
|
+
if (arg === '--dry-run') {
|
|
9078
|
+
options.dryRun = true;
|
|
9079
|
+
continue;
|
|
9080
|
+
}
|
|
9081
|
+
if (arg === '--json') {
|
|
9082
|
+
options.json = true;
|
|
9083
|
+
continue;
|
|
9084
|
+
}
|
|
9085
|
+
if (arg === '--input') {
|
|
9086
|
+
options.inputRaw = args[i + 1] || '';
|
|
9087
|
+
i += 1;
|
|
9088
|
+
continue;
|
|
9089
|
+
}
|
|
9090
|
+
if (arg.startsWith('--input=')) {
|
|
9091
|
+
options.inputRaw = arg.slice('--input='.length);
|
|
9092
|
+
continue;
|
|
9093
|
+
}
|
|
9094
|
+
if (arg === '--limit') {
|
|
9095
|
+
const raw = args[i + 1];
|
|
9096
|
+
i += 1;
|
|
9097
|
+
const value = parseInt(raw, 10);
|
|
9098
|
+
if (Number.isFinite(value)) {
|
|
9099
|
+
options.limit = value;
|
|
9100
|
+
}
|
|
9101
|
+
continue;
|
|
9102
|
+
}
|
|
9103
|
+
if (arg.startsWith('--limit=')) {
|
|
9104
|
+
const value = parseInt(arg.slice('--limit='.length), 10);
|
|
9105
|
+
if (Number.isFinite(value)) {
|
|
9106
|
+
options.limit = value;
|
|
9107
|
+
}
|
|
9108
|
+
continue;
|
|
9109
|
+
}
|
|
9110
|
+
rest.push(arg);
|
|
9111
|
+
}
|
|
9112
|
+
return { options, rest };
|
|
9113
|
+
}
|
|
9114
|
+
|
|
9115
|
+
async function cmdWorkflow(args = []) {
|
|
9116
|
+
const argv = Array.isArray(args) ? args : [];
|
|
9117
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
9118
|
+
printWorkflowHelp();
|
|
9119
|
+
return;
|
|
9120
|
+
}
|
|
9121
|
+
const subcommand = String(argv[0] || '').trim().toLowerCase();
|
|
9122
|
+
const parsed = parseWorkflowCliOptions(argv.slice(1));
|
|
9123
|
+
const options = parsed.options;
|
|
9124
|
+
const rest = parsed.rest;
|
|
9125
|
+
|
|
9126
|
+
if (subcommand === 'list') {
|
|
9127
|
+
const result = listWorkflowDefinitions();
|
|
9128
|
+
if (options.json) {
|
|
9129
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9130
|
+
return;
|
|
9131
|
+
}
|
|
9132
|
+
const workflows = Array.isArray(result.workflows) ? result.workflows : [];
|
|
9133
|
+
console.log('\n可用工作流:');
|
|
9134
|
+
for (const item of workflows) {
|
|
9135
|
+
const mode = item.readOnly ? 'read-only' : 'read-write';
|
|
9136
|
+
console.log(` - ${item.id} (${mode}, steps=${item.stepCount})`);
|
|
9137
|
+
if (item.description) {
|
|
9138
|
+
console.log(` ${item.description}`);
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
9142
|
+
console.log('\n警告:');
|
|
9143
|
+
result.warnings.forEach((msg) => console.log(` - ${msg}`));
|
|
9144
|
+
}
|
|
9145
|
+
console.log();
|
|
9146
|
+
return;
|
|
9147
|
+
}
|
|
9148
|
+
|
|
9149
|
+
if (subcommand === 'runs') {
|
|
9150
|
+
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 20;
|
|
9151
|
+
const runs = listWorkflowRunRecords(limit);
|
|
9152
|
+
if (options.json) {
|
|
9153
|
+
console.log(JSON.stringify({ runs, limit }, null, 2));
|
|
9154
|
+
return;
|
|
9155
|
+
}
|
|
9156
|
+
console.log(`\n最近执行记录(${runs.length}/${limit}):`);
|
|
9157
|
+
for (const item of runs) {
|
|
9158
|
+
const status = item && item.success ? 'OK' : 'FAIL';
|
|
9159
|
+
console.log(` - [${status}] ${item.workflowId || '(unknown)'} runId=${item.runId || ''} duration=${item.durationMs || 0}ms`);
|
|
9160
|
+
if (item && item.error) {
|
|
9161
|
+
console.log(` error: ${item.error}`);
|
|
9162
|
+
}
|
|
9163
|
+
}
|
|
9164
|
+
console.log();
|
|
9165
|
+
return;
|
|
9166
|
+
}
|
|
9167
|
+
|
|
9168
|
+
const workflowId = typeof rest[0] === 'string' ? rest[0].trim() : '';
|
|
9169
|
+
if (!workflowId) {
|
|
9170
|
+
throw new Error('workflow id is required');
|
|
9171
|
+
}
|
|
9172
|
+
const input = parseWorkflowInputArg(options.inputRaw);
|
|
9173
|
+
|
|
9174
|
+
if (subcommand === 'get') {
|
|
9175
|
+
const result = getWorkflowDefinitionById(workflowId);
|
|
9176
|
+
if (result.error) {
|
|
9177
|
+
throw new Error(result.error);
|
|
9178
|
+
}
|
|
9179
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9180
|
+
return;
|
|
9181
|
+
}
|
|
9182
|
+
|
|
9183
|
+
if (subcommand === 'validate') {
|
|
9184
|
+
const result = validateWorkflowById(workflowId, input);
|
|
9185
|
+
if (!result.ok) {
|
|
9186
|
+
throw new Error(result.error || 'workflow validate failed');
|
|
9187
|
+
}
|
|
9188
|
+
if (options.json) {
|
|
9189
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9190
|
+
} else {
|
|
9191
|
+
console.log(`✓ 工作流校验通过: ${workflowId}`);
|
|
9192
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
9193
|
+
result.warnings.forEach((msg) => console.log(` - ${msg}`));
|
|
9194
|
+
}
|
|
9195
|
+
console.log();
|
|
9196
|
+
}
|
|
9197
|
+
return;
|
|
9198
|
+
}
|
|
9199
|
+
|
|
9200
|
+
if (subcommand === 'run') {
|
|
9201
|
+
const result = await runWorkflowById(workflowId, input, {
|
|
9202
|
+
allowWrite: options.allowWrite,
|
|
9203
|
+
dryRun: options.dryRun
|
|
9204
|
+
});
|
|
9205
|
+
if (options.json) {
|
|
9206
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9207
|
+
} else {
|
|
9208
|
+
if (result.error) {
|
|
9209
|
+
console.error(`✗ 工作流执行失败: ${result.error}`);
|
|
9210
|
+
} else {
|
|
9211
|
+
console.log(`✓ 工作流执行完成: ${workflowId} (${result.durationMs || 0}ms)`);
|
|
9212
|
+
}
|
|
9213
|
+
const steps = Array.isArray(result.steps) ? result.steps : [];
|
|
9214
|
+
for (const step of steps) {
|
|
9215
|
+
const status = step.status || 'unknown';
|
|
9216
|
+
const label = step.id || step.tool || '(step)';
|
|
9217
|
+
console.log(` - ${label}: ${status} (${step.durationMs || 0}ms)`);
|
|
9218
|
+
if (step.error) {
|
|
9219
|
+
console.log(` error: ${step.error}`);
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
if (result.runId) {
|
|
9223
|
+
console.log(` runId: ${result.runId}`);
|
|
9224
|
+
}
|
|
9225
|
+
console.log();
|
|
9226
|
+
}
|
|
9227
|
+
if (result.error) {
|
|
9228
|
+
throw new Error(result.error);
|
|
9229
|
+
}
|
|
9230
|
+
return;
|
|
9231
|
+
}
|
|
9232
|
+
|
|
9233
|
+
throw new Error(`未知 workflow 子命令: ${subcommand}`);
|
|
6952
9234
|
}
|
|
6953
9235
|
|
|
6954
9236
|
async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
|
|
@@ -7023,10 +9305,6 @@ async function cmdQwen(args = []) {
|
|
|
7023
9305
|
return runProxyCommand('Qwen', ['qwen', 'qwen-code'], args, 'npm install -g @qwen-code/qwen-code');
|
|
7024
9306
|
}
|
|
7025
9307
|
|
|
7026
|
-
async function cmdGemini(args = []) {
|
|
7027
|
-
return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
|
|
7028
|
-
}
|
|
7029
|
-
|
|
7030
9308
|
function parseMcpOptions(args = []) {
|
|
7031
9309
|
const options = {
|
|
7032
9310
|
subcommand: 'serve',
|
|
@@ -7103,6 +9381,7 @@ function buildMcpStatusPayload() {
|
|
|
7103
9381
|
serviceTier,
|
|
7104
9382
|
modelReasoningEffort,
|
|
7105
9383
|
configReady: !statusConfigResult.isVirtual,
|
|
9384
|
+
configErrorType: statusConfigResult.errorType || '',
|
|
7106
9385
|
configNotice: statusConfigResult.reason || '',
|
|
7107
9386
|
initNotice: consumeInitNotice()
|
|
7108
9387
|
};
|
|
@@ -7115,6 +9394,8 @@ function buildMcpProviderListPayload() {
|
|
|
7115
9394
|
const current = listConfig.model_provider;
|
|
7116
9395
|
return {
|
|
7117
9396
|
configReady: !listConfigResult.isVirtual,
|
|
9397
|
+
configErrorType: listConfigResult.errorType || '',
|
|
9398
|
+
configNotice: listConfigResult.reason || '',
|
|
7118
9399
|
providers: Object.entries(providers).map(([name, p]) => ({
|
|
7119
9400
|
name,
|
|
7120
9401
|
url: p.base_url || '',
|
|
@@ -7167,6 +9448,541 @@ function normalizeMcpSource(value) {
|
|
|
7167
9448
|
return null;
|
|
7168
9449
|
}
|
|
7169
9450
|
|
|
9451
|
+
const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
|
|
9452
|
+
'diagnose-config': {
|
|
9453
|
+
id: 'diagnose-config',
|
|
9454
|
+
name: 'Diagnose Config',
|
|
9455
|
+
description: 'Collect status/providers/proxy snapshots for troubleshooting.',
|
|
9456
|
+
readOnly: true,
|
|
9457
|
+
inputSchema: {
|
|
9458
|
+
type: 'object',
|
|
9459
|
+
properties: {},
|
|
9460
|
+
additionalProperties: false
|
|
9461
|
+
},
|
|
9462
|
+
steps: [
|
|
9463
|
+
{ id: 'status', tool: 'codexmate.status.get', arguments: {} },
|
|
9464
|
+
{ id: 'providers', tool: 'codexmate.provider.list', arguments: {} },
|
|
9465
|
+
{ id: 'proxy', tool: 'codexmate.proxy.status', arguments: {} }
|
|
9466
|
+
]
|
|
9467
|
+
},
|
|
9468
|
+
'safe-provider-switch': {
|
|
9469
|
+
id: 'safe-provider-switch',
|
|
9470
|
+
name: 'Safe Provider Switch',
|
|
9471
|
+
description: 'Build template for a provider switch and optionally apply it.',
|
|
9472
|
+
readOnly: false,
|
|
9473
|
+
inputSchema: {
|
|
9474
|
+
type: 'object',
|
|
9475
|
+
properties: {
|
|
9476
|
+
provider: { type: 'string' },
|
|
9477
|
+
model: { type: 'string' },
|
|
9478
|
+
serviceTier: { type: 'string' },
|
|
9479
|
+
reasoningEffort: { type: 'string' },
|
|
9480
|
+
apply: { type: 'boolean' }
|
|
9481
|
+
},
|
|
9482
|
+
required: ['provider'],
|
|
9483
|
+
additionalProperties: false
|
|
9484
|
+
},
|
|
9485
|
+
steps: [
|
|
9486
|
+
{ id: 'providers', tool: 'codexmate.provider.list', arguments: {} },
|
|
9487
|
+
{
|
|
9488
|
+
id: 'template',
|
|
9489
|
+
tool: 'codexmate.config.template.get',
|
|
9490
|
+
arguments: {
|
|
9491
|
+
provider: '{{input.provider}}',
|
|
9492
|
+
model: '{{input.model}}',
|
|
9493
|
+
serviceTier: '{{input.serviceTier}}',
|
|
9494
|
+
reasoningEffort: '{{input.reasoningEffort}}'
|
|
9495
|
+
}
|
|
9496
|
+
},
|
|
9497
|
+
{
|
|
9498
|
+
id: 'apply',
|
|
9499
|
+
tool: 'codexmate.config.template.apply',
|
|
9500
|
+
when: { path: 'input.apply', equals: true },
|
|
9501
|
+
arguments: {
|
|
9502
|
+
template: '{{steps.template.output.template}}'
|
|
9503
|
+
}
|
|
9504
|
+
},
|
|
9505
|
+
{
|
|
9506
|
+
id: 'statusAfter',
|
|
9507
|
+
tool: 'codexmate.status.get',
|
|
9508
|
+
when: { path: 'input.apply', equals: true },
|
|
9509
|
+
arguments: {}
|
|
9510
|
+
}
|
|
9511
|
+
]
|
|
9512
|
+
},
|
|
9513
|
+
'session-issue-pack': {
|
|
9514
|
+
id: 'session-issue-pack',
|
|
9515
|
+
name: 'Session Issue Pack',
|
|
9516
|
+
description: 'Collect session detail and markdown export for issue reports.',
|
|
9517
|
+
readOnly: true,
|
|
9518
|
+
inputSchema: {
|
|
9519
|
+
type: 'object',
|
|
9520
|
+
properties: {
|
|
9521
|
+
source: { type: 'string' },
|
|
9522
|
+
sessionId: { type: 'string' },
|
|
9523
|
+
file: { type: 'string' },
|
|
9524
|
+
maxMessages: { type: ['string', 'number'] }
|
|
9525
|
+
},
|
|
9526
|
+
additionalProperties: true
|
|
9527
|
+
},
|
|
9528
|
+
steps: [
|
|
9529
|
+
{
|
|
9530
|
+
id: 'detail',
|
|
9531
|
+
tool: 'codexmate.session.detail',
|
|
9532
|
+
arguments: {
|
|
9533
|
+
source: '{{input.source}}',
|
|
9534
|
+
sessionId: '{{input.sessionId}}',
|
|
9535
|
+
file: '{{input.file}}',
|
|
9536
|
+
maxMessages: '{{input.maxMessages}}'
|
|
9537
|
+
}
|
|
9538
|
+
},
|
|
9539
|
+
{
|
|
9540
|
+
id: 'export',
|
|
9541
|
+
tool: 'codexmate.session.export',
|
|
9542
|
+
arguments: {
|
|
9543
|
+
source: '{{input.source}}',
|
|
9544
|
+
sessionId: '{{input.sessionId}}',
|
|
9545
|
+
file: '{{input.file}}',
|
|
9546
|
+
maxMessages: '{{input.maxMessages}}'
|
|
9547
|
+
}
|
|
9548
|
+
}
|
|
9549
|
+
]
|
|
9550
|
+
}
|
|
9551
|
+
});
|
|
9552
|
+
|
|
9553
|
+
function cloneJson(value, fallback) {
|
|
9554
|
+
try {
|
|
9555
|
+
return JSON.parse(JSON.stringify(value));
|
|
9556
|
+
} catch (_) {
|
|
9557
|
+
return fallback;
|
|
9558
|
+
}
|
|
9559
|
+
}
|
|
9560
|
+
|
|
9561
|
+
function normalizeWorkflowId(value) {
|
|
9562
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
9563
|
+
if (!raw) return '';
|
|
9564
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(raw)) {
|
|
9565
|
+
return '';
|
|
9566
|
+
}
|
|
9567
|
+
return raw.toLowerCase();
|
|
9568
|
+
}
|
|
9569
|
+
|
|
9570
|
+
function normalizeWorkflowDefinition(raw, idHint = '', source = 'custom') {
|
|
9571
|
+
const safe = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
|
|
9572
|
+
if (!safe) {
|
|
9573
|
+
return { ok: false, error: 'workflow must be an object' };
|
|
9574
|
+
}
|
|
9575
|
+
const id = normalizeWorkflowId(safe.id || idHint);
|
|
9576
|
+
if (!id) {
|
|
9577
|
+
return { ok: false, error: 'workflow id is invalid' };
|
|
9578
|
+
}
|
|
9579
|
+
const name = typeof safe.name === 'string' && safe.name.trim()
|
|
9580
|
+
? safe.name.trim()
|
|
9581
|
+
: id;
|
|
9582
|
+
const description = typeof safe.description === 'string' ? safe.description.trim() : '';
|
|
9583
|
+
const inputSchema = safe.inputSchema && typeof safe.inputSchema === 'object'
|
|
9584
|
+
? cloneJson(safe.inputSchema, { type: 'object', properties: {}, additionalProperties: true })
|
|
9585
|
+
: { type: 'object', properties: {}, additionalProperties: true };
|
|
9586
|
+
const stepsRaw = Array.isArray(safe.steps) ? safe.steps : [];
|
|
9587
|
+
if (stepsRaw.length === 0) {
|
|
9588
|
+
return { ok: false, error: 'workflow steps cannot be empty' };
|
|
9589
|
+
}
|
|
9590
|
+
|
|
9591
|
+
const steps = [];
|
|
9592
|
+
for (let i = 0; i < stepsRaw.length; i += 1) {
|
|
9593
|
+
const item = stepsRaw[i];
|
|
9594
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
9595
|
+
return { ok: false, error: `workflow step #${i + 1} must be an object` };
|
|
9596
|
+
}
|
|
9597
|
+
const stepIdRaw = typeof item.id === 'string' && item.id.trim()
|
|
9598
|
+
? item.id.trim()
|
|
9599
|
+
: `step${i + 1}`;
|
|
9600
|
+
const stepId = normalizeWorkflowId(stepIdRaw);
|
|
9601
|
+
if (!stepId) {
|
|
9602
|
+
return { ok: false, error: `workflow step id invalid at #${i + 1}` };
|
|
9603
|
+
}
|
|
9604
|
+
const toolName = typeof item.tool === 'string' ? item.tool.trim() : '';
|
|
9605
|
+
if (!toolName) {
|
|
9606
|
+
return { ok: false, error: `workflow step "${stepId}" missing tool` };
|
|
9607
|
+
}
|
|
9608
|
+
const args = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
9609
|
+
? cloneJson(item.arguments, {})
|
|
9610
|
+
: {};
|
|
9611
|
+
const when = item.when && typeof item.when === 'object' && !Array.isArray(item.when)
|
|
9612
|
+
? cloneJson(item.when, {})
|
|
9613
|
+
: null;
|
|
9614
|
+
steps.push({
|
|
9615
|
+
id: stepId,
|
|
9616
|
+
name: typeof item.name === 'string' ? item.name.trim() : '',
|
|
9617
|
+
tool: toolName,
|
|
9618
|
+
arguments: args,
|
|
9619
|
+
when,
|
|
9620
|
+
continueOnError: item.continueOnError === true,
|
|
9621
|
+
write: item.write === true
|
|
9622
|
+
});
|
|
9623
|
+
}
|
|
9624
|
+
|
|
9625
|
+
return {
|
|
9626
|
+
ok: true,
|
|
9627
|
+
data: {
|
|
9628
|
+
id,
|
|
9629
|
+
name,
|
|
9630
|
+
description,
|
|
9631
|
+
source,
|
|
9632
|
+
readOnly: safe.readOnly !== false,
|
|
9633
|
+
inputSchema,
|
|
9634
|
+
steps
|
|
9635
|
+
}
|
|
9636
|
+
};
|
|
9637
|
+
}
|
|
9638
|
+
|
|
9639
|
+
function loadBuiltinWorkflowDefinitions() {
|
|
9640
|
+
const items = [];
|
|
9641
|
+
for (const [id, raw] of Object.entries(BUILTIN_WORKFLOW_DEFINITIONS)) {
|
|
9642
|
+
const normalized = normalizeWorkflowDefinition(raw, id, 'builtin');
|
|
9643
|
+
if (!normalized.ok) {
|
|
9644
|
+
continue;
|
|
9645
|
+
}
|
|
9646
|
+
items.push(normalized.data);
|
|
9647
|
+
}
|
|
9648
|
+
return items;
|
|
9649
|
+
}
|
|
9650
|
+
|
|
9651
|
+
function loadCustomWorkflowDefinitions() {
|
|
9652
|
+
const parsed = readJsonObjectFromFile(WORKFLOW_DEFINITIONS_FILE, {});
|
|
9653
|
+
if (!parsed.ok || !parsed.exists) {
|
|
9654
|
+
return {
|
|
9655
|
+
items: [],
|
|
9656
|
+
warnings: parsed.ok ? [] : [parsed.error || 'workflow file parse failed']
|
|
9657
|
+
};
|
|
9658
|
+
}
|
|
9659
|
+
const data = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
|
|
9660
|
+
let list = [];
|
|
9661
|
+
if (Array.isArray(data.workflows)) {
|
|
9662
|
+
list = data.workflows;
|
|
9663
|
+
} else if (data.workflows && typeof data.workflows === 'object') {
|
|
9664
|
+
list = Object.entries(data.workflows).map(([id, item]) => ({ ...(item || {}), id }));
|
|
9665
|
+
} else {
|
|
9666
|
+
list = Object.entries(data).map(([id, item]) => ({ ...(item || {}), id }));
|
|
9667
|
+
}
|
|
9668
|
+
|
|
9669
|
+
const items = [];
|
|
9670
|
+
const warnings = [];
|
|
9671
|
+
for (const item of list) {
|
|
9672
|
+
const normalized = normalizeWorkflowDefinition(item, item && item.id ? item.id : '', 'custom');
|
|
9673
|
+
if (!normalized.ok) {
|
|
9674
|
+
warnings.push(normalized.error || 'invalid custom workflow');
|
|
9675
|
+
continue;
|
|
9676
|
+
}
|
|
9677
|
+
items.push(normalized.data);
|
|
9678
|
+
}
|
|
9679
|
+
return { items, warnings };
|
|
9680
|
+
}
|
|
9681
|
+
|
|
9682
|
+
function buildWorkflowRegistry() {
|
|
9683
|
+
const registry = new Map();
|
|
9684
|
+
const warnings = [];
|
|
9685
|
+
const builtin = loadBuiltinWorkflowDefinitions();
|
|
9686
|
+
for (const item of builtin) {
|
|
9687
|
+
registry.set(item.id, item);
|
|
9688
|
+
}
|
|
9689
|
+
const custom = loadCustomWorkflowDefinitions();
|
|
9690
|
+
for (const item of custom.items) {
|
|
9691
|
+
if (registry.has(item.id)) {
|
|
9692
|
+
warnings.push(`custom workflow id duplicated with builtin and ignored: ${item.id}`);
|
|
9693
|
+
continue;
|
|
9694
|
+
}
|
|
9695
|
+
registry.set(item.id, item);
|
|
9696
|
+
}
|
|
9697
|
+
warnings.push(...custom.warnings);
|
|
9698
|
+
return { registry, warnings };
|
|
9699
|
+
}
|
|
9700
|
+
|
|
9701
|
+
function listWorkflowDefinitions() {
|
|
9702
|
+
const { registry, warnings } = buildWorkflowRegistry();
|
|
9703
|
+
const workflows = Array.from(registry.values())
|
|
9704
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
9705
|
+
.map((item) => ({
|
|
9706
|
+
id: item.id,
|
|
9707
|
+
name: item.name,
|
|
9708
|
+
description: item.description,
|
|
9709
|
+
source: item.source,
|
|
9710
|
+
readOnly: item.readOnly !== false,
|
|
9711
|
+
stepCount: Array.isArray(item.steps) ? item.steps.length : 0
|
|
9712
|
+
}));
|
|
9713
|
+
return {
|
|
9714
|
+
workflows,
|
|
9715
|
+
warnings
|
|
9716
|
+
};
|
|
9717
|
+
}
|
|
9718
|
+
|
|
9719
|
+
function getWorkflowDefinitionById(rawId) {
|
|
9720
|
+
const id = normalizeWorkflowId(rawId);
|
|
9721
|
+
if (!id) {
|
|
9722
|
+
return { error: 'workflow id is required' };
|
|
9723
|
+
}
|
|
9724
|
+
const { registry, warnings } = buildWorkflowRegistry();
|
|
9725
|
+
const workflow = registry.get(id);
|
|
9726
|
+
if (!workflow) {
|
|
9727
|
+
return { error: `workflow not found: ${id}` };
|
|
9728
|
+
}
|
|
9729
|
+
return {
|
|
9730
|
+
workflow: cloneJson(workflow, {}),
|
|
9731
|
+
warnings
|
|
9732
|
+
};
|
|
9733
|
+
}
|
|
9734
|
+
|
|
9735
|
+
function createWorkflowToolCatalog() {
|
|
9736
|
+
return {
|
|
9737
|
+
'codexmate.status.get': {
|
|
9738
|
+
readOnly: true,
|
|
9739
|
+
handler: async () => buildMcpStatusPayload()
|
|
9740
|
+
},
|
|
9741
|
+
'codexmate.provider.list': {
|
|
9742
|
+
readOnly: true,
|
|
9743
|
+
handler: async () => buildMcpProviderListPayload()
|
|
9744
|
+
},
|
|
9745
|
+
'codexmate.proxy.status': {
|
|
9746
|
+
readOnly: true,
|
|
9747
|
+
handler: async () => getBuiltinProxyStatus()
|
|
9748
|
+
},
|
|
9749
|
+
'codexmate.session.list': {
|
|
9750
|
+
readOnly: true,
|
|
9751
|
+
handler: async (args = {}) => {
|
|
9752
|
+
const source = normalizeMcpSource(args.source);
|
|
9753
|
+
if (source === null) {
|
|
9754
|
+
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
9755
|
+
}
|
|
9756
|
+
return {
|
|
9757
|
+
source: source || 'all',
|
|
9758
|
+
sessions: listAllSessions({
|
|
9759
|
+
...args,
|
|
9760
|
+
source: source || 'all'
|
|
9761
|
+
})
|
|
9762
|
+
};
|
|
9763
|
+
}
|
|
9764
|
+
},
|
|
9765
|
+
'codexmate.session.detail': {
|
|
9766
|
+
readOnly: true,
|
|
9767
|
+
handler: async (args = {}) => readSessionDetail(args || {})
|
|
9768
|
+
},
|
|
9769
|
+
'codexmate.session.export': {
|
|
9770
|
+
readOnly: true,
|
|
9771
|
+
handler: async (args = {}) => exportSessionData(args || {})
|
|
9772
|
+
},
|
|
9773
|
+
'codexmate.config.template.get': {
|
|
9774
|
+
readOnly: true,
|
|
9775
|
+
handler: async (args = {}) => getConfigTemplate(args || {})
|
|
9776
|
+
},
|
|
9777
|
+
'codexmate.config.template.apply': {
|
|
9778
|
+
readOnly: false,
|
|
9779
|
+
handler: async (args = {}) => applyConfigTemplate(args || {})
|
|
9780
|
+
}
|
|
9781
|
+
};
|
|
9782
|
+
}
|
|
9783
|
+
|
|
9784
|
+
function getWorkflowKnownToolsSet() {
|
|
9785
|
+
return new Set(Object.keys(createWorkflowToolCatalog()));
|
|
9786
|
+
}
|
|
9787
|
+
|
|
9788
|
+
function resolveWorkflowDefinitionWithToolMeta(workflow) {
|
|
9789
|
+
const catalog = createWorkflowToolCatalog();
|
|
9790
|
+
const safe = cloneJson(workflow, {});
|
|
9791
|
+
safe.steps = (Array.isArray(safe.steps) ? safe.steps : []).map((step) => {
|
|
9792
|
+
const tool = catalog[step.tool];
|
|
9793
|
+
return {
|
|
9794
|
+
...step,
|
|
9795
|
+
write: step.write === true || !!(tool && tool.readOnly === false)
|
|
9796
|
+
};
|
|
9797
|
+
});
|
|
9798
|
+
return safe;
|
|
9799
|
+
}
|
|
9800
|
+
|
|
9801
|
+
function validateWorkflowInputBySchema(inputSchema, input) {
|
|
9802
|
+
const schema = inputSchema && typeof inputSchema === 'object' ? inputSchema : {};
|
|
9803
|
+
if (schema.type && schema.type !== 'object') {
|
|
9804
|
+
return { ok: false, error: `unsupported input schema type: ${schema.type}` };
|
|
9805
|
+
}
|
|
9806
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
9807
|
+
return { ok: false, error: 'workflow input must be an object' };
|
|
9808
|
+
}
|
|
9809
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
9810
|
+
for (const key of required) {
|
|
9811
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
9812
|
+
return { ok: false, error: `missing required input field: ${key}` };
|
|
9813
|
+
}
|
|
9814
|
+
}
|
|
9815
|
+
const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
|
|
9816
|
+
for (const [key, expected] of Object.entries(properties)) {
|
|
9817
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
|
|
9818
|
+
const value = input[key];
|
|
9819
|
+
if (!expected || typeof expected !== 'object') continue;
|
|
9820
|
+
const type = expected.type;
|
|
9821
|
+
if (!type) continue;
|
|
9822
|
+
const typeList = Array.isArray(type) ? type : [type];
|
|
9823
|
+
const actualType = value === null ? 'null' : (Array.isArray(value) ? 'array' : typeof value);
|
|
9824
|
+
const matched = typeList.some((candidate) => {
|
|
9825
|
+
if (candidate === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
9826
|
+
if (candidate === 'integer') return Number.isInteger(value);
|
|
9827
|
+
if (candidate === 'array') return Array.isArray(value);
|
|
9828
|
+
if (candidate === 'object') return value && typeof value === 'object' && !Array.isArray(value);
|
|
9829
|
+
if (candidate === 'null') return value === null;
|
|
9830
|
+
return actualType === candidate;
|
|
9831
|
+
});
|
|
9832
|
+
if (!matched) {
|
|
9833
|
+
return { ok: false, error: `input field "${key}" type mismatch` };
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
return { ok: true };
|
|
9837
|
+
}
|
|
9838
|
+
|
|
9839
|
+
function appendWorkflowRunRecord(record) {
|
|
9840
|
+
ensureDir(path.dirname(WORKFLOW_RUNS_FILE));
|
|
9841
|
+
const content = `${JSON.stringify(record)}\n`;
|
|
9842
|
+
fs.appendFileSync(WORKFLOW_RUNS_FILE, content, { encoding: 'utf-8', mode: 0o600 });
|
|
9843
|
+
}
|
|
9844
|
+
|
|
9845
|
+
function listWorkflowRunRecords(limit = 20) {
|
|
9846
|
+
const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
|
|
9847
|
+
if (!fs.existsSync(WORKFLOW_RUNS_FILE)) {
|
|
9848
|
+
return [];
|
|
9849
|
+
}
|
|
9850
|
+
let content = '';
|
|
9851
|
+
try {
|
|
9852
|
+
content = fs.readFileSync(WORKFLOW_RUNS_FILE, 'utf-8');
|
|
9853
|
+
} catch (_) {
|
|
9854
|
+
return [];
|
|
9855
|
+
}
|
|
9856
|
+
const rows = content
|
|
9857
|
+
.split(/\r?\n/g)
|
|
9858
|
+
.map((line) => line.trim())
|
|
9859
|
+
.filter(Boolean);
|
|
9860
|
+
const parsed = [];
|
|
9861
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
9862
|
+
try {
|
|
9863
|
+
const item = JSON.parse(rows[i]);
|
|
9864
|
+
parsed.push(item);
|
|
9865
|
+
if (parsed.length >= max) {
|
|
9866
|
+
break;
|
|
9867
|
+
}
|
|
9868
|
+
} catch (_) {}
|
|
9869
|
+
}
|
|
9870
|
+
return parsed;
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9873
|
+
function validateWorkflowById(workflowId, input = {}) {
|
|
9874
|
+
const definitionResult = getWorkflowDefinitionById(workflowId);
|
|
9875
|
+
if (definitionResult.error) {
|
|
9876
|
+
return { ok: false, error: definitionResult.error };
|
|
9877
|
+
}
|
|
9878
|
+
const workflow = resolveWorkflowDefinitionWithToolMeta(definitionResult.workflow);
|
|
9879
|
+
const knownTools = getWorkflowKnownToolsSet();
|
|
9880
|
+
const validation = validateWorkflowDefinition(workflow, { knownTools });
|
|
9881
|
+
if (!validation.ok) {
|
|
9882
|
+
return {
|
|
9883
|
+
ok: false,
|
|
9884
|
+
error: validation.error || 'workflow validation failed',
|
|
9885
|
+
issues: validation.issues || []
|
|
9886
|
+
};
|
|
9887
|
+
}
|
|
9888
|
+
const schemaValidation = validateWorkflowInputBySchema(workflow.inputSchema, input || {});
|
|
9889
|
+
if (!schemaValidation.ok) {
|
|
9890
|
+
return { ok: false, error: schemaValidation.error || 'workflow input validation failed' };
|
|
9891
|
+
}
|
|
9892
|
+
return {
|
|
9893
|
+
ok: true,
|
|
9894
|
+
workflow: {
|
|
9895
|
+
id: workflow.id,
|
|
9896
|
+
name: workflow.name,
|
|
9897
|
+
readOnly: workflow.readOnly !== false,
|
|
9898
|
+
stepCount: Array.isArray(workflow.steps) ? workflow.steps.length : 0
|
|
9899
|
+
},
|
|
9900
|
+
warnings: definitionResult.warnings || []
|
|
9901
|
+
};
|
|
9902
|
+
}
|
|
9903
|
+
|
|
9904
|
+
async function runWorkflowById(workflowId, input = {}, options = {}) {
|
|
9905
|
+
const definitionResult = getWorkflowDefinitionById(workflowId);
|
|
9906
|
+
if (definitionResult.error) {
|
|
9907
|
+
return { error: definitionResult.error };
|
|
9908
|
+
}
|
|
9909
|
+
const workflow = resolveWorkflowDefinitionWithToolMeta(definitionResult.workflow);
|
|
9910
|
+
const knownTools = getWorkflowKnownToolsSet();
|
|
9911
|
+
const validation = validateWorkflowDefinition(workflow, { knownTools });
|
|
9912
|
+
if (!validation.ok) {
|
|
9913
|
+
return {
|
|
9914
|
+
error: validation.error || 'workflow validation failed',
|
|
9915
|
+
issues: validation.issues || []
|
|
9916
|
+
};
|
|
9917
|
+
}
|
|
9918
|
+
const schemaValidation = validateWorkflowInputBySchema(workflow.inputSchema, input || {});
|
|
9919
|
+
if (!schemaValidation.ok) {
|
|
9920
|
+
return { error: schemaValidation.error || 'workflow input validation failed' };
|
|
9921
|
+
}
|
|
9922
|
+
|
|
9923
|
+
const catalog = createWorkflowToolCatalog();
|
|
9924
|
+
const allowWrite = options.allowWrite === true;
|
|
9925
|
+
const dryRun = options.dryRun === true;
|
|
9926
|
+
const runId = `wf-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
9927
|
+
const startedAt = toIsoTime(Date.now());
|
|
9928
|
+
|
|
9929
|
+
const execution = await executeWorkflowDefinition(workflow, input || {}, {
|
|
9930
|
+
allowWrite,
|
|
9931
|
+
dryRun,
|
|
9932
|
+
invokeTool: async (toolName, args = {}) => {
|
|
9933
|
+
const tool = catalog[toolName];
|
|
9934
|
+
if (!tool) {
|
|
9935
|
+
return { error: `workflow tool not supported: ${toolName}` };
|
|
9936
|
+
}
|
|
9937
|
+
if (!tool.readOnly && !allowWrite) {
|
|
9938
|
+
return { error: `workflow requires write permission for tool: ${toolName}` };
|
|
9939
|
+
}
|
|
9940
|
+
return tool.handler(args || {});
|
|
9941
|
+
}
|
|
9942
|
+
});
|
|
9943
|
+
|
|
9944
|
+
const endedAt = toIsoTime(Date.now());
|
|
9945
|
+
const record = {
|
|
9946
|
+
runId,
|
|
9947
|
+
workflowId: workflow.id,
|
|
9948
|
+
workflowName: workflow.name,
|
|
9949
|
+
success: execution.success === true,
|
|
9950
|
+
error: execution.error || '',
|
|
9951
|
+
allowWrite,
|
|
9952
|
+
dryRun,
|
|
9953
|
+
startedAt,
|
|
9954
|
+
endedAt,
|
|
9955
|
+
durationMs: execution.durationMs || 0,
|
|
9956
|
+
steps: Array.isArray(execution.steps) ? execution.steps.map((step) => ({
|
|
9957
|
+
id: step.id,
|
|
9958
|
+
tool: step.tool,
|
|
9959
|
+
status: step.status,
|
|
9960
|
+
durationMs: step.durationMs || 0,
|
|
9961
|
+
error: step.error || ''
|
|
9962
|
+
})) : [],
|
|
9963
|
+
input: cloneJson(input || {}, {})
|
|
9964
|
+
};
|
|
9965
|
+
try {
|
|
9966
|
+
appendWorkflowRunRecord(record);
|
|
9967
|
+
} catch (_) {}
|
|
9968
|
+
|
|
9969
|
+
return {
|
|
9970
|
+
success: execution.success === true,
|
|
9971
|
+
runId,
|
|
9972
|
+
workflowId: workflow.id,
|
|
9973
|
+
workflowName: workflow.name,
|
|
9974
|
+
allowWrite,
|
|
9975
|
+
dryRun,
|
|
9976
|
+
startedAt: execution.startedAt || startedAt,
|
|
9977
|
+
endedAt: execution.endedAt || endedAt,
|
|
9978
|
+
durationMs: execution.durationMs || 0,
|
|
9979
|
+
steps: execution.steps || [],
|
|
9980
|
+
output: execution.output || null,
|
|
9981
|
+
warnings: definitionResult.warnings || [],
|
|
9982
|
+
...(execution.error ? { error: execution.error } : {})
|
|
9983
|
+
};
|
|
9984
|
+
}
|
|
9985
|
+
|
|
7170
9986
|
function createMcpTools(options = {}) {
|
|
7171
9987
|
const allowWrite = !!options.allowWrite;
|
|
7172
9988
|
const tools = [];
|
|
@@ -7362,6 +10178,89 @@ function createMcpTools(options = {}) {
|
|
|
7362
10178
|
handler: async () => getBuiltinProxyStatus()
|
|
7363
10179
|
});
|
|
7364
10180
|
|
|
10181
|
+
pushTool({
|
|
10182
|
+
name: 'codexmate.workflow.list',
|
|
10183
|
+
description: 'List available workflows (builtin + custom).',
|
|
10184
|
+
readOnly: true,
|
|
10185
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
10186
|
+
handler: async () => listWorkflowDefinitions()
|
|
10187
|
+
});
|
|
10188
|
+
|
|
10189
|
+
pushTool({
|
|
10190
|
+
name: 'codexmate.workflow.get',
|
|
10191
|
+
description: 'Get one workflow definition by id.',
|
|
10192
|
+
readOnly: true,
|
|
10193
|
+
inputSchema: {
|
|
10194
|
+
type: 'object',
|
|
10195
|
+
properties: {
|
|
10196
|
+
id: { type: 'string' }
|
|
10197
|
+
},
|
|
10198
|
+
required: ['id'],
|
|
10199
|
+
additionalProperties: false
|
|
10200
|
+
},
|
|
10201
|
+
handler: async (args = {}) => {
|
|
10202
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
10203
|
+
if (!id) {
|
|
10204
|
+
return { error: 'workflow id is required' };
|
|
10205
|
+
}
|
|
10206
|
+
return getWorkflowDefinitionById(id);
|
|
10207
|
+
}
|
|
10208
|
+
});
|
|
10209
|
+
|
|
10210
|
+
pushTool({
|
|
10211
|
+
name: 'codexmate.workflow.validate',
|
|
10212
|
+
description: 'Validate workflow definition and input payload.',
|
|
10213
|
+
readOnly: true,
|
|
10214
|
+
inputSchema: {
|
|
10215
|
+
type: 'object',
|
|
10216
|
+
properties: {
|
|
10217
|
+
id: { type: 'string' },
|
|
10218
|
+
input: { type: 'object' }
|
|
10219
|
+
},
|
|
10220
|
+
required: ['id'],
|
|
10221
|
+
additionalProperties: false
|
|
10222
|
+
},
|
|
10223
|
+
handler: async (args = {}) => {
|
|
10224
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
10225
|
+
if (!id) {
|
|
10226
|
+
return { ok: false, error: 'workflow id is required' };
|
|
10227
|
+
}
|
|
10228
|
+
const input = args.input && typeof args.input === 'object' && !Array.isArray(args.input)
|
|
10229
|
+
? args.input
|
|
10230
|
+
: {};
|
|
10231
|
+
return validateWorkflowById(id, input);
|
|
10232
|
+
}
|
|
10233
|
+
});
|
|
10234
|
+
|
|
10235
|
+
pushTool({
|
|
10236
|
+
name: 'codexmate.workflow.run',
|
|
10237
|
+
description: 'Run workflow by id. Write steps require allow-write mode.',
|
|
10238
|
+
readOnly: true,
|
|
10239
|
+
inputSchema: {
|
|
10240
|
+
type: 'object',
|
|
10241
|
+
properties: {
|
|
10242
|
+
id: { type: 'string' },
|
|
10243
|
+
input: { type: 'object' },
|
|
10244
|
+
dryRun: { type: 'boolean' }
|
|
10245
|
+
},
|
|
10246
|
+
required: ['id'],
|
|
10247
|
+
additionalProperties: false
|
|
10248
|
+
},
|
|
10249
|
+
handler: async (args = {}) => {
|
|
10250
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
10251
|
+
if (!id) {
|
|
10252
|
+
return { error: 'workflow id is required' };
|
|
10253
|
+
}
|
|
10254
|
+
const input = args.input && typeof args.input === 'object' && !Array.isArray(args.input)
|
|
10255
|
+
? args.input
|
|
10256
|
+
: {};
|
|
10257
|
+
return runWorkflowById(id, input, {
|
|
10258
|
+
allowWrite,
|
|
10259
|
+
dryRun: args.dryRun === true
|
|
10260
|
+
});
|
|
10261
|
+
}
|
|
10262
|
+
});
|
|
10263
|
+
|
|
7365
10264
|
pushTool({
|
|
7366
10265
|
name: 'codexmate.config.template.apply',
|
|
7367
10266
|
description: 'Apply Codex TOML template and sync auth/model pointers.',
|
|
@@ -7636,6 +10535,50 @@ function createMcpResources() {
|
|
|
7636
10535
|
}]
|
|
7637
10536
|
};
|
|
7638
10537
|
}
|
|
10538
|
+
},
|
|
10539
|
+
{
|
|
10540
|
+
uri: 'codexmate://workflows',
|
|
10541
|
+
name: 'Workflows',
|
|
10542
|
+
description: 'Workflow list resource (builtin + custom).',
|
|
10543
|
+
mimeType: 'application/json',
|
|
10544
|
+
read: async () => ({
|
|
10545
|
+
contents: [{
|
|
10546
|
+
uri: 'codexmate://workflows',
|
|
10547
|
+
mimeType: 'application/json',
|
|
10548
|
+
text: JSON.stringify(listWorkflowDefinitions(), null, 2)
|
|
10549
|
+
}]
|
|
10550
|
+
})
|
|
10551
|
+
},
|
|
10552
|
+
{
|
|
10553
|
+
uri: 'codexmate://workflow-runs',
|
|
10554
|
+
name: 'WorkflowRuns',
|
|
10555
|
+
description: 'Recent workflow execution records. Supports ?limit=<N>.',
|
|
10556
|
+
mimeType: 'application/json',
|
|
10557
|
+
read: async (params = {}) => {
|
|
10558
|
+
const uri = typeof params.uri === 'string' ? params.uri : 'codexmate://workflow-runs';
|
|
10559
|
+
let limit = 20;
|
|
10560
|
+
try {
|
|
10561
|
+
const parsed = new URL(uri);
|
|
10562
|
+
const rawLimit = parsed.searchParams.get('limit');
|
|
10563
|
+
if (rawLimit) {
|
|
10564
|
+
const parsedLimit = parseInt(rawLimit, 10);
|
|
10565
|
+
if (Number.isFinite(parsedLimit)) {
|
|
10566
|
+
limit = parsedLimit;
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
} catch (_) {}
|
|
10570
|
+
const payload = {
|
|
10571
|
+
runs: listWorkflowRunRecords(limit),
|
|
10572
|
+
limit: Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20
|
|
10573
|
+
};
|
|
10574
|
+
return {
|
|
10575
|
+
contents: [{
|
|
10576
|
+
uri,
|
|
10577
|
+
mimeType: 'application/json',
|
|
10578
|
+
text: JSON.stringify(payload, null, 2)
|
|
10579
|
+
}]
|
|
10580
|
+
};
|
|
10581
|
+
}
|
|
7639
10582
|
}
|
|
7640
10583
|
];
|
|
7641
10584
|
}
|
|
@@ -7820,10 +10763,10 @@ async function main() {
|
|
|
7820
10763
|
console.log(' codexmate delete-model <模型> 删除模型');
|
|
7821
10764
|
console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
|
|
7822
10765
|
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
7823
|
-
console.log(' codexmate
|
|
10766
|
+
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
10767
|
+
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
7824
10768
|
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
7825
10769
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
7826
|
-
console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
|
|
7827
10770
|
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
7828
10771
|
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
7829
10772
|
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
@@ -7846,6 +10789,7 @@ async function main() {
|
|
|
7846
10789
|
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
7847
10790
|
case 'auth': cmdAuth(args.slice(1)); break;
|
|
7848
10791
|
case 'proxy': await cmdProxy(args.slice(1)); break;
|
|
10792
|
+
case 'workflow': await cmdWorkflow(args.slice(1)); break;
|
|
7849
10793
|
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
7850
10794
|
case 'start':
|
|
7851
10795
|
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|
|
@@ -7861,11 +10805,6 @@ async function main() {
|
|
|
7861
10805
|
process.exit(exitCode);
|
|
7862
10806
|
break;
|
|
7863
10807
|
}
|
|
7864
|
-
case 'gemini': {
|
|
7865
|
-
const exitCode = await cmdGemini(args.slice(1));
|
|
7866
|
-
process.exit(exitCode);
|
|
7867
|
-
break;
|
|
7868
|
-
}
|
|
7869
10808
|
case 'mcp': await cmdMcp(args.slice(1)); break;
|
|
7870
10809
|
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
7871
10810
|
case 'zip': {
|