@gobing-ai/knowledge-kit 0.1.9 → 0.1.11

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +132 -25
  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-scheduled-run.ts +298 -0
  22. package/plugins/kk/scripts/daily-stages.ts +1083 -49
  23. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  24. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  25. package/plugins/kk/scripts/sentences.ts +134 -0
  26. package/plugins/kk/scripts/series-names.ts +124 -0
  27. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +194 -113
  28. package/plugins/kk/workflows/kk-daily-safety.yaml +160 -36
  29. package/plugins/publishings/bilibili-pub/dist/index.js +6 -1
  30. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/cnblogs-pub/dist/index.js +6 -1
  32. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  33. package/plugins/publishings/csdn-pub/dist/index.js +6 -1
  34. package/plugins/publishings/csdn-pub/dist/profile-check.js +6 -1
  35. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  36. package/plugins/publishings/infoq-pub/dist/index.js +6 -1
  37. package/plugins/publishings/infoq-pub/dist/profile-check.js +6 -1
  38. package/plugins/publishings/jike-pub/dist/index.js +6 -1
  39. package/plugins/publishings/jike-pub/dist/profile-check.js +6 -1
  40. package/plugins/publishings/juejin-pub/dist/index.js +6 -1
  41. package/plugins/publishings/juejin-pub/dist/profile-check.js +6 -1
  42. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  43. package/plugins/publishings/podcast-pub/dist/index.js +151 -16
  44. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  45. package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
  46. package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
  47. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  48. package/plugins/publishings/shipinhao-pub/dist/index.js +6 -1
  49. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  50. package/plugins/publishings/sspai-pub/dist/index.js +6 -1
  51. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  52. package/plugins/publishings/substack-pub/dist/index.js +6 -1
  53. package/plugins/publishings/substack-pub/dist/profile-check.js +6 -1
  54. package/plugins/publishings/surfdash-pub/dist/index.js +691 -63
  55. package/plugins/publishings/surfdash-pub/src/digest-check.ts +284 -0
  56. package/plugins/publishings/surfdash-pub/src/index.ts +656 -84
  57. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  58. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +937 -98
  60. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +64 -3
  61. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  62. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  63. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  64. package/plugins/publishings/weibo-pub/dist/index.js +6 -1
  65. package/plugins/publishings/weibo-pub/dist/profile-check.js +6 -1
  66. package/plugins/publishings/x-pub/dist/index.js +6 -1
  67. package/plugins/publishings/x-pub/dist/profile-check.js +6 -1
  68. package/plugins/publishings/xhs-pub/dist/index.js +6 -1
  69. package/plugins/publishings/xhs-pub/dist/profile-check.js +6 -1
  70. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +6 -1
  71. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
  72. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -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 = [];
@@ -11984,6 +11984,7 @@ function findProjectRoot(startDir) {
11984
11984
  var init_file_system_node = () => {};
11985
11985
 
11986
11986
  // ../../plugins/publishings/podcast-pub/src/index.ts
11987
+ import { createHash } from "crypto";
11987
11988
  import { readFileSync as readFileSync3 } from "fs";
11988
11989
  import { readFile } from "fs/promises";
11989
11990
  import { basename, dirname as dirname2, extname as extname2 } from "path";
@@ -12312,6 +12313,107 @@ async function writeJsonFile(path, value, fs = createNodeFileSystem()) {
12312
12313
  }
12313
12314
  // ../../packages/kk-core/src/render-md.ts
12314
12315
  if (false) {}
12316
+ // ../../packages/kk-core/src/sentences.ts
12317
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
12318
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
12319
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
12320
+ var OPEN_PAIRS = {
12321
+ "\u300C": "\u300D",
12322
+ "\u300E": "\u300F",
12323
+ "\u201C": "\u201D",
12324
+ "\uFF08": "\uFF09",
12325
+ "(": ")"
12326
+ };
12327
+ var CLOSE_PAIRS = {
12328
+ "\u300D": "\u300C",
12329
+ "\u300F": "\u300E",
12330
+ "\u201D": "\u201C",
12331
+ "\uFF09": "\uFF08",
12332
+ ")": "("
12333
+ };
12334
+ function splitSentences(text, options = {}) {
12335
+ if (!text || typeof text !== "string")
12336
+ return [];
12337
+ const results = [];
12338
+ let sentenceStart = 0;
12339
+ let stack = [];
12340
+ let i = 0;
12341
+ const isTerminator = (c) => CJK_TERMINATORS.has(c) || options.cjkOnly !== true && LATIN_TERMINATORS.has(c);
12342
+ while (i < text.length) {
12343
+ const ch = text[i];
12344
+ if (isTerminator(ch)) {
12345
+ let termEnd = i;
12346
+ let hasCjkTerm = false;
12347
+ let hasLatinTerm = false;
12348
+ while (termEnd < text.length) {
12349
+ const termChar = text[termEnd];
12350
+ if (!termChar || !isTerminator(termChar)) {
12351
+ break;
12352
+ }
12353
+ if (CJK_TERMINATORS.has(termChar))
12354
+ hasCjkTerm = true;
12355
+ if (LATIN_TERMINATORS.has(termChar))
12356
+ hasLatinTerm = true;
12357
+ termEnd++;
12358
+ }
12359
+ let closerEnd = termEnd;
12360
+ const tempStack = [...stack];
12361
+ while (closerEnd < text.length) {
12362
+ const closer = text[closerEnd];
12363
+ if (!closer || !ATTACHED_CLOSERS.has(closer)) {
12364
+ break;
12365
+ }
12366
+ const top = tempStack[tempStack.length - 1];
12367
+ if (top !== undefined && top === closer) {
12368
+ tempStack.pop();
12369
+ } else if (CLOSE_PAIRS[closer]) {
12370
+ const idx = tempStack.lastIndexOf(closer);
12371
+ if (idx >= 0)
12372
+ tempStack.splice(idx, 1);
12373
+ }
12374
+ closerEnd++;
12375
+ }
12376
+ let isBoundary = false;
12377
+ if (tempStack.length === 0) {
12378
+ if (hasCjkTerm) {
12379
+ isBoundary = true;
12380
+ } else if (hasLatinTerm) {
12381
+ const nextChar = text[closerEnd];
12382
+ if (closerEnd === text.length || nextChar !== undefined && /\s/.test(nextChar)) {
12383
+ isBoundary = true;
12384
+ }
12385
+ }
12386
+ }
12387
+ if (isBoundary) {
12388
+ const sentence = text.slice(sentenceStart, closerEnd).trim();
12389
+ if (sentence.length > 0) {
12390
+ results.push(sentence);
12391
+ }
12392
+ sentenceStart = closerEnd;
12393
+ stack = tempStack;
12394
+ i = closerEnd;
12395
+ continue;
12396
+ }
12397
+ }
12398
+ const openMatch = OPEN_PAIRS[ch];
12399
+ const closeMatch = CLOSE_PAIRS[ch];
12400
+ if (openMatch) {
12401
+ stack.push(openMatch);
12402
+ } else if (closeMatch) {
12403
+ const idx = stack.lastIndexOf(ch);
12404
+ if (idx >= 0)
12405
+ stack.splice(idx, 1);
12406
+ }
12407
+ i++;
12408
+ }
12409
+ if (sentenceStart < text.length) {
12410
+ const tail = text.slice(sentenceStart).trim();
12411
+ if (tail.length > 0) {
12412
+ results.push(tail);
12413
+ }
12414
+ }
12415
+ return results;
12416
+ }
12315
12417
  // ../../packages/publish-harness/src/index.ts
12316
12418
  class FakeTransportCore {
12317
12419
  callLogs = [];
@@ -12514,8 +12616,8 @@ class MicrofeedClient {
12514
12616
  }
12515
12617
 
12516
12618
  // ../../plugins/publishings/podcast-pub/src/seo.ts
12517
- init_dist();
12518
12619
  import { readFileSync as readFileSync2 } from "fs";
12620
+ init_dist();
12519
12621
  function codePointLength(value) {
12520
12622
  return Array.from(value).length;
12521
12623
  }
@@ -12869,11 +12971,7 @@ function clauseFromTitle(title) {
12869
12971
  return cut.replace(/\s+/g, " ").trim();
12870
12972
  }
12871
12973
  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();
12974
+ return splitSentences(text.replace(/\s+/g, " ").trim())[0] ?? "";
12877
12975
  }
12878
12976
  function planItemsFromMetadata(metadata) {
12879
12977
  const raw = podcastOf(metadata).planItems;
@@ -13869,7 +13967,7 @@ function itemBrief(d) {
13869
13967
  const meta = d.metadata ?? {};
13870
13968
  if (typeof meta.brief === "string" && meta.brief.trim().length > 0)
13871
13969
  return meta.brief.trim();
13872
- return (d.body ?? "").trim().split(/(?<=[\u3002\uFF01\uFF1F])\s*/).slice(0, 2).join("");
13970
+ return splitSentences((d.body ?? "").trim(), { cjkOnly: true }).slice(0, 2).join("");
13873
13971
  }
13874
13972
  function renderShowNotesMarkdown(docs, opts, warnings) {
13875
13973
  const slugify = (t) => {
@@ -13937,7 +14035,7 @@ function renderShowNotesMarkdown(docs, opts, warnings) {
13937
14035
  } else {
13938
14036
  docs.forEach((d, i) => {
13939
14037
  const body = (d.body ?? "").trim();
13940
- const sentences = body.split(/(?<=[\u3002\uFF01\uFF1F])\s*/).slice(0, 2).join("");
14038
+ const sentences = splitSentences(body, { cjkOnly: true }).slice(0, 2).join("");
13941
14039
  lines.push(`## ${i + 1}. ${d.title ?? `\u6761\u76EE ${i + 1}`}`, "", sentences, "", `[\u6765\u6E90\uFF1A${sourceLabel(d)}](${d.sourceUri ?? ""})`, "");
13942
14040
  });
13943
14041
  lines.push(`\u8BE6\u60C5\u89C1\u672C\u671F\u5B8C\u6574\u6587\u7AE0\uFF1A[ ${opts.title} ](${articleUrl})`, "");
@@ -14237,26 +14335,53 @@ function shapedEpisode(episode, fields) {
14237
14335
  link: fields.link
14238
14336
  };
14239
14337
  }
14240
- async function createEpisodeItem(client, body, slug, warnings) {
14338
+ function canonicalJson(value) {
14339
+ if (value === null || value === undefined)
14340
+ return "null";
14341
+ if (typeof value === "string" || typeof value === "boolean")
14342
+ return JSON.stringify(value);
14343
+ if (typeof value === "number")
14344
+ return Number.isFinite(value) ? JSON.stringify(value) : "null";
14345
+ if (Array.isArray(value))
14346
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
14347
+ if (typeof value === "object") {
14348
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
14349
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
14350
+ }
14351
+ return JSON.stringify(String(value));
14352
+ }
14353
+ function deterministicUuid(name) {
14354
+ const namespace = Buffer.from("6ba7b8119dad11d180b400c04fd430c8", "hex");
14355
+ const digest = createHash("sha1").update(namespace).update(name, "utf8").digest();
14356
+ digest[6] = (digest[6] ?? 0) & 15 | 80;
14357
+ digest[8] = (digest[8] ?? 0) & 63 | 128;
14358
+ const hex = digest.subarray(0, 16).toString("hex");
14359
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
14360
+ }
14361
+ function createItemIdempotencyKey(body, mediaIdentity) {
14362
+ return deterministicUuid(canonicalJson({ mediaIdentity, payload: body }));
14363
+ }
14364
+ async function createEpisodeItem(client, body, slug, warnings, mediaIdentity) {
14365
+ const create = (input) => client.createItem(input, createItemIdempotencyKey(input, mediaIdentity));
14241
14366
  const withSlug = (itemSlug) => {
14242
14367
  const micro = body._microfeed ?? {};
14243
14368
  return { ...body, _microfeed: { ...micro, slug: itemSlug } };
14244
14369
  };
14245
14370
  if (!slug)
14246
- return client.createItem(body, crypto.randomUUID());
14371
+ return create(body);
14247
14372
  try {
14248
- return await client.createItem(withSlug(slug), crypto.randomUUID());
14373
+ return await create(withSlug(slug));
14249
14374
  } catch (error) {
14250
14375
  if (!(error instanceof MicrofeedRequestError) || error.status !== 409)
14251
14376
  throw error;
14252
14377
  const retry = `${slug}-2`;
14253
14378
  try {
14254
- return await client.createItem(withSlug(retry), crypto.randomUUID());
14379
+ return await create(withSlug(retry));
14255
14380
  } catch (retryError) {
14256
14381
  if (!(retryError instanceof MicrofeedRequestError) || retryError.status !== 409)
14257
14382
  throw retryError;
14258
14383
  warn(`slug "${slug}" conflicted (HTTP 409); publishing without a custom slug`, warnings);
14259
- return client.createItem(body, crypto.randomUUID());
14384
+ return create(body);
14260
14385
  }
14261
14386
  }
14262
14387
  }
@@ -14361,6 +14486,14 @@ async function publishEpisode(client, payload, options) {
14361
14486
  }
14362
14487
  const seo = mergeSocial(fields.seo, social);
14363
14488
  episode = shapedEpisode(episode, fields);
14489
+ let localAudioBytes;
14490
+ let mediaIdentity;
14491
+ if (episode.audio.kind === "url") {
14492
+ mediaIdentity = `audio-url:${episode.audio.url}\x00${episode.audio.mimeType}`;
14493
+ } else {
14494
+ localAudioBytes = new Uint8Array(await readFile(episode.audio.path));
14495
+ mediaIdentity = `audio-file-sha256:${createHash("sha256").update(localAudioBytes).digest("hex")}\x00${episode.audio.mimeType}`;
14496
+ }
14364
14497
  if (episode.audio.kind === "url") {
14365
14498
  const itemId = await createEpisodeItem(client, {
14366
14499
  title: episode.title,
@@ -14368,7 +14501,7 @@ async function publishEpisode(client, payload, options) {
14368
14501
  status: episode.status,
14369
14502
  attachments: [audioAttachment(episode.audio, episode.audio.url, undefined, episode.durationSeconds)],
14370
14503
  ...episodeExtras(episode, seo)
14371
- }, fields.slug, fields.warnings);
14504
+ }, fields.slug, fields.warnings, mediaIdentity);
14372
14505
  let hostedItem;
14373
14506
  try {
14374
14507
  hostedItem = (await client.getItem(itemId)).items?.[0];
@@ -14388,8 +14521,10 @@ async function publishEpisode(client, payload, options) {
14388
14521
  title: episode.title,
14389
14522
  content_html: episode.contentHtml,
14390
14523
  status: "unpublished"
14391
- }, fields.slug, fields.warnings);
14392
- const bytes = new Uint8Array(await readFile(episode.audio.path));
14524
+ }, fields.slug, fields.warnings, mediaIdentity);
14525
+ const bytes = localAudioBytes;
14526
+ if (!bytes)
14527
+ return fail2("local audio identity was not prepared before item creation");
14393
14528
  const prepared = await client.prepareUpload({
14394
14529
  category: "audio",
14395
14530
  full_local_file_path: episode.audio.path,
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { basename, dirname, extname } from 'node:path';
@@ -306,29 +307,72 @@ function shapedEpisode(episode: EpisodeMapping, fields: PreparedEpisodeFields):
306
307
  };
307
308
  }
308
309
 
310
+ /**
311
+ * Canonical JSON for the create request fingerprint. The API's idempotency window is only 24h,
312
+ * so the key must be stable across a process restart while remaining different for a changed body.
313
+ * Object-key ordering is normalized because callers build the same logical payload in more than
314
+ * one branch (hosted audio and the draft half of local-audio publishing).
315
+ */
316
+ function canonicalJson(value: unknown): string {
317
+ if (value === null || value === undefined) return 'null';
318
+ if (typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);
319
+ if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : 'null';
320
+ if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
321
+ if (typeof value === 'object') {
322
+ const entries = Object.entries(value as Record<string, unknown>)
323
+ .filter(([, entry]) => entry !== undefined)
324
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
325
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
326
+ }
327
+ return JSON.stringify(String(value));
328
+ }
329
+
330
+ /** RFC 4122 UUIDv5-shaped key; microfeed validates Idempotency-Key as a UUID. */
331
+ function deterministicUuid(name: string): string {
332
+ // The URL namespace is only a stable namespace marker; the payload remains the name input.
333
+ const namespace = Buffer.from('6ba7b8119dad11d180b400c04fd430c8', 'hex');
334
+ const digest = createHash('sha1').update(namespace).update(name, 'utf8').digest();
335
+ digest[6] = ((digest[6] ?? 0) & 0x0f) | 0x50;
336
+ digest[8] = ((digest[8] ?? 0) & 0x3f) | 0x80;
337
+ const hex = digest.subarray(0, 16).toString('hex');
338
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
339
+ }
340
+
341
+ /**
342
+ * Derives the caller-supplied create key from the exact request body and its primary media.
343
+ * Local audio uses content identity rather than its temporary path, so a resumed run can replay
344
+ * the same draft even when the workspace path changes.
345
+ */
346
+ function createItemIdempotencyKey(body: MicrofeedItemInput, mediaIdentity: string): string {
347
+ return deterministicUuid(canonicalJson({ mediaIdentity, payload: body }));
348
+ }
349
+
309
350
  /** First createItem carries the slug. A 409 retries once as `{slug}-2`, then publishes with no slug. */
310
351
  async function createEpisodeItem(
311
352
  client: MicrofeedClient,
312
353
  body: MicrofeedItemInput,
313
354
  slug: string | undefined,
314
355
  warnings: string[],
356
+ mediaIdentity: string,
315
357
  ): Promise<string> {
358
+ const create = (input: MicrofeedItemInput): Promise<string> =>
359
+ client.createItem(input, createItemIdempotencyKey(input, mediaIdentity));
316
360
  const withSlug = (itemSlug: string): MicrofeedItemInput => {
317
361
  const micro = (body._microfeed as Record<string, unknown> | undefined) ?? {};
318
362
  return { ...body, _microfeed: { ...micro, slug: itemSlug } };
319
363
  };
320
- if (!slug) return client.createItem(body, crypto.randomUUID());
364
+ if (!slug) return create(body);
321
365
  try {
322
- return await client.createItem(withSlug(slug), crypto.randomUUID());
366
+ return await create(withSlug(slug));
323
367
  } catch (error: unknown) {
324
368
  if (!(error instanceof MicrofeedRequestError) || error.status !== 409) throw error;
325
369
  const retry = `${slug}-2`;
326
370
  try {
327
- return await client.createItem(withSlug(retry), crypto.randomUUID());
371
+ return await create(withSlug(retry));
328
372
  } catch (retryError: unknown) {
329
373
  if (!(retryError instanceof MicrofeedRequestError) || retryError.status !== 409) throw retryError;
330
374
  warn(`slug "${slug}" conflicted (HTTP 409); publishing without a custom slug`, warnings);
331
- return client.createItem(body, crypto.randomUUID());
375
+ return create(body);
332
376
  }
333
377
  }
334
378
  }
@@ -464,6 +508,15 @@ async function publishEpisode(
464
508
  const seo = mergeSocial(fields.seo, social);
465
509
  episode = shapedEpisode(episode, fields);
466
510
 
511
+ let localAudioBytes: Uint8Array | undefined;
512
+ let mediaIdentity: string;
513
+ if (episode.audio.kind === 'url') {
514
+ mediaIdentity = `audio-url:${episode.audio.url}\0${episode.audio.mimeType}`;
515
+ } else {
516
+ localAudioBytes = new Uint8Array(await readFile(episode.audio.path));
517
+ mediaIdentity = `audio-file-sha256:${createHash('sha256').update(localAudioBytes).digest('hex')}\0${episode.audio.mimeType}`;
518
+ }
519
+
467
520
  if (episode.audio.kind === 'url') {
468
521
  const itemId = await createEpisodeItem(
469
522
  client,
@@ -476,6 +529,7 @@ async function publishEpisode(
476
529
  },
477
530
  fields.slug,
478
531
  fields.warnings,
532
+ mediaIdentity,
479
533
  );
480
534
  // Best-effort canonical-URL enrichment: the episode is already published, so a failed
481
535
  // follow-up GET (or an unparsable response) falls back to publicItemUrl's own chain.
@@ -506,10 +560,13 @@ async function publishEpisode(
506
560
  },
507
561
  fields.slug,
508
562
  fields.warnings,
563
+ mediaIdentity,
509
564
  );
510
565
 
511
566
  // Phase 2-3: presign against the draft, then PUT the raw bytes.
512
- const bytes = new Uint8Array(await readFile(episode.audio.path));
567
+ // The bytes were read before create so their digest can make a resumed create idempotent.
568
+ const bytes = localAudioBytes;
569
+ if (!bytes) return fail('local audio identity was not prepared before item creation');
513
570
  const prepared = await client.prepareUpload({
514
571
  category: 'audio',
515
572
  full_local_file_path: episode.audio.path,
@@ -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";
@@ -20436,9 +20440,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20436
20440
  // ../../packages/publish-harness/src/bot-fs.ts
20437
20441
  import {
20438
20442
  chmodSync,
20443
+ closeSync,
20439
20444
  copyFileSync,
20440
20445
  lstatSync,
20441
20446
  mkdirSync as mkdirSync2,
20447
+ openSync,
20442
20448
  readdirSync as readdirSync2,
20443
20449
  realpathSync as realpathSync2,
20444
20450
  rmdirSync
@@ -20683,7 +20689,6 @@ class BotError extends Error {
20683
20689
  this.code = code;
20684
20690
  }
20685
20691
  }
20686
-
20687
20692
  // ../../packages/publish-harness/src/profile-dir.ts
20688
20693
  import { mkdir as mkdir2, stat } from "fs/promises";
20689
20694
  async function ensureProfileDir2(profileDir, headless) {
@@ -19847,9 +19847,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
19847
19847
  // ../../packages/publish-harness/src/bot-fs.ts
19848
19848
  import {
19849
19849
  chmodSync,
19850
+ closeSync,
19850
19851
  copyFileSync,
19851
19852
  lstatSync,
19852
19853
  mkdirSync as mkdirSync2,
19854
+ openSync,
19853
19855
  readdirSync as readdirSync2,
19854
19856
  realpathSync as realpathSync2,
19855
19857
  rmdirSync
@@ -20101,7 +20103,6 @@ class BotError extends Error {
20101
20103
  this.code = code;
20102
20104
  }
20103
20105
  }
20104
-
20105
20106
  // ../../packages/publish-harness/src/index.ts
20106
20107
  import { homedir as homedir4 } from "os";
20107
20108
  import { join as join4, resolve as resolve4 } 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";
@@ -20460,9 +20464,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20460
20464
  // ../../packages/publish-harness/src/bot-fs.ts
20461
20465
  import {
20462
20466
  chmodSync,
20467
+ closeSync,
20463
20468
  copyFileSync,
20464
20469
  lstatSync,
20465
20470
  mkdirSync as mkdirSync2,
20471
+ openSync,
20466
20472
  readdirSync as readdirSync2,
20467
20473
  realpathSync as realpathSync2,
20468
20474
  rmdirSync
@@ -20707,7 +20713,6 @@ class BotError extends Error {
20707
20713
  this.code = code;
20708
20714
  }
20709
20715
  }
20710
-
20711
20716
  // ../../packages/publish-harness/src/profile-dir.ts
20712
20717
  import { mkdir as mkdir2, stat } from "fs/promises";
20713
20718
  async function ensureProfileDir2(profileDir, headless) {
@@ -19847,9 +19847,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
19847
19847
  // ../../packages/publish-harness/src/bot-fs.ts
19848
19848
  import {
19849
19849
  chmodSync,
19850
+ closeSync,
19850
19851
  copyFileSync,
19851
19852
  lstatSync,
19852
19853
  mkdirSync as mkdirSync2,
19854
+ openSync,
19853
19855
  readdirSync as readdirSync2,
19854
19856
  realpathSync as realpathSync2,
19855
19857
  rmdirSync
@@ -20101,7 +20103,6 @@ class BotError extends Error {
20101
20103
  this.code = code;
20102
20104
  }
20103
20105
  }
20104
-
20105
20106
  // ../../packages/publish-harness/src/index.ts
20106
20107
  import { homedir as homedir4 } from "os";
20107
20108
  import { join as join4, resolve as resolve4 } 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";
@@ -20454,9 +20458,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20454
20458
  // ../../packages/publish-harness/src/bot-fs.ts
20455
20459
  import {
20456
20460
  chmodSync,
20461
+ closeSync,
20457
20462
  copyFileSync,
20458
20463
  lstatSync,
20459
20464
  mkdirSync as mkdirSync2,
20465
+ openSync,
20460
20466
  readdirSync as readdirSync2,
20461
20467
  realpathSync as realpathSync2,
20462
20468
  rmdirSync
@@ -20701,7 +20707,6 @@ class BotError extends Error {
20701
20707
  this.code = code;
20702
20708
  }
20703
20709
  }
20704
-
20705
20710
  // ../../packages/publish-harness/src/profile-dir.ts
20706
20711
  import { mkdir as mkdir2, stat } from "fs/promises";
20707
20712
  async function ensureProfileDir2(profileDir, headless) {
@@ -20119,9 +20119,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
20119
20119
  // ../../packages/publish-harness/src/bot-fs.ts
20120
20120
  import {
20121
20121
  chmodSync,
20122
+ closeSync,
20122
20123
  copyFileSync,
20123
20124
  lstatSync,
20124
20125
  mkdirSync as mkdirSync2,
20126
+ openSync,
20125
20127
  readdirSync as readdirSync2,
20126
20128
  realpathSync as realpathSync2,
20127
20129
  rmdirSync
@@ -20373,7 +20375,6 @@ class BotError extends Error {
20373
20375
  this.code = code;
20374
20376
  }
20375
20377
  }
20376
-
20377
20378
  // ../../packages/publish-harness/src/index.ts
20378
20379
  import { homedir as homedir4 } from "os";
20379
20380
  import { join as join4, resolve as resolve4 } from "path";
@@ -20972,6 +20973,10 @@ var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx)
20972
20973
  });
20973
20974
  // ../../packages/kk-core/src/render-md.ts
20974
20975
  if (false) {}
20976
+ // ../../packages/kk-core/src/sentences.ts
20977
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
20978
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
20979
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
20975
20980
  // ../../plugins/publishings/substack-pub/src/index.ts
20976
20981
  var CHANNEL = "substack";
20977
20982
  var LOGIN_URL_RE = /(\/sign-in|substack\.com\/account\/login)/;