@gobing-ai/knowledge-kit 0.1.10 → 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 (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +128 -25
  3. package/package.json +1 -1
  4. package/plugins/kk/plugin.json +1 -1
  5. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  6. package/plugins/kk/scripts/daily-stages.ts +1057 -108
  7. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  8. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  9. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +168 -121
  10. package/plugins/kk/workflows/kk-daily-safety.yaml +129 -50
  11. package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
  12. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  13. package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
  14. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  15. package/plugins/publishings/csdn-pub/dist/index.js +2 -1
  16. package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
  17. package/plugins/publishings/infoq-pub/dist/index.js +2 -1
  18. package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
  19. package/plugins/publishings/jike-pub/dist/index.js +2 -1
  20. package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
  21. package/plugins/publishings/juejin-pub/dist/index.js +2 -1
  22. package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
  23. package/plugins/publishings/podcast-pub/dist/index.js +46 -8
  24. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  25. package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
  26. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  27. package/plugins/publishings/sspai-pub/dist/index.js +2 -1
  28. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  29. package/plugins/publishings/substack-pub/dist/index.js +2 -1
  30. package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/surfdash-pub/dist/index.js +460 -52
  32. package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
  33. package/plugins/publishings/surfdash-pub/src/index.ts +589 -64
  34. package/plugins/publishings/wechatmp-pub/dist/index.js +933 -98
  35. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
  36. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  37. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  38. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  39. package/plugins/publishings/weibo-pub/dist/index.js +2 -1
  40. package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
  41. package/plugins/publishings/x-pub/dist/index.js +2 -1
  42. package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
  43. package/plugins/publishings/xhs-pub/dist/index.js +2 -1
  44. package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
  45. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
  46. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
@@ -12070,6 +12070,7 @@ class FakeFileCliTransport {
12070
12070
  var init_src = () => {};
12071
12071
 
12072
12072
  // ../../plugins/publishings/surfdash-pub/src/index.ts
12073
+ import { createHash } from "crypto";
12073
12074
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
12074
12075
  import { copyFile, cp, writeFile } from "fs/promises";
12075
12076
  import { dirname as dirname3, extname, isAbsolute, join as join2, relative, resolve, sep as sep2 } from "path";
@@ -12463,7 +12464,47 @@ var DIGEST_TAG = "sub-category:ai-daily";
12463
12464
  var PODCAST_INTROS = ["\u8FD9\u91CC\u662F\u7531Robin", "\u54B8\u8BDD\u54B8\u8BF4", "This is Robin", "Robin \u304C\u304A\u5C4A\u3051"];
12464
12465
  var SEO_TITLE_LIMIT = 60;
12465
12466
  var KEY_LINE = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/;
12467
+ var BLOCK_SCALAR = /^[>|][+-]?$/;
12466
12468
  var escapeRe = (token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12469
+ function leadingIndent(line) {
12470
+ return line.match(/^[ \t]*/)?.[0].length ?? 0;
12471
+ }
12472
+ function readBlockScalar(lines, start, end, parentIndent, indicator) {
12473
+ const content = [];
12474
+ let j = start;
12475
+ while (j < end) {
12476
+ const line = lines[j] ?? "";
12477
+ if (line.trim() === "") {
12478
+ content.push("");
12479
+ j++;
12480
+ continue;
12481
+ }
12482
+ if (leadingIndent(line) <= parentIndent)
12483
+ break;
12484
+ content.push(line);
12485
+ j++;
12486
+ }
12487
+ if (content.length === 0)
12488
+ return { value: "", nextIndex: j };
12489
+ const normalized = content.map((line) => line === "" ? "" : line.slice(Math.min(parentIndent, leadingIndent(line))));
12490
+ try {
12491
+ const parsed = Bun.YAML.parse([`value: ${indicator}`, ...normalized, "sentinel: null"].join(`
12492
+ `));
12493
+ if (typeof parsed.value === "string")
12494
+ return { value: parsed.value, nextIndex: j };
12495
+ } catch {}
12496
+ const folded = indicator.startsWith(">");
12497
+ const parts = [];
12498
+ for (const line of normalized) {
12499
+ if (line !== "")
12500
+ parts.push(line.trim());
12501
+ }
12502
+ return {
12503
+ value: parts.join(folded ? " " : `
12504
+ `),
12505
+ nextIndex: j
12506
+ };
12507
+ }
12467
12508
  function unquote(value) {
12468
12509
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
12469
12510
  try {
@@ -12494,28 +12535,33 @@ function parseFrontmatterScalars(markdown) {
12494
12535
  continue;
12495
12536
  const key = match[1];
12496
12537
  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;
12538
+ if (BLOCK_SCALAR.test(value)) {
12539
+ const block = readBlockScalar(lines, i + 1, end, leadingIndent(line), value);
12540
+ out.set(key, block.value);
12541
+ i = block.nextIndex - 1;
12509
12542
  continue;
12510
12543
  }
12511
12544
  if (value === "") {
12512
12545
  const items = [];
12546
+ let listIndent;
12513
12547
  let j = i + 1;
12514
12548
  while (j < end) {
12515
12549
  const itemLine = lines[j];
12516
12550
  if (!itemLine || !/^\s*-\s+/.test(itemLine))
12517
12551
  break;
12518
- items.push(unquote(itemLine.replace(/^\s*-\s+/, "").trim()));
12552
+ const itemIndent = leadingIndent(itemLine);
12553
+ if (listIndent === undefined)
12554
+ listIndent = itemIndent;
12555
+ if (itemIndent !== listIndent)
12556
+ break;
12557
+ const raw = itemLine.replace(/^\s*-\s+/, "").trim();
12558
+ if (BLOCK_SCALAR.test(raw)) {
12559
+ const block = readBlockScalar(lines, j + 1, end, itemIndent, raw);
12560
+ items.push(block.value);
12561
+ j = block.nextIndex;
12562
+ continue;
12563
+ }
12564
+ items.push(unquote(raw));
12519
12565
  j++;
12520
12566
  }
12521
12567
  if (items.length > 0) {
@@ -12626,7 +12672,15 @@ function checkDigestFrontmatter(markdown, locale) {
12626
12672
 
12627
12673
  // ../../plugins/publishings/surfdash-pub/src/index.ts
12628
12674
  var TARGET = "surfdash";
12629
- var OPERATIONS = ["publish", "scaffold-locale", "sync", "patch"];
12675
+ var OPERATIONS = [
12676
+ "publish",
12677
+ "prepare",
12678
+ "refine-prepared",
12679
+ "publish-prepared",
12680
+ "scaffold-locale",
12681
+ "sync",
12682
+ "patch"
12683
+ ];
12630
12684
  var SCAFFOLD_LOCALES = ["en", "ja"];
12631
12685
  var COVER_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
12632
12686
  function slugify2(str) {
@@ -12681,6 +12735,15 @@ function isDigestContent(content) {
12681
12735
  function fail(error) {
12682
12736
  return ResultSchema.parse({ ok: false, target: TARGET, error });
12683
12737
  }
12738
+ function failWithDeliveryState(error, deliveryState) {
12739
+ return ResultSchema.parse({ ok: false, target: TARGET, error, metadata: { deliveryState } });
12740
+ }
12741
+ function failNotSent(error) {
12742
+ return failWithDeliveryState(error, "not-sent");
12743
+ }
12744
+ function failUncertain(error) {
12745
+ return failWithDeliveryState(error, "uncertain");
12746
+ }
12684
12747
  async function writePublishResult(outputPath, result) {
12685
12748
  const outDir = dirname3(outputPath);
12686
12749
  if (outDir && outDir !== ".") {
@@ -12785,6 +12848,8 @@ function requireRefined(filePath) {
12785
12848
  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
12849
  }
12787
12850
  function digestCheckGate(filePath, locale) {
12851
+ if (process.env.SURFDASH_DIGEST_CHECK === "off")
12852
+ return;
12788
12853
  const problems = checkDigestFrontmatter(readFileSync2(filePath, "utf-8"), locale);
12789
12854
  if (problems.length === 0)
12790
12855
  return;
@@ -12795,8 +12860,69 @@ function digestCheckGate(filePath, locale) {
12795
12860
  }
12796
12861
  return `digest frontmatter check failed: ${problems.join("; ")}`;
12797
12862
  }
12798
- async function pushPostGit(surfdashRoot, relPostDir, message) {
12799
- return commitPostPaths(surfdashRoot, [relPostDir], message);
12863
+ function parsePorcelainPaths(stdout) {
12864
+ return stdout.split(`
12865
+ `).map((line) => line.trimEnd()).filter((line) => line.length > 3).map((line) => {
12866
+ const raw = line.slice(3).trim();
12867
+ const arrowIdx = raw.indexOf(" -> ");
12868
+ const target = arrowIdx >= 0 ? raw.slice(arrowIdx + 4).trim() : raw;
12869
+ if (target.startsWith('"') && target.endsWith('"')) {
12870
+ return target.slice(1, -1);
12871
+ }
12872
+ return target;
12873
+ }).filter((p) => p.length > 0);
12874
+ }
12875
+ async function runListenLinkBackfill(surfdashRoot) {
12876
+ if (process.env.SURFDASH_LISTEN_BACKFILL === "off") {
12877
+ return [];
12878
+ }
12879
+ const before = await spawnCli(["git", "status", "--porcelain", "--", "content/posts"], surfdashRoot);
12880
+ const beforePaths = new Set(parsePorcelainPaths(before.stdout));
12881
+ const spawned = await spawnCli(["bun", "run", "sd", "seo", "listen-link", "--backfill", "--write"], surfdashRoot);
12882
+ if (spawned.exitCode !== 0) {
12883
+ echoError(`listen-link backfill: ${cliErrorText(spawned)}`);
12884
+ }
12885
+ const after = await spawnCli(["git", "status", "--porcelain", "--", "content/posts"], surfdashRoot);
12886
+ const afterPaths = parsePorcelainPaths(after.stdout);
12887
+ return [...new Set(afterPaths.filter((p) => !beforePaths.has(p)))];
12888
+ }
12889
+ var SITE_GATE_COMMANDS = [
12890
+ ["bun", "run", "sd", "check", "digest"],
12891
+ ["bun", "run", "check:content"]
12892
+ ];
12893
+ function extractFailingPostHint(text) {
12894
+ const match = text.match(/(?:content[\\/])?posts[\\/][^\s:)]+/);
12895
+ if (!match)
12896
+ return;
12897
+ return match[0].replace(/[.,;]+$/, "");
12898
+ }
12899
+ async function runSiteGate(surfdashRoot) {
12900
+ if (process.env.SURFDASH_DIGEST_CHECK === "off") {
12901
+ return { ok: true };
12902
+ }
12903
+ for (const cmd of SITE_GATE_COMMANDS) {
12904
+ const spawned = await spawnCli([...cmd], surfdashRoot);
12905
+ if (spawned.exitCode !== 0) {
12906
+ const output = cliErrorText(spawned);
12907
+ const postHint = extractFailingPostHint(output);
12908
+ const hint = postHint ? ` [failing post: ${postHint}]` : "";
12909
+ const error = `site gate failed: ${cmd.join(" ")} (exit ${spawned.exitCode})${hint}: ${output}`;
12910
+ if (process.env.SURFDASH_DIGEST_CHECK === "warn") {
12911
+ for (const line of error.split(`
12912
+ `)) {
12913
+ echoError(`site gate (warn): ${line}`);
12914
+ }
12915
+ continue;
12916
+ }
12917
+ return { ok: false, error };
12918
+ }
12919
+ }
12920
+ return { ok: true };
12921
+ }
12922
+ async function pushPostGit(surfdashRoot, relPostDir, message, extraPaths = []) {
12923
+ const rawPaths = Array.isArray(relPostDir) ? [...relPostDir, ...extraPaths] : [relPostDir, ...extraPaths];
12924
+ const paths = [...new Set(rawPaths)];
12925
+ return commitPostPaths(surfdashRoot, paths, message);
12800
12926
  }
12801
12927
  async function assertMainBranch(surfdashRoot) {
12802
12928
  const branch = await spawnCli(["git", "rev-parse", "--abbrev-ref", "HEAD"], surfdashRoot);
@@ -12912,7 +13038,19 @@ async function scaffoldLocale(options) {
12912
13038
  return fail(`options.postPath must be a zh post path containing a "${sep2}zh${sep2}" segment: ${postPath.postPath}`);
12913
13039
  }
12914
13040
  try {
12915
- const spawned = await spawnCli(["bun", "run", "sd", "translate", postPath.postPath, "--source", "zh", "--target", target.locale], surfdashRoot);
13041
+ const force = options.force === true;
13042
+ const spawned = await spawnCli([
13043
+ "bun",
13044
+ "run",
13045
+ "sd",
13046
+ "translate",
13047
+ postPath.postPath,
13048
+ "--source",
13049
+ "zh",
13050
+ "--target",
13051
+ target.locale,
13052
+ ...force ? ["--force"] : []
13053
+ ], surfdashRoot);
12916
13054
  if (spawned.exitCode !== 0) {
12917
13055
  return fail(`sd translate failed (exit ${spawned.exitCode}): ${cliErrorText(spawned)}`);
12918
13056
  }
@@ -13030,6 +13168,14 @@ async function patchPost(options) {
13030
13168
  commitPaths.add(relative(surfdashRoot, assetsDir));
13031
13169
  }
13032
13170
  }
13171
+ if (process.env.SURFDASH_AUTO_PUSH !== "0") {
13172
+ const backfilled = await runListenLinkBackfill(surfdashRoot);
13173
+ for (const p of backfilled)
13174
+ commitPaths.add(p);
13175
+ const gate = await runSiteGate(surfdashRoot);
13176
+ if (!gate.ok)
13177
+ return fail(gate.error);
13178
+ }
13033
13179
  const commit = await commitPostPaths(surfdashRoot, [...commitPaths], `feat: cover + podcast cross-link for ${outcome.slug} (kk daily patch)`, process.env.SURFDASH_AUTO_PUSH !== "0");
13034
13180
  if (!commit.ok)
13035
13181
  return fail(`patch: ${commit.error}`);
@@ -13056,94 +13202,347 @@ async function patchPost(options) {
13056
13202
  metadata
13057
13203
  });
13058
13204
  }
13059
- async function publishPost(payload) {
13060
- const postsurfingBin = process.env.POSTSURFING_BIN ?? "postsurfing";
13205
+ function fileSha256(filePath) {
13206
+ return createHash("sha256").update(readFileSync2(filePath)).digest("hex");
13207
+ }
13208
+ function preparedLocaleIdentity(postPath, runDate) {
13209
+ const enPath = deriveLocaleDraftPath(postPath, "en");
13210
+ const jaPath = deriveLocaleDraftPath(postPath, "ja");
13211
+ if (enPath === undefined || jaPath === undefined) {
13212
+ return { ok: false, error: `prepared zh post path must contain a "/zh/" locale segment: ${postPath}` };
13213
+ }
13214
+ const localePaths = { zh: postPath, en: enPath, ja: jaPath };
13215
+ const localeDigests = {};
13216
+ for (const locale of ["zh", "en", "ja"]) {
13217
+ const path = localePaths[locale];
13218
+ if (!existsSync2(path))
13219
+ return { ok: false, error: `${locale} locale file is missing: ${path}` };
13220
+ try {
13221
+ localeDigests[locale] = fileSha256(path);
13222
+ } catch (error) {
13223
+ return {
13224
+ ok: false,
13225
+ error: `${locale} locale file cannot be fingerprinted: ${error instanceof Error ? error.message : String(error)}`
13226
+ };
13227
+ }
13228
+ }
13229
+ const identity = { runDate, postPath, localePaths, localeDigests };
13230
+ return {
13231
+ ok: true,
13232
+ value: { ...identity, fingerprint: createHash("sha256").update(JSON.stringify(identity)).digest("hex") }
13233
+ };
13234
+ }
13235
+ function resolvePreparedLocaleSet(options) {
13236
+ const postPath = resolvePostPath(options);
13237
+ if ("error" in postPath)
13238
+ return { ok: false, result: failNotSent(postPath.error) };
13061
13239
  const surfdashRoot = resolveSurfdashRoot();
13062
13240
  if (!surfdashRoot) {
13063
- return fail("SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout");
13241
+ return {
13242
+ ok: false,
13243
+ result: failNotSent("SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout")
13244
+ };
13245
+ }
13246
+ const outsideRoot = postPathOutsideRoot(postPath.postPath, surfdashRoot);
13247
+ if (outsideRoot)
13248
+ return { ok: false, result: failNotSent(outsideRoot) };
13249
+ if (!existsSync2(postPath.postPath))
13250
+ return { ok: false, result: failNotSent(`prepared post does not exist: ${postPath.postPath}`) };
13251
+ const runDate = options.runDate;
13252
+ if (typeof runDate !== "string" || !/^\d{8}$/.test(runDate)) {
13253
+ return { ok: false, result: failNotSent(`options.runDate must be YYYYMMDD, got '${String(runDate)}'`) };
13064
13254
  }
13065
- const locale = process.env.SURFDASH_LOCALE ?? "zh";
13066
- if (!["zh", "en", "ja"].includes(locale)) {
13067
- return fail(`SURFDASH_LOCALE must be one of zh/en/ja, got '${locale}'`);
13255
+ const articlesRoot = resolve(surfdashRoot, "content", "posts", "articles");
13256
+ const relativePost = relative(articlesRoot, postPath.postPath);
13257
+ const parts = relativePost.split(sep2);
13258
+ const slug = parts[1];
13259
+ if (parts.length !== 3 || parts[0] !== "zh" || parts[2] !== "index.md" || slug === undefined || slug === "") {
13260
+ return {
13261
+ ok: false,
13262
+ result: failNotSent(`options.postPath must be content/posts/articles/zh/<slug>/index.md: ${postPath.postPath}`)
13263
+ };
13264
+ }
13265
+ const enPath = deriveLocaleDraftPath(postPath.postPath, "en");
13266
+ const jaPath = deriveLocaleDraftPath(postPath.postPath, "ja");
13267
+ if (enPath === undefined || jaPath === undefined) {
13268
+ return {
13269
+ ok: false,
13270
+ result: failNotSent(`prepared zh post path must contain a "/zh/" locale segment: ${postPath.postPath}`)
13271
+ };
13068
13272
  }
13273
+ const localePaths = { zh: postPath.postPath, en: enPath, ja: jaPath };
13274
+ for (const locale of ["zh", "en", "ja"]) {
13275
+ if (!existsSync2(localePaths[locale])) {
13276
+ return { ok: false, result: failNotSent(`${locale} locale file is missing: ${localePaths[locale]}`) };
13277
+ }
13278
+ }
13279
+ return { ok: true, value: { surfdashRoot, runDate, postPath: postPath.postPath, slug, localePaths } };
13280
+ }
13281
+ function resolvePreparedPost(options) {
13282
+ const resolved = resolvePreparedLocaleSet(options);
13283
+ if (!resolved.ok)
13284
+ return resolved;
13285
+ const { surfdashRoot, runDate, postPath: preparedPath, slug } = resolved.value;
13286
+ const expectedPostSha256 = options.expectedPostSha256;
13287
+ if (typeof expectedPostSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedPostSha256)) {
13288
+ return { ok: false, result: failNotSent("options.expectedPostSha256 must be a lowercase SHA-256 digest") };
13289
+ }
13290
+ const expectedLocaleFingerprint = options.expectedLocaleFingerprint;
13291
+ if (typeof expectedLocaleFingerprint !== "string" || !/^[0-9a-f]{64}$/.test(expectedLocaleFingerprint)) {
13292
+ return {
13293
+ ok: false,
13294
+ result: failNotSent("options.expectedLocaleFingerprint must be a lowercase SHA-256 digest")
13295
+ };
13296
+ }
13297
+ const identity = preparedLocaleIdentity(preparedPath, runDate);
13298
+ if (!identity.ok)
13299
+ return { ok: false, result: failNotSent(identity.error) };
13300
+ if (identity.value.localeDigests.zh !== expectedPostSha256) {
13301
+ return {
13302
+ ok: false,
13303
+ result: failNotSent(`prepared zh post bytes changed after validation: ${preparedPath}`)
13304
+ };
13305
+ }
13306
+ if (identity.value.fingerprint !== expectedLocaleFingerprint) {
13307
+ return {
13308
+ ok: false,
13309
+ result: failNotSent("prepared locale files changed after validation; refresh the locale gate before publishing")
13310
+ };
13311
+ }
13312
+ const baseUrl = (process.env.SURFDASH_URL ?? "https://surfing.salty.vip").replace(/\/$/, "");
13313
+ return {
13314
+ ok: true,
13315
+ prepared: {
13316
+ surfdashRoot,
13317
+ locale: "zh",
13318
+ dateStamp: runDate,
13319
+ slug,
13320
+ filePath: preparedPath,
13321
+ relPostDir: join2("content", "posts", "articles", "zh", slug),
13322
+ relPostDirs: ["zh", "en", "ja"].map((locale) => join2("content", "posts", "articles", locale, slug)),
13323
+ articleUrl: `${baseUrl}/zh/posts/${slug}`
13324
+ },
13325
+ identity: identity.value
13326
+ };
13327
+ }
13328
+ async function preparePost(payload) {
13329
+ const surfdashRoot = resolveSurfdashRoot();
13330
+ if (!surfdashRoot) {
13331
+ return {
13332
+ ok: false,
13333
+ result: failNotSent("SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout")
13334
+ };
13335
+ }
13336
+ const rawLocale = process.env.SURFDASH_LOCALE ?? "zh";
13337
+ if (!["zh", "en", "ja"].includes(rawLocale)) {
13338
+ return { ok: false, result: failNotSent(`SURFDASH_LOCALE must be one of zh/en/ja, got '${rawLocale}'`) };
13339
+ }
13340
+ const locale = rawLocale;
13069
13341
  const runDate = typeof payload.content.metadata?.runDate === "string" ? payload.content.metadata.runDate : "";
13070
13342
  const today = new Date().toISOString().slice(0, 10).replaceAll("-", "");
13071
13343
  const dateStamp = runDate || today;
13072
13344
  const titleSlug = slugify2(payload.content.title ?? "");
13073
13345
  const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
13074
13346
  if (runDate !== "" && !/^\d{8}$/.test(runDate)) {
13075
- return fail(`metadata.runDate must be YYYYMMDD, got '${runDate}'`);
13347
+ return { ok: false, result: failNotSent(`metadata.runDate must be YYYYMMDD, got '${runDate}'`) };
13076
13348
  }
13077
13349
  const articlesRoot = resolve(surfdashRoot, "content", "posts", "articles");
13078
13350
  const localeRoot = resolve(articlesRoot, locale);
13079
13351
  const postDir = resolve(localeRoot, slug);
13080
13352
  if (dirname3(localeRoot) !== articlesRoot || dirname3(postDir) !== localeRoot) {
13081
- return fail(`post path escapes ${articlesRoot}`);
13353
+ return { ok: false, result: failNotSent(`post path escapes ${articlesRoot}`) };
13082
13354
  }
13083
13355
  const coverPath = payload.content.metadata?.coverImage;
13084
13356
  if (typeof coverPath === "string" && !existsSync2(coverPath)) {
13085
- return fail(`metadata.coverImage not found: ${coverPath}`);
13357
+ return { ok: false, result: failNotSent(`metadata.coverImage not found: ${coverPath}`) };
13086
13358
  }
13087
13359
  if (typeof coverPath === "string" && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
13088
- return fail(`metadata.coverImage must be one of ${[...COVER_EXTS].join(", ")}`);
13360
+ return {
13361
+ ok: false,
13362
+ result: failNotSent(`metadata.coverImage must be one of ${[...COVER_EXTS].join(", ")}`)
13363
+ };
13089
13364
  }
13090
13365
  if (isDigestContent(payload.content) && typeof coverPath !== "string") {
13091
- return fail("digest post has no cover image (metadata.coverImage)");
13366
+ return { ok: false, result: failNotSent("digest post has no cover image (metadata.coverImage)") };
13092
13367
  }
13093
- await createNodeFileSystem().ensureDir(postDir);
13094
- let imageFrontmatter;
13095
- if (typeof coverPath === "string") {
13096
- const assetsDir = join2(postDir, "assets");
13097
- await createNodeFileSystem().ensureDir(assetsDir);
13098
- const coverName = `cover${extname(coverPath).toLowerCase()}`;
13099
- await copyFile(coverPath, join2(assetsDir, coverName));
13100
- imageFrontmatter = `./assets/${coverName}`;
13101
- }
13102
- const markdown = mapContentToSurfingMarkdown(payload.content, {
13103
- image: imageFrontmatter,
13104
- publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined
13105
- });
13106
13368
  const filePath = join2(postDir, "index.md");
13107
13369
  try {
13370
+ await createNodeFileSystem().ensureDir(postDir);
13371
+ let imageFrontmatter;
13372
+ if (typeof coverPath === "string") {
13373
+ const assetsDir = join2(postDir, "assets");
13374
+ await createNodeFileSystem().ensureDir(assetsDir);
13375
+ const coverName = `cover${extname(coverPath).toLowerCase()}`;
13376
+ await copyFile(coverPath, join2(assetsDir, coverName));
13377
+ imageFrontmatter = `./assets/${coverName}`;
13378
+ }
13379
+ const markdown = mapContentToSurfingMarkdown(payload.content, {
13380
+ image: imageFrontmatter,
13381
+ publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined
13382
+ });
13108
13383
  await createNodeFileSystem().writeFile(filePath, markdown);
13384
+ } catch (err) {
13385
+ return {
13386
+ ok: false,
13387
+ result: failNotSent(`Surfdash post preparation failed: ${err instanceof Error ? err.message : String(err)}`)
13388
+ };
13389
+ }
13390
+ const baseUrl = (process.env.SURFDASH_URL ?? "https://surfing.salty.vip").replace(/\/$/, "");
13391
+ const articleUrl = `${baseUrl}${locale === "en" ? "" : `/${locale}`}/posts/${slug}`;
13392
+ return {
13393
+ ok: true,
13394
+ prepared: {
13395
+ surfdashRoot,
13396
+ locale,
13397
+ dateStamp,
13398
+ slug,
13399
+ filePath,
13400
+ relPostDir: join2("content", "posts", "articles", locale, slug),
13401
+ articleUrl
13402
+ }
13403
+ };
13404
+ }
13405
+ async function preparePostResult(payload) {
13406
+ const outcome = await preparePost(payload);
13407
+ if (!outcome.ok)
13408
+ return outcome.result;
13409
+ return ResultSchema.parse({
13410
+ ok: true,
13411
+ target: TARGET,
13412
+ message: `Prepared ${outcome.prepared.locale} post at ${outcome.prepared.filePath}`,
13413
+ metadata: {
13414
+ postPath: outcome.prepared.filePath,
13415
+ articleUrl: outcome.prepared.articleUrl,
13416
+ dateStamp: outcome.prepared.dateStamp,
13417
+ slug: outcome.prepared.slug
13418
+ }
13419
+ });
13420
+ }
13421
+ async function publishPreparedFile(prepared, identity) {
13422
+ const postsurfingBin = process.env.POSTSURFING_BIN ?? "postsurfing";
13423
+ const { surfdashRoot, locale, dateStamp, filePath, relPostDir, relPostDirs, articleUrl } = prepared;
13424
+ const identityMetadata = identity ? {
13425
+ runDate: identity.runDate,
13426
+ localeFingerprint: identity.fingerprint,
13427
+ ...identity.postSha256 ? { postSha256: identity.postSha256 } : {}
13428
+ } : {};
13429
+ try {
13109
13430
  const spawned = await spawnCli([postsurfingBin, "publish", filePath], surfdashRoot);
13110
13431
  if (spawned.exitCode !== 0) {
13111
- return fail(`postsurfing CLI failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`);
13432
+ return failUncertain(`postsurfing CLI failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`);
13433
+ }
13434
+ if (identity !== undefined) {
13435
+ const afterRemote = preparedLocaleIdentity(filePath, identity.runDate);
13436
+ if (!afterRemote.ok)
13437
+ return failUncertain(`prepared locale files changed after remote publish: ${afterRemote.error}`);
13438
+ if (afterRemote.value.fingerprint !== identity.fingerprint || identity.postSha256 !== undefined && afterRemote.value.localeDigests.zh !== identity.postSha256) {
13439
+ return failUncertain("prepared locale files changed after remote publish; reconcile before retrying");
13440
+ }
13112
13441
  }
13113
13442
  const notRefined = requireRefined(filePath);
13114
13443
  if (notRefined)
13115
- return fail(notRefined);
13444
+ return failUncertain(notRefined);
13116
13445
  const digestProblem = digestCheckGate(filePath, locale);
13117
13446
  if (digestProblem)
13118
- return fail(digestProblem);
13447
+ return failUncertain(digestProblem);
13119
13448
  const refined = readRefinedSnapshot(filePath);
13120
- const baseUrl = (process.env.SURFDASH_URL ?? "https://surfing.salty.vip").replace(/\/$/, "");
13121
- const articleUrl = `${baseUrl}${locale === "en" ? "" : `/${locale}`}/posts/${slug}`;
13122
13449
  if (process.env.SURFDASH_AUTO_PUSH === "0") {
13123
13450
  return ResultSchema.parse({
13124
13451
  ok: true,
13125
13452
  target: TARGET,
13126
13453
  url: articleUrl,
13127
13454
  message: "Published via postsurfing CLI (auto-push disabled)",
13128
- metadata: { postPath: filePath, refinedTitle: refined?.title, refinedCanonical: refined?.canonical }
13455
+ metadata: {
13456
+ postPath: filePath,
13457
+ ...identityMetadata,
13458
+ refinedTitle: refined?.title,
13459
+ refinedCanonical: refined?.canonical
13460
+ }
13129
13461
  });
13130
13462
  }
13131
- const relPostDir = join2("content", "posts", "articles", locale, slug);
13132
- const pushed = await pushPostGit(surfdashRoot, relPostDir, `content: daily AI news ${dateStamp} (zh)`);
13463
+ const backfilled = await runListenLinkBackfill(surfdashRoot);
13464
+ const gate = await runSiteGate(surfdashRoot);
13465
+ if (!gate.ok)
13466
+ return failUncertain(gate.error);
13467
+ const commitPaths = relPostDirs ?? [relPostDir];
13468
+ const commitLocales = relPostDirs === undefined ? [locale] : ["zh", "en", "ja"];
13469
+ const pushed = await pushPostGit(surfdashRoot, commitPaths, `content: daily AI news ${dateStamp} (${commitLocales.join(", ")})`, backfilled);
13133
13470
  if (!pushed.ok)
13134
- return fail(pushed.error);
13471
+ return failUncertain(pushed.error);
13135
13472
  const live = await awaitArticleLive(articleUrl);
13136
13473
  return ResultSchema.parse({
13137
13474
  ok: true,
13138
13475
  target: TARGET,
13139
13476
  url: articleUrl,
13140
13477
  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",
13141
- metadata: { postPath: filePath, gitCommit: pushed.hash, live, refinedTitle: refined?.title }
13478
+ metadata: {
13479
+ postPath: filePath,
13480
+ ...identityMetadata,
13481
+ gitCommit: pushed.hash,
13482
+ live,
13483
+ refinedTitle: refined?.title
13484
+ }
13142
13485
  });
13143
13486
  } catch (err) {
13144
- return fail(`Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`);
13487
+ return failUncertain(`Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`);
13145
13488
  }
13146
13489
  }
13490
+ async function publishPost(payload) {
13491
+ const outcome = await preparePost(payload);
13492
+ if (!outcome.ok)
13493
+ return outcome.result;
13494
+ return publishPreparedFile(outcome.prepared);
13495
+ }
13496
+ async function refinePreparedPost(options) {
13497
+ const resolved = resolvePreparedLocaleSet(options);
13498
+ if (!resolved.ok)
13499
+ return resolved.result;
13500
+ const orchestrator = join2(resolved.value.surfdashRoot, "scripts", "publish-orchestrator.ts");
13501
+ if (!existsSync2(orchestrator))
13502
+ return failNotSent(`Surfdash refine orchestrator is missing: ${orchestrator}`);
13503
+ try {
13504
+ for (const locale of ["zh", "en", "ja"]) {
13505
+ const filePath = resolved.value.localePaths[locale];
13506
+ const spawned = await spawnCli([process.execPath, orchestrator, "refine", filePath], resolved.value.surfdashRoot);
13507
+ if (spawned.exitCode !== 0) {
13508
+ return failNotSent(`Surfdash refine failed for ${locale} (exit ${spawned.exitCode}): ${cliErrorText(spawned)}`);
13509
+ }
13510
+ const notRefined = requireRefined(filePath);
13511
+ if (notRefined)
13512
+ return failNotSent(notRefined);
13513
+ const digestProblem = digestCheckGate(filePath, locale);
13514
+ if (digestProblem)
13515
+ return failNotSent(digestProblem);
13516
+ }
13517
+ } catch (error) {
13518
+ return failNotSent(`Surfdash refine transport error: ${error instanceof Error ? error.message : String(error)}`);
13519
+ }
13520
+ const identity = preparedLocaleIdentity(resolved.value.postPath, resolved.value.runDate);
13521
+ if (!identity.ok)
13522
+ return failNotSent(identity.error);
13523
+ return ResultSchema.parse({
13524
+ ok: true,
13525
+ target: TARGET,
13526
+ message: "Refined zh/en/ja via the Surfdash-owned orchestrator",
13527
+ metadata: {
13528
+ postPath: resolved.value.postPath,
13529
+ runDate: resolved.value.runDate,
13530
+ localePaths: identity.value.localePaths,
13531
+ localeDigests: identity.value.localeDigests,
13532
+ localeFingerprint: identity.value.fingerprint
13533
+ }
13534
+ });
13535
+ }
13536
+ async function publishPreparedPost(options) {
13537
+ const outcome = resolvePreparedPost(options);
13538
+ if (!outcome.ok)
13539
+ return outcome.result;
13540
+ return publishPreparedFile(outcome.prepared, {
13541
+ runDate: outcome.identity.runDate,
13542
+ fingerprint: outcome.identity.fingerprint,
13543
+ postSha256: outcome.identity.localeDigests.zh
13544
+ });
13545
+ }
13147
13546
 
13148
13547
  class PostsurfingFileCliTransport {
13149
13548
  kind = "file-cli";
@@ -13153,6 +13552,12 @@ class PostsurfingFileCliTransport {
13153
13552
  if ("error" in routed)
13154
13553
  return fail(routed.error);
13155
13554
  switch (routed.operation) {
13555
+ case "prepare":
13556
+ return preparePostResult(payload);
13557
+ case "refine-prepared":
13558
+ return refinePreparedPost(options);
13559
+ case "publish-prepared":
13560
+ return publishPreparedPost(options);
13156
13561
  case "scaffold-locale":
13157
13562
  return scaffoldLocale(options);
13158
13563
  case "sync":
@@ -13222,6 +13627,7 @@ export {
13222
13627
  commitPostPaths,
13223
13628
  contentTags,
13224
13629
  digestCheckGate,
13630
+ extractFailingPostHint,
13225
13631
  fastForwardToOrigin,
13226
13632
  getPublishTransport,
13227
13633
  main2 as main,
@@ -13232,6 +13638,8 @@ export {
13232
13638
  pushPostGit,
13233
13639
  readRefinedSnapshot,
13234
13640
  requireRefined,
13641
+ runListenLinkBackfill,
13642
+ runSiteGate,
13235
13643
  slugify2 as slugify,
13236
13644
  withEmdashTokenHint
13237
13645
  };