@gobing-ai/knowledge-kit 0.1.11 → 0.1.13
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 +2372 -1722
- 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 +15 -7
- 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-scheduled-run.ts +91 -41
- package/plugins/kk/scripts/daily-stages.ts +394 -53
- 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/content-safety/references/platforms/podcast-salty.md +41 -0
- package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -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-collect.yaml +3 -3
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +21 -19
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +163 -63
- 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 +53 -24
- package/plugins/publishings/surfdash-pub/src/index.ts +72 -25
- 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 +176 -32
- package/plugins/publishings/wechatmp-pub/src/flow.ts +298 -52
- package/plugins/publishings/weibo-pub/dist/index.js +100 -7
- package/plugins/publishings/weibo-pub/src/index.ts +111 -11
- package/plugins/publishings/x-pub/dist/index.js +7 -0
- package/plugins/publishings/x-pub/src/index.ts +11 -0
- package/plugins/publishings/xhs-pub/dist/index.js +164 -12
- package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
- package/plugins/publishings/xhs-pub/src/page.ts +26 -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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Salty Podcast (podcast-salty) — platform reference
|
|
2
|
+
|
|
3
|
+
`podcast.salty.vip` 是本项目自有的播客站点(Robin Min 自有域名,microfeed 实例,节目「每日AI乱炖」,RSS `/rss/`)。
|
|
4
|
+
这是**自有渠道**,不是第三方分发平台:没有平台审核队列、字级词表或收听量干预可引用。
|
|
5
|
+
本文件存在的意义是把「自有播客宿主没有平台规则」这件事写清楚,避免审核流程把「无三方平台规则」
|
|
6
|
+
误判成证据缺口并默认 BLOCK。
|
|
7
|
+
|
|
8
|
+
| Field | Value |
|
|
9
|
+
|-------|-------|
|
|
10
|
+
| Official source | 自有站点,无第三方协议页;本文件由站点维护者(Robin Min)本人核定 |
|
|
11
|
+
| Scope | `podcast.salty.vip` 节目标题、音频、show notes、封面(RSS 输出随 item 状态变化) |
|
|
12
|
+
| Effective date | 本文件创建日即生效(自有平台规则由维护者裁定) |
|
|
13
|
+
| Last verified | 2026-11-05(创建) |
|
|
14
|
+
| Evidence status | 维护者本人口径,非三方引用;无字级禁用词表 |
|
|
15
|
+
|
|
16
|
+
## Evidence distinctions
|
|
17
|
+
|
|
18
|
+
- **维护者裁定(本文件的权威来源)**:播客节目是自有域名下的小型日更节目,采用与本项目其他
|
|
19
|
+
渠一致的通用内容纪律;没有平台方提供的可引用条款。
|
|
20
|
+
- **与 apple-podcasts 的边界(AP-06 不在此生效)**:Apple Podcasts 目录收录是第三方目录的准入
|
|
21
|
+
问题,由维护者人工决定,不由本 pipeline 控制;本 host 的 RSS 只是自有分发的出口。对未核实
|
|
22
|
+
的估值/数字,节目文案应保持项目既有的 hedging 与出处注明(这是事实纪律,不是平台准入规则),
|
|
23
|
+
但**不要**因为「聚合页转述未读原文」而 block——那属于 sourcing note,由维护者自行复核。
|
|
24
|
+
- **通用纪律(沿用技能默认)**:AI 声明、来源可靠性、隐私/PII、医疗/投资类内容红线全部沿用
|
|
25
|
+
技能 default;本文件不新增平台规则。
|
|
26
|
+
|
|
27
|
+
## Review items
|
|
28
|
+
|
|
29
|
+
| ID | Item | Grade |
|
|
30
|
+
|----|------|-------|
|
|
31
|
+
| PS-01 | 来源诚实性:show notes 中的对外引用(媒体/论文/Blog)须给出真实出处;转述链保持既有 hedging;未读原文的聚合转述记 note,不 push block。 | note |
|
|
32
|
+
| PS-02 | 数字与事实核对:与源链接一致的数字/日期/机构名冲突时按技能通用规则处理(correctable → revise;冲突无解 → block)。 | note→judge |
|
|
33
|
+
| PS-03 | 可识别个人信息(听众或第三方的私人信息) | desensitize→block if unfixable |
|
|
34
|
+
| PS-04 | 医疗/投资劝诱内容技能默认红线不变(未核实估值→保持 hedging 并注明出处,而非删除条目)。 | default |
|
|
35
|
+
|
|
36
|
+
## Open questions / escalation
|
|
37
|
+
|
|
38
|
+
- 自有播客无第三方审核 → 本文件不应产生 `evidenceGap: true` 类 BLOCK;任何「podcast-salty 无
|
|
39
|
+
reference 文件」的发现都指向本文件缺失,而不是内容风险。
|
|
40
|
+
- 若未来节目上线商业化、赞助或接入第三方目录(Apple/小宇宙实际分配),本文件需要为相应渠道
|
|
41
|
+
追加准入条款。
|