@gobing-ai/knowledge-kit 0.0.35 → 0.0.37
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/README.md +1 -1
- package/dist/board/collect/index.css +42 -0
- package/dist/board/collect/index.js +787 -0
- package/dist/board/daily-voice/index.css +13 -0
- package/dist/board/daily-voice/index.js +902 -0
- package/dist/board/itc/index.css +14 -0
- package/dist/board/itc/index.js +1026 -0
- package/dist/index.js +12996 -10476
- package/package.json +10 -9
- package/plugins/generations/content-gen/dist/index.js +1015 -10814
- package/plugins/generations/content-gen/package.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +1002 -10801
- package/plugins/generations/core-facts-gen/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +1004 -10803
- package/plugins/generations/daily-article-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/dist/index.js +1111 -10888
- package/plugins/generations/dailynews-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +1124 -10923
- package/plugins/generations/episode-plan-gen/package.json +1 -1
- package/plugins/generations/gemini-tts/dist/index.js +1104 -10903
- package/plugins/generations/gemini-tts/package.json +1 -1
- package/plugins/generations/image-gen/dist/index.js +1239 -11035
- package/plugins/generations/image-gen/package.json +1 -1
- package/plugins/generations/news-report-gen/dist/index.js +1048 -10847
- package/plugins/generations/news-report-gen/package.json +1 -1
- package/plugins/generations/voice-gen/dist/index.js +1062 -10861
- package/plugins/generations/voice-gen/package.json +1 -1
- package/plugins/generations/voxcpm-tts/README.md +72 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +14006 -0
- package/plugins/generations/voxcpm-tts/package.json +16 -0
- package/plugins/generations/voxcpm-tts/plugin.json +6 -0
- package/plugins/generations/voxcpm-tts/profiles.json +10 -0
- package/plugins/generations/voxcpm-tts/pyproject.toml +9 -0
- package/plugins/generations/voxcpm-tts/python/worker.py +353 -0
- package/plugins/generations/voxcpm-tts/src/index.ts +42 -0
- package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +765 -0
- package/plugins/generations/voxcpm-tts/tsconfig.json +8 -0
- package/plugins/generations/voxcpm-tts/uv.lock +2571 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +1081 -10628
- package/plugins/ingestions/aihot-ingest/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +1013 -10812
- package/plugins/ingestions/horizon-ingest/package.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +689 -10774
- package/plugins/ingestions/karakeep-local/package.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +1011 -10810
- package/plugins/ingestions/last30days-ingest/package.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +1250 -11033
- package/plugins/ingestions/web-search/package.json +1 -1
- package/plugins/kk/README.md +29 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/schedules/cron.tmpl +9 -1
- package/plugins/kk/schedules/kk-serve.launchd.plist.tmpl +79 -0
- package/plugins/kk/schedules/launchd.plist.tmpl +11 -1
- package/plugins/kk/scripts/daily-stages.ts +428 -46
- package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +76 -146
- package/plugins/kk/skills/publish/SKILL.md +8 -4
- package/plugins/kk/workflows/feature-verification.yaml +93 -0
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +1 -0
- package/plugins/kk/workflows/kk-collect.yaml +43 -11
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +4 -2
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +87 -16
- package/plugins/kk/workflows/kk-daily-safety.yaml +15 -2
- package/plugins/kk/workflows/kk-itc.yaml +8 -1
- package/plugins/kk/workflows/kk-solo-podcast.yaml +2 -0
- package/plugins/publishings/emdash-pub/dist/index.js +987 -10814
- package/plugins/publishings/emdash-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/dist/index.js +1928 -11444
- package/plugins/publishings/infoq-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/infoq-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/plugin.json +5 -1
- package/plugins/publishings/juejin-pub/dist/index.js +1940 -11456
- package/plugins/publishings/juejin-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/juejin-pub/package.json +1 -1
- package/plugins/publishings/juejin-pub/plugin.json +5 -1
- package/plugins/publishings/medium-pub/dist/index.js +978 -10805
- package/plugins/publishings/medium-pub/package.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +1016 -10843
- package/plugins/publishings/podcast-pub/package.json +1 -1
- package/plugins/publishings/qiita-pub/dist/index.js +976 -10803
- package/plugins/publishings/qiita-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/dist/index.js +1932 -11448
- package/plugins/publishings/substack-pub/dist/profile-check.js +1698 -11200
- package/plugins/publishings/substack-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/plugin.json +4 -1
- package/plugins/publishings/substack-pub/src/profile-check.ts +36 -3
- package/plugins/publishings/surfdash-pub/dist/index.js +1151 -10834
- package/plugins/publishings/surfdash-pub/package.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +137 -15
- package/plugins/publishings/surfdash-pub/src/patch.ts +144 -0
- package/plugins/publishings/threads-pub/dist/index.js +12394 -0
- package/plugins/publishings/threads-pub/package.json +17 -0
- package/plugins/publishings/threads-pub/plugin.json +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +340 -0
- package/plugins/publishings/threads-pub/tsconfig.json +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +2508 -11695
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +1656 -11188
- package/plugins/publishings/wechatmp-pub/package.json +1 -1
- package/plugins/publishings/wechatmp-pub/plugin.json +5 -1
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +244 -71
- package/plugins/publishings/wechatmp-pub/src/flow.ts +412 -96
- package/plugins/publishings/wechatmp-pub/src/index.ts +24 -3
- package/plugins/publishings/wechatmp-pub/src/page.ts +16 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +40 -4
- package/plugins/publishings/weibo-pub/dist/index.js +21345 -0
- package/plugins/publishings/weibo-pub/dist/profile-check.js +20817 -0
- package/plugins/publishings/weibo-pub/package.json +19 -0
- package/plugins/publishings/weibo-pub/plugin.json +12 -0
- package/plugins/publishings/weibo-pub/src/index.ts +868 -0
- package/plugins/publishings/weibo-pub/src/profile-check.ts +20 -0
- package/plugins/publishings/weibo-pub/tsconfig.json +4 -0
- package/plugins/publishings/x-pub/dist/index.js +1948 -11440
- package/plugins/publishings/x-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/x-pub/package.json +1 -1
- package/plugins/publishings/x-pub/plugin.json +5 -1
- package/plugins/publishings/xhs-pub/dist/index.js +1993 -11509
- package/plugins/publishings/xhs-pub/dist/profile-check.js +1697 -11212
- package/plugins/publishings/xhs-pub/package.json +1 -1
- package/plugins/publishings/xhs-pub/plugin.json +5 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +72 -2
- package/plugins/publishings/zenn-pub/dist/index.js +988 -10815
- package/plugins/publishings/zenn-pub/package.json +1 -1
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { copyFile, cp, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
6
6
|
import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
7
7
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
8
8
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
9
|
+
import { type PatchPostOutcome, patchPostFiles } from './patch';
|
|
9
10
|
|
|
10
11
|
/** Result target name for this plugin. */
|
|
11
12
|
const TARGET = 'surfdash';
|
|
12
13
|
|
|
13
14
|
/** Management operations routed via `options.operation` (L1 R7; pattern from podcast-pub). */
|
|
14
|
-
const OPERATIONS = ['publish', 'scaffold-locale', 'sync'] as const;
|
|
15
|
+
const OPERATIONS = ['publish', 'scaffold-locale', 'sync', 'patch'] as const;
|
|
15
16
|
type Operation = (typeof OPERATIONS)[number];
|
|
16
17
|
|
|
17
18
|
/** Locales `scaffold-locale` can scaffold; LLM translation stays in the workflow's agent step. */
|
|
@@ -24,10 +25,18 @@ const COVER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
|
24
25
|
/** Target-specific options carried on the invoke payload's `options` channel. */
|
|
25
26
|
interface SurfdashPubOptions {
|
|
26
27
|
operation?: unknown;
|
|
27
|
-
/** zh `index.md` absolute path inside the surfdash tree; required for `scaffold-locale` and `
|
|
28
|
+
/** zh `index.md` absolute path inside the surfdash tree; required for `scaffold-locale`, `sync` and `patch`. */
|
|
28
29
|
postPath?: unknown;
|
|
29
30
|
/** Draft locale to scaffold; required for `scaffold-locale`. */
|
|
30
31
|
target?: unknown;
|
|
32
|
+
/** `patch`: podcast-hosted cover URL for `image`/`og_image` (empty → the local cover fallback). */
|
|
33
|
+
coverUrl?: unknown;
|
|
34
|
+
/** `patch`: podcast episode URL appended as the cross-link (empty → none). */
|
|
35
|
+
showNotesUrl?: unknown;
|
|
36
|
+
/** `patch`: this run's rendered cover, used when `coverUrl` is empty. */
|
|
37
|
+
localCover?: unknown;
|
|
38
|
+
/** `patch`: `YYYY-MM-DD` episode date for the cross-link text. */
|
|
39
|
+
dateIso?: unknown;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -81,9 +90,16 @@ export function mapContentToSurfingMarkdown(content: Content, options: MapConten
|
|
|
81
90
|
: Array.isArray(content.metadata?.keywords)
|
|
82
91
|
? content.metadata.keywords
|
|
83
92
|
: [];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
// `slugify` drops non-ASCII, so a Chinese tag (`硬件发布`) collapsed to '' and shipped as an empty
|
|
94
|
+
// YAML tag — the site renders it as a blank chip (seen on 20261011-ai-2026-10-11). Case variants
|
|
95
|
+
// also collapsed to the same slug twice (`AI` + `ai`), so the set dedupes.
|
|
96
|
+
const tags = [
|
|
97
|
+
...new Set(
|
|
98
|
+
rawTags
|
|
99
|
+
.filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
|
100
|
+
.map((t: string) => slugify(t)),
|
|
101
|
+
),
|
|
102
|
+
].filter((tag) => tag.length > 0);
|
|
87
103
|
|
|
88
104
|
const isoDate = options.publishDate ?? new Date().toISOString().slice(0, 10);
|
|
89
105
|
const firstRefUrl = content.references?.[0]?.url;
|
|
@@ -250,6 +266,10 @@ export type GitPushOutcome = { ok: true; hash: string } | { ok: false; error: st
|
|
|
250
266
|
* out of the publish commit. Fails loud on branch/commit/push errors.
|
|
251
267
|
*/
|
|
252
268
|
export async function pushPostGit(surfdashRoot: string, relPostDir: string, message: string): Promise<GitPushOutcome> {
|
|
269
|
+
return commitPostPaths(surfdashRoot, [relPostDir], message);
|
|
270
|
+
}
|
|
271
|
+
/** Refuse a checkout that cannot deploy before anything is written (`deploy.yml` builds `main` only). */
|
|
272
|
+
async function assertMainBranch(surfdashRoot: string): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
253
273
|
const branch = await spawnCli(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], surfdashRoot);
|
|
254
274
|
if (branch.exitCode !== 0) return { ok: false, error: `surfdash git branch check failed: ${cliErrorText(branch)}` };
|
|
255
275
|
const branchName = branch.stdout.trim();
|
|
@@ -259,14 +279,34 @@ export async function pushPostGit(surfdashRoot: string, relPostDir: string, mess
|
|
|
259
279
|
error: `surfdash checkout is on '${branchName}', not 'main' — deploy.yml only builds main; refusing to push from a feature branch`,
|
|
260
280
|
};
|
|
261
281
|
}
|
|
262
|
-
|
|
282
|
+
return { ok: true };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Commits several paths of the surfdash checkout and pushes them to `main` — the plural form of
|
|
287
|
+
* {@link pushPostGit} the post-patch needs (it writes the posts and any copied cover). Same
|
|
288
|
+
* pathspec-scoped commit, so unrelated staged work is never swept in, and the same idempotent retry:
|
|
289
|
+
* content identical to HEAD stages nothing and only pushes when `origin/main` lacks the paths.
|
|
290
|
+
*/
|
|
291
|
+
export async function commitPostPaths(
|
|
292
|
+
surfdashRoot: string,
|
|
293
|
+
relPaths: string[],
|
|
294
|
+
message: string,
|
|
295
|
+
push = true,
|
|
296
|
+
): Promise<GitPushOutcome> {
|
|
297
|
+
if (relPaths.length === 0) return { ok: true, hash: 'unchanged' };
|
|
298
|
+
const branch = await assertMainBranch(surfdashRoot);
|
|
299
|
+
if (!branch.ok) return branch;
|
|
300
|
+
const add = await spawnCli(['git', 'add', '--', ...relPaths], surfdashRoot);
|
|
263
301
|
if (add.exitCode !== 0) return { ok: false, error: `surfdash git add failed: ${cliErrorText(add)}` };
|
|
264
|
-
|
|
265
|
-
// origin lacks commits touching this post; a clean post means it's already live.
|
|
266
|
-
const staged = await spawnCli(['git', 'diff', '--cached', '--quiet', '--', relPostDir], surfdashRoot);
|
|
302
|
+
const staged = await spawnCli(['git', 'diff', '--cached', '--quiet', '--', ...relPaths], surfdashRoot);
|
|
267
303
|
if (staged.exitCode === 0) {
|
|
304
|
+
if (!push) {
|
|
305
|
+
const hash = await spawnCli(['git', 'rev-parse', '--short', 'HEAD'], surfdashRoot);
|
|
306
|
+
return { ok: true, hash: hash.stdout.trim() || 'unknown' };
|
|
307
|
+
}
|
|
268
308
|
const unpushed = await spawnCli(
|
|
269
|
-
['git', 'diff', '--quiet', 'origin/main..HEAD', '--',
|
|
309
|
+
['git', 'diff', '--quiet', 'origin/main..HEAD', '--', ...relPaths],
|
|
270
310
|
surfdashRoot,
|
|
271
311
|
);
|
|
272
312
|
if (unpushed.exitCode === 0) {
|
|
@@ -274,14 +314,18 @@ export async function pushPostGit(surfdashRoot: string, relPostDir: string, mess
|
|
|
274
314
|
return { ok: true, hash: hash.stdout.trim() || 'unknown' };
|
|
275
315
|
}
|
|
276
316
|
} else {
|
|
277
|
-
const commit = await spawnCli(['git', 'commit', '-m', message, '--',
|
|
317
|
+
const commit = await spawnCli(['git', 'commit', '-m', message, '--', ...relPaths], surfdashRoot);
|
|
278
318
|
if (commit.exitCode !== 0) return { ok: false, error: `surfdash git commit failed: ${cliErrorText(commit)}` };
|
|
279
319
|
}
|
|
280
|
-
|
|
281
|
-
|
|
320
|
+
if (!push) {
|
|
321
|
+
const hash = await spawnCli(['git', 'rev-parse', '--short', 'HEAD'], surfdashRoot);
|
|
322
|
+
return { ok: true, hash: hash.stdout.trim() || 'unknown' };
|
|
323
|
+
}
|
|
324
|
+
const pushed = await spawnCli(['git', 'push', 'origin', 'main'], surfdashRoot);
|
|
325
|
+
if (pushed.exitCode !== 0) {
|
|
282
326
|
return {
|
|
283
327
|
ok: false,
|
|
284
|
-
error: `surfdash git push failed (deploy.yml deploys on push to main): ${cliErrorText(
|
|
328
|
+
error: `surfdash git push failed (deploy.yml deploys on push to main): ${cliErrorText(pushed)}`,
|
|
285
329
|
};
|
|
286
330
|
}
|
|
287
331
|
const hash = await spawnCli(['git', 'rev-parse', '--short', 'HEAD'], surfdashRoot);
|
|
@@ -433,6 +477,82 @@ async function syncPost(options: SurfdashPubOptions): Promise<Result> {
|
|
|
433
477
|
}
|
|
434
478
|
}
|
|
435
479
|
|
|
480
|
+
/** `options.<key>` when it is a string, trimmed; else ''. `patch`'s four optional fields read this way. */
|
|
481
|
+
function strOption(options: SurfdashPubOptions, key: 'coverUrl' | 'showNotesUrl' | 'localCover' | 'dateIso'): string {
|
|
482
|
+
const raw = options[key];
|
|
483
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* `patch` — post-publish healing for one published episode (2026-10-12; moved here from the kk stage
|
|
488
|
+
* `daily-patch-surfdash`, which owned the publisher's work): the podcast-hosted cover URL in
|
|
489
|
+
* `image`/`og_image`, the per-locale podcast cross-link, the local cover's `assets/` fallback, the
|
|
490
|
+
* EmDash re-sync that makes CMS cards carry a servable image, then a pathspec commit + push.
|
|
491
|
+
*
|
|
492
|
+
* Ordering is load-bearing: the branch guard runs before anything is written (a checkout that cannot
|
|
493
|
+
* deploy must not be patched, and the failure leaves the tree untouched), and the re-sync runs even
|
|
494
|
+
* when no file changed, so a re-run heals rows an earlier run left stale.
|
|
495
|
+
*/
|
|
496
|
+
async function patchPost(options: SurfdashPubOptions): Promise<Result> {
|
|
497
|
+
const postPath = resolvePostPath(options);
|
|
498
|
+
if ('error' in postPath) return fail(postPath.error);
|
|
499
|
+
const surfdashRoot = resolveSurfdashRoot();
|
|
500
|
+
if (!surfdashRoot) {
|
|
501
|
+
return fail('SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout');
|
|
502
|
+
}
|
|
503
|
+
const outsideRoot = postPathOutsideRoot(postPath.postPath, surfdashRoot);
|
|
504
|
+
if (outsideRoot) return fail(outsideRoot);
|
|
505
|
+
const branch = await assertMainBranch(surfdashRoot);
|
|
506
|
+
if (!branch.ok) return fail(branch.error);
|
|
507
|
+
|
|
508
|
+
let outcome: PatchPostOutcome;
|
|
509
|
+
try {
|
|
510
|
+
outcome = await patchPostFiles({
|
|
511
|
+
postPath: postPath.postPath,
|
|
512
|
+
coverUrl: strOption(options, 'coverUrl'),
|
|
513
|
+
showNotesUrl: strOption(options, 'showNotesUrl'),
|
|
514
|
+
localCover: strOption(options, 'localCover'),
|
|
515
|
+
dateIso: strOption(options, 'dateIso'),
|
|
516
|
+
});
|
|
517
|
+
} catch (error: unknown) {
|
|
518
|
+
return fail(`patch error: ${error instanceof Error ? error.message : String(error)}`);
|
|
519
|
+
}
|
|
520
|
+
if (outcome.changed === 0) echoError(`patch: no post bytes changed for ${outcome.slug}`);
|
|
521
|
+
const synced = await syncPost({ postPath: postPath.postPath });
|
|
522
|
+
const commit = await commitPostPaths(
|
|
523
|
+
surfdashRoot,
|
|
524
|
+
outcome.written.map((path) => relative(surfdashRoot, path)),
|
|
525
|
+
`feat: cover + podcast cross-link for ${outcome.slug} (kk daily patch)`,
|
|
526
|
+
process.env.SURFDASH_AUTO_PUSH !== '0',
|
|
527
|
+
);
|
|
528
|
+
if (!commit.ok) return fail(`patch: ${commit.error}`);
|
|
529
|
+
const metadata = {
|
|
530
|
+
postPath: postPath.postPath,
|
|
531
|
+
changed: outcome.changed,
|
|
532
|
+
locales: Object.keys(outcome.images),
|
|
533
|
+
absentLocales: outcome.absent,
|
|
534
|
+
cmsResync: synced.ok ? 'ok' : 'failed',
|
|
535
|
+
gitCommit: commit.hash,
|
|
536
|
+
};
|
|
537
|
+
if (!synced.ok) {
|
|
538
|
+
// The posts are patched and committed; only the CMS rows keep the unservable cover. Reported
|
|
539
|
+
// as a failure so the completeness report says `cms-resync=failed`, with `metadata.cmsResync`
|
|
540
|
+
// letting the caller treat it as the soft channel it is.
|
|
541
|
+
return ResultSchema.parse({
|
|
542
|
+
ok: false,
|
|
543
|
+
target: TARGET,
|
|
544
|
+
error: `cms re-sync failed: ${synced.error ?? 'ok=false'}`,
|
|
545
|
+
metadata,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return ResultSchema.parse({
|
|
549
|
+
ok: true,
|
|
550
|
+
target: TARGET,
|
|
551
|
+
message: `patched ${outcome.changed} post(s) for ${outcome.slug}`,
|
|
552
|
+
metadata,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
436
556
|
/**
|
|
437
557
|
* Publish path (behavior unchanged since E3): maps Content into surfdash markdown under
|
|
438
558
|
* `content/posts/<collection>/<locale>/<slug>/index.md` and invokes the postsurfing CLI.
|
|
@@ -565,6 +685,8 @@ export class PostsurfingFileCliTransport implements PublishTransport {
|
|
|
565
685
|
return scaffoldLocale(options);
|
|
566
686
|
case 'sync':
|
|
567
687
|
return syncPost(options);
|
|
688
|
+
case 'patch':
|
|
689
|
+
return patchPost(options);
|
|
568
690
|
case 'publish':
|
|
569
691
|
return publishPost(payload);
|
|
570
692
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-publish patching for a published daily episode (2026-10-12).
|
|
3
|
+
*
|
|
4
|
+
* Moved here from the kk stage `daily-patch-surfdash`, which owned the publisher's work: the
|
|
5
|
+
* podcast-hosted cover URL in `image`/`og_image`, the per-locale podcast cross-link, the local
|
|
6
|
+
* cover's `assets/` fallback, and the EmDash re-sync that makes CMS cards carry a servable image
|
|
7
|
+
* (the pre-patch `./assets/cover.png` returns 404 from the deployed worker — prod feedback
|
|
8
|
+
* 2026-09-29 / 2026-10-02 / 2026-10-11). Living in the publisher means every path that publishes a
|
|
9
|
+
* post can heal it: the voice1 workflow, the voice2 DAG and a hand run call one operation.
|
|
10
|
+
*
|
|
11
|
+
* This module owns only the file work (deterministic, no network); the caller (`./index`) owns the
|
|
12
|
+
* branch guard, the CMS re-sync and the git commit, in that order.
|
|
13
|
+
*/
|
|
14
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
15
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
16
|
+
|
|
17
|
+
/** The locales a daily post carries; zh is the primary whose directory anchors the other two. */
|
|
18
|
+
export const PATCH_LOCALES = ['zh', 'en', 'ja'] as const;
|
|
19
|
+
export type PatchLocale = (typeof PATCH_LOCALES)[number];
|
|
20
|
+
|
|
21
|
+
/** Everything {@link patchPostFiles} needs; all paths absolute. */
|
|
22
|
+
export interface PatchPostRequest {
|
|
23
|
+
/** Absolute `<root>/content/posts/articles/zh/<slug>/index.md` — the primary post. */
|
|
24
|
+
postPath: string;
|
|
25
|
+
/** Podcast-hosted cover URL; empty → the local cover is copied into `assets/` instead. */
|
|
26
|
+
coverUrl: string;
|
|
27
|
+
/** Podcast episode URL for the cross-link line; empty → no cross-link. */
|
|
28
|
+
showNotesUrl: string;
|
|
29
|
+
/** This run's rendered cover, the fallback when `coverUrl` is empty. */
|
|
30
|
+
localCover: string;
|
|
31
|
+
/** `YYYY-MM-DD` episode date used in the cross-link text. */
|
|
32
|
+
dateIso: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What the file pass changed; the caller commits exactly {@link PatchPostOutcome.written}. */
|
|
36
|
+
export interface PatchPostOutcome {
|
|
37
|
+
/** Absolute paths written (posts plus any copied cover). */
|
|
38
|
+
written: string[];
|
|
39
|
+
/** Files whose bytes changed (not the copied cover). */
|
|
40
|
+
changed: number;
|
|
41
|
+
/** Locales with no published post on disk, in request order. */
|
|
42
|
+
absent: PatchLocale[];
|
|
43
|
+
/** Locale → the frontmatter image value the patch settled on. */
|
|
44
|
+
images: Record<string, string>;
|
|
45
|
+
/** The primary post's directory name, for the commit message. */
|
|
46
|
+
slug: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `…/articles/zh/<slug>/index.md` → the sibling locale's `index.md`, with the LAST `/zh/` segment
|
|
51
|
+
* swapped. Undefined when the path carries no `/zh/` segment, which the caller reports as absent.
|
|
52
|
+
*/
|
|
53
|
+
export function localeIndexPath(postPath: string, locale: PatchLocale): string | undefined {
|
|
54
|
+
if (locale === 'zh') return postPath;
|
|
55
|
+
const marker = `${sep}zh${sep}`;
|
|
56
|
+
const idx = postPath.lastIndexOf(marker);
|
|
57
|
+
if (idx < 0) return undefined;
|
|
58
|
+
return `${postPath.slice(0, idx)}${sep}${locale}${sep}${postPath.slice(idx + marker.length)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The podcast cross-link line per locale (task 0189 R4: the column's episode-title pattern, not the
|
|
63
|
+
* retired news-bulletin name).
|
|
64
|
+
*/
|
|
65
|
+
export function crossLinkFor(locale: PatchLocale, dateIso: string, showNotesUrl: string): string {
|
|
66
|
+
const links: Record<PatchLocale, string> = {
|
|
67
|
+
zh: `🎧 本期已同步制作为播客节目,[点击收听《每日AI乱炖 · ${dateIso}》](${showNotesUrl})。`,
|
|
68
|
+
en: `🎧 This episode is also available as a podcast: [listen to 每日AI乱炖 · ${dateIso}](${showNotesUrl}).`,
|
|
69
|
+
ja: `🎧 本エピソードはポッドキャストでも配信中:[每日AI乱炖 · ${dateIso} を聴く](${showNotesUrl})。`,
|
|
70
|
+
};
|
|
71
|
+
return links[locale];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The closing `---` of a document's frontmatter, or -1 when there is none. */
|
|
75
|
+
function frontmatterEnd(markdown: string): number {
|
|
76
|
+
return markdown.indexOf('\n---', 3);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Upsert one single-line `key: 'value'` in a document's frontmatter.
|
|
81
|
+
*
|
|
82
|
+
* The regex consumes folded-scalar continuation lines (`og_image: >-\n https://…`) so multi-line
|
|
83
|
+
* publish output is replaced whole, and the replacement is a function so `$&`/`$1` in the value are
|
|
84
|
+
* never expanded. Scoped to the frontmatter, so a body line starting with `image:` is never
|
|
85
|
+
* rewritten. A key that is absent is inserted after `publishDate`.
|
|
86
|
+
*/
|
|
87
|
+
export function upsertFrontmatterKey(markdown: string, key: string, value: string): string {
|
|
88
|
+
const end = frontmatterEnd(markdown);
|
|
89
|
+
if (end < 0) return markdown;
|
|
90
|
+
const fm = markdown.slice(0, end);
|
|
91
|
+
const body = markdown.slice(end);
|
|
92
|
+
const line = `${key}: '${value.replaceAll("'", "''")}'`;
|
|
93
|
+
const re = new RegExp(`^${key}:.*(?:\\n[ \\t]+.*)*`, 'm');
|
|
94
|
+
const next = re.test(fm) ? fm.replace(re, () => line) : fm.replace(/^publishDate: .*$/m, (m0) => `${m0}\n${line}`);
|
|
95
|
+
return next + body;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Patch the published posts of one episode: cover URL (or the local cover's `assets/` fallback),
|
|
100
|
+
* then the podcast cross-link. Idempotent per file — a post that already carries the values keeps
|
|
101
|
+
* its bytes and is not committed.
|
|
102
|
+
*/
|
|
103
|
+
export async function patchPostFiles(request: PatchPostRequest): Promise<PatchPostOutcome> {
|
|
104
|
+
const fs = createNodeFileSystem();
|
|
105
|
+
const written: string[] = [];
|
|
106
|
+
const absent: PatchLocale[] = [];
|
|
107
|
+
const images: Record<string, string> = {};
|
|
108
|
+
let changed = 0;
|
|
109
|
+
for (const locale of PATCH_LOCALES) {
|
|
110
|
+
const indexMd = localeIndexPath(request.postPath, locale);
|
|
111
|
+
if (indexMd === undefined || !(await fs.exists(indexMd))) {
|
|
112
|
+
absent.push(locale);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const dir = dirname(indexMd);
|
|
116
|
+
const before = await fs.readFile(indexMd);
|
|
117
|
+
let md = before;
|
|
118
|
+
const end = frontmatterEnd(md);
|
|
119
|
+
const fm = end < 0 ? md : md.slice(0, end);
|
|
120
|
+
if (request.coverUrl !== '') {
|
|
121
|
+
md = upsertFrontmatterKey(md, 'image', request.coverUrl);
|
|
122
|
+
md = upsertFrontmatterKey(md, 'og_image', request.coverUrl);
|
|
123
|
+
images[locale] = request.coverUrl;
|
|
124
|
+
} else if (!/^image:/m.test(fm) && (await fs.exists(request.localCover))) {
|
|
125
|
+
// Fallback for a run without a podcast cover: ship the rendered cover with the post.
|
|
126
|
+
const asset = join(dir, 'assets', 'cover.png');
|
|
127
|
+
await fs.ensureDir(dirname(asset));
|
|
128
|
+
await fs.copy(request.localCover, asset);
|
|
129
|
+
written.push(asset);
|
|
130
|
+
md = upsertFrontmatterKey(md, 'image', './assets/cover.png');
|
|
131
|
+
md = upsertFrontmatterKey(md, 'og_image', './assets/cover.png');
|
|
132
|
+
images[locale] = './assets/cover.png';
|
|
133
|
+
}
|
|
134
|
+
if (request.showNotesUrl !== '' && !md.includes(request.showNotesUrl)) {
|
|
135
|
+
md = `${md.replace(/\s*$/, '')}\n\n${crossLinkFor(locale, request.dateIso, request.showNotesUrl)}\n`;
|
|
136
|
+
}
|
|
137
|
+
if (md !== before) {
|
|
138
|
+
await fs.writeFile(indexMd, md);
|
|
139
|
+
written.push(indexMd);
|
|
140
|
+
changed += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { written, changed, absent, images, slug: basename(dirname(request.postPath)) };
|
|
144
|
+
}
|