@gobing-ai/knowledge-kit 0.0.25 → 0.0.26

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.
@@ -438,15 +438,15 @@ function parseStage(
438
438
  // deliberately stay in the YAML.
439
439
 
440
440
  const DAILY_DIRS = [
441
- '1-ingest',
442
- '2-facts',
443
- '2-plan',
444
- '2-article',
445
- '2-cover',
446
- '2-xhs',
447
- '2-script',
448
- '3-audio',
449
- '3-publish',
441
+ '10-ingest',
442
+ '20-facts',
443
+ '21-plan',
444
+ '22-article',
445
+ '23-cover',
446
+ '25-xhs',
447
+ '24-script',
448
+ '30-audio',
449
+ '31-publish',
450
450
  ] as const;
451
451
 
452
452
  /** `date -u +%FT%TZ` — the timestamp grammar the step-timing lines have always used. */
@@ -619,23 +619,23 @@ function dailyPrepare(args: string[]): void {
619
619
  mkdirSync(anchorDir, { recursive: true });
620
620
  writeFileSync(join(anchorDir, 'work-dir.txt'), `${workDir}\n`);
621
621
  });
622
- sted('write', join(workDir, '1-ingest/source.json'), () =>
623
- writeJsonText(join(workDir, '1-ingest/source.json'), {
622
+ sted('write', join(workDir, '10-ingest/source.json'), () =>
623
+ writeJsonText(join(workDir, '10-ingest/source.json'), {
624
624
  limit: Number(limit),
625
625
  cursor: cursor === '' ? undefined : cursor,
626
626
  stateFile: stateFile === '' ? undefined : stateFile,
627
627
  }),
628
628
  );
629
629
  if (last30daysEnabled !== 'true') {
630
- sted('write', join(workDir, '1-ingest/last30days-docs.json'), () =>
631
- writeFileSync(join(workDir, '1-ingest/last30days-docs.json'), '[]'),
630
+ sted('write', join(workDir, '10-ingest/last30days-docs.json'), () =>
631
+ writeFileSync(join(workDir, '10-ingest/last30days-docs.json'), '[]'),
632
632
  );
633
633
  }
634
- sted('write', join(workDir, '1-ingest/last30days.json'), () =>
635
- writeJsonText(join(workDir, '1-ingest/last30days.json'), { topic }),
634
+ sted('write', join(workDir, '10-ingest/last30days.json'), () =>
635
+ writeJsonText(join(workDir, '10-ingest/last30days.json'), { topic }),
636
636
  );
637
- sted('write', join(workDir, '1-ingest/horizon.json'), () =>
638
- writeJsonText(join(workDir, '1-ingest/horizon.json'), { hours: Number(horizonHours) }),
637
+ sted('write', join(workDir, '10-ingest/horizon.json'), () =>
638
+ writeJsonText(join(workDir, '10-ingest/horizon.json'), { hours: Number(horizonHours) }),
639
639
  );
640
640
  });
641
641
  }
@@ -679,9 +679,9 @@ function resolveWorksDir(): string {
679
679
 
680
680
  function dailyCollectFacts(args: string[]): void {
681
681
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
682
- const content = join(workDir, '2-facts', `${runDate}_02_collect_content.json`);
683
- const facts = join(workDir, '2-facts', `${runDate}_02_collect_core-facts.md`);
684
- const blended = join(workDir, '1-ingest', `${runDate}_02_collect_blended.json`);
682
+ const content = join(workDir, '20-facts', `${runDate}_02_collect_content.json`);
683
+ const facts = join(workDir, '20-facts', `${runDate}_02_collect_core-facts.md`);
684
+ const blended = join(workDir, '10-ingest', `${runDate}_02_collect_blended.json`);
685
685
  timed(workDir, 'collect', () => {
686
686
  if (isFile(content) && isFile(facts)) {
687
687
  console.log(`core facts cached: ${facts}`);
@@ -696,9 +696,9 @@ function dailyCollectFacts(args: string[]): void {
696
696
 
697
697
  function dailyPlan(args: string[]): void {
698
698
  const [workDir = '', runDate = '', pluginsPath = '', planMaxItems = ''] = args;
699
- const planContent = join(workDir, '2-plan', `${runDate}_03_plan_content.json`);
700
- const planDocs = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
701
- const blended = join(workDir, '1-ingest', `${runDate}_02_collect_blended.json`);
699
+ const planContent = join(workDir, '21-plan', `${runDate}_03_plan_content.json`);
700
+ const planDocs = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
701
+ const blended = join(workDir, '10-ingest', `${runDate}_02_collect_blended.json`);
702
702
  timed(workDir, 'plan', () => {
703
703
  if (isFile(planDocs)) {
704
704
  console.log(`plan cached: ${planDocs}`);
@@ -728,9 +728,9 @@ function dailyQcContent(args: string[]): void {
728
728
  minUrgency = '',
729
729
  minImpact = '',
730
730
  ] = args;
731
- const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
732
- const annotated = join(workDir, '2-plan', `${runDate}_04_quality-control-content_annotated.json`);
733
- const planContent = join(workDir, '2-plan', `${runDate}_03_plan_content.json`);
731
+ const candidates = join(workDir, '21-plan', `${runDate}_04_quality-control-content_candidates.json`);
732
+ const annotated = join(workDir, '21-plan', `${runDate}_04_quality-control-content_annotated.json`);
733
+ const planContent = join(workDir, '21-plan', `${runDate}_03_plan_content.json`);
734
734
  timed(workDir, 'quality-control-content', () => {
735
735
  if (isFile(candidates)) {
736
736
  console.log(`candidates cached: ${candidates}`);
@@ -830,10 +830,10 @@ function assertScriptContent(scriptPath: string): void {
830
830
 
831
831
  function dailyArticle(args: string[]): void {
832
832
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
833
- const articleMd = join(workDir, '2-article', `${runDate}_06_article_article.md`);
834
- const articleContent = join(workDir, '2-article', `${runDate}_06_article_content.json`);
835
- const planIn = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
836
- const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
833
+ const articleMd = join(workDir, '22-article', `${runDate}_06_article_article.md`);
834
+ const articleContent = join(workDir, '22-article', `${runDate}_06_article_content.json`);
835
+ const planIn = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
836
+ const candidates = join(workDir, '21-plan', `${runDate}_04_quality-control-content_candidates.json`);
837
837
  timed(workDir, 'article', () => {
838
838
  let planUsable = true;
839
839
  const raw = isFile(planIn) ? readFileSync(planIn, 'utf-8') : '';
@@ -870,7 +870,7 @@ function dailyArticle(args: string[]): void {
870
870
  // Agent-written prose body (workflow `article-write` state) replaces the mechanical
871
871
  // per-item render when present — the 3-format split: surfdash gets an article,
872
872
  // not a news list (2026-09-16 feedback).
873
- const proseBody = join(workDir, '2-article', `${runDate}_06_article-write_body.md`);
873
+ const proseBody = join(workDir, '22-article', `${runDate}_06_article-write_body.md`);
874
874
  kk(
875
875
  [
876
876
  'executor',
@@ -928,9 +928,9 @@ function coverBrief(briefPath: string): { reason: string | null; subject: string
928
928
  */
929
929
  function dailyCover(args: string[]): void {
930
930
  const [workDir = '', runDate = '', pluginsPath = '', provider = '', model = '', fallback = ''] = args;
931
- const briefPath = join(workDir, '2-cover', `${runDate}_07_cover-brief.json`);
932
- const envPath = join(workDir, '2-cover', `${runDate}_07_cover_image-gen-in.json`);
933
- const contentOut = join(workDir, '2-cover', 'content.json');
931
+ const briefPath = join(workDir, '23-cover', `${runDate}_07_cover-brief.json`);
932
+ const envPath = join(workDir, '23-cover', `${runDate}_07_cover_image-gen-in.json`);
933
+ const contentOut = join(workDir, '23-cover', 'content.json');
934
934
  timed(workDir, 'cover', () => {
935
935
  const brief = coverBrief(briefPath);
936
936
  const vars: Record<string, string> = {};
@@ -987,20 +987,20 @@ function coverNormalizeBody(workDir: string, runDate: string): void {
987
987
  // F614 R5: the artifact name follows the format preset (`<stem>-<format>-NN.<ext>`,
988
988
  // paths.ts), so normalize reads the path the plugin itself recorded in the manifest instead
989
989
  // of a hard-coded name. The legacy content-cover-01.{png,jpg} paths stay as stale cleanup.
990
- const legacyPng = join(workDir, '2-cover', 'content-cover-01.png');
991
- const legacyJpg = join(workDir, '2-cover', 'content-cover-01.jpg');
990
+ const legacyPng = join(workDir, '23-cover', 'content-cover-01.png');
991
+ const legacyJpg = join(workDir, '23-cover', 'content-cover-01.jpg');
992
992
  // Must match the name dailyPublishPrep and the article->cover workflow guard read:
993
993
  // `${runDate}_07_cover_cover.png` (a plain `cover-${runDate}.png` left options.coverPath
994
994
  // undefined and silently dropped the cover from both podcast and surfdash — 2026-09-16 run).
995
- const out = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
996
- const images = metadataOf(readJson(join(workDir, '2-cover', 'content.json'))).images;
995
+ const out = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
996
+ const images = metadataOf(readJson(join(workDir, '23-cover', 'content.json'))).images;
997
997
  const first = Array.isArray(images) ? images[0] : undefined;
998
998
  const recorded =
999
999
  first !== null && typeof first === 'object' && typeof (first as Record<string, unknown>).path === 'string'
1000
1000
  ? ((first as Record<string, unknown>).path as string)
1001
1001
  : '';
1002
1002
  if (recorded === '' || !isFile(recorded)) {
1003
- failWith(1, 'cover normalize: no image artifact in 2-cover/content.json');
1003
+ failWith(1, 'cover normalize: no image artifact in 23-cover/content.json');
1004
1004
  }
1005
1005
  const source = recorded;
1006
1006
  if (source.endsWith('.jpg') || source.endsWith('.jpeg')) {
@@ -1096,10 +1096,10 @@ function xhsPlan(planPath: string, count: number): { title: string; body: string
1096
1096
  function dailyXhsRender(args: string[]): void {
1097
1097
  const [workDir = '', runDate = '', pluginsPath = '', countArg = '', style = '', provider = '', model = ''] = args;
1098
1098
  const count = Number.parseInt(countArg, 10);
1099
- const planPath = join(workDir, '2-xhs', `${runDate}_18_xhs_plan.json`);
1100
- const cardsDir = join(workDir, '2-xhs', 'cards');
1101
- const manifestPath = join(workDir, '2-xhs', 'cards.json');
1102
- const contentPath = join(workDir, '2-xhs', 'content.json');
1099
+ const planPath = join(workDir, '25-xhs', `${runDate}_18_xhs_plan.json`);
1100
+ const cardsDir = join(workDir, '25-xhs', 'cards');
1101
+ const manifestPath = join(workDir, '25-xhs', 'cards.json');
1102
+ const contentPath = join(workDir, '25-xhs', 'content.json');
1103
1103
  timed(workDir, 'xhs-render', () => {
1104
1104
  if (!Number.isInteger(count) || count < 1 || count > XHS_MAX_CARDS) {
1105
1105
  failWith(1, `xhs render: xhs_cards must be 1..${XHS_MAX_CARDS}, got '${countArg}'`);
@@ -1176,8 +1176,8 @@ function dailyXhsRender(args: string[]): void {
1176
1176
  });
1177
1177
  }
1178
1178
 
1179
- /** A single-target xhs Result, not the fan-out aggregate: `ok` plus `metadata.publishStatus`. */
1180
- function xhsStatus(path: string): string {
1179
+ /** A single-target publish Result, not the fan-out aggregate: `ok` plus `metadata.publishStatus`. */
1180
+ function resultPublishStatus(path: string): string {
1181
1181
  const parsed = readJson(path);
1182
1182
  if (parsed?.ok !== true) return '';
1183
1183
  const status = metadataOf(parsed).publishStatus;
@@ -1193,14 +1193,14 @@ function xhsStatus(path: string): string {
1193
1193
  */
1194
1194
  function dailyPublishXhs(args: string[]): void {
1195
1195
  const [workDir = '', runDate = '', pluginsPath = '', live = 'false'] = args;
1196
- const content = join(workDir, '2-xhs', 'content.json');
1197
- const input = join(workDir, '3-publish', `${runDate}_19_publish-xhs_content.json`);
1198
- const result = join(workDir, '3-publish', `${runDate}_19_publish-xhs_result.json`);
1196
+ const content = join(workDir, '25-xhs', 'content.json');
1197
+ const input = join(workDir, '31-publish', `${runDate}_19_publish-xhs_content.json`);
1198
+ const result = join(workDir, '31-publish', `${runDate}_19_publish-xhs_result.json`);
1199
1199
  const want = live === 'true' ? 'public' : 'draft';
1200
1200
  timed(workDir, 'publish-xhs', () => {
1201
1201
  // Cache on the *requested* status: a cached draft must not silently satisfy a later
1202
1202
  // xhs_live=true re-run (that would report a live post that never happened).
1203
- if (xhsStatus(result) === want) {
1203
+ if (resultPublishStatus(result) === want) {
1204
1204
  console.log(`publish-xhs cached: ${want} already recorded in ${result}`);
1205
1205
  return;
1206
1206
  }
@@ -1226,6 +1226,50 @@ function dailyPublishXhs(args: string[]): void {
1226
1226
  });
1227
1227
  }
1228
1228
 
1229
+ /**
1230
+ * WeChat MP: drive the console via `wechatmp-pub`'s browser transport and save the article to the
1231
+ * draft box (草稿箱). Draft by default; the 发表 flow runs only on an explicit launch-time
1232
+ * `wechat_live=true` (ADR-022 rail, same as xhs). Independent and non-blocking like XHS: the
1233
+ * outcome never enters `publish_status` (surfdash + podcast remain the publication contract).
1234
+ */
1235
+ function dailyPublishWechat(args: string[]): void {
1236
+ const [workDir = '', runDate = '', pluginsPath = '', live = 'false'] = args;
1237
+ const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
1238
+ const input = join(workDir, '31-publish', `${runDate}_20_publish-wechat_content.json`);
1239
+ const result = join(workDir, '31-publish', `${runDate}_20_publish-wechat_result.json`);
1240
+ const want = live === 'true' ? 'public' : 'draft';
1241
+ timed(workDir, 'publish-wechat', () => {
1242
+ // Cache on the *requested* status: a cached draft must not silently satisfy a later
1243
+ // wechat_live=true re-run (that would report a live post that never happened).
1244
+ if (resultPublishStatus(result) === want) {
1245
+ console.log(`publish-wechat cached: ${want} already recorded in ${result}`);
1246
+ return;
1247
+ }
1248
+ if (!isFile(article)) {
1249
+ console.log(`publish-wechat: no article at ${article} — skip`);
1250
+ return;
1251
+ }
1252
+ const packaged = requireJson(article, 'publish-wechat');
1253
+ sted('write', input, () =>
1254
+ writeJsonText(input, { ...packaged, metadata: { ...metadataOf(packaged), publishStatus: want } }),
1255
+ );
1256
+ // `soft` keeps the WeChat target non-blocking: the plugin still writes its own failed Result
1257
+ // and prints the failing step (session preflight, editor paste, save control), which is what
1258
+ // the operator needs to re-run — never a silent skip.
1259
+ const status = kk(
1260
+ ['executor', 'run', 'wechatmp-pub', '--in', input, '--out', result, ...pluginsArgs(pluginsPath)],
1261
+ {
1262
+ soft: true,
1263
+ },
1264
+ );
1265
+ console.log(
1266
+ status === 0
1267
+ ? `publish-wechat: ${want} dispatched; outcome recorded in ${result}`
1268
+ : `WARNING: publish-wechat — wechatmp-pub exited ${status}; WeChat target skipped (run continues, see ${result})`,
1269
+ );
1270
+ });
1271
+ }
1272
+
1229
1273
  /**
1230
1274
  * F613 R1: snapshot the shipped style contract into the run's work dir. Spur workflows have no
1231
1275
  * workflow-dir var, so the script-write prompt reads this copy — and the snapshot freezes the
@@ -1234,13 +1278,13 @@ function dailyPublishXhs(args: string[]): void {
1234
1278
  */
1235
1279
  function dailyScriptStyle(args: string[]): void {
1236
1280
  const [workDir = '', runDate = ''] = args;
1237
- const snapshot = join(workDir, '2-script', `${runDate}_08_script-write_style.md`);
1281
+ const snapshot = join(workDir, '24-script', `${runDate}_08_script-write_style.md`);
1238
1282
  if (isFile(snapshot)) {
1239
1283
  console.log(`script style cached: ${snapshot}`);
1240
1284
  return;
1241
1285
  }
1242
1286
  const sidecar = requireSidecar('', STYLE_SIDECAR, 'daily-voice-style');
1243
- const scriptDir = join(workDir, '2-script');
1287
+ const scriptDir = join(workDir, '24-script');
1244
1288
  sted('mkdir', scriptDir, () => mkdirSync(scriptDir, { recursive: true }));
1245
1289
  sted('cp', snapshot, () => copyFileSync(sidecar, snapshot));
1246
1290
  console.log(`script style snapshot: ${snapshot}`);
@@ -1248,9 +1292,9 @@ function dailyScriptStyle(args: string[]): void {
1248
1292
 
1249
1293
  function dailyScript(args: string[]): void {
1250
1294
  const [workDir = '', runDate = '', pluginsPath = '', language = '', title = '', voiceProfile = ''] = args;
1251
- const yamlOut = join(workDir, '2-script', `${runDate}_08_script_voicescript.yaml`);
1252
- const contentOut = join(workDir, '2-script', `${runDate}_08_script_content.json`);
1253
- const planIn = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
1295
+ const yamlOut = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
1296
+ const contentOut = join(workDir, '24-script', `${runDate}_08_script_content.json`);
1297
+ const planIn = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
1254
1298
  timed(workDir, 'script', () => {
1255
1299
  if (isFile(yamlOut)) {
1256
1300
  console.log(`voicescript capped by plan: ${yamlOut}`);
@@ -1264,7 +1308,7 @@ function dailyScript(args: string[]): void {
1264
1308
  // F613 R4: always passed as absolute paths — spawnPluginEntry runs plugins with
1265
1309
  // cwd = the plugin package dir, so a relative workDir would silently miss the
1266
1310
  // plan; a plan file absent at build time reports missing-plan (template fallback).
1267
- DAILYNEWS_SCRIPT_PLAN: resolve(join(workDir, '2-script', `${runDate}_08_script-write_plan.json`)),
1311
+ DAILYNEWS_SCRIPT_PLAN: resolve(join(workDir, '24-script', `${runDate}_08_script-write_plan.json`)),
1268
1312
  DAILYNEWS_STYLE_FILE: resolve(resolveSidecar('', STYLE_SIDECAR)),
1269
1313
  },
1270
1314
  });
@@ -1286,8 +1330,8 @@ function dailyScript(args: string[]): void {
1286
1330
 
1287
1331
  function dailyWrapDocs(args: string[]): void {
1288
1332
  const [workDir = '', runDate = '', voiceProfile = ''] = args;
1289
- const scriptPath = join(workDir, '2-script', `${runDate}_08_script_voicescript.yaml`);
1290
- const docsPath = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
1333
+ const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
1334
+ const docsPath = join(workDir, '30-audio', `${runDate}_10_wrap-docs_docs.json`);
1291
1335
  timed(workDir, 'wrap-docs', () => {
1292
1336
  // R3 (task 0139): the second validator placement — covers a HITL-edited YAML and the
1293
1337
  // script-stage cache path, neither of which re-enters dailyScript's fresh-write branch.
@@ -1323,8 +1367,8 @@ function dailyGenerate(args: string[]): void {
1323
1367
  `generate: unknown vdriver '${vdriver}' (expected one of: ${Object.keys(VOICE_DRIVERS).join(', ')})`,
1324
1368
  );
1325
1369
  }
1326
- const docs = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
1327
- const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
1370
+ const docs = join(workDir, '30-audio', `${runDate}_10_wrap-docs_docs.json`);
1371
+ const content = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
1328
1372
  timed(workDir, 'generate', () => {
1329
1373
  if (isFile(content) && metadataOf(readJson(content)).qc !== undefined) {
1330
1374
  const qc = metadataOf(readJson(content)).qc as Record<string, unknown> | undefined;
@@ -1350,7 +1394,7 @@ function dailyIntro(args: string[]): void {
1350
1394
  console.log('intro-music: intro_music var empty — skip');
1351
1395
  return;
1352
1396
  }
1353
- const contentPath = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
1397
+ const contentPath = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
1354
1398
  timed(workDir, 'intro-music', () => {
1355
1399
  const content = requireJson(contentPath, 'intro-music');
1356
1400
  const meta = metadataOf(content);
@@ -1453,7 +1497,7 @@ function dailyIntro(args: string[]): void {
1453
1497
 
1454
1498
  function dailyQualityReport(args: string[]): void {
1455
1499
  const [workDir = '', runDate = ''] = args;
1456
- const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
1500
+ const content = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
1457
1501
  timed(workDir, 'quality-control', () => {
1458
1502
  const qc = metadataOf(readJson(content)).qc as Record<string, unknown> | undefined;
1459
1503
  console.log('=== Voice Quality Control Report ===');
@@ -1511,10 +1555,10 @@ function stampId3Tags(audioFile: string, title: string, runDate: string, artist:
1511
1555
 
1512
1556
  function dailyPublishPrep(args: string[]): void {
1513
1557
  const [workDir = '', runDate = '', artist = '', album = ''] = args;
1514
- const articlePath = join(workDir, '2-article', `${runDate}_06_article_content.json`);
1515
- const audioPath = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
1516
- const prepPath = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
1517
- const coverPath = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
1558
+ const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
1559
+ const audioPath = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
1560
+ const prepPath = join(workDir, '31-publish', `${runDate}_13_publish-prep_content.json`);
1561
+ const coverPath = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
1518
1562
  timed(workDir, 'publish-prep', () => {
1519
1563
  const article = requireJson(articlePath, 'publish-prep');
1520
1564
  const audioMeta = metadataOf(readJson(audioPath));
@@ -1553,8 +1597,8 @@ function dailyPublishPrep(args: string[]): void {
1553
1597
 
1554
1598
  function dailyPublishSurfdash(args: string[]): void {
1555
1599
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
1556
- const prep = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
1557
- const result = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
1600
+ const prep = join(workDir, '31-publish', `${runDate}_13_publish-prep_content.json`);
1601
+ const result = join(workDir, '31-publish', `${runDate}_14_publish-surfdash_result.json`);
1558
1602
  timed(workDir, 'publish-surfdash', () => {
1559
1603
  if (targetPublished(result, 'surfdash-pub')) {
1560
1604
  console.log(`publish-surfdash cached: surfdash target ok in ${result}`);
@@ -1583,16 +1627,16 @@ function dailyPublishSurfdash(args: string[]): void {
1583
1627
 
1584
1628
  function dailyShowNotes(args: string[]): void {
1585
1629
  const [workDir = '', runDate = '', pluginsPath = '', title = ''] = args;
1586
- const article = join(workDir, '2-article', `${runDate}_06_article_content.json`);
1630
+ const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
1587
1631
  // The preceding `publish-surfdash` state writes this artifact; `_17_publish_result.json` does
1588
1632
  // not exist yet at `show-notes` time (the later `publish` state merges it), so reading it left
1589
1633
  // PODCAST_PUB_ARTICLE_POST_PATH permanently empty (task 0137).
1590
- const result = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
1591
- const out = join(workDir, '3-publish', `${runDate}_15_show-notes_show-notes.md`);
1592
- const plan = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
1634
+ const result = join(workDir, '31-publish', `${runDate}_14_publish-surfdash_result.json`);
1635
+ const out = join(workDir, '31-publish', `${runDate}_15_show-notes_show-notes.md`);
1636
+ const plan = join(workDir, '21-plan', `${runDate}_03_plan_plan.json`);
1593
1637
  // Generated-episode content carries `metadata.segments[]` — the source of real
1594
1638
  // audio offsets for the show-notes timeline (jump-to-item timestamps).
1595
- const audioContent = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
1639
+ const audioContent = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
1596
1640
  timed(workDir, 'show-notes', () => {
1597
1641
  const effective = readJson(article)?.title;
1598
1642
  const titleArg = title !== '' ? title : typeof effective === 'string' ? effective : '';
@@ -1625,9 +1669,9 @@ function firstPostPath(parsed: Record<string, unknown> | null, target: string):
1625
1669
 
1626
1670
  function dailyPublishPodcast(args: string[]): void {
1627
1671
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
1628
- const prep = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
1629
- const notes = join(workDir, '3-publish', `${runDate}_15_show-notes_show-notes.md`);
1630
- const result = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
1672
+ const prep = join(workDir, '31-publish', `${runDate}_13_publish-prep_content.json`);
1673
+ const notes = join(workDir, '31-publish', `${runDate}_15_show-notes_show-notes.md`);
1674
+ const result = join(workDir, '31-publish', `${runDate}_16_publish-podcast_result.json`);
1631
1675
  timed(workDir, 'publish-podcast', () => {
1632
1676
  if (targetPublished(result, 'podcast-pub')) {
1633
1677
  console.log(`publish-podcast cached: podcast target ok in ${result}`);
@@ -1667,10 +1711,10 @@ function dailyPublishPodcast(args: string[]): void {
1667
1711
 
1668
1712
  function dailyPublishClassify(args: string[]): void {
1669
1713
  const [workDir = '', runDate = ''] = args;
1670
- const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
1671
- const statusFile = join(workDir, '3-publish', `${runDate}_17_publish_publish-status.txt`);
1672
- const surfdash = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
1673
- const podcast = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
1714
+ const result = join(workDir, '31-publish', `${runDate}_17_publish_result.json`);
1715
+ const statusFile = join(workDir, '31-publish', `${runDate}_17_publish_publish-status.txt`);
1716
+ const surfdash = join(workDir, '31-publish', `${runDate}_14_publish-surfdash_result.json`);
1717
+ const podcast = join(workDir, '31-publish', `${runDate}_16_publish-podcast_result.json`);
1674
1718
  timed(workDir, 'publish', () => {
1675
1719
  // Both per-target files must exist: the pre-0133 `jq -s … || echo '[]'` chain could not read
1676
1720
  // a missing one (jq exits 2 after printing the slurped partial), which drove status to `none`.
@@ -1696,7 +1740,7 @@ function targetsOf(path: string): Record<string, unknown>[] {
1696
1740
 
1697
1741
  function dailyPublishPartial(args: string[]): void {
1698
1742
  const [workDir = '', runDate = ''] = args;
1699
- const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
1743
+ const result = join(workDir, '31-publish', `${runDate}_17_publish_result.json`);
1700
1744
  timed(workDir, 'publish-partial', () => {
1701
1745
  const failed = targetsOf(result)
1702
1746
  .filter((entry) => entry.ok !== true || entry.status !== 'published')
@@ -1727,8 +1771,8 @@ function dailyPatchSurfdash(args: string[]): void {
1727
1771
  return;
1728
1772
  }
1729
1773
  timed(workDir, 'patch-surfdash', () => {
1730
- const sdResult = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
1731
- const podcastResult = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
1774
+ const sdResult = join(workDir, '31-publish', `${runDate}_14_publish-surfdash_result.json`);
1775
+ const podcastResult = join(workDir, '31-publish', `${runDate}_16_publish-podcast_result.json`);
1732
1776
  if (!isFile(sdResult) || !isFile(podcastResult)) {
1733
1777
  failWith(1, 'patch-surfdash: missing surfdash/podcast publish results — run the publish states first');
1734
1778
  }
@@ -1755,7 +1799,7 @@ function dailyPatchSurfdash(args: string[]): void {
1755
1799
  const slugDir = dirname(postPath);
1756
1800
  const showNotesUrl = typeof pod.url === 'string' ? pod.url : '';
1757
1801
  const coverUrl = ((pod.metadata as Record<string, unknown> | undefined)?.imageUrl as string | undefined) ?? '';
1758
- const localCover = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
1802
+ const localCover = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
1759
1803
  const dateIso = `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`;
1760
1804
  const crossLink: Record<string, string> = {
1761
1805
  zh: `🎧 本期已同步制作为播客节目,[点击收听《每日 AI 资讯 · ${dateIso}》](${showNotesUrl})。`,
@@ -1857,8 +1901,8 @@ function dailyRunReport(args: string[]): void {
1857
1901
  ] = args;
1858
1902
  const reportMd = join(workDir, `${runDate}_19_run-report_report.md`);
1859
1903
  const reportJson = join(workDir, `${runDate}_19_run-report_report.json`);
1860
- const cands = join(workDir, '2-plan', `${runDate}_19_run-report_episode-plan.json`);
1861
- const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
1904
+ const cands = join(workDir, '21-plan', `${runDate}_19_run-report_episode-plan.json`);
1905
+ const candidates = join(workDir, '21-plan', `${runDate}_04_quality-control-content_candidates.json`);
1862
1906
  timed(workDir, 'run-report', () => {
1863
1907
  if (isFile(reportMd)) {
1864
1908
  console.log(`run report cached: ${reportMd}`);
@@ -1880,20 +1924,20 @@ function dailyRunReport(args: string[]): void {
1880
1924
  QC_MIN_IMPACT: minImpact,
1881
1925
  NEWS_REPORT_REJECTED_FILE: join(
1882
1926
  absWorkDir,
1883
- '2-plan',
1927
+ '21-plan',
1884
1928
  `${runDate}_04_quality-control-content_candidates.rejected.json`,
1885
1929
  ),
1886
1930
  NEWS_REPORT_TIMING_FILE: join(absWorkDir, 'step-timing.json'),
1887
1931
  // 0139 R5b: the generate stage's QC audit, read best-effort (absent -> section omitted).
1888
- NEWS_REPORT_QC_FILE: join(absWorkDir, '3-audio', `${runDate}_11_generate_content.json`),
1932
+ NEWS_REPORT_QC_FILE: join(absWorkDir, '30-audio', `${runDate}_11_generate_content.json`),
1889
1933
  // 20260917 funnel: the plan stage's full selection trail (every fetched title +
1890
1934
  // scores + disposition), rendered as the 选稿漏斗 section (absent -> omitted).
1891
- NEWS_REPORT_PLAN_AUDIT_FILE: join(absWorkDir, '2-plan', `${runDate}_03_plan_plan.json.audit.json`),
1935
+ NEWS_REPORT_PLAN_AUDIT_FILE: join(absWorkDir, '21-plan', `${runDate}_03_plan_plan.json.audit.json`),
1892
1936
  // 20260917 funnel: the blended fan-in input, rendered as the 语料构成
1893
1937
  // source/category composition (absent -> omitted).
1894
- NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '1-ingest', `${runDate}_02_collect_blended.json`),
1938
+ NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '10-ingest', `${runDate}_02_collect_blended.json`),
1895
1939
  // F613 R5: the script stage's Content, read best-effort (absent -> 脚本来源 omitted).
1896
- NEWS_REPORT_SCRIPT_FILE: join(absWorkDir, '2-script', `${runDate}_08_script_content.json`),
1940
+ NEWS_REPORT_SCRIPT_FILE: join(absWorkDir, '24-script', `${runDate}_08_script_content.json`),
1897
1941
  NEWS_REPORT_DATE: runDate,
1898
1942
  },
1899
1943
  });
@@ -1914,8 +1958,8 @@ function dailyRunReport(args: string[]): void {
1914
1958
  */
1915
1959
  function dailyTranslateLocalize(args: string[]): void {
1916
1960
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
1917
- const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
1918
- const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-localize_zh-post-path.txt`);
1961
+ const result = join(workDir, '31-publish', `${runDate}_17_publish_result.json`);
1962
+ const zhFile = join(workDir, '31-publish', `${runDate}_20_translate-localize_zh-post-path.txt`);
1919
1963
  timed(workDir, 'translate-localize', () => {
1920
1964
  const postPath = firstPostPath(readJson(result), 'surfdash-pub');
1921
1965
  const zh = postPath.endsWith('index.md') ? postPath : '';
@@ -1924,10 +1968,10 @@ function dailyTranslateLocalize(args: string[]): void {
1924
1968
  for (const target of ['en', 'ja'] as const) {
1925
1969
  sted(
1926
1970
  'write',
1927
- join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
1971
+ join(workDir, '31-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
1928
1972
  () =>
1929
1973
  writeRaw(
1930
- join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
1974
+ join(workDir, '31-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
1931
1975
  '',
1932
1976
  ),
1933
1977
  );
@@ -1936,17 +1980,17 @@ function dailyTranslateLocalize(args: string[]): void {
1936
1980
  return;
1937
1981
  }
1938
1982
  for (const target of ['en', 'ja'] as const) {
1939
- const markerFile = join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`);
1983
+ const markerFile = join(workDir, '31-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`);
1940
1984
  const existing = zh.replace('/zh/', `/${target}/`);
1941
1985
  if (isFile(existing)) {
1942
1986
  sted('write', markerFile, () => writeRaw(markerFile, existing));
1943
1987
  console.log(`${target} draft cached: ${existing}`);
1944
1988
  continue;
1945
1989
  }
1946
- const scaffold = join(workDir, '3-publish', `${runDate}_20_translate-localize_scaffold-${target}.json`);
1990
+ const scaffold = join(workDir, '31-publish', `${runDate}_20_translate-localize_scaffold-${target}.json`);
1947
1991
  const scaffoldResult = join(
1948
1992
  workDir,
1949
- '3-publish',
1993
+ '31-publish',
1950
1994
  `${runDate}_20_translate-localize_scaffold-${target}-result.json`,
1951
1995
  );
1952
1996
  sted('write', scaffold, () =>
@@ -1970,8 +2014,8 @@ function dailyTranslateLocalize(args: string[]): void {
1970
2014
 
1971
2015
  function dailyTranslateSync(args: string[]): void {
1972
2016
  const [workDir = '', runDate = '', pluginsPath = '', zhPathFile = ''] = args;
1973
- const sync = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync.json`);
1974
- const syncResult = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync-result.json`);
2017
+ const sync = join(workDir, '31-publish', `${runDate}_24_translate-sync_sync.json`);
2018
+ const syncResult = join(workDir, '31-publish', `${runDate}_24_translate-sync_sync-result.json`);
1975
2019
  timed(workDir, 'translate-sync', () => {
1976
2020
  // Reads the marker file itself — the collapsed `translate` state has no
1977
2021
  // cross-state var to hand the path through.
@@ -2101,6 +2145,9 @@ if (import.meta.main)
2101
2145
  case 'daily-publish-xhs':
2102
2146
  dailyPublishXhs(rest);
2103
2147
  break;
2148
+ case 'daily-publish-wechat':
2149
+ dailyPublishWechat(rest);
2150
+ break;
2104
2151
  case 'daily-publish-classify':
2105
2152
  dailyPublishClassify(rest);
2106
2153
  break;
@@ -129,8 +129,11 @@ const CONTENT_ECHO_NOISE_RE = /[,。、::;;!!??…\s「」『』【】
129
129
  /** Research scaffold labels the plan-translate stage must not emit as spoken copy. */
130
130
  const CONTENT_SCAFFOLD_LABEL_RE =
131
131
  /^(?:\*\*)?[「『]?(?:背景|潜在影响|可做角度|为什么现在值得注意|社区讨论)[」』]?(?:\*\*)?(?:[::,,]|\s|$)/;
132
- /** `标签,` metadata tails — the plan item's tag line spoken aloud. */
133
- const CONTENT_TAG_TAIL_RE = /标签[::,,]/;
132
+ /** `标签:…` / `标签,ai、google` metadata tails — the plan item's tag line spoken aloud.
133
+ * Terminal-anchored: the tail runs to the end of the segment with no sentence-ending
134
+ * punctuation. Prose like 「一个推了两年的硬件标签,突然从新品上消失了。」 continues past the
135
+ * comma to a 。 and must not trip the gate (false positive of the 2026-09-28 daily run). */
136
+ const CONTENT_TAG_TAIL_RE = /标签[::,,][^\n。!?…]*$/;
134
137
  /** `wss,//` / `https。//` — the translator replacing a URL colon with a Chinese comma. */
135
138
  const CONTENT_URL_MANGLE_RE = /[,。]\/\//;
136
139