@gobing-ai/knowledge-kit 0.1.8 → 0.1.10

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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.js +725 -1101
  3. package/dist/board/daily-voice/index.js +722 -101
  4. package/dist/board/itc/index.js +13 -0
  5. package/dist/index.js +1796 -102
  6. package/package.json +1 -1
  7. package/plugins/generations/content-gen/dist/index.js +4 -0
  8. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  9. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  10. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  11. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  12. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  13. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  14. package/plugins/generations/image-gen/dist/index.js +4 -0
  15. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  16. package/plugins/generations/voice-gen/dist/index.js +4 -0
  17. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  18. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  19. package/plugins/ingestions/horizon-ingest/dist/index.js +28 -1
  20. package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
  21. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  22. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  23. package/plugins/ingestions/web-search/dist/index.js +4 -0
  24. package/plugins/kk/config.example.yaml +8 -0
  25. package/plugins/kk/plugin.json +1 -1
  26. package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
  27. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
  28. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
  29. package/plugins/kk/scripts/daily-stages.ts +665 -78
  30. package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
  31. package/plugins/kk/scripts/sentences.ts +134 -0
  32. package/plugins/kk/scripts/series-names.ts +124 -0
  33. package/plugins/kk/scripts/topic-boundary.json +18 -83
  34. package/plugins/kk/workflows/kk-collect.yaml +2 -0
  35. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +112 -9
  36. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
  37. package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
  38. package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
  39. package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
  40. package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
  41. package/plugins/publishings/csdn-pub/dist/index.js +4 -0
  42. package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
  43. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  44. package/plugins/publishings/infoq-pub/dist/index.js +4 -0
  45. package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
  46. package/plugins/publishings/jike-pub/dist/index.js +4 -0
  47. package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
  48. package/plugins/publishings/juejin-pub/dist/index.js +4 -0
  49. package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
  50. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  51. package/plugins/publishings/podcast-pub/README.md +65 -0
  52. package/plugins/publishings/podcast-pub/dist/index.js +1906 -374
  53. package/plugins/publishings/podcast-pub/plugin.json +1 -1
  54. package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
  55. package/plugins/publishings/podcast-pub/src/index.ts +211 -34
  56. package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
  57. package/plugins/publishings/podcast-pub/src/seo.ts +1079 -0
  58. package/plugins/publishings/podcast-pub/src/show-notes.ts +74 -14
  59. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  60. package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
  61. package/plugins/publishings/sspai-pub/dist/index.js +4 -0
  62. package/plugins/publishings/substack-pub/dist/index.js +4 -0
  63. package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
  64. package/plugins/publishings/surfdash-pub/dist/index.js +313 -32
  65. package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
  66. package/plugins/publishings/surfdash-pub/src/index.ts +170 -39
  67. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  68. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  69. package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
  70. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
  71. package/plugins/publishings/weibo-pub/dist/index.js +4 -0
  72. package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
  73. package/plugins/publishings/x-pub/dist/index.js +4 -0
  74. package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
  75. package/plugins/publishings/xhs-pub/dist/index.js +4 -0
  76. package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
  77. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
  78. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -12345,6 +12345,10 @@ init_config();
12345
12345
 
12346
12346
  // ../../packages/kk-core/src/render-md.ts
12347
12347
  if (false) {}
12348
+ // ../../packages/kk-core/src/sentences.ts
12349
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12350
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12351
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12348
12352
  // ../../plugins/publishings/surfdash-pub/src/index.ts
12349
12353
  init_dist();
12350
12354
 
@@ -12360,11 +12364,17 @@ function localeIndexPath(postPath, locale) {
12360
12364
  return;
12361
12365
  return `${postPath.slice(0, idx)}${sep}${locale}${sep}${postPath.slice(idx + marker.length)}`;
12362
12366
  }
12367
+ var SERIES_NAMES = {
12368
+ zh: "\u6BCF\u65E5AI\u4E71\u7096",
12369
+ en: "Daily AI Stew",
12370
+ ja: "\u30C7\u30A4\u30EA\u30FCAI\u30B7\u30C1\u30E5\u30FC"
12371
+ };
12363
12372
  function crossLinkFor(locale, dateIso, showNotesUrl) {
12373
+ const series = SERIES_NAMES[locale];
12364
12374
  const links = {
12365
- zh: `\uD83C\uDFA7 \u672C\u671F\u5DF2\u540C\u6B65\u5236\u4F5C\u4E3A\u64AD\u5BA2\u8282\u76EE\uFF0C[\u70B9\u51FB\u6536\u542C\u300A\u6BCF\u65E5AI\u4E71\u7096 \xB7 ${dateIso}\u300B](${showNotesUrl})\u3002`,
12366
- en: `\uD83C\uDFA7 This episode is also available as a podcast: [listen to \u6BCF\u65E5AI\u4E71\u7096 \xB7 ${dateIso}](${showNotesUrl}).`,
12367
- ja: `\uD83C\uDFA7 \u672C\u30A8\u30D4\u30BD\u30FC\u30C9\u306F\u30DD\u30C3\u30C9\u30AD\u30E3\u30B9\u30C8\u3067\u3082\u914D\u4FE1\u4E2D\uFF1A[\u6BCF\u65E5AI\u4E71\u7096 \xB7 ${dateIso} \u3092\u8074\u304F](${showNotesUrl})\u3002`
12375
+ zh: `\uD83C\uDFA7 \u672C\u671F\u5DF2\u540C\u6B65\u5236\u4F5C\u4E3A\u64AD\u5BA2\u8282\u76EE\uFF0C[\u70B9\u51FB\u6536\u542C\u300A${series} \xB7 ${dateIso}\u300B](${showNotesUrl})\u3002`,
12376
+ en: `\uD83C\uDFA7 This episode is also available as a podcast: [listen to ${series} \xB7 ${dateIso}](${showNotesUrl}).`,
12377
+ ja: `\uD83C\uDFA7 \u672C\u30A8\u30D4\u30BD\u30FC\u30C9\u306F\u30DD\u30C3\u30C9\u30AD\u30E3\u30B9\u30C8\u3067\u3082\u914D\u4FE1\u4E2D\uFF1A[${series} \xB7 ${dateIso} \u3092\u8074\u304F](${showNotesUrl})\u3002`
12368
12378
  };
12369
12379
  return links[locale];
12370
12380
  }
@@ -12402,10 +12412,16 @@ async function patchPostFiles(request) {
12402
12412
  const end = frontmatterEnd(md);
12403
12413
  const fm = end < 0 ? md : md.slice(0, end);
12404
12414
  if (request.coverUrl !== "") {
12415
+ if (!/^https:\/\//.test(request.coverUrl)) {
12416
+ throw new Error(`coverUrl must be an absolute https:// URL, got: ${request.coverUrl}`);
12417
+ }
12405
12418
  md = upsertFrontmatterKey(md, "image", request.coverUrl);
12406
12419
  md = upsertFrontmatterKey(md, "og_image", request.coverUrl);
12407
12420
  images[locale] = request.coverUrl;
12408
- } else if (!/^image:/m.test(fm) && await fs.exists(request.localCover)) {
12421
+ } else if (!/^image:/m.test(fm)) {
12422
+ if (!await fs.exists(request.localCover)) {
12423
+ throw new Error(`${locale} post has no cover: coverUrl is empty, the frontmatter has no image key, and localCover does not exist (${request.localCover})`);
12424
+ }
12409
12425
  const asset = join(dir, "assets", "cover.png");
12410
12426
  await fs.ensureDir(dirname2(asset));
12411
12427
  await fs.copy(request.localCover, asset);
@@ -12414,11 +12430,20 @@ async function patchPostFiles(request) {
12414
12430
  md = upsertFrontmatterKey(md, "og_image", "./assets/cover.png");
12415
12431
  images[locale] = "./assets/cover.png";
12416
12432
  }
12417
- if (request.showNotesUrl !== "" && !md.includes(request.showNotesUrl)) {
12418
- md = `${md.replace(/\s*$/, "")}
12433
+ if (request.showNotesUrl !== "") {
12434
+ const link = crossLinkFor(locale, request.dateIso, request.showNotesUrl);
12435
+ const lines = md.split(`
12436
+ `);
12437
+ const carriesUrl = lines.some((line) => line.includes(request.showNotesUrl));
12438
+ if (!carriesUrl) {
12439
+ md = `${md.replace(/\s*$/, "")}
12419
12440
 
12420
- ${crossLinkFor(locale, request.dateIso, request.showNotesUrl)}
12441
+ ${link}
12421
12442
  `;
12443
+ } else {
12444
+ md = lines.map((line) => line.includes(request.showNotesUrl) && line.trimStart().startsWith("\uD83C\uDFA7") ? link : line).join(`
12445
+ `);
12446
+ }
12422
12447
  }
12423
12448
  if (md !== before) {
12424
12449
  await fs.writeFile(indexMd, md);
@@ -12433,6 +12458,172 @@ ${crossLinkFor(locale, request.dateIso, request.showNotesUrl)}
12433
12458
  return { written, changed, absent, images, slug: basename(dirname2(request.postPath)) };
12434
12459
  }
12435
12460
 
12461
+ // ../../plugins/publishings/surfdash-pub/src/digest-check.ts
12462
+ var DIGEST_TAG = "sub-category:ai-daily";
12463
+ var PODCAST_INTROS = ["\u8FD9\u91CC\u662F\u7531Robin", "\u54B8\u8BDD\u54B8\u8BF4", "This is Robin", "Robin \u304C\u304A\u5C4A\u3051"];
12464
+ var SEO_TITLE_LIMIT = 60;
12465
+ var KEY_LINE = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/;
12466
+ var escapeRe = (token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12467
+ function unquote(value) {
12468
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
12469
+ try {
12470
+ return JSON.parse(value);
12471
+ } catch {
12472
+ return value.slice(1, -1);
12473
+ }
12474
+ }
12475
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
12476
+ return value.slice(1, -1).replaceAll("''", "'");
12477
+ }
12478
+ return value;
12479
+ }
12480
+ function parseFrontmatterScalars(markdown) {
12481
+ const out = new Map;
12482
+ const lines = markdown.split(/\r?\n/);
12483
+ if (lines[0]?.trim() !== "---")
12484
+ return out;
12485
+ const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
12486
+ if (end < 0)
12487
+ return out;
12488
+ for (let i = 1;i < end; i++) {
12489
+ const line = lines[i];
12490
+ if (!line)
12491
+ continue;
12492
+ const match = line.match(KEY_LINE);
12493
+ if (!match?.[1] || match[2] === undefined)
12494
+ continue;
12495
+ const key = match[1];
12496
+ const value = match[2].trim();
12497
+ if (value === ">-" || value === ">" || value === ">+" || value === "|-" || value === "|") {
12498
+ const parts = [];
12499
+ let j = i + 1;
12500
+ while (j < end) {
12501
+ const continuation = lines[j];
12502
+ if (!continuation || !/^[ \t]+\S/.test(continuation))
12503
+ break;
12504
+ parts.push(continuation.trim());
12505
+ j++;
12506
+ }
12507
+ out.set(key, parts.join(" "));
12508
+ i = j - 1;
12509
+ continue;
12510
+ }
12511
+ if (value === "") {
12512
+ const items = [];
12513
+ let j = i + 1;
12514
+ while (j < end) {
12515
+ const itemLine = lines[j];
12516
+ if (!itemLine || !/^\s*-\s+/.test(itemLine))
12517
+ break;
12518
+ items.push(unquote(itemLine.replace(/^\s*-\s+/, "").trim()));
12519
+ j++;
12520
+ }
12521
+ if (items.length > 0) {
12522
+ out.set(key, items);
12523
+ i = j - 1;
12524
+ } else {
12525
+ out.set(key, "");
12526
+ }
12527
+ continue;
12528
+ }
12529
+ if (value.startsWith("[") && value.endsWith("]")) {
12530
+ out.set(key, value.slice(1, -1).split(",").map((item) => unquote(item.trim())).filter((item) => item !== ""));
12531
+ continue;
12532
+ }
12533
+ out.set(key, unquote(value));
12534
+ }
12535
+ return out;
12536
+ }
12537
+ function scalar(frontmatter, key) {
12538
+ const value = frontmatter.get(key);
12539
+ if (value === undefined)
12540
+ return;
12541
+ return Array.isArray(value) ? value.join(" ") : value;
12542
+ }
12543
+ function endsAsSentence(text, locale) {
12544
+ return locale === "en" ? /[.!?]["')\u201D\u2019]*$/u.test(text.trimEnd()) : /[\u3002\uFF01\uFF1F][\u300D\u300F\u201D\uFF09]*$/u.test(text.trimEnd());
12545
+ }
12546
+ function checkDigestFrontmatter(markdown, locale) {
12547
+ const frontmatter = parseFrontmatterScalars(markdown);
12548
+ const tags = frontmatter.get("tags");
12549
+ const tagList = Array.isArray(tags) ? tags : typeof tags === "string" && tags !== "" ? [tags] : [];
12550
+ if (!tagList.some((tag) => tag.includes(DIGEST_TAG)))
12551
+ return [];
12552
+ const problems = [];
12553
+ const series = SERIES_NAMES[locale];
12554
+ const titleSep = locale === "en" ? ": " : "\uFF1A";
12555
+ const listSep = locale === "en" ? ", " : "\u3001";
12556
+ const title = scalar(frontmatter, "title");
12557
+ if (title === undefined || title === "") {
12558
+ problems.push("title: missing");
12559
+ } else {
12560
+ const match = title.match(new RegExp(`^${escapeRe(series)} \xB7 (\\d{4}-\\d{2}-\\d{2})${escapeRe(titleSep)}(.+)$`));
12561
+ if (!match) {
12562
+ problems.push(`title: expected "<series> \xB7 YYYY-MM-DD${titleSep}<h1>${listSep}<h2>" for ${series}, got "${title}"`);
12563
+ } else {
12564
+ const rest = match[2] ?? "";
12565
+ const heads = locale === "en" ? rest.split(", ") : rest.split("\u3001");
12566
+ const seps = locale === "en" ? (rest.match(/, /g) ?? []).length : (rest.match(/\u3001/g) ?? []).length;
12567
+ if (seps !== 1 || heads.some((head) => head.trim() === "")) {
12568
+ problems.push(`title: expected exactly two non-empty headlines joined "${listSep}", got "${rest}"`);
12569
+ }
12570
+ }
12571
+ const seoTitle = `${title} | Surfing`;
12572
+ const seoLength = Array.from(seoTitle).length;
12573
+ if (seoLength > SEO_TITLE_LIMIT) {
12574
+ problems.push(`title: "${seoTitle}" is ${seoLength} code points (max ${SEO_TITLE_LIMIT})`);
12575
+ }
12576
+ if (title.includes("\u2026") || title.includes("..."))
12577
+ problems.push('title: contains "\u2026" or "..."');
12578
+ }
12579
+ const description = scalar(frontmatter, "description");
12580
+ const [min, max] = locale === "en" ? [120, 155] : [70, 100];
12581
+ if (description === undefined || description === "") {
12582
+ problems.push("description: missing");
12583
+ } else {
12584
+ const length = Array.from(description).length;
12585
+ if (length < min || length > max) {
12586
+ problems.push(`description: ${length} code points (expected ${min}\u2013${max} for ${locale})`);
12587
+ }
12588
+ if (!endsAsSentence(description, locale)) {
12589
+ problems.push("description: does not end with sentence punctuation");
12590
+ }
12591
+ const lower = description.toLowerCase();
12592
+ const intro = PODCAST_INTROS.find((candidate) => lower.startsWith(candidate.toLowerCase()));
12593
+ if (intro)
12594
+ problems.push(`description: starts with the podcast intro ("${intro}")`);
12595
+ if (description.includes("\u2026") || description.includes("...")) {
12596
+ problems.push('description: contains "\u2026" or "..."');
12597
+ }
12598
+ }
12599
+ const points = frontmatter.get("key_points");
12600
+ if (!Array.isArray(points) || points.length === 0) {
12601
+ problems.push("key_points: missing");
12602
+ } else {
12603
+ const labelMax = locale === "en" ? 28 : 14;
12604
+ points.forEach((point, index) => {
12605
+ const sep = point.indexOf(" \u2014 ");
12606
+ if (sep < 0) {
12607
+ problems.push(`key_points[${index}]: missing " \u2014 " separator`);
12608
+ return;
12609
+ }
12610
+ const label = point.slice(0, sep);
12611
+ const text = point.slice(sep + 3);
12612
+ const labelLength = Array.from(label).length;
12613
+ if (labelLength === 0 || labelLength > labelMax) {
12614
+ problems.push(`key_points[${index}]: label "${label}" is ${labelLength} code points (max ${labelMax})`);
12615
+ }
12616
+ if (!endsAsSentence(text, locale)) {
12617
+ problems.push(`key_points[${index}]: text does not end with sentence punctuation`);
12618
+ }
12619
+ if (point.includes("\u2026") || point.includes("...")) {
12620
+ problems.push(`key_points[${index}]: contains "\u2026" or "..."`);
12621
+ }
12622
+ });
12623
+ }
12624
+ return problems;
12625
+ }
12626
+
12436
12627
  // ../../plugins/publishings/surfdash-pub/src/index.ts
12437
12628
  var TARGET = "surfdash";
12438
12629
  var OPERATIONS = ["publish", "scaffold-locale", "sync", "patch"];
@@ -12449,39 +12640,31 @@ function normalizeTag(raw) {
12449
12640
  }
12450
12641
  return slugify2(raw);
12451
12642
  }
12452
- function mapContentToSurfingMarkdown(content, options = {}) {
12453
- let title = content.title;
12454
- if (!title) {
12455
- const match = content.body.match(/^#\s+(.+)$/m);
12456
- title = match?.[1] ? match[1].trim() : "Untitled Post";
12457
- }
12458
- let description;
12459
- if (typeof content.metadata?.description === "string") {
12460
- description = content.metadata.description;
12461
- } else {
12462
- const paragraphs = content.body.split(/\n\s*\n/).map((p) => p.replace(/^#+\s+.*$/, "").trim()).filter((p) => p.length > 0);
12463
- if (paragraphs.length > 0) {
12464
- description = paragraphs[0];
12465
- }
12466
- }
12643
+ function contentTags(content) {
12467
12644
  const rawTags = Array.isArray(content.metadata?.tags) ? [...content.metadata.tags] : Array.isArray(content.metadata?.keywords) ? [...content.metadata.keywords] : [];
12468
12645
  if (typeof content.metadata?.subCategory === "string" && content.metadata.subCategory.trim().length > 0) {
12469
12646
  const subRaw = content.metadata.subCategory.trim();
12470
12647
  rawTags.push(/^sub-category\s*:/i.test(subRaw) ? subRaw : `sub-category:${subRaw}`);
12471
12648
  }
12472
- const tags = [
12649
+ return [
12473
12650
  ...new Set(rawTags.filter((t) => typeof t === "string" && t.trim().length > 0).map((t) => normalizeTag(t)))
12474
12651
  ].filter((tag) => tag.length > 0);
12652
+ }
12653
+ function mapContentToSurfingMarkdown(content, options = {}) {
12654
+ let title = content.title;
12655
+ if (!title) {
12656
+ const match = content.body.match(/^#\s+(.+)$/m);
12657
+ title = match?.[1] ? match[1].trim() : "Untitled Post";
12658
+ }
12659
+ const description = typeof content.metadata?.description === "string" && content.metadata.description.length > 0 ? content.metadata.description : "";
12660
+ const tags = contentTags(content);
12475
12661
  const isoDate = options.publishDate ?? new Date().toISOString().slice(0, 10);
12476
- const firstRefUrl = content.references?.[0]?.url;
12477
- const canonical = typeof firstRefUrl === "string" ? firstRefUrl : typeof content.metadata?.canonicalUrl === "string" ? content.metadata.canonicalUrl : undefined;
12478
12662
  const draft = typeof content.metadata?.published === "boolean" ? !content.metadata.published : false;
12479
12663
  const frontmatterObj = {
12480
12664
  title,
12481
- description: description ?? "",
12665
+ description,
12482
12666
  tags,
12483
12667
  publishDate: isoDate,
12484
- ...canonical ? { canonical } : {},
12485
12668
  draft,
12486
12669
  ...options.image ? { image: options.image, og_image: options.image } : {}
12487
12670
  };
@@ -12492,6 +12675,9 @@ function mapContentToSurfingMarkdown(content, options = {}) {
12492
12675
  ${content.body.trim()}
12493
12676
  `;
12494
12677
  }
12678
+ function isDigestContent(content) {
12679
+ return contentTags(content).some((tag) => tag.includes(DIGEST_TAG));
12680
+ }
12495
12681
  function fail(error) {
12496
12682
  return ResultSchema.parse({ ok: false, target: TARGET, error });
12497
12683
  }
@@ -12552,7 +12738,8 @@ async function spawnCli(cmd, cwd) {
12552
12738
  const proc = Bun.spawn(cmd, {
12553
12739
  cwd,
12554
12740
  stdout: "pipe",
12555
- stderr: "pipe"
12741
+ stderr: "pipe",
12742
+ env: process.env
12556
12743
  });
12557
12744
  const [exitCode, stdout, stderr] = await Promise.all([
12558
12745
  proc.exited,
@@ -12568,6 +12755,46 @@ function cliErrorText(spawned) {
12568
12755
  function withEmdashTokenHint(errText) {
12569
12756
  return /INVALID_TOKEN|API error 401/.test(errText) ? `${errText} \u2014 hint: regenerate the EmDash token (surfing.salty.vip admin console) and update EMDASH_API_TOKEN in ~/.config/kk/config.yaml` : errText;
12570
12757
  }
12758
+ function readRefinedSnapshot(filePath) {
12759
+ if (!existsSync2(filePath))
12760
+ return;
12761
+ const raw = readFileSync2(filePath, "utf-8");
12762
+ const block = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
12763
+ const fm = block?.[1] ?? "";
12764
+ if (!fm || !/^refined:\s*true$/m.test(fm))
12765
+ return;
12766
+ const read = (key) => {
12767
+ const m = fm.match(new RegExp(`^${key}:\\s*(?:>-?
12768
+ ((?:[ \\t]+.*
12769
+ ?)+)|(.*))$`, "m"));
12770
+ if (!m)
12771
+ return;
12772
+ const value = (m[1] ?? m[2] ?? "").replace(/\n[ \t]+/g, " ").trim();
12773
+ return value.replace(/^["']|["']$/g, "") || undefined;
12774
+ };
12775
+ return {
12776
+ title: read("title"),
12777
+ seoTitle: read("seo_title"),
12778
+ description: read("description"),
12779
+ canonical: read("canonical")
12780
+ };
12781
+ }
12782
+ function requireRefined(filePath) {
12783
+ if (readRefinedSnapshot(filePath))
12784
+ return;
12785
+ return `surfdash refine step did not run for ${filePath} (no \`refined: true\`): the publishing pipeline is broken \u2014 the post would ship with generator-supplied SEO metadata`;
12786
+ }
12787
+ function digestCheckGate(filePath, locale) {
12788
+ const problems = checkDigestFrontmatter(readFileSync2(filePath, "utf-8"), locale);
12789
+ if (problems.length === 0)
12790
+ return;
12791
+ if (process.env.SURFDASH_DIGEST_CHECK === "warn") {
12792
+ for (const problem of problems)
12793
+ echoError(`digest check (warn): ${problem}`);
12794
+ return;
12795
+ }
12796
+ return `digest frontmatter check failed: ${problems.join("; ")}`;
12797
+ }
12571
12798
  async function pushPostGit(surfdashRoot, relPostDir, message) {
12572
12799
  return commitPostPaths(surfdashRoot, [relPostDir], message);
12573
12800
  }
@@ -12584,12 +12811,40 @@ async function assertMainBranch(surfdashRoot) {
12584
12811
  }
12585
12812
  return { ok: true };
12586
12813
  }
12814
+ async function fastForwardToOrigin(surfdashRoot) {
12815
+ const fetched = await spawnCli(["git", "fetch", "--quiet", "origin", "main"], surfdashRoot);
12816
+ if (fetched.exitCode !== 0)
12817
+ return { ok: true };
12818
+ const counts = await spawnCli(["git", "rev-list", "--left-right", "--count", "origin/main...HEAD"], surfdashRoot);
12819
+ if (counts.exitCode !== 0)
12820
+ return { ok: true };
12821
+ const [behindRaw = "0", aheadRaw = "0"] = counts.stdout.trim().split(/\s+/);
12822
+ const behind = Number(behindRaw);
12823
+ const ahead = Number(aheadRaw);
12824
+ if (!Number.isFinite(behind) || !Number.isFinite(ahead) || behind === 0)
12825
+ return { ok: true };
12826
+ if (ahead > 0) {
12827
+ return {
12828
+ ok: false,
12829
+ error: `surfdash checkout diverged from origin/main (${ahead} local, ${behind} remote commit(s)): reconcile it by hand before publishing`
12830
+ };
12831
+ }
12832
+ const merged = await spawnCli(["git", "merge", "--ff-only", "--quiet", "origin/main"], surfdashRoot);
12833
+ if (merged.exitCode !== 0) {
12834
+ return { ok: false, error: `surfdash fast-forward to origin/main failed: ${cliErrorText(merged)}` };
12835
+ }
12836
+ echoError(`surfdash: fast-forwarded ${behind} commit(s) to origin/main before writing`);
12837
+ return { ok: true };
12838
+ }
12587
12839
  async function commitPostPaths(surfdashRoot, relPaths, message, push = true) {
12588
12840
  if (relPaths.length === 0)
12589
12841
  return { ok: true, hash: "unchanged" };
12590
12842
  const branch = await assertMainBranch(surfdashRoot);
12591
12843
  if (!branch.ok)
12592
12844
  return branch;
12845
+ const base = await fastForwardToOrigin(surfdashRoot);
12846
+ if (!base.ok)
12847
+ return base;
12593
12848
  const add = await spawnCli(["git", "add", "--", ...relPaths], surfdashRoot);
12594
12849
  if (add.exitCode !== 0)
12595
12850
  return { ok: false, error: `surfdash git add failed: ${cliErrorText(add)}` };
@@ -12716,6 +12971,12 @@ async function syncPost(options) {
12716
12971
  if (spawned.exitCode !== 0) {
12717
12972
  return fail(`publish-orchestrator sync failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`);
12718
12973
  }
12974
+ const localeInPath = postPath.postPath.match(/[\\/]content[\\/]posts[\\/]articles[\\/](zh|en|ja)[\\/]/);
12975
+ if (localeInPath) {
12976
+ const digestProblem = digestCheckGate(postPath.postPath, localeInPath[1]);
12977
+ if (digestProblem)
12978
+ return fail(digestProblem);
12979
+ }
12719
12980
  return ResultSchema.parse({
12720
12981
  ok: true,
12721
12982
  target: TARGET,
@@ -12820,12 +13081,18 @@ async function publishPost(payload) {
12820
13081
  return fail(`post path escapes ${articlesRoot}`);
12821
13082
  }
12822
13083
  const coverPath = payload.content.metadata?.coverImage;
12823
- if (typeof coverPath === "string" && existsSync2(coverPath) && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
13084
+ if (typeof coverPath === "string" && !existsSync2(coverPath)) {
13085
+ return fail(`metadata.coverImage not found: ${coverPath}`);
13086
+ }
13087
+ if (typeof coverPath === "string" && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
12824
13088
  return fail(`metadata.coverImage must be one of ${[...COVER_EXTS].join(", ")}`);
12825
13089
  }
13090
+ if (isDigestContent(payload.content) && typeof coverPath !== "string") {
13091
+ return fail("digest post has no cover image (metadata.coverImage)");
13092
+ }
12826
13093
  await createNodeFileSystem().ensureDir(postDir);
12827
13094
  let imageFrontmatter;
12828
- if (typeof coverPath === "string" && existsSync2(coverPath)) {
13095
+ if (typeof coverPath === "string") {
12829
13096
  const assetsDir = join2(postDir, "assets");
12830
13097
  await createNodeFileSystem().ensureDir(assetsDir);
12831
13098
  const coverName = `cover${extname(coverPath).toLowerCase()}`;
@@ -12843,6 +13110,13 @@ async function publishPost(payload) {
12843
13110
  if (spawned.exitCode !== 0) {
12844
13111
  return fail(`postsurfing CLI failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`);
12845
13112
  }
13113
+ const notRefined = requireRefined(filePath);
13114
+ if (notRefined)
13115
+ return fail(notRefined);
13116
+ const digestProblem = digestCheckGate(filePath, locale);
13117
+ if (digestProblem)
13118
+ return fail(digestProblem);
13119
+ const refined = readRefinedSnapshot(filePath);
12846
13120
  const baseUrl = (process.env.SURFDASH_URL ?? "https://surfing.salty.vip").replace(/\/$/, "");
12847
13121
  const articleUrl = `${baseUrl}${locale === "en" ? "" : `/${locale}`}/posts/${slug}`;
12848
13122
  if (process.env.SURFDASH_AUTO_PUSH === "0") {
@@ -12851,7 +13125,7 @@ async function publishPost(payload) {
12851
13125
  target: TARGET,
12852
13126
  url: articleUrl,
12853
13127
  message: "Published via postsurfing CLI (auto-push disabled)",
12854
- metadata: { postPath: filePath }
13128
+ metadata: { postPath: filePath, refinedTitle: refined?.title, refinedCanonical: refined?.canonical }
12855
13129
  });
12856
13130
  }
12857
13131
  const relPostDir = join2("content", "posts", "articles", locale, slug);
@@ -12864,7 +13138,7 @@ async function publishPost(payload) {
12864
13138
  target: TARGET,
12865
13139
  url: articleUrl,
12866
13140
  message: live ? "Published, committed, pushed, and live on the site" : "Published and pushed; URL not live within the verify window \u2014 check the surfdash deploy workflow",
12867
- metadata: { postPath: filePath, gitCommit: pushed.hash, live }
13141
+ metadata: { postPath: filePath, gitCommit: pushed.hash, live, refinedTitle: refined?.title }
12868
13142
  });
12869
13143
  } catch (err) {
12870
13144
  return fail(`Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`);
@@ -12941,9 +13215,14 @@ if (import.meta.main) {
12941
13215
  process.exit(await main2());
12942
13216
  }
12943
13217
  export {
13218
+ DIGEST_TAG,
12944
13219
  PostsurfingFileCliTransport,
12945
13220
  awaitArticleLive,
13221
+ checkDigestFrontmatter,
12946
13222
  commitPostPaths,
13223
+ contentTags,
13224
+ digestCheckGate,
13225
+ fastForwardToOrigin,
12947
13226
  getPublishTransport,
12948
13227
  main2 as main,
12949
13228
  mapContentToSurfingMarkdown,
@@ -12951,6 +13230,8 @@ export {
12951
13230
  normalizeTag,
12952
13231
  processPublishIO,
12953
13232
  pushPostGit,
13233
+ readRefinedSnapshot,
13234
+ requireRefined,
12954
13235
  slugify2 as slugify,
12955
13236
  withEmdashTokenHint
12956
13237
  };