@ninkch/dsh-artcraft 0.2.10 → 0.3.1
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.md +38 -13
- package/README.zh-CN.md +33 -10
- package/apps/catalog.json +53 -1
- package/artcraft.config.json.example +5 -2
- package/client.js +11 -2
- package/cordis.patch.yml +4 -0
- package/index.js +451 -19
- package/package.json +6 -2
- package/recipes/recipes.json +315 -32
- package/skills/README.md +43 -0
- package/skills/artcraft-batch-pipeline/SKILL.md +96 -0
- package/skills/artcraft-color-light/SKILL.md +96 -0
- package/skills/artcraft-color-light/references/develop-controls.md +43 -0
- package/skills/artcraft-color-light/references/lumetri-and-scopes.md +65 -0
- package/skills/artcraft-delivery-qc/SKILL.md +105 -0
- package/skills/artcraft-delivery-qc/references/platform-specs.md +74 -0
- package/skills/artcraft-edit-rhythm/SKILL.md +110 -0
- package/skills/artcraft-edit-rhythm/references/transitions.md +53 -0
- package/skills/artcraft-film-study/SKILL.md +109 -0
- package/skills/artcraft-motion-vfx/SKILL.md +117 -0
- package/skills/artcraft-motion-vfx/references/effect-catalogue.md +429 -0
- package/skills/artcraft-motion-vfx/references/scene-to-effect.md +103 -0
- package/skills/artcraft-multicam/SKILL.md +79 -0
- package/skills/artcraft-ops/SKILL.md +116 -0
- package/skills/artcraft-ops/references/command-families.md +57 -0
- package/skills/artcraft-shot-language/SKILL.md +112 -0
- package/skills/artcraft-shot-language/references/shot-vocabulary.md +98 -0
- package/skills/artcraft-sound-design/SKILL.md +95 -0
- package/skills/artcraft-sound-design/references/essential-sound.md +77 -0
- package/skills/artcraft-style-bible/SKILL.md +84 -0
- package/skills.js +84 -0
package/index.js
CHANGED
|
@@ -8,12 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 两种形态:
|
|
10
10
|
* 1) Agent Tools(对话内直接驱动):
|
|
11
|
-
* artcraft_status —— 检查
|
|
12
|
-
* artcraft_cli —— 运行
|
|
11
|
+
* artcraft_status —— 检查 13 个家族 CLI 是否可用、配置目录、控制通道配置
|
|
12
|
+
* artcraft_cli —— 运行 13 个家族 CLI 任意命令(cwd / 文件参数由调用方给绝对路径)
|
|
13
13
|
* artcraft_control —— 向运行中的 FilmCraft/EffectCraft/VectorCraft/LightCraft JSON 控制通道发一条命令
|
|
14
14
|
* artcraft_fs —— 桥接文件操作(list/stat/readtext/write/mkdir,绝对路径)
|
|
15
15
|
* artcraft_update —— 插件自身更新(检查/应用)
|
|
16
|
-
* artcraft_apps ——
|
|
16
|
+
* artcraft_apps —— 13 个家族应用的自动化能力目录(子命令/控制端口/MCP/仓库)
|
|
17
|
+
* artcraft_qc —— 交付质检(家族探针 + 平台规格核对,返回 PASS/WARN/FAIL/MANUAL)
|
|
18
|
+
* artcraft_live —— 控制通道活性探测(哪些 App 正开着,可直接边看边调)
|
|
19
|
+
*
|
|
20
|
+
* 技能包(独立插件行 @ninkch/dsh-artcraft/skills,见 skills.js):artcraft-ops / artcraft-shot-language /
|
|
21
|
+
* artcraft-edit-rhythm / artcraft-color-light / artcraft-sound-design / artcraft-motion-vfx /
|
|
22
|
+
* artcraft-delivery-qc —— 教 agent 怎么调得顺、何时用哪种效果、镜头语言怎么设计。
|
|
17
23
|
* 2) HTTP API(同源 /artcraft/api/*,供无限画布等前端调用,无 CORS 问题):
|
|
18
24
|
* GET /artcraft/api/status 环境自检
|
|
19
25
|
* GET /artcraft/api/fs list / stat / readtext
|
|
@@ -23,10 +29,13 @@
|
|
|
23
29
|
* POST /artcraft/api/job 启动长任务(渲染/导出/批处理)
|
|
24
30
|
* POST /artcraft/api/job/cancel 取消任务
|
|
25
31
|
* POST /artcraft/api/control 控制通道同步调用(engine.execute / ui.*)
|
|
26
|
-
* GET /artcraft/api/apps 家族能力目录(list / ?id=
|
|
32
|
+
* GET /artcraft/api/apps 家族能力目录(list / ?id= 单个,含命令族统计)
|
|
33
|
+
* GET /artcraft/api/qc 交付质检(?path= 产物[&platform=][&kind=])
|
|
34
|
+
* GET /artcraft/api/skills 内置技能包清单
|
|
35
|
+
* GET /artcraft/api/live 控制通道活性(?ports=app:port,… 追加)
|
|
27
36
|
*
|
|
28
37
|
* 安全约定:
|
|
29
|
-
* - 子进程只允许
|
|
38
|
+
* - 子进程只允许 13 个白名单键(家族 CLI),具体可执行文件由 artcraft.config.json 指定;
|
|
30
39
|
* - 控制通道只连 127.0.0.1,与 ArtCraft 官方协议一致(App 需以 --control 启动;
|
|
31
40
|
* 已知默认端口:filmcraft 9876 / effectcraft 9877 / vectorcraft 7979 / lightcraft 7980;
|
|
32
41
|
* PhotoCraft 控制通道为令牌鉴权模式、PdfCraft 为文件式(--control FILE),均不代理;
|
|
@@ -46,12 +55,12 @@ import crypto from 'node:crypto';
|
|
|
46
55
|
import { fileURLToPath } from 'node:url';
|
|
47
56
|
|
|
48
57
|
const NAME = 'artcraft';
|
|
49
|
-
const VERSION = '0.
|
|
58
|
+
const VERSION = '0.3.1';
|
|
50
59
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
51
60
|
|
|
52
61
|
const BINS = [
|
|
53
62
|
'photocraft', 'vectorcraft', 'filmcraft', 'lightcraft', 'pdfcraft', 'effectcraft',
|
|
54
|
-
'designcraft', 'wordcraft', 'deckcraft', 'gridcraft', 'cadcraft', 'soundcraft',
|
|
63
|
+
'designcraft', 'wordcraft', 'deckcraft', 'gridcraft', 'cadcraft', 'soundcraft', 'figcraft',
|
|
55
64
|
];
|
|
56
65
|
const PKG_NAME = '@ninkch/dsh-artcraft';
|
|
57
66
|
const DEFAULT_PORTS = { filmcraft: 9876, effectcraft: 9877, vectorcraft: 7979, lightcraft: 7980 };
|
|
@@ -78,6 +87,7 @@ const DEFAULT_CONFIG = {
|
|
|
78
87
|
gridcraft: 'gridcraft-cli',
|
|
79
88
|
cadcraft: 'cadcraft-cli',
|
|
80
89
|
soundcraft: 'soundcraft-cli',
|
|
90
|
+
figcraft: 'figcraft-cli',
|
|
81
91
|
},
|
|
82
92
|
control: { ...DEFAULT_PORTS },
|
|
83
93
|
};
|
|
@@ -86,7 +96,7 @@ const APP_DISPLAY = {
|
|
|
86
96
|
photocraft: 'PhotoCraft', vectorcraft: 'VectorCraft', filmcraft: 'FilmCraft',
|
|
87
97
|
lightcraft: 'LightCraft', pdfcraft: 'PdfCraft', effectcraft: 'EffectCraft',
|
|
88
98
|
designcraft: 'DesignCraft', wordcraft: 'WordCraft', deckcraft: 'DeckCraft',
|
|
89
|
-
gridcraft: 'GridCraft', cadcraft: 'CADCraft', soundcraft: 'SoundCraft',
|
|
99
|
+
gridcraft: 'GridCraft', cadcraft: 'CADCraft', soundcraft: 'SoundCraft', figcraft: 'FigCraft',
|
|
90
100
|
};
|
|
91
101
|
|
|
92
102
|
/* ============================ 配置 ============================ */
|
|
@@ -489,6 +499,306 @@ async function statusInfo(cfg, meta) {
|
|
|
489
499
|
};
|
|
490
500
|
}
|
|
491
501
|
|
|
502
|
+
/* ============================ 交付质检(确定性规则) ============================ */
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 质检原则:只报事实,不下判断(判定交给调用方/agent)。
|
|
506
|
+
* 每条 check 给 level(ok|warn|fail|manual) + 实测值 + 建议命令。
|
|
507
|
+
*/
|
|
508
|
+
const QC_KINDS = [
|
|
509
|
+
{ kind: 'video', bin: 'filmcraft', re: /\.(mp4|mov|mkv|webm|avi|m4v|mxf)$/i, args: (p) => ['probe', p] },
|
|
510
|
+
{ kind: 'audio', bin: 'soundcraft', re: /\.(wav|aif|aiff|mp3|m4a|flac|aac|ogg)$/i, args: (p) => ['info', p] },
|
|
511
|
+
{ kind: 'image', bin: 'photocraft', re: /\.(png|jpe?g|tiff?|webp|bmp|gif|pcraft|psd|exr)$/i, args: (p) => ['info', p, '--compact'] },
|
|
512
|
+
{ kind: 'pdf', bin: 'pdfcraft', re: /\.pdf$/i, args: (p) => ['info', p] },
|
|
513
|
+
{ kind: 'doc', bin: 'wordcraft', re: /\.(docx|doc|odt|rtf)$/i, args: (p) => ['info', p] },
|
|
514
|
+
{ kind: 'sheet', bin: 'gridcraft', re: /\.(xlsx|xls|csv|tsv|ods)$/i, args: (p) => ['info', p, '--json'] },
|
|
515
|
+
{ kind: 'deck', bin: 'deckcraft', re: /\.(pptx|ppt|odp|deckcraft)$/i, args: (p) => ['info', p] },
|
|
516
|
+
{ kind: 'fig', bin: 'figcraft', re: /\.(figcraft|fig)$/i, args: (p) => ['info', p, '--json'] },
|
|
517
|
+
{ kind: 'cad', bin: 'cadcraft', re: /\.(dxf|dwg)$/i, args: (p) => ['info', p] },
|
|
518
|
+
{ kind: 'design', bin: 'designcraft', re: /\.(indd|idml|icml|designcraft)$/i, args: null },
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
const PLATFORM_SPECS = {
|
|
522
|
+
bilibili: { label: 'B 站/横屏', width: 1920, height: 1080, fps: [24, 25, 30, 50, 60], loudness: -14, tp: -1 },
|
|
523
|
+
youtube: { label: 'YouTube/横屏', width: 1920, height: 1080, fps: [24, 25, 30, 50, 60], loudness: -14, tp: -1 },
|
|
524
|
+
douyin: { label: '抖音/竖屏', width: 1080, height: 1920, fps: [30, 60], loudness: -14, tp: -1 },
|
|
525
|
+
reels: { label: 'Reels/竖屏', width: 1080, height: 1920, fps: [30, 60], loudness: -14, tp: -1 },
|
|
526
|
+
shorts: { label: 'Shorts/竖屏', width: 1080, height: 1920, fps: [30, 60], loudness: -14, tp: -1 },
|
|
527
|
+
wechat: { label: '视频号/竖屏', width: 1080, height: 1920, fps: [30], loudness: -14, tp: -1 },
|
|
528
|
+
broadcast: { label: '广播 EBU R128', width: 1920, height: 1080, fps: [25, 50, 29.97, 59.94], loudness: -23, tp: -2 },
|
|
529
|
+
podcast: { label: '播客(纯音频)', audioOnly: true, loudness: -16, tp: -1.5 },
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
/** 从 CLI 输出里尽力抽出 JSON(前面可能有日志行) */
|
|
533
|
+
function extractJson(text) {
|
|
534
|
+
const t = String(text ?? '');
|
|
535
|
+
const start = t.search(/[[{]/);
|
|
536
|
+
if (start < 0) return null;
|
|
537
|
+
for (let end = t.length; end > start; end--) {
|
|
538
|
+
const slice = t.slice(start, end).trim();
|
|
539
|
+
if (!slice || (slice[0] === '{' && !slice.endsWith('}')) || (slice[0] === '[' && !slice.endsWith(']'))) continue;
|
|
540
|
+
try { return JSON.parse(slice); } catch { /* 继续缩短 */ }
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function qcKindFor(file) {
|
|
546
|
+
for (const k of QC_KINDS) if (k.re.test(file)) return k;
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function num(v) { const n = Number(v); return Number.isFinite(n) ? n : null; }
|
|
551
|
+
|
|
552
|
+
/** 深挖对象树,找第一个匹配键(不同成员的字段命名不统一) */
|
|
553
|
+
function dig(obj, keys, depth = 0) {
|
|
554
|
+
if (!obj || typeof obj !== 'object' || depth > 4) return null;
|
|
555
|
+
for (const k of keys) if (obj[k] !== undefined && obj[k] !== null) return obj[k];
|
|
556
|
+
for (const v of Object.values(obj)) {
|
|
557
|
+
if (v && typeof v === 'object') { const r = dig(v, keys, depth + 1); if (r !== null) return r; }
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function runQc(cfg, { path: rawPath, platform, kind: wantKind, live: wantLive = true } = {}) {
|
|
563
|
+
const file = resolveCallerPath(rawPath);
|
|
564
|
+
const st = await fs.stat(file).catch(() => null);
|
|
565
|
+
if (!st || !st.isFile()) throw new Error('文件不存在或不是常规文件: ' + file);
|
|
566
|
+
const k = wantKind ? QC_KINDS.find((x) => x.kind === wantKind) : qcKindFor(file);
|
|
567
|
+
if (!k) throw new Error('无法识别文件类型(可用 kind 显式指定: ' + QC_KINDS.map((x) => x.kind).join('|') + ')');
|
|
568
|
+
|
|
569
|
+
const spec = platform ? PLATFORM_SPECS[String(platform).toLowerCase()] : null;
|
|
570
|
+
if (platform && !spec) throw new Error('未知 platform: ' + platform + '(可用: ' + Object.keys(PLATFORM_SPECS).join('|') + ')');
|
|
571
|
+
|
|
572
|
+
const checks = [];
|
|
573
|
+
const add = (id, level, detail, fix) => checks.push({ id, level, detail, ...(fix ? { fix } : {}) });
|
|
574
|
+
|
|
575
|
+
// 通用:文件体量
|
|
576
|
+
st.size > 0 ? add('file.size', 'ok', `${(st.size / 1048576).toFixed(2)} MB`) : add('file.size', 'fail', '文件为空');
|
|
577
|
+
|
|
578
|
+
// 成员探针
|
|
579
|
+
let probe = null; let raw = ''; let bin = null; let exe = null; let probeOk = false;
|
|
580
|
+
if (k.args) {
|
|
581
|
+
try {
|
|
582
|
+
exe = await resolveBin(cfg, k.bin);
|
|
583
|
+
bin = k.bin;
|
|
584
|
+
const r = await runProcess(exe, k.args(file), { timeoutMs: 120000 });
|
|
585
|
+
raw = (r.stdout || '') + (r.stderr ? '\n' + r.stderr : '');
|
|
586
|
+
probe = extractJson(r.stdout) ?? extractJson(raw);
|
|
587
|
+
probeOk = r.ok && !!probe;
|
|
588
|
+
probeOk ? add('probe', 'ok', `${k.bin} ${k.args(file)[0]} 成功`)
|
|
589
|
+
: add('probe', probe ? 'warn' : 'fail', probe ? '探针有输出但退出码非 0' : `探针失败:${(r.stderr || '').split('\n')[0] || '无输出'}`, `artcraft_cli { bin:"${k.bin}", args:${JSON.stringify(k.args(file))} }`);
|
|
590
|
+
} catch (e) {
|
|
591
|
+
add('probe', 'fail', `无法执行 ${k.bin}: ${String(e?.message ?? e)}`, `artcraft_status 检查 bins.${k.bin} 路径`);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
add('probe', 'manual', `${k.bin} 无 info 子命令,需打开工程检查`, `artcraft_cli { bin:"${k.bin}", args:["commands","--json"] }`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 按类型核对
|
|
598
|
+
if (probe && k.kind === 'video') {
|
|
599
|
+
const w = num(dig(probe, ['width', 'videoWidth']));
|
|
600
|
+
const h = num(dig(probe, ['height', 'videoHeight']));
|
|
601
|
+
const fps = num(dig(probe, ['fps', 'frameRate', 'frame_rate']));
|
|
602
|
+
const dur = num(dig(probe, ['seconds', 'duration', 'durationSeconds']));
|
|
603
|
+
const audioStreams = dig(probe, ['audioStreams', 'audio_streams', 'audioTracks']);
|
|
604
|
+
if (w && h) add('video.resolution', 'ok', `${w}×${h}`); else add('video.resolution', 'manual', '未能从探针读到分辨率');
|
|
605
|
+
if (fps) add('video.fps', 'ok', `${fps} fps`);
|
|
606
|
+
if (dur) add('video.duration', 'ok', `${dur.toFixed(2)} s`);
|
|
607
|
+
const hasAudio = Array.isArray(audioStreams) ? audioStreams.length > 0 : audioStreams !== null ? true : null;
|
|
608
|
+
if (hasAudio === false) add('video.audio', 'warn', '未检测到音轨', '确认是否需要静音交付');
|
|
609
|
+
else if (hasAudio === true) add('video.audio', 'ok', '存在音轨');
|
|
610
|
+
if (spec) {
|
|
611
|
+
if (spec.audioOnly) add('platform.match', 'warn', `${spec.label} 需要纯音频交付,当前是视频`);
|
|
612
|
+
if (w && h && (spec.width !== w || spec.height !== h)) {
|
|
613
|
+
add('platform.resolution', 'warn', `${w}×${h} ≠ 目标 ${spec.width}×${spec.height}`, `按目标画幅重排或改用对应平台导出预设`);
|
|
614
|
+
} else if (w && h) add('platform.resolution', 'ok', `符合 ${spec.label}`);
|
|
615
|
+
if (fps && spec.fps && !spec.fps.includes(fps)) add('platform.fps', 'warn', `${fps} fps 不在推荐 ${spec.fps.join('/')}`);
|
|
616
|
+
}
|
|
617
|
+
add('loudness', 'manual', '文件响度需在应用内测量', `soundcraft bounce 返回 lufs/true_peak_db;或用 FilmCraft essentialSound.inspect + autoMatch { target: ${spec?.loudness ?? -14} }`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (probe && k.kind === 'audio') {
|
|
621
|
+
const sr = num(dig(probe, ['sample_rate', 'sampleRate']));
|
|
622
|
+
const ch = num(dig(probe, ['channels']));
|
|
623
|
+
const sec = num(dig(probe, ['seconds', 'duration']));
|
|
624
|
+
const fmt = dig(probe, ['sample_format', 'sampleFormat', 'format']);
|
|
625
|
+
if (sr) add('audio.sampleRate', sr >= 44100 ? 'ok' : (sr >= 32000 ? 'warn' : 'fail'), `${sr} Hz`, sr < 44100 ? '交付建议 ≥ 44.1 kHz' : undefined);
|
|
626
|
+
if (ch) add('audio.channels', 'ok', `${ch} ch`);
|
|
627
|
+
if (sec) add('audio.duration', 'ok', `${sec.toFixed(2)} s`);
|
|
628
|
+
if (fmt) add('audio.format', 'ok', String(fmt));
|
|
629
|
+
add('loudness', 'manual', '需在应用内测量或 bounce 时读取', `soundcraft run --in <session> --bounce <out> 的返回含 lufs/true_peak_db;目标 ${spec?.loudness ?? -14} LUFS`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (probe && k.kind === 'image') {
|
|
633
|
+
const w = num(dig(probe, ['width'])); const h = num(dig(probe, ['height'])); const depth = num(dig(probe, ['depth']));
|
|
634
|
+
if (w && h) add('image.size', 'ok', `${w}×${h}`);
|
|
635
|
+
if (depth) add('image.depth', 'ok', `${depth} bit/通道`);
|
|
636
|
+
if (spec && !spec.audioOnly && w && h && (spec.width !== w || spec.height !== h)) {
|
|
637
|
+
add('platform.resolution', 'warn', `${w}×${h} ≠ 目标 ${spec.width}×${spec.height}`, '按目标画幅导出');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (probe && (k.kind === 'pdf' || k.kind === 'doc' || k.kind === 'deck' || k.kind === 'sheet')) {
|
|
642
|
+
const pages = num(dig(probe, ['pages', 'pageCount', 'page_count']));
|
|
643
|
+
const enc = dig(probe, ['encrypted']);
|
|
644
|
+
const sheets = dig(probe, ['sheets']);
|
|
645
|
+
if (pages) add('doc.pages', 'ok', `${pages} 页`);
|
|
646
|
+
if (enc === true) add('doc.encrypted', 'warn', '文档已加密', '确认交付对象可打开;必要时 pdfcraft protect.remove');
|
|
647
|
+
if (Array.isArray(sheets)) {
|
|
648
|
+
add('sheet.count', 'ok', `${sheets.length} 个工作表`);
|
|
649
|
+
const used = sheets.map((s) => s?.usedRange).filter(Boolean);
|
|
650
|
+
used.length ? add('sheet.usedRange', 'ok', used.slice(0, 3).join(' / ')) : add('sheet.usedRange', 'warn', '工作表未见已用区域', '确认数据是否已写入');
|
|
651
|
+
const charts = sheets.reduce((n, s) => n + (Array.isArray(s?.charts) ? s.charts.length : 0), 0);
|
|
652
|
+
add('sheet.charts', charts > 0 ? 'ok' : 'warn', `${charts} 个图表`);
|
|
653
|
+
}
|
|
654
|
+
if (k.kind === 'pdf') {
|
|
655
|
+
add('doc.a11y', 'manual', '无障碍需在 PdfCraft 内跑检查', `artcraft_cli { bin:"pdfcraft", args:["run","a11y.check","path=<file>"] }(或 a11y.report 出报告)`);
|
|
656
|
+
add('doc.print', 'manual', '印刷尺寸/出血需测量', `artcraft_cli { bin:"pdfcraft", args:["run","measure.info"] }`);
|
|
657
|
+
}
|
|
658
|
+
if (k.kind === 'doc') {
|
|
659
|
+
add('doc.a11y', 'manual', 'Word 侧无障碍需在应用内检查', 'artcraft_cli { bin:"wordcraft", args:["run","--cmd","file.accessibility"] }');
|
|
660
|
+
const comments = num(dig(probe, ['comments']));
|
|
661
|
+
if (comments !== null) add('doc.comments', comments === 0 ? 'ok' : 'warn', `${comments} 条批注`, comments ? '交付前清点批注(review.changes)' : undefined);
|
|
662
|
+
}
|
|
663
|
+
if (k.kind === 'deck') add('deck.review', 'manual', '动画/转场与备注需在应用内核对', 'artcraft_cli { bin:"deckcraft", args:["commands","animation"] }');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 运行中 App 的实测信号(QC v2):示波器 / 声音体检 / 字幕长度 / 合成能力
|
|
667
|
+
let liveInfo = null;
|
|
668
|
+
const appChecks = [];
|
|
669
|
+
if (wantLive) {
|
|
670
|
+
try {
|
|
671
|
+
liveInfo = await probeLiveApps(cfg);
|
|
672
|
+
const liveApps = new Set(liveInfo.apps.filter((a) => a.live).map((a) => a.app));
|
|
673
|
+
checks.push({
|
|
674
|
+
id: 'app.live',
|
|
675
|
+
level: liveInfo.live ? 'ok' : 'manual',
|
|
676
|
+
detail: liveInfo.live ? `控制通道可达:${[...liveApps].join(' / ')}` : '无运行中的控制通道(无头 CLI 已足够;需信号级实测时以 --control 启动 App)',
|
|
677
|
+
});
|
|
678
|
+
const brief = (v) => { const s = JSON.stringify(v ?? null); return s && s.length > 360 ? s.slice(0, 360) + '…' : s; };
|
|
679
|
+
if (liveApps.has('filmcraft')) {
|
|
680
|
+
const port = cfg.control.filmcraft;
|
|
681
|
+
try {
|
|
682
|
+
const sc = await controlCall(port, 'scopes.read', { scopes: ['waveform', 'vectorscopeYuv', 'histogram'] }, { timeoutMs: 4000 });
|
|
683
|
+
appChecks.push({ app: 'filmcraft', method: 'scopes.read', result: brief(sc) });
|
|
684
|
+
checks.push({ id: 'app.scopes', level: 'ok', detail: '已取 FilmCraft 示波器读数(详见 appChecks)' });
|
|
685
|
+
} catch (e) { checks.push({ id: 'app.scopes', level: 'manual', detail: '示波器读取失败:' + String(e?.message ?? e).slice(0, 120) }); }
|
|
686
|
+
try {
|
|
687
|
+
const es = await controlCall(port, 'essentialSound.inspect', {}, { timeoutMs: 4000 });
|
|
688
|
+
appChecks.push({ app: 'filmcraft', method: 'essentialSound.inspect', result: brief(es) });
|
|
689
|
+
checks.push({ id: 'app.audio', level: 'ok', detail: '已取 Essential Sound 体检(详见 appChecks)' });
|
|
690
|
+
} catch (e) { checks.push({ id: 'app.audio', level: 'manual', detail: '声音体检失败:' + String(e?.message ?? e).slice(0, 120) }); }
|
|
691
|
+
try {
|
|
692
|
+
const caps = await controlCall(port, 'captions.list', {}, { timeoutMs: 4000 });
|
|
693
|
+
const arr = Array.isArray(caps) ? caps : (caps?.captions ?? []);
|
|
694
|
+
const texts = arr.map((c) => String(c?.text ?? '')).filter(Boolean);
|
|
695
|
+
const tooLong = texts.filter((t) => t.length > 20);
|
|
696
|
+
checks.push({
|
|
697
|
+
id: 'app.captions',
|
|
698
|
+
level: texts.length === 0 ? 'manual' : (tooLong.length ? 'warn' : 'ok'),
|
|
699
|
+
detail: texts.length === 0 ? '当前序列未见字幕条目' : `${texts.length} 条字幕${tooLong.length ? `,其中 ${tooLong.length} 条超过 20 字(竖屏建议 ≤15 字,横屏 ≤20)` : ',长度合规'}`,
|
|
700
|
+
...(tooLong.length ? { fix: 'captions.split / captions.setText 拆分或精简;并确认字幕在安全区内' } : {}),
|
|
701
|
+
});
|
|
702
|
+
} catch (e) { checks.push({ id: 'app.captions', level: 'manual', detail: '字幕清点失败:' + String(e?.message ?? e).slice(0, 120) }); }
|
|
703
|
+
}
|
|
704
|
+
if (liveApps.has('effectcraft')) {
|
|
705
|
+
try {
|
|
706
|
+
const caps = await controlCall(cfg.control.effectcraft, 'app.capabilities', {}, { timeoutMs: 4000 });
|
|
707
|
+
appChecks.push({ app: 'effectcraft', method: 'app.capabilities', result: brief(caps) });
|
|
708
|
+
checks.push({ id: 'app.effectcraft', level: 'ok', detail: '已取 EffectCraft 能力表(命令数/效果数,详见 appChecks)' });
|
|
709
|
+
} catch (e) { checks.push({ id: 'app.effectcraft', level: 'manual', detail: '能力表读取失败:' + String(e?.message ?? e).slice(0, 120) }); }
|
|
710
|
+
}
|
|
711
|
+
} catch (e) {
|
|
712
|
+
checks.push({ id: 'app.live', level: 'manual', detail: '活性探测失败:' + String(e?.message ?? e).slice(0, 120) });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const fails = checks.filter((c) => c.level === 'fail').length;
|
|
717
|
+
const warns = checks.filter((c) => c.level === 'warn').length;
|
|
718
|
+
const manuals = checks.filter((c) => c.level === 'manual').length;
|
|
719
|
+
return {
|
|
720
|
+
path: file,
|
|
721
|
+
kind: k.kind,
|
|
722
|
+
bin,
|
|
723
|
+
exe,
|
|
724
|
+
platform: spec ? { id: String(platform).toLowerCase(), ...spec } : null,
|
|
725
|
+
verdict: fails ? 'fail' : warns ? 'warn' : manuals ? 'pass-with-manual-checks' : 'pass',
|
|
726
|
+
counts: { fail: fails, warn: warns, manual: manuals, ok: checks.filter((c) => c.level === 'ok').length },
|
|
727
|
+
checks,
|
|
728
|
+
appChecks,
|
|
729
|
+
live: liveInfo ? { live: liveInfo.live, apps: liveInfo.apps.map((a) => ({ app: a.app, port: a.port, live: a.live })) } : null,
|
|
730
|
+
probe,
|
|
731
|
+
rawTail: raw ? raw.slice(-1200) : null,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* ============================ 控制通道活性探测 ============================ */
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 探测哪些家族 App 正在运行(控制通道可达 = 该 App 以 --control 启动且工程已打开)。
|
|
739
|
+
* 活性证据用一次 engine.commands 调用,顺带拿到该应用当前可用命令数(parity 视角的"活着")。
|
|
740
|
+
*/
|
|
741
|
+
async function probeLiveApps(cfg, extraPorts = []) {
|
|
742
|
+
const targets = [];
|
|
743
|
+
for (const [app, port] of Object.entries(cfg.control || {})) targets.push({ app, port: Number(port) });
|
|
744
|
+
for (const ap of extraPorts) {
|
|
745
|
+
const p = Number(ap?.port ?? ap);
|
|
746
|
+
if (Number.isInteger(p) && p > 0 && p < 65536) targets.push({ app: ap?.app ?? null, port: p });
|
|
747
|
+
}
|
|
748
|
+
const seen = new Set();
|
|
749
|
+
const apps = [];
|
|
750
|
+
for (const t of targets) {
|
|
751
|
+
if (seen.has(t.port)) continue;
|
|
752
|
+
seen.add(t.port);
|
|
753
|
+
const started = Date.now();
|
|
754
|
+
try {
|
|
755
|
+
const result = await controlCall(t.port, 'engine.commands', {}, { timeoutMs: 2500 });
|
|
756
|
+
const list = Array.isArray(result) ? result : (result?.commands ?? result?.result ?? null);
|
|
757
|
+
apps.push({
|
|
758
|
+
app: t.app,
|
|
759
|
+
port: t.port,
|
|
760
|
+
live: true,
|
|
761
|
+
ms: Date.now() - started,
|
|
762
|
+
commands: Array.isArray(list) ? list.length : null,
|
|
763
|
+
});
|
|
764
|
+
} catch (e) {
|
|
765
|
+
apps.push({ app: t.app, port: t.port, live: false, ms: Date.now() - started, error: String(e?.message ?? e).slice(0, 160) });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return { live: apps.filter((a) => a.live).length, total: apps.length, apps, note: apps.some((a) => a.live) ? '控制通道可达:可直接用 artcraft_control 边看边调' : '未发现运行中的控制通道(App 需以 --control 启动)' };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* ============================ 技能包清单 ============================ */
|
|
772
|
+
|
|
773
|
+
/** 扫描包内技能目录(供 /api/skills 与界面展示;技能实际加载由 skills.js 的提供者负责) */
|
|
774
|
+
async function listBundledSkills() {
|
|
775
|
+
const dir = path.join(HERE, 'skills');
|
|
776
|
+
const out = [];
|
|
777
|
+
let entries = [];
|
|
778
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return { skills: [] }; }
|
|
779
|
+
for (const ent of entries) {
|
|
780
|
+
if (!ent.isDirectory()) continue;
|
|
781
|
+
const file = path.join(dir, ent.name, 'SKILL.md');
|
|
782
|
+
try {
|
|
783
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
784
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
785
|
+
const meta = {};
|
|
786
|
+
if (fm) for (const line of fm[1].split(/\r?\n/)) {
|
|
787
|
+
const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.+)$/.exec(line);
|
|
788
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
789
|
+
}
|
|
790
|
+
const refs = [];
|
|
791
|
+
try {
|
|
792
|
+
for (const r of await fs.readdir(path.join(dir, ent.name, 'references'), { withFileTypes: true })) {
|
|
793
|
+
if (r.isFile()) refs.push(r.name);
|
|
794
|
+
}
|
|
795
|
+
} catch { /* 无 references 目录 */ }
|
|
796
|
+
out.push({ dir: ent.name, name: meta.name ?? ent.name, description: meta.description ?? '', references: refs, bytes: raw.length });
|
|
797
|
+
} catch { /* 跳过不可读 */ }
|
|
798
|
+
}
|
|
799
|
+
return { count: out.length, skills: out.sort((a, b) => a.dir.localeCompare(b.dir)) };
|
|
800
|
+
}
|
|
801
|
+
|
|
492
802
|
/* ============================ HTTP 基础设施 ============================ */
|
|
493
803
|
|
|
494
804
|
function sendJson(res, code, obj) {
|
|
@@ -735,13 +1045,15 @@ export function apply(ctx) {
|
|
|
735
1045
|
ctx.effect(() => {
|
|
736
1046
|
const disposers = [];
|
|
737
1047
|
const attempt = (def) => {
|
|
1048
|
+
// 44.0.0 起注册器强制要求 output(缺了整工具拒注册,见 RELEASING.md 教训与 issue #1):
|
|
1049
|
+
// 兜底 output 必须随 shape 一起传入,两个命名约定都要带。
|
|
738
1050
|
const output = def.output ?? {
|
|
739
1051
|
schema: { type: 'object', additionalProperties: true },
|
|
740
1052
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
741
1053
|
};
|
|
742
1054
|
for (const shape of [
|
|
743
|
-
{ name: def.name, description: def.description, parameters: def.schema, execute: def.execute },
|
|
744
|
-
{ name: def.name, description: def.description, inputSchema: def.schema, handler: def.execute },
|
|
1055
|
+
{ name: def.name, description: def.description, parameters: def.schema, execute: def.execute, output },
|
|
1056
|
+
{ name: def.name, description: def.description, inputSchema: def.schema, handler: def.execute, output },
|
|
745
1057
|
]) {
|
|
746
1058
|
try {
|
|
747
1059
|
const d = ctx.tools.register(shape);
|
|
@@ -757,7 +1069,7 @@ export function apply(ctx) {
|
|
|
757
1069
|
|
|
758
1070
|
attempt({
|
|
759
1071
|
name: 'artcraft_status',
|
|
760
|
-
description: '检查 ArtCraft 家族
|
|
1072
|
+
description: '检查 ArtCraft 家族 13 个 CLI 是否可用,报告配置目录、控制通道端口与任务状态。本插件只是桥接,产物路径由调用方决定。',
|
|
761
1073
|
schema: { type: 'object', properties: {}, additionalProperties: false },
|
|
762
1074
|
async execute() {
|
|
763
1075
|
const cfg = await loadConfig();
|
|
@@ -767,7 +1079,7 @@ export function apply(ctx) {
|
|
|
767
1079
|
|
|
768
1080
|
attempt({
|
|
769
1081
|
name: 'artcraft_cli',
|
|
770
|
-
description: '运行 ArtCraft 家族 CLI 的任意子命令(photocraft-cli / wordcraft-cli /
|
|
1082
|
+
description: '运行 ArtCraft 家族 CLI 的任意子命令(photocraft-cli / wordcraft-cli / figcraft-cli 等 13 个,bin 参数的 enum 即完整清单)。长渲染请改用 HTTP POST /artcraft/api/job 后台执行。',
|
|
771
1083
|
schema: {
|
|
772
1084
|
type: 'object',
|
|
773
1085
|
properties: {
|
|
@@ -919,12 +1231,12 @@ export function apply(ctx) {
|
|
|
919
1231
|
|
|
920
1232
|
attempt({
|
|
921
1233
|
name: 'artcraft_recipes',
|
|
922
|
-
description: '
|
|
1234
|
+
description: '内置专家处理方案库(v2):8 套专业配方(短剧海报、分镜批量调色、横转竖、响度标准化、字幕生成与烧录、动态片头、多平台分发包、先探测后验证纪律)+ 14 条「任务 → 技能 → 命令链」编排模板(竖屏短剧成片、剧本到分镜、RAW 批量冲洗、拉片分析、音频后期、合成镜头、设计系统交付、文档交付、批量横转竖、封面缩略图、多语言字幕、素材清理修复、社交分发包、长素材切片)。action=list 列配方;action=get + id 取配方详情;action=chains 列编排链;action=chain + id 取完整命令链。配方中的 skills 字段指名应先读的技能包。',
|
|
923
1235
|
schema: {
|
|
924
1236
|
type: 'object',
|
|
925
1237
|
properties: {
|
|
926
|
-
action: { type: 'string', enum: ['list', 'get'] },
|
|
927
|
-
id: { type: 'string', description: 'action=get
|
|
1238
|
+
action: { type: 'string', enum: ['list', 'get', 'chains', 'chain'] },
|
|
1239
|
+
id: { type: 'string', description: 'action=get / chain 时的 id' },
|
|
928
1240
|
},
|
|
929
1241
|
required: ['action'],
|
|
930
1242
|
additionalProperties: false,
|
|
@@ -932,22 +1244,37 @@ export function apply(ctx) {
|
|
|
932
1244
|
async execute(args) {
|
|
933
1245
|
const recipesPath = path.join(HERE, 'recipes', 'recipes.json');
|
|
934
1246
|
const catalog = JSON.parse(await fs.readFile(recipesPath, 'utf8'));
|
|
935
|
-
|
|
1247
|
+
const action = String(args.action);
|
|
1248
|
+
const chains = Array.isArray(catalog.chains) ? catalog.chains : [];
|
|
1249
|
+
if (action === 'get') {
|
|
936
1250
|
const r = catalog.recipes.find((x) => x.id === String(args.id ?? ''));
|
|
937
1251
|
if (!r) throw new Error('配方不存在: ' + args.id + '(可用: ' + catalog.recipes.map((x) => x.id).join(', ') + ')');
|
|
938
1252
|
return r;
|
|
939
1253
|
}
|
|
1254
|
+
if (action === 'chain') {
|
|
1255
|
+
const c = chains.find((x) => x.id === String(args.id ?? ''));
|
|
1256
|
+
if (!c) throw new Error('编排链不存在: ' + args.id + '(可用: ' + chains.map((x) => x.id).join(', ') + ')');
|
|
1257
|
+
return c;
|
|
1258
|
+
}
|
|
1259
|
+
if (action === 'chains') {
|
|
1260
|
+
return {
|
|
1261
|
+
version: catalog.version,
|
|
1262
|
+
chains: chains.map((c) => ({ id: c.id, title: c.title, skills: c.skills, when: c.when, steps: c.steps?.length ?? 0 })),
|
|
1263
|
+
hint: 'action=chain + id 取完整命令链(每步 app + cmd + 说明 + 验证项)',
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
940
1266
|
return {
|
|
941
1267
|
version: catalog.version,
|
|
942
|
-
recipes: catalog.recipes.map((r) => ({ id: r.id, title: r.title, apps: r.apps, goal: r.goal })),
|
|
943
|
-
|
|
1268
|
+
recipes: catalog.recipes.map((r) => ({ id: r.id, title: r.title, apps: r.apps, skills: r.skills, goal: r.goal })),
|
|
1269
|
+
chains: chains.map((c) => ({ id: c.id, title: c.title, skills: c.skills })),
|
|
1270
|
+
hint: 'action=get + id 取配方(原则 + 步骤 + 验证);action=chain + id 取编排链;技能包见 artcraft-ops 等 7 个技能',
|
|
944
1271
|
};
|
|
945
1272
|
},
|
|
946
1273
|
});
|
|
947
1274
|
|
|
948
1275
|
attempt({
|
|
949
1276
|
name: 'artcraft_apps',
|
|
950
|
-
description: 'ArtCraft 家族
|
|
1277
|
+
description: 'ArtCraft 家族 13 应用的自动化能力目录:每个应用的定位、对标软件、CLI 子命令用法、控制通道端口、MCP 可用性与仓库/文档链接,另含各成员实测命令族统计。action=list 列出全部;action=get + id 取单个应用的完整能力卡。要拿某应用的实时命令注册表,用 artcraft_cli 跑该应用的 commands 子命令。',
|
|
951
1278
|
schema: {
|
|
952
1279
|
type: 'object',
|
|
953
1280
|
properties: {
|
|
@@ -974,6 +1301,71 @@ export function apply(ctx) {
|
|
|
974
1301
|
},
|
|
975
1302
|
});
|
|
976
1303
|
|
|
1304
|
+
attempt({
|
|
1305
|
+
name: 'artcraft_qc',
|
|
1306
|
+
description: '交付质检(确定性规则):对产物跑家族探针并按平台规格核对,返回 PASS/WARN/FAIL/MANUAL 清单与修复命令。支持 video/audio/image/pdf/doc/sheet/deck/fig/cad。用法:path=产物绝对路径;platform 可选(bilibili|youtube|douyin|reels|shorts|wechat|broadcast|podcast)核对画幅/帧率/响度目标;kind 可选,强制指定类型;live 默认 true——若检测到运行中的 App,会额外取示波器读数(filmcraft scopes.read)、Essential Sound 体检、字幕长度与 EffectCraft 能力表(结果在 appChecks)。只报事实与实测值,判定留给调用方。',
|
|
1307
|
+
output: {
|
|
1308
|
+
schema: { type: 'object', additionalProperties: true },
|
|
1309
|
+
render: (_args, v) => {
|
|
1310
|
+
if (!v || !Array.isArray(v.checks)) return [{ type: 'text', text: JSON.stringify(v, null, 2) }];
|
|
1311
|
+
const lines = [`QC ${String(v.verdict || '').toUpperCase()} ${v.path ?? ''}`,
|
|
1312
|
+
`kind=${v.kind ?? '?'} bin=${v.bin ?? '-'} platform=${v.platform?.id ?? '-'} 计数=${JSON.stringify(v.counts ?? {})}`];
|
|
1313
|
+
for (const c of v.checks) {
|
|
1314
|
+
lines.push(`[${String(c.level).toUpperCase().padEnd(6)}] ${c.id}: ${c.detail}`);
|
|
1315
|
+
if (c.fix) lines.push(` fix: ${c.fix}`);
|
|
1316
|
+
}
|
|
1317
|
+
if (Array.isArray(v.appChecks) && v.appChecks.length) {
|
|
1318
|
+
lines.push('appChecks: ' + JSON.stringify(v.appChecks));
|
|
1319
|
+
}
|
|
1320
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
schema: {
|
|
1324
|
+
type: 'object',
|
|
1325
|
+
properties: {
|
|
1326
|
+
path: { type: 'string', description: '待检产物绝对路径(或 ~/ 展开)' },
|
|
1327
|
+
platform: { type: 'string', enum: Object.keys(PLATFORM_SPECS), description: '可选:目标平台,用于规格核对' },
|
|
1328
|
+
kind: { type: 'string', enum: QC_KINDS.map((k) => k.kind), description: '可选:强制指定文件类型' },
|
|
1329
|
+
live: { type: 'boolean', description: '可选:是否探测运行中的 App 做信号级检查(默认 true)' },
|
|
1330
|
+
},
|
|
1331
|
+
required: ['path'],
|
|
1332
|
+
additionalProperties: false,
|
|
1333
|
+
},
|
|
1334
|
+
async execute(args) {
|
|
1335
|
+
const cfg = await loadConfig();
|
|
1336
|
+
return runQc(cfg, { path: args.path, platform: args.platform, kind: args.kind, live: args.live !== false });
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
attempt({
|
|
1341
|
+
name: 'artcraft_live',
|
|
1342
|
+
description: '探测哪些 ArtCraft App 正在运行(控制通道活性):对已知端口逐个调用 engine.commands 作为活性证据,返回 live 列表、耗时与当前可用命令数。用于"边看边调"前的准备——确认目标 App 已以 --control 启动。可用 ports 追加自定义端口(如 [{app:"designcraft",port:8000}])。',
|
|
1343
|
+
output: {
|
|
1344
|
+
schema: { type: 'object', additionalProperties: true },
|
|
1345
|
+
render: (_args, v) => {
|
|
1346
|
+
if (!v || !Array.isArray(v.apps)) return [{ type: 'text', text: JSON.stringify(v, null, 2) }];
|
|
1347
|
+
const lines = [`LIVE ${v.live}/${v.total} ${v.note ?? ''}`];
|
|
1348
|
+
for (const a of v.apps) {
|
|
1349
|
+
lines.push(a.live
|
|
1350
|
+
? `[LIVE ] ${String(a.app).padEnd(13)} :${a.port} ${a.commands ?? '?'} cmds, ${a.ms}ms`
|
|
1351
|
+
: `[— ] ${String(a.app).padEnd(13)} :${a.port} ${(a.error || '').slice(0, 90)}`);
|
|
1352
|
+
}
|
|
1353
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
schema: {
|
|
1357
|
+
type: 'object',
|
|
1358
|
+
properties: {
|
|
1359
|
+
ports: { type: 'array', items: { type: 'object' }, description: '可选:追加探测的端口,元素形如 {app:"x", port:8000} 或直接端口号' },
|
|
1360
|
+
},
|
|
1361
|
+
additionalProperties: false,
|
|
1362
|
+
},
|
|
1363
|
+
async execute(args) {
|
|
1364
|
+
const cfg = await loadConfig();
|
|
1365
|
+
return probeLiveApps(cfg, Array.isArray(args?.ports) ? args.ports : []);
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
|
|
977
1369
|
return () => {
|
|
978
1370
|
for (const d of disposers) { try { d(); } catch { /* ignore */ } }
|
|
979
1371
|
};
|
|
@@ -1165,9 +1557,49 @@ export function apply(ctx) {
|
|
|
1165
1557
|
version: catalog.version,
|
|
1166
1558
|
controlProtocol: catalog.controlProtocol,
|
|
1167
1559
|
apps: catalog.apps.map((a) => ({ id: a.id, name: a.name, tagline: a.tagline, ref: a.ref, cli: a.cli, control: a.control, mcp: a.mcp, stage: a.stage })),
|
|
1560
|
+
capabilityStats: catalog.capabilityStats ?? null,
|
|
1168
1561
|
});
|
|
1169
1562
|
}),
|
|
1170
1563
|
|
|
1564
|
+
makeRoute(`/${NAME}/api/live`, async (req, res) => {
|
|
1565
|
+
if (req.method !== 'GET') throw new Error('仅支持 GET');
|
|
1566
|
+
const cfg = await loadConfig();
|
|
1567
|
+
const url = new URL(req.url, 'http://x');
|
|
1568
|
+
const extra = String(url.searchParams.get('ports') ?? '')
|
|
1569
|
+
.split(',').map((s) => s.trim()).filter(Boolean)
|
|
1570
|
+
.map((s) => { const [app, port] = s.split(':'); return port ? { app, port } : { port: app }; });
|
|
1571
|
+
sendJson(res, 200, await probeLiveApps(cfg, extra));
|
|
1572
|
+
}),
|
|
1573
|
+
|
|
1574
|
+
makeRoute(`/${NAME}/api/skills`, async (req, res) => {
|
|
1575
|
+
if (req.method !== 'GET') throw new Error('仅支持 GET');
|
|
1576
|
+
sendJson(res, 200, await listBundledSkills());
|
|
1577
|
+
}),
|
|
1578
|
+
|
|
1579
|
+
makeRoute(`/${NAME}/api/qc`, async (req, res) => {
|
|
1580
|
+
const cfg = await loadConfig();
|
|
1581
|
+
const url = new URL(req.url, 'http://x');
|
|
1582
|
+
if (req.method === 'GET') {
|
|
1583
|
+
sendJson(res, 200, await runQc(cfg, {
|
|
1584
|
+
path: url.searchParams.get('path') ?? url.searchParams.get('rel'),
|
|
1585
|
+
platform: url.searchParams.get('platform') ?? undefined,
|
|
1586
|
+
kind: url.searchParams.get('kind') ?? undefined,
|
|
1587
|
+
live: url.searchParams.get('live') !== '0' && url.searchParams.get('live') !== 'false',
|
|
1588
|
+
}));
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (req.method === 'POST') {
|
|
1592
|
+
const body = await readJson(req);
|
|
1593
|
+
sendJson(res, 200, await runQc(cfg, {
|
|
1594
|
+
path: body.path ?? body.rel ?? url.searchParams.get('path'),
|
|
1595
|
+
platform: body.platform,
|
|
1596
|
+
kind: body.kind,
|
|
1597
|
+
}));
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
throw new Error('仅支持 GET/POST');
|
|
1601
|
+
}),
|
|
1602
|
+
|
|
1171
1603
|
makeRoute(`/${NAME}/api/control`, async (req, res) => {
|
|
1172
1604
|
if (req.method !== 'POST') throw new Error('仅支持 POST');
|
|
1173
1605
|
const cfg = await loadConfig();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-artcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Drive the open-source ArtCraft studio suite (
|
|
5
|
+
"description": "Drive the open-source ArtCraft studio suite (13 family apps: PhotoCraft, VectorCraft, FilmCraft, LightCraft, PdfCraft, EffectCraft, DesignCraft, WordCraft, DeckCraft, GridCraft, CADCraft, SoundCraft, FigCraft) from DeepSeek Harness: agent tools, a same-origin HTTP API, a bundled craft skill pack, and plugins-page status with self-update.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
8
8
|
"dsh-plugin",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"photocraft",
|
|
11
11
|
"filmcraft",
|
|
12
12
|
"effectcraft",
|
|
13
|
+
"skills",
|
|
13
14
|
"automation",
|
|
14
15
|
"ai-agents"
|
|
15
16
|
],
|
|
@@ -28,11 +29,14 @@
|
|
|
28
29
|
"exports": {
|
|
29
30
|
".": "./index.js",
|
|
30
31
|
"./client": "./client.js",
|
|
32
|
+
"./skills": "./skills.js",
|
|
31
33
|
"./package.json": "./package.json"
|
|
32
34
|
},
|
|
33
35
|
"files": [
|
|
34
36
|
"index.js",
|
|
35
37
|
"client.js",
|
|
38
|
+
"skills.js",
|
|
39
|
+
"skills",
|
|
36
40
|
"cordis.patch.yml",
|
|
37
41
|
"artcraft.config.json.example",
|
|
38
42
|
"apps",
|