@gobing-ai/knowledge-kit 0.1.11 → 0.1.12
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/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +2359 -1721
- package/package.json +1 -1
- package/plugins/generations/image-gen/README.md +108 -0
- package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/dist/index.js +102 -58
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
- package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
- package/plugins/generations/image-gen/src/presets.ts +6 -3
- package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
- package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
- package/plugins/ingestions/web-search/src/index.ts +1 -1
- package/plugins/kk/README.md +18 -0
- package/plugins/kk/commands/image-extract.md +1 -0
- package/plugins/kk/commands/image-generate.md +1 -0
- package/plugins/kk/commands/tell-me.md +1 -0
- package/plugins/kk/commands/workflow-run.md +7 -1
- package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
- package/plugins/kk/decisions/kk-itc.yaml +6 -4
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
- package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
- package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
- package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
- package/plugins/kk/scripts/daily-stages.ts +346 -48
- package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
- package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
- package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
- package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
- package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/content-judge/SKILL.md +1 -0
- package/plugins/kk/skills/content-safety/SKILL.md +1 -0
- package/plugins/kk/skills/explain-things/SKILL.md +1 -0
- package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
- package/plugins/kk/skills/publish/SKILL.md +1 -0
- package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
- package/plugins/kk/skills/storm-research/SKILL.md +1 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
- package/plugins/kk/skills/topic/SKILL.md +1 -0
- package/plugins/kk/skills/web-automation/SKILL.md +1 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +9 -7
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
- package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
- package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
- package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
- package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
- package/plugins/kk/workflows/kk-itc.yaml +36 -4
- package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
- package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
- package/plugins/publishings/linkedin-pub/package.json +19 -0
- package/plugins/publishings/linkedin-pub/plugin.json +12 -0
- package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
- package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
- package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +3 -1
- package/plugins/publishings/threads-pub/dist/index.js +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +12 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +5 -2
- package/plugins/publishings/wechatmp-pub/src/flow.ts +11 -3
- package/plugins/publishings/x-pub/dist/index.js +7 -0
- package/plugins/publishings/x-pub/src/index.ts +11 -0
|
@@ -1442,6 +1442,180 @@ function dailyPublishPartial(args: string[]): void {
|
|
|
1442
1442
|
* `KK_SURFDASH_PATCH_OFFLINE=1` keeps fixture tests hermetic — the plugin would otherwise write to,
|
|
1443
1443
|
* commit and push a real checkout.
|
|
1444
1444
|
*/
|
|
1445
|
+
/**
|
|
1446
|
+
* `publish-matrix-partial` (0333): a partial/none window leaves an explicit recovery marker.
|
|
1447
|
+
*
|
|
1448
|
+
* Reads the window's receipt-derived outcome (written by `publish-matrix-classify`) — the legacy
|
|
1449
|
+
* `_17_publish_result.json` aggregate is never an input — names the exact failed target ids, and
|
|
1450
|
+
* points at the matrix rerun contract (`kk stage run publish-matrix-send <work_dir> --window <w>
|
|
1451
|
+
* --retry`). The rerun is receipt-scoped: `--retry` resends only effect-none failures, never a
|
|
1452
|
+
* succeeded or live-uncertain row, so there is no `--retry-from`/`_17` style file replay. A full
|
|
1453
|
+
* window deletes any stale marker from a previous attempt, so the report never re-flags recovered
|
|
1454
|
+
* targets.
|
|
1455
|
+
*/
|
|
1456
|
+
function publishMatrixPartial(args: string[]): void {
|
|
1457
|
+
const flag = (name: string): string | undefined => {
|
|
1458
|
+
const i = args.indexOf(name);
|
|
1459
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
1460
|
+
};
|
|
1461
|
+
const workDir = args[0] ?? '';
|
|
1462
|
+
const window = flag('--window');
|
|
1463
|
+
if (workDir === '' || window === undefined || window === '') {
|
|
1464
|
+
failWith(1, 'usage: publish-matrix-partial <work_dir> --window <w>');
|
|
1465
|
+
}
|
|
1466
|
+
const outcomePath = join(workDir, '4-publish', `outcome.${window}.json`);
|
|
1467
|
+
const outcome = requireJson(outcomePath, 'publish-matrix-partial') as {
|
|
1468
|
+
status?: string;
|
|
1469
|
+
rows?: Array<{ id?: string; status?: string; dummy?: boolean }>;
|
|
1470
|
+
};
|
|
1471
|
+
const marker = join(workDir, '4-publish', `partial-marker.${window}.txt`);
|
|
1472
|
+
timed(workDir, 'publish-matrix-partial', () => {
|
|
1473
|
+
if (outcome.status === 'full') {
|
|
1474
|
+
if (isFile(marker)) sted('rm', marker, () => unlinkSync(marker));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const failed = (outcome.rows ?? [])
|
|
1478
|
+
.filter((row) => row.dummy !== true && row.status !== 'succeeded')
|
|
1479
|
+
.map((row) => row.id ?? '(unknown)')
|
|
1480
|
+
.join(',');
|
|
1481
|
+
sted('mkdir', dirname(marker), () => mkdirSync(dirname(marker), { recursive: true }));
|
|
1482
|
+
sted('write', marker, () =>
|
|
1483
|
+
writeRaw(
|
|
1484
|
+
marker,
|
|
1485
|
+
`window ${window} ended ${outcome.status ?? 'unknown'}: failed targets: ${failed === '' ? '(unparsed)' : failed}\n` +
|
|
1486
|
+
`recovery: kk stage run publish-matrix-send ${workDir} --window ${window} --retry (resends effect-none failures only; receipts scope the retry)\n`,
|
|
1487
|
+
),
|
|
1488
|
+
);
|
|
1489
|
+
console.error(`WARNING: partial publish — failed targets: ${failed === '' ? '(unparsed)' : failed}`);
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* `publish-matrix-patch-surfdash` (0333): patch podcast links into a real surfdash post.
|
|
1495
|
+
*
|
|
1496
|
+
* The patch payload is derived from the window's own receipts, not from legacy aggregate files and
|
|
1497
|
+
* not from a `KK_SURFDASH_ROOT` argument — the plugin resolves its checkout itself. Evidence rules:
|
|
1498
|
+
* the surfdash row must have a real (non-dummy) succeeded receipt whose aggregate metadata carries
|
|
1499
|
+
* an absolute `postPath`; without that there is nothing to patch and the stage records an explicit
|
|
1500
|
+
* skip result instead of fabricating one. Podcast links ride along only when the podcast row is
|
|
1501
|
+
* real too — a dummy or missing episode yields explicit empty `coverUrl`/`showNotesUrl`, never a
|
|
1502
|
+
* fabricated link. `KK_SURFDASH_PATCH_OFFLINE=1` keeps rehearsal runs hermetic the same way the
|
|
1503
|
+
* legacy voice2 stage does.
|
|
1504
|
+
*/
|
|
1505
|
+
function publishMatrixPatchSurfdash(args: string[]): void {
|
|
1506
|
+
const flag = (name: string): string | undefined => {
|
|
1507
|
+
const i = args.indexOf(name);
|
|
1508
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
1509
|
+
};
|
|
1510
|
+
const workDir = args[0] ?? '';
|
|
1511
|
+
const window = flag('--window');
|
|
1512
|
+
if (workDir === '' || window === undefined || window === '') {
|
|
1513
|
+
failWith(1, 'usage: publish-matrix-patch-surfdash <work_dir> --window <w>');
|
|
1514
|
+
}
|
|
1515
|
+
const publishDir = join(workDir, '4-publish');
|
|
1516
|
+
const resultPath = join(publishDir, 'patch-surfdash.result.json');
|
|
1517
|
+
const writeResult = (value: Record<string, unknown>): void => {
|
|
1518
|
+
sted('write', resultPath, () => writeJqText(resultPath, value));
|
|
1519
|
+
};
|
|
1520
|
+
const receipt = (id: string): Record<string, unknown> | null =>
|
|
1521
|
+
readJson(join(publishDir, 'receipts', `${id}.json`));
|
|
1522
|
+
const aggregateTarget = (receiptPath: string | undefined): Record<string, unknown> => {
|
|
1523
|
+
const aggregate = receiptPath === undefined ? null : readJson(receiptPath);
|
|
1524
|
+
const targets = aggregate?.targets;
|
|
1525
|
+
if (!Array.isArray(targets) || targets.length === 0 || targets[0] === null || typeof targets[0] !== 'object') {
|
|
1526
|
+
return {};
|
|
1527
|
+
}
|
|
1528
|
+
return targets[0] as Record<string, unknown>;
|
|
1529
|
+
};
|
|
1530
|
+
timed(workDir, 'publish-matrix-patch-surfdash', () => {
|
|
1531
|
+
const outcome = readJson(join(publishDir, `outcome.${window}.json`)) as {
|
|
1532
|
+
rows?: Array<{ id?: string }>;
|
|
1533
|
+
} | null;
|
|
1534
|
+
const rowId = (prefix: string): string | undefined =>
|
|
1535
|
+
(outcome?.rows ?? []).map((row) => row.id ?? '').find((id) => id.startsWith(prefix));
|
|
1536
|
+
const sdId = rowId('surfdash-pub');
|
|
1537
|
+
const podId = rowId('podcast-pub');
|
|
1538
|
+
if (sdId === undefined) {
|
|
1539
|
+
const error = 'skipped: no surfdash target in this window — nothing to patch';
|
|
1540
|
+
writeResult({ ok: false, target: 'surfdash-pub', error });
|
|
1541
|
+
console.error(`patch-surfdash: ${error}`);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const sdReceipt = receipt(sdId);
|
|
1545
|
+
const sdReal = sdReceipt !== null && sdReceipt?.dummy !== true && sdReceipt?.status === 'succeeded';
|
|
1546
|
+
const sdTarget = aggregateTarget(
|
|
1547
|
+
typeof sdReceipt?.aggregatePath === 'string' ? sdReceipt.aggregatePath : undefined,
|
|
1548
|
+
);
|
|
1549
|
+
const postPath = (sdTarget.metadata as Record<string, unknown> | undefined)?.postPath;
|
|
1550
|
+
if (!sdReal || typeof postPath !== 'string' || postPath === '') {
|
|
1551
|
+
const error =
|
|
1552
|
+
sdReceipt === null
|
|
1553
|
+
? 'skipped: no surfdash publish receipt — nothing to patch'
|
|
1554
|
+
: typeof postPath !== 'string' || postPath === ''
|
|
1555
|
+
? 'skipped: surfdash receipt carries no real postPath — nothing to patch'
|
|
1556
|
+
: 'skipped: surfdash receipt is dummy — nothing to patch';
|
|
1557
|
+
writeResult({ ok: false, target: 'surfdash-pub', error });
|
|
1558
|
+
console.error(`patch-surfdash: ${error}`);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
// Links ride along only from real podcast evidence; dummy/missing episodes patch as
|
|
1562
|
+
// explicit empty strings — the patch must never fabricate a link.
|
|
1563
|
+
const podReceipt = podId === undefined ? null : receipt(podId);
|
|
1564
|
+
const podReal = podReceipt !== null && podReceipt?.dummy !== true && podReceipt?.status === 'succeeded';
|
|
1565
|
+
const podTarget = aggregateTarget(
|
|
1566
|
+
typeof podReceipt?.aggregatePath === 'string' ? podReceipt.aggregatePath : undefined,
|
|
1567
|
+
);
|
|
1568
|
+
const podMeta = (podTarget.metadata ?? {}) as Record<string, unknown>;
|
|
1569
|
+
const runDate = String(readJson(join(publishDir, `outcome.${window}.json`))?.runDate ?? '');
|
|
1570
|
+
const payload = join(publishDir, 'patch-surfdash.payload.json');
|
|
1571
|
+
sted('write', payload, () =>
|
|
1572
|
+
writeJqText(payload, {
|
|
1573
|
+
body: '',
|
|
1574
|
+
options: {
|
|
1575
|
+
operation: 'patch',
|
|
1576
|
+
postPath,
|
|
1577
|
+
coverUrl: podReal && typeof podMeta.imageUrl === 'string' ? podMeta.imageUrl : '',
|
|
1578
|
+
showNotesUrl: podReal && typeof podTarget.url === 'string' ? podTarget.url : '',
|
|
1579
|
+
...(runDate.length === 8
|
|
1580
|
+
? { dateIso: `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` }
|
|
1581
|
+
: runDate.length === 10
|
|
1582
|
+
? { dateIso: runDate }
|
|
1583
|
+
: {}),
|
|
1584
|
+
},
|
|
1585
|
+
}),
|
|
1586
|
+
);
|
|
1587
|
+
if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
|
|
1588
|
+
// Offline rehearsals still derive the payload (evidence), but never invoke the plugin.
|
|
1589
|
+
writeResult({ ok: false, target: 'surfdash-pub', error: 'skipped: offline (KK_SURFDASH_PATCH_OFFLINE=1)' });
|
|
1590
|
+
console.error(
|
|
1591
|
+
'patch-surfdash: offline — payload derived, plugin patch skipped (KK_SURFDASH_PATCH_OFFLINE=1)',
|
|
1592
|
+
);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const status = kk(['executor', 'run', 'surfdash-pub', '--in', payload, '--out', resultPath], {
|
|
1596
|
+
unset: NESTED_SPUR_UNSET,
|
|
1597
|
+
soft: true,
|
|
1598
|
+
});
|
|
1599
|
+
const result = readJson(resultPath);
|
|
1600
|
+
if (result?.ok === true) {
|
|
1601
|
+
const meta = (result.metadata ?? {}) as Record<string, unknown>;
|
|
1602
|
+
console.error(
|
|
1603
|
+
`patch-surfdash: ${String(result.message ?? 'patched')} — cms-resync ${String(meta.cmsResync ?? 'ok')}`,
|
|
1604
|
+
);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const meta = (result?.metadata ?? {}) as Record<string, unknown>;
|
|
1608
|
+
if (meta.cmsResync === 'failed') {
|
|
1609
|
+
// The posts are patched and committed; only the CMS re-sync (a soft channel) failed.
|
|
1610
|
+
console.error(
|
|
1611
|
+
`patch-surfdash: WARNING: cms re-sync failed — ${String(result?.error ?? `exit ${status}`)}; fix: kk executor run surfdash-pub --in ${payload} --out ${resultPath}`,
|
|
1612
|
+
);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
failWith(1, `patch-surfdash: ${String(result?.error ?? `surfdash-pub patch exited ${status}`)}`);
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1445
1619
|
function dailyPatchSurfdash(args: string[]): void {
|
|
1446
1620
|
const [workDir = '', runDate = '', surfdashRoot = '', pluginsPath = ''] = args;
|
|
1447
1621
|
if (!surfdashRoot || !existsSync(surfdashRoot)) {
|
|
@@ -1718,6 +1892,11 @@ if (import.meta.main)
|
|
|
1718
1892
|
dailyPreflight(rest);
|
|
1719
1893
|
break;
|
|
1720
1894
|
}
|
|
1895
|
+
case 'daily-safety-manifest': {
|
|
1896
|
+
const { dailySafetyManifest } = await loadDailyStages();
|
|
1897
|
+
dailySafetyManifest(rest);
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1721
1900
|
case 'daily-safety-prep': {
|
|
1722
1901
|
const { dailySafetyPrep } = await loadDailyStages();
|
|
1723
1902
|
dailySafetyPrep(rest);
|
|
@@ -1768,6 +1947,11 @@ if (import.meta.main)
|
|
|
1768
1947
|
dailyQualityReport(rest);
|
|
1769
1948
|
break;
|
|
1770
1949
|
}
|
|
1950
|
+
case 'daily-quality-verdict': {
|
|
1951
|
+
const { dailyQualityVerdict } = await loadDailyStages();
|
|
1952
|
+
dailyQualityVerdict(rest);
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1771
1955
|
case 'daily-publish-prep': {
|
|
1772
1956
|
const { dailyPublishPrep } = await loadDailyStages();
|
|
1773
1957
|
dailyPublishPrep(rest);
|
|
@@ -1820,6 +2004,12 @@ if (import.meta.main)
|
|
|
1820
2004
|
case 'daily-publish-partial':
|
|
1821
2005
|
dailyPublishPartial(rest);
|
|
1822
2006
|
break;
|
|
2007
|
+
case 'publish-matrix-partial':
|
|
2008
|
+
publishMatrixPartial(rest);
|
|
2009
|
+
break;
|
|
2010
|
+
case 'publish-matrix-patch-surfdash':
|
|
2011
|
+
publishMatrixPatchSurfdash(rest);
|
|
2012
|
+
break;
|
|
1823
2013
|
case 'daily-run-report':
|
|
1824
2014
|
dailyRunReport(rest);
|
|
1825
2015
|
break;
|
|
@@ -41,8 +41,10 @@ function readJson(path: string): JsonObject | null {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
/** 0331: scope-aware artifact root — `24-script` (or empty) keeps the drafts paths byte-identical. */
|
|
45
|
+
function safetyPath(workDir: string, scopeDir: string, runDate: string, name: string): string {
|
|
46
|
+
const dir = scopeDir.trim() === '' || scopeDir.trim() === '24-script' ? '24-script' : scopeDir.trim();
|
|
47
|
+
return join(workDir, dir, `${runDate}_09_${name}`);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function nonEmptyString(value: unknown): value is string {
|
|
@@ -59,9 +61,9 @@ function sortedUnique(values: string[]): string[] | null {
|
|
|
59
61
|
return unique.length === values.length ? unique.sort() : null;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
function reportValid(workDir: string, runDate: string): string | null {
|
|
63
|
-
const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
|
|
64
|
-
const report = readJson(safetyPath(workDir, runDate, 'safety-review_report.json'));
|
|
64
|
+
function reportValid(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
65
|
+
const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
|
|
66
|
+
const report = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_report.json'));
|
|
65
67
|
if (request === null || report === null) return 'review request or report is missing/invalid JSON';
|
|
66
68
|
|
|
67
69
|
const requested = stringList(request.platforms);
|
|
@@ -100,8 +102,8 @@ function reportValid(workDir: string, runDate: string): string | null {
|
|
|
100
102
|
return null;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
function judgeValid(workDir: string, runDate: string): string | null {
|
|
104
|
-
const verdict = readJson(safetyPath(workDir, runDate, 'safety-judge_verdict.json'));
|
|
105
|
+
function judgeValid(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
106
|
+
const verdict = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-judge_verdict.json'));
|
|
105
107
|
if (verdict === null) return 'judge verdict is missing/invalid JSON';
|
|
106
108
|
if (!VERDICTS.has(String(verdict.verdict))) return 'judge verdict value is invalid';
|
|
107
109
|
if (!CONFIDENCE.has(String(verdict.confidence))) return 'judge confidence is missing/invalid';
|
|
@@ -149,10 +151,10 @@ function judgeValid(workDir: string, runDate: string): string | null {
|
|
|
149
151
|
* Only the gate's persisted status represents a real content-safety escalation, so unattended mode
|
|
150
152
|
* rejects that status while attended mode leaves it for the caller's pause node.
|
|
151
153
|
*/
|
|
152
|
-
function publishValid(workDir: string, runDate: string, unattendedArg: string): string | null {
|
|
154
|
+
function publishValid(workDir: string, scopeDir: string, runDate: string, unattendedArg: string): string | null {
|
|
153
155
|
if (unattendedArg !== 'true' && unattendedArg !== 'false') return 'unattended must be true or false';
|
|
154
|
-
const verdictToken = safetyPath(workDir, runDate, 'safety-verdict.status');
|
|
155
|
-
const gateStatusPath = safetyPath(workDir, runDate, 'safety-judge_status.txt');
|
|
156
|
+
const verdictToken = safetyPath(workDir, scopeDir, runDate, 'safety-verdict.status');
|
|
157
|
+
const gateStatusPath = safetyPath(workDir, scopeDir, runDate, 'safety-judge_status.txt');
|
|
156
158
|
let token = '';
|
|
157
159
|
let gateStatus = '';
|
|
158
160
|
try {
|
|
@@ -171,27 +173,62 @@ function publishValid(workDir: string, runDate: string, unattendedArg: string):
|
|
|
171
173
|
return null;
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
function draftDigest(workDir: string, runDate: string): string | null {
|
|
175
|
-
const
|
|
176
|
-
|
|
176
|
+
function draftDigest(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
177
|
+
const hash = createHash('sha256');
|
|
178
|
+
if (scopeDir.trim() === '' || scopeDir.trim() === '24-script') {
|
|
179
|
+
const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|
|
180
|
+
const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
|
|
181
|
+
try {
|
|
182
|
+
hash.update(readFileSync(article)).update('\0');
|
|
183
|
+
hash.update(readFileSync(script)).update('\0');
|
|
184
|
+
return hash.digest('hex');
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Variants scope: digest over the manifest's sorted keys + current bytes (mirrors the
|
|
190
|
+
// daily-stages scopeDigest algorithm, so request/guard/verdict all bind the same bytes).
|
|
177
191
|
try {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
|
|
193
|
+
variants?: Record<string, { path?: unknown }>;
|
|
194
|
+
} | null;
|
|
195
|
+
if (manifest === null || !isObject(manifest.variants)) return null;
|
|
196
|
+
for (const key of Object.keys(manifest.variants).sort()) {
|
|
197
|
+
const path = join(workDir, String(manifest.variants[key].path));
|
|
198
|
+
hash.update(key).update('\0').update(readFileSync(path)).update('\0');
|
|
199
|
+
}
|
|
181
200
|
return hash.digest('hex');
|
|
182
201
|
} catch {
|
|
183
202
|
return null;
|
|
184
203
|
}
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
function revisionValid(workDir: string, runDate: string): string | null {
|
|
188
|
-
const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
|
|
206
|
+
function revisionValid(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
207
|
+
const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
|
|
208
|
+
if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
|
|
209
|
+
const isVariants = scopeDir.trim() !== '' && scopeDir.trim() !== '24-script';
|
|
210
|
+
const digest = draftDigest(workDir, scopeDir, runDate);
|
|
211
|
+
if (digest === null) {
|
|
212
|
+
return isVariants
|
|
213
|
+
? 'a manifest-listed variant is missing/unreadable'
|
|
214
|
+
: 'revised article or VoiceScript is missing/unreadable';
|
|
215
|
+
}
|
|
216
|
+
if (digest === request.digest) return isVariants ? 'variant digest is unchanged' : 'draft digest is unchanged';
|
|
217
|
+
if (isVariants) {
|
|
218
|
+
const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
|
|
219
|
+
variants?: Record<string, { path?: unknown }>;
|
|
220
|
+
} | null;
|
|
221
|
+
if (manifest === null || !isObject(manifest.variants)) return 'variants manifest is missing/malformed';
|
|
222
|
+
for (const entry of Object.values(manifest.variants)) {
|
|
223
|
+
const value = readJson(join(workDir, String(entry.path)));
|
|
224
|
+
if (value === null || !nonEmptyString(value.title) || !nonEmptyString(value.body)) {
|
|
225
|
+
return 'revised variant title/body is missing';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
189
230
|
const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|
|
190
231
|
const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
|
|
191
|
-
if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
|
|
192
|
-
if (draftDigest(workDir, runDate) === null) return 'revised article or VoiceScript is missing/unreadable';
|
|
193
|
-
if (draftDigest(workDir, runDate) === request.digest) return 'draft digest is unchanged';
|
|
194
|
-
|
|
195
232
|
const article = readJson(articlePath);
|
|
196
233
|
if (article === null || !nonEmptyString(article.title) || !nonEmptyString(article.body)) {
|
|
197
234
|
return 'revised article title/body is missing';
|
|
@@ -207,22 +244,27 @@ function revisionValid(workDir: string, runDate: string): string | null {
|
|
|
207
244
|
}
|
|
208
245
|
|
|
209
246
|
function main(): number {
|
|
210
|
-
|
|
247
|
+
// 0331: an optional trailing scope dir re-roots every artifact path (variants review reads
|
|
248
|
+
// 4-safety-variants); publish-valid keeps `unattended` in the 4th slot it has always had.
|
|
249
|
+
const [kind = '', workDir = '', runDate = '', fourth = '', fifth = ''] = process.argv.slice(2);
|
|
250
|
+
const isPublish = kind === 'publish-valid';
|
|
251
|
+
const unattended = isPublish ? fourth : 'false';
|
|
252
|
+
const scopeDir = isPublish ? fifth : fourth;
|
|
211
253
|
if (!kind || !workDir || !runDate) {
|
|
212
254
|
console.error(
|
|
213
|
-
'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended]',
|
|
255
|
+
'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended] [scope_dir]',
|
|
214
256
|
);
|
|
215
257
|
return 2;
|
|
216
258
|
}
|
|
217
259
|
const reason =
|
|
218
260
|
kind === 'report-valid'
|
|
219
|
-
? reportValid(workDir, runDate)
|
|
261
|
+
? reportValid(workDir, scopeDir, runDate)
|
|
220
262
|
: kind === 'judge-valid'
|
|
221
|
-
? judgeValid(workDir, runDate)
|
|
263
|
+
? judgeValid(workDir, scopeDir, runDate)
|
|
222
264
|
: kind === 'revision-valid'
|
|
223
|
-
? revisionValid(workDir, runDate)
|
|
265
|
+
? revisionValid(workDir, scopeDir, runDate)
|
|
224
266
|
: kind === 'publish-valid'
|
|
225
|
-
? publishValid(workDir, runDate, unattended)
|
|
267
|
+
? publishValid(workDir, scopeDir, runDate, unattended)
|
|
226
268
|
: 'unknown predicate';
|
|
227
269
|
if (reason !== null) {
|
|
228
270
|
console.error(`safety-artifact-check: ${kind}: ${reason}`);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* voice3-publish-window — cron-safe launcher for the voice3 shadow publish window (task 0334).
|
|
4
|
+
*
|
|
5
|
+
* WHY a launcher (audit F08/F09/F10, docs/design/kk-daily-ai-voice3.md):
|
|
6
|
+
* - cron ends a command at the first UNESCAPED % and feeds the rest to stdin, so any inline
|
|
7
|
+
* `$(date +%Y%m%d)` shell in a crontab entry silently truncates the command. Date math and
|
|
8
|
+
* flag assembly live here, in plain TypeScript, behind zero percent signs.
|
|
9
|
+
* - the publish window must read the GENERATION bundle (`<works>/kk-daily-ai-voice2/<date>/
|
|
10
|
+
* bundle.json` — voice3 prepares under the voice2 namespace until the operator's cutover
|
|
11
|
+
* changes it), never the publish3 output directory that earlier entries pointed at.
|
|
12
|
+
* - the CN consent token list is validated against the pinned preset's WINDOW rows before any
|
|
13
|
+
* spur process starts; the old baked `wechatmp-pub,xhs-pub,…` list (tokens that have no
|
|
14
|
+
* cn-peak row) is refused up front.
|
|
15
|
+
*
|
|
16
|
+
* Contract: everything before `spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml`
|
|
17
|
+
* is preparation and refusal; the only side effects are refusal messages and, on success, the
|
|
18
|
+
* single spur exec. `KK_SPUR_BIN` pins the spur binary (same convention as board/capture.ts and
|
|
19
|
+
* decision-hub.ts); `--dry-run` prints the forwarded vars and stops (rehearsal aid).
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join, resolve } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const USAGE =
|
|
28
|
+
'usage: voice3-publish-window.ts --window <after-generate|cn-peak|overseas-rss> [--run-date <YYYY-MM-DD|YYYYMMDD>] ' +
|
|
29
|
+
'[--preset <slug>] [--live-channels a,b] [--work-dir <dir>] [--dummy] [--dry-run]';
|
|
30
|
+
|
|
31
|
+
const WINDOWS = new Set(['after-generate', 'cn-peak', 'overseas-rss']);
|
|
32
|
+
|
|
33
|
+
function failWith(code: number, message: string): never {
|
|
34
|
+
console.error(message);
|
|
35
|
+
process.exit(code);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expandHome(raw: string): string {
|
|
39
|
+
return raw === '~' || raw.startsWith('~/') ? join(homedir(), raw.slice(1)) : raw;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** KK_WORKS_DIR env > config works_dir > <cwd>/.works — the kk-workflow-stages precedence. */
|
|
43
|
+
function resolveWorksDir(): string {
|
|
44
|
+
const fromEnv = process.env.KK_WORKS_DIR;
|
|
45
|
+
if (fromEnv) return expandHome(fromEnv);
|
|
46
|
+
const configPath = process.env.KK_CONFIG || join(homedir(), '.config/kk/config.yaml');
|
|
47
|
+
if (existsSync(configPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = Bun.YAML.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown> | null;
|
|
50
|
+
const worksDir = parsed !== null && typeof parsed === 'object' ? parsed.works_dir : undefined;
|
|
51
|
+
if (typeof worksDir === 'string' && worksDir !== '') return expandHome(worksDir);
|
|
52
|
+
} catch {
|
|
53
|
+
// fall through to the compiled default
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return resolve(process.cwd(), '.works');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A real calendar day as `YYYY-MM-DD`, or undefined (mirrors publish-matrix normalizeRunDate). */
|
|
60
|
+
function normalizeRunDate(raw: string): string | undefined {
|
|
61
|
+
const match = /^(\d{4})-?(\d{2})-?(\d{2})$/.exec(raw.trim());
|
|
62
|
+
if (match === null) return undefined;
|
|
63
|
+
const iso = `${match[1]}-${match[2]}-${match[3]}`;
|
|
64
|
+
const parsed = new Date(`${iso}T00:00:00Z`);
|
|
65
|
+
if (
|
|
66
|
+
Number.isNaN(parsed.getTime()) ||
|
|
67
|
+
parsed.getUTCFullYear() !== Number(match[1]) ||
|
|
68
|
+
parsed.getUTCMonth() + 1 !== Number(match[2]) ||
|
|
69
|
+
parsed.getUTCDate() !== Number(match[3])
|
|
70
|
+
) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return iso;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Today (offset 0) or yesterday (offset -1) in America/Los_Angeles — the episodes' own day. */
|
|
77
|
+
function losAngelesDate(offsetDays: number): string {
|
|
78
|
+
const instant = new Date(Date.now() + offsetDays * 24 * 60 * 60 * 1000);
|
|
79
|
+
// en-CA renders YYYY-MM-DD; Intl does the zone math cron cannot do inline.
|
|
80
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Los_Angeles' }).format(instant);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `--flag value`, or undefined. */
|
|
84
|
+
function flagValue(args: string[], flag: string): string | undefined {
|
|
85
|
+
const index = args.indexOf(flag);
|
|
86
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function main(): void {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
92
|
+
console.log(USAGE);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
const window = flagValue(args, '--window');
|
|
96
|
+
if (window === undefined || !WINDOWS.has(window))
|
|
97
|
+
failWith(1, `${USAGE}\nvoice3-publish-window: --window must be one of ${[...WINDOWS].join('|')}`);
|
|
98
|
+
const dummy = args.includes('--dummy');
|
|
99
|
+
const dryRun = args.includes('--dry-run');
|
|
100
|
+
const preset = flagValue(args, '--preset');
|
|
101
|
+
const liveChannelsRaw = flagValue(args, '--live-channels');
|
|
102
|
+
const workDirOverride = flagValue(args, '--work-dir');
|
|
103
|
+
|
|
104
|
+
// R1: run date = explicit flag, else window-derived LA day (overseas reads the episode that
|
|
105
|
+
// landed after the 07:00 tick, i.e. yesterday).
|
|
106
|
+
const runDateRaw = flagValue(args, '--run-date');
|
|
107
|
+
let runDate: string | undefined;
|
|
108
|
+
if (runDateRaw !== undefined && runDateRaw.trim() !== '') {
|
|
109
|
+
runDate = normalizeRunDate(runDateRaw);
|
|
110
|
+
if (runDate === undefined)
|
|
111
|
+
failWith(1, `voice3-publish-window: '${runDateRaw}' is not a valid calendar day (YYYY-MM-DD or YYYYMMDD)`);
|
|
112
|
+
} else {
|
|
113
|
+
runDate = losAngelesDate(window === 'overseas-rss' ? -1 : 0);
|
|
114
|
+
}
|
|
115
|
+
const digits = runDate.replaceAll('-', '');
|
|
116
|
+
|
|
117
|
+
if (dummy && liveChannelsRaw !== undefined) {
|
|
118
|
+
failWith(1, 'voice3-publish-window: --dummy and --live-channels are mutually exclusive');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// R3/F10: live tokens validate against the pinned preset's WINDOW rows before any spawn.
|
|
122
|
+
const liveChannels =
|
|
123
|
+
liveChannelsRaw !== undefined && liveChannelsRaw.trim() !== ''
|
|
124
|
+
? liveChannelsRaw
|
|
125
|
+
.split(',')
|
|
126
|
+
.map((token) => token.trim())
|
|
127
|
+
.filter((token) => token !== '')
|
|
128
|
+
: [];
|
|
129
|
+
if (liveChannels.length > 0 && preset === undefined) {
|
|
130
|
+
failWith(1, 'voice3-publish-window: --live-channels requires an explicit --preset to validate the window');
|
|
131
|
+
}
|
|
132
|
+
if (liveChannels.length > 0) {
|
|
133
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
134
|
+
const presetPath = join(dirname(scriptDir), 'publish-presets', `${preset}.json`);
|
|
135
|
+
if (!existsSync(presetPath))
|
|
136
|
+
failWith(1, `voice3-publish-window: preset '${preset}' not found at ${presetPath}`);
|
|
137
|
+
const parsed = JSON.parse(readFileSync(presetPath, 'utf-8')) as {
|
|
138
|
+
targets?: Array<{ plugin: string; id?: string; mode?: string; window?: string }>;
|
|
139
|
+
};
|
|
140
|
+
const targets = parsed.targets ?? [];
|
|
141
|
+
const inWindow = targets.filter((target) => (target.window ?? 'after-generate') === window);
|
|
142
|
+
const refused = liveChannels.filter((token) => {
|
|
143
|
+
const row = inWindow.find((target) => target.plugin === token || target.id === token);
|
|
144
|
+
return row === undefined || (row.mode ?? 'draft') === 'off';
|
|
145
|
+
});
|
|
146
|
+
if (refused.length > 0) {
|
|
147
|
+
failWith(
|
|
148
|
+
1,
|
|
149
|
+
`voice3-publish-window: live token(s) ${refused.join(', ')} have no active row in window '${window}' of preset '${preset}'` +
|
|
150
|
+
` (send-confirm resumes are consent-scoped per window; see docs/design/kk-daily-ai-voice3.md)`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// R1/F08: the bundle is the GENERATION artifact under the voice2 namespace, never a publish3
|
|
156
|
+
// output dir. Mismatched/missing bundles refuse before spur starts.
|
|
157
|
+
const genDir =
|
|
158
|
+
workDirOverride !== undefined && workDirOverride.trim() !== ''
|
|
159
|
+
? resolve(workDirOverride)
|
|
160
|
+
: join(resolveWorksDir(), 'kk-daily-ai-voice2', digits);
|
|
161
|
+
const bundlePath = join(genDir, 'bundle.json');
|
|
162
|
+
if (!existsSync(bundlePath)) failWith(1, `voice3-publish-window: no generation bundle at ${bundlePath}`);
|
|
163
|
+
let bundleRunDate: string | undefined;
|
|
164
|
+
try {
|
|
165
|
+
const bundle = JSON.parse(readFileSync(bundlePath, 'utf-8')) as { runDate?: string };
|
|
166
|
+
bundleRunDate = typeof bundle.runDate === 'string' ? normalizeRunDate(bundle.runDate) : undefined;
|
|
167
|
+
} catch {
|
|
168
|
+
failWith(1, `voice3-publish-window: unreadable generation bundle at ${bundlePath}`);
|
|
169
|
+
}
|
|
170
|
+
if (bundleRunDate !== runDate) {
|
|
171
|
+
failWith(
|
|
172
|
+
1,
|
|
173
|
+
`voice3-publish-window: bundle at ${bundlePath} is dated ${bundleRunDate ?? 'unknown'}, not the requested ${runDate}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const vars: Record<string, string> = {
|
|
178
|
+
work_dir: genDir,
|
|
179
|
+
run_date: digits,
|
|
180
|
+
window,
|
|
181
|
+
};
|
|
182
|
+
if (preset !== undefined && preset.trim() !== '') vars.preset = preset;
|
|
183
|
+
if (liveChannels.length > 0) vars.live_channels = liveChannels.join(',');
|
|
184
|
+
if (dummy) vars.dummy = 'true';
|
|
185
|
+
|
|
186
|
+
if (dryRun) {
|
|
187
|
+
console.log(JSON.stringify(vars, null, 2));
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const proc = spawnSync(
|
|
192
|
+
process.env.KK_SPUR_BIN || 'spur',
|
|
193
|
+
['workflow', 'run', 'plugins/kk/workflows/kk-daily-publish3.yaml', '--vars', JSON.stringify(vars)],
|
|
194
|
+
{
|
|
195
|
+
stdio: 'inherit',
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
// node spawnSync surfaces the child's code as `status` (exitCode is the async-API field).
|
|
199
|
+
process.exit(proc.status ?? 1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main();
|
|
@@ -3,6 +3,7 @@ name: article-adapt
|
|
|
3
3
|
# Description rules: front-load the leading identity phrase; one trigger per genuine
|
|
4
4
|
# branch (collapse synonym triggers into one); never restate the body's identity line.
|
|
5
5
|
description: This skill should be used when a kk-itc publish plan contains variants with adapt=true and per-variant Content files must be written — "adapt the article", "translate for the target profile", "write content.zh-article.json", or when a spur workflow agent.run step invokes kk:article-adapt. Translates title and body to the variant locale, applies the plan limits, and preserves code blocks, URLs and references; it holds no platform table.
|
|
6
|
+
requires: [cli:kk, cli:spur, local-fs]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# article-adapt
|
|
@@ -6,6 +6,7 @@ description: >-
|
|
|
6
6
|
"outline options for audio", "author a VoiceScript", "revise the script".
|
|
7
7
|
Runs briefing → outline → script with explicit operator gates; solo mode
|
|
8
8
|
ships first, dialogue/interview/narration are named extension points.
|
|
9
|
+
requires: [local-fs]
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# audio-authoring — spoken-audio authoring (create | briefing | outline | script)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: content-judge
|
|
3
3
|
description: This skill should be used when an LLM evaluation gate is needed in a knowledge-kit pipeline — "judge this content", "evaluate against rubric", "quality gate before publish", "check technical accuracy", "review brand tone", "compliance check", or when a spur workflow step of type `agent.run` targets the content-judge skill to emit a PASS/FAIL/NEEDS_REVISION verdict. Centralizes evaluation rubrics, prompt templates, and structured verdict emission for generated Content.
|
|
4
4
|
version: 0.1.0
|
|
5
|
+
requires: [cli:kk, cli:spur, local-fs]
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# content-judge — LLM-as-judge evaluation skill
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: content-safety
|
|
3
3
|
description: This skill should be used when a knowledge-kit pipeline or operator needs a content-safety / platform-admission review before publishing — "check this before publishing", "小宇宙/公众号/网易云/苹果播客 审核风险", "desensitize personal info", "verify disclosure and sourcing before publish". Detects admission risk per distribution platform, proposes honest revisions or exclusion, and escalates what it cannot verify. Quality scoring is content-judge's job.
|
|
4
|
+
requires: [local-fs]
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# content-safety — pre-publication content-safety review
|
|
@@ -7,6 +7,7 @@ description: >-
|
|
|
7
7
|
"how does X work", "ELI5", "like I'm 5", or "show me". Normal explanations are
|
|
8
8
|
concise inline views; the ELI5 register is opt-in. This is not for content authoring,
|
|
9
9
|
research, judging, publishing, or package/code review workflows.
|
|
10
|
+
requires: [local-fs]
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# explain-things
|
|
@@ -7,6 +7,7 @@ description: >-
|
|
|
7
7
|
"list the image formats and styles". Three operations — generate,
|
|
8
8
|
extract-style, list-presets — over named format and style presets, with a
|
|
9
9
|
confirmation gate before any paid generation.
|
|
10
|
+
requires: [cli:kk, cli:spur, local-fs]
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# image-authoring — image generation agent surface (generate | extract-style | list-presets)
|
|
@@ -7,6 +7,7 @@ description: >-
|
|
|
7
7
|
writing method", "generate an IT technical draft on X", "create a technical
|
|
8
8
|
article for WeChat/English platforms". Orchestrates topic (outline + draft),
|
|
9
9
|
optional storm-research, and optional content-judge without enlarging those skills.
|
|
10
|
+
requires: [cli:kk, local-fs]
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# itc-generating — IT technical draft generation (method + orchestration)
|
|
@@ -6,6 +6,7 @@ description: >-
|
|
|
6
6
|
`kk executor run` / `kk fanout` for file-cli and script channels; sequences
|
|
7
7
|
agent-default CMS channels as workflow `agent.run` steps. Does not own
|
|
8
8
|
transport.
|
|
9
|
+
requires: [cli:kk, local-fs]
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# publish — leftover channel operator surface
|