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