@gobing-ai/knowledge-kit 0.0.17 → 0.0.18
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 +74 -0
- package/dist/index.js +46 -100
- package/package.json +19 -1
- package/plugins/generations/content-gen/dist/index.js +42 -94
- package/plugins/generations/core-facts-gen/dist/index.js +51 -100
- package/plugins/generations/daily-article-gen/dist/index.js +57 -99
- package/plugins/generations/daily-article-gen/src/index.ts +12 -4
- package/plugins/generations/dailynews-gen/dist/index.js +57 -102
- package/plugins/generations/dailynews-gen/src/script-builder.ts +9 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +120 -113
- package/plugins/generations/episode-plan-gen/src/index.ts +102 -22
- package/plugins/generations/image-gen/dist/index.js +63 -115
- package/plugins/generations/news-report-gen/dist/index.js +117 -99
- package/plugins/generations/news-report-gen/src/index.ts +117 -1
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +1 -1
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +8 -0
- package/plugins/generations/voice-gen/dist/index.js +101 -117
- package/plugins/generations/voice-gen/src/concat.ts +34 -0
- package/plugins/generations/voice-gen/src/index.ts +6 -4
- package/plugins/generations/voice-gen/src/voicescript.ts +8 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +47 -97
- package/plugins/ingestions/horizon-ingest/dist/index.js +47 -96
- package/plugins/ingestions/karakeep-local/dist/index.js +217 -266
- package/plugins/ingestions/last30days-ingest/dist/index.js +42 -91
- package/plugins/ingestions/web-search/dist/index.js +68 -117
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +320 -62
- package/plugins/kk/skills/publish/SKILL.md +80 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +107 -107
- package/plugins/kk/workflows/leftover-publish-fanout-example.yaml +53 -0
- package/plugins/publishings/emdash-pub/dist/index.js +42 -91
- package/plugins/publishings/infoq-pub/dist/index.js +21994 -0
- package/plugins/publishings/infoq-pub/package.json +17 -0
- package/plugins/publishings/infoq-pub/plugin.json +7 -0
- package/plugins/publishings/infoq-pub/src/index.ts +123 -0
- package/plugins/publishings/infoq-pub/tsconfig.json +4 -0
- package/plugins/publishings/juejin-pub/dist/index.js +21994 -0
- package/plugins/publishings/juejin-pub/package.json +17 -0
- package/plugins/publishings/juejin-pub/plugin.json +7 -0
- package/plugins/publishings/juejin-pub/src/index.ts +123 -0
- package/plugins/publishings/juejin-pub/tsconfig.json +4 -0
- package/plugins/publishings/medium-pub/dist/index.js +22041 -0
- package/plugins/publishings/medium-pub/package.json +17 -0
- package/plugins/publishings/medium-pub/plugin.json +7 -0
- package/plugins/publishings/medium-pub/src/index.ts +215 -0
- package/plugins/publishings/medium-pub/tsconfig.json +4 -0
- package/plugins/publishings/podcast-pub/dist/index.js +183 -123
- package/plugins/publishings/podcast-pub/src/index.ts +47 -3
- package/plugins/publishings/podcast-pub/src/show-notes.ts +153 -34
- package/plugins/publishings/qiita-pub/dist/index.js +42 -91
- package/plugins/publishings/substack-pub/dist/index.js +21994 -0
- package/plugins/publishings/substack-pub/package.json +17 -0
- package/plugins/publishings/substack-pub/plugin.json +7 -0
- package/plugins/publishings/substack-pub/src/index.ts +123 -0
- package/plugins/publishings/substack-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +172 -103
- package/plugins/publishings/surfdash-pub/src/index.ts +32 -2
- package/plugins/publishings/wechatmp-pub/dist/index.js +22119 -0
- package/plugins/publishings/wechatmp-pub/package.json +17 -0
- package/plugins/publishings/wechatmp-pub/plugin.json +7 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +304 -0
- package/plugins/publishings/wechatmp-pub/tsconfig.json +4 -0
- package/plugins/publishings/x-pub/dist/index.js +22110 -0
- package/plugins/publishings/x-pub/package.json +17 -0
- package/plugins/publishings/x-pub/plugin.json +7 -0
- package/plugins/publishings/x-pub/src/index.ts +222 -0
- package/plugins/publishings/x-pub/tsconfig.json +4 -0
- package/plugins/publishings/xhs-pub/dist/index.js +21994 -0
- package/plugins/publishings/xhs-pub/package.json +17 -0
- package/plugins/publishings/xhs-pub/plugin.json +7 -0
- package/plugins/publishings/xhs-pub/src/index.ts +123 -0
- package/plugins/publishings/xhs-pub/tsconfig.json +4 -0
- package/plugins/publishings/zenn-pub/dist/index.js +42 -91
|
@@ -500,6 +500,16 @@ function metadataOf(value: Record<string, unknown> | null): Record<string, unkno
|
|
|
500
500
|
return metadata !== null && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : {};
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Required stage input: a missing or unparsable file fails the stage naming the path, so a
|
|
505
|
+
* run-date/path mismatch surfaces as its real cause instead of a downstream "field missing" error.
|
|
506
|
+
*/
|
|
507
|
+
function requireJson(path: string, label: string): Record<string, unknown> {
|
|
508
|
+
const parsed = readJson(path);
|
|
509
|
+
if (parsed === null) failWith(1, `${label}: required input not found or unparsable: ${path}`);
|
|
510
|
+
return parsed;
|
|
511
|
+
}
|
|
512
|
+
|
|
503
513
|
/** `jq -e '.targets[]? | select(.target == t and .ok == true and .status == "published")'`. */
|
|
504
514
|
function targetPublished(path: string, target: string): boolean {
|
|
505
515
|
const parsed = readJson(path);
|
|
@@ -752,6 +762,10 @@ function dailyArticle(args: string[]): void {
|
|
|
752
762
|
console.log(`article cached: ${articleMd}`);
|
|
753
763
|
return;
|
|
754
764
|
}
|
|
765
|
+
// Agent-written prose body (workflow `article-write` state) replaces the mechanical
|
|
766
|
+
// per-item render when present — the 3-format split: surfdash gets an article,
|
|
767
|
+
// not a news list (2026-09-16 feedback).
|
|
768
|
+
const proseBody = join(workDir, '2-article', `${runDate}_06_article-write_body.md`);
|
|
755
769
|
kk(
|
|
756
770
|
[
|
|
757
771
|
'executor',
|
|
@@ -764,7 +778,10 @@ function dailyArticle(args: string[]): void {
|
|
|
764
778
|
...pluginsArgs(pluginsPath),
|
|
765
779
|
],
|
|
766
780
|
{
|
|
767
|
-
env: {
|
|
781
|
+
env: {
|
|
782
|
+
ARTICLE_DATE: runDate,
|
|
783
|
+
...(isFile(proseBody) ? { ARTICLE_BODY_FILE: proseBody } : {}),
|
|
784
|
+
},
|
|
768
785
|
},
|
|
769
786
|
);
|
|
770
787
|
const body = readJson(articleContent)?.body;
|
|
@@ -792,7 +809,10 @@ function dailyCoverNormalize(args: string[]): void {
|
|
|
792
809
|
function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
793
810
|
const png = join(workDir, '2-cover', 'content-cover-01.png');
|
|
794
811
|
const jpg = join(workDir, '2-cover', 'content-cover-01.jpg');
|
|
795
|
-
|
|
812
|
+
// Must match the name dailyPublishPrep and the article->cover workflow guard read:
|
|
813
|
+
// `${runDate}_07_cover_cover.png` (a plain `cover-${runDate}.png` left options.coverPath
|
|
814
|
+
// undefined and silently dropped the cover from both podcast and surfdash — 2026-09-16 run).
|
|
815
|
+
const out = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
796
816
|
const source = isFile(png) ? png : isFile(jpg) ? jpg : '';
|
|
797
817
|
if (source === '') failWith(1, 'cover normalize: no content-cover-01.{png,jpg} artifact');
|
|
798
818
|
if (source === jpg) {
|
|
@@ -815,7 +835,7 @@ function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
|
815
835
|
// `rm -f` is silent when the file is already gone.
|
|
816
836
|
}
|
|
817
837
|
}
|
|
818
|
-
console.log(`cover normalized:
|
|
838
|
+
console.log(`cover normalized: ${runDate}_07_cover_cover.png`);
|
|
819
839
|
}
|
|
820
840
|
|
|
821
841
|
function dailyScript(args: string[]): void {
|
|
@@ -864,7 +884,17 @@ function dailyWrapDocs(args: string[]): void {
|
|
|
864
884
|
}
|
|
865
885
|
|
|
866
886
|
function dailyGenerate(args: string[]): void {
|
|
867
|
-
const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '',
|
|
887
|
+
const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '', vdriver = ''] = args;
|
|
888
|
+
// 20260917 review: the workflow picks a voice driver by short name; both drivers' envs are
|
|
889
|
+
// passed unconditionally so switching `vdriver` needs no shell changes. Unknown -> fail loud.
|
|
890
|
+
const VOICE_DRIVERS: Record<string, string> = { ominivoice: 'omni-voice-gen', voicebox: 'voice-gen' };
|
|
891
|
+
const voiceGenerator = VOICE_DRIVERS[vdriver];
|
|
892
|
+
if (!voiceGenerator) {
|
|
893
|
+
failWith(
|
|
894
|
+
1,
|
|
895
|
+
`generate: unknown vdriver '${vdriver}' (expected one of: ${Object.keys(VOICE_DRIVERS).join(', ')})`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
868
898
|
const docs = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
|
|
869
899
|
const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
870
900
|
timed(workDir, 'generate', () => {
|
|
@@ -881,6 +911,111 @@ function dailyGenerate(args: string[]): void {
|
|
|
881
911
|
});
|
|
882
912
|
}
|
|
883
913
|
|
|
914
|
+
/**
|
|
915
|
+
* 20260917 review #7: prepend the show's 8s intro sting to the generated audio.
|
|
916
|
+
* `intro_music` empty -> skip. Writes `<stem>.intro.wav` (original render kept), points
|
|
917
|
+
* `audioPath` at the merged file, extends `duration`, marks `introMerged` (idempotent).
|
|
918
|
+
*/
|
|
919
|
+
function dailyIntro(args: string[]): void {
|
|
920
|
+
const [workDir = '', runDate = '', introSrc = ''] = args;
|
|
921
|
+
if (!introSrc || introSrc.trim() === '') {
|
|
922
|
+
console.log('intro-music: intro_music var empty — skip');
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const contentPath = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
926
|
+
timed(workDir, 'intro-music', () => {
|
|
927
|
+
const content = requireJson(contentPath, 'intro-music');
|
|
928
|
+
const meta = metadataOf(content);
|
|
929
|
+
if (meta.introMerged === true) {
|
|
930
|
+
console.log(`intro-music cached: ${contentPath}`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const rawFile = meta.mp3Path ?? meta.audioPath;
|
|
934
|
+
if (typeof rawFile !== 'string' || rawFile === '') {
|
|
935
|
+
failWith(1, 'intro-music: voice content carries no mp3Path/audioPath');
|
|
936
|
+
}
|
|
937
|
+
const introPath = resolve(introSrc);
|
|
938
|
+
if (!existsSync(introPath)) failWith(1, `intro-music: intro file not found: ${introPath}`);
|
|
939
|
+
const mainPath = resolve(rawFile);
|
|
940
|
+
if (!existsSync(mainPath)) failWith(1, `intro-music: generated audio not found: ${mainPath}`);
|
|
941
|
+
const mergedPath = mainPath.replace(/\.wav$/, '.intro.wav');
|
|
942
|
+
const probe = (file: string, entries: string): string =>
|
|
943
|
+
spawnSync('ffprobe', [
|
|
944
|
+
'-v',
|
|
945
|
+
'error',
|
|
946
|
+
'-select_streams',
|
|
947
|
+
'a:0',
|
|
948
|
+
'-show_entries',
|
|
949
|
+
entries,
|
|
950
|
+
'-of',
|
|
951
|
+
'csv=p=0',
|
|
952
|
+
file,
|
|
953
|
+
])
|
|
954
|
+
.stdout?.toString()
|
|
955
|
+
.trim() ?? '';
|
|
956
|
+
const mainDur = Number.parseFloat(probe(mainPath, 'format=duration'));
|
|
957
|
+
const introDur = Number.parseFloat(probe(introPath, 'format=duration'));
|
|
958
|
+
if (!Number.isFinite(mainDur) || !Number.isFinite(introDur)) {
|
|
959
|
+
failWith(1, `intro-music: ffprobe could not read durations (main=${mainDur} intro=${introDur})`);
|
|
960
|
+
}
|
|
961
|
+
const rate = probe(mainPath, 'stream=sample_rate') || '24000'; // match the generated audio's rate; mono s16 both sides
|
|
962
|
+
const norm = (label: string): string =>
|
|
963
|
+
`[${label}:a]aresample=${rate},aformat=sample_fmts=s16:channel_layouts=mono[${label}]`;
|
|
964
|
+
const fc = `${norm('0')};${norm('1')};[0][1]concat=n=2:v=0:a=1[out]`;
|
|
965
|
+
const conv = spawnSync('ffmpeg', [
|
|
966
|
+
'-y',
|
|
967
|
+
'-hide_banner',
|
|
968
|
+
'-loglevel',
|
|
969
|
+
'error',
|
|
970
|
+
'-i',
|
|
971
|
+
introPath,
|
|
972
|
+
'-i',
|
|
973
|
+
mainPath,
|
|
974
|
+
'-filter_complex',
|
|
975
|
+
fc,
|
|
976
|
+
'-map',
|
|
977
|
+
'[out]',
|
|
978
|
+
'-c:a',
|
|
979
|
+
'pcm_s16le',
|
|
980
|
+
mergedPath,
|
|
981
|
+
]);
|
|
982
|
+
if (conv.status !== 0 || !existsSync(mergedPath)) {
|
|
983
|
+
failWith(1, `intro-music: ffmpeg concat failed: ${conv.stderr?.toString().trim()}`);
|
|
984
|
+
}
|
|
985
|
+
if (meta.mp3Path && typeof meta.mp3Path === 'string') {
|
|
986
|
+
// transcode_mp3=true: keep the mp3 path honest by re-transcoding the merged wav.
|
|
987
|
+
const mp3 = spawnSync('ffmpeg', [
|
|
988
|
+
'-y',
|
|
989
|
+
'-hide_banner',
|
|
990
|
+
'-loglevel',
|
|
991
|
+
'error',
|
|
992
|
+
'-i',
|
|
993
|
+
mergedPath,
|
|
994
|
+
'-codec:a',
|
|
995
|
+
'libmp3lame',
|
|
996
|
+
'-qscale:a',
|
|
997
|
+
'2',
|
|
998
|
+
resolve(meta.mp3Path),
|
|
999
|
+
]);
|
|
1000
|
+
if (mp3.status !== 0) failWith(1, `intro-music: mp3 re-transcode failed: ${mp3.stderr?.toString().trim()}`);
|
|
1001
|
+
}
|
|
1002
|
+
const mergedDur = Number.parseFloat(probe(mergedPath, 'format=duration')) || mainDur + introDur;
|
|
1003
|
+
const updated = {
|
|
1004
|
+
...content,
|
|
1005
|
+
metadata: {
|
|
1006
|
+
...meta,
|
|
1007
|
+
audioPath: mergedPath,
|
|
1008
|
+
duration: mergedDur,
|
|
1009
|
+
introMerged: true,
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
sted('write', contentPath, () => writeJsonText(contentPath, updated));
|
|
1013
|
+
console.log(
|
|
1014
|
+
`intro-music: merged ${introDur.toFixed(1)}s intro -> ${mergedPath} (${mergedDur.toFixed(1)}s total)`,
|
|
1015
|
+
);
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
884
1019
|
function dailyQualityReport(args: string[]): void {
|
|
885
1020
|
const [workDir = '', runDate = ''] = args;
|
|
886
1021
|
const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
@@ -904,7 +1039,7 @@ function dailyPublishPrep(args: string[]): void {
|
|
|
904
1039
|
const prepPath = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
|
|
905
1040
|
const coverPath = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
906
1041
|
timed(workDir, 'publish-prep', () => {
|
|
907
|
-
const article =
|
|
1042
|
+
const article = requireJson(articlePath, 'publish-prep');
|
|
908
1043
|
const audioMeta = metadataOf(readJson(audioPath));
|
|
909
1044
|
const rawFile = audioMeta.mp3Path ?? audioMeta.audioPath;
|
|
910
1045
|
if (typeof rawFile !== 'string' || rawFile === '') {
|
|
@@ -912,13 +1047,21 @@ function dailyPublishPrep(args: string[]): void {
|
|
|
912
1047
|
}
|
|
913
1048
|
const audioFile = resolve(rawFile);
|
|
914
1049
|
const resolvedCover = resolve(coverPath);
|
|
1050
|
+
const hasCover = existsSync(resolvedCover);
|
|
1051
|
+
// podcast-pub's documented contract reads metadata.coverImage (map.ts); options.coverPath
|
|
1052
|
+
// alone was silently dropped by the mapper — episodes published without a cover.
|
|
915
1053
|
const merged = {
|
|
916
1054
|
...article,
|
|
917
|
-
metadata: {
|
|
1055
|
+
metadata: {
|
|
1056
|
+
...metadataOf(article),
|
|
1057
|
+
audioFile,
|
|
1058
|
+
runDate,
|
|
1059
|
+
...(hasCover ? { coverImage: resolvedCover } : {}),
|
|
1060
|
+
},
|
|
918
1061
|
options: {
|
|
919
1062
|
audioFile,
|
|
920
1063
|
durationSec: audioMeta.duration,
|
|
921
|
-
coverPath:
|
|
1064
|
+
coverPath: hasCover ? resolvedCover : undefined,
|
|
922
1065
|
},
|
|
923
1066
|
};
|
|
924
1067
|
sted('write', prepPath, () => writeJsonText(prepPath, merged));
|
|
@@ -964,6 +1107,9 @@ function dailyShowNotes(args: string[]): void {
|
|
|
964
1107
|
const result = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
965
1108
|
const out = join(workDir, '3-publish', `${runDate}_15_show-notes_show-notes.md`);
|
|
966
1109
|
const plan = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
1110
|
+
// Generated-episode content carries `metadata.segments[]` — the source of real
|
|
1111
|
+
// audio offsets for the show-notes timeline (jump-to-item timestamps).
|
|
1112
|
+
const audioContent = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
967
1113
|
timed(workDir, 'show-notes', () => {
|
|
968
1114
|
const effective = readJson(article)?.title;
|
|
969
1115
|
const titleArg = title !== '' ? title : typeof effective === 'string' ? effective : '';
|
|
@@ -974,6 +1120,7 @@ function dailyShowNotes(args: string[]): void {
|
|
|
974
1120
|
PODCAST_PUB_RUN_DATE: runDate,
|
|
975
1121
|
PODCAST_PUB_BRIEF_MODE: 'true',
|
|
976
1122
|
PODCAST_PUB_ARTICLE_POST_PATH: postPath,
|
|
1123
|
+
...(isFile(audioContent) ? { PODCAST_PUB_AUDIO_CONTENT: audioContent } : {}),
|
|
977
1124
|
},
|
|
978
1125
|
});
|
|
979
1126
|
});
|
|
@@ -1004,7 +1151,7 @@ function dailyPublishPodcast(args: string[]): void {
|
|
|
1004
1151
|
return;
|
|
1005
1152
|
}
|
|
1006
1153
|
try {
|
|
1007
|
-
const content =
|
|
1154
|
+
const content = requireJson(prep, 'publish-podcast');
|
|
1008
1155
|
const showNotes = readFileSync(notes, 'utf-8');
|
|
1009
1156
|
const merged = { ...content, metadata: { ...metadataOf(content), showNotes } };
|
|
1010
1157
|
sted('write', prep, () => writeJsonText(prep, merged));
|
|
@@ -1084,6 +1231,95 @@ function dailyPublishPartial(args: string[]): void {
|
|
|
1084
1231
|
});
|
|
1085
1232
|
}
|
|
1086
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* 20260917 review #4/#6: after both publishes land, patch the surfdash disk posts —
|
|
1236
|
+
* reuse the podcast-hosted cover URL in frontmatter (image/og_image; no re-upload; fallback:
|
|
1237
|
+
* copy the local cover into assets/) and append a podcast cross-link at the end of zh/en/ja.
|
|
1238
|
+
* Idempotent per post; commits + pushes the surfdash checkout (SURFDASH_AUTO_PUSH=0 keeps commit-only).
|
|
1239
|
+
*/
|
|
1240
|
+
function dailyPatchSurfdash(args: string[]): void {
|
|
1241
|
+
const [workDir = '', runDate = '', surfdashRoot = ''] = args;
|
|
1242
|
+
if (!surfdashRoot || !existsSync(surfdashRoot)) {
|
|
1243
|
+
console.log(`patch-surfdash: surfdash root '${surfdashRoot}' not found — skip`);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
timed(workDir, 'patch-surfdash', () => {
|
|
1247
|
+
const sdResult = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
1248
|
+
const podcastResult = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
|
|
1249
|
+
if (!isFile(sdResult) || !isFile(podcastResult)) {
|
|
1250
|
+
failWith(1, 'patch-surfdash: missing surfdash/podcast publish results — run the publish states first');
|
|
1251
|
+
}
|
|
1252
|
+
const targetOf = (path: string): Record<string, unknown> => {
|
|
1253
|
+
const parsed = readJson(path) as { targets?: Array<Record<string, unknown>> };
|
|
1254
|
+
return parsed.targets?.[0] ?? {};
|
|
1255
|
+
};
|
|
1256
|
+
const sd = targetOf(sdResult);
|
|
1257
|
+
const pod = targetOf(podcastResult);
|
|
1258
|
+
const postPath = (sd.metadata as Record<string, unknown> | undefined)?.postPath;
|
|
1259
|
+
if (typeof postPath !== 'string' || postPath === '')
|
|
1260
|
+
failWith(1, 'patch-surfdash: surfdash result carries no postPath');
|
|
1261
|
+
const slugDir = dirname(postPath);
|
|
1262
|
+
const showNotesUrl = typeof pod.url === 'string' ? pod.url : '';
|
|
1263
|
+
const coverUrl = ((pod.metadata as Record<string, unknown> | undefined)?.imageUrl as string | undefined) ?? '';
|
|
1264
|
+
const localCover = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
1265
|
+
const dateIso = `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`;
|
|
1266
|
+
const crossLink: Record<string, string> = {
|
|
1267
|
+
zh: `🎧 本期已同步制作为播客节目,[点击收听《每日 AI 资讯 · ${dateIso}》](${showNotesUrl})。`,
|
|
1268
|
+
en: `🎧 This episode is also available as a podcast: [listen to the Daily AI Briefing · ${dateIso}](${showNotesUrl}).`,
|
|
1269
|
+
ja: `🎧 本エピソードはポッドキャストでも配信中:[Daily AI Briefing · ${dateIso} を聴く](${showNotesUrl})。`,
|
|
1270
|
+
};
|
|
1271
|
+
let changed = 0;
|
|
1272
|
+
for (const [locale, dir] of [
|
|
1273
|
+
['zh', slugDir],
|
|
1274
|
+
['en', join(dirname(slugDir), 'en', basename(slugDir))],
|
|
1275
|
+
['ja', join(dirname(slugDir), 'ja', basename(slugDir))],
|
|
1276
|
+
] as const) {
|
|
1277
|
+
const indexMd = join(dir, 'index.md');
|
|
1278
|
+
if (!existsSync(indexMd)) {
|
|
1279
|
+
console.log(`patch-surfdash: ${locale} post not published (${indexMd}) — skip`);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
let md = readFileSync(indexMd, 'utf-8');
|
|
1283
|
+
const fmEnd = md.indexOf('---', 3); // frontmatter is `---\n ... \n---\n`
|
|
1284
|
+
if (fmEnd < 0) failWith(1, `patch-surfdash: no frontmatter in ${indexMd}`);
|
|
1285
|
+
if (!/^image:/m.test(md.slice(0, fmEnd))) {
|
|
1286
|
+
let image = coverUrl;
|
|
1287
|
+
if (image === '' && existsSync(localCover)) {
|
|
1288
|
+
// Fallback for pre-#6 runs: copy the local cover instead of a hosted URL.
|
|
1289
|
+
const assets = join(dir, 'assets');
|
|
1290
|
+
mkdirSync(assets, { recursive: true });
|
|
1291
|
+
copyFileSync(localCover, join(assets, 'cover.png'));
|
|
1292
|
+
image = './assets/cover.png';
|
|
1293
|
+
}
|
|
1294
|
+
if (image !== '') {
|
|
1295
|
+
md = md.replace(/^publishDate: .*$/m, (m0) => `${m0}\nimage: '${image}'\nog_image: '${image}'`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (showNotesUrl !== '' && !md.includes(showNotesUrl)) {
|
|
1299
|
+
md = `${md.replace(/\s*$/, '')}\n\n${crossLink[locale]}\n`;
|
|
1300
|
+
}
|
|
1301
|
+
if (md !== readFileSync(indexMd, 'utf-8')) {
|
|
1302
|
+
sted('write', indexMd, () => writeFileSync(indexMd, md));
|
|
1303
|
+
changed++;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (changed === 0) {
|
|
1307
|
+
console.log('patch-surfdash: nothing to change');
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const git = (...a: string[]): void => {
|
|
1311
|
+
const r = spawnSync('git', ['-C', surfdashRoot, ...a]);
|
|
1312
|
+
if (r.status !== 0) failWith(1, `patch-surfdash: git ${a[0]} failed: ${r.stderr?.toString().trim()}`);
|
|
1313
|
+
};
|
|
1314
|
+
git('add', 'content/posts/articles');
|
|
1315
|
+
git('commit', '-m', `feat: cover + podcast cross-link for ${basename(slugDir)} (kk daily patch)`);
|
|
1316
|
+
if (process.env.SURFDASH_AUTO_PUSH !== '0') git('push');
|
|
1317
|
+
console.log(
|
|
1318
|
+
`patch-surfdash: patched ${changed} post(s), committed${process.env.SURFDASH_AUTO_PUSH !== '0' ? ' + pushed' : ' (push skipped: SURFDASH_AUTO_PUSH=0)'}`,
|
|
1319
|
+
);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1087
1323
|
function dailyRunReport(args: string[]): void {
|
|
1088
1324
|
const [
|
|
1089
1325
|
workDir = '',
|
|
@@ -1108,6 +1344,9 @@ function dailyRunReport(args: string[]): void {
|
|
|
1108
1344
|
return;
|
|
1109
1345
|
}
|
|
1110
1346
|
sted('write', cands, () => writeJqText(cands, metadataOf(readJson(candidates)).docs ?? []));
|
|
1347
|
+
// Plugin env paths must be absolute: spawnPluginEntry runs plugins with cwd = the
|
|
1348
|
+
// plugin package dir, so a relative workDir silently misses every best-effort read.
|
|
1349
|
+
const absWorkDir = resolve(workDir);
|
|
1111
1350
|
kk(['executor', 'run', 'news-report-gen', '--in', cands, '--out', reportJson, ...pluginsArgs(pluginsPath)], {
|
|
1112
1351
|
env: {
|
|
1113
1352
|
QC_MIN_QUALITY: minQuality,
|
|
@@ -1115,13 +1354,19 @@ function dailyRunReport(args: string[]): void {
|
|
|
1115
1354
|
QC_MIN_URGENCY: minUrgency,
|
|
1116
1355
|
QC_MIN_IMPACT: minImpact,
|
|
1117
1356
|
NEWS_REPORT_REJECTED_FILE: join(
|
|
1118
|
-
|
|
1357
|
+
absWorkDir,
|
|
1119
1358
|
'2-plan',
|
|
1120
1359
|
`${runDate}_04_quality-control-content_candidates.rejected.json`,
|
|
1121
1360
|
),
|
|
1122
|
-
NEWS_REPORT_TIMING_FILE: join(
|
|
1361
|
+
NEWS_REPORT_TIMING_FILE: join(absWorkDir, 'step-timing.json'),
|
|
1123
1362
|
// 0139 R5b: the generate stage's QC audit, read best-effort (absent -> section omitted).
|
|
1124
|
-
NEWS_REPORT_QC_FILE: join(
|
|
1363
|
+
NEWS_REPORT_QC_FILE: join(absWorkDir, '3-audio', `${runDate}_11_generate_content.json`),
|
|
1364
|
+
// 20260917 funnel: the plan stage's full selection trail (every fetched title +
|
|
1365
|
+
// scores + disposition), rendered as the 选稿漏斗 section (absent -> omitted).
|
|
1366
|
+
NEWS_REPORT_PLAN_AUDIT_FILE: join(absWorkDir, '2-plan', `${runDate}_03_plan_plan.json.audit.json`),
|
|
1367
|
+
// 20260917 funnel: the blended fan-in input, rendered as the 语料构成
|
|
1368
|
+
// source/category composition (absent -> omitted).
|
|
1369
|
+
NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '1-ingest', `${runDate}_02_collect_blended.json`),
|
|
1125
1370
|
NEWS_REPORT_DATE: runDate,
|
|
1126
1371
|
},
|
|
1127
1372
|
});
|
|
@@ -1132,68 +1377,78 @@ function dailyRunReport(args: string[]): void {
|
|
|
1132
1377
|
});
|
|
1133
1378
|
}
|
|
1134
1379
|
|
|
1135
|
-
|
|
1380
|
+
/**
|
|
1381
|
+
* Collapsed translate scaffolding (2026-09-16 feedback): surfdash-pub owns the
|
|
1382
|
+
* deterministic mechanics — `scaffold-locale` for both target locales — while the
|
|
1383
|
+
* workflow `translate` state's agent step owns the LLM translation of the drafts.
|
|
1384
|
+
* Writes one marker file per locale (zh post path + en/ja draft paths); empty
|
|
1385
|
+
* marker = locale absent (callers skip). Reuses an existing on-disk draft as the
|
|
1386
|
+
* cache across run retries — re-scaffolding would erase the agent's translation.
|
|
1387
|
+
*/
|
|
1388
|
+
function dailyTranslateLocalize(args: string[]): void {
|
|
1136
1389
|
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
1137
1390
|
const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
|
|
1138
|
-
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-
|
|
1139
|
-
|
|
1140
|
-
const scaffold = join(workDir, '3-publish', `${runDate}_20_translate-en_scaffold-en.json`);
|
|
1141
|
-
const scaffoldResult = join(workDir, '3-publish', `${runDate}_20_translate-en_scaffold-en-result.json`);
|
|
1142
|
-
timed(workDir, 'translate-en', () => {
|
|
1391
|
+
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-localize_zh-post-path.txt`);
|
|
1392
|
+
timed(workDir, 'translate-localize', () => {
|
|
1143
1393
|
const postPath = firstPostPath(readJson(result), 'surfdash-pub');
|
|
1144
1394
|
const zh = postPath.endsWith('index.md') ? postPath : '';
|
|
1145
1395
|
sted('write', zhFile, () => writeRaw(zhFile, zh));
|
|
1146
|
-
const en = zh.replace('/zh/', '/en/');
|
|
1147
|
-
sted('write', enFile, () => writeRaw(enFile, en));
|
|
1148
|
-
if (zh === '') {
|
|
1149
|
-
console.log('no zh post path — skip translate-en');
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
if (isFile(en)) {
|
|
1153
|
-
console.log(`en draft cached: ${en}`);
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
sted('write', scaffold, () =>
|
|
1157
|
-
writeJqText(scaffold, { body: '', options: { operation: 'scaffold-locale', postPath: zh, target: 'en' } }),
|
|
1158
|
-
);
|
|
1159
|
-
kk(['executor', 'run', 'surfdash-pub', '--in', scaffold, '--out', scaffoldResult, ...pluginsArgs(pluginsPath)]);
|
|
1160
|
-
const draftPath = metadataOf(readJson(scaffoldResult)).draftPath;
|
|
1161
|
-
sted('write', enFile, () => writeRaw(enFile, typeof draftPath === 'string' ? draftPath : ''));
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
function dailyTranslateJa(args: string[]): void {
|
|
1166
|
-
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
1167
|
-
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-en_zh-post-path.txt`);
|
|
1168
|
-
const jaFile = join(workDir, '3-publish', `${runDate}_22_translate-ja_ja-post-path.txt`);
|
|
1169
|
-
const scaffold = join(workDir, '3-publish', `${runDate}_22_translate-ja_scaffold-ja.json`);
|
|
1170
|
-
const scaffoldResult = join(workDir, '3-publish', `${runDate}_22_translate-ja_scaffold-ja-result.json`);
|
|
1171
|
-
timed(workDir, 'translate-ja', () => {
|
|
1172
|
-
const zh = isFile(zhFile) ? readFileSync(zhFile, 'utf-8') : '';
|
|
1173
|
-
const ja = zh.replace('/zh/', '/ja/');
|
|
1174
|
-
sted('write', jaFile, () => writeRaw(jaFile, ja));
|
|
1175
1396
|
if (zh === '') {
|
|
1176
|
-
|
|
1397
|
+
for (const target of ['en', 'ja'] as const) {
|
|
1398
|
+
sted(
|
|
1399
|
+
'write',
|
|
1400
|
+
join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
|
|
1401
|
+
() =>
|
|
1402
|
+
writeRaw(
|
|
1403
|
+
join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
|
|
1404
|
+
'',
|
|
1405
|
+
),
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
console.log('no zh post path — skip localize');
|
|
1177
1409
|
return;
|
|
1178
1410
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1411
|
+
for (const target of ['en', 'ja'] as const) {
|
|
1412
|
+
const markerFile = join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`);
|
|
1413
|
+
const existing = zh.replace('/zh/', `/${target}/`);
|
|
1414
|
+
if (isFile(existing)) {
|
|
1415
|
+
sted('write', markerFile, () => writeRaw(markerFile, existing));
|
|
1416
|
+
console.log(`${target} draft cached: ${existing}`);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const scaffold = join(workDir, '3-publish', `${runDate}_20_translate-localize_scaffold-${target}.json`);
|
|
1420
|
+
const scaffoldResult = join(
|
|
1421
|
+
workDir,
|
|
1422
|
+
'3-publish',
|
|
1423
|
+
`${runDate}_20_translate-localize_scaffold-${target}-result.json`,
|
|
1424
|
+
);
|
|
1425
|
+
sted('write', scaffold, () =>
|
|
1426
|
+
writeJqText(scaffold, { body: '', options: { operation: 'scaffold-locale', postPath: zh, target } }),
|
|
1427
|
+
);
|
|
1428
|
+
kk([
|
|
1429
|
+
'executor',
|
|
1430
|
+
'run',
|
|
1431
|
+
'surfdash-pub',
|
|
1432
|
+
'--in',
|
|
1433
|
+
scaffold,
|
|
1434
|
+
'--out',
|
|
1435
|
+
scaffoldResult,
|
|
1436
|
+
...pluginsArgs(pluginsPath),
|
|
1437
|
+
]);
|
|
1438
|
+
const draftPath = metadataOf(readJson(scaffoldResult)).draftPath;
|
|
1439
|
+
sted('write', markerFile, () => writeRaw(markerFile, typeof draftPath === 'string' ? draftPath : ''));
|
|
1182
1440
|
}
|
|
1183
|
-
sted('write', scaffold, () =>
|
|
1184
|
-
writeJqText(scaffold, { body: '', options: { operation: 'scaffold-locale', postPath: zh, target: 'ja' } }),
|
|
1185
|
-
);
|
|
1186
|
-
kk(['executor', 'run', 'surfdash-pub', '--in', scaffold, '--out', scaffoldResult, ...pluginsArgs(pluginsPath)]);
|
|
1187
|
-
const draftPath = metadataOf(readJson(scaffoldResult)).draftPath;
|
|
1188
|
-
sted('write', jaFile, () => writeRaw(jaFile, typeof draftPath === 'string' ? draftPath : ''));
|
|
1189
1441
|
});
|
|
1190
1442
|
}
|
|
1191
1443
|
|
|
1192
1444
|
function dailyTranslateSync(args: string[]): void {
|
|
1193
|
-
const [workDir = '', runDate = '', pluginsPath = '',
|
|
1445
|
+
const [workDir = '', runDate = '', pluginsPath = '', zhPathFile = ''] = args;
|
|
1194
1446
|
const sync = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync.json`);
|
|
1195
1447
|
const syncResult = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync-result.json`);
|
|
1196
1448
|
timed(workDir, 'translate-sync', () => {
|
|
1449
|
+
// Reads the marker file itself — the collapsed `translate` state has no
|
|
1450
|
+
// cross-state var to hand the path through.
|
|
1451
|
+
const zhPostPath = isFile(zhPathFile) ? readFileSync(zhPathFile, 'utf-8') : '';
|
|
1197
1452
|
if (zhPostPath === '') {
|
|
1198
1453
|
console.log('no zh post path — skip translate-sync');
|
|
1199
1454
|
return;
|
|
@@ -1287,6 +1542,9 @@ if (import.meta.main)
|
|
|
1287
1542
|
case 'daily-generate':
|
|
1288
1543
|
dailyGenerate(rest);
|
|
1289
1544
|
break;
|
|
1545
|
+
case 'daily-intro':
|
|
1546
|
+
dailyIntro(rest);
|
|
1547
|
+
break;
|
|
1290
1548
|
case 'daily-quality-report':
|
|
1291
1549
|
dailyQualityReport(rest);
|
|
1292
1550
|
break;
|
|
@@ -1305,17 +1563,17 @@ if (import.meta.main)
|
|
|
1305
1563
|
case 'daily-publish-classify':
|
|
1306
1564
|
dailyPublishClassify(rest);
|
|
1307
1565
|
break;
|
|
1566
|
+
case 'daily-patch-surfdash':
|
|
1567
|
+
dailyPatchSurfdash(rest);
|
|
1568
|
+
break;
|
|
1308
1569
|
case 'daily-publish-partial':
|
|
1309
1570
|
dailyPublishPartial(rest);
|
|
1310
1571
|
break;
|
|
1311
1572
|
case 'daily-run-report':
|
|
1312
1573
|
dailyRunReport(rest);
|
|
1313
1574
|
break;
|
|
1314
|
-
case 'daily-translate-
|
|
1315
|
-
|
|
1316
|
-
break;
|
|
1317
|
-
case 'daily-translate-ja':
|
|
1318
|
-
dailyTranslateJa(rest);
|
|
1575
|
+
case 'daily-translate-localize':
|
|
1576
|
+
dailyTranslateLocalize(rest);
|
|
1319
1577
|
break;
|
|
1320
1578
|
case 'daily-translate-sync':
|
|
1321
1579
|
dailyTranslateSync(rest);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: publish
|
|
3
|
+
description: >-
|
|
4
|
+
Thin operator surface for leftover publish channels — "publish this draft to
|
|
5
|
+
Medium", "fan out to Qiita and Medium", "prepare a Juejin draft". Shells
|
|
6
|
+
`kk executor run` / `kk fanout` for file-cli and script channels; sequences
|
|
7
|
+
agent-default CMS channels as workflow `agent.run` steps. Does not own
|
|
8
|
+
transport.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# publish — leftover channel operator surface
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
|
|
15
|
+
Drive harvested `kind: publish` plugins after a draft exists. **The plugin owns
|
|
16
|
+
transport and Result.** This skill selects the channel, confirms draft vs live,
|
|
17
|
+
and shells `kk` — it does not call an LLM from inside a plugin process and does
|
|
18
|
+
not add a `kk publish` noun.
|
|
19
|
+
|
|
20
|
+
## Channels
|
|
21
|
+
|
|
22
|
+
| Channel | Plugin | Transport | How this skill invokes it |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| medium | `medium-pub` | `file-cli` (token API) | `kk executor run medium-pub --in <content.json> --out <result.json>` |
|
|
25
|
+
| wechatmp | `wechatmp-pub` | `file-cli` (`draft/add`) | `kk executor run wechatmp-pub` (default operation `draft`) |
|
|
26
|
+
| x post | `x-pub` | `file-cli` (`POST /2/tweets`) | `kk executor run x-pub` (`options.operation` omitted / `post`) |
|
|
27
|
+
| x article | `x-pub` | `browser` / `agent` | workflow `agent.run` — not `kk fanout` |
|
|
28
|
+
| juejin | `juejin-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
29
|
+
| infoq | `infoq-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
30
|
+
| substack | `substack-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
31
|
+
| xhs | `xhs-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
32
|
+
|
|
33
|
+
Already-harvested file-cli channels (`qiita-pub`, `zenn-pub`, `surfdash-pub`,
|
|
34
|
+
`emdash-pub`, `podcast-pub`) compose the same way.
|
|
35
|
+
|
|
36
|
+
## Do not
|
|
37
|
+
|
|
38
|
+
- Embed Playwright selectors or reimplement a transport here.
|
|
39
|
+
- Spawn an LLM from `kk fanout` (ADR-007 R4). Agent-default channels are
|
|
40
|
+
sequential workflow `agent.run` steps — see
|
|
41
|
+
`plugins/kk/workflows/leftover-publish-fanout-example.yaml`.
|
|
42
|
+
- Add a first-layer `kk publish` command.
|
|
43
|
+
- Put secrets in `--in` JSON or `plugin.json`. Env only.
|
|
44
|
+
|
|
45
|
+
## Confirmation
|
|
46
|
+
|
|
47
|
+
Default Result is **draft / unpublished**. Live publish (`public`, WeChat
|
|
48
|
+
`freepublish/submit`, CMS submit) requires an explicit operator confirmation
|
|
49
|
+
in this skill before the plugin is invoked with a live status. Under
|
|
50
|
+
`KK_NONINTERACTIVE=1`, proceed only when the `--in` payload already carries
|
|
51
|
+
the confirmed live status; otherwise fail naming the switch.
|
|
52
|
+
|
|
53
|
+
## Invocation
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
<channel> [--in <content.json>] [--out <result.json>] [--live]
|
|
57
|
+
[--fanout <channel>,<channel>]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
1. Resolve `--in` Content (required).
|
|
61
|
+
2. If `--fanout` lists two or more **file-cli or script** channels: run
|
|
62
|
+
`kk fanout --in <content.json> --targets <list> --out <aggregate.json>`.
|
|
63
|
+
Each target contributes one `targets[]` Result entry.
|
|
64
|
+
3. If the channel's default driver is `agent`: do **not** call `kk fanout`.
|
|
65
|
+
Tell the operator (or the workflow) to run the page against
|
|
66
|
+
`~/.config/kk/browser-profiles/<channel>` then `kk executor run <plugin>`
|
|
67
|
+
so the plugin writes Result. The example workflow sequences that as
|
|
68
|
+
`agent.run`.
|
|
69
|
+
4. Otherwise: `kk executor run <plugin> --in … --out …`.
|
|
70
|
+
5. Report `--out` `ok`, `target`, `id`/`url`, and `error` verbatim on failure.
|
|
71
|
+
|
|
72
|
+
## Env (plugin-owned; never echoed)
|
|
73
|
+
|
|
74
|
+
| Plugin | Required |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| medium-pub | `MEDIUM_INTEGRATION_TOKEN` |
|
|
77
|
+
| wechatmp-pub | `WECHAT_APP_ID`, `WECHAT_APP_SECRET` |
|
|
78
|
+
| x-pub `post` | `X_API_KEY`, `X_API_SECRET`, `X_ACCESS_TOKEN`, `X_ACCESS_TOKEN_SECRET` |
|
|
79
|
+
|
|
80
|
+
Missing env → plugin exit 1, `Result.ok false`, error names the variable.
|