@gobing-ai/knowledge-kit 0.1.9 → 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 (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +4 -0
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +4 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  7. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  8. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  9. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  10. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  11. package/plugins/generations/image-gen/dist/index.js +4 -0
  12. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  13. package/plugins/generations/voice-gen/dist/index.js +4 -0
  14. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  15. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  16. package/plugins/ingestions/horizon-ingest/dist/index.js +4 -0
  17. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  18. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  19. package/plugins/ingestions/web-search/dist/index.js +4 -0
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/daily-stages.ts +86 -1
  22. package/plugins/kk/scripts/sentences.ts +134 -0
  23. package/plugins/kk/scripts/series-names.ts +124 -0
  24. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +37 -3
  25. package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
  26. package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
  27. package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
  28. package/plugins/publishings/csdn-pub/dist/index.js +4 -0
  29. package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
  30. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  31. package/plugins/publishings/infoq-pub/dist/index.js +4 -0
  32. package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
  33. package/plugins/publishings/jike-pub/dist/index.js +4 -0
  34. package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
  35. package/plugins/publishings/juejin-pub/dist/index.js +4 -0
  36. package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
  37. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  38. package/plugins/publishings/podcast-pub/dist/index.js +105 -8
  39. package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
  40. package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
  41. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  42. package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
  43. package/plugins/publishings/sspai-pub/dist/index.js +4 -0
  44. package/plugins/publishings/substack-pub/dist/index.js +4 -0
  45. package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
  46. package/plugins/publishings/surfdash-pub/dist/index.js +247 -27
  47. package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
  48. package/plugins/publishings/surfdash-pub/src/index.ts +76 -29
  49. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  50. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  51. package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
  52. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
  53. package/plugins/publishings/weibo-pub/dist/index.js +4 -0
  54. package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
  55. package/plugins/publishings/x-pub/dist/index.js +4 -0
  56. package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
  57. package/plugins/publishings/xhs-pub/dist/index.js +4 -0
  58. package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
  59. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
  60. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -12312,6 +12312,107 @@ async function writeJsonFile(path, value, fs = createNodeFileSystem()) {
12312
12312
  }
12313
12313
  // ../../packages/kk-core/src/render-md.ts
12314
12314
  if (false) {}
12315
+ // ../../packages/kk-core/src/sentences.ts
12316
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12317
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12318
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12319
+ var OPEN_PAIRS = {
12320
+ "\u300C": "\u300D",
12321
+ "\u300E": "\u300F",
12322
+ "\u201C": "\u201D",
12323
+ "\uFF08": "\uFF09",
12324
+ "(": ")"
12325
+ };
12326
+ var CLOSE_PAIRS = {
12327
+ "\u300D": "\u300C",
12328
+ "\u300F": "\u300E",
12329
+ "\u201D": "\u201C",
12330
+ "\uFF09": "\uFF08",
12331
+ ")": "("
12332
+ };
12333
+ function splitSentences(text, options = {}) {
12334
+ if (!text || typeof text !== "string")
12335
+ return [];
12336
+ const results = [];
12337
+ let sentenceStart = 0;
12338
+ let stack = [];
12339
+ let i = 0;
12340
+ const isTerminator = (c) => CJK_TERMINATORS.has(c) || options.cjkOnly !== true && LATIN_TERMINATORS.has(c);
12341
+ while (i < text.length) {
12342
+ const ch = text[i];
12343
+ if (isTerminator(ch)) {
12344
+ let termEnd = i;
12345
+ let hasCjkTerm = false;
12346
+ let hasLatinTerm = false;
12347
+ while (termEnd < text.length) {
12348
+ const termChar = text[termEnd];
12349
+ if (!termChar || !isTerminator(termChar)) {
12350
+ break;
12351
+ }
12352
+ if (CJK_TERMINATORS.has(termChar))
12353
+ hasCjkTerm = true;
12354
+ if (LATIN_TERMINATORS.has(termChar))
12355
+ hasLatinTerm = true;
12356
+ termEnd++;
12357
+ }
12358
+ let closerEnd = termEnd;
12359
+ const tempStack = [...stack];
12360
+ while (closerEnd < text.length) {
12361
+ const closer = text[closerEnd];
12362
+ if (!closer || !ATTACHED_CLOSERS.has(closer)) {
12363
+ break;
12364
+ }
12365
+ const top = tempStack[tempStack.length - 1];
12366
+ if (top !== undefined && top === closer) {
12367
+ tempStack.pop();
12368
+ } else if (CLOSE_PAIRS[closer]) {
12369
+ const idx = tempStack.lastIndexOf(closer);
12370
+ if (idx >= 0)
12371
+ tempStack.splice(idx, 1);
12372
+ }
12373
+ closerEnd++;
12374
+ }
12375
+ let isBoundary = false;
12376
+ if (tempStack.length === 0) {
12377
+ if (hasCjkTerm) {
12378
+ isBoundary = true;
12379
+ } else if (hasLatinTerm) {
12380
+ const nextChar = text[closerEnd];
12381
+ if (closerEnd === text.length || nextChar !== undefined && /\s/.test(nextChar)) {
12382
+ isBoundary = true;
12383
+ }
12384
+ }
12385
+ }
12386
+ if (isBoundary) {
12387
+ const sentence = text.slice(sentenceStart, closerEnd).trim();
12388
+ if (sentence.length > 0) {
12389
+ results.push(sentence);
12390
+ }
12391
+ sentenceStart = closerEnd;
12392
+ stack = tempStack;
12393
+ i = closerEnd;
12394
+ continue;
12395
+ }
12396
+ }
12397
+ const openMatch = OPEN_PAIRS[ch];
12398
+ const closeMatch = CLOSE_PAIRS[ch];
12399
+ if (openMatch) {
12400
+ stack.push(openMatch);
12401
+ } else if (closeMatch) {
12402
+ const idx = stack.lastIndexOf(ch);
12403
+ if (idx >= 0)
12404
+ stack.splice(idx, 1);
12405
+ }
12406
+ i++;
12407
+ }
12408
+ if (sentenceStart < text.length) {
12409
+ const tail = text.slice(sentenceStart).trim();
12410
+ if (tail.length > 0) {
12411
+ results.push(tail);
12412
+ }
12413
+ }
12414
+ return results;
12415
+ }
12315
12416
  // ../../packages/publish-harness/src/index.ts
12316
12417
  class FakeTransportCore {
12317
12418
  callLogs = [];
@@ -12514,8 +12615,8 @@ class MicrofeedClient {
12514
12615
  }
12515
12616
 
12516
12617
  // ../../plugins/publishings/podcast-pub/src/seo.ts
12517
- init_dist();
12518
12618
  import { readFileSync as readFileSync2 } from "fs";
12619
+ init_dist();
12519
12620
  function codePointLength(value) {
12520
12621
  return Array.from(value).length;
12521
12622
  }
@@ -12869,11 +12970,7 @@ function clauseFromTitle(title) {
12869
12970
  return cut.replace(/\s+/g, " ").trim();
12870
12971
  }
12871
12972
  function firstSentence(text) {
12872
- const trimmed = text.replace(/\s+/g, " ").trim();
12873
- if (trimmed.length === 0)
12874
- return "";
12875
- const parts = trimmed.split(/(?<=[\u3002\uFF01\uFF1F.!?])/);
12876
- return (parts[0] ?? trimmed).trim();
12973
+ return splitSentences(text.replace(/\s+/g, " ").trim())[0] ?? "";
12877
12974
  }
12878
12975
  function planItemsFromMetadata(metadata) {
12879
12976
  const raw = podcastOf(metadata).planItems;
@@ -13869,7 +13966,7 @@ function itemBrief(d) {
13869
13966
  const meta = d.metadata ?? {};
13870
13967
  if (typeof meta.brief === "string" && meta.brief.trim().length > 0)
13871
13968
  return meta.brief.trim();
13872
- return (d.body ?? "").trim().split(/(?<=[\u3002\uFF01\uFF1F])\s*/).slice(0, 2).join("");
13969
+ return splitSentences((d.body ?? "").trim(), { cjkOnly: true }).slice(0, 2).join("");
13873
13970
  }
13874
13971
  function renderShowNotesMarkdown(docs, opts, warnings) {
13875
13972
  const slugify = (t) => {
@@ -13937,7 +14034,7 @@ function renderShowNotesMarkdown(docs, opts, warnings) {
13937
14034
  } else {
13938
14035
  docs.forEach((d, i) => {
13939
14036
  const body = (d.body ?? "").trim();
13940
- const sentences = body.split(/(?<=[\u3002\uFF01\uFF1F])\s*/).slice(0, 2).join("");
14037
+ const sentences = splitSentences(body, { cjkOnly: true }).slice(0, 2).join("");
13941
14038
  lines.push(`## ${i + 1}. ${d.title ?? `\u6761\u76EE ${i + 1}`}`, "", sentences, "", `[\u6765\u6E90\uFF1A${sourceLabel(d)}](${d.sourceUri ?? ""})`, "");
13942
14039
  });
13943
14040
  lines.push(`\u8BE6\u60C5\u89C1\u672C\u671F\u5B8C\u6574\u6587\u7AE0\uFF1A[ ${opts.title} ](${articleUrl})`, "");
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { readFileSync } from 'node:fs';
15
+ import { splitSentences } from '@gobing-ai/kk-core';
15
16
  import { echoError } from '@gobing-ai/ts-utils';
16
17
 
17
18
  /** Code-point length. Microfeed counts the same way for slugs and SEO limits. */
@@ -506,10 +507,7 @@ export function clauseFromTitle(title: string): string {
506
507
  }
507
508
 
508
509
  export function firstSentence(text: string): string {
509
- const trimmed = text.replace(/\s+/g, ' ').trim();
510
- if (trimmed.length === 0) return '';
511
- const parts = trimmed.split(/(?<=[。!?.!?])/);
512
- return (parts[0] ?? trimmed).trim();
510
+ return splitSentences(text.replace(/\s+/g, ' ').trim())[0] ?? '';
513
511
  }
514
512
 
515
513
  export function planItemsFromMetadata(metadata: Record<string, unknown> | undefined): PlanItem[] {
@@ -11,7 +11,7 @@
11
11
  * input is escaped first, and only the tags this module emits reach the API.
12
12
  */
13
13
 
14
- import type { Doc } from '@gobing-ai/kk-core';
14
+ import { type Doc, splitSentences } from '@gobing-ai/kk-core';
15
15
  import {
16
16
  type PlanItem,
17
17
  type PodcastSeoConfig,
@@ -311,9 +311,7 @@ export interface ShowNotesRenderOptions {
311
311
  function itemBrief(d: Doc): string {
312
312
  const meta = d.metadata ?? {};
313
313
  if (typeof meta.brief === 'string' && meta.brief.trim().length > 0) return meta.brief.trim();
314
- return (d.body ?? '')
315
- .trim()
316
- .split(/(?<=[。!?])\s*/)
314
+ return splitSentences((d.body ?? '').trim(), { cjkOnly: true })
317
315
  .slice(0, 2)
318
316
  .join('');
319
317
  }
@@ -407,10 +405,7 @@ export function renderShowNotesMarkdown(docs: Doc[], opts: ShowNotesRenderOption
407
405
  } else {
408
406
  docs.forEach((d, i) => {
409
407
  const body = (d.body ?? '').trim();
410
- const sentences = body
411
- .split(/(?<=[。!?])\s*/)
412
- .slice(0, 2)
413
- .join('');
408
+ const sentences = splitSentences(body, { cjkOnly: true }).slice(0, 2).join('');
414
409
  lines.push(
415
410
  `## ${i + 1}. ${d.title ?? `条目 ${i + 1}`}`,
416
411
  '',
@@ -12254,6 +12254,10 @@ init_config();
12254
12254
 
12255
12255
  // ../../packages/kk-core/src/render-md.ts
12256
12256
  if (false) {}
12257
+ // ../../packages/kk-core/src/sentences.ts
12258
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12259
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12260
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12257
12261
  // ../../packages/publish-harness/src/index.ts
12258
12262
  class FakeTransportCore {
12259
12263
  callLogs = [];
@@ -20163,6 +20163,10 @@ init_runtime_node_bun();
20163
20163
 
20164
20164
  // ../../packages/kk-core/src/render-md.ts
20165
20165
  if (false) {}
20166
+ // ../../packages/kk-core/src/sentences.ts
20167
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
20168
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
20169
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
20166
20170
  // ../../packages/publish-harness/src/index.ts
20167
20171
  import { homedir } from "os";
20168
20172
  import { join, resolve } from "path";
@@ -20163,6 +20163,10 @@ init_runtime_node_bun();
20163
20163
 
20164
20164
  // ../../packages/kk-core/src/render-md.ts
20165
20165
  if (false) {}
20166
+ // ../../packages/kk-core/src/sentences.ts
20167
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
20168
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
20169
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
20166
20170
  // ../../packages/publish-harness/src/index.ts
20167
20171
  import { homedir } from "os";
20168
20172
  import { join, resolve } from "path";
@@ -20163,6 +20163,10 @@ init_runtime_node_bun();
20163
20163
 
20164
20164
  // ../../packages/kk-core/src/render-md.ts
20165
20165
  if (false) {}
20166
+ // ../../packages/kk-core/src/sentences.ts
20167
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
20168
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
20169
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
20166
20170
  // ../../packages/publish-harness/src/index.ts
20167
20171
  import { homedir } from "os";
20168
20172
  import { join, resolve } from "path";
@@ -20972,6 +20972,10 @@ var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx)
20972
20972
  });
20973
20973
  // ../../packages/kk-core/src/render-md.ts
20974
20974
  if (false) {}
20975
+ // ../../packages/kk-core/src/sentences.ts
20976
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
20977
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
20978
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
20975
20979
  // ../../plugins/publishings/substack-pub/src/index.ts
20976
20980
  var CHANNEL = "substack";
20977
20981
  var LOGIN_URL_RE = /(\/sign-in|substack\.com\/account\/login)/;
@@ -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,34 +12640,29 @@ 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
12662
  const draft = typeof content.metadata?.published === "boolean" ? !content.metadata.published : false;
12477
12663
  const frontmatterObj = {
12478
12664
  title,
12479
- description: description ?? "",
12665
+ description,
12480
12666
  tags,
12481
12667
  publishDate: isoDate,
12482
12668
  draft,
@@ -12489,6 +12675,9 @@ function mapContentToSurfingMarkdown(content, options = {}) {
12489
12675
  ${content.body.trim()}
12490
12676
  `;
12491
12677
  }
12678
+ function isDigestContent(content) {
12679
+ return contentTags(content).some((tag) => tag.includes(DIGEST_TAG));
12680
+ }
12492
12681
  function fail(error) {
12493
12682
  return ResultSchema.parse({ ok: false, target: TARGET, error });
12494
12683
  }
@@ -12549,7 +12738,8 @@ async function spawnCli(cmd, cwd) {
12549
12738
  const proc = Bun.spawn(cmd, {
12550
12739
  cwd,
12551
12740
  stdout: "pipe",
12552
- stderr: "pipe"
12741
+ stderr: "pipe",
12742
+ env: process.env
12553
12743
  });
12554
12744
  const [exitCode, stdout, stderr] = await Promise.all([
12555
12745
  proc.exited,
@@ -12594,6 +12784,17 @@ function requireRefined(filePath) {
12594
12784
  return;
12595
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`;
12596
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
+ }
12597
12798
  async function pushPostGit(surfdashRoot, relPostDir, message) {
12598
12799
  return commitPostPaths(surfdashRoot, [relPostDir], message);
12599
12800
  }
@@ -12770,6 +12971,12 @@ async function syncPost(options) {
12770
12971
  if (spawned.exitCode !== 0) {
12771
12972
  return fail(`publish-orchestrator sync failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`);
12772
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
+ }
12773
12980
  return ResultSchema.parse({
12774
12981
  ok: true,
12775
12982
  target: TARGET,
@@ -12874,12 +13081,18 @@ async function publishPost(payload) {
12874
13081
  return fail(`post path escapes ${articlesRoot}`);
12875
13082
  }
12876
13083
  const coverPath = payload.content.metadata?.coverImage;
12877
- 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())) {
12878
13088
  return fail(`metadata.coverImage must be one of ${[...COVER_EXTS].join(", ")}`);
12879
13089
  }
13090
+ if (isDigestContent(payload.content) && typeof coverPath !== "string") {
13091
+ return fail("digest post has no cover image (metadata.coverImage)");
13092
+ }
12880
13093
  await createNodeFileSystem().ensureDir(postDir);
12881
13094
  let imageFrontmatter;
12882
- if (typeof coverPath === "string" && existsSync2(coverPath)) {
13095
+ if (typeof coverPath === "string") {
12883
13096
  const assetsDir = join2(postDir, "assets");
12884
13097
  await createNodeFileSystem().ensureDir(assetsDir);
12885
13098
  const coverName = `cover${extname(coverPath).toLowerCase()}`;
@@ -12900,6 +13113,9 @@ async function publishPost(payload) {
12900
13113
  const notRefined = requireRefined(filePath);
12901
13114
  if (notRefined)
12902
13115
  return fail(notRefined);
13116
+ const digestProblem = digestCheckGate(filePath, locale);
13117
+ if (digestProblem)
13118
+ return fail(digestProblem);
12903
13119
  const refined = readRefinedSnapshot(filePath);
12904
13120
  const baseUrl = (process.env.SURFDASH_URL ?? "https://surfing.salty.vip").replace(/\/$/, "");
12905
13121
  const articleUrl = `${baseUrl}${locale === "en" ? "" : `/${locale}`}/posts/${slug}`;
@@ -12999,9 +13215,13 @@ if (import.meta.main) {
12999
13215
  process.exit(await main2());
13000
13216
  }
13001
13217
  export {
13218
+ DIGEST_TAG,
13002
13219
  PostsurfingFileCliTransport,
13003
13220
  awaitArticleLive,
13221
+ checkDigestFrontmatter,
13004
13222
  commitPostPaths,
13223
+ contentTags,
13224
+ digestCheckGate,
13005
13225
  fastForwardToOrigin,
13006
13226
  getPublishTransport,
13007
13227
  main2 as main,