@gobing-ai/knowledge-kit 0.0.28 → 0.0.30
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 +114 -38
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +45 -5
- package/plugins/generations/daily-article-gen/src/index.ts +59 -4
- package/plugins/generations/dailynews-gen/dist/index.js +46 -9
- package/plugins/generations/dailynews-gen/src/index.ts +36 -1
- package/plugins/generations/dailynews-gen/src/script-builder.ts +15 -12
- package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
- package/plugins/generations/episode-plan-gen/dist/index.js +55 -4
- package/plugins/generations/episode-plan-gen/src/index.ts +80 -2
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/OFL.txt +92 -0
- package/plugins/generations/image-gen/assets/fonts/SOURCE.md +15 -0
- package/plugins/generations/image-gen/dist/index.js +700 -22
- package/plugins/generations/image-gen/package.json +1 -0
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +2 -2
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +19 -11
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -1
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +135 -0
- package/plugins/generations/image-gen/src/bytes.ts +116 -0
- package/plugins/generations/image-gen/src/compose/card.ts +294 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +147 -0
- package/plugins/generations/image-gen/src/compose/cmap.ts +177 -0
- package/plugins/generations/image-gen/src/index.ts +211 -13
- package/plugins/generations/image-gen/src/job.ts +12 -0
- package/plugins/generations/image-gen/src/presets.ts +53 -13
- package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
- package/plugins/generations/image-gen/src/providers/types.ts +12 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +27 -14
- package/plugins/kk/scripts/itc-stages.ts +21 -3
- package/plugins/kk/scripts/kk-workflow-stages.ts +461 -10
- package/plugins/kk/scripts/topic-boundary.json +96 -0
- package/plugins/kk/scripts/validate-voicescript.ts +15 -0
- package/plugins/kk/skills/content-judge/SKILL.md +30 -0
- package/plugins/kk/skills/content-judge/references/workflow-integration.md +9 -2
- package/plugins/kk/skills/content-safety/SKILL.md +163 -0
- package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/netease-music.md +31 -0
- package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +32 -0
- package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +35 -0
- package/plugins/kk/skills/publish/SKILL.md +1 -1
- package/plugins/kk/skills/xhs-images/SKILL.md +11 -7
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +4 -4
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +139 -24
- package/plugins/kk/workflows/kk-itc.yaml +21 -8
- package/plugins/publishings/podcast-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/src/index.ts +2 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -0
- package/plugins/publishings/xhs-pub/dist/index.js +1 -1
- package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
- package/plugins/publishings/xhs-pub/src/index.ts +1 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
- package/plugins/sp/lib/env.ts +0 -46
- package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
- package/plugins/sp/scripts/batch-preflight.mjs +0 -346
- package/plugins/sp/scripts/batch-preflight.ts +0 -459
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
- package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
- package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
- package/plugins/sp/scripts/idea-handoff.mjs +0 -22
- package/plugins/sp/scripts/idea-handoff.ts +0 -44
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
- package/plugins/sp/scripts/inline-run-setup.ts +0 -722
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
- package/plugins/sp/scripts/pr-reviewing.ts +0 -925
- package/plugins/sp/scripts/quality-gate.mjs +0 -179
- package/plugins/sp/scripts/quality-gate.ts +0 -217
- package/plugins/sp/scripts/script-contract-check.ts +0 -319
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
- package/plugins/sp/scripts/task-size-precheck.ts +0 -175
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
- package/plugins/sp/scripts/wrapup-steps.ts +0 -552
package/dist/index.js
CHANGED
|
@@ -26530,6 +26530,11 @@ function killSubtree(pid) {
|
|
|
26530
26530
|
} catch {}
|
|
26531
26531
|
}
|
|
26532
26532
|
async function killProcessTree(pid) {
|
|
26533
|
+
if (process.platform !== "win32") {
|
|
26534
|
+
try {
|
|
26535
|
+
process.kill(-pid, "SIGKILL");
|
|
26536
|
+
} catch {}
|
|
26537
|
+
}
|
|
26533
26538
|
const sweep = () => {
|
|
26534
26539
|
for (const child of listChildren(pid)) {
|
|
26535
26540
|
killSubtree(child);
|
|
@@ -26549,7 +26554,8 @@ async function spawnPluginEntryAsync(opts) {
|
|
|
26549
26554
|
cwd: opts.cwd,
|
|
26550
26555
|
env: pluginEntryEnv(opts),
|
|
26551
26556
|
stdout: "pipe",
|
|
26552
|
-
stderr: "pipe"
|
|
26557
|
+
stderr: "pipe",
|
|
26558
|
+
detached: process.platform !== "win32"
|
|
26553
26559
|
});
|
|
26554
26560
|
if (opts.timeoutMs !== undefined) {
|
|
26555
26561
|
timer = setTimeout(() => {
|
|
@@ -27021,41 +27027,70 @@ function aggregate(entries) {
|
|
|
27021
27027
|
}
|
|
27022
27028
|
return { ok: summary.failed === 0, summary, targets: entries };
|
|
27023
27029
|
}
|
|
27024
|
-
async function
|
|
27025
|
-
const
|
|
27026
|
-
|
|
27027
|
-
const
|
|
27028
|
-
|
|
27029
|
-
|
|
27030
|
-
|
|
27031
|
-
|
|
27032
|
-
|
|
27033
|
-
|
|
27034
|
-
|
|
27035
|
-
|
|
27036
|
-
|
|
27037
|
-
|
|
27038
|
-
|
|
27039
|
-
|
|
27040
|
-
|
|
27041
|
-
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
|
|
27045
|
-
|
|
27046
|
-
|
|
27047
|
-
|
|
27048
|
-
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
|
|
27052
|
-
error: err instanceof Error ? err.message : String(err),
|
|
27053
|
-
durationMs: Date.now() - started
|
|
27054
|
-
};
|
|
27055
|
-
}
|
|
27056
|
-
entries.push(entry);
|
|
27030
|
+
async function dispatchOne(target, dispatch, timeoutMs) {
|
|
27031
|
+
const started = Date.now();
|
|
27032
|
+
try {
|
|
27033
|
+
const result = timeoutMs === undefined ? await dispatch(target) : await withTimeout(dispatch(target), timeoutMs, target);
|
|
27034
|
+
const durationMs = Date.now() - started;
|
|
27035
|
+
return result.ok ? {
|
|
27036
|
+
target,
|
|
27037
|
+
ok: true,
|
|
27038
|
+
status: "published",
|
|
27039
|
+
...result.url !== undefined ? { url: result.url } : {},
|
|
27040
|
+
...result.id !== undefined ? { id: result.id } : {},
|
|
27041
|
+
...result.metadata !== undefined ? { metadata: result.metadata } : {},
|
|
27042
|
+
durationMs
|
|
27043
|
+
} : {
|
|
27044
|
+
target,
|
|
27045
|
+
ok: false,
|
|
27046
|
+
status: "failed",
|
|
27047
|
+
error: result.error ?? result.message ?? "publish failed",
|
|
27048
|
+
durationMs
|
|
27049
|
+
};
|
|
27050
|
+
} catch (err) {
|
|
27051
|
+
return {
|
|
27052
|
+
target,
|
|
27053
|
+
ok: false,
|
|
27054
|
+
status: "failed",
|
|
27055
|
+
error: err instanceof Error ? err.message : String(err),
|
|
27056
|
+
durationMs: Date.now() - started
|
|
27057
|
+
};
|
|
27057
27058
|
}
|
|
27058
|
-
|
|
27059
|
+
}
|
|
27060
|
+
function withTimeout(promise2, timeoutMs, target) {
|
|
27061
|
+
let timer;
|
|
27062
|
+
const budget = new Promise((_resolve, reject) => {
|
|
27063
|
+
timer = setTimeout(() => reject(new Error(`target '${target}' exceeded its ${timeoutMs}ms budget`)), timeoutMs);
|
|
27064
|
+
});
|
|
27065
|
+
return Promise.race([promise2, budget]).finally(() => clearTimeout(timer));
|
|
27066
|
+
}
|
|
27067
|
+
async function fanout(content, targets, dispatch, options = {}) {
|
|
27068
|
+
const outcomes = new Map;
|
|
27069
|
+
const runOne = async (target) => {
|
|
27070
|
+
outcomes.set(target, await dispatchOne(target, dispatch, options.timeoutMs));
|
|
27071
|
+
};
|
|
27072
|
+
if (options.mode === "serial") {
|
|
27073
|
+
for (const target of targets)
|
|
27074
|
+
await runOne(target);
|
|
27075
|
+
} else {
|
|
27076
|
+
const serial = new Set(options.serialTargets ?? []);
|
|
27077
|
+
const parallel = targets.filter((target) => !serial.has(target));
|
|
27078
|
+
const solo = targets.filter((target) => serial.has(target));
|
|
27079
|
+
const jobs = options.jobs !== undefined && options.jobs > 0 ? options.jobs : parallel.length;
|
|
27080
|
+
let cursor2 = 0;
|
|
27081
|
+
const worker = async () => {
|
|
27082
|
+
while (cursor2 < parallel.length) {
|
|
27083
|
+
const target = parallel[cursor2++];
|
|
27084
|
+
if (target === undefined)
|
|
27085
|
+
return;
|
|
27086
|
+
await runOne(target);
|
|
27087
|
+
}
|
|
27088
|
+
};
|
|
27089
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(jobs, parallel.length)) }, worker));
|
|
27090
|
+
for (const target of solo)
|
|
27091
|
+
await runOne(target);
|
|
27092
|
+
}
|
|
27093
|
+
return aggregate(targets.map((target) => outcomes.get(target)).filter((entry) => entry !== undefined));
|
|
27059
27094
|
}
|
|
27060
27095
|
async function readContent(fs2, path3) {
|
|
27061
27096
|
if (!await fs2.exists(path3)) {
|
|
@@ -27139,10 +27174,14 @@ async function readAggregateResult(fs2, path3) {
|
|
|
27139
27174
|
function parseArgs2(argv) {
|
|
27140
27175
|
const targets = [];
|
|
27141
27176
|
const pluginsPath = [];
|
|
27177
|
+
const serialTargets = [];
|
|
27142
27178
|
let contentPath;
|
|
27143
27179
|
let outPath;
|
|
27144
27180
|
let retryFromPath;
|
|
27145
27181
|
let pluginsRoot;
|
|
27182
|
+
let serial = false;
|
|
27183
|
+
let jobs;
|
|
27184
|
+
let targetTimeoutMs;
|
|
27146
27185
|
for (let i = 0;i < argv.length; i++) {
|
|
27147
27186
|
const arg = argv[i];
|
|
27148
27187
|
if (arg === "--content") {
|
|
@@ -27193,6 +27232,34 @@ function parseArgs2(argv) {
|
|
|
27193
27232
|
throw new Error("--plugins-path requires a directory path");
|
|
27194
27233
|
}
|
|
27195
27234
|
pluginsPath.push(value);
|
|
27235
|
+
} else if (arg === "--serial") {
|
|
27236
|
+
serial = true;
|
|
27237
|
+
} else if (arg === "--jobs") {
|
|
27238
|
+
const value = argv[++i];
|
|
27239
|
+
if (value === undefined) {
|
|
27240
|
+
throw new Error("--jobs requires a positive integer");
|
|
27241
|
+
}
|
|
27242
|
+
const parsed = Number(value);
|
|
27243
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
27244
|
+
throw new Error(`--jobs requires a positive integer, got '${value}'`);
|
|
27245
|
+
}
|
|
27246
|
+
jobs = parsed;
|
|
27247
|
+
} else if (arg === "--serial-target") {
|
|
27248
|
+
const value = argv[++i];
|
|
27249
|
+
if (value === undefined) {
|
|
27250
|
+
throw new Error("--serial-target requires a plugin name");
|
|
27251
|
+
}
|
|
27252
|
+
serialTargets.push(value);
|
|
27253
|
+
} else if (arg === "--target-timeout") {
|
|
27254
|
+
const value = argv[++i];
|
|
27255
|
+
if (value === undefined) {
|
|
27256
|
+
throw new Error("--target-timeout requires milliseconds");
|
|
27257
|
+
}
|
|
27258
|
+
const parsed = Number(value);
|
|
27259
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
27260
|
+
throw new Error(`--target-timeout requires a positive integer in milliseconds, got '${value}'`);
|
|
27261
|
+
}
|
|
27262
|
+
targetTimeoutMs = parsed;
|
|
27196
27263
|
} else {
|
|
27197
27264
|
throw new Error(`Unknown argument: ${arg}`);
|
|
27198
27265
|
}
|
|
@@ -27212,7 +27279,11 @@ function parseArgs2(argv) {
|
|
|
27212
27279
|
outPath,
|
|
27213
27280
|
...retryFromPath !== undefined ? { retryFromPath } : {},
|
|
27214
27281
|
...pluginsRoot !== undefined ? { pluginsRoot } : {},
|
|
27215
|
-
...pluginsPath.length > 0 ? { pluginsPath } : {}
|
|
27282
|
+
...pluginsPath.length > 0 ? { pluginsPath } : {},
|
|
27283
|
+
...serial ? { serial } : {},
|
|
27284
|
+
...jobs !== undefined ? { jobs } : {},
|
|
27285
|
+
...serialTargets.length > 0 ? { serialTargets } : {},
|
|
27286
|
+
...targetTimeoutMs !== undefined ? { targetTimeoutMs } : {}
|
|
27216
27287
|
};
|
|
27217
27288
|
}
|
|
27218
27289
|
async function pluginDispatch(target, contentPath, discovery) {
|
|
@@ -27315,7 +27386,12 @@ async function runFanout(argv, deps = {}) {
|
|
|
27315
27386
|
if (deps.dispatch === undefined && !validateTargets(targets, discovery)) {
|
|
27316
27387
|
return EXIT_INPUT2;
|
|
27317
27388
|
}
|
|
27318
|
-
const fresh = await fanout(content, targets, dispatch
|
|
27389
|
+
const fresh = await fanout(content, targets, dispatch, {
|
|
27390
|
+
...opts.serial === true ? { mode: "serial" } : {},
|
|
27391
|
+
...opts.jobs !== undefined ? { jobs: opts.jobs } : {},
|
|
27392
|
+
...opts.serialTargets !== undefined ? { serialTargets: opts.serialTargets } : {},
|
|
27393
|
+
...opts.targetTimeoutMs !== undefined ? { timeoutMs: opts.targetTimeoutMs } : {}
|
|
27394
|
+
});
|
|
27319
27395
|
const result = prior !== undefined ? aggregate([...skipped, ...retainedFailed, ...fresh.targets]) : fresh;
|
|
27320
27396
|
try {
|
|
27321
27397
|
await fs2.writeFile(resolve4(opts.outPath), `${JSON.stringify(result, null, 2)}
|
|
@@ -27399,7 +27475,7 @@ function withFaninOptions(cmd) {
|
|
|
27399
27475
|
return cmd.option("--in <path>", "Input Doc[] JSON file (repeatable)", collectRepeatable, []).option("--source <plugin>:<file>", "Ingestion source to spawn concurrently: plugin name and its input file (repeatable)", collectRepeatable, []).option("--source-timeout <ms>", "Per-source timeout in milliseconds (default 1800000)").option("--source-timeout-override <plugin>:<ms>", "Per-source timeout override for one named --source plugin, e.g. horizon-ingest:2700000 (repeatable; wins over --source-timeout/default)", collectRepeatable, []).requiredOption("--out <path>", "Output merged Doc[] JSON file path").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectRepeatable, []);
|
|
27400
27476
|
}
|
|
27401
27477
|
function withFanoutOptions(cmd) {
|
|
27402
|
-
return cmd.requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectRepeatable, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectRepeatable, []);
|
|
27478
|
+
return cmd.requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectRepeatable, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--serial", "Dispatch the parallel batch one target at a time (pre-2026-10-03 behaviour)").option("--jobs <n>", "Concurrent-start bound for the parallel batch (default: every target at once)").option("--serial-target <plugin>", "Target kept out of the parallel batch and run alone afterwards, e.g. a headed browser channel or a shared checkout (repeatable)", collectRepeatable, []).option("--target-timeout <ms>", "Per-target wall-clock budget; an overrun is recorded failed and siblings continue").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectRepeatable, []);
|
|
27403
27479
|
}
|
|
27404
27480
|
function registerExecutorCommand(program2) {
|
|
27405
27481
|
const executor = program2.command("executor").description("Run knowledge-kit plugin steps (invoke, fan-in, fan-out)");
|
package/package.json
CHANGED
|
@@ -22035,13 +22035,17 @@ ${metaLine ? `${metaLine}
|
|
|
22035
22035
|
|
|
22036
22036
|
` : ""}${stripScaffold(doc2.body ?? "").trim()}`;
|
|
22037
22037
|
}
|
|
22038
|
+
function episodeTitle(date6) {
|
|
22039
|
+
return `AI \u4E71\u7096 \xB7 ${date6}`;
|
|
22040
|
+
}
|
|
22038
22041
|
function renderDailyArticle(docs, date6) {
|
|
22039
22042
|
const items = docs.map(renderDoc).join(`
|
|
22040
22043
|
|
|
22041
22044
|
`);
|
|
22042
|
-
|
|
22045
|
+
const intro = docs.length > 0 ? `\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u5171 ${docs.length} \u6761\u5185\u5BB9\uFF0C\u5747\u9644\u53EF\u70B9\u51FB\u6765\u6E90\u94FE\u63A5\u3002` : "\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002";
|
|
22046
|
+
return `# ${episodeTitle(date6)}
|
|
22043
22047
|
|
|
22044
|
-
|
|
22048
|
+
${intro}
|
|
22045
22049
|
|
|
22046
22050
|
${items}
|
|
22047
22051
|
`;
|
|
@@ -22054,10 +22058,42 @@ function tagsFor(docs) {
|
|
|
22054
22058
|
});
|
|
22055
22059
|
return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
|
|
22056
22060
|
}
|
|
22061
|
+
var LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
22062
|
+
var NOTE_EXCLUDED_RE = /\u672A\u5199\u5165[\uFF1A:](.*)/g;
|
|
22063
|
+
var HEX_ID_RE = /(?:[a-zA-Z0-9_-]+:)?[0-9a-f]{16}/g;
|
|
22064
|
+
function excludedIdsFromNote(body) {
|
|
22065
|
+
const ids = new Set;
|
|
22066
|
+
if (!body)
|
|
22067
|
+
return ids;
|
|
22068
|
+
for (const note of body.matchAll(NOTE_EXCLUDED_RE)) {
|
|
22069
|
+
for (const id of (note[1] ?? "").matchAll(HEX_ID_RE)) {
|
|
22070
|
+
ids.add(id[0]);
|
|
22071
|
+
const colon = id[0].indexOf(":");
|
|
22072
|
+
if (colon !== -1)
|
|
22073
|
+
ids.add(id[0].slice(colon + 1));
|
|
22074
|
+
}
|
|
22075
|
+
}
|
|
22076
|
+
return ids;
|
|
22077
|
+
}
|
|
22057
22078
|
function docsToContent(docs, date6, bodyOverride) {
|
|
22079
|
+
const title = episodeTitle(date6);
|
|
22080
|
+
let body = renderDailyArticle(docs, date6);
|
|
22081
|
+
if (bodyOverride) {
|
|
22082
|
+
const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), "");
|
|
22083
|
+
body = `# ${title}
|
|
22084
|
+
|
|
22085
|
+
${stripped}
|
|
22086
|
+
`;
|
|
22087
|
+
}
|
|
22088
|
+
body = `${body.trimEnd()}
|
|
22089
|
+
|
|
22090
|
+
---
|
|
22091
|
+
|
|
22092
|
+
*\u6587\u5B57\u7531 AI \u8F85\u52A9\u6574\u7406\uFF0C\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002*
|
|
22093
|
+
`;
|
|
22058
22094
|
return ContentSchema.parse({
|
|
22059
|
-
title
|
|
22060
|
-
body
|
|
22095
|
+
title,
|
|
22096
|
+
body,
|
|
22061
22097
|
format: "markdown",
|
|
22062
22098
|
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
22063
22099
|
metadata: { generator: "kk:daily-article", date: date6, doc_count: docs.length, tags: tagsFor(docs) }
|
|
@@ -22074,7 +22110,9 @@ async function processGeneratorIO(inputPath, outputPath) {
|
|
|
22074
22110
|
if (bodyOverride.trim() === "")
|
|
22075
22111
|
bodyOverride = undefined;
|
|
22076
22112
|
}
|
|
22077
|
-
const
|
|
22113
|
+
const excluded = excludedIdsFromNote(bodyOverride);
|
|
22114
|
+
const kept = excluded.size === 0 ? docs : docs.filter((d) => !excluded.has(d.id) && !(d.id.includes(":") && excluded.has(d.id.slice(d.id.indexOf(":") + 1))));
|
|
22115
|
+
const content = docsToContent(kept, date6, bodyOverride);
|
|
22078
22116
|
const fs = createNodeFileSystem();
|
|
22079
22117
|
const outDir = dirname2(outputPath);
|
|
22080
22118
|
if (outDir && outDir !== ".")
|
|
@@ -22110,5 +22148,7 @@ export {
|
|
|
22110
22148
|
renderDailyArticle,
|
|
22111
22149
|
processGeneratorIO,
|
|
22112
22150
|
main2 as main,
|
|
22151
|
+
excludedIdsFromNote,
|
|
22152
|
+
episodeTitle,
|
|
22113
22153
|
docsToContent
|
|
22114
22154
|
};
|
|
@@ -102,10 +102,25 @@ function renderDoc(doc: Doc, index: number): string {
|
|
|
102
102
|
return `## ${index + 1}. ${heading}\n\n${metaLine ? `${metaLine}\n\n` : ''}${stripScaffold(doc.body ?? '').trim()}`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Episode title for the 「AI 乱炖」 column: fixed `AI 乱炖 · YYYY-MM-DD` (2026-10-02 operator
|
|
107
|
+
* simplification, superseding the 0189 R1/R4 `AI 乱炖|<lead item> · date` pattern). The date
|
|
108
|
+
* alone identifies the episode; the day's stories live in the show notes, cards and post body.
|
|
109
|
+
* Pure function so the agent-written and template article paths, ID3, RSS and show notes all
|
|
110
|
+
* derive one identical title.
|
|
111
|
+
*/
|
|
112
|
+
export function episodeTitle(date: string): string {
|
|
113
|
+
return `AI 乱炖 · ${date}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
/** Render the article markdown for a merged Doc[] (pure — exported for tests). */
|
|
106
117
|
export function renderDailyArticle(docs: Doc[], date: string): string {
|
|
107
118
|
const items = docs.map(renderDoc).join('\n\n');
|
|
108
|
-
|
|
119
|
+
const intro =
|
|
120
|
+
docs.length > 0
|
|
121
|
+
? `「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期共 ${docs.length} 条内容,均附可点击来源链接。`
|
|
122
|
+
: '「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期没有足够扎实的可聊内容。';
|
|
123
|
+
return `# ${episodeTitle(date)}\n\n${intro}\n\n${items}\n`;
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
/** Single-source daily tag: stamped on every Content this generator emits (task 0119). */
|
|
@@ -120,11 +135,43 @@ function tagsFor(docs: Doc[]): string[] {
|
|
|
120
135
|
return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
/** A leading H1 line — agent bodies used to carry their own title; the canonical title is computed (task 0189 R4). */
|
|
139
|
+
const LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
140
|
+
|
|
141
|
+
/** ids the article-write note marks as dropped on the topic boundary, e.g. 「…未写入:<id>(原因)、<id>(原因)」. */
|
|
142
|
+
const NOTE_EXCLUDED_RE = /未写入[::](.*)/g;
|
|
143
|
+
const HEX_ID_RE = /(?:[a-zA-Z0-9_-]+:)?[0-9a-f]{16}/g;
|
|
144
|
+
|
|
145
|
+
/** Parse the boundary-note ids out of an article body so references/metadata never cite excluded docs (judge finding 2026-10-02). */
|
|
146
|
+
export function excludedIdsFromNote(body: string | undefined): Set<string> {
|
|
147
|
+
const ids = new Set<string>();
|
|
148
|
+
if (!body) return ids;
|
|
149
|
+
for (const note of body.matchAll(NOTE_EXCLUDED_RE)) {
|
|
150
|
+
for (const id of (note[1] ?? '').matchAll(HEX_ID_RE)) {
|
|
151
|
+
ids.add(id[0]);
|
|
152
|
+
const colon = id[0].indexOf(':');
|
|
153
|
+
if (colon !== -1) ids.add(id[0].slice(colon + 1));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return ids;
|
|
157
|
+
}
|
|
158
|
+
|
|
123
159
|
/** Build the article Content from a merged Doc[]; `bodyOverride` replaces the mechanical render (agent-written prose), still scaffold-stripped as a safety net. */
|
|
124
160
|
export function docsToContent(docs: Doc[], date: string, bodyOverride?: string): Content {
|
|
161
|
+
const title = episodeTitle(date);
|
|
162
|
+
let body = renderDailyArticle(docs, date);
|
|
163
|
+
if (bodyOverride) {
|
|
164
|
+
// The canonical episode title is computed from the lead item; a leading H1 the agent may
|
|
165
|
+
// still have written is replaced (not duplicated) so body and metadata agree on every path.
|
|
166
|
+
const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), '');
|
|
167
|
+
body = `# ${title}\n\n${stripped}\n`;
|
|
168
|
+
}
|
|
169
|
+
// Task 0189 R1: the AI-production note rides on every article path (mechanical and agent
|
|
170
|
+
// prose). Podcast show notes are rendered separately by podcast-pub and do not inherit it.
|
|
171
|
+
body = `${body.trimEnd()}\n\n---\n\n*文字由 AI 辅助整理,音频为 AI 合成配音。*\n`;
|
|
125
172
|
return ContentSchema.parse({
|
|
126
|
-
title
|
|
127
|
-
body
|
|
173
|
+
title,
|
|
174
|
+
body,
|
|
128
175
|
format: 'markdown',
|
|
129
176
|
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
130
177
|
metadata: { generator: 'kk:daily-article', date, doc_count: docs.length, tags: tagsFor(docs) },
|
|
@@ -143,7 +190,15 @@ export async function processGeneratorIO(inputPath: string, outputPath: string):
|
|
|
143
190
|
bodyOverride = await Bun.file(bodyFile).text();
|
|
144
191
|
if (bodyOverride.trim() === '') bodyOverride = undefined;
|
|
145
192
|
}
|
|
146
|
-
const
|
|
193
|
+
const excluded = excludedIdsFromNote(bodyOverride);
|
|
194
|
+
const kept =
|
|
195
|
+
excluded.size === 0
|
|
196
|
+
? docs
|
|
197
|
+
: docs.filter(
|
|
198
|
+
(d) =>
|
|
199
|
+
!excluded.has(d.id) && !(d.id.includes(':') && excluded.has(d.id.slice(d.id.indexOf(':') + 1))),
|
|
200
|
+
);
|
|
201
|
+
const content = docsToContent(kept, date, bodyOverride);
|
|
147
202
|
const fs = createNodeFileSystem();
|
|
148
203
|
const outDir = dirname(outputPath);
|
|
149
204
|
if (outDir && outDir !== '.') await fs.ensureDir(outDir);
|
|
@@ -22116,10 +22116,10 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22116
22116
|
const broadcastDate = options?.date ?? new Date;
|
|
22117
22117
|
const dateStr = formatBroadcastDate(broadcastDate, language);
|
|
22118
22118
|
if (docs.length === 0) {
|
|
22119
|
-
const title2 = options?.title || (isZh ? "\
|
|
22119
|
+
const title2 = options?.title || (isZh ? "AI \u4E71\u7096\uFF08\u6682\u65E0\u66F4\u65B0\uFF09" : "AI \u4E71\u7096 (No Updates)");
|
|
22120
22120
|
const segments2 = isZh ? [
|
|
22121
22121
|
{
|
|
22122
|
-
text: `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\u3002\u6B22\u8FCE\u6536\u542C\
|
|
22122
|
+
text: `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\u3002\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${voiceProfile}\u3002\u4ECA\u5929\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002`,
|
|
22123
22123
|
gap_ms: 400,
|
|
22124
22124
|
emotion: "calm"
|
|
22125
22125
|
},
|
|
@@ -22130,7 +22130,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22130
22130
|
}
|
|
22131
22131
|
] : [
|
|
22132
22132
|
{
|
|
22133
|
-
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with
|
|
22133
|
+
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with AI \u4E71\u7096, our chat about AI tools, papers, and hands-on practice. There is nothing solid enough to share today.`,
|
|
22134
22134
|
gap_ms: 400,
|
|
22135
22135
|
emotion: "calm"
|
|
22136
22136
|
},
|
|
@@ -22151,7 +22151,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22151
22151
|
segments: segments2
|
|
22152
22152
|
});
|
|
22153
22153
|
}
|
|
22154
|
-
const title = options?.title || (isZh ?
|
|
22154
|
+
const title = options?.title || (isZh ? `AI \u4E71\u7096\uFF08\u5171${docs.length}\u6761\uFF09` : `AI \u4E71\u7096 (${docs.length} stories)`);
|
|
22155
22155
|
const segments = [];
|
|
22156
22156
|
let hostName = options?.speakers?.host?.name || voiceProfile.replace(/[-_]?(news|voice|bot|profile)$/i, "") || voiceProfile;
|
|
22157
22157
|
if (hostName.length > 0) {
|
|
@@ -22161,7 +22161,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22161
22161
|
const planItems = new Map((scriptPlan?.items ?? []).map((item) => [item.id, item]));
|
|
22162
22162
|
const countText = isZh ? `${formatChineseCount(docs.length)}\u6761` : `${docs.length}`;
|
|
22163
22163
|
const titlesSummary = docs.slice(0, 3).map((d) => (d.title ?? "").trim()).filter(Boolean).join("\u3001");
|
|
22164
|
-
const introText = isZh ? scriptPlan ? `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\
|
|
22164
|
+
const introText = isZh ? scriptPlan ? `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${hostName}\u3002${scriptPlan.intro}` : `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${hostName}\u3002\u4ECA\u5929\u6211\u4ECE AI \u5DE5\u5177\u3001\u8BBA\u6587\u548C\u5B9E\u8DF5\u4E2D\u6574\u7406\u4E86${countText}\u503C\u5F97\u804A\u7684\u5185\u5BB9${titlesSummary ? `\uFF0C\u5305\u62EC${titlesSummary}` : ""}\uFF0C\u54B1\u4EEC\u4E00\u6761\u6761\u6765\u804A\u3002` : `Hello and welcome. Today is ${dateStr}. This is ${hostName} with AI \u4E71\u7096, where we talk about AI tools, papers, and hands-on practice. In today's episode, we have ${countText} stories. Let's dive straight in.`;
|
|
22165
22165
|
segments.push({
|
|
22166
22166
|
text: normalizeBroadcastText(introText, isZh),
|
|
22167
22167
|
gap_ms: 700,
|
|
@@ -22196,7 +22196,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22196
22196
|
});
|
|
22197
22197
|
}
|
|
22198
22198
|
});
|
|
22199
|
-
const outroText = isZh ? scriptPlan ? `${scriptPlan.outro}\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `\u597D\u4E86\uFF0C\
|
|
22199
|
+
const outroText = isZh ? scriptPlan ? `${scriptPlan.outro}\u672C\u671F\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `\u597D\u4E86\uFF0C\u672C\u671FAI\u4E71\u7096\u5C31\u804A\u5230\u8FD9\u91CC\u3002\u5E0C\u671B\u4ECA\u5929\u8FD9\u4E9B\u5DE5\u5177\u3001\u8BBA\u6587\u548C\u5B9E\u8DF5\u7684\u7ECF\u9A8C\u5BF9\u60A8\u6709\u542F\u53D1\u3002\u672C\u671F\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `That wraps up this episode of AI \u4E71\u7096. I hope today's takeaways were useful. The audio in this episode is AI-synthesized. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
22200
22200
|
segments.push({
|
|
22201
22201
|
text: normalizeBroadcastText(outroText, isZh),
|
|
22202
22202
|
gap_ms: 300,
|
|
@@ -22344,11 +22344,16 @@ var ScriptPlanItemSchema = exports_external.object({
|
|
|
22344
22344
|
lead: exports_external.string().min(1),
|
|
22345
22345
|
insight: exports_external.string().min(1)
|
|
22346
22346
|
});
|
|
22347
|
+
var ScriptPlanExclusionSchema = exports_external.object({
|
|
22348
|
+
id: exports_external.string().min(1),
|
|
22349
|
+
reason: exports_external.string().min(1)
|
|
22350
|
+
});
|
|
22347
22351
|
var ScriptPlanSchema = exports_external.object({
|
|
22348
22352
|
version: exports_external.literal(1),
|
|
22349
22353
|
intro: exports_external.string().min(1),
|
|
22350
22354
|
outro: exports_external.string().min(1),
|
|
22351
|
-
items: exports_external.array(ScriptPlanItemSchema)
|
|
22355
|
+
items: exports_external.array(ScriptPlanItemSchema),
|
|
22356
|
+
excluded: exports_external.array(ScriptPlanExclusionSchema).optional()
|
|
22352
22357
|
});
|
|
22353
22358
|
var StyleContractSchema = exports_external.object({
|
|
22354
22359
|
caps: exports_external.object({
|
|
@@ -22386,8 +22391,15 @@ function validateScriptPlan(plan, docs, style) {
|
|
|
22386
22391
|
errors5.push({ itemId: item.id, rule: "unknown-id" });
|
|
22387
22392
|
}
|
|
22388
22393
|
const plannedIds = new Set(valid.items.map((item) => item.id));
|
|
22394
|
+
const excludedIds = new Set((valid.excluded ?? []).map((ex) => ex.id));
|
|
22395
|
+
for (const ex of valid.excluded ?? []) {
|
|
22396
|
+
if (!docIds.has(ex.id))
|
|
22397
|
+
errors5.push({ itemId: ex.id, rule: "unknown-id" });
|
|
22398
|
+
if (plannedIds.has(ex.id))
|
|
22399
|
+
errors5.push({ itemId: ex.id, rule: "conflict-id" });
|
|
22400
|
+
}
|
|
22389
22401
|
for (const doc2 of docs) {
|
|
22390
|
-
if (!plannedIds.has(doc2.id))
|
|
22402
|
+
if (!plannedIds.has(doc2.id) && !excludedIds.has(doc2.id))
|
|
22391
22403
|
errors5.push({ itemId: doc2.id, rule: "missing-item" });
|
|
22392
22404
|
}
|
|
22393
22405
|
if (valid.intro.length > style.caps.intro)
|
|
@@ -22456,6 +22468,24 @@ async function resolveScriptPlan(fs, docs) {
|
|
|
22456
22468
|
}
|
|
22457
22469
|
return { scriptPlan: validated.plan, scriptSource: "plan", scriptPlanErrors: [] };
|
|
22458
22470
|
}
|
|
22471
|
+
function broadcastDateFromEnv(raw) {
|
|
22472
|
+
if (!raw)
|
|
22473
|
+
return;
|
|
22474
|
+
const match = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})$/);
|
|
22475
|
+
if (match === null) {
|
|
22476
|
+
echoError(`dailynews-gen: ignoring malformed DAILYNEWS_DATE "${raw}" (expected YYYYMMDD or YYYY-MM-DD)`);
|
|
22477
|
+
return;
|
|
22478
|
+
}
|
|
22479
|
+
const year = Number(match[1]);
|
|
22480
|
+
const month = Number(match[2]) - 1;
|
|
22481
|
+
const day = Number(match[3]);
|
|
22482
|
+
const date6 = new Date(year, month, day);
|
|
22483
|
+
if (date6.getFullYear() !== year || date6.getMonth() !== month || date6.getDate() !== day) {
|
|
22484
|
+
echoError(`dailynews-gen: ignoring out-of-range DAILYNEWS_DATE "${raw}"`);
|
|
22485
|
+
return;
|
|
22486
|
+
}
|
|
22487
|
+
return date6;
|
|
22488
|
+
}
|
|
22459
22489
|
async function processGeneratorIO(inputPath, outputPath, options) {
|
|
22460
22490
|
const fs = createNodeFileSystem();
|
|
22461
22491
|
let docs;
|
|
@@ -22475,7 +22505,13 @@ async function processGeneratorIO(inputPath, outputPath, options) {
|
|
|
22475
22505
|
}
|
|
22476
22506
|
const isZh = (options?.language || "zh").toLowerCase() === "zh";
|
|
22477
22507
|
const planOutcome = isZh ? await resolveScriptPlan(fs, docs) : { scriptSource: "template", scriptPlanErrors: [] };
|
|
22478
|
-
const
|
|
22508
|
+
const excludedIds = new Set(planOutcome.scriptPlan?.excluded?.map((ex) => ex.id) ?? []);
|
|
22509
|
+
if (excludedIds.size > 0) {
|
|
22510
|
+
echoError(`dailynews-gen: topic boundary excludes ${excludedIds.size} doc(s): ${[...excludedIds].join(", ")}`);
|
|
22511
|
+
docs = docs.filter((doc2) => !excludedIds.has(doc2.id));
|
|
22512
|
+
}
|
|
22513
|
+
const date6 = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
|
|
22514
|
+
const script = buildNewsVoiceScript(docs, { ...options, date: date6, scriptPlan: planOutcome.scriptPlan });
|
|
22479
22515
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
22480
22516
|
const content = {
|
|
22481
22517
|
title: script.title,
|
|
@@ -22556,6 +22592,7 @@ export {
|
|
|
22556
22592
|
formatChineseCount,
|
|
22557
22593
|
formatBroadcastDate,
|
|
22558
22594
|
buildNewsVoiceScript,
|
|
22595
|
+
broadcastDateFromEnv,
|
|
22559
22596
|
VoiceSpeakerSchema,
|
|
22560
22597
|
VoiceScriptSegmentSchema,
|
|
22561
22598
|
VoiceScriptSchema,
|
|
@@ -66,6 +66,32 @@ async function resolveScriptPlan(fs: FileSystem, docs: Doc[]): Promise<ScriptPla
|
|
|
66
66
|
return { scriptPlan: validated.plan, scriptSource: 'plan', scriptPlanErrors: [] };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* `DAILYNEWS_DATE` (YYYYMMDD or `YYYY-MM-DD`) → a local-midnight Date. Task 0189: the broadcast
|
|
71
|
+
* greeting must carry the episode's run date, not the render date — a 9/29 episode rebuilt on 9/30
|
|
72
|
+
* must not greet "今天是9月30日". Parsed as local calendar parts, never UTC (`new Date('2026-09-29')`
|
|
73
|
+
* is UTC midnight and renders the previous day west of UTC). Absent → undefined; garbled or
|
|
74
|
+
* out-of-range (the Date constructor rolls over, e.g. month 13) → undefined with a warn so a
|
|
75
|
+
* caller bug surfaces instead of silently broadcasting the wrong day.
|
|
76
|
+
*/
|
|
77
|
+
export function broadcastDateFromEnv(raw: string | undefined): Date | undefined {
|
|
78
|
+
if (!raw) return undefined;
|
|
79
|
+
const match = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})$/);
|
|
80
|
+
if (match === null) {
|
|
81
|
+
echoError(`dailynews-gen: ignoring malformed DAILYNEWS_DATE "${raw}" (expected YYYYMMDD or YYYY-MM-DD)`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const year = Number(match[1]);
|
|
85
|
+
const month = Number(match[2]) - 1;
|
|
86
|
+
const day = Number(match[3]);
|
|
87
|
+
const date = new Date(year, month, day);
|
|
88
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
89
|
+
echoError(`dailynews-gen: ignoring out-of-range DAILYNEWS_DATE "${raw}"`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return date;
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
export async function processGeneratorIO(
|
|
70
96
|
inputPath: string,
|
|
71
97
|
outputPath: string,
|
|
@@ -101,7 +127,16 @@ export async function processGeneratorIO(
|
|
|
101
127
|
const planOutcome = isZh
|
|
102
128
|
? await resolveScriptPlan(fs, docs)
|
|
103
129
|
: { scriptSource: 'template' as const, scriptPlanErrors: [] as ScriptPlanError[] };
|
|
104
|
-
|
|
130
|
+
// Topic boundary (task 0189): docs the plan declares excluded are not spoken, not counted,
|
|
131
|
+
// and not referenced — title, storyCount, and references must match what is actually said.
|
|
132
|
+
const excludedIds = new Set(planOutcome.scriptPlan?.excluded?.map((ex) => ex.id) ?? []);
|
|
133
|
+
if (excludedIds.size > 0) {
|
|
134
|
+
echoError(`dailynews-gen: topic boundary excludes ${excludedIds.size} doc(s): ${[...excludedIds].join(', ')}`);
|
|
135
|
+
docs = docs.filter((doc) => !excludedIds.has(doc.id));
|
|
136
|
+
}
|
|
137
|
+
// Explicit option wins; else DAILYNEWS_DATE (workflow backfill/retry); else the builder's now.
|
|
138
|
+
const date = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
|
|
139
|
+
const script = buildNewsVoiceScript(docs, { ...options, date, scriptPlan: planOutcome.scriptPlan });
|
|
105
140
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
106
141
|
|
|
107
142
|
const content: Content = {
|