@gobing-ai/knowledge-kit 0.1.13 → 0.1.15
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 +796 -176
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +2 -2
- package/plugins/generations/core-facts-gen/dist/index.js +2 -2
- package/plugins/generations/daily-article-gen/dist/index.js +2 -2
- package/plugins/generations/dailynews-gen/dist/index.js +72 -9
- package/plugins/generations/dailynews-gen/src/index.ts +23 -1
- package/plugins/generations/dailynews-gen/src/script-plan.ts +103 -5
- package/plugins/generations/episode-plan-gen/dist/index.js +2 -2
- package/plugins/generations/gemini-tts/dist/index.js +2 -2
- package/plugins/generations/image-gen/dist/index.js +2 -2
- package/plugins/generations/news-report-gen/dist/index.js +2 -2
- package/plugins/generations/voice-gen/dist/index.js +2 -2
- package/plugins/generations/voxcpm-tts/dist/index.js +2 -2
- package/plugins/ingestions/aihot-ingest/dist/index.js +2 -2
- package/plugins/ingestions/horizon-ingest/dist/index.js +2 -2
- package/plugins/ingestions/last30days-ingest/dist/index.js +2 -2
- package/plugins/ingestions/web-search/dist/index.js +2 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-scheduled-run.ts +53 -5
- package/plugins/kk/scripts/daily-stages.ts +476 -12
- package/plugins/kk/scripts/kk-workflow-stages.ts +20 -4
- package/plugins/kk/skills/content-judge/SKILL.md +5 -4
- package/plugins/kk/skills/content-judge/references/rubrics.md +14 -0
- package/plugins/kk/skills/news-editorial/SKILL.md +122 -0
- package/plugins/kk/skills/news-editorial/references/selection.md +47 -0
- package/plugins/kk/skills/news-editorial/references/style-contract.zh.md +99 -0
- package/plugins/kk/skills/publish/SKILL.md +3 -3
- package/plugins/kk/skills/web-automation/SKILL.md +229 -77
- package/plugins/kk/skills/web-automation/references/browser-launch.md +127 -36
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +4 -6
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +183 -222
- package/plugins/kk/workflows/kk-daily-publish3.yaml +16 -8
- package/plugins/publishings/bilibili-pub/dist/index.js +5 -3
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +5 -3
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/csdn-pub/dist/index.js +5 -3
- package/plugins/publishings/csdn-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/emdash-pub/dist/index.js +2 -2
- package/plugins/publishings/infoq-pub/dist/index.js +5 -3
- package/plugins/publishings/infoq-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/jike-pub/dist/index.js +5 -3
- package/plugins/publishings/jike-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/juejin-pub/dist/index.js +5 -3
- package/plugins/publishings/juejin-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/linkedin-pub/dist/index.js +5 -3
- package/plugins/publishings/linkedin-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/medium-pub/dist/index.js +2 -2
- package/plugins/publishings/podcast-pub/dist/index.js +147 -4
- package/plugins/publishings/podcast-pub/src/index.ts +71 -2
- package/plugins/publishings/podcast-pub/src/map.ts +3 -0
- package/plugins/publishings/podcast-pub/src/transcript.ts +195 -0
- package/plugins/publishings/qiita-pub/dist/index.js +2 -2
- package/plugins/publishings/shipinhao-pub/dist/index.js +5 -3
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/sspai-pub/dist/index.js +5 -3
- package/plugins/publishings/sspai-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/substack-pub/dist/index.js +5 -3
- package/plugins/publishings/substack-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/surfdash-pub/dist/index.js +2 -2
- package/plugins/publishings/threads-pub/dist/index.js +2 -2
- package/plugins/publishings/wechatmp-pub/dist/index.js +52 -33
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +70 -33
- package/plugins/publishings/weibo-pub/dist/index.js +21 -6
- package/plugins/publishings/weibo-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/weibo-pub/src/index.ts +31 -7
- package/plugins/publishings/x-pub/dist/index.js +5 -3
- package/plugins/publishings/x-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/xhs-pub/dist/index.js +5 -3
- package/plugins/publishings/xhs-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +5 -3
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/zenn-pub/dist/index.js +2 -2
- package/plugins/kk/scripts/daily-voice-style.zh.md +0 -70
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/** Supervise one scheduled daily run and persist its terminal outcome even after a deadline. */
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
|
|
@@ -17,8 +17,10 @@ const { values } = parseArgs({
|
|
|
17
17
|
'required-channels': { type: 'string', default: 'wechat,locales,cms-resync' },
|
|
18
18
|
'wechat-live': { type: 'boolean', default: false },
|
|
19
19
|
workflow: { type: 'string', default: 'kk-daily-ai-voice2' },
|
|
20
|
+
'generate-only': { type: 'boolean', default: false },
|
|
20
21
|
window: { type: 'string' },
|
|
21
22
|
'publish-preset': { type: 'string' },
|
|
23
|
+
'publish-live-channels': { type: 'string' },
|
|
22
24
|
},
|
|
23
25
|
});
|
|
24
26
|
const timeoutMs = Number(values['timeout-ms']);
|
|
@@ -33,13 +35,22 @@ const runId = values['run-id'] ?? `daily-ai-voice-${stamp}-${now.getTime()}`;
|
|
|
33
35
|
const workDir = join(resolve(values['works-dir'] ?? join(repo, '.works')), 'kk-daily-ai-voice2', stamp);
|
|
34
36
|
const receiptPath = resolve(values.receipt ?? join(workDir, 'schedule-receipt.json'));
|
|
35
37
|
const startedAt = now.toISOString();
|
|
38
|
+
mkdirSync(workDir, { recursive: true });
|
|
36
39
|
mkdirSync(dirname(receiptPath), { recursive: true });
|
|
37
40
|
// voice3 shares the voice2 workspace (F08); only the vars and the publish seam differ.
|
|
38
41
|
const isVoice3 = values.workflow === 'kk-daily-ai-voice3';
|
|
42
|
+
const generateOnly = values['generate-only'] === true;
|
|
43
|
+
if (generateOnly && !isVoice3) {
|
|
44
|
+
console.error('daily-scheduled-run: --generate-only requires --workflow kk-daily-ai-voice3');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
39
47
|
const vars = isVoice3
|
|
40
48
|
? {
|
|
41
|
-
auto_publish: 'true',
|
|
49
|
+
auto_publish: generateOnly ? 'false' : 'true',
|
|
42
50
|
...(values['publish-preset'] !== undefined ? { publish_preset: values['publish-preset'] } : {}),
|
|
51
|
+
...(values['publish-live-channels'] !== undefined
|
|
52
|
+
? { publish_live_channels: values['publish-live-channels'] }
|
|
53
|
+
: {}),
|
|
43
54
|
...(values.window ? { window: values.window } : {}),
|
|
44
55
|
skip_review: 'true',
|
|
45
56
|
agent: values.agent,
|
|
@@ -312,6 +323,37 @@ const missingRequiredChannels = isVoice3
|
|
|
312
323
|
const rows = channelCompleteness.filter((row) => row.channel === channel);
|
|
313
324
|
return rows.length !== 1 || rows[0]?.state !== 'done';
|
|
314
325
|
});
|
|
326
|
+
/** Generate-only receipts use the same fail-closed bundle admission as posting and validation. */
|
|
327
|
+
function inspectGenerationOnly(): { status: string; reason: string } {
|
|
328
|
+
const inspector = join(repo, 'scripts/daily-ai-news.ts');
|
|
329
|
+
if (!existsSync(inspector)) return { status: 'incomplete', reason: `bundle inspector not found: ${inspector}` };
|
|
330
|
+
// The child is terminal, but our initial receipt still says running. Mark the inspection phase so
|
|
331
|
+
// the shared admission check does not mistake this supervisor for a concurrent active run.
|
|
332
|
+
writeFileSync(receiptPath, JSON.stringify({ runId, startedAt, status: 'inspecting', workDir }, null, 2));
|
|
333
|
+
const probe = spawnSync(process.execPath, [inspector, 'inspect', '--work-dir', workDir, '--run-date', stamp], {
|
|
334
|
+
cwd: repo,
|
|
335
|
+
encoding: 'utf-8',
|
|
336
|
+
timeout: 30000,
|
|
337
|
+
maxBuffer: 1024 * 1024,
|
|
338
|
+
});
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(probe.stdout) as { status?: unknown; reason?: unknown };
|
|
341
|
+
if (typeof parsed.status === 'string') {
|
|
342
|
+
return {
|
|
343
|
+
status: probe.status === 0 && parsed.status === 'complete' ? 'complete' : 'incomplete',
|
|
344
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : `inspector exited ${probe.status ?? 1}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
} catch {}
|
|
348
|
+
return {
|
|
349
|
+
status: 'incomplete',
|
|
350
|
+
reason: probe.stderr.trim() || probe.error?.message || `bundle inspector exited ${probe.status ?? 1}`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const generationInspection =
|
|
354
|
+
generateOnly && workflowStatus === 'done' && !runSummary?.startsWith('no-content:')
|
|
355
|
+
? inspectGenerationOnly()
|
|
356
|
+
: null;
|
|
315
357
|
// A terminal green workflow can mean "no content" or failed delivery. Make that observable to
|
|
316
358
|
// the scheduler instead of equating the workflow's process exit with a published edition.
|
|
317
359
|
// A voice3 outcome classified 'full' already proves every non-dummy row succeeded (0326 R2).
|
|
@@ -320,14 +362,20 @@ const status =
|
|
|
320
362
|
? workflowStatus
|
|
321
363
|
: runSummary?.startsWith('no-content:')
|
|
322
364
|
? 'no-content'
|
|
323
|
-
:
|
|
324
|
-
? '
|
|
325
|
-
|
|
365
|
+
: generateOnly
|
|
366
|
+
? generationInspection?.status === 'complete'
|
|
367
|
+
? 'done'
|
|
368
|
+
: 'incomplete'
|
|
369
|
+
: primaryPublishStatus === 'full' && !(!isVoice3 && missingRequiredChannels.length > 0)
|
|
370
|
+
? 'done'
|
|
371
|
+
: 'incomplete';
|
|
326
372
|
const receipt = {
|
|
327
373
|
runId,
|
|
328
374
|
startedAt,
|
|
329
375
|
completedAt: new Date().toISOString(),
|
|
330
376
|
status,
|
|
377
|
+
generateOnly,
|
|
378
|
+
generationInspection,
|
|
331
379
|
exitCode: timedOut ? 124 : interrupted ? 143 : exitCode !== 0 ? exitCode : status === 'done' ? 0 : 4,
|
|
332
380
|
workDir,
|
|
333
381
|
timeoutMs,
|
|
@@ -55,6 +55,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
55
55
|
import { createHash } from 'node:crypto';
|
|
56
56
|
import {
|
|
57
57
|
copyFileSync,
|
|
58
|
+
cpSync,
|
|
58
59
|
existsSync,
|
|
59
60
|
mkdirSync,
|
|
60
61
|
readdirSync,
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
75
76
|
coverBrief,
|
|
76
77
|
coverNormalizeBody,
|
|
77
78
|
dedupPlanByTitle,
|
|
79
|
+
EDITORIAL_SKILL_DIR,
|
|
78
80
|
expandHome,
|
|
79
81
|
fail,
|
|
80
82
|
failWith,
|
|
@@ -91,12 +93,10 @@ import {
|
|
|
91
93
|
readJson,
|
|
92
94
|
readTopicBoundary,
|
|
93
95
|
requireJson,
|
|
94
|
-
requireSidecar,
|
|
95
|
-
resolveSidecar,
|
|
96
96
|
resultPublishStatus,
|
|
97
97
|
SAFETY_PLATFORM_DECISIONS,
|
|
98
98
|
SAFETY_REVISE_COUNT,
|
|
99
|
-
|
|
99
|
+
STYLE_CONTRACT,
|
|
100
100
|
StageFailure,
|
|
101
101
|
safetyDigest,
|
|
102
102
|
safetyDraftPaths,
|
|
@@ -511,14 +511,24 @@ export function dailyArticle(args: string[]): void {
|
|
|
511
511
|
// half-ran (or the json was pruned), and returning here starves every downstream stage
|
|
512
512
|
// that reads content.json — publish-prep refuses to merge a draft it cannot read. Requiring
|
|
513
513
|
// both artifacts makes the pair self-healing instead of permanently stuck.
|
|
514
|
-
|
|
514
|
+
// The prose body is the generator's INPUT: when it is newer than content.json the pair
|
|
515
|
+
// is stale (an operator or a retry edited the headings after the render), and serving it
|
|
516
|
+
// ships last round's body to surfdash. Re-derive instead of returning.
|
|
517
|
+
const proseBody = join(workDir, '22-article', `${runDate}_06_article-write_body.md`);
|
|
518
|
+
const proseStale =
|
|
519
|
+
isFile(proseBody) &&
|
|
520
|
+
isFile(articleContent) &&
|
|
521
|
+
statSync(proseBody).mtimeMs > statSync(articleContent).mtimeMs;
|
|
522
|
+
if (isFile(articleMd) && isFile(articleContent) && !proseStale) {
|
|
515
523
|
console.log(`article cached: ${articleMd}`);
|
|
516
524
|
return;
|
|
517
525
|
}
|
|
526
|
+
if (proseStale) {
|
|
527
|
+
console.error(`article: stale pair — ${proseBody} is newer than ${articleContent}, re-deriving`);
|
|
528
|
+
}
|
|
518
529
|
// Agent-written prose body (workflow `article-write` state) replaces the mechanical
|
|
519
530
|
// per-item render when present — the 3-format split: surfdash gets an article,
|
|
520
531
|
// not a news list (2026-09-16 feedback).
|
|
521
|
-
const proseBody = join(workDir, '22-article', `${runDate}_06_article-write_body.md`);
|
|
522
532
|
kk(
|
|
523
533
|
[
|
|
524
534
|
'executor',
|
|
@@ -3999,7 +4009,8 @@ export function dailyScript(args: string[]): void {
|
|
|
3999
4009
|
// cwd = the plugin package dir, so a relative workDir would silently miss the
|
|
4000
4010
|
// plan; a plan file absent at build time reports missing-plan (template fallback).
|
|
4001
4011
|
DAILYNEWS_SCRIPT_PLAN: resolve(join(workDir, '24-script', `${runDate}_08_script-write_plan.json`)),
|
|
4002
|
-
DAILYNEWS_STYLE_FILE: resolve(
|
|
4012
|
+
DAILYNEWS_STYLE_FILE: resolve(STYLE_CONTRACT),
|
|
4013
|
+
DAILYNEWS_CURATE_EXCLUDED: resolve(join(workDir, '21-plan', `${runDate}_03_curate_excluded.json`)),
|
|
4003
4014
|
},
|
|
4004
4015
|
});
|
|
4005
4016
|
const body = readJson(contentOut)?.body;
|
|
@@ -4021,25 +4032,478 @@ export function dailyScript(args: string[]): void {
|
|
|
4021
4032
|
|
|
4022
4033
|
/** Task 0189 R10 artifacts of the content-safety verdict gate (24-script, step 09). */
|
|
4023
4034
|
/**
|
|
4024
|
-
* F613 R1: snapshot the shipped
|
|
4025
|
-
* workflow-dir var
|
|
4026
|
-
*
|
|
4027
|
-
*
|
|
4035
|
+
* F613 R1 / ADR-045: snapshot the shipped kk:news-editorial skill into the run's work dir. Spur
|
|
4036
|
+
* workflows have no workflow-dir var and kk skills are not installed for every executor, so the
|
|
4037
|
+
* writer prompts read this copy by path — and the snapshot freezes the exact policy the run's plan
|
|
4038
|
+
* was written against. `20-editorial/news-editorial/` holds SKILL.md + references; the
|
|
4039
|
+
* `_08_script-write_style.md` copy of the style contract stays the run's audit record.
|
|
4040
|
+
* Idempotent per artifact (an existing snapshot is kept); a missing skill is a broken install.
|
|
4028
4041
|
*/
|
|
4029
4042
|
export function dailyScriptStyle(args: string[]): void {
|
|
4030
4043
|
const [workDir = '', runDate = ''] = args;
|
|
4044
|
+
if (!isFile(STYLE_CONTRACT)) {
|
|
4045
|
+
failWith(
|
|
4046
|
+
1,
|
|
4047
|
+
`news-editorial skill not found — no ${STYLE_CONTRACT}; install or refresh the kk package (bun link in a checkout)`,
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
const skillSnapshot = join(workDir, '20-editorial', 'news-editorial');
|
|
4051
|
+
if (isFile(join(skillSnapshot, 'SKILL.md'))) {
|
|
4052
|
+
console.log(`editorial skill cached: ${skillSnapshot}`);
|
|
4053
|
+
} else {
|
|
4054
|
+
sted('cp', skillSnapshot, () => cpSync(EDITORIAL_SKILL_DIR, skillSnapshot, { recursive: true }));
|
|
4055
|
+
console.log(`editorial skill snapshot: ${skillSnapshot}`);
|
|
4056
|
+
}
|
|
4031
4057
|
const snapshot = join(workDir, '24-script', `${runDate}_08_script-write_style.md`);
|
|
4032
4058
|
if (isFile(snapshot)) {
|
|
4033
4059
|
console.log(`script style cached: ${snapshot}`);
|
|
4034
4060
|
return;
|
|
4035
4061
|
}
|
|
4036
|
-
const sidecar = requireSidecar('', STYLE_SIDECAR, 'daily-voice-style');
|
|
4037
4062
|
const scriptDir = join(workDir, '24-script');
|
|
4038
4063
|
sted('mkdir', scriptDir, () => mkdirSync(scriptDir, { recursive: true }));
|
|
4039
|
-
sted('cp', snapshot, () => copyFileSync(
|
|
4064
|
+
sted('cp', snapshot, () => copyFileSync(STYLE_CONTRACT, snapshot));
|
|
4040
4065
|
console.log(`script style snapshot: ${snapshot}`);
|
|
4041
4066
|
}
|
|
4042
4067
|
|
|
4068
|
+
/**
|
|
4069
|
+
* 0345 R4: dry-run the dailynews-gen validator on the authored script plan so a rejected plan gets
|
|
4070
|
+
* one targeted `script-write-fix` pass instead of silently shipping the template fallback. Writes
|
|
4071
|
+
* `24-script/<date>_08_script-write_errors.json` ([{itemId, rule}]) when the plan is absent or
|
|
4072
|
+
* rejected, and removes it when the plan is accepted. The trial build goes to a scratch path so the
|
|
4073
|
+
* `script` stage's isFile cache is never primed by a check.
|
|
4074
|
+
*/
|
|
4075
|
+
export function dailyScriptCheck(args: string[]): void {
|
|
4076
|
+
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
4077
|
+
const planOut = join(workDir, '24-script', `${runDate}_08_script-write_plan.json`);
|
|
4078
|
+
const errorsOut = join(workDir, '24-script', `${runDate}_08_script-write_errors.json`);
|
|
4079
|
+
const writeErrors = (errors: unknown[]) => {
|
|
4080
|
+
sted('write', errorsOut, () => writeJsonText(errorsOut, errors));
|
|
4081
|
+
console.log(`script plan rejected (${errors.length} error(s)): ${errorsOut}`);
|
|
4082
|
+
};
|
|
4083
|
+
if (!isFile(planOut)) {
|
|
4084
|
+
writeErrors([{ itemId: 'episode', rule: 'missing-plan' }]);
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
const trialOut = join(workDir, '24-script', `.${runDate}_08_script-check_content.json`);
|
|
4088
|
+
const planIn = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
|
|
4089
|
+
try {
|
|
4090
|
+
kk(['executor', 'run', 'dailynews-gen', '--in', planIn, '--out', trialOut, ...pluginsArgs(pluginsPath)], {
|
|
4091
|
+
env: {
|
|
4092
|
+
DAILYNEWS_LANGUAGE: 'zh',
|
|
4093
|
+
DAILYNEWS_DATE: runDate,
|
|
4094
|
+
DAILYNEWS_SCRIPT_PLAN: resolve(planOut),
|
|
4095
|
+
DAILYNEWS_STYLE_FILE: resolve(STYLE_CONTRACT),
|
|
4096
|
+
DAILYNEWS_CURATE_EXCLUDED: resolve(join(workDir, '21-plan', `${runDate}_03_curate_excluded.json`)),
|
|
4097
|
+
},
|
|
4098
|
+
});
|
|
4099
|
+
const metadata = readJson(trialOut)?.metadata;
|
|
4100
|
+
const source = metadata?.scriptSource;
|
|
4101
|
+
if (source !== 'plan' && source !== 'template') {
|
|
4102
|
+
failWith(1, `script-check: ${trialOut} carries no scriptSource stamp`);
|
|
4103
|
+
}
|
|
4104
|
+
if (source === 'plan') {
|
|
4105
|
+
if (isFile(errorsOut)) unlinkSync(errorsOut);
|
|
4106
|
+
console.log('script plan accepted by the validator');
|
|
4107
|
+
return;
|
|
4108
|
+
}
|
|
4109
|
+
writeErrors(Array.isArray(metadata.scriptPlanErrors) ? metadata.scriptPlanErrors : []);
|
|
4110
|
+
} finally {
|
|
4111
|
+
if (isFile(trialOut)) unlinkSync(trialOut);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
/**
|
|
4116
|
+
* 0345 R8: validate the `curate` agent output (`21-plan/<date>_03_plan_plan.json` +
|
|
4117
|
+
* `21-plan/<date>_03_curate_excluded.json`) against the QC candidates
|
|
4118
|
+
* (`21-plan/<date>_04_quality-control-content_candidates.json`). Unwraps a `Content` envelope and
|
|
4119
|
+
* dedups near-duplicate titles in place before `write` reads the plan. Writes
|
|
4120
|
+
* `21-plan/<date>_03_curate_errors.json` (`[{itemId, rule}]`) on failure and removes it on success.
|
|
4121
|
+
* With `--final`, falls back to the untranslated candidates and `[]` exclusions on failure.
|
|
4122
|
+
*/
|
|
4123
|
+
export function dailyCurateCheck(args: string[]): void {
|
|
4124
|
+
const [workDir = '', runDate = '', flag = ''] = args;
|
|
4125
|
+
if (!workDir) fail('missing work_dir');
|
|
4126
|
+
if (!runDate) fail('missing run_date');
|
|
4127
|
+
const isFinal = flag === '--final';
|
|
4128
|
+
const candidatesFile = join(workDir, '21-plan', `${runDate}_04_quality-control-content_candidates.json`);
|
|
4129
|
+
const planFile = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
|
|
4130
|
+
const excludedFile = join(workDir, '21-plan', `${runDate}_03_curate_excluded.json`);
|
|
4131
|
+
const errFile = join(workDir, '21-plan', `${runDate}_03_curate_errors.json`);
|
|
4132
|
+
|
|
4133
|
+
const candRaw = readJson(candidatesFile);
|
|
4134
|
+
const candidateDocs: Record<string, unknown>[] = Array.isArray(candRaw)
|
|
4135
|
+
? (candRaw as Record<string, unknown>[])
|
|
4136
|
+
: Array.isArray(metadataOf(candRaw).docs)
|
|
4137
|
+
? (metadataOf(candRaw).docs as Record<string, unknown>[])
|
|
4138
|
+
: [];
|
|
4139
|
+
const candidateIds = candidateDocs.map((doc) => (typeof doc?.id === 'string' ? doc.id.trim() : '')).filter(Boolean);
|
|
4140
|
+
const candidateIdSet = new Set(candidateIds);
|
|
4141
|
+
|
|
4142
|
+
const handleErrors = (errors: { itemId: string; rule: string }[]): void => {
|
|
4143
|
+
if (isFinal) {
|
|
4144
|
+
const reason = errors.map((e) => `${e.itemId}:${e.rule}`).join(', ');
|
|
4145
|
+
console.error(
|
|
4146
|
+
`curate-check: final check failed (${reason}) — falling back to ${candidateDocs.length} untranslated candidate(s)`,
|
|
4147
|
+
);
|
|
4148
|
+
sted('write', planFile, () => writeJsonText(planFile, candidateDocs));
|
|
4149
|
+
sted('write', excludedFile, () => writeJsonText(excludedFile, []));
|
|
4150
|
+
if (isFile(errFile)) unlinkSync(errFile);
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
sted('write', errFile, () => writeJsonText(errFile, errors));
|
|
4154
|
+
console.log(`curate-check: rejected (${errors.length} error(s)): ${errFile}`);
|
|
4155
|
+
};
|
|
4156
|
+
|
|
4157
|
+
if (!isFile(planFile)) {
|
|
4158
|
+
handleErrors([{ itemId: 'episode', rule: 'missing-plan' }]);
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
let planParsed: unknown;
|
|
4163
|
+
try {
|
|
4164
|
+
planParsed = JSON.parse(readFileSync(planFile, 'utf-8'));
|
|
4165
|
+
} catch {
|
|
4166
|
+
handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
let planDocs: Record<string, unknown>[];
|
|
4171
|
+
if (Array.isArray(planParsed)) {
|
|
4172
|
+
planDocs = planParsed as Record<string, unknown>[];
|
|
4173
|
+
} else if (
|
|
4174
|
+
planParsed !== null &&
|
|
4175
|
+
typeof planParsed === 'object' &&
|
|
4176
|
+
Array.isArray(metadataOf(planParsed as Record<string, unknown>).docs)
|
|
4177
|
+
) {
|
|
4178
|
+
planDocs = metadataOf(planParsed as Record<string, unknown>).docs as Record<string, unknown>[];
|
|
4179
|
+
sted('write', planFile, () => writeJsonText(planFile, planDocs));
|
|
4180
|
+
console.log(`curate-check: unwrapped Content envelope (${planDocs.length} doc(s)) in ${planFile}`);
|
|
4181
|
+
} else {
|
|
4182
|
+
handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
for (const entry of planDocs) {
|
|
4187
|
+
if (entry === null || typeof entry !== 'object' || typeof entry.id !== 'string' || entry.id.trim() === '') {
|
|
4188
|
+
handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
const excludedItems: { id: string; reason: string }[] = [];
|
|
4194
|
+
if (isFile(excludedFile)) {
|
|
4195
|
+
let exParsed: unknown;
|
|
4196
|
+
try {
|
|
4197
|
+
exParsed = JSON.parse(readFileSync(excludedFile, 'utf-8'));
|
|
4198
|
+
} catch {
|
|
4199
|
+
handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
if (!Array.isArray(exParsed)) {
|
|
4203
|
+
handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
for (const entry of exParsed) {
|
|
4207
|
+
if (
|
|
4208
|
+
entry === null ||
|
|
4209
|
+
typeof entry !== 'object' ||
|
|
4210
|
+
typeof (entry as { id?: unknown }).id !== 'string' ||
|
|
4211
|
+
(entry as { id: string }).id.trim() === '' ||
|
|
4212
|
+
typeof (entry as { reason?: unknown }).reason !== 'string' ||
|
|
4213
|
+
(entry as { reason: string }).reason.trim() === ''
|
|
4214
|
+
) {
|
|
4215
|
+
handleErrors([{ itemId: 'episode', rule: 'invalid-json' }]);
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4218
|
+
excludedItems.push({
|
|
4219
|
+
id: (entry as { id: string }).id.trim(),
|
|
4220
|
+
reason: (entry as { reason: string }).reason.trim(),
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
const errors: { itemId: string; rule: string }[] = [];
|
|
4226
|
+
if (planDocs.length === 0) {
|
|
4227
|
+
errors.push({ itemId: 'episode', rule: 'empty-plan' });
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
const keptIds = new Set<string>();
|
|
4231
|
+
for (const doc of planDocs) {
|
|
4232
|
+
const id = (doc.id as string).trim();
|
|
4233
|
+
keptIds.add(id);
|
|
4234
|
+
if (candidateIdSet.size > 0 && !candidateIdSet.has(id)) {
|
|
4235
|
+
errors.push({ itemId: id, rule: 'unknown-id' });
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
const excludedIds = new Set<string>();
|
|
4240
|
+
for (const ex of excludedItems) {
|
|
4241
|
+
excludedIds.add(ex.id);
|
|
4242
|
+
if (candidateIdSet.size > 0 && !candidateIdSet.has(ex.id)) {
|
|
4243
|
+
errors.push({ itemId: ex.id, rule: 'unknown-id' });
|
|
4244
|
+
}
|
|
4245
|
+
if (keptIds.has(ex.id)) {
|
|
4246
|
+
errors.push({ itemId: ex.id, rule: 'conflict-id' });
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
for (const cid of candidateIds) {
|
|
4251
|
+
if (!keptIds.has(cid) && !excludedIds.has(cid)) {
|
|
4252
|
+
errors.push({ itemId: cid, rule: 'missing-item' });
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
if (errors.length > 0) {
|
|
4257
|
+
handleErrors(errors);
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
const { kept, dropped } = dedupPlanByTitle(planDocs);
|
|
4262
|
+
if (dropped.length > 0) {
|
|
4263
|
+
sted('write', planFile, () => writeJsonText(planFile, kept));
|
|
4264
|
+
console.error(`curate-check: dropped ${dropped.length} near-duplicate plan item(s): ${dropped.join(', ')}`);
|
|
4265
|
+
for (const droppedId of dropped) {
|
|
4266
|
+
if (!excludedIds.has(droppedId)) {
|
|
4267
|
+
excludedIds.add(droppedId);
|
|
4268
|
+
excludedItems.push({ id: droppedId, reason: '标题近似重复自动去重' });
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
sted('write', excludedFile, () => writeJsonText(excludedFile, excludedItems));
|
|
4272
|
+
} else if (!isFile(excludedFile)) {
|
|
4273
|
+
sted('write', excludedFile, () => writeJsonText(excludedFile, excludedItems));
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
if (isFile(errFile)) unlinkSync(errFile);
|
|
4277
|
+
console.log(`curate-check: accepted (${kept.length} kept, ${excludedItems.length} excluded)`);
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
function sourceLabelForDoc(doc: Record<string, unknown>, sourceUri: string): string {
|
|
4281
|
+
const meta = metadataOf(doc);
|
|
4282
|
+
if (typeof meta.source === 'string' && meta.source.trim() !== '') {
|
|
4283
|
+
return meta.source.trim();
|
|
4284
|
+
}
|
|
4285
|
+
try {
|
|
4286
|
+
return new URL(sourceUri).hostname.replace(/^www\./, '') || '原文';
|
|
4287
|
+
} catch {
|
|
4288
|
+
return '原文';
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
/** Fixed column opening line of the rendered article body: house copy, not editorial judgement (0345). */
|
|
4293
|
+
const EDITORIAL_ARTICLE_OPENING = '这里是由Robin为你带来的咸话咸说之每日AI乱炖。';
|
|
4294
|
+
|
|
4295
|
+
/**
|
|
4296
|
+
* 0345 R10: deterministically render the five sibling editorial artifacts from the master script
|
|
4297
|
+
* plan (`24-script/<date>_08_script-write_plan.json`) and the curated plan
|
|
4298
|
+
* (`21-plan/<date>_03_plan_plan.json`):
|
|
4299
|
+
* 1. `22-article/<date>_06_article-write_body.md`
|
|
4300
|
+
* 2. `23-cover/<date>_07_cover-brief.json`
|
|
4301
|
+
* 3. `25-xhs/<date>_18_xhs_plan.json` (only when `cards_enabled === 'true'`)
|
|
4302
|
+
* 4. `26-social/{x,threads}.md` (`social.short`)
|
|
4303
|
+
* 5. `26-social/{weibo,linkedin}.md` (`social.long`)
|
|
4304
|
+
* Missing master field -> skip that one artifact with a logged reason, write the others.
|
|
4305
|
+
*/
|
|
4306
|
+
export function dailyEditorialRender(args: string[]): void {
|
|
4307
|
+
const [workDir = '', runDate = '', cardsEnabled = 'false', cardsRaw = '6'] = args;
|
|
4308
|
+
if (!workDir) fail('missing work_dir');
|
|
4309
|
+
if (!runDate) fail('missing run_date');
|
|
4310
|
+
const cardsCount = Math.max(2, Number.parseInt(cardsRaw, 10) || 6);
|
|
4311
|
+
const planFile = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
|
|
4312
|
+
const masterFile = join(workDir, '24-script', `${runDate}_08_script-write_plan.json`);
|
|
4313
|
+
|
|
4314
|
+
const master = readJson(masterFile);
|
|
4315
|
+
if (master === null) {
|
|
4316
|
+
console.log(`editorial-render: no master script plan at ${masterFile} — skipping all renders`);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
const planRaw = readJson(planFile);
|
|
4321
|
+
const planDocs: Record<string, unknown>[] = Array.isArray(planRaw)
|
|
4322
|
+
? (planRaw as Record<string, unknown>[])
|
|
4323
|
+
: Array.isArray(metadataOf(planRaw).docs)
|
|
4324
|
+
? (metadataOf(planRaw).docs as Record<string, unknown>[])
|
|
4325
|
+
: [];
|
|
4326
|
+
const docById = new Map<string, Record<string, unknown>>();
|
|
4327
|
+
for (const doc of planDocs) {
|
|
4328
|
+
if (typeof doc?.id === 'string' && doc.id.trim() !== '') {
|
|
4329
|
+
docById.set(doc.id.trim(), doc);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
const rawItems = Array.isArray(master.items) ? (master.items as Record<string, unknown>[]) : [];
|
|
4334
|
+
const itemById = new Map<string, Record<string, unknown>>();
|
|
4335
|
+
for (const item of rawItems) {
|
|
4336
|
+
if (item !== null && typeof item === 'object' && typeof item.id === 'string' && item.id.trim() !== '') {
|
|
4337
|
+
itemById.set(item.id.trim(), item);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
const orderedIds =
|
|
4341
|
+
planDocs.length > 0
|
|
4342
|
+
? planDocs
|
|
4343
|
+
.map((d) => (typeof d.id === 'string' ? d.id.trim() : ''))
|
|
4344
|
+
.filter((id) => id !== '' && itemById.has(id))
|
|
4345
|
+
: [...itemById.keys()];
|
|
4346
|
+
|
|
4347
|
+
// 1. Article body: 22-article/<date>_06_article-write_body.md
|
|
4348
|
+
const outro = typeof master.outro === 'string' ? master.outro.trim() : '';
|
|
4349
|
+
const articleSections: string[] = [];
|
|
4350
|
+
for (const id of orderedIds) {
|
|
4351
|
+
const doc = docById.get(id);
|
|
4352
|
+
const item = itemById.get(id);
|
|
4353
|
+
const title = typeof doc?.title === 'string' ? doc.title.trim() : '';
|
|
4354
|
+
const body = typeof doc?.body === 'string' ? doc.body.trim() : '';
|
|
4355
|
+
const lead = typeof item?.lead === 'string' ? item.lead.trim() : '';
|
|
4356
|
+
const insight = typeof item?.insight === 'string' ? item.insight.trim() : '';
|
|
4357
|
+
if (!title || !body || !lead || !insight) continue;
|
|
4358
|
+
const sourceUri = typeof doc?.sourceUri === 'string' ? doc.sourceUri.trim() : '';
|
|
4359
|
+
const sourceLine = sourceUri ? `[来源:${sourceLabelForDoc(doc ?? {}, sourceUri)}](${sourceUri})` : '';
|
|
4360
|
+
const parts = [`## ${title}`, lead, body, insight, ...(sourceLine ? [sourceLine] : [])];
|
|
4361
|
+
articleSections.push(parts.join('\n\n'));
|
|
4362
|
+
}
|
|
4363
|
+
if (articleSections.length > 0 && outro !== '') {
|
|
4364
|
+
const articleDir = join(workDir, '22-article');
|
|
4365
|
+
sted('mkdir', articleDir, () => mkdirSync(articleDir, { recursive: true }));
|
|
4366
|
+
const articleOut = join(articleDir, `${runDate}_06_article-write_body.md`);
|
|
4367
|
+
const articleText = `${[EDITORIAL_ARTICLE_OPENING, ...articleSections, outro].join('\n\n')}\n`;
|
|
4368
|
+
sted('write', articleOut, () => writeRaw(articleOut, articleText));
|
|
4369
|
+
console.log(`editorial-render: wrote article body (${articleSections.length} item(s)): ${articleOut}`);
|
|
4370
|
+
} else {
|
|
4371
|
+
console.log('editorial-render: skipping article body (missing items/doc fields or outro)');
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// 2. Cover brief: 23-cover/<date>_07_cover-brief.json
|
|
4375
|
+
const coverSubject = typeof master.cover === 'string' ? master.cover.trim() : '';
|
|
4376
|
+
if (coverSubject !== '') {
|
|
4377
|
+
const coverDir = join(workDir, '23-cover');
|
|
4378
|
+
sted('mkdir', coverDir, () => mkdirSync(coverDir, { recursive: true }));
|
|
4379
|
+
const coverOut = join(coverDir, `${runDate}_07_cover-brief.json`);
|
|
4380
|
+
sted('write', coverOut, () => writeJsonText(coverOut, { subject: coverSubject }));
|
|
4381
|
+
console.log(`editorial-render: wrote cover brief: ${coverOut}`);
|
|
4382
|
+
} else {
|
|
4383
|
+
console.log('editorial-render: skipping cover brief (missing cover in script plan)');
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
// 3. XHS card plan: 25-xhs/<date>_18_xhs_plan.json (only when cards_enabled === 'true')
|
|
4387
|
+
if (cardsEnabled === 'true') {
|
|
4388
|
+
const cardsMeta =
|
|
4389
|
+
master.cards !== null && typeof master.cards === 'object'
|
|
4390
|
+
? (master.cards as Record<string, unknown>)
|
|
4391
|
+
: null;
|
|
4392
|
+
const cardTitle = typeof cardsMeta?.title === 'string' ? cardsMeta.title.trim() : '';
|
|
4393
|
+
const cardBody = typeof cardsMeta?.body === 'string' ? cardsMeta.body.trim() : '';
|
|
4394
|
+
const contentItems = orderedIds
|
|
4395
|
+
.map((id) => ({ id, item: itemById.get(id), doc: docById.get(id) }))
|
|
4396
|
+
.filter(({ item }) => {
|
|
4397
|
+
const c = item?.card as Record<string, unknown> | undefined;
|
|
4398
|
+
return (
|
|
4399
|
+
c !== null &&
|
|
4400
|
+
typeof c === 'object' &&
|
|
4401
|
+
typeof c.copy === 'string' &&
|
|
4402
|
+
c.copy.trim() !== '' &&
|
|
4403
|
+
Array.isArray(c.points) &&
|
|
4404
|
+
c.points.length >= 1 &&
|
|
4405
|
+
typeof c.subject === 'string' &&
|
|
4406
|
+
c.subject.trim() !== ''
|
|
4407
|
+
);
|
|
4408
|
+
});
|
|
4409
|
+
const maxContent = Math.max(1, cardsCount - 2);
|
|
4410
|
+
const chosenContent = contentItems.slice(0, maxContent);
|
|
4411
|
+
if (cardTitle !== '' && cardBody !== '' && chosenContent.length > 0) {
|
|
4412
|
+
const firstSubject =
|
|
4413
|
+
coverSubject ||
|
|
4414
|
+
((chosenContent[0]?.item?.card as { subject?: string } | undefined)?.subject ?? '').trim() ||
|
|
4415
|
+
'Minimalist geometric light sculpture on a dark studio table';
|
|
4416
|
+
const introText =
|
|
4417
|
+
typeof master.intro === 'string' && master.intro.trim() !== '' ? master.intro.trim() : cardTitle;
|
|
4418
|
+
const coverPoints = chosenContent
|
|
4419
|
+
.slice(0, 4)
|
|
4420
|
+
.map(({ item }) => ((item?.card as { copy?: string } | undefined)?.copy || '').trim())
|
|
4421
|
+
.filter(Boolean);
|
|
4422
|
+
const renderedCards: Record<string, unknown>[] = [
|
|
4423
|
+
{
|
|
4424
|
+
role: 'cover',
|
|
4425
|
+
copy: cardTitle.slice(0, 12),
|
|
4426
|
+
points: coverPoints.length >= 2 ? coverPoints : [cardTitle, introText.slice(0, 36)],
|
|
4427
|
+
layout: 'hero',
|
|
4428
|
+
subject: firstSubject,
|
|
4429
|
+
sourcePoint: introText,
|
|
4430
|
+
},
|
|
4431
|
+
...chosenContent.map(({ id, item, doc }) => {
|
|
4432
|
+
const card = item?.card as { copy: string; points: string[]; subject: string };
|
|
4433
|
+
const lead = typeof item?.lead === 'string' && item.lead.trim() !== '' ? item.lead.trim() : '';
|
|
4434
|
+
const docTitle = typeof doc?.title === 'string' && doc.title.trim() !== '' ? doc.title.trim() : id;
|
|
4435
|
+
return {
|
|
4436
|
+
role: 'content',
|
|
4437
|
+
copy: card.copy.trim(),
|
|
4438
|
+
points: card.points
|
|
4439
|
+
.map((p) => String(p).trim())
|
|
4440
|
+
.filter(Boolean)
|
|
4441
|
+
.slice(0, 4),
|
|
4442
|
+
layout: 'split',
|
|
4443
|
+
subject: card.subject.trim(),
|
|
4444
|
+
sourcePoint: lead || docTitle,
|
|
4445
|
+
itemId: id,
|
|
4446
|
+
};
|
|
4447
|
+
}),
|
|
4448
|
+
];
|
|
4449
|
+
// Pad content cards if chosenContent.length < maxContent so total length equals cardsCount
|
|
4450
|
+
while (renderedCards.length < cardsCount - 1) {
|
|
4451
|
+
const seed = renderedCards[renderedCards.length - 1] as Record<string, unknown>;
|
|
4452
|
+
renderedCards.push({ ...seed });
|
|
4453
|
+
}
|
|
4454
|
+
const closingPoints = [outro || introText, cardTitle].filter(Boolean);
|
|
4455
|
+
renderedCards.push({
|
|
4456
|
+
role: 'closing',
|
|
4457
|
+
copy: '本期小结',
|
|
4458
|
+
points: closingPoints.length >= 2 ? closingPoints : [outro || cardTitle, '感谢关注每日AI乱炖'],
|
|
4459
|
+
layout: 'summary',
|
|
4460
|
+
subject: firstSubject,
|
|
4461
|
+
sourcePoint: outro || introText,
|
|
4462
|
+
});
|
|
4463
|
+
const xhsDir = join(workDir, '25-xhs');
|
|
4464
|
+
sted('mkdir', xhsDir, () => mkdirSync(xhsDir, { recursive: true }));
|
|
4465
|
+
const xhsOut = join(xhsDir, `${runDate}_18_xhs_plan.json`);
|
|
4466
|
+
sted('write', xhsOut, () =>
|
|
4467
|
+
writeJsonText(xhsOut, {
|
|
4468
|
+
title: cardTitle,
|
|
4469
|
+
body: cardBody,
|
|
4470
|
+
cards: renderedCards,
|
|
4471
|
+
}),
|
|
4472
|
+
);
|
|
4473
|
+
console.log(`editorial-render: wrote xhs card plan (${renderedCards.length} card(s)): ${xhsOut}`);
|
|
4474
|
+
} else {
|
|
4475
|
+
console.log('editorial-render: skipping xhs card plan (missing cards.title/body or items[].card)');
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
// 4 & 5. Social posts: 26-social/{x,threads}.md and 26-social/{weibo,linkedin}.md
|
|
4480
|
+
const social =
|
|
4481
|
+
master.social !== null && typeof master.social === 'object' ? (master.social as Record<string, unknown>) : null;
|
|
4482
|
+
const socialShort = typeof social?.short === 'string' ? social.short.trim() : '';
|
|
4483
|
+
const socialDir = join(workDir, '26-social');
|
|
4484
|
+
if (socialShort !== '') {
|
|
4485
|
+
sted('mkdir', socialDir, () => mkdirSync(socialDir, { recursive: true }));
|
|
4486
|
+
for (const name of ['x.md', 'threads.md']) {
|
|
4487
|
+
const out = join(socialDir, name);
|
|
4488
|
+
sted('write', out, () => writeRaw(out, `${socialShort}\n`));
|
|
4489
|
+
}
|
|
4490
|
+
console.log('editorial-render: wrote 26-social/{x,threads}.md');
|
|
4491
|
+
} else {
|
|
4492
|
+
console.log('editorial-render: skipping social short posts (missing social.short in script plan)');
|
|
4493
|
+
}
|
|
4494
|
+
const socialLong = typeof social?.long === 'string' ? social.long.trim() : '';
|
|
4495
|
+
if (socialLong !== '') {
|
|
4496
|
+
sted('mkdir', socialDir, () => mkdirSync(socialDir, { recursive: true }));
|
|
4497
|
+
for (const name of ['weibo.md', 'linkedin.md']) {
|
|
4498
|
+
const out = join(socialDir, name);
|
|
4499
|
+
sted('write', out, () => writeRaw(out, `${socialLong}\n`));
|
|
4500
|
+
}
|
|
4501
|
+
console.log('editorial-render: wrote 26-social/{weibo,linkedin}.md');
|
|
4502
|
+
} else {
|
|
4503
|
+
console.log('editorial-render: skipping social long posts (missing social.long in script plan)');
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4043
4507
|
export function dailyShowNotes(args: string[]): void {
|
|
4044
4508
|
const [workDir = '', runDate = '', pluginsPath = '', title = ''] = args;
|
|
4045
4509
|
const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|