@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +132 -25
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +4 -0
- package/plugins/generations/core-facts-gen/dist/index.js +4 -0
- package/plugins/generations/daily-article-gen/dist/index.js +4 -0
- package/plugins/generations/dailynews-gen/dist/index.js +4 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
- package/plugins/generations/gemini-tts/dist/index.js +4 -0
- package/plugins/generations/image-gen/dist/index.js +4 -0
- package/plugins/generations/news-report-gen/dist/index.js +4 -0
- package/plugins/generations/voice-gen/dist/index.js +4 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +4 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
- package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
- package/plugins/ingestions/web-search/dist/index.js +4 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
- package/plugins/kk/scripts/daily-stages.ts +1083 -49
- package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
- package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
- package/plugins/kk/scripts/sentences.ts +134 -0
- package/plugins/kk/scripts/series-names.ts +124 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +194 -113
- package/plugins/kk/workflows/kk-daily-safety.yaml +160 -36
- package/plugins/publishings/bilibili-pub/dist/index.js +6 -1
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +6 -1
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/csdn-pub/dist/index.js +6 -1
- package/plugins/publishings/csdn-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/emdash-pub/dist/index.js +4 -0
- package/plugins/publishings/infoq-pub/dist/index.js +6 -1
- package/plugins/publishings/infoq-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/jike-pub/dist/index.js +6 -1
- package/plugins/publishings/jike-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/juejin-pub/dist/index.js +6 -1
- package/plugins/publishings/juejin-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/medium-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/dist/index.js +151 -16
- package/plugins/publishings/podcast-pub/src/index.ts +62 -5
- package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
- package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
- package/plugins/publishings/qiita-pub/dist/index.js +4 -0
- package/plugins/publishings/shipinhao-pub/dist/index.js +6 -1
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/sspai-pub/dist/index.js +6 -1
- package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/substack-pub/dist/index.js +6 -1
- package/plugins/publishings/substack-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +691 -63
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +284 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +656 -84
- package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
- package/plugins/publishings/threads-pub/dist/index.js +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +937 -98
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +64 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
- package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
- package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
- package/plugins/publishings/weibo-pub/dist/index.js +6 -1
- package/plugins/publishings/weibo-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/x-pub/dist/index.js +6 -1
- package/plugins/publishings/x-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/xhs-pub/dist/index.js +6 -1
- package/plugins/publishings/xhs-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +6 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/zenn-pub/dist/index.js +4 -0
|
@@ -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";
|
|
@@ -12345,6 +12346,10 @@ init_config();
|
|
|
12345
12346
|
|
|
12346
12347
|
// ../../packages/kk-core/src/render-md.ts
|
|
12347
12348
|
if (false) {}
|
|
12349
|
+
// ../../packages/kk-core/src/sentences.ts
|
|
12350
|
+
var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
|
|
12351
|
+
var LATIN_TERMINATORS = new Set([".", "!", "?"]);
|
|
12352
|
+
var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
|
|
12348
12353
|
// ../../plugins/publishings/surfdash-pub/src/index.ts
|
|
12349
12354
|
init_dist();
|
|
12350
12355
|
|
|
@@ -12360,11 +12365,17 @@ function localeIndexPath(postPath, locale) {
|
|
|
12360
12365
|
return;
|
|
12361
12366
|
return `${postPath.slice(0, idx)}${sep}${locale}${sep}${postPath.slice(idx + marker.length)}`;
|
|
12362
12367
|
}
|
|
12368
|
+
var SERIES_NAMES = {
|
|
12369
|
+
zh: "\u6BCF\u65E5AI\u4E71\u7096",
|
|
12370
|
+
en: "Daily AI Stew",
|
|
12371
|
+
ja: "\u30C7\u30A4\u30EA\u30FCAI\u30B7\u30C1\u30E5\u30FC"
|
|
12372
|
+
};
|
|
12363
12373
|
function crossLinkFor(locale, dateIso, showNotesUrl) {
|
|
12374
|
+
const series = SERIES_NAMES[locale];
|
|
12364
12375
|
const links = {
|
|
12365
|
-
zh: `\uD83C\uDFA7 \u672C\u671F\u5DF2\u540C\u6B65\u5236\u4F5C\u4E3A\u64AD\u5BA2\u8282\u76EE\uFF0C[\u70B9\u51FB\u6536\u542C\u300A
|
|
12366
|
-
en: `\uD83C\uDFA7 This episode is also available as a podcast: [listen to
|
|
12367
|
-
ja: `\uD83C\uDFA7 \u672C\u30A8\u30D4\u30BD\u30FC\u30C9\u306F\u30DD\u30C3\u30C9\u30AD\u30E3\u30B9\u30C8\u3067\u3082\u914D\u4FE1\u4E2D\uFF1A[
|
|
12376
|
+
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`,
|
|
12377
|
+
en: `\uD83C\uDFA7 This episode is also available as a podcast: [listen to ${series} \xB7 ${dateIso}](${showNotesUrl}).`,
|
|
12378
|
+
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
12379
|
};
|
|
12369
12380
|
return links[locale];
|
|
12370
12381
|
}
|
|
@@ -12402,10 +12413,16 @@ async function patchPostFiles(request) {
|
|
|
12402
12413
|
const end = frontmatterEnd(md);
|
|
12403
12414
|
const fm = end < 0 ? md : md.slice(0, end);
|
|
12404
12415
|
if (request.coverUrl !== "") {
|
|
12416
|
+
if (!/^https:\/\//.test(request.coverUrl)) {
|
|
12417
|
+
throw new Error(`coverUrl must be an absolute https:// URL, got: ${request.coverUrl}`);
|
|
12418
|
+
}
|
|
12405
12419
|
md = upsertFrontmatterKey(md, "image", request.coverUrl);
|
|
12406
12420
|
md = upsertFrontmatterKey(md, "og_image", request.coverUrl);
|
|
12407
12421
|
images[locale] = request.coverUrl;
|
|
12408
|
-
} else if (!/^image:/m.test(fm)
|
|
12422
|
+
} else if (!/^image:/m.test(fm)) {
|
|
12423
|
+
if (!await fs.exists(request.localCover)) {
|
|
12424
|
+
throw new Error(`${locale} post has no cover: coverUrl is empty, the frontmatter has no image key, and localCover does not exist (${request.localCover})`);
|
|
12425
|
+
}
|
|
12409
12426
|
const asset = join(dir, "assets", "cover.png");
|
|
12410
12427
|
await fs.ensureDir(dirname2(asset));
|
|
12411
12428
|
await fs.copy(request.localCover, asset);
|
|
@@ -12414,11 +12431,20 @@ async function patchPostFiles(request) {
|
|
|
12414
12431
|
md = upsertFrontmatterKey(md, "og_image", "./assets/cover.png");
|
|
12415
12432
|
images[locale] = "./assets/cover.png";
|
|
12416
12433
|
}
|
|
12417
|
-
if (request.showNotesUrl !== ""
|
|
12418
|
-
|
|
12434
|
+
if (request.showNotesUrl !== "") {
|
|
12435
|
+
const link = crossLinkFor(locale, request.dateIso, request.showNotesUrl);
|
|
12436
|
+
const lines = md.split(`
|
|
12437
|
+
`);
|
|
12438
|
+
const carriesUrl = lines.some((line) => line.includes(request.showNotesUrl));
|
|
12439
|
+
if (!carriesUrl) {
|
|
12440
|
+
md = `${md.replace(/\s*$/, "")}
|
|
12419
12441
|
|
|
12420
|
-
${
|
|
12442
|
+
${link}
|
|
12421
12443
|
`;
|
|
12444
|
+
} else {
|
|
12445
|
+
md = lines.map((line) => line.includes(request.showNotesUrl) && line.trimStart().startsWith("\uD83C\uDFA7") ? link : line).join(`
|
|
12446
|
+
`);
|
|
12447
|
+
}
|
|
12422
12448
|
}
|
|
12423
12449
|
if (md !== before) {
|
|
12424
12450
|
await fs.writeFile(indexMd, md);
|
|
@@ -12433,9 +12459,228 @@ ${crossLinkFor(locale, request.dateIso, request.showNotesUrl)}
|
|
|
12433
12459
|
return { written, changed, absent, images, slug: basename(dirname2(request.postPath)) };
|
|
12434
12460
|
}
|
|
12435
12461
|
|
|
12462
|
+
// ../../plugins/publishings/surfdash-pub/src/digest-check.ts
|
|
12463
|
+
var DIGEST_TAG = "sub-category:ai-daily";
|
|
12464
|
+
var PODCAST_INTROS = ["\u8FD9\u91CC\u662F\u7531Robin", "\u54B8\u8BDD\u54B8\u8BF4", "This is Robin", "Robin \u304C\u304A\u5C4A\u3051"];
|
|
12465
|
+
var SEO_TITLE_LIMIT = 60;
|
|
12466
|
+
var KEY_LINE = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/;
|
|
12467
|
+
var BLOCK_SCALAR = /^[>|][+-]?$/;
|
|
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
|
+
}
|
|
12508
|
+
function unquote(value) {
|
|
12509
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
12510
|
+
try {
|
|
12511
|
+
return JSON.parse(value);
|
|
12512
|
+
} catch {
|
|
12513
|
+
return value.slice(1, -1);
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
12517
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
12518
|
+
}
|
|
12519
|
+
return value;
|
|
12520
|
+
}
|
|
12521
|
+
function parseFrontmatterScalars(markdown) {
|
|
12522
|
+
const out = new Map;
|
|
12523
|
+
const lines = markdown.split(/\r?\n/);
|
|
12524
|
+
if (lines[0]?.trim() !== "---")
|
|
12525
|
+
return out;
|
|
12526
|
+
const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
12527
|
+
if (end < 0)
|
|
12528
|
+
return out;
|
|
12529
|
+
for (let i = 1;i < end; i++) {
|
|
12530
|
+
const line = lines[i];
|
|
12531
|
+
if (!line)
|
|
12532
|
+
continue;
|
|
12533
|
+
const match = line.match(KEY_LINE);
|
|
12534
|
+
if (!match?.[1] || match[2] === undefined)
|
|
12535
|
+
continue;
|
|
12536
|
+
const key = match[1];
|
|
12537
|
+
const value = match[2].trim();
|
|
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;
|
|
12542
|
+
continue;
|
|
12543
|
+
}
|
|
12544
|
+
if (value === "") {
|
|
12545
|
+
const items = [];
|
|
12546
|
+
let listIndent;
|
|
12547
|
+
let j = i + 1;
|
|
12548
|
+
while (j < end) {
|
|
12549
|
+
const itemLine = lines[j];
|
|
12550
|
+
if (!itemLine || !/^\s*-\s+/.test(itemLine))
|
|
12551
|
+
break;
|
|
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));
|
|
12565
|
+
j++;
|
|
12566
|
+
}
|
|
12567
|
+
if (items.length > 0) {
|
|
12568
|
+
out.set(key, items);
|
|
12569
|
+
i = j - 1;
|
|
12570
|
+
} else {
|
|
12571
|
+
out.set(key, "");
|
|
12572
|
+
}
|
|
12573
|
+
continue;
|
|
12574
|
+
}
|
|
12575
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
12576
|
+
out.set(key, value.slice(1, -1).split(",").map((item) => unquote(item.trim())).filter((item) => item !== ""));
|
|
12577
|
+
continue;
|
|
12578
|
+
}
|
|
12579
|
+
out.set(key, unquote(value));
|
|
12580
|
+
}
|
|
12581
|
+
return out;
|
|
12582
|
+
}
|
|
12583
|
+
function scalar(frontmatter, key) {
|
|
12584
|
+
const value = frontmatter.get(key);
|
|
12585
|
+
if (value === undefined)
|
|
12586
|
+
return;
|
|
12587
|
+
return Array.isArray(value) ? value.join(" ") : value;
|
|
12588
|
+
}
|
|
12589
|
+
function endsAsSentence(text, locale) {
|
|
12590
|
+
return locale === "en" ? /[.!?]["')\u201D\u2019]*$/u.test(text.trimEnd()) : /[\u3002\uFF01\uFF1F][\u300D\u300F\u201D\uFF09]*$/u.test(text.trimEnd());
|
|
12591
|
+
}
|
|
12592
|
+
function checkDigestFrontmatter(markdown, locale) {
|
|
12593
|
+
const frontmatter = parseFrontmatterScalars(markdown);
|
|
12594
|
+
const tags = frontmatter.get("tags");
|
|
12595
|
+
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" && tags !== "" ? [tags] : [];
|
|
12596
|
+
if (!tagList.some((tag) => tag.includes(DIGEST_TAG)))
|
|
12597
|
+
return [];
|
|
12598
|
+
const problems = [];
|
|
12599
|
+
const series = SERIES_NAMES[locale];
|
|
12600
|
+
const titleSep = locale === "en" ? ": " : "\uFF1A";
|
|
12601
|
+
const listSep = locale === "en" ? ", " : "\u3001";
|
|
12602
|
+
const title = scalar(frontmatter, "title");
|
|
12603
|
+
if (title === undefined || title === "") {
|
|
12604
|
+
problems.push("title: missing");
|
|
12605
|
+
} else {
|
|
12606
|
+
const match = title.match(new RegExp(`^${escapeRe(series)} \xB7 (\\d{4}-\\d{2}-\\d{2})${escapeRe(titleSep)}(.+)$`));
|
|
12607
|
+
if (!match) {
|
|
12608
|
+
problems.push(`title: expected "<series> \xB7 YYYY-MM-DD${titleSep}<h1>${listSep}<h2>" for ${series}, got "${title}"`);
|
|
12609
|
+
} else {
|
|
12610
|
+
const rest = match[2] ?? "";
|
|
12611
|
+
const heads = locale === "en" ? rest.split(", ") : rest.split("\u3001");
|
|
12612
|
+
const seps = locale === "en" ? (rest.match(/, /g) ?? []).length : (rest.match(/\u3001/g) ?? []).length;
|
|
12613
|
+
if (seps !== 1 || heads.some((head) => head.trim() === "")) {
|
|
12614
|
+
problems.push(`title: expected exactly two non-empty headlines joined "${listSep}", got "${rest}"`);
|
|
12615
|
+
}
|
|
12616
|
+
}
|
|
12617
|
+
const seoTitle = `${title} | Surfing`;
|
|
12618
|
+
const seoLength = Array.from(seoTitle).length;
|
|
12619
|
+
if (seoLength > SEO_TITLE_LIMIT) {
|
|
12620
|
+
problems.push(`title: "${seoTitle}" is ${seoLength} code points (max ${SEO_TITLE_LIMIT})`);
|
|
12621
|
+
}
|
|
12622
|
+
if (title.includes("\u2026") || title.includes("..."))
|
|
12623
|
+
problems.push('title: contains "\u2026" or "..."');
|
|
12624
|
+
}
|
|
12625
|
+
const description = scalar(frontmatter, "description");
|
|
12626
|
+
const [min, max] = locale === "en" ? [120, 155] : [70, 100];
|
|
12627
|
+
if (description === undefined || description === "") {
|
|
12628
|
+
problems.push("description: missing");
|
|
12629
|
+
} else {
|
|
12630
|
+
const length = Array.from(description).length;
|
|
12631
|
+
if (length < min || length > max) {
|
|
12632
|
+
problems.push(`description: ${length} code points (expected ${min}\u2013${max} for ${locale})`);
|
|
12633
|
+
}
|
|
12634
|
+
if (!endsAsSentence(description, locale)) {
|
|
12635
|
+
problems.push("description: does not end with sentence punctuation");
|
|
12636
|
+
}
|
|
12637
|
+
const lower = description.toLowerCase();
|
|
12638
|
+
const intro = PODCAST_INTROS.find((candidate) => lower.startsWith(candidate.toLowerCase()));
|
|
12639
|
+
if (intro)
|
|
12640
|
+
problems.push(`description: starts with the podcast intro ("${intro}")`);
|
|
12641
|
+
if (description.includes("\u2026") || description.includes("...")) {
|
|
12642
|
+
problems.push('description: contains "\u2026" or "..."');
|
|
12643
|
+
}
|
|
12644
|
+
}
|
|
12645
|
+
const points = frontmatter.get("key_points");
|
|
12646
|
+
if (!Array.isArray(points) || points.length === 0) {
|
|
12647
|
+
problems.push("key_points: missing");
|
|
12648
|
+
} else {
|
|
12649
|
+
const labelMax = locale === "en" ? 28 : 14;
|
|
12650
|
+
points.forEach((point, index) => {
|
|
12651
|
+
const sep = point.indexOf(" \u2014 ");
|
|
12652
|
+
if (sep < 0) {
|
|
12653
|
+
problems.push(`key_points[${index}]: missing " \u2014 " separator`);
|
|
12654
|
+
return;
|
|
12655
|
+
}
|
|
12656
|
+
const label = point.slice(0, sep);
|
|
12657
|
+
const text = point.slice(sep + 3);
|
|
12658
|
+
const labelLength = Array.from(label).length;
|
|
12659
|
+
if (labelLength === 0 || labelLength > labelMax) {
|
|
12660
|
+
problems.push(`key_points[${index}]: label "${label}" is ${labelLength} code points (max ${labelMax})`);
|
|
12661
|
+
}
|
|
12662
|
+
if (!endsAsSentence(text, locale)) {
|
|
12663
|
+
problems.push(`key_points[${index}]: text does not end with sentence punctuation`);
|
|
12664
|
+
}
|
|
12665
|
+
if (point.includes("\u2026") || point.includes("...")) {
|
|
12666
|
+
problems.push(`key_points[${index}]: contains "\u2026" or "..."`);
|
|
12667
|
+
}
|
|
12668
|
+
});
|
|
12669
|
+
}
|
|
12670
|
+
return problems;
|
|
12671
|
+
}
|
|
12672
|
+
|
|
12436
12673
|
// ../../plugins/publishings/surfdash-pub/src/index.ts
|
|
12437
12674
|
var TARGET = "surfdash";
|
|
12438
|
-
var OPERATIONS = [
|
|
12675
|
+
var OPERATIONS = [
|
|
12676
|
+
"publish",
|
|
12677
|
+
"prepare",
|
|
12678
|
+
"refine-prepared",
|
|
12679
|
+
"publish-prepared",
|
|
12680
|
+
"scaffold-locale",
|
|
12681
|
+
"sync",
|
|
12682
|
+
"patch"
|
|
12683
|
+
];
|
|
12439
12684
|
var SCAFFOLD_LOCALES = ["en", "ja"];
|
|
12440
12685
|
var COVER_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
|
|
12441
12686
|
function slugify2(str) {
|
|
@@ -12449,34 +12694,29 @@ function normalizeTag(raw) {
|
|
|
12449
12694
|
}
|
|
12450
12695
|
return slugify2(raw);
|
|
12451
12696
|
}
|
|
12452
|
-
function
|
|
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
|
-
}
|
|
12697
|
+
function contentTags(content) {
|
|
12467
12698
|
const rawTags = Array.isArray(content.metadata?.tags) ? [...content.metadata.tags] : Array.isArray(content.metadata?.keywords) ? [...content.metadata.keywords] : [];
|
|
12468
12699
|
if (typeof content.metadata?.subCategory === "string" && content.metadata.subCategory.trim().length > 0) {
|
|
12469
12700
|
const subRaw = content.metadata.subCategory.trim();
|
|
12470
12701
|
rawTags.push(/^sub-category\s*:/i.test(subRaw) ? subRaw : `sub-category:${subRaw}`);
|
|
12471
12702
|
}
|
|
12472
|
-
|
|
12703
|
+
return [
|
|
12473
12704
|
...new Set(rawTags.filter((t) => typeof t === "string" && t.trim().length > 0).map((t) => normalizeTag(t)))
|
|
12474
12705
|
].filter((tag) => tag.length > 0);
|
|
12706
|
+
}
|
|
12707
|
+
function mapContentToSurfingMarkdown(content, options = {}) {
|
|
12708
|
+
let title = content.title;
|
|
12709
|
+
if (!title) {
|
|
12710
|
+
const match = content.body.match(/^#\s+(.+)$/m);
|
|
12711
|
+
title = match?.[1] ? match[1].trim() : "Untitled Post";
|
|
12712
|
+
}
|
|
12713
|
+
const description = typeof content.metadata?.description === "string" && content.metadata.description.length > 0 ? content.metadata.description : "";
|
|
12714
|
+
const tags = contentTags(content);
|
|
12475
12715
|
const isoDate = options.publishDate ?? new Date().toISOString().slice(0, 10);
|
|
12476
12716
|
const draft = typeof content.metadata?.published === "boolean" ? !content.metadata.published : false;
|
|
12477
12717
|
const frontmatterObj = {
|
|
12478
12718
|
title,
|
|
12479
|
-
description
|
|
12719
|
+
description,
|
|
12480
12720
|
tags,
|
|
12481
12721
|
publishDate: isoDate,
|
|
12482
12722
|
draft,
|
|
@@ -12489,9 +12729,21 @@ function mapContentToSurfingMarkdown(content, options = {}) {
|
|
|
12489
12729
|
${content.body.trim()}
|
|
12490
12730
|
`;
|
|
12491
12731
|
}
|
|
12732
|
+
function isDigestContent(content) {
|
|
12733
|
+
return contentTags(content).some((tag) => tag.includes(DIGEST_TAG));
|
|
12734
|
+
}
|
|
12492
12735
|
function fail(error) {
|
|
12493
12736
|
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
12494
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
|
+
}
|
|
12495
12747
|
async function writePublishResult(outputPath, result) {
|
|
12496
12748
|
const outDir = dirname3(outputPath);
|
|
12497
12749
|
if (outDir && outDir !== ".") {
|
|
@@ -12549,7 +12801,8 @@ async function spawnCli(cmd, cwd) {
|
|
|
12549
12801
|
const proc = Bun.spawn(cmd, {
|
|
12550
12802
|
cwd,
|
|
12551
12803
|
stdout: "pipe",
|
|
12552
|
-
stderr: "pipe"
|
|
12804
|
+
stderr: "pipe",
|
|
12805
|
+
env: process.env
|
|
12553
12806
|
});
|
|
12554
12807
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
12555
12808
|
proc.exited,
|
|
@@ -12594,8 +12847,82 @@ function requireRefined(filePath) {
|
|
|
12594
12847
|
return;
|
|
12595
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`;
|
|
12596
12849
|
}
|
|
12597
|
-
|
|
12598
|
-
|
|
12850
|
+
function digestCheckGate(filePath, locale) {
|
|
12851
|
+
if (process.env.SURFDASH_DIGEST_CHECK === "off")
|
|
12852
|
+
return;
|
|
12853
|
+
const problems = checkDigestFrontmatter(readFileSync2(filePath, "utf-8"), locale);
|
|
12854
|
+
if (problems.length === 0)
|
|
12855
|
+
return;
|
|
12856
|
+
if (process.env.SURFDASH_DIGEST_CHECK === "warn") {
|
|
12857
|
+
for (const problem of problems)
|
|
12858
|
+
echoError(`digest check (warn): ${problem}`);
|
|
12859
|
+
return;
|
|
12860
|
+
}
|
|
12861
|
+
return `digest frontmatter check failed: ${problems.join("; ")}`;
|
|
12862
|
+
}
|
|
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);
|
|
12599
12926
|
}
|
|
12600
12927
|
async function assertMainBranch(surfdashRoot) {
|
|
12601
12928
|
const branch = await spawnCli(["git", "rev-parse", "--abbrev-ref", "HEAD"], surfdashRoot);
|
|
@@ -12711,7 +13038,19 @@ async function scaffoldLocale(options) {
|
|
|
12711
13038
|
return fail(`options.postPath must be a zh post path containing a "${sep2}zh${sep2}" segment: ${postPath.postPath}`);
|
|
12712
13039
|
}
|
|
12713
13040
|
try {
|
|
12714
|
-
const
|
|
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);
|
|
12715
13054
|
if (spawned.exitCode !== 0) {
|
|
12716
13055
|
return fail(`sd translate failed (exit ${spawned.exitCode}): ${cliErrorText(spawned)}`);
|
|
12717
13056
|
}
|
|
@@ -12770,6 +13109,12 @@ async function syncPost(options) {
|
|
|
12770
13109
|
if (spawned.exitCode !== 0) {
|
|
12771
13110
|
return fail(`publish-orchestrator sync failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`);
|
|
12772
13111
|
}
|
|
13112
|
+
const localeInPath = postPath.postPath.match(/[\\/]content[\\/]posts[\\/]articles[\\/](zh|en|ja)[\\/]/);
|
|
13113
|
+
if (localeInPath) {
|
|
13114
|
+
const digestProblem = digestCheckGate(postPath.postPath, localeInPath[1]);
|
|
13115
|
+
if (digestProblem)
|
|
13116
|
+
return fail(digestProblem);
|
|
13117
|
+
}
|
|
12773
13118
|
return ResultSchema.parse({
|
|
12774
13119
|
ok: true,
|
|
12775
13120
|
target: TARGET,
|
|
@@ -12823,6 +13168,14 @@ async function patchPost(options) {
|
|
|
12823
13168
|
commitPaths.add(relative(surfdashRoot, assetsDir));
|
|
12824
13169
|
}
|
|
12825
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
|
+
}
|
|
12826
13179
|
const commit = await commitPostPaths(surfdashRoot, [...commitPaths], `feat: cover + podcast cross-link for ${outcome.slug} (kk daily patch)`, process.env.SURFDASH_AUTO_PUSH !== "0");
|
|
12827
13180
|
if (!commit.ok)
|
|
12828
13181
|
return fail(`patch: ${commit.error}`);
|
|
@@ -12849,85 +13202,347 @@ async function patchPost(options) {
|
|
|
12849
13202
|
metadata
|
|
12850
13203
|
});
|
|
12851
13204
|
}
|
|
12852
|
-
|
|
12853
|
-
|
|
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) };
|
|
12854
13239
|
const surfdashRoot = resolveSurfdashRoot();
|
|
12855
13240
|
if (!surfdashRoot) {
|
|
12856
|
-
return
|
|
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)}'`) };
|
|
13254
|
+
}
|
|
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
|
+
};
|
|
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
|
+
};
|
|
12857
13296
|
}
|
|
12858
|
-
const
|
|
12859
|
-
if (!
|
|
12860
|
-
return
|
|
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
|
+
};
|
|
12861
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;
|
|
12862
13341
|
const runDate = typeof payload.content.metadata?.runDate === "string" ? payload.content.metadata.runDate : "";
|
|
12863
13342
|
const today = new Date().toISOString().slice(0, 10).replaceAll("-", "");
|
|
12864
13343
|
const dateStamp = runDate || today;
|
|
12865
13344
|
const titleSlug = slugify2(payload.content.title ?? "");
|
|
12866
13345
|
const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
|
|
12867
13346
|
if (runDate !== "" && !/^\d{8}$/.test(runDate)) {
|
|
12868
|
-
return
|
|
13347
|
+
return { ok: false, result: failNotSent(`metadata.runDate must be YYYYMMDD, got '${runDate}'`) };
|
|
12869
13348
|
}
|
|
12870
13349
|
const articlesRoot = resolve(surfdashRoot, "content", "posts", "articles");
|
|
12871
13350
|
const localeRoot = resolve(articlesRoot, locale);
|
|
12872
13351
|
const postDir = resolve(localeRoot, slug);
|
|
12873
13352
|
if (dirname3(localeRoot) !== articlesRoot || dirname3(postDir) !== localeRoot) {
|
|
12874
|
-
return
|
|
13353
|
+
return { ok: false, result: failNotSent(`post path escapes ${articlesRoot}`) };
|
|
12875
13354
|
}
|
|
12876
13355
|
const coverPath = payload.content.metadata?.coverImage;
|
|
12877
|
-
if (typeof coverPath === "string" && existsSync2(coverPath)
|
|
12878
|
-
return
|
|
12879
|
-
}
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
}
|
|
12889
|
-
const markdown = mapContentToSurfingMarkdown(payload.content, {
|
|
12890
|
-
image: imageFrontmatter,
|
|
12891
|
-
publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined
|
|
12892
|
-
});
|
|
13356
|
+
if (typeof coverPath === "string" && !existsSync2(coverPath)) {
|
|
13357
|
+
return { ok: false, result: failNotSent(`metadata.coverImage not found: ${coverPath}`) };
|
|
13358
|
+
}
|
|
13359
|
+
if (typeof coverPath === "string" && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
|
|
13360
|
+
return {
|
|
13361
|
+
ok: false,
|
|
13362
|
+
result: failNotSent(`metadata.coverImage must be one of ${[...COVER_EXTS].join(", ")}`)
|
|
13363
|
+
};
|
|
13364
|
+
}
|
|
13365
|
+
if (isDigestContent(payload.content) && typeof coverPath !== "string") {
|
|
13366
|
+
return { ok: false, result: failNotSent("digest post has no cover image (metadata.coverImage)") };
|
|
13367
|
+
}
|
|
12893
13368
|
const filePath = join2(postDir, "index.md");
|
|
12894
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
|
+
});
|
|
12895
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 {
|
|
12896
13430
|
const spawned = await spawnCli([postsurfingBin, "publish", filePath], surfdashRoot);
|
|
12897
13431
|
if (spawned.exitCode !== 0) {
|
|
12898
|
-
return
|
|
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
|
+
}
|
|
12899
13441
|
}
|
|
12900
13442
|
const notRefined = requireRefined(filePath);
|
|
12901
13443
|
if (notRefined)
|
|
12902
|
-
return
|
|
13444
|
+
return failUncertain(notRefined);
|
|
13445
|
+
const digestProblem = digestCheckGate(filePath, locale);
|
|
13446
|
+
if (digestProblem)
|
|
13447
|
+
return failUncertain(digestProblem);
|
|
12903
13448
|
const refined = readRefinedSnapshot(filePath);
|
|
12904
|
-
const baseUrl = (process.env.SURFDASH_URL ?? "https://surfing.salty.vip").replace(/\/$/, "");
|
|
12905
|
-
const articleUrl = `${baseUrl}${locale === "en" ? "" : `/${locale}`}/posts/${slug}`;
|
|
12906
13449
|
if (process.env.SURFDASH_AUTO_PUSH === "0") {
|
|
12907
13450
|
return ResultSchema.parse({
|
|
12908
13451
|
ok: true,
|
|
12909
13452
|
target: TARGET,
|
|
12910
13453
|
url: articleUrl,
|
|
12911
13454
|
message: "Published via postsurfing CLI (auto-push disabled)",
|
|
12912
|
-
metadata: {
|
|
13455
|
+
metadata: {
|
|
13456
|
+
postPath: filePath,
|
|
13457
|
+
...identityMetadata,
|
|
13458
|
+
refinedTitle: refined?.title,
|
|
13459
|
+
refinedCanonical: refined?.canonical
|
|
13460
|
+
}
|
|
12913
13461
|
});
|
|
12914
13462
|
}
|
|
12915
|
-
const
|
|
12916
|
-
const
|
|
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);
|
|
12917
13470
|
if (!pushed.ok)
|
|
12918
|
-
return
|
|
13471
|
+
return failUncertain(pushed.error);
|
|
12919
13472
|
const live = await awaitArticleLive(articleUrl);
|
|
12920
13473
|
return ResultSchema.parse({
|
|
12921
13474
|
ok: true,
|
|
12922
13475
|
target: TARGET,
|
|
12923
13476
|
url: articleUrl,
|
|
12924
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",
|
|
12925
|
-
metadata: {
|
|
13478
|
+
metadata: {
|
|
13479
|
+
postPath: filePath,
|
|
13480
|
+
...identityMetadata,
|
|
13481
|
+
gitCommit: pushed.hash,
|
|
13482
|
+
live,
|
|
13483
|
+
refinedTitle: refined?.title
|
|
13484
|
+
}
|
|
12926
13485
|
});
|
|
12927
13486
|
} catch (err) {
|
|
12928
|
-
return
|
|
13487
|
+
return failUncertain(`Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`);
|
|
12929
13488
|
}
|
|
12930
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
|
+
}
|
|
12931
13546
|
|
|
12932
13547
|
class PostsurfingFileCliTransport {
|
|
12933
13548
|
kind = "file-cli";
|
|
@@ -12937,6 +13552,12 @@ class PostsurfingFileCliTransport {
|
|
|
12937
13552
|
if ("error" in routed)
|
|
12938
13553
|
return fail(routed.error);
|
|
12939
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);
|
|
12940
13561
|
case "scaffold-locale":
|
|
12941
13562
|
return scaffoldLocale(options);
|
|
12942
13563
|
case "sync":
|
|
@@ -12999,9 +13620,14 @@ if (import.meta.main) {
|
|
|
12999
13620
|
process.exit(await main2());
|
|
13000
13621
|
}
|
|
13001
13622
|
export {
|
|
13623
|
+
DIGEST_TAG,
|
|
13002
13624
|
PostsurfingFileCliTransport,
|
|
13003
13625
|
awaitArticleLive,
|
|
13626
|
+
checkDigestFrontmatter,
|
|
13004
13627
|
commitPostPaths,
|
|
13628
|
+
contentTags,
|
|
13629
|
+
digestCheckGate,
|
|
13630
|
+
extractFailingPostHint,
|
|
13005
13631
|
fastForwardToOrigin,
|
|
13006
13632
|
getPublishTransport,
|
|
13007
13633
|
main2 as main,
|
|
@@ -13012,6 +13638,8 @@ export {
|
|
|
13012
13638
|
pushPostGit,
|
|
13013
13639
|
readRefinedSnapshot,
|
|
13014
13640
|
requireRefined,
|
|
13641
|
+
runListenLinkBackfill,
|
|
13642
|
+
runSiteGate,
|
|
13015
13643
|
slugify2 as slugify,
|
|
13016
13644
|
withEmdashTokenHint
|
|
13017
13645
|
};
|