codexmate 0.0.13 → 0.0.14
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 +429 -0
- package/README.md +231 -215
- package/cli.js +2450 -137
- package/doc/CHANGELOG.md +10 -3
- package/doc/CHANGELOG.zh-CN.md +7 -0
- package/lib/cli-utils.js +16 -0
- package/lib/workflow-engine.js +340 -0
- package/package.json +11 -4
- package/web-ui/app.js +515 -5
- package/web-ui/index.html +242 -19
- package/web-ui/logic.mjs +147 -1
- package/web-ui/styles.css +648 -10
- package/README.zh-CN.md +0 -419
package/cli.js
CHANGED
|
@@ -21,6 +21,8 @@ const {
|
|
|
21
21
|
detectLineEnding,
|
|
22
22
|
normalizeLineEnding,
|
|
23
23
|
isValidProviderName,
|
|
24
|
+
escapeTomlBasicString,
|
|
25
|
+
buildModelProviderTableHeader,
|
|
24
26
|
buildModelsCandidates,
|
|
25
27
|
isValidHttpUrl,
|
|
26
28
|
normalizeBaseUrl,
|
|
@@ -54,6 +56,10 @@ const {
|
|
|
54
56
|
resolveMaxMessagesValue
|
|
55
57
|
} = require('./lib/cli-session-utils');
|
|
56
58
|
const { createMcpStdioServer } = require('./lib/mcp-stdio');
|
|
59
|
+
const {
|
|
60
|
+
validateWorkflowDefinition,
|
|
61
|
+
executeWorkflowDefinition
|
|
62
|
+
} = require('./lib/workflow-engine');
|
|
57
63
|
|
|
58
64
|
const DEFAULT_WEB_PORT = 3737;
|
|
59
65
|
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
@@ -78,6 +84,8 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
78
84
|
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
79
85
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
80
86
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
87
|
+
const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
|
|
88
|
+
const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
|
|
81
89
|
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
82
90
|
const CODEX_BACKUP_NAME = 'codex-config';
|
|
83
91
|
|
|
@@ -98,6 +106,15 @@ const SESSION_SCAN_FACTOR = 4;
|
|
|
98
106
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
99
107
|
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
100
108
|
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
109
|
+
const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
|
|
110
|
+
const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
|
|
111
|
+
const GEMINI_SKILLS_DIR = path.join(os.homedir(), '.gemini', 'skills');
|
|
112
|
+
const OPENCODE_SKILLS_DIR = path.join(os.homedir(), '.opencode', 'skills');
|
|
113
|
+
const SKILL_IMPORT_SOURCES = Object.freeze([
|
|
114
|
+
{ app: 'claude', label: 'Claude Code', dir: CLAUDE_SKILLS_DIR },
|
|
115
|
+
{ app: 'gemini', label: 'Gemini CLI', dir: GEMINI_SKILLS_DIR },
|
|
116
|
+
{ app: 'opencode', label: 'OpenCode', dir: OPENCODE_SKILLS_DIR }
|
|
117
|
+
]);
|
|
101
118
|
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
102
119
|
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
103
120
|
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
@@ -215,16 +232,50 @@ function ensureConfigDir() {
|
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
function createConfigLoadError(type, message, detail) {
|
|
236
|
+
const err = new Error(detail || message);
|
|
237
|
+
err.configErrorType = type || 'read';
|
|
238
|
+
err.configPublicReason = message || '读取 config.toml 失败';
|
|
239
|
+
err.configDetail = detail || message || '';
|
|
240
|
+
return err;
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
function readConfig() {
|
|
219
244
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
220
|
-
throw
|
|
245
|
+
throw createConfigLoadError(
|
|
246
|
+
'missing',
|
|
247
|
+
'未检测到 config.toml',
|
|
248
|
+
`配置文件不存在: ${CONFIG_FILE}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let content = '';
|
|
253
|
+
try {
|
|
254
|
+
content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
255
|
+
} catch (e) {
|
|
256
|
+
throw createConfigLoadError(
|
|
257
|
+
'read',
|
|
258
|
+
'读取 config.toml 失败',
|
|
259
|
+
`读取配置文件失败: ${e && e.message ? e.message : e}`
|
|
260
|
+
);
|
|
221
261
|
}
|
|
262
|
+
|
|
263
|
+
let parsed;
|
|
222
264
|
try {
|
|
223
|
-
|
|
224
|
-
return toml.parse(content);
|
|
265
|
+
parsed = toml.parse(content);
|
|
225
266
|
} catch (e) {
|
|
226
|
-
throw
|
|
267
|
+
throw createConfigLoadError(
|
|
268
|
+
'parse',
|
|
269
|
+
'config.toml 解析失败',
|
|
270
|
+
`配置文件解析失败: ${e && e.message ? e.message : e}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (isPlainObject(parsed) && isPlainObject(parsed.model_providers)) {
|
|
275
|
+
const providerHeaderSegmentKeySet = collectModelProviderHeaderSegmentKeySet(content);
|
|
276
|
+
parsed.model_providers = normalizeLegacyModelProviders(parsed.model_providers, providerHeaderSegmentKeySet);
|
|
227
277
|
}
|
|
278
|
+
return parsed;
|
|
228
279
|
}
|
|
229
280
|
|
|
230
281
|
function writeConfig(content) {
|
|
@@ -277,6 +328,519 @@ function isPlainObject(value) {
|
|
|
277
328
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
278
329
|
}
|
|
279
330
|
|
|
331
|
+
const PROVIDER_CONFIG_KEYS = new Set([
|
|
332
|
+
'name',
|
|
333
|
+
'base_url',
|
|
334
|
+
'wire_api',
|
|
335
|
+
'requires_openai_auth',
|
|
336
|
+
'preferred_auth_method',
|
|
337
|
+
'request_max_retries',
|
|
338
|
+
'stream_max_retries',
|
|
339
|
+
'stream_idle_timeout_ms'
|
|
340
|
+
]);
|
|
341
|
+
const RECOVERABLE_PROVIDER_SIGNAL_KEYS = [...PROVIDER_CONFIG_KEYS].filter((key) => key !== 'name' && key !== 'base_url');
|
|
342
|
+
|
|
343
|
+
function looksLikeProviderConfig(value) {
|
|
344
|
+
if (!isPlainObject(value)) return false;
|
|
345
|
+
return Object.keys(value).some((key) => PROVIDER_CONFIG_KEYS.has(key));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isRecoverableNestedProviderConfig(value) {
|
|
349
|
+
if (!isPlainObject(value)) return false;
|
|
350
|
+
const hasBaseUrl = typeof value.base_url === 'string' && value.base_url.trim() !== '';
|
|
351
|
+
if (!hasBaseUrl) return false;
|
|
352
|
+
const hasName = typeof value.name === 'string' && value.name.trim() !== '';
|
|
353
|
+
const hasProviderSignals = RECOVERABLE_PROVIDER_SIGNAL_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
354
|
+
return hasName || hasProviderSignals;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function collectNestedProviderConfigs(node, pathSegments, collector) {
|
|
358
|
+
if (!isPlainObject(node)) return;
|
|
359
|
+
const segments = Array.isArray(pathSegments) ? pathSegments : [String(pathSegments || '')];
|
|
360
|
+
const lastSegment = segments.length > 0 ? segments[segments.length - 1] : '';
|
|
361
|
+
if (segments.length > 1 && lastSegment === 'metadata') {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (isRecoverableNestedProviderConfig(node)) {
|
|
365
|
+
collector.push({
|
|
366
|
+
name: segments.join('.'),
|
|
367
|
+
segments: segments.slice(),
|
|
368
|
+
provider: node
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
for (const [childKey, childValue] of Object.entries(node)) {
|
|
372
|
+
if (!isPlainObject(childValue)) continue;
|
|
373
|
+
collectNestedProviderConfigs(childValue, [...segments, childKey], collector);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function normalizeLegacySegments(segments) {
|
|
378
|
+
if (!Array.isArray(segments) || segments.length === 0) return null;
|
|
379
|
+
return segments.map((item) => String(item));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildLegacySegmentsKey(segments) {
|
|
383
|
+
const normalized = normalizeLegacySegments(segments);
|
|
384
|
+
return normalized ? JSON.stringify(normalized) : '';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function appendLegacySegmentsVariant(provider, segments) {
|
|
388
|
+
if (!isPlainObject(provider)) return;
|
|
389
|
+
const normalized = normalizeLegacySegments(segments);
|
|
390
|
+
if (!normalized) return;
|
|
391
|
+
|
|
392
|
+
const variants = [];
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
const pushVariant = (candidate) => {
|
|
395
|
+
const key = buildLegacySegmentsKey(candidate);
|
|
396
|
+
if (!key || seen.has(key)) return;
|
|
397
|
+
seen.add(key);
|
|
398
|
+
variants.push(normalizeLegacySegments(candidate));
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (Array.isArray(provider.__codexmate_legacy_segments)) {
|
|
402
|
+
pushVariant(provider.__codexmate_legacy_segments);
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(provider.__codexmate_legacy_segment_variants)) {
|
|
405
|
+
for (const candidate of provider.__codexmate_legacy_segment_variants) {
|
|
406
|
+
pushVariant(candidate);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
pushVariant(normalized);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
if (!Array.isArray(provider.__codexmate_legacy_segments)) {
|
|
413
|
+
Object.defineProperty(provider, '__codexmate_legacy_segments', {
|
|
414
|
+
value: normalized,
|
|
415
|
+
enumerable: false,
|
|
416
|
+
configurable: true,
|
|
417
|
+
writable: true
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
Object.defineProperty(provider, '__codexmate_legacy_segment_variants', {
|
|
421
|
+
value: variants,
|
|
422
|
+
enumerable: false,
|
|
423
|
+
configurable: true,
|
|
424
|
+
writable: true
|
|
425
|
+
});
|
|
426
|
+
} catch (e) {}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function setLegacySegmentsMetadata(provider, segments) {
|
|
430
|
+
appendLegacySegmentsVariant(provider, segments);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizeLegacyModelProviders(modelProviders, providerHeaderSegmentKeySet = null) {
|
|
434
|
+
if (!isPlainObject(modelProviders)) {
|
|
435
|
+
return modelProviders;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let changed = false;
|
|
439
|
+
const normalized = {};
|
|
440
|
+
const addRecovered = (entry) => {
|
|
441
|
+
const name = entry && typeof entry.name === 'string' ? entry.name : '';
|
|
442
|
+
const segments = entry && Array.isArray(entry.segments) ? entry.segments.slice() : null;
|
|
443
|
+
const provider = entry ? entry.provider : null;
|
|
444
|
+
if (!name || !isPlainObject(provider)) return;
|
|
445
|
+
const segmentKey = buildLegacySegmentsKey(segments);
|
|
446
|
+
if (providerHeaderSegmentKeySet instanceof Set && segmentKey && !providerHeaderSegmentKeySet.has(segmentKey)) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const existing = Object.prototype.hasOwnProperty.call(normalized, name)
|
|
450
|
+
? normalized[name]
|
|
451
|
+
: (Object.prototype.hasOwnProperty.call(modelProviders, name) ? modelProviders[name] : null);
|
|
452
|
+
if (isPlainObject(existing)) {
|
|
453
|
+
if (!Array.isArray(existing.__codexmate_legacy_segments)) {
|
|
454
|
+
setLegacySegmentsMetadata(existing, [name]);
|
|
455
|
+
}
|
|
456
|
+
appendLegacySegmentsVariant(existing, segments);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (Object.prototype.hasOwnProperty.call(modelProviders, name)) return;
|
|
460
|
+
if (Object.prototype.hasOwnProperty.call(normalized, name)) return;
|
|
461
|
+
setLegacySegmentsMetadata(provider, segments);
|
|
462
|
+
normalized[name] = provider;
|
|
463
|
+
changed = true;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
for (const [name, provider] of Object.entries(modelProviders)) {
|
|
467
|
+
normalized[name] = provider;
|
|
468
|
+
if (!isPlainObject(provider)) continue;
|
|
469
|
+
|
|
470
|
+
if (looksLikeProviderConfig(provider)) {
|
|
471
|
+
setLegacySegmentsMetadata(provider, [name]);
|
|
472
|
+
for (const [childKey, childValue] of Object.entries(provider)) {
|
|
473
|
+
if (!isPlainObject(childValue)) continue;
|
|
474
|
+
const recovered = [];
|
|
475
|
+
collectNestedProviderConfigs(childValue, [name, childKey], recovered);
|
|
476
|
+
for (const recoveredEntry of recovered) {
|
|
477
|
+
addRecovered(recoveredEntry);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const recovered = [];
|
|
484
|
+
collectNestedProviderConfigs(provider, [name], recovered);
|
|
485
|
+
delete normalized[name];
|
|
486
|
+
changed = true;
|
|
487
|
+
for (const recoveredEntry of recovered) {
|
|
488
|
+
addRecovered(recoveredEntry);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return changed ? normalized : modelProviders;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function escapeRegex(value) {
|
|
496
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function areStringArraysEqual(a, b) {
|
|
500
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
501
|
+
for (let i = 0; i < a.length; i++) {
|
|
502
|
+
if (String(a[i]) !== String(b[i])) return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function parseTomlDottedKeyExpression(expression) {
|
|
508
|
+
const text = String(expression || '');
|
|
509
|
+
let index = 0;
|
|
510
|
+
const segments = [];
|
|
511
|
+
const skipWhitespace = () => {
|
|
512
|
+
while (index < text.length && /\s/.test(text[index])) index++;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
while (index < text.length) {
|
|
516
|
+
skipWhitespace();
|
|
517
|
+
if (index >= text.length) break;
|
|
518
|
+
|
|
519
|
+
const ch = text[index];
|
|
520
|
+
if (ch === "'") {
|
|
521
|
+
const end = text.indexOf("'", index + 1);
|
|
522
|
+
if (end === -1) return null;
|
|
523
|
+
segments.push(text.slice(index + 1, end));
|
|
524
|
+
index = end + 1;
|
|
525
|
+
} else if (ch === '"') {
|
|
526
|
+
index += 1;
|
|
527
|
+
let value = '';
|
|
528
|
+
let closed = false;
|
|
529
|
+
while (index < text.length) {
|
|
530
|
+
const cur = text[index];
|
|
531
|
+
if (cur === '"') {
|
|
532
|
+
index += 1;
|
|
533
|
+
closed = true;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
if (cur !== '\\') {
|
|
537
|
+
value += cur;
|
|
538
|
+
index += 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (index + 1 >= text.length) return null;
|
|
542
|
+
const esc = text[index + 1];
|
|
543
|
+
if (esc === 'u' || esc === 'U') {
|
|
544
|
+
const hexLen = esc === 'u' ? 4 : 8;
|
|
545
|
+
const hex = text.slice(index + 2, index + 2 + hexLen);
|
|
546
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
547
|
+
try {
|
|
548
|
+
value += String.fromCodePoint(parseInt(hex, 16));
|
|
549
|
+
} catch (e) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
index += 2 + hexLen;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const unescaped = {
|
|
556
|
+
b: '\b',
|
|
557
|
+
t: '\t',
|
|
558
|
+
n: '\n',
|
|
559
|
+
f: '\f',
|
|
560
|
+
r: '\r',
|
|
561
|
+
'"': '"',
|
|
562
|
+
'\\': '\\'
|
|
563
|
+
}[esc];
|
|
564
|
+
if (unescaped === undefined) return null;
|
|
565
|
+
value += unescaped;
|
|
566
|
+
index += 2;
|
|
567
|
+
}
|
|
568
|
+
if (!closed) return null;
|
|
569
|
+
segments.push(value);
|
|
570
|
+
} else {
|
|
571
|
+
const start = index;
|
|
572
|
+
while (index < text.length && !/\s|\./.test(text[index])) index++;
|
|
573
|
+
const bare = text.slice(start, index);
|
|
574
|
+
if (!bare) return null;
|
|
575
|
+
segments.push(bare);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
skipWhitespace();
|
|
579
|
+
if (index >= text.length) break;
|
|
580
|
+
if (text[index] !== '.') return null;
|
|
581
|
+
index += 1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return segments.length > 0 ? segments : null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function collectTomlMultilineStringRanges(text) {
|
|
588
|
+
const source = typeof text === 'string' ? text : '';
|
|
589
|
+
const ranges = [];
|
|
590
|
+
let i = 0;
|
|
591
|
+
let inMultilineBasic = false;
|
|
592
|
+
let inMultilineLiteral = false;
|
|
593
|
+
let rangeStart = -1;
|
|
594
|
+
|
|
595
|
+
while (i < source.length) {
|
|
596
|
+
if (inMultilineBasic) {
|
|
597
|
+
if (source.slice(i, i + 3) === '"""') {
|
|
598
|
+
let slashCount = 0;
|
|
599
|
+
for (let j = i - 1; j >= 0 && source[j] === '\\'; j--) {
|
|
600
|
+
slashCount++;
|
|
601
|
+
}
|
|
602
|
+
if (slashCount % 2 === 0) {
|
|
603
|
+
let runEnd = i + 3;
|
|
604
|
+
while (runEnd < source.length && source[runEnd] === '"') runEnd++;
|
|
605
|
+
ranges.push({ start: rangeStart, end: runEnd });
|
|
606
|
+
inMultilineBasic = false;
|
|
607
|
+
rangeStart = -1;
|
|
608
|
+
i = runEnd;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
i++;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (inMultilineLiteral) {
|
|
617
|
+
if (source.slice(i, i + 3) === "'''") {
|
|
618
|
+
let runEnd = i + 3;
|
|
619
|
+
while (runEnd < source.length && source[runEnd] === '\'') runEnd++;
|
|
620
|
+
ranges.push({ start: rangeStart, end: runEnd });
|
|
621
|
+
inMultilineLiteral = false;
|
|
622
|
+
rangeStart = -1;
|
|
623
|
+
i = runEnd;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
i++;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const ch = source[i];
|
|
631
|
+
if (ch === '#') {
|
|
632
|
+
while (i < source.length && source[i] !== '\n') i++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (source.slice(i, i + 3) === '"""') {
|
|
637
|
+
inMultilineBasic = true;
|
|
638
|
+
rangeStart = i;
|
|
639
|
+
i += 3;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (source.slice(i, i + 3) === "'''") {
|
|
644
|
+
inMultilineLiteral = true;
|
|
645
|
+
rangeStart = i;
|
|
646
|
+
i += 3;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (ch === '"') {
|
|
651
|
+
i++;
|
|
652
|
+
while (i < source.length) {
|
|
653
|
+
if (source[i] === '\\') {
|
|
654
|
+
i += 2;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (source[i] === '"' || source[i] === '\n') {
|
|
658
|
+
i++;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
i++;
|
|
662
|
+
}
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (ch === '\'') {
|
|
667
|
+
i++;
|
|
668
|
+
while (i < source.length) {
|
|
669
|
+
if (source[i] === '\'' || source[i] === '\n') {
|
|
670
|
+
i++;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
i++;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (rangeStart >= 0) {
|
|
682
|
+
ranges.push({ start: rangeStart, end: source.length });
|
|
683
|
+
}
|
|
684
|
+
return ranges;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function isIndexInRanges(index, ranges) {
|
|
688
|
+
for (const range of ranges) {
|
|
689
|
+
if (index < range.start) return false;
|
|
690
|
+
if (index >= range.start && index < range.end) return true;
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function findProviderSectionRanges(content, providerName, exactSegments = null) {
|
|
696
|
+
const text = typeof content === 'string' ? content : '';
|
|
697
|
+
const name = typeof providerName === 'string' ? providerName.trim() : '';
|
|
698
|
+
const targetSegments = Array.isArray(exactSegments) ? exactSegments.map((item) => String(item)) : null;
|
|
699
|
+
if (!text || !name) return [];
|
|
700
|
+
|
|
701
|
+
const safeName = escapeRegex(name);
|
|
702
|
+
const headerPatterns = [
|
|
703
|
+
{ priority: 0, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*"${safeName}"\\s*$`) },
|
|
704
|
+
{ priority: 1, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*'${safeName}'\\s*$`) },
|
|
705
|
+
{ priority: 2, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*${safeName}\\s*$`) }
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
const allHeaders = [];
|
|
709
|
+
const targetPriorityByStart = new Map();
|
|
710
|
+
const multilineStringRanges = collectTomlMultilineStringRanges(text);
|
|
711
|
+
const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
712
|
+
let match;
|
|
713
|
+
while ((match = sectionLineRegex.exec(text)) !== null) {
|
|
714
|
+
const start = match.index;
|
|
715
|
+
if (isIndexInRanges(start, multilineStringRanges)) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
allHeaders.push(start);
|
|
719
|
+
const headerExpr = String(match[1] || '').trim();
|
|
720
|
+
|
|
721
|
+
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
|
|
722
|
+
if (Array.isArray(parsedSegments) && parsedSegments.length >= 2 && parsedSegments[0] === 'model_providers') {
|
|
723
|
+
const providerSegments = parsedSegments.slice(1);
|
|
724
|
+
if (targetSegments && targetSegments.length > 0 && areStringArraysEqual(providerSegments, targetSegments)) {
|
|
725
|
+
const prev = targetPriorityByStart.get(start);
|
|
726
|
+
if (prev === undefined || -3 < prev) {
|
|
727
|
+
targetPriorityByStart.set(start, -3);
|
|
728
|
+
}
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!targetSegments || targetSegments.length === 0) {
|
|
732
|
+
const parsedName = providerSegments.join('.');
|
|
733
|
+
if (parsedName === name) {
|
|
734
|
+
const prev = targetPriorityByStart.get(start);
|
|
735
|
+
if (prev === undefined || -2 < prev) {
|
|
736
|
+
targetPriorityByStart.set(start, -2);
|
|
737
|
+
}
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
for (const pattern of headerPatterns) {
|
|
744
|
+
if (pattern.regex.test(headerExpr)) {
|
|
745
|
+
const prev = targetPriorityByStart.get(start);
|
|
746
|
+
if (prev === undefined || pattern.priority < prev) {
|
|
747
|
+
targetPriorityByStart.set(start, pattern.priority);
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (targetPriorityByStart.size === 0) {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const ranges = [];
|
|
759
|
+
for (let i = 0; i < allHeaders.length; i++) {
|
|
760
|
+
const start = allHeaders[i];
|
|
761
|
+
if (!targetPriorityByStart.has(start)) continue;
|
|
762
|
+
const end = i + 1 < allHeaders.length ? allHeaders[i + 1] : text.length;
|
|
763
|
+
ranges.push({
|
|
764
|
+
start,
|
|
765
|
+
end,
|
|
766
|
+
priority: targetPriorityByStart.get(start)
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
const exactMatches = ranges.filter((range) => range.priority === -3);
|
|
770
|
+
return exactMatches.length > 0 ? exactMatches : ranges;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function doesSegmentsStartWith(segments, prefix) {
|
|
774
|
+
if (!Array.isArray(segments) || !Array.isArray(prefix) || prefix.length === 0 || segments.length < prefix.length) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
778
|
+
if (String(segments[i]) !== String(prefix[i])) return false;
|
|
779
|
+
}
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function findProviderDescendantSectionRanges(content, prefixSegments) {
|
|
784
|
+
const text = typeof content === 'string' ? content : '';
|
|
785
|
+
const prefix = Array.isArray(prefixSegments) ? prefixSegments.map((item) => String(item)) : [];
|
|
786
|
+
if (!text || prefix.length === 0) return [];
|
|
787
|
+
|
|
788
|
+
const allHeaders = [];
|
|
789
|
+
const parsedProviderSegmentsByStart = new Map();
|
|
790
|
+
const multilineStringRanges = collectTomlMultilineStringRanges(text);
|
|
791
|
+
const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
792
|
+
let match;
|
|
793
|
+
while ((match = sectionLineRegex.exec(text)) !== null) {
|
|
794
|
+
const start = match.index;
|
|
795
|
+
if (isIndexInRanges(start, multilineStringRanges)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
allHeaders.push(start);
|
|
799
|
+
const headerExpr = String(match[1] || '').trim();
|
|
800
|
+
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
|
|
801
|
+
if (!Array.isArray(parsedSegments) || parsedSegments.length < 2 || parsedSegments[0] !== 'model_providers') {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
parsedProviderSegmentsByStart.set(start, parsedSegments.slice(1));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const ranges = [];
|
|
808
|
+
for (let i = 0; i < allHeaders.length; i++) {
|
|
809
|
+
const start = allHeaders[i];
|
|
810
|
+
const providerSegments = parsedProviderSegmentsByStart.get(start);
|
|
811
|
+
if (!providerSegments) continue;
|
|
812
|
+
if (!doesSegmentsStartWith(providerSegments, prefix)) continue;
|
|
813
|
+
if (providerSegments.length <= prefix.length) continue;
|
|
814
|
+
const end = i + 1 < allHeaders.length ? allHeaders[i + 1] : text.length;
|
|
815
|
+
ranges.push({ start, end, priority: 0 });
|
|
816
|
+
}
|
|
817
|
+
return ranges;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function collectModelProviderHeaderSegmentKeySet(content) {
|
|
821
|
+
const text = typeof content === 'string' ? content : '';
|
|
822
|
+
const keys = new Set();
|
|
823
|
+
if (!text) return keys;
|
|
824
|
+
|
|
825
|
+
const multilineStringRanges = collectTomlMultilineStringRanges(text);
|
|
826
|
+
const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
|
|
827
|
+
let match;
|
|
828
|
+
while ((match = sectionLineRegex.exec(text)) !== null) {
|
|
829
|
+
const start = match.index;
|
|
830
|
+
if (isIndexInRanges(start, multilineStringRanges)) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const headerExpr = String(match[1] || '').trim();
|
|
834
|
+
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
|
|
835
|
+
if (!Array.isArray(parsedSegments) || parsedSegments.length < 2 || parsedSegments[0] !== 'model_providers') {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const key = buildLegacySegmentsKey(parsedSegments.slice(1));
|
|
839
|
+
if (key) keys.add(key);
|
|
840
|
+
}
|
|
841
|
+
return keys;
|
|
842
|
+
}
|
|
843
|
+
|
|
280
844
|
function normalizeAuthProfileName(value) {
|
|
281
845
|
const raw = typeof value === 'string' ? value.trim() : '';
|
|
282
846
|
if (!raw) return '';
|
|
@@ -790,78 +1354,620 @@ function validateAgentsBaseDir(filePath) {
|
|
|
790
1354
|
return { ok: true, dirPath };
|
|
791
1355
|
}
|
|
792
1356
|
|
|
793
|
-
function
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return { error: dirCheck.error };
|
|
1357
|
+
function normalizeCodexSkillName(name) {
|
|
1358
|
+
const value = typeof name === 'string' ? name.trim() : '';
|
|
1359
|
+
if (!value) {
|
|
1360
|
+
return { error: '技能名称不能为空' };
|
|
798
1361
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
return {
|
|
802
|
-
exists: false,
|
|
803
|
-
path: filePath,
|
|
804
|
-
content: '',
|
|
805
|
-
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
806
|
-
};
|
|
1362
|
+
if (value.includes('\0')) {
|
|
1363
|
+
return { error: '技能名称非法' };
|
|
807
1364
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
811
|
-
return {
|
|
812
|
-
exists: true,
|
|
813
|
-
path: filePath,
|
|
814
|
-
content: stripUtf8Bom(raw),
|
|
815
|
-
lineEnding: detectLineEnding(raw)
|
|
816
|
-
};
|
|
817
|
-
} catch (e) {
|
|
818
|
-
return { error: `读取 AGENTS.md 失败: ${e.message}` };
|
|
1365
|
+
if (value === '.' || value === '..') {
|
|
1366
|
+
return { error: '技能名称非法' };
|
|
819
1367
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
function applyAgentsFile(params = {}) {
|
|
823
|
-
const filePath = resolveAgentsFilePath(params);
|
|
824
|
-
const dirCheck = validateAgentsBaseDir(filePath);
|
|
825
|
-
if (dirCheck.error) {
|
|
826
|
-
return { error: dirCheck.error };
|
|
1368
|
+
if (value.includes('/') || value.includes('\\')) {
|
|
1369
|
+
return { error: '技能名称非法' };
|
|
827
1370
|
}
|
|
1371
|
+
if (path.basename(value) !== value) {
|
|
1372
|
+
return { error: '技能名称非法' };
|
|
1373
|
+
}
|
|
1374
|
+
if (value.startsWith('.')) {
|
|
1375
|
+
return { error: '系统技能不可删除' };
|
|
1376
|
+
}
|
|
1377
|
+
return { name: value };
|
|
1378
|
+
}
|
|
828
1379
|
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
const normalized = normalizeLineEnding(content, lineEnding);
|
|
832
|
-
const finalContent = ensureUtf8Bom(normalized);
|
|
833
|
-
|
|
1380
|
+
function isSkillDirectoryEntry(entryName) {
|
|
1381
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
|
|
834
1382
|
try {
|
|
835
|
-
fs.
|
|
836
|
-
return
|
|
1383
|
+
const stat = fs.statSync(targetPath);
|
|
1384
|
+
return stat.isDirectory();
|
|
837
1385
|
} catch (e) {
|
|
838
|
-
return
|
|
1386
|
+
return false;
|
|
839
1387
|
}
|
|
840
1388
|
}
|
|
841
1389
|
|
|
842
|
-
function
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
&& config.agents.defaults
|
|
846
|
-
&& typeof config.agents.defaults.workspace === 'string'
|
|
847
|
-
? config.agents.defaults.workspace
|
|
848
|
-
: '';
|
|
849
|
-
const resolved = resolveHomePath(workspace);
|
|
850
|
-
if (!resolved) {
|
|
851
|
-
return OPENCLAW_WORKSPACE_DIR;
|
|
852
|
-
}
|
|
853
|
-
if (path.isAbsolute(resolved)) {
|
|
854
|
-
return resolved;
|
|
855
|
-
}
|
|
856
|
-
return path.join(OPENCLAW_DIR, resolved);
|
|
1390
|
+
function normalizeSkillImportSourceApp(app) {
|
|
1391
|
+
const value = typeof app === 'string' ? app.trim().toLowerCase() : '';
|
|
1392
|
+
return SKILL_IMPORT_SOURCES.some((item) => item.app === value) ? value : '';
|
|
857
1393
|
}
|
|
858
1394
|
|
|
859
|
-
function
|
|
860
|
-
const
|
|
861
|
-
if (!
|
|
862
|
-
|
|
1395
|
+
function getSkillImportSourceByApp(app) {
|
|
1396
|
+
const normalizedApp = normalizeSkillImportSourceApp(app);
|
|
1397
|
+
if (!normalizedApp) return null;
|
|
1398
|
+
return SKILL_IMPORT_SOURCES.find((item) => item.app === normalizedApp) || null;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function parseSimpleSkillFrontmatter(content = '') {
|
|
1402
|
+
const normalized = String(content || '').replace(/\r\n/g, '\n');
|
|
1403
|
+
if (!normalized.startsWith('---\n')) {
|
|
1404
|
+
return {};
|
|
863
1405
|
}
|
|
864
|
-
|
|
1406
|
+
const endIndex = normalized.indexOf('\n---\n', 4);
|
|
1407
|
+
if (endIndex <= 4) {
|
|
1408
|
+
return {};
|
|
1409
|
+
}
|
|
1410
|
+
const frontmatterRaw = normalized.slice(4, endIndex);
|
|
1411
|
+
const result = {};
|
|
1412
|
+
const lines = frontmatterRaw.split('\n');
|
|
1413
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
1414
|
+
const line = lines[lineIndex];
|
|
1415
|
+
const trimmed = line.trim();
|
|
1416
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
1417
|
+
const matched = trimmed.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
1418
|
+
if (!matched) continue;
|
|
1419
|
+
const key = matched[1];
|
|
1420
|
+
let value = matched[2] || '';
|
|
1421
|
+
const indicator = value.trim();
|
|
1422
|
+
if (/^[>|]/.test(indicator)) {
|
|
1423
|
+
const blockLines = [];
|
|
1424
|
+
let cursor = lineIndex + 1;
|
|
1425
|
+
while (cursor < lines.length) {
|
|
1426
|
+
const candidateLine = lines[cursor];
|
|
1427
|
+
if (!candidateLine.trim()) {
|
|
1428
|
+
blockLines.push('');
|
|
1429
|
+
cursor += 1;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (/^\s/.test(candidateLine)) {
|
|
1433
|
+
blockLines.push(candidateLine);
|
|
1434
|
+
cursor += 1;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
lineIndex = cursor - 1;
|
|
1440
|
+
const indents = blockLines
|
|
1441
|
+
.filter((item) => item.trim())
|
|
1442
|
+
.map((item) => {
|
|
1443
|
+
const indentMatch = item.match(/^[ \t]*/);
|
|
1444
|
+
return indentMatch ? indentMatch[0].length : 0;
|
|
1445
|
+
});
|
|
1446
|
+
const commonIndent = indents.length ? Math.min(...indents) : 0;
|
|
1447
|
+
const deindented = blockLines.map((item) => {
|
|
1448
|
+
if (!item.trim()) return '';
|
|
1449
|
+
return item.slice(commonIndent);
|
|
1450
|
+
});
|
|
1451
|
+
if (indicator.startsWith('>')) {
|
|
1452
|
+
const paragraphs = [];
|
|
1453
|
+
let paragraphLines = [];
|
|
1454
|
+
for (const blockLine of deindented) {
|
|
1455
|
+
const blockTrimmed = blockLine.trim();
|
|
1456
|
+
if (!blockTrimmed) {
|
|
1457
|
+
if (paragraphLines.length) {
|
|
1458
|
+
paragraphs.push(paragraphLines.join(' '));
|
|
1459
|
+
paragraphLines = [];
|
|
1460
|
+
}
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
paragraphLines.push(blockTrimmed);
|
|
1464
|
+
}
|
|
1465
|
+
if (paragraphLines.length) {
|
|
1466
|
+
paragraphs.push(paragraphLines.join(' '));
|
|
1467
|
+
}
|
|
1468
|
+
value = paragraphs.join('\n');
|
|
1469
|
+
} else {
|
|
1470
|
+
value = deindented.join('\n');
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
1474
|
+
value = value.slice(1, -1);
|
|
1475
|
+
}
|
|
1476
|
+
result[key] = value.trim();
|
|
1477
|
+
}
|
|
1478
|
+
return result;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function stripMarkdownFrontmatter(content = '') {
|
|
1482
|
+
const normalized = String(content || '').replace(/\r\n/g, '\n');
|
|
1483
|
+
if (!normalized.startsWith('---\n')) {
|
|
1484
|
+
return normalized;
|
|
1485
|
+
}
|
|
1486
|
+
const endIndex = normalized.indexOf('\n---\n', 4);
|
|
1487
|
+
if (endIndex <= 4) {
|
|
1488
|
+
return normalized;
|
|
1489
|
+
}
|
|
1490
|
+
return normalized.slice(endIndex + 5);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function extractSkillDescriptionFromMarkdown(content = '') {
|
|
1494
|
+
const normalized = String(content || '').replace(/\r\n/g, '\n');
|
|
1495
|
+
const lines = normalized.split('\n');
|
|
1496
|
+
let inFence = false;
|
|
1497
|
+
for (const line of lines) {
|
|
1498
|
+
const trimmedStart = line.trimStart();
|
|
1499
|
+
if (trimmedStart.startsWith('```')) {
|
|
1500
|
+
inFence = !inFence;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (inFence) continue;
|
|
1504
|
+
if (/^( {4}|\t)/.test(line)) continue;
|
|
1505
|
+
const trimmed = line.trim();
|
|
1506
|
+
if (!trimmed) continue;
|
|
1507
|
+
if (trimmed.startsWith('#')) continue;
|
|
1508
|
+
if (trimmed.startsWith('---')) continue;
|
|
1509
|
+
if (/^([A-Za-z0-9_-]+)\s*:\s*/.test(trimmed)) continue;
|
|
1510
|
+
return trimmed.slice(0, 200);
|
|
1511
|
+
}
|
|
1512
|
+
return '';
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function readCodexSkillMetadata(skillPath) {
|
|
1516
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
1517
|
+
if (!fs.existsSync(skillFile)) {
|
|
1518
|
+
return {
|
|
1519
|
+
hasSkillFile: false,
|
|
1520
|
+
displayName: '',
|
|
1521
|
+
description: ''
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
const raw = fs.readFileSync(skillFile, 'utf-8');
|
|
1526
|
+
const content = stripUtf8Bom(raw);
|
|
1527
|
+
const frontmatter = parseSimpleSkillFrontmatter(content);
|
|
1528
|
+
const contentWithoutFrontmatter = stripMarkdownFrontmatter(content);
|
|
1529
|
+
const heading = contentWithoutFrontmatter.match(/^\s*#\s+(.+)$/m);
|
|
1530
|
+
const displayName = typeof frontmatter.name === 'string' && frontmatter.name.trim()
|
|
1531
|
+
? frontmatter.name.trim()
|
|
1532
|
+
: (heading && heading[1] ? heading[1].trim() : '');
|
|
1533
|
+
const description = typeof frontmatter.description === 'string' && frontmatter.description.trim()
|
|
1534
|
+
? frontmatter.description.trim().slice(0, 200)
|
|
1535
|
+
: extractSkillDescriptionFromMarkdown(contentWithoutFrontmatter);
|
|
1536
|
+
return {
|
|
1537
|
+
hasSkillFile: true,
|
|
1538
|
+
displayName,
|
|
1539
|
+
description
|
|
1540
|
+
};
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
return {
|
|
1543
|
+
hasSkillFile: false,
|
|
1544
|
+
displayName: '',
|
|
1545
|
+
description: ''
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function getCodexSkillEntryInfoByName(entryName) {
|
|
1551
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
|
|
1552
|
+
const normalized = normalizeCodexSkillName(entryName);
|
|
1553
|
+
if (normalized.error) {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
const relativePath = path.relative(CODEX_SKILLS_DIR, targetPath);
|
|
1557
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
try {
|
|
1562
|
+
const lstat = fs.lstatSync(targetPath);
|
|
1563
|
+
const isSymbolicLink = lstat.isSymbolicLink();
|
|
1564
|
+
if (!lstat.isDirectory() && !isSymbolicLink) {
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
if (isSymbolicLink && !isSkillDirectoryEntry(entryName)) {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
const metadata = readCodexSkillMetadata(targetPath);
|
|
1571
|
+
return {
|
|
1572
|
+
name: entryName,
|
|
1573
|
+
path: targetPath,
|
|
1574
|
+
hasSkillFile: !!metadata.hasSkillFile,
|
|
1575
|
+
displayName: metadata.displayName || entryName,
|
|
1576
|
+
description: metadata.description || '',
|
|
1577
|
+
sourceType: isSymbolicLink ? 'symlink' : 'directory',
|
|
1578
|
+
updatedAt: Number.isFinite(lstat.mtimeMs) ? Math.floor(lstat.mtimeMs) : 0
|
|
1579
|
+
};
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function listCodexSkills() {
|
|
1586
|
+
if (!fs.existsSync(CODEX_SKILLS_DIR)) {
|
|
1587
|
+
return {
|
|
1588
|
+
root: CODEX_SKILLS_DIR,
|
|
1589
|
+
exists: false,
|
|
1590
|
+
items: []
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
const entries = fs.readdirSync(CODEX_SKILLS_DIR, { withFileTypes: true });
|
|
1595
|
+
const items = entries
|
|
1596
|
+
.map((entry) => {
|
|
1597
|
+
const name = entry && entry.name ? entry.name : '';
|
|
1598
|
+
if (!name || name.startsWith('.')) return null;
|
|
1599
|
+
return getCodexSkillEntryInfoByName(name);
|
|
1600
|
+
})
|
|
1601
|
+
.filter(Boolean)
|
|
1602
|
+
.sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
|
|
1603
|
+
return {
|
|
1604
|
+
root: CODEX_SKILLS_DIR,
|
|
1605
|
+
exists: true,
|
|
1606
|
+
items
|
|
1607
|
+
};
|
|
1608
|
+
} catch (e) {
|
|
1609
|
+
return { error: `读取 skills 目录失败: ${e.message}` };
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function listSkillEntriesByRoot(rootDir) {
|
|
1614
|
+
if (!rootDir || !fs.existsSync(rootDir)) {
|
|
1615
|
+
return [];
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
1619
|
+
return entries
|
|
1620
|
+
.map((entry) => {
|
|
1621
|
+
const name = entry && entry.name ? entry.name : '';
|
|
1622
|
+
if (!name || name.startsWith('.')) return null;
|
|
1623
|
+
const normalized = normalizeCodexSkillName(name);
|
|
1624
|
+
if (normalized.error) return null;
|
|
1625
|
+
const skillPath = path.join(rootDir, name);
|
|
1626
|
+
const relativePath = path.relative(rootDir, skillPath);
|
|
1627
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
try {
|
|
1631
|
+
const lstat = fs.lstatSync(skillPath);
|
|
1632
|
+
const isSymbolicLink = lstat.isSymbolicLink();
|
|
1633
|
+
if (!lstat.isDirectory() && !isSymbolicLink) {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
if (isSymbolicLink) {
|
|
1637
|
+
const realPath = fs.realpathSync(skillPath);
|
|
1638
|
+
const realStat = fs.statSync(realPath);
|
|
1639
|
+
if (!realStat.isDirectory()) {
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
name,
|
|
1645
|
+
path: skillPath,
|
|
1646
|
+
sourceType: isSymbolicLink ? 'symlink' : 'directory'
|
|
1647
|
+
};
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
})
|
|
1652
|
+
.filter(Boolean);
|
|
1653
|
+
} catch (e) {
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function scanUnmanagedCodexSkills() {
|
|
1659
|
+
const existing = listCodexSkills();
|
|
1660
|
+
if (existing.error) {
|
|
1661
|
+
return { error: existing.error };
|
|
1662
|
+
}
|
|
1663
|
+
const existingNames = new Set((Array.isArray(existing.items) ? existing.items : [])
|
|
1664
|
+
.map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
|
|
1665
|
+
.filter(Boolean));
|
|
1666
|
+
|
|
1667
|
+
const items = [];
|
|
1668
|
+
for (const source of SKILL_IMPORT_SOURCES) {
|
|
1669
|
+
const sourceEntries = listSkillEntriesByRoot(source.dir);
|
|
1670
|
+
for (const entry of sourceEntries) {
|
|
1671
|
+
if (existingNames.has(entry.name)) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
const metadata = readCodexSkillMetadata(entry.path);
|
|
1675
|
+
items.push({
|
|
1676
|
+
key: `${source.app}:${entry.name}`,
|
|
1677
|
+
name: entry.name,
|
|
1678
|
+
displayName: metadata.displayName || entry.name,
|
|
1679
|
+
description: metadata.description || '',
|
|
1680
|
+
sourceApp: source.app,
|
|
1681
|
+
sourceLabel: source.label,
|
|
1682
|
+
sourcePath: entry.path,
|
|
1683
|
+
sourceType: entry.sourceType,
|
|
1684
|
+
hasSkillFile: !!metadata.hasSkillFile
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
items.sort((a, b) => {
|
|
1690
|
+
const nameCompare = a.displayName.localeCompare(b.displayName, 'zh-Hans-CN');
|
|
1691
|
+
if (nameCompare !== 0) return nameCompare;
|
|
1692
|
+
return a.sourceLabel.localeCompare(b.sourceLabel, 'zh-Hans-CN');
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
return {
|
|
1696
|
+
root: CODEX_SKILLS_DIR,
|
|
1697
|
+
items,
|
|
1698
|
+
sources: SKILL_IMPORT_SOURCES.map((source) => ({
|
|
1699
|
+
app: source.app,
|
|
1700
|
+
label: source.label,
|
|
1701
|
+
path: source.dir,
|
|
1702
|
+
exists: fs.existsSync(source.dir)
|
|
1703
|
+
}))
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function importCodexSkills(params = {}) {
|
|
1708
|
+
const rawItems = Array.isArray(params.items) ? params.items : [];
|
|
1709
|
+
if (!rawItems.length) {
|
|
1710
|
+
return { error: '请先选择要导入的 skill' };
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
ensureDir(CODEX_SKILLS_DIR);
|
|
1714
|
+
|
|
1715
|
+
const imported = [];
|
|
1716
|
+
const failed = [];
|
|
1717
|
+
const dedup = new Set();
|
|
1718
|
+
|
|
1719
|
+
for (const rawItem of rawItems) {
|
|
1720
|
+
const safeItem = rawItem && typeof rawItem === 'object' ? rawItem : {};
|
|
1721
|
+
const normalizedName = normalizeCodexSkillName(safeItem.name);
|
|
1722
|
+
if (normalizedName.error) {
|
|
1723
|
+
failed.push({
|
|
1724
|
+
name: safeItem && safeItem.name ? String(safeItem.name) : '',
|
|
1725
|
+
sourceApp: safeItem && safeItem.sourceApp ? String(safeItem.sourceApp) : '',
|
|
1726
|
+
error: normalizedName.error
|
|
1727
|
+
});
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const source = getSkillImportSourceByApp(safeItem.sourceApp);
|
|
1731
|
+
if (!source) {
|
|
1732
|
+
failed.push({
|
|
1733
|
+
name: normalizedName.name,
|
|
1734
|
+
sourceApp: safeItem && safeItem.sourceApp ? String(safeItem.sourceApp) : '',
|
|
1735
|
+
error: '来源应用不支持'
|
|
1736
|
+
});
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
const dedupKey = `${source.app}:${normalizedName.name}`;
|
|
1740
|
+
if (dedup.has(dedupKey)) {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
dedup.add(dedupKey);
|
|
1744
|
+
|
|
1745
|
+
const sourcePath = path.join(source.dir, normalizedName.name);
|
|
1746
|
+
const sourceRelative = path.relative(source.dir, sourcePath);
|
|
1747
|
+
if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
|
|
1748
|
+
failed.push({
|
|
1749
|
+
name: normalizedName.name,
|
|
1750
|
+
sourceApp: source.app,
|
|
1751
|
+
error: '来源路径非法'
|
|
1752
|
+
});
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1756
|
+
failed.push({
|
|
1757
|
+
name: normalizedName.name,
|
|
1758
|
+
sourceApp: source.app,
|
|
1759
|
+
error: '来源 skill 不存在'
|
|
1760
|
+
});
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
|
|
1765
|
+
const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
|
|
1766
|
+
if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
|
|
1767
|
+
failed.push({
|
|
1768
|
+
name: normalizedName.name,
|
|
1769
|
+
sourceApp: source.app,
|
|
1770
|
+
error: '目标路径非法'
|
|
1771
|
+
});
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (fs.existsSync(targetPath)) {
|
|
1775
|
+
failed.push({
|
|
1776
|
+
name: normalizedName.name,
|
|
1777
|
+
sourceApp: source.app,
|
|
1778
|
+
error: 'Codex 中已存在同名 skill'
|
|
1779
|
+
});
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
let copiedToTarget = false;
|
|
1784
|
+
try {
|
|
1785
|
+
const lstat = fs.lstatSync(sourcePath);
|
|
1786
|
+
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) {
|
|
1787
|
+
failed.push({
|
|
1788
|
+
name: normalizedName.name,
|
|
1789
|
+
sourceApp: source.app,
|
|
1790
|
+
error: '来源不是技能目录'
|
|
1791
|
+
});
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
const sourceDirForCopy = lstat.isSymbolicLink() ? fs.realpathSync(sourcePath) : sourcePath;
|
|
1795
|
+
const sourceStat = fs.statSync(sourceDirForCopy);
|
|
1796
|
+
if (!sourceStat.isDirectory()) {
|
|
1797
|
+
failed.push({
|
|
1798
|
+
name: normalizedName.name,
|
|
1799
|
+
sourceApp: source.app,
|
|
1800
|
+
error: '来源 skill 无法读取'
|
|
1801
|
+
});
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
const visitedRealPaths = new Set([sourceDirForCopy]);
|
|
1805
|
+
copyDirRecursive(sourceDirForCopy, targetPath, {
|
|
1806
|
+
dereferenceSymlinks: true,
|
|
1807
|
+
visitedRealPaths
|
|
1808
|
+
});
|
|
1809
|
+
copiedToTarget = true;
|
|
1810
|
+
imported.push({
|
|
1811
|
+
name: normalizedName.name,
|
|
1812
|
+
sourceApp: source.app,
|
|
1813
|
+
sourceLabel: source.label,
|
|
1814
|
+
path: targetPath
|
|
1815
|
+
});
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
if (!copiedToTarget && fs.existsSync(targetPath)) {
|
|
1818
|
+
try {
|
|
1819
|
+
removeDirectoryRecursive(targetPath);
|
|
1820
|
+
} catch (_) {}
|
|
1821
|
+
}
|
|
1822
|
+
failed.push({
|
|
1823
|
+
name: normalizedName.name,
|
|
1824
|
+
sourceApp: source.app,
|
|
1825
|
+
error: e && e.message ? e.message : '导入失败'
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
return {
|
|
1831
|
+
success: failed.length === 0,
|
|
1832
|
+
imported,
|
|
1833
|
+
failed,
|
|
1834
|
+
root: CODEX_SKILLS_DIR
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function removeDirectoryRecursive(targetPath) {
|
|
1839
|
+
if (typeof fs.rmSync === 'function') {
|
|
1840
|
+
fs.rmSync(targetPath, { recursive: true, force: false });
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
fs.rmdirSync(targetPath, { recursive: true });
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function deleteCodexSkills(params = {}) {
|
|
1847
|
+
const rawList = Array.isArray(params.names) ? params.names : [];
|
|
1848
|
+
const uniqueNames = Array.from(new Set(rawList
|
|
1849
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
1850
|
+
.filter(Boolean)));
|
|
1851
|
+
if (!uniqueNames.length) {
|
|
1852
|
+
return { error: '请先选择要删除的 skill' };
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const deleted = [];
|
|
1856
|
+
const failed = [];
|
|
1857
|
+
for (const rawName of uniqueNames) {
|
|
1858
|
+
const normalized = normalizeCodexSkillName(rawName);
|
|
1859
|
+
if (normalized.error) {
|
|
1860
|
+
failed.push({ name: rawName, error: normalized.error });
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const skillPath = path.join(CODEX_SKILLS_DIR, normalized.name);
|
|
1865
|
+
const relativePath = path.relative(CODEX_SKILLS_DIR, skillPath);
|
|
1866
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1867
|
+
failed.push({ name: normalized.name, error: '技能路径非法' });
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
if (!fs.existsSync(skillPath)) {
|
|
1871
|
+
failed.push({ name: normalized.name, error: 'skill 不存在' });
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
try {
|
|
1876
|
+
const stat = fs.lstatSync(skillPath);
|
|
1877
|
+
if (!stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
1878
|
+
failed.push({ name: normalized.name, error: '仅支持删除技能目录' });
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
removeDirectoryRecursive(skillPath);
|
|
1882
|
+
deleted.push(normalized.name);
|
|
1883
|
+
} catch (e) {
|
|
1884
|
+
failed.push({
|
|
1885
|
+
name: normalized.name,
|
|
1886
|
+
error: e && e.message ? e.message : '删除失败'
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
return {
|
|
1892
|
+
success: failed.length === 0,
|
|
1893
|
+
deleted,
|
|
1894
|
+
failed,
|
|
1895
|
+
root: CODEX_SKILLS_DIR
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function readAgentsFile(params = {}) {
|
|
1900
|
+
const filePath = resolveAgentsFilePath(params);
|
|
1901
|
+
const dirCheck = validateAgentsBaseDir(filePath);
|
|
1902
|
+
if (dirCheck.error) {
|
|
1903
|
+
return { error: dirCheck.error };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (!fs.existsSync(filePath)) {
|
|
1907
|
+
return {
|
|
1908
|
+
exists: false,
|
|
1909
|
+
path: filePath,
|
|
1910
|
+
content: '',
|
|
1911
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
try {
|
|
1916
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
1917
|
+
return {
|
|
1918
|
+
exists: true,
|
|
1919
|
+
path: filePath,
|
|
1920
|
+
content: stripUtf8Bom(raw),
|
|
1921
|
+
lineEnding: detectLineEnding(raw)
|
|
1922
|
+
};
|
|
1923
|
+
} catch (e) {
|
|
1924
|
+
return { error: `读取 AGENTS.md 失败: ${e.message}` };
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function applyAgentsFile(params = {}) {
|
|
1929
|
+
const filePath = resolveAgentsFilePath(params);
|
|
1930
|
+
const dirCheck = validateAgentsBaseDir(filePath);
|
|
1931
|
+
if (dirCheck.error) {
|
|
1932
|
+
return { error: dirCheck.error };
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
1936
|
+
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
1937
|
+
const normalized = normalizeLineEnding(content, lineEnding);
|
|
1938
|
+
const finalContent = ensureUtf8Bom(normalized);
|
|
1939
|
+
|
|
1940
|
+
try {
|
|
1941
|
+
fs.writeFileSync(filePath, finalContent, 'utf-8');
|
|
1942
|
+
return { success: true, path: filePath };
|
|
1943
|
+
} catch (e) {
|
|
1944
|
+
return { error: `写入 AGENTS.md 失败: ${e.message}` };
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function resolveOpenclawWorkspaceDir(config) {
|
|
1949
|
+
const workspace = config
|
|
1950
|
+
&& config.agents
|
|
1951
|
+
&& config.agents.defaults
|
|
1952
|
+
&& typeof config.agents.defaults.workspace === 'string'
|
|
1953
|
+
? config.agents.defaults.workspace
|
|
1954
|
+
: '';
|
|
1955
|
+
const resolved = resolveHomePath(workspace);
|
|
1956
|
+
if (!resolved) {
|
|
1957
|
+
return OPENCLAW_WORKSPACE_DIR;
|
|
1958
|
+
}
|
|
1959
|
+
if (path.isAbsolute(resolved)) {
|
|
1960
|
+
return resolved;
|
|
1961
|
+
}
|
|
1962
|
+
return path.join(OPENCLAW_DIR, resolved);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function normalizeOpenclawWorkspaceFileName(input) {
|
|
1966
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
1967
|
+
if (!raw) {
|
|
1968
|
+
return { error: '文件名不能为空' };
|
|
1969
|
+
}
|
|
1970
|
+
if (raw.includes('\0')) {
|
|
865
1971
|
return { error: '文件名非法' };
|
|
866
1972
|
}
|
|
867
1973
|
if (raw.includes('/') || raw.includes('\\') || raw.includes('..')) {
|
|
@@ -1307,11 +2413,28 @@ async function buildConfigHealthReport(params = {}) {
|
|
|
1307
2413
|
const config = status.config || {};
|
|
1308
2414
|
|
|
1309
2415
|
if (status.isVirtual) {
|
|
2416
|
+
const parseFailed = status.errorType === 'parse';
|
|
2417
|
+
const readFailed = status.errorType === 'read';
|
|
1310
2418
|
issues.push({
|
|
1311
|
-
code: 'config-missing',
|
|
1312
|
-
message: status.reason ||
|
|
1313
|
-
|
|
2419
|
+
code: parseFailed ? 'config-parse-failed' : (readFailed ? 'config-read-failed' : 'config-missing'),
|
|
2420
|
+
message: status.reason || (parseFailed
|
|
2421
|
+
? 'config.toml 解析失败'
|
|
2422
|
+
: (readFailed ? '读取 config.toml 失败' : '未检测到 config.toml')),
|
|
2423
|
+
suggestion: parseFailed
|
|
2424
|
+
? '修复 config.toml 语法错误后重试'
|
|
2425
|
+
: (readFailed ? '检查文件权限后重试' : '在模板编辑器中确认应用配置,生成可用的 config.toml')
|
|
1314
2426
|
});
|
|
2427
|
+
if (parseFailed || readFailed) {
|
|
2428
|
+
return {
|
|
2429
|
+
ok: false,
|
|
2430
|
+
issues,
|
|
2431
|
+
summary: {
|
|
2432
|
+
currentProvider: '',
|
|
2433
|
+
currentModel: ''
|
|
2434
|
+
},
|
|
2435
|
+
remote: null
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
1315
2438
|
}
|
|
1316
2439
|
|
|
1317
2440
|
const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
@@ -1498,13 +2621,26 @@ function readConfigOrVirtualDefault() {
|
|
|
1498
2621
|
return {
|
|
1499
2622
|
config: readConfig(),
|
|
1500
2623
|
isVirtual: false,
|
|
1501
|
-
reason: ''
|
|
2624
|
+
reason: '',
|
|
2625
|
+
detail: '',
|
|
2626
|
+
errorType: ''
|
|
1502
2627
|
};
|
|
1503
2628
|
} catch (e) {
|
|
2629
|
+
const errorType = typeof e.configErrorType === 'string' && e.configErrorType.trim()
|
|
2630
|
+
? e.configErrorType.trim()
|
|
2631
|
+
: 'read';
|
|
2632
|
+
const publicReason = typeof e.configPublicReason === 'string' && e.configPublicReason.trim()
|
|
2633
|
+
? e.configPublicReason.trim()
|
|
2634
|
+
: (errorType === 'parse' ? 'config.toml 解析失败' : '读取 config.toml 失败');
|
|
2635
|
+
const detail = typeof e.configDetail === 'string' && e.configDetail.trim()
|
|
2636
|
+
? e.configDetail.trim()
|
|
2637
|
+
: (e && e.message ? e.message : publicReason);
|
|
1504
2638
|
return {
|
|
1505
|
-
config: buildVirtualDefaultConfig(),
|
|
2639
|
+
config: errorType === 'missing' ? buildVirtualDefaultConfig() : {},
|
|
1506
2640
|
isVirtual: true,
|
|
1507
|
-
reason:
|
|
2641
|
+
reason: publicReason,
|
|
2642
|
+
detail,
|
|
2643
|
+
errorType
|
|
1508
2644
|
};
|
|
1509
2645
|
}
|
|
1510
2646
|
}
|
|
@@ -1512,10 +2648,31 @@ function readConfigOrVirtualDefault() {
|
|
|
1512
2648
|
return {
|
|
1513
2649
|
config: buildVirtualDefaultConfig(),
|
|
1514
2650
|
isVirtual: true,
|
|
1515
|
-
reason:
|
|
2651
|
+
reason: '未检测到 config.toml',
|
|
2652
|
+
detail: `配置文件不存在: ${CONFIG_FILE}`,
|
|
2653
|
+
errorType: 'missing'
|
|
1516
2654
|
};
|
|
1517
2655
|
}
|
|
1518
2656
|
|
|
2657
|
+
function hasConfigLoadError(result) {
|
|
2658
|
+
return !!(result
|
|
2659
|
+
&& result.isVirtual
|
|
2660
|
+
&& (result.errorType === 'parse' || result.errorType === 'read'));
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function printConfigLoadErrorAndMarkExit(result) {
|
|
2664
|
+
const isReadError = result && result.errorType === 'read';
|
|
2665
|
+
const detail = result && typeof result.detail === 'string' && result.detail.trim()
|
|
2666
|
+
? result.detail.trim()
|
|
2667
|
+
: (isReadError ? '读取配置文件失败' : '配置文件解析失败');
|
|
2668
|
+
console.error(`\n错误: ${isReadError ? '读取 config.toml 失败' : '配置文件解析失败'}`);
|
|
2669
|
+
console.error(` 详情: ${detail}`);
|
|
2670
|
+
console.error(` 路径: ${CONFIG_FILE}`);
|
|
2671
|
+
console.error(` 建议: ${isReadError ? '检查文件权限后重试' : '修复 config.toml 语法后重试'}`);
|
|
2672
|
+
console.error();
|
|
2673
|
+
process.exitCode = 1;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
1519
2676
|
function normalizeTopLevelConfigWithTemplate(template, selectedProvider, selectedModel) {
|
|
1520
2677
|
let content = typeof template === 'string' ? template : '';
|
|
1521
2678
|
if (!content.trim()) {
|
|
@@ -1656,6 +2813,9 @@ function addProviderToConfig(params = {}) {
|
|
|
1656
2813
|
|
|
1657
2814
|
if (!name) return { error: '名称不能为空' };
|
|
1658
2815
|
if (!url) return { error: 'URL 不能为空' };
|
|
2816
|
+
if (!isValidProviderName(name)) {
|
|
2817
|
+
return { error: '名称仅支持字母/数字/._-' };
|
|
2818
|
+
}
|
|
1659
2819
|
if (isReservedProviderNameForCreation(name)) {
|
|
1660
2820
|
return { error: 'local provider 为系统保留名称,不可新增' };
|
|
1661
2821
|
}
|
|
@@ -1687,24 +2847,20 @@ function addProviderToConfig(params = {}) {
|
|
|
1687
2847
|
return { error: `config.toml 解析失败: ${e.message}` };
|
|
1688
2848
|
}
|
|
1689
2849
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if (
|
|
2850
|
+
const providerHeaderSegmentKeySet = collectModelProviderHeaderSegmentKeySet(content);
|
|
2851
|
+
const normalizedProviders = isPlainObject(parsed.model_providers)
|
|
2852
|
+
? normalizeLegacyModelProviders(parsed.model_providers, providerHeaderSegmentKeySet)
|
|
2853
|
+
: {};
|
|
2854
|
+
if (normalizedProviders && normalizedProviders[name]) {
|
|
1695
2855
|
return { error: '提供商已存在' };
|
|
1696
2856
|
}
|
|
1697
2857
|
|
|
1698
|
-
const escapeTomlString = (value) => String(value || '')
|
|
1699
|
-
.replace(/\\/g, '\\\\')
|
|
1700
|
-
.replace(/"/g, '\\"');
|
|
1701
|
-
|
|
1702
2858
|
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
1703
|
-
const safeName =
|
|
1704
|
-
const safeUrl =
|
|
1705
|
-
const safeKey =
|
|
2859
|
+
const safeName = escapeTomlBasicString(name);
|
|
2860
|
+
const safeUrl = escapeTomlBasicString(url);
|
|
2861
|
+
const safeKey = escapeTomlBasicString(key);
|
|
1706
2862
|
const block = [
|
|
1707
|
-
|
|
2863
|
+
buildModelProviderTableHeader(name),
|
|
1708
2864
|
`name = "${safeName}"`,
|
|
1709
2865
|
`base_url = "${safeUrl}"`,
|
|
1710
2866
|
`wire_api = "responses"`,
|
|
@@ -1804,8 +2960,36 @@ function performProviderDeletion(name, options = {}) {
|
|
|
1804
2960
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1805
2961
|
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
1806
2962
|
const hasBom = content.charCodeAt(0) === 0xFEFF;
|
|
1807
|
-
const
|
|
1808
|
-
const
|
|
2963
|
+
const providerConfig = config.model_providers[name];
|
|
2964
|
+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
|
|
2965
|
+
? providerConfig.__codexmate_legacy_segments
|
|
2966
|
+
: null;
|
|
2967
|
+
const providerSegmentVariants = (() => {
|
|
2968
|
+
const variants = [];
|
|
2969
|
+
const seen = new Set();
|
|
2970
|
+
const pushVariant = (segments) => {
|
|
2971
|
+
const normalized = normalizeLegacySegments(segments);
|
|
2972
|
+
const key = buildLegacySegmentsKey(normalized);
|
|
2973
|
+
if (!key || seen.has(key)) return;
|
|
2974
|
+
seen.add(key);
|
|
2975
|
+
variants.push(normalized);
|
|
2976
|
+
};
|
|
2977
|
+
if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
|
|
2978
|
+
pushVariant(providerConfig.__codexmate_legacy_segments);
|
|
2979
|
+
}
|
|
2980
|
+
if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
|
|
2981
|
+
for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
|
|
2982
|
+
pushVariant(segments);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
if (providerSegments) {
|
|
2986
|
+
pushVariant(providerSegments);
|
|
2987
|
+
}
|
|
2988
|
+
if (variants.length === 0) {
|
|
2989
|
+
pushVariant(String(name || '').split('.').filter((item) => item));
|
|
2990
|
+
}
|
|
2991
|
+
return variants;
|
|
2992
|
+
})();
|
|
1809
2993
|
|
|
1810
2994
|
const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
|
|
1811
2995
|
if (remainingProviders.length === 0) {
|
|
@@ -1844,17 +3028,25 @@ function performProviderDeletion(name, options = {}) {
|
|
|
1844
3028
|
};
|
|
1845
3029
|
|
|
1846
3030
|
let updatedContent = null;
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
3031
|
+
const combinedRanges = [];
|
|
3032
|
+
for (const segments of providerSegmentVariants) {
|
|
3033
|
+
combinedRanges.push(...findProviderSectionRanges(content, name, segments));
|
|
3034
|
+
combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
|
|
3035
|
+
}
|
|
3036
|
+
if (combinedRanges.length === 0) {
|
|
3037
|
+
combinedRanges.push(...findProviderSectionRanges(content, name, providerSegments));
|
|
3038
|
+
}
|
|
3039
|
+
if (combinedRanges.length > 0) {
|
|
3040
|
+
const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
|
|
3041
|
+
const seen = new Set();
|
|
3042
|
+
let removedContent = content;
|
|
3043
|
+
for (const range of sorted) {
|
|
3044
|
+
const rangeKey = `${range.start}:${range.end}`;
|
|
3045
|
+
if (seen.has(rangeKey)) continue;
|
|
3046
|
+
seen.add(rangeKey);
|
|
3047
|
+
removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
|
|
3048
|
+
}
|
|
3049
|
+
updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
|
|
1858
3050
|
}
|
|
1859
3051
|
|
|
1860
3052
|
if (updatedContent) {
|
|
@@ -4857,7 +6049,12 @@ async function cmdSetup() {
|
|
|
4857
6049
|
|
|
4858
6050
|
// 显示当前状态
|
|
4859
6051
|
function cmdStatus() {
|
|
4860
|
-
const
|
|
6052
|
+
const configResult = readConfigOrVirtualDefault();
|
|
6053
|
+
if (hasConfigLoadError(configResult)) {
|
|
6054
|
+
printConfigLoadErrorAndMarkExit(configResult);
|
|
6055
|
+
return;
|
|
6056
|
+
}
|
|
6057
|
+
const { config, isVirtual } = configResult;
|
|
4861
6058
|
const current = config.model_provider || '未设置';
|
|
4862
6059
|
const currentModel = config.model || '未设置';
|
|
4863
6060
|
|
|
@@ -4873,7 +6070,12 @@ function cmdStatus() {
|
|
|
4873
6070
|
|
|
4874
6071
|
// 列出所有提供商
|
|
4875
6072
|
function cmdList() {
|
|
4876
|
-
const
|
|
6073
|
+
const configResult = readConfigOrVirtualDefault();
|
|
6074
|
+
if (hasConfigLoadError(configResult)) {
|
|
6075
|
+
printConfigLoadErrorAndMarkExit(configResult);
|
|
6076
|
+
return;
|
|
6077
|
+
}
|
|
6078
|
+
const { config, isVirtual } = configResult;
|
|
4877
6079
|
const providers = config.model_providers || {};
|
|
4878
6080
|
const current = config.model_provider;
|
|
4879
6081
|
|
|
@@ -5028,6 +6230,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
5028
6230
|
}
|
|
5029
6231
|
throw new Error('名称和URL必填');
|
|
5030
6232
|
}
|
|
6233
|
+
if (!isValidProviderName(providerName)) {
|
|
6234
|
+
if (!silent) console.error('错误: 名称仅支持字母/数字/._-');
|
|
6235
|
+
throw new Error('名称仅支持字母/数字/._-');
|
|
6236
|
+
}
|
|
5031
6237
|
if (isReservedProviderNameForCreation(providerName)) {
|
|
5032
6238
|
if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
|
|
5033
6239
|
throw new Error('local provider 为系统保留名称,不可新增');
|
|
@@ -5039,13 +6245,16 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
5039
6245
|
throw new Error('提供商已存在');
|
|
5040
6246
|
}
|
|
5041
6247
|
|
|
6248
|
+
const safeName = escapeTomlBasicString(providerName);
|
|
6249
|
+
const safeBaseUrl = escapeTomlBasicString(providerBaseUrl);
|
|
6250
|
+
const safeApiKey = escapeTomlBasicString(apiKey || '');
|
|
5042
6251
|
const newBlock = `
|
|
5043
|
-
|
|
5044
|
-
name = "${
|
|
5045
|
-
base_url = "${
|
|
6252
|
+
${buildModelProviderTableHeader(providerName)}
|
|
6253
|
+
name = "${safeName}"
|
|
6254
|
+
base_url = "${safeBaseUrl}"
|
|
5046
6255
|
wire_api = "responses"
|
|
5047
6256
|
requires_openai_auth = false
|
|
5048
|
-
preferred_auth_method = "${
|
|
6257
|
+
preferred_auth_method = "${safeApiKey}"
|
|
5049
6258
|
request_max_retries = 4
|
|
5050
6259
|
stream_max_retries = 10
|
|
5051
6260
|
stream_idle_timeout_ms = 300000
|
|
@@ -5105,42 +6314,156 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
|
5105
6314
|
}
|
|
5106
6315
|
|
|
5107
6316
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
5108
|
-
const
|
|
5109
|
-
const
|
|
5110
|
-
|
|
5111
|
-
|
|
6317
|
+
const providerConfig = config.model_providers[name];
|
|
6318
|
+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
|
|
6319
|
+
? providerConfig.__codexmate_legacy_segments
|
|
6320
|
+
: null;
|
|
6321
|
+
const ranges = findProviderSectionRanges(content, name, providerSegments);
|
|
6322
|
+
if (ranges.length === 0) {
|
|
5112
6323
|
if (!silent) console.error('错误: 无法找到提供商配置块');
|
|
5113
6324
|
throw new Error('无法找到提供商配置块');
|
|
5114
6325
|
}
|
|
5115
6326
|
|
|
5116
|
-
const
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
6327
|
+
const replaceTomlStringField = (block, fieldName, rawValue) => {
|
|
6328
|
+
const safeValue = escapeTomlBasicString(rawValue);
|
|
6329
|
+
const escapedFieldName = escapeRegex(fieldName);
|
|
6330
|
+
const multilineRanges = collectTomlMultilineStringRanges(block);
|
|
6331
|
+
const tripleStartRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)(\"\"\"|''')`, 'mg');
|
|
6332
|
+
let tripleStartMatch = null;
|
|
6333
|
+
let tripleCandidate;
|
|
6334
|
+
while ((tripleCandidate = tripleStartRegex.exec(block)) !== null) {
|
|
6335
|
+
if (isIndexInRanges(tripleCandidate.index, multilineRanges)) {
|
|
6336
|
+
continue;
|
|
6337
|
+
}
|
|
6338
|
+
tripleStartMatch = tripleCandidate;
|
|
6339
|
+
break;
|
|
6340
|
+
}
|
|
6341
|
+
if (tripleStartMatch) {
|
|
6342
|
+
const prefixStart = tripleStartMatch.index;
|
|
6343
|
+
const prefixEnd = prefixStart + tripleStartMatch[1].length;
|
|
6344
|
+
const tripleQuote = tripleStartMatch[2];
|
|
6345
|
+
const valueStart = prefixEnd + tripleQuote.length;
|
|
6346
|
+
const quoteChar = tripleQuote[0];
|
|
6347
|
+
let valueEnd = -1;
|
|
6348
|
+
let closingRunLength = 0;
|
|
6349
|
+
for (let i = valueStart; i < block.length; i++) {
|
|
6350
|
+
if (block[i] !== quoteChar) continue;
|
|
6351
|
+
let runEnd = i + 1;
|
|
6352
|
+
while (runEnd < block.length && block[runEnd] === quoteChar) {
|
|
6353
|
+
runEnd++;
|
|
6354
|
+
}
|
|
6355
|
+
const runLength = runEnd - i;
|
|
6356
|
+
if (runLength < tripleQuote.length) {
|
|
6357
|
+
i = runEnd - 1;
|
|
6358
|
+
continue;
|
|
6359
|
+
}
|
|
6360
|
+
if (tripleQuote === '"""') {
|
|
6361
|
+
let slashCount = 0;
|
|
6362
|
+
for (let j = i - 1; j >= valueStart && block[j] === '\\'; j--) {
|
|
6363
|
+
slashCount++;
|
|
6364
|
+
}
|
|
6365
|
+
if (slashCount % 2 !== 0) {
|
|
6366
|
+
continue;
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
valueEnd = i;
|
|
6370
|
+
closingRunLength = runLength;
|
|
6371
|
+
break;
|
|
6372
|
+
}
|
|
6373
|
+
if (valueEnd === -1) {
|
|
6374
|
+
throw new Error(`${fieldName} 使用了未闭合的多行 TOML 字符串,无法安全更新`);
|
|
6375
|
+
}
|
|
6376
|
+
const lineEndIndex = block.indexOf('\n', valueEnd + closingRunLength);
|
|
6377
|
+
let tailEnd = lineEndIndex === -1 ? block.length : lineEndIndex;
|
|
6378
|
+
if (lineEndIndex > 0 && block[lineEndIndex - 1] === '\r') {
|
|
6379
|
+
tailEnd = lineEndIndex - 1;
|
|
6380
|
+
}
|
|
6381
|
+
const tail = block.slice(valueEnd + closingRunLength, tailEnd);
|
|
6382
|
+
const tailMatch = tail.match(/^(\s+#.*)?\s*$/);
|
|
6383
|
+
if (!tailMatch) {
|
|
6384
|
+
throw new Error(`${fieldName} 多行字符串后的语法不受支持,无法安全更新`);
|
|
6385
|
+
}
|
|
6386
|
+
const commentSuffix = tailMatch[1] || '';
|
|
6387
|
+
const replacementLine = `${block.slice(prefixStart, prefixEnd)}"${safeValue}"${commentSuffix}`;
|
|
6388
|
+
return block.slice(0, prefixStart) + replacementLine + block.slice(tailEnd);
|
|
6389
|
+
}
|
|
5123
6390
|
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
updatedBlock = updatedBlock.replace(
|
|
5128
|
-
/^(base_url\s*=\s*)(["']).*?\2/m,
|
|
5129
|
-
`$1$2${baseUrl}$2`
|
|
6391
|
+
const withCommentRegex = new RegExp(
|
|
6392
|
+
`^(\\s*${escapedFieldName}\\s*=\\s*)(?:"(?:\\\\.|[^"\\\\])*"|'[^'\\n]*')(\\s+#.*)?$`,
|
|
6393
|
+
'mg'
|
|
5130
6394
|
);
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
6395
|
+
let replaced = false;
|
|
6396
|
+
let next = block.replace(
|
|
6397
|
+
withCommentRegex,
|
|
6398
|
+
(full, prefix, suffix = '', offset) => {
|
|
6399
|
+
if (replaced || isIndexInRanges(offset, multilineRanges)) {
|
|
6400
|
+
return full;
|
|
6401
|
+
}
|
|
6402
|
+
replaced = true;
|
|
6403
|
+
return `${prefix}"${safeValue}"${suffix}`;
|
|
6404
|
+
}
|
|
5138
6405
|
);
|
|
6406
|
+
if (!replaced) {
|
|
6407
|
+
const fallbackRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)(.*?)(\\s+#.*)?$`, 'mg');
|
|
6408
|
+
let fallbackReplaced = false;
|
|
6409
|
+
const multilineRangesForNext = collectTomlMultilineStringRanges(next);
|
|
6410
|
+
let fallbackMatch;
|
|
6411
|
+
let fallbackCandidate;
|
|
6412
|
+
while ((fallbackCandidate = fallbackRegex.exec(next)) !== null) {
|
|
6413
|
+
if (isIndexInRanges(fallbackCandidate.index, multilineRangesForNext)) {
|
|
6414
|
+
continue;
|
|
6415
|
+
}
|
|
6416
|
+
fallbackMatch = fallbackCandidate;
|
|
6417
|
+
break;
|
|
6418
|
+
}
|
|
6419
|
+
if (fallbackMatch) {
|
|
6420
|
+
const existingValue = String(fallbackMatch[2] || '').trim();
|
|
6421
|
+
const looksLikeMultilineArray = existingValue.startsWith('[') && !existingValue.endsWith(']');
|
|
6422
|
+
const looksLikeMultilineInlineTable = existingValue.startsWith('{') && !existingValue.endsWith('}');
|
|
6423
|
+
if (looksLikeMultilineArray || looksLikeMultilineInlineTable) {
|
|
6424
|
+
throw new Error(`${fieldName} 当前值是多行 TOML 结构,无法安全更新`);
|
|
6425
|
+
}
|
|
6426
|
+
const prefix = fallbackMatch[1];
|
|
6427
|
+
const suffix = fallbackMatch[3] || '';
|
|
6428
|
+
const replacement = `${prefix}"${safeValue}"${suffix}`;
|
|
6429
|
+
next = `${next.slice(0, fallbackMatch.index)}${replacement}${next.slice(fallbackMatch.index + fallbackMatch[0].length)}`;
|
|
6430
|
+
fallbackReplaced = true;
|
|
6431
|
+
}
|
|
6432
|
+
if (!fallbackReplaced) {
|
|
6433
|
+
const keyIndentMatch = block.match(/^(\s*)[A-Za-z0-9_.-]+\s*=/m);
|
|
6434
|
+
const indent = keyIndentMatch ? keyIndentMatch[1] : '';
|
|
6435
|
+
const lineEnding = block.includes('\r\n') ? '\r\n' : '\n';
|
|
6436
|
+
const tailMatch = block.match(/(\s*)$/);
|
|
6437
|
+
const tail = tailMatch ? tailMatch[1] : '';
|
|
6438
|
+
const body = block.slice(0, block.length - tail.length);
|
|
6439
|
+
const separator = body.endsWith('\n') || body.endsWith('\r') ? '' : lineEnding;
|
|
6440
|
+
next = `${body}${separator}${indent}${fieldName} = "${safeValue}"${tail}`;
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
return next;
|
|
6444
|
+
};
|
|
6445
|
+
|
|
6446
|
+
let newContent = content;
|
|
6447
|
+
const sorted = ranges.sort((a, b) => b.start - a.start);
|
|
6448
|
+
for (const range of sorted) {
|
|
6449
|
+
const providerBlock = newContent.slice(range.start, range.end);
|
|
6450
|
+
let updatedBlock = providerBlock;
|
|
6451
|
+
if (baseUrl) {
|
|
6452
|
+
updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', baseUrl);
|
|
6453
|
+
}
|
|
6454
|
+
if (apiKey !== undefined) {
|
|
6455
|
+
updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', apiKey);
|
|
6456
|
+
}
|
|
6457
|
+
newContent = newContent.slice(0, range.start) + updatedBlock + newContent.slice(range.end);
|
|
5139
6458
|
}
|
|
5140
6459
|
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
6460
|
+
const finalContent = newContent.trim();
|
|
6461
|
+
try {
|
|
6462
|
+
toml.parse(finalContent);
|
|
6463
|
+
} catch (e) {
|
|
6464
|
+
throw new Error(`更新后的 config.toml 无效: ${e.message}`);
|
|
6465
|
+
}
|
|
6466
|
+
writeConfig(finalContent);
|
|
5144
6467
|
|
|
5145
6468
|
// 如果更新了 API Key 且该提供商是当前激活的,同步更新 auth.json
|
|
5146
6469
|
const currentProvider = config.model_provider;
|
|
@@ -5368,17 +6691,57 @@ async function prepareCodexDirDownload() {
|
|
|
5368
6691
|
}
|
|
5369
6692
|
}
|
|
5370
6693
|
|
|
5371
|
-
function copyDirRecursive(srcDir, destDir) {
|
|
6694
|
+
function copyDirRecursive(srcDir, destDir, options = {}) {
|
|
6695
|
+
const dereferenceSymlinks = !!(options && options.dereferenceSymlinks);
|
|
6696
|
+
const visitedRealPaths = options && options.visitedRealPaths instanceof Set
|
|
6697
|
+
? options.visitedRealPaths
|
|
6698
|
+
: new Set();
|
|
6699
|
+
const childOptions = {
|
|
6700
|
+
...options,
|
|
6701
|
+
dereferenceSymlinks,
|
|
6702
|
+
visitedRealPaths
|
|
6703
|
+
};
|
|
5372
6704
|
ensureDir(destDir);
|
|
5373
6705
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
5374
6706
|
for (const entry of entries) {
|
|
5375
6707
|
const srcPath = path.join(srcDir, entry.name);
|
|
5376
6708
|
const destPath = path.join(destDir, entry.name);
|
|
5377
6709
|
if (entry.isDirectory()) {
|
|
5378
|
-
|
|
6710
|
+
if (!dereferenceSymlinks) {
|
|
6711
|
+
copyDirRecursive(srcPath, destPath, childOptions);
|
|
6712
|
+
continue;
|
|
6713
|
+
}
|
|
6714
|
+
const realPath = fs.realpathSync(srcPath);
|
|
6715
|
+
if (visitedRealPaths.has(realPath)) {
|
|
6716
|
+
continue;
|
|
6717
|
+
}
|
|
6718
|
+
visitedRealPaths.add(realPath);
|
|
6719
|
+
try {
|
|
6720
|
+
copyDirRecursive(srcPath, destPath, childOptions);
|
|
6721
|
+
} finally {
|
|
6722
|
+
visitedRealPaths.delete(realPath);
|
|
6723
|
+
}
|
|
5379
6724
|
} else if (entry.isSymbolicLink()) {
|
|
5380
|
-
|
|
5381
|
-
|
|
6725
|
+
if (dereferenceSymlinks) {
|
|
6726
|
+
const realPath = fs.realpathSync(srcPath);
|
|
6727
|
+
const realStat = fs.statSync(realPath);
|
|
6728
|
+
if (realStat.isDirectory()) {
|
|
6729
|
+
if (visitedRealPaths.has(realPath)) {
|
|
6730
|
+
continue;
|
|
6731
|
+
}
|
|
6732
|
+
visitedRealPaths.add(realPath);
|
|
6733
|
+
try {
|
|
6734
|
+
copyDirRecursive(realPath, destPath, childOptions);
|
|
6735
|
+
} finally {
|
|
6736
|
+
visitedRealPaths.delete(realPath);
|
|
6737
|
+
}
|
|
6738
|
+
} else {
|
|
6739
|
+
fs.copyFileSync(realPath, destPath);
|
|
6740
|
+
}
|
|
6741
|
+
} else {
|
|
6742
|
+
const target = fs.readlinkSync(srcPath);
|
|
6743
|
+
fs.symlinkSync(target, destPath);
|
|
6744
|
+
}
|
|
5382
6745
|
} else {
|
|
5383
6746
|
fs.copyFileSync(srcPath, destPath);
|
|
5384
6747
|
}
|
|
@@ -6185,6 +7548,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6185
7548
|
serviceTier,
|
|
6186
7549
|
modelReasoningEffort,
|
|
6187
7550
|
configReady: !statusConfigResult.isVirtual,
|
|
7551
|
+
configErrorType: statusConfigResult.errorType || '',
|
|
6188
7552
|
configNotice: statusConfigResult.reason || '',
|
|
6189
7553
|
initNotice: consumeInitNotice()
|
|
6190
7554
|
};
|
|
@@ -6199,6 +7563,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6199
7563
|
const current = listConfig.model_provider;
|
|
6200
7564
|
result = {
|
|
6201
7565
|
configReady: !listConfigResult.isVirtual,
|
|
7566
|
+
configErrorType: listConfigResult.errorType || '',
|
|
7567
|
+
configNotice: listConfigResult.reason || '',
|
|
6202
7568
|
providers: Object.entries(providers).map(([name, p]) => ({
|
|
6203
7569
|
name,
|
|
6204
7570
|
url: p.base_url || '',
|
|
@@ -6273,6 +7639,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6273
7639
|
case 'apply-agents-file':
|
|
6274
7640
|
result = applyAgentsFile(params || {});
|
|
6275
7641
|
break;
|
|
7642
|
+
case 'list-codex-skills':
|
|
7643
|
+
result = listCodexSkills();
|
|
7644
|
+
break;
|
|
7645
|
+
case 'delete-codex-skills':
|
|
7646
|
+
result = deleteCodexSkills(params || {});
|
|
7647
|
+
break;
|
|
7648
|
+
case 'scan-unmanaged-codex-skills':
|
|
7649
|
+
result = scanUnmanagedCodexSkills();
|
|
7650
|
+
break;
|
|
7651
|
+
case 'import-codex-skills':
|
|
7652
|
+
result = importCodexSkills(params || {});
|
|
7653
|
+
break;
|
|
6276
7654
|
case 'get-openclaw-config':
|
|
6277
7655
|
result = readOpenclawConfigFile();
|
|
6278
7656
|
break;
|
|
@@ -6433,6 +7811,58 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
6433
7811
|
case 'proxy-apply-provider':
|
|
6434
7812
|
result = applyBuiltinProxyProvider(params || {});
|
|
6435
7813
|
break;
|
|
7814
|
+
case 'workflow-list':
|
|
7815
|
+
result = listWorkflowDefinitions();
|
|
7816
|
+
break;
|
|
7817
|
+
case 'workflow-get':
|
|
7818
|
+
{
|
|
7819
|
+
const id = params && typeof params.id === 'string' ? params.id.trim() : '';
|
|
7820
|
+
if (!id) {
|
|
7821
|
+
result = { error: 'workflow id is required' };
|
|
7822
|
+
} else {
|
|
7823
|
+
result = getWorkflowDefinitionById(id);
|
|
7824
|
+
}
|
|
7825
|
+
}
|
|
7826
|
+
break;
|
|
7827
|
+
case 'workflow-validate':
|
|
7828
|
+
{
|
|
7829
|
+
const id = params && typeof params.id === 'string' ? params.id.trim() : '';
|
|
7830
|
+
if (!id) {
|
|
7831
|
+
result = { ok: false, error: 'workflow id is required' };
|
|
7832
|
+
break;
|
|
7833
|
+
}
|
|
7834
|
+
const input = params && params.input && typeof params.input === 'object' && !Array.isArray(params.input)
|
|
7835
|
+
? params.input
|
|
7836
|
+
: {};
|
|
7837
|
+
result = validateWorkflowById(id, input);
|
|
7838
|
+
}
|
|
7839
|
+
break;
|
|
7840
|
+
case 'workflow-run':
|
|
7841
|
+
{
|
|
7842
|
+
const id = params && typeof params.id === 'string' ? params.id.trim() : '';
|
|
7843
|
+
if (!id) {
|
|
7844
|
+
result = { error: 'workflow id is required' };
|
|
7845
|
+
break;
|
|
7846
|
+
}
|
|
7847
|
+
const input = params && params.input && typeof params.input === 'object' && !Array.isArray(params.input)
|
|
7848
|
+
? params.input
|
|
7849
|
+
: {};
|
|
7850
|
+
result = await runWorkflowById(id, input, {
|
|
7851
|
+
allowWrite: !!(params && params.allowWrite),
|
|
7852
|
+
dryRun: !!(params && params.dryRun)
|
|
7853
|
+
});
|
|
7854
|
+
}
|
|
7855
|
+
break;
|
|
7856
|
+
case 'workflow-runs':
|
|
7857
|
+
{
|
|
7858
|
+
const rawLimit = params && Number.isFinite(params.limit) ? params.limit : parseInt(params && params.limit, 10);
|
|
7859
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : 20;
|
|
7860
|
+
result = {
|
|
7861
|
+
runs: listWorkflowRunRecords(limit),
|
|
7862
|
+
limit
|
|
7863
|
+
};
|
|
7864
|
+
}
|
|
7865
|
+
break;
|
|
6436
7866
|
default:
|
|
6437
7867
|
result = { error: '未知操作' };
|
|
6438
7868
|
}
|
|
@@ -6942,13 +8372,229 @@ async function cmdProxy(args = []) {
|
|
|
6942
8372
|
return;
|
|
6943
8373
|
}
|
|
6944
8374
|
|
|
6945
|
-
if (subcommand === 'stop') {
|
|
6946
|
-
await stopBuiltinProxyRuntime();
|
|
6947
|
-
console.log('✓ 内建代理已停止\n');
|
|
8375
|
+
if (subcommand === 'stop') {
|
|
8376
|
+
await stopBuiltinProxyRuntime();
|
|
8377
|
+
console.log('✓ 内建代理已停止\n');
|
|
8378
|
+
return;
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8381
|
+
throw new Error(`未知 proxy 子命令: ${subcommand}`);
|
|
8382
|
+
}
|
|
8383
|
+
|
|
8384
|
+
function parseWorkflowInputArg(rawInput) {
|
|
8385
|
+
const raw = typeof rawInput === 'string' ? rawInput.trim() : '';
|
|
8386
|
+
if (!raw) {
|
|
8387
|
+
return {};
|
|
8388
|
+
}
|
|
8389
|
+
let content = raw;
|
|
8390
|
+
if (raw.startsWith('@')) {
|
|
8391
|
+
const filePath = path.resolve(raw.slice(1));
|
|
8392
|
+
if (!fs.existsSync(filePath)) {
|
|
8393
|
+
throw new Error(`工作流输入文件不存在: ${filePath}`);
|
|
8394
|
+
}
|
|
8395
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
8396
|
+
}
|
|
8397
|
+
let parsed;
|
|
8398
|
+
try {
|
|
8399
|
+
parsed = JSON.parse(content);
|
|
8400
|
+
} catch (e) {
|
|
8401
|
+
throw new Error(`工作流输入 JSON 解析失败: ${e.message}`);
|
|
8402
|
+
}
|
|
8403
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
8404
|
+
throw new Error('工作流输入必须是 JSON 对象');
|
|
8405
|
+
}
|
|
8406
|
+
return parsed;
|
|
8407
|
+
}
|
|
8408
|
+
|
|
8409
|
+
function printWorkflowHelp() {
|
|
8410
|
+
console.log('\n用法: codexmate workflow <list|get|validate|run|runs> [参数]');
|
|
8411
|
+
console.log(' codexmate workflow list');
|
|
8412
|
+
console.log(' codexmate workflow get diagnose-config');
|
|
8413
|
+
console.log(' codexmate workflow validate safe-provider-switch --input \'{"provider":"e2e"}\'');
|
|
8414
|
+
console.log(' codexmate workflow run diagnose-config --input \'{}\'');
|
|
8415
|
+
console.log(' codexmate workflow run safe-provider-switch --input \'{"provider":"e2e","apply":true}\' --allow-write');
|
|
8416
|
+
console.log(' codexmate workflow runs --limit 20');
|
|
8417
|
+
console.log('参数:');
|
|
8418
|
+
console.log(' --input <JSON|@file> 传入工作流输入');
|
|
8419
|
+
console.log(' --allow-write 允许执行写入步骤');
|
|
8420
|
+
console.log(' --dry-run 跳过写入步骤,仅预演');
|
|
8421
|
+
console.log(' --limit <N> 读取最近执行记录数量(runs)');
|
|
8422
|
+
console.log(' --json 以 JSON 输出');
|
|
8423
|
+
console.log();
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
function parseWorkflowCliOptions(args = []) {
|
|
8427
|
+
const options = {
|
|
8428
|
+
inputRaw: '',
|
|
8429
|
+
allowWrite: false,
|
|
8430
|
+
dryRun: false,
|
|
8431
|
+
limit: 20,
|
|
8432
|
+
json: false
|
|
8433
|
+
};
|
|
8434
|
+
const rest = [];
|
|
8435
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
8436
|
+
const arg = args[i];
|
|
8437
|
+
if (arg === '--allow-write') {
|
|
8438
|
+
options.allowWrite = true;
|
|
8439
|
+
continue;
|
|
8440
|
+
}
|
|
8441
|
+
if (arg === '--dry-run') {
|
|
8442
|
+
options.dryRun = true;
|
|
8443
|
+
continue;
|
|
8444
|
+
}
|
|
8445
|
+
if (arg === '--json') {
|
|
8446
|
+
options.json = true;
|
|
8447
|
+
continue;
|
|
8448
|
+
}
|
|
8449
|
+
if (arg === '--input') {
|
|
8450
|
+
options.inputRaw = args[i + 1] || '';
|
|
8451
|
+
i += 1;
|
|
8452
|
+
continue;
|
|
8453
|
+
}
|
|
8454
|
+
if (arg.startsWith('--input=')) {
|
|
8455
|
+
options.inputRaw = arg.slice('--input='.length);
|
|
8456
|
+
continue;
|
|
8457
|
+
}
|
|
8458
|
+
if (arg === '--limit') {
|
|
8459
|
+
const raw = args[i + 1];
|
|
8460
|
+
i += 1;
|
|
8461
|
+
const value = parseInt(raw, 10);
|
|
8462
|
+
if (Number.isFinite(value)) {
|
|
8463
|
+
options.limit = value;
|
|
8464
|
+
}
|
|
8465
|
+
continue;
|
|
8466
|
+
}
|
|
8467
|
+
if (arg.startsWith('--limit=')) {
|
|
8468
|
+
const value = parseInt(arg.slice('--limit='.length), 10);
|
|
8469
|
+
if (Number.isFinite(value)) {
|
|
8470
|
+
options.limit = value;
|
|
8471
|
+
}
|
|
8472
|
+
continue;
|
|
8473
|
+
}
|
|
8474
|
+
rest.push(arg);
|
|
8475
|
+
}
|
|
8476
|
+
return { options, rest };
|
|
8477
|
+
}
|
|
8478
|
+
|
|
8479
|
+
async function cmdWorkflow(args = []) {
|
|
8480
|
+
const argv = Array.isArray(args) ? args : [];
|
|
8481
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
8482
|
+
printWorkflowHelp();
|
|
8483
|
+
return;
|
|
8484
|
+
}
|
|
8485
|
+
const subcommand = String(argv[0] || '').trim().toLowerCase();
|
|
8486
|
+
const parsed = parseWorkflowCliOptions(argv.slice(1));
|
|
8487
|
+
const options = parsed.options;
|
|
8488
|
+
const rest = parsed.rest;
|
|
8489
|
+
|
|
8490
|
+
if (subcommand === 'list') {
|
|
8491
|
+
const result = listWorkflowDefinitions();
|
|
8492
|
+
if (options.json) {
|
|
8493
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
const workflows = Array.isArray(result.workflows) ? result.workflows : [];
|
|
8497
|
+
console.log('\n可用工作流:');
|
|
8498
|
+
for (const item of workflows) {
|
|
8499
|
+
const mode = item.readOnly ? 'read-only' : 'read-write';
|
|
8500
|
+
console.log(` - ${item.id} (${mode}, steps=${item.stepCount})`);
|
|
8501
|
+
if (item.description) {
|
|
8502
|
+
console.log(` ${item.description}`);
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
8505
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
8506
|
+
console.log('\n警告:');
|
|
8507
|
+
result.warnings.forEach((msg) => console.log(` - ${msg}`));
|
|
8508
|
+
}
|
|
8509
|
+
console.log();
|
|
8510
|
+
return;
|
|
8511
|
+
}
|
|
8512
|
+
|
|
8513
|
+
if (subcommand === 'runs') {
|
|
8514
|
+
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 20;
|
|
8515
|
+
const runs = listWorkflowRunRecords(limit);
|
|
8516
|
+
if (options.json) {
|
|
8517
|
+
console.log(JSON.stringify({ runs, limit }, null, 2));
|
|
8518
|
+
return;
|
|
8519
|
+
}
|
|
8520
|
+
console.log(`\n最近执行记录(${runs.length}/${limit}):`);
|
|
8521
|
+
for (const item of runs) {
|
|
8522
|
+
const status = item && item.success ? 'OK' : 'FAIL';
|
|
8523
|
+
console.log(` - [${status}] ${item.workflowId || '(unknown)'} runId=${item.runId || ''} duration=${item.durationMs || 0}ms`);
|
|
8524
|
+
if (item && item.error) {
|
|
8525
|
+
console.log(` error: ${item.error}`);
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
console.log();
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
|
|
8532
|
+
const workflowId = typeof rest[0] === 'string' ? rest[0].trim() : '';
|
|
8533
|
+
if (!workflowId) {
|
|
8534
|
+
throw new Error('workflow id is required');
|
|
8535
|
+
}
|
|
8536
|
+
const input = parseWorkflowInputArg(options.inputRaw);
|
|
8537
|
+
|
|
8538
|
+
if (subcommand === 'get') {
|
|
8539
|
+
const result = getWorkflowDefinitionById(workflowId);
|
|
8540
|
+
if (result.error) {
|
|
8541
|
+
throw new Error(result.error);
|
|
8542
|
+
}
|
|
8543
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8544
|
+
return;
|
|
8545
|
+
}
|
|
8546
|
+
|
|
8547
|
+
if (subcommand === 'validate') {
|
|
8548
|
+
const result = validateWorkflowById(workflowId, input);
|
|
8549
|
+
if (!result.ok) {
|
|
8550
|
+
throw new Error(result.error || 'workflow validate failed');
|
|
8551
|
+
}
|
|
8552
|
+
if (options.json) {
|
|
8553
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8554
|
+
} else {
|
|
8555
|
+
console.log(`✓ 工作流校验通过: ${workflowId}`);
|
|
8556
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
8557
|
+
result.warnings.forEach((msg) => console.log(` - ${msg}`));
|
|
8558
|
+
}
|
|
8559
|
+
console.log();
|
|
8560
|
+
}
|
|
8561
|
+
return;
|
|
8562
|
+
}
|
|
8563
|
+
|
|
8564
|
+
if (subcommand === 'run') {
|
|
8565
|
+
const result = await runWorkflowById(workflowId, input, {
|
|
8566
|
+
allowWrite: options.allowWrite,
|
|
8567
|
+
dryRun: options.dryRun
|
|
8568
|
+
});
|
|
8569
|
+
if (options.json) {
|
|
8570
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8571
|
+
} else {
|
|
8572
|
+
if (result.error) {
|
|
8573
|
+
console.error(`✗ 工作流执行失败: ${result.error}`);
|
|
8574
|
+
} else {
|
|
8575
|
+
console.log(`✓ 工作流执行完成: ${workflowId} (${result.durationMs || 0}ms)`);
|
|
8576
|
+
}
|
|
8577
|
+
const steps = Array.isArray(result.steps) ? result.steps : [];
|
|
8578
|
+
for (const step of steps) {
|
|
8579
|
+
const status = step.status || 'unknown';
|
|
8580
|
+
const label = step.id || step.tool || '(step)';
|
|
8581
|
+
console.log(` - ${label}: ${status} (${step.durationMs || 0}ms)`);
|
|
8582
|
+
if (step.error) {
|
|
8583
|
+
console.log(` error: ${step.error}`);
|
|
8584
|
+
}
|
|
8585
|
+
}
|
|
8586
|
+
if (result.runId) {
|
|
8587
|
+
console.log(` runId: ${result.runId}`);
|
|
8588
|
+
}
|
|
8589
|
+
console.log();
|
|
8590
|
+
}
|
|
8591
|
+
if (result.error) {
|
|
8592
|
+
throw new Error(result.error);
|
|
8593
|
+
}
|
|
6948
8594
|
return;
|
|
6949
8595
|
}
|
|
6950
8596
|
|
|
6951
|
-
throw new Error(`未知
|
|
8597
|
+
throw new Error(`未知 workflow 子命令: ${subcommand}`);
|
|
6952
8598
|
}
|
|
6953
8599
|
|
|
6954
8600
|
async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
|
|
@@ -7103,6 +8749,7 @@ function buildMcpStatusPayload() {
|
|
|
7103
8749
|
serviceTier,
|
|
7104
8750
|
modelReasoningEffort,
|
|
7105
8751
|
configReady: !statusConfigResult.isVirtual,
|
|
8752
|
+
configErrorType: statusConfigResult.errorType || '',
|
|
7106
8753
|
configNotice: statusConfigResult.reason || '',
|
|
7107
8754
|
initNotice: consumeInitNotice()
|
|
7108
8755
|
};
|
|
@@ -7115,6 +8762,8 @@ function buildMcpProviderListPayload() {
|
|
|
7115
8762
|
const current = listConfig.model_provider;
|
|
7116
8763
|
return {
|
|
7117
8764
|
configReady: !listConfigResult.isVirtual,
|
|
8765
|
+
configErrorType: listConfigResult.errorType || '',
|
|
8766
|
+
configNotice: listConfigResult.reason || '',
|
|
7118
8767
|
providers: Object.entries(providers).map(([name, p]) => ({
|
|
7119
8768
|
name,
|
|
7120
8769
|
url: p.base_url || '',
|
|
@@ -7167,6 +8816,541 @@ function normalizeMcpSource(value) {
|
|
|
7167
8816
|
return null;
|
|
7168
8817
|
}
|
|
7169
8818
|
|
|
8819
|
+
const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
|
|
8820
|
+
'diagnose-config': {
|
|
8821
|
+
id: 'diagnose-config',
|
|
8822
|
+
name: 'Diagnose Config',
|
|
8823
|
+
description: 'Collect status/providers/proxy snapshots for troubleshooting.',
|
|
8824
|
+
readOnly: true,
|
|
8825
|
+
inputSchema: {
|
|
8826
|
+
type: 'object',
|
|
8827
|
+
properties: {},
|
|
8828
|
+
additionalProperties: false
|
|
8829
|
+
},
|
|
8830
|
+
steps: [
|
|
8831
|
+
{ id: 'status', tool: 'codexmate.status.get', arguments: {} },
|
|
8832
|
+
{ id: 'providers', tool: 'codexmate.provider.list', arguments: {} },
|
|
8833
|
+
{ id: 'proxy', tool: 'codexmate.proxy.status', arguments: {} }
|
|
8834
|
+
]
|
|
8835
|
+
},
|
|
8836
|
+
'safe-provider-switch': {
|
|
8837
|
+
id: 'safe-provider-switch',
|
|
8838
|
+
name: 'Safe Provider Switch',
|
|
8839
|
+
description: 'Build template for a provider switch and optionally apply it.',
|
|
8840
|
+
readOnly: false,
|
|
8841
|
+
inputSchema: {
|
|
8842
|
+
type: 'object',
|
|
8843
|
+
properties: {
|
|
8844
|
+
provider: { type: 'string' },
|
|
8845
|
+
model: { type: 'string' },
|
|
8846
|
+
serviceTier: { type: 'string' },
|
|
8847
|
+
reasoningEffort: { type: 'string' },
|
|
8848
|
+
apply: { type: 'boolean' }
|
|
8849
|
+
},
|
|
8850
|
+
required: ['provider'],
|
|
8851
|
+
additionalProperties: false
|
|
8852
|
+
},
|
|
8853
|
+
steps: [
|
|
8854
|
+
{ id: 'providers', tool: 'codexmate.provider.list', arguments: {} },
|
|
8855
|
+
{
|
|
8856
|
+
id: 'template',
|
|
8857
|
+
tool: 'codexmate.config.template.get',
|
|
8858
|
+
arguments: {
|
|
8859
|
+
provider: '{{input.provider}}',
|
|
8860
|
+
model: '{{input.model}}',
|
|
8861
|
+
serviceTier: '{{input.serviceTier}}',
|
|
8862
|
+
reasoningEffort: '{{input.reasoningEffort}}'
|
|
8863
|
+
}
|
|
8864
|
+
},
|
|
8865
|
+
{
|
|
8866
|
+
id: 'apply',
|
|
8867
|
+
tool: 'codexmate.config.template.apply',
|
|
8868
|
+
when: { path: 'input.apply', equals: true },
|
|
8869
|
+
arguments: {
|
|
8870
|
+
template: '{{steps.template.output.template}}'
|
|
8871
|
+
}
|
|
8872
|
+
},
|
|
8873
|
+
{
|
|
8874
|
+
id: 'statusAfter',
|
|
8875
|
+
tool: 'codexmate.status.get',
|
|
8876
|
+
when: { path: 'input.apply', equals: true },
|
|
8877
|
+
arguments: {}
|
|
8878
|
+
}
|
|
8879
|
+
]
|
|
8880
|
+
},
|
|
8881
|
+
'session-issue-pack': {
|
|
8882
|
+
id: 'session-issue-pack',
|
|
8883
|
+
name: 'Session Issue Pack',
|
|
8884
|
+
description: 'Collect session detail and markdown export for issue reports.',
|
|
8885
|
+
readOnly: true,
|
|
8886
|
+
inputSchema: {
|
|
8887
|
+
type: 'object',
|
|
8888
|
+
properties: {
|
|
8889
|
+
source: { type: 'string' },
|
|
8890
|
+
sessionId: { type: 'string' },
|
|
8891
|
+
file: { type: 'string' },
|
|
8892
|
+
maxMessages: { type: ['string', 'number'] }
|
|
8893
|
+
},
|
|
8894
|
+
additionalProperties: true
|
|
8895
|
+
},
|
|
8896
|
+
steps: [
|
|
8897
|
+
{
|
|
8898
|
+
id: 'detail',
|
|
8899
|
+
tool: 'codexmate.session.detail',
|
|
8900
|
+
arguments: {
|
|
8901
|
+
source: '{{input.source}}',
|
|
8902
|
+
sessionId: '{{input.sessionId}}',
|
|
8903
|
+
file: '{{input.file}}',
|
|
8904
|
+
maxMessages: '{{input.maxMessages}}'
|
|
8905
|
+
}
|
|
8906
|
+
},
|
|
8907
|
+
{
|
|
8908
|
+
id: 'export',
|
|
8909
|
+
tool: 'codexmate.session.export',
|
|
8910
|
+
arguments: {
|
|
8911
|
+
source: '{{input.source}}',
|
|
8912
|
+
sessionId: '{{input.sessionId}}',
|
|
8913
|
+
file: '{{input.file}}',
|
|
8914
|
+
maxMessages: '{{input.maxMessages}}'
|
|
8915
|
+
}
|
|
8916
|
+
}
|
|
8917
|
+
]
|
|
8918
|
+
}
|
|
8919
|
+
});
|
|
8920
|
+
|
|
8921
|
+
function cloneJson(value, fallback) {
|
|
8922
|
+
try {
|
|
8923
|
+
return JSON.parse(JSON.stringify(value));
|
|
8924
|
+
} catch (_) {
|
|
8925
|
+
return fallback;
|
|
8926
|
+
}
|
|
8927
|
+
}
|
|
8928
|
+
|
|
8929
|
+
function normalizeWorkflowId(value) {
|
|
8930
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
8931
|
+
if (!raw) return '';
|
|
8932
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(raw)) {
|
|
8933
|
+
return '';
|
|
8934
|
+
}
|
|
8935
|
+
return raw.toLowerCase();
|
|
8936
|
+
}
|
|
8937
|
+
|
|
8938
|
+
function normalizeWorkflowDefinition(raw, idHint = '', source = 'custom') {
|
|
8939
|
+
const safe = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
|
|
8940
|
+
if (!safe) {
|
|
8941
|
+
return { ok: false, error: 'workflow must be an object' };
|
|
8942
|
+
}
|
|
8943
|
+
const id = normalizeWorkflowId(safe.id || idHint);
|
|
8944
|
+
if (!id) {
|
|
8945
|
+
return { ok: false, error: 'workflow id is invalid' };
|
|
8946
|
+
}
|
|
8947
|
+
const name = typeof safe.name === 'string' && safe.name.trim()
|
|
8948
|
+
? safe.name.trim()
|
|
8949
|
+
: id;
|
|
8950
|
+
const description = typeof safe.description === 'string' ? safe.description.trim() : '';
|
|
8951
|
+
const inputSchema = safe.inputSchema && typeof safe.inputSchema === 'object'
|
|
8952
|
+
? cloneJson(safe.inputSchema, { type: 'object', properties: {}, additionalProperties: true })
|
|
8953
|
+
: { type: 'object', properties: {}, additionalProperties: true };
|
|
8954
|
+
const stepsRaw = Array.isArray(safe.steps) ? safe.steps : [];
|
|
8955
|
+
if (stepsRaw.length === 0) {
|
|
8956
|
+
return { ok: false, error: 'workflow steps cannot be empty' };
|
|
8957
|
+
}
|
|
8958
|
+
|
|
8959
|
+
const steps = [];
|
|
8960
|
+
for (let i = 0; i < stepsRaw.length; i += 1) {
|
|
8961
|
+
const item = stepsRaw[i];
|
|
8962
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
8963
|
+
return { ok: false, error: `workflow step #${i + 1} must be an object` };
|
|
8964
|
+
}
|
|
8965
|
+
const stepIdRaw = typeof item.id === 'string' && item.id.trim()
|
|
8966
|
+
? item.id.trim()
|
|
8967
|
+
: `step${i + 1}`;
|
|
8968
|
+
const stepId = normalizeWorkflowId(stepIdRaw);
|
|
8969
|
+
if (!stepId) {
|
|
8970
|
+
return { ok: false, error: `workflow step id invalid at #${i + 1}` };
|
|
8971
|
+
}
|
|
8972
|
+
const toolName = typeof item.tool === 'string' ? item.tool.trim() : '';
|
|
8973
|
+
if (!toolName) {
|
|
8974
|
+
return { ok: false, error: `workflow step "${stepId}" missing tool` };
|
|
8975
|
+
}
|
|
8976
|
+
const args = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
8977
|
+
? cloneJson(item.arguments, {})
|
|
8978
|
+
: {};
|
|
8979
|
+
const when = item.when && typeof item.when === 'object' && !Array.isArray(item.when)
|
|
8980
|
+
? cloneJson(item.when, {})
|
|
8981
|
+
: null;
|
|
8982
|
+
steps.push({
|
|
8983
|
+
id: stepId,
|
|
8984
|
+
name: typeof item.name === 'string' ? item.name.trim() : '',
|
|
8985
|
+
tool: toolName,
|
|
8986
|
+
arguments: args,
|
|
8987
|
+
when,
|
|
8988
|
+
continueOnError: item.continueOnError === true,
|
|
8989
|
+
write: item.write === true
|
|
8990
|
+
});
|
|
8991
|
+
}
|
|
8992
|
+
|
|
8993
|
+
return {
|
|
8994
|
+
ok: true,
|
|
8995
|
+
data: {
|
|
8996
|
+
id,
|
|
8997
|
+
name,
|
|
8998
|
+
description,
|
|
8999
|
+
source,
|
|
9000
|
+
readOnly: safe.readOnly !== false,
|
|
9001
|
+
inputSchema,
|
|
9002
|
+
steps
|
|
9003
|
+
}
|
|
9004
|
+
};
|
|
9005
|
+
}
|
|
9006
|
+
|
|
9007
|
+
function loadBuiltinWorkflowDefinitions() {
|
|
9008
|
+
const items = [];
|
|
9009
|
+
for (const [id, raw] of Object.entries(BUILTIN_WORKFLOW_DEFINITIONS)) {
|
|
9010
|
+
const normalized = normalizeWorkflowDefinition(raw, id, 'builtin');
|
|
9011
|
+
if (!normalized.ok) {
|
|
9012
|
+
continue;
|
|
9013
|
+
}
|
|
9014
|
+
items.push(normalized.data);
|
|
9015
|
+
}
|
|
9016
|
+
return items;
|
|
9017
|
+
}
|
|
9018
|
+
|
|
9019
|
+
function loadCustomWorkflowDefinitions() {
|
|
9020
|
+
const parsed = readJsonObjectFromFile(WORKFLOW_DEFINITIONS_FILE, {});
|
|
9021
|
+
if (!parsed.ok || !parsed.exists) {
|
|
9022
|
+
return {
|
|
9023
|
+
items: [],
|
|
9024
|
+
warnings: parsed.ok ? [] : [parsed.error || 'workflow file parse failed']
|
|
9025
|
+
};
|
|
9026
|
+
}
|
|
9027
|
+
const data = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
|
|
9028
|
+
let list = [];
|
|
9029
|
+
if (Array.isArray(data.workflows)) {
|
|
9030
|
+
list = data.workflows;
|
|
9031
|
+
} else if (data.workflows && typeof data.workflows === 'object') {
|
|
9032
|
+
list = Object.entries(data.workflows).map(([id, item]) => ({ ...(item || {}), id }));
|
|
9033
|
+
} else {
|
|
9034
|
+
list = Object.entries(data).map(([id, item]) => ({ ...(item || {}), id }));
|
|
9035
|
+
}
|
|
9036
|
+
|
|
9037
|
+
const items = [];
|
|
9038
|
+
const warnings = [];
|
|
9039
|
+
for (const item of list) {
|
|
9040
|
+
const normalized = normalizeWorkflowDefinition(item, item && item.id ? item.id : '', 'custom');
|
|
9041
|
+
if (!normalized.ok) {
|
|
9042
|
+
warnings.push(normalized.error || 'invalid custom workflow');
|
|
9043
|
+
continue;
|
|
9044
|
+
}
|
|
9045
|
+
items.push(normalized.data);
|
|
9046
|
+
}
|
|
9047
|
+
return { items, warnings };
|
|
9048
|
+
}
|
|
9049
|
+
|
|
9050
|
+
function buildWorkflowRegistry() {
|
|
9051
|
+
const registry = new Map();
|
|
9052
|
+
const warnings = [];
|
|
9053
|
+
const builtin = loadBuiltinWorkflowDefinitions();
|
|
9054
|
+
for (const item of builtin) {
|
|
9055
|
+
registry.set(item.id, item);
|
|
9056
|
+
}
|
|
9057
|
+
const custom = loadCustomWorkflowDefinitions();
|
|
9058
|
+
for (const item of custom.items) {
|
|
9059
|
+
if (registry.has(item.id)) {
|
|
9060
|
+
warnings.push(`custom workflow id duplicated with builtin and ignored: ${item.id}`);
|
|
9061
|
+
continue;
|
|
9062
|
+
}
|
|
9063
|
+
registry.set(item.id, item);
|
|
9064
|
+
}
|
|
9065
|
+
warnings.push(...custom.warnings);
|
|
9066
|
+
return { registry, warnings };
|
|
9067
|
+
}
|
|
9068
|
+
|
|
9069
|
+
function listWorkflowDefinitions() {
|
|
9070
|
+
const { registry, warnings } = buildWorkflowRegistry();
|
|
9071
|
+
const workflows = Array.from(registry.values())
|
|
9072
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
9073
|
+
.map((item) => ({
|
|
9074
|
+
id: item.id,
|
|
9075
|
+
name: item.name,
|
|
9076
|
+
description: item.description,
|
|
9077
|
+
source: item.source,
|
|
9078
|
+
readOnly: item.readOnly !== false,
|
|
9079
|
+
stepCount: Array.isArray(item.steps) ? item.steps.length : 0
|
|
9080
|
+
}));
|
|
9081
|
+
return {
|
|
9082
|
+
workflows,
|
|
9083
|
+
warnings
|
|
9084
|
+
};
|
|
9085
|
+
}
|
|
9086
|
+
|
|
9087
|
+
function getWorkflowDefinitionById(rawId) {
|
|
9088
|
+
const id = normalizeWorkflowId(rawId);
|
|
9089
|
+
if (!id) {
|
|
9090
|
+
return { error: 'workflow id is required' };
|
|
9091
|
+
}
|
|
9092
|
+
const { registry, warnings } = buildWorkflowRegistry();
|
|
9093
|
+
const workflow = registry.get(id);
|
|
9094
|
+
if (!workflow) {
|
|
9095
|
+
return { error: `workflow not found: ${id}` };
|
|
9096
|
+
}
|
|
9097
|
+
return {
|
|
9098
|
+
workflow: cloneJson(workflow, {}),
|
|
9099
|
+
warnings
|
|
9100
|
+
};
|
|
9101
|
+
}
|
|
9102
|
+
|
|
9103
|
+
function createWorkflowToolCatalog() {
|
|
9104
|
+
return {
|
|
9105
|
+
'codexmate.status.get': {
|
|
9106
|
+
readOnly: true,
|
|
9107
|
+
handler: async () => buildMcpStatusPayload()
|
|
9108
|
+
},
|
|
9109
|
+
'codexmate.provider.list': {
|
|
9110
|
+
readOnly: true,
|
|
9111
|
+
handler: async () => buildMcpProviderListPayload()
|
|
9112
|
+
},
|
|
9113
|
+
'codexmate.proxy.status': {
|
|
9114
|
+
readOnly: true,
|
|
9115
|
+
handler: async () => getBuiltinProxyStatus()
|
|
9116
|
+
},
|
|
9117
|
+
'codexmate.session.list': {
|
|
9118
|
+
readOnly: true,
|
|
9119
|
+
handler: async (args = {}) => {
|
|
9120
|
+
const source = normalizeMcpSource(args.source);
|
|
9121
|
+
if (source === null) {
|
|
9122
|
+
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
9123
|
+
}
|
|
9124
|
+
return {
|
|
9125
|
+
source: source || 'all',
|
|
9126
|
+
sessions: listAllSessions({
|
|
9127
|
+
...args,
|
|
9128
|
+
source: source || 'all'
|
|
9129
|
+
})
|
|
9130
|
+
};
|
|
9131
|
+
}
|
|
9132
|
+
},
|
|
9133
|
+
'codexmate.session.detail': {
|
|
9134
|
+
readOnly: true,
|
|
9135
|
+
handler: async (args = {}) => readSessionDetail(args || {})
|
|
9136
|
+
},
|
|
9137
|
+
'codexmate.session.export': {
|
|
9138
|
+
readOnly: true,
|
|
9139
|
+
handler: async (args = {}) => exportSessionData(args || {})
|
|
9140
|
+
},
|
|
9141
|
+
'codexmate.config.template.get': {
|
|
9142
|
+
readOnly: true,
|
|
9143
|
+
handler: async (args = {}) => getConfigTemplate(args || {})
|
|
9144
|
+
},
|
|
9145
|
+
'codexmate.config.template.apply': {
|
|
9146
|
+
readOnly: false,
|
|
9147
|
+
handler: async (args = {}) => applyConfigTemplate(args || {})
|
|
9148
|
+
}
|
|
9149
|
+
};
|
|
9150
|
+
}
|
|
9151
|
+
|
|
9152
|
+
function getWorkflowKnownToolsSet() {
|
|
9153
|
+
return new Set(Object.keys(createWorkflowToolCatalog()));
|
|
9154
|
+
}
|
|
9155
|
+
|
|
9156
|
+
function resolveWorkflowDefinitionWithToolMeta(workflow) {
|
|
9157
|
+
const catalog = createWorkflowToolCatalog();
|
|
9158
|
+
const safe = cloneJson(workflow, {});
|
|
9159
|
+
safe.steps = (Array.isArray(safe.steps) ? safe.steps : []).map((step) => {
|
|
9160
|
+
const tool = catalog[step.tool];
|
|
9161
|
+
return {
|
|
9162
|
+
...step,
|
|
9163
|
+
write: step.write === true || !!(tool && tool.readOnly === false)
|
|
9164
|
+
};
|
|
9165
|
+
});
|
|
9166
|
+
return safe;
|
|
9167
|
+
}
|
|
9168
|
+
|
|
9169
|
+
function validateWorkflowInputBySchema(inputSchema, input) {
|
|
9170
|
+
const schema = inputSchema && typeof inputSchema === 'object' ? inputSchema : {};
|
|
9171
|
+
if (schema.type && schema.type !== 'object') {
|
|
9172
|
+
return { ok: false, error: `unsupported input schema type: ${schema.type}` };
|
|
9173
|
+
}
|
|
9174
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
9175
|
+
return { ok: false, error: 'workflow input must be an object' };
|
|
9176
|
+
}
|
|
9177
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
9178
|
+
for (const key of required) {
|
|
9179
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
9180
|
+
return { ok: false, error: `missing required input field: ${key}` };
|
|
9181
|
+
}
|
|
9182
|
+
}
|
|
9183
|
+
const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
|
|
9184
|
+
for (const [key, expected] of Object.entries(properties)) {
|
|
9185
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
|
|
9186
|
+
const value = input[key];
|
|
9187
|
+
if (!expected || typeof expected !== 'object') continue;
|
|
9188
|
+
const type = expected.type;
|
|
9189
|
+
if (!type) continue;
|
|
9190
|
+
const typeList = Array.isArray(type) ? type : [type];
|
|
9191
|
+
const actualType = value === null ? 'null' : (Array.isArray(value) ? 'array' : typeof value);
|
|
9192
|
+
const matched = typeList.some((candidate) => {
|
|
9193
|
+
if (candidate === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
9194
|
+
if (candidate === 'integer') return Number.isInteger(value);
|
|
9195
|
+
if (candidate === 'array') return Array.isArray(value);
|
|
9196
|
+
if (candidate === 'object') return value && typeof value === 'object' && !Array.isArray(value);
|
|
9197
|
+
if (candidate === 'null') return value === null;
|
|
9198
|
+
return actualType === candidate;
|
|
9199
|
+
});
|
|
9200
|
+
if (!matched) {
|
|
9201
|
+
return { ok: false, error: `input field "${key}" type mismatch` };
|
|
9202
|
+
}
|
|
9203
|
+
}
|
|
9204
|
+
return { ok: true };
|
|
9205
|
+
}
|
|
9206
|
+
|
|
9207
|
+
function appendWorkflowRunRecord(record) {
|
|
9208
|
+
ensureDir(path.dirname(WORKFLOW_RUNS_FILE));
|
|
9209
|
+
const content = `${JSON.stringify(record)}\n`;
|
|
9210
|
+
fs.appendFileSync(WORKFLOW_RUNS_FILE, content, { encoding: 'utf-8', mode: 0o600 });
|
|
9211
|
+
}
|
|
9212
|
+
|
|
9213
|
+
function listWorkflowRunRecords(limit = 20) {
|
|
9214
|
+
const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
|
|
9215
|
+
if (!fs.existsSync(WORKFLOW_RUNS_FILE)) {
|
|
9216
|
+
return [];
|
|
9217
|
+
}
|
|
9218
|
+
let content = '';
|
|
9219
|
+
try {
|
|
9220
|
+
content = fs.readFileSync(WORKFLOW_RUNS_FILE, 'utf-8');
|
|
9221
|
+
} catch (_) {
|
|
9222
|
+
return [];
|
|
9223
|
+
}
|
|
9224
|
+
const rows = content
|
|
9225
|
+
.split(/\r?\n/g)
|
|
9226
|
+
.map((line) => line.trim())
|
|
9227
|
+
.filter(Boolean);
|
|
9228
|
+
const parsed = [];
|
|
9229
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
9230
|
+
try {
|
|
9231
|
+
const item = JSON.parse(rows[i]);
|
|
9232
|
+
parsed.push(item);
|
|
9233
|
+
if (parsed.length >= max) {
|
|
9234
|
+
break;
|
|
9235
|
+
}
|
|
9236
|
+
} catch (_) {}
|
|
9237
|
+
}
|
|
9238
|
+
return parsed;
|
|
9239
|
+
}
|
|
9240
|
+
|
|
9241
|
+
function validateWorkflowById(workflowId, input = {}) {
|
|
9242
|
+
const definitionResult = getWorkflowDefinitionById(workflowId);
|
|
9243
|
+
if (definitionResult.error) {
|
|
9244
|
+
return { ok: false, error: definitionResult.error };
|
|
9245
|
+
}
|
|
9246
|
+
const workflow = resolveWorkflowDefinitionWithToolMeta(definitionResult.workflow);
|
|
9247
|
+
const knownTools = getWorkflowKnownToolsSet();
|
|
9248
|
+
const validation = validateWorkflowDefinition(workflow, { knownTools });
|
|
9249
|
+
if (!validation.ok) {
|
|
9250
|
+
return {
|
|
9251
|
+
ok: false,
|
|
9252
|
+
error: validation.error || 'workflow validation failed',
|
|
9253
|
+
issues: validation.issues || []
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
9256
|
+
const schemaValidation = validateWorkflowInputBySchema(workflow.inputSchema, input || {});
|
|
9257
|
+
if (!schemaValidation.ok) {
|
|
9258
|
+
return { ok: false, error: schemaValidation.error || 'workflow input validation failed' };
|
|
9259
|
+
}
|
|
9260
|
+
return {
|
|
9261
|
+
ok: true,
|
|
9262
|
+
workflow: {
|
|
9263
|
+
id: workflow.id,
|
|
9264
|
+
name: workflow.name,
|
|
9265
|
+
readOnly: workflow.readOnly !== false,
|
|
9266
|
+
stepCount: Array.isArray(workflow.steps) ? workflow.steps.length : 0
|
|
9267
|
+
},
|
|
9268
|
+
warnings: definitionResult.warnings || []
|
|
9269
|
+
};
|
|
9270
|
+
}
|
|
9271
|
+
|
|
9272
|
+
async function runWorkflowById(workflowId, input = {}, options = {}) {
|
|
9273
|
+
const definitionResult = getWorkflowDefinitionById(workflowId);
|
|
9274
|
+
if (definitionResult.error) {
|
|
9275
|
+
return { error: definitionResult.error };
|
|
9276
|
+
}
|
|
9277
|
+
const workflow = resolveWorkflowDefinitionWithToolMeta(definitionResult.workflow);
|
|
9278
|
+
const knownTools = getWorkflowKnownToolsSet();
|
|
9279
|
+
const validation = validateWorkflowDefinition(workflow, { knownTools });
|
|
9280
|
+
if (!validation.ok) {
|
|
9281
|
+
return {
|
|
9282
|
+
error: validation.error || 'workflow validation failed',
|
|
9283
|
+
issues: validation.issues || []
|
|
9284
|
+
};
|
|
9285
|
+
}
|
|
9286
|
+
const schemaValidation = validateWorkflowInputBySchema(workflow.inputSchema, input || {});
|
|
9287
|
+
if (!schemaValidation.ok) {
|
|
9288
|
+
return { error: schemaValidation.error || 'workflow input validation failed' };
|
|
9289
|
+
}
|
|
9290
|
+
|
|
9291
|
+
const catalog = createWorkflowToolCatalog();
|
|
9292
|
+
const allowWrite = options.allowWrite === true;
|
|
9293
|
+
const dryRun = options.dryRun === true;
|
|
9294
|
+
const runId = `wf-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
9295
|
+
const startedAt = toIsoTime(Date.now());
|
|
9296
|
+
|
|
9297
|
+
const execution = await executeWorkflowDefinition(workflow, input || {}, {
|
|
9298
|
+
allowWrite,
|
|
9299
|
+
dryRun,
|
|
9300
|
+
invokeTool: async (toolName, args = {}) => {
|
|
9301
|
+
const tool = catalog[toolName];
|
|
9302
|
+
if (!tool) {
|
|
9303
|
+
return { error: `workflow tool not supported: ${toolName}` };
|
|
9304
|
+
}
|
|
9305
|
+
if (!tool.readOnly && !allowWrite) {
|
|
9306
|
+
return { error: `workflow requires write permission for tool: ${toolName}` };
|
|
9307
|
+
}
|
|
9308
|
+
return tool.handler(args || {});
|
|
9309
|
+
}
|
|
9310
|
+
});
|
|
9311
|
+
|
|
9312
|
+
const endedAt = toIsoTime(Date.now());
|
|
9313
|
+
const record = {
|
|
9314
|
+
runId,
|
|
9315
|
+
workflowId: workflow.id,
|
|
9316
|
+
workflowName: workflow.name,
|
|
9317
|
+
success: execution.success === true,
|
|
9318
|
+
error: execution.error || '',
|
|
9319
|
+
allowWrite,
|
|
9320
|
+
dryRun,
|
|
9321
|
+
startedAt,
|
|
9322
|
+
endedAt,
|
|
9323
|
+
durationMs: execution.durationMs || 0,
|
|
9324
|
+
steps: Array.isArray(execution.steps) ? execution.steps.map((step) => ({
|
|
9325
|
+
id: step.id,
|
|
9326
|
+
tool: step.tool,
|
|
9327
|
+
status: step.status,
|
|
9328
|
+
durationMs: step.durationMs || 0,
|
|
9329
|
+
error: step.error || ''
|
|
9330
|
+
})) : [],
|
|
9331
|
+
input: cloneJson(input || {}, {})
|
|
9332
|
+
};
|
|
9333
|
+
try {
|
|
9334
|
+
appendWorkflowRunRecord(record);
|
|
9335
|
+
} catch (_) {}
|
|
9336
|
+
|
|
9337
|
+
return {
|
|
9338
|
+
success: execution.success === true,
|
|
9339
|
+
runId,
|
|
9340
|
+
workflowId: workflow.id,
|
|
9341
|
+
workflowName: workflow.name,
|
|
9342
|
+
allowWrite,
|
|
9343
|
+
dryRun,
|
|
9344
|
+
startedAt: execution.startedAt || startedAt,
|
|
9345
|
+
endedAt: execution.endedAt || endedAt,
|
|
9346
|
+
durationMs: execution.durationMs || 0,
|
|
9347
|
+
steps: execution.steps || [],
|
|
9348
|
+
output: execution.output || null,
|
|
9349
|
+
warnings: definitionResult.warnings || [],
|
|
9350
|
+
...(execution.error ? { error: execution.error } : {})
|
|
9351
|
+
};
|
|
9352
|
+
}
|
|
9353
|
+
|
|
7170
9354
|
function createMcpTools(options = {}) {
|
|
7171
9355
|
const allowWrite = !!options.allowWrite;
|
|
7172
9356
|
const tools = [];
|
|
@@ -7362,6 +9546,89 @@ function createMcpTools(options = {}) {
|
|
|
7362
9546
|
handler: async () => getBuiltinProxyStatus()
|
|
7363
9547
|
});
|
|
7364
9548
|
|
|
9549
|
+
pushTool({
|
|
9550
|
+
name: 'codexmate.workflow.list',
|
|
9551
|
+
description: 'List available workflows (builtin + custom).',
|
|
9552
|
+
readOnly: true,
|
|
9553
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
9554
|
+
handler: async () => listWorkflowDefinitions()
|
|
9555
|
+
});
|
|
9556
|
+
|
|
9557
|
+
pushTool({
|
|
9558
|
+
name: 'codexmate.workflow.get',
|
|
9559
|
+
description: 'Get one workflow definition by id.',
|
|
9560
|
+
readOnly: true,
|
|
9561
|
+
inputSchema: {
|
|
9562
|
+
type: 'object',
|
|
9563
|
+
properties: {
|
|
9564
|
+
id: { type: 'string' }
|
|
9565
|
+
},
|
|
9566
|
+
required: ['id'],
|
|
9567
|
+
additionalProperties: false
|
|
9568
|
+
},
|
|
9569
|
+
handler: async (args = {}) => {
|
|
9570
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
9571
|
+
if (!id) {
|
|
9572
|
+
return { error: 'workflow id is required' };
|
|
9573
|
+
}
|
|
9574
|
+
return getWorkflowDefinitionById(id);
|
|
9575
|
+
}
|
|
9576
|
+
});
|
|
9577
|
+
|
|
9578
|
+
pushTool({
|
|
9579
|
+
name: 'codexmate.workflow.validate',
|
|
9580
|
+
description: 'Validate workflow definition and input payload.',
|
|
9581
|
+
readOnly: true,
|
|
9582
|
+
inputSchema: {
|
|
9583
|
+
type: 'object',
|
|
9584
|
+
properties: {
|
|
9585
|
+
id: { type: 'string' },
|
|
9586
|
+
input: { type: 'object' }
|
|
9587
|
+
},
|
|
9588
|
+
required: ['id'],
|
|
9589
|
+
additionalProperties: false
|
|
9590
|
+
},
|
|
9591
|
+
handler: async (args = {}) => {
|
|
9592
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
9593
|
+
if (!id) {
|
|
9594
|
+
return { ok: false, error: 'workflow id is required' };
|
|
9595
|
+
}
|
|
9596
|
+
const input = args.input && typeof args.input === 'object' && !Array.isArray(args.input)
|
|
9597
|
+
? args.input
|
|
9598
|
+
: {};
|
|
9599
|
+
return validateWorkflowById(id, input);
|
|
9600
|
+
}
|
|
9601
|
+
});
|
|
9602
|
+
|
|
9603
|
+
pushTool({
|
|
9604
|
+
name: 'codexmate.workflow.run',
|
|
9605
|
+
description: 'Run workflow by id. Write steps require allow-write mode.',
|
|
9606
|
+
readOnly: true,
|
|
9607
|
+
inputSchema: {
|
|
9608
|
+
type: 'object',
|
|
9609
|
+
properties: {
|
|
9610
|
+
id: { type: 'string' },
|
|
9611
|
+
input: { type: 'object' },
|
|
9612
|
+
dryRun: { type: 'boolean' }
|
|
9613
|
+
},
|
|
9614
|
+
required: ['id'],
|
|
9615
|
+
additionalProperties: false
|
|
9616
|
+
},
|
|
9617
|
+
handler: async (args = {}) => {
|
|
9618
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
9619
|
+
if (!id) {
|
|
9620
|
+
return { error: 'workflow id is required' };
|
|
9621
|
+
}
|
|
9622
|
+
const input = args.input && typeof args.input === 'object' && !Array.isArray(args.input)
|
|
9623
|
+
? args.input
|
|
9624
|
+
: {};
|
|
9625
|
+
return runWorkflowById(id, input, {
|
|
9626
|
+
allowWrite,
|
|
9627
|
+
dryRun: args.dryRun === true
|
|
9628
|
+
});
|
|
9629
|
+
}
|
|
9630
|
+
});
|
|
9631
|
+
|
|
7365
9632
|
pushTool({
|
|
7366
9633
|
name: 'codexmate.config.template.apply',
|
|
7367
9634
|
description: 'Apply Codex TOML template and sync auth/model pointers.',
|
|
@@ -7636,6 +9903,50 @@ function createMcpResources() {
|
|
|
7636
9903
|
}]
|
|
7637
9904
|
};
|
|
7638
9905
|
}
|
|
9906
|
+
},
|
|
9907
|
+
{
|
|
9908
|
+
uri: 'codexmate://workflows',
|
|
9909
|
+
name: 'Workflows',
|
|
9910
|
+
description: 'Workflow list resource (builtin + custom).',
|
|
9911
|
+
mimeType: 'application/json',
|
|
9912
|
+
read: async () => ({
|
|
9913
|
+
contents: [{
|
|
9914
|
+
uri: 'codexmate://workflows',
|
|
9915
|
+
mimeType: 'application/json',
|
|
9916
|
+
text: JSON.stringify(listWorkflowDefinitions(), null, 2)
|
|
9917
|
+
}]
|
|
9918
|
+
})
|
|
9919
|
+
},
|
|
9920
|
+
{
|
|
9921
|
+
uri: 'codexmate://workflow-runs',
|
|
9922
|
+
name: 'WorkflowRuns',
|
|
9923
|
+
description: 'Recent workflow execution records. Supports ?limit=<N>.',
|
|
9924
|
+
mimeType: 'application/json',
|
|
9925
|
+
read: async (params = {}) => {
|
|
9926
|
+
const uri = typeof params.uri === 'string' ? params.uri : 'codexmate://workflow-runs';
|
|
9927
|
+
let limit = 20;
|
|
9928
|
+
try {
|
|
9929
|
+
const parsed = new URL(uri);
|
|
9930
|
+
const rawLimit = parsed.searchParams.get('limit');
|
|
9931
|
+
if (rawLimit) {
|
|
9932
|
+
const parsedLimit = parseInt(rawLimit, 10);
|
|
9933
|
+
if (Number.isFinite(parsedLimit)) {
|
|
9934
|
+
limit = parsedLimit;
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
} catch (_) {}
|
|
9938
|
+
const payload = {
|
|
9939
|
+
runs: listWorkflowRunRecords(limit),
|
|
9940
|
+
limit: Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20
|
|
9941
|
+
};
|
|
9942
|
+
return {
|
|
9943
|
+
contents: [{
|
|
9944
|
+
uri,
|
|
9945
|
+
mimeType: 'application/json',
|
|
9946
|
+
text: JSON.stringify(payload, null, 2)
|
|
9947
|
+
}]
|
|
9948
|
+
};
|
|
9949
|
+
}
|
|
7639
9950
|
}
|
|
7640
9951
|
];
|
|
7641
9952
|
}
|
|
@@ -7820,6 +10131,7 @@ async function main() {
|
|
|
7820
10131
|
console.log(' codexmate delete-model <模型> 删除模型');
|
|
7821
10132
|
console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
|
|
7822
10133
|
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
10134
|
+
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
7823
10135
|
console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
|
|
7824
10136
|
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
7825
10137
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
@@ -7846,6 +10158,7 @@ async function main() {
|
|
|
7846
10158
|
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
7847
10159
|
case 'auth': cmdAuth(args.slice(1)); break;
|
|
7848
10160
|
case 'proxy': await cmdProxy(args.slice(1)); break;
|
|
10161
|
+
case 'workflow': await cmdWorkflow(args.slice(1)); break;
|
|
7849
10162
|
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
7850
10163
|
case 'start':
|
|
7851
10164
|
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|