@gobing-ai/knowledge-kit 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +137 -59
- package/package.json +4 -4
- package/plugins/generations/content-gen/dist/index.js +97 -45
- package/plugins/generations/core-facts-gen/dist/index.js +106 -57
- package/plugins/generations/daily-article-gen/dist/index.js +105 -56
- package/plugins/generations/dailynews-gen/dist/index.js +235 -57
- package/plugins/generations/dailynews-gen/src/index.ts +67 -2
- package/plugins/generations/dailynews-gen/src/script-builder.ts +36 -15
- package/plugins/generations/dailynews-gen/src/script-plan.ts +142 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +115 -66
- package/plugins/generations/gemini-tts/README.md +125 -0
- package/plugins/generations/gemini-tts/dist/index.js +23348 -0
- package/plugins/generations/gemini-tts/package.json +16 -0
- package/plugins/generations/gemini-tts/plugin.json +6 -0
- package/plugins/generations/gemini-tts/profiles.json +3 -0
- package/plugins/generations/gemini-tts/src/gemini-client.ts +220 -0
- package/plugins/generations/gemini-tts/src/index.ts +33 -0
- package/plugins/generations/gemini-tts/src/voice-create.ts +180 -0
- package/plugins/generations/gemini-tts/tsconfig.json +8 -0
- package/plugins/generations/image-gen/dist/index.js +338 -117
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +19 -0
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +55 -0
- package/plugins/generations/image-gen/presets/styles/daily-news.yaml +9 -0
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +7 -0
- package/plugins/generations/image-gen/src/index.ts +148 -14
- package/plugins/generations/image-gen/src/job.ts +30 -0
- package/plugins/generations/image-gen/src/providers/errors.ts +91 -0
- package/plugins/generations/image-gen/src/providers/types.ts +79 -0
- package/plugins/generations/news-report-gen/dist/index.js +136 -55
- package/plugins/generations/news-report-gen/src/index.ts +57 -1
- package/plugins/generations/voice-gen/dist/index.js +121 -69
- package/plugins/generations/voice-gen/package.json +3 -0
- package/plugins/generations/voice-gen/src/index.ts +2 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +2 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +103 -53
- package/plugins/ingestions/horizon-ingest/dist/index.js +102 -53
- package/plugins/ingestions/karakeep-local/dist/index.js +273 -224
- package/plugins/ingestions/last30days-ingest/dist/index.js +97 -48
- package/plugins/ingestions/web-search/dist/index.js +121 -72
- package/plugins/kk/config.example.yaml +13 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +52 -0
- package/plugins/kk/scripts/kk-workflow-stages.ts +438 -17
- package/plugins/kk/skills/publish/SKILL.md +24 -1
- package/plugins/kk/skills/xhs-images/SKILL.md +89 -0
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +35 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +164 -13
- package/plugins/publishings/emdash-pub/dist/index.js +94 -45
- package/plugins/publishings/infoq-pub/dist/index.js +94 -45
- package/plugins/publishings/juejin-pub/dist/index.js +94 -45
- package/plugins/publishings/medium-pub/dist/index.js +94 -45
- package/plugins/publishings/podcast-pub/dist/index.js +115 -55
- package/plugins/publishings/podcast-pub/src/map.ts +30 -10
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -1
- package/plugins/publishings/qiita-pub/dist/index.js +94 -45
- package/plugins/publishings/substack-pub/dist/index.js +94 -45
- package/plugins/publishings/surfdash-pub/dist/index.js +99 -63
- package/plugins/publishings/surfdash-pub/src/index.ts +10 -1
- package/plugins/publishings/wechatmp-pub/dist/index.js +94 -45
- package/plugins/publishings/x-pub/dist/index.js +94 -45
- package/plugins/publishings/xhs-pub/dist/index.js +8908 -229
- package/plugins/publishings/xhs-pub/package.json +1 -0
- package/plugins/publishings/xhs-pub/plugin.json +1 -1
- package/plugins/publishings/xhs-pub/src/automation-guard.ts +128 -0
- package/plugins/publishings/xhs-pub/src/index.ts +753 -24
- package/plugins/publishings/xhs-pub/src/profile-check.ts +133 -0
- package/plugins/publishings/zenn-pub/dist/index.js +94 -45
- package/plugins/sp/lib/env.ts +46 -0
- package/plugins/sp/lib/inline-run.generated.mjs +1489 -0
- package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
- package/plugins/sp/scripts/inline-run-setup.ts +571 -47
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +190 -0
- package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +77 -6
- package/plugins/sp/scripts/wrapup-steps.ts +91 -5
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
* comes from `realpath "$(command -v kk)"` (the installed package root), and falls back to the
|
|
33
33
|
* cwd-relative `plugins/kk/scripts/kk-workflow-stages.ts` when that copy is absent — the checkout
|
|
34
34
|
* tier kk-storm-research's render has always relied on, preserved on purpose. The daily-voice
|
|
35
|
-
* workflow reaches the same script through `kk stage` (apps/cli/src/stage.ts), which
|
|
36
|
-
*
|
|
35
|
+
* workflow reaches the same script through `kk stage` (apps/cli/src/stage.ts), which resolves a
|
|
36
|
+
* recognized checkout entry to this checked-in copy (never a stale `apps/cli/plugins/` stage) and an
|
|
37
|
+
* installed entry to its own package-shipped copy.
|
|
37
38
|
*
|
|
38
39
|
* SYNC: plugins/kk/workflows/{kk-solo-podcast,kk-itc,kk-storm-research}.yaml shell this script
|
|
39
40
|
* (kk-daily-ai-voice goes through `kk stage`). Keep message substrings stable —
|
|
@@ -60,11 +61,39 @@ const SCRIPTS_DIR = import.meta.dir;
|
|
|
60
61
|
const VALIDATE_SIDECAR = 'validate-voicescript.ts';
|
|
61
62
|
const WRAP_SIDECAR = 'wrap-voicescript-doc.ts';
|
|
62
63
|
const RENDER_SIDECAR = 'render-md.ts';
|
|
64
|
+
// F613: the zh style contract the script-write prompt reads (snapshot stage) and dailynews-gen
|
|
65
|
+
// consumes (DAILYNEWS_STYLE_FILE); no workflow var carries an override, so the chain is
|
|
66
|
+
// $KK_WORKFLOWS_DIR -> package copy -> $HOME.
|
|
67
|
+
const STYLE_SIDECAR = 'daily-voice-style.zh.md';
|
|
63
68
|
const REVISE_COUNT = '.revise-count';
|
|
64
69
|
// Post-translation title-dedup threshold (task 0138 R3). ponytail: the 2026-09-16 Gemini 3.8 Live
|
|
65
70
|
// pair measures 0.417 zh title-only Jaccard, the nearest distinct pair 0.1 (4x margin). Character
|
|
66
71
|
// bigrams need same-language input — pre-translate dedup over the mixed-language blend cannot work.
|
|
67
72
|
const DEDUP_TITLE_THRESHOLD = 0.4;
|
|
73
|
+
// F614: the daily cover renders through the shipped daily-news presets; the brief's subject is
|
|
74
|
+
// the only per-run var. The format/style pins and the subject cap are part of the style
|
|
75
|
+
// contract — changing the look means editing the preset files, not this stage.
|
|
76
|
+
const COVER_FORMAT = 'daily-news-cover';
|
|
77
|
+
const COVER_STYLE = 'daily-news';
|
|
78
|
+
const COVER_SUBJECT_CAP = 240;
|
|
79
|
+
// E8: the XHS card series. Format and style are pinned exactly like the cover's; only the copy is
|
|
80
|
+
// model-authored. `XHS_MAX_CARDS` mirrors the format preset's own series guidance.
|
|
81
|
+
const XHS_FORMAT = 'xhs-card';
|
|
82
|
+
const XHS_MAX_CARDS = 10;
|
|
83
|
+
const XHS_TITLE_CAP = 20;
|
|
84
|
+
const XHS_BODY_CAP = 1000;
|
|
85
|
+
const XHS_ROLES: readonly string[] = ['cover', 'content', 'closing'];
|
|
86
|
+
|
|
87
|
+
/** One planned card, normalized: `index` zero-padded for the preset's artifact name. */
|
|
88
|
+
interface XhsCard {
|
|
89
|
+
index: string;
|
|
90
|
+
role: string;
|
|
91
|
+
copy: string;
|
|
92
|
+
points: string[];
|
|
93
|
+
layout: string;
|
|
94
|
+
subject: string;
|
|
95
|
+
sourcePoint: string;
|
|
96
|
+
}
|
|
68
97
|
|
|
69
98
|
function fail(message: string): never {
|
|
70
99
|
console.error(message);
|
|
@@ -414,6 +443,7 @@ const DAILY_DIRS = [
|
|
|
414
443
|
'2-plan',
|
|
415
444
|
'2-article',
|
|
416
445
|
'2-cover',
|
|
446
|
+
'2-xhs',
|
|
417
447
|
'2-script',
|
|
418
448
|
'3-audio',
|
|
419
449
|
'3-publish',
|
|
@@ -478,6 +508,12 @@ function pluginsArgs(pluginsPath: string): string[] {
|
|
|
478
508
|
}
|
|
479
509
|
|
|
480
510
|
/** `kk <args>` with the caller's env overrides; `soft` mirrors the shell's `|| true`. */
|
|
511
|
+
// spur --async exports SPUR_EXPECTED_DEFINITION_DIGEST (its own worker's identity binding) into
|
|
512
|
+
// every descendant env; a nested `spur workflow run` — postsurfing's publishing workflow under
|
|
513
|
+
// surfdash-pub — resolves a DIFFERENT digest and refuses to start. Strip both launcher vars at
|
|
514
|
+
// the nested-spur boundary (SPUR_WORKFLOW_RUN_ACTIVE is the older sibling guard, task 0133-era).
|
|
515
|
+
const NESTED_SPUR_UNSET = ['SPUR_WORKFLOW_RUN_ACTIVE', 'SPUR_EXPECTED_DEFINITION_DIGEST'] as const;
|
|
516
|
+
|
|
481
517
|
function kk(
|
|
482
518
|
args: string[],
|
|
483
519
|
options: { env?: Record<string, string>; unset?: readonly string[]; soft?: boolean } = {},
|
|
@@ -861,6 +897,76 @@ function dailyArticle(args: string[]): void {
|
|
|
861
897
|
});
|
|
862
898
|
}
|
|
863
899
|
|
|
900
|
+
/**
|
|
901
|
+
* F614 R2/R4: validate the agent-written cover brief in the order R4 names — the first failing
|
|
902
|
+
* check is the fallback reason; `reason: null` carries the trimmed subject.
|
|
903
|
+
*/
|
|
904
|
+
function coverBrief(briefPath: string): { reason: string | null; subject: string } {
|
|
905
|
+
if (!isFile(briefPath)) return { reason: 'missing-brief', subject: '' };
|
|
906
|
+
let parsed: unknown;
|
|
907
|
+
try {
|
|
908
|
+
parsed = JSON.parse(readFileSync(briefPath, 'utf-8'));
|
|
909
|
+
} catch {
|
|
910
|
+
return { reason: 'invalid-json', subject: '' };
|
|
911
|
+
}
|
|
912
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
913
|
+
return { reason: 'invalid-json', subject: '' };
|
|
914
|
+
}
|
|
915
|
+
const subject = (parsed as Record<string, unknown>).subject;
|
|
916
|
+
if (typeof subject !== 'string') return { reason: 'no-subject', subject: '' };
|
|
917
|
+
const trimmed = subject.trim();
|
|
918
|
+
if (trimmed === '' || trimmed.length > COVER_SUBJECT_CAP) return { reason: 'subject-cap', subject: '' };
|
|
919
|
+
return { reason: null, subject: trimmed };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* F614 R3: build the image-gen envelope from the day's cover brief and render it. The subject is
|
|
924
|
+
* the only model-authored input (the workflow's brief agent.run wrote it); format, style,
|
|
925
|
+
* provider and model are pinned by this stage so no caller can drift the look. A missing or
|
|
926
|
+
* invalid brief degrades to the format's default subject (R4) — a plugin failure still fails
|
|
927
|
+
* the stage, matching the pre-F614 cover behavior.
|
|
928
|
+
*/
|
|
929
|
+
function dailyCover(args: string[]): void {
|
|
930
|
+
const [workDir = '', runDate = '', pluginsPath = '', provider = '', model = '', fallback = ''] = args;
|
|
931
|
+
const briefPath = join(workDir, '2-cover', `${runDate}_07_cover-brief.json`);
|
|
932
|
+
const envPath = join(workDir, '2-cover', `${runDate}_07_cover_image-gen-in.json`);
|
|
933
|
+
const contentOut = join(workDir, '2-cover', 'content.json');
|
|
934
|
+
timed(workDir, 'cover', () => {
|
|
935
|
+
const brief = coverBrief(briefPath);
|
|
936
|
+
const vars: Record<string, string> = {};
|
|
937
|
+
if (brief.reason !== null) {
|
|
938
|
+
console.error(`cover brief: fallback (${brief.reason})`);
|
|
939
|
+
} else {
|
|
940
|
+
vars.subject = brief.subject;
|
|
941
|
+
}
|
|
942
|
+
const imageGen: Record<string, unknown> = {
|
|
943
|
+
job: 'cover',
|
|
944
|
+
format: COVER_FORMAT,
|
|
945
|
+
style: COVER_STYLE,
|
|
946
|
+
vars,
|
|
947
|
+
maxImages: 1,
|
|
948
|
+
};
|
|
949
|
+
if (provider !== '') imageGen.provider = provider;
|
|
950
|
+
if (model !== '') imageGen.model = model;
|
|
951
|
+
// D6 0166 R7: runtime-fallback opt-in. '' (the shipped var default) keeps strict fail-fast —
|
|
952
|
+
// F614's pinned look stays the default; 'true' = retry down AUTO_ORDER; anything else is a
|
|
953
|
+
// comma-split, trimmed candidate list. Unknown names are rejected by image-gen, not here.
|
|
954
|
+
if (fallback !== '') {
|
|
955
|
+
imageGen.fallback =
|
|
956
|
+
fallback === 'true'
|
|
957
|
+
? true
|
|
958
|
+
: fallback
|
|
959
|
+
.split(',')
|
|
960
|
+
.map((s) => s.trim())
|
|
961
|
+
.filter(Boolean);
|
|
962
|
+
}
|
|
963
|
+
sted('write', envPath, () =>
|
|
964
|
+
writeJsonText(envPath, [{ id: 'daily-cover', title: runDate, body: '', metadata: { imageGen } }]),
|
|
965
|
+
);
|
|
966
|
+
kk(['executor', 'run', 'image-gen', '--in', envPath, '--out', contentOut, ...pluginsArgs(pluginsPath)]);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
864
970
|
function dailyCoverNormalize(args: string[]): void {
|
|
865
971
|
const [workDir = '', runDate = ''] = args;
|
|
866
972
|
// The pre-0133 shell had no timing trap, so this stage is not `timed`; it still fails through
|
|
@@ -878,15 +984,26 @@ function dailyCoverNormalize(args: string[]): void {
|
|
|
878
984
|
}
|
|
879
985
|
|
|
880
986
|
function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
881
|
-
|
|
882
|
-
|
|
987
|
+
// F614 R5: the artifact name follows the format preset (`<stem>-<format>-NN.<ext>`,
|
|
988
|
+
// paths.ts), so normalize reads the path the plugin itself recorded in the manifest instead
|
|
989
|
+
// of a hard-coded name. The legacy content-cover-01.{png,jpg} paths stay as stale cleanup.
|
|
990
|
+
const legacyPng = join(workDir, '2-cover', 'content-cover-01.png');
|
|
991
|
+
const legacyJpg = join(workDir, '2-cover', 'content-cover-01.jpg');
|
|
883
992
|
// Must match the name dailyPublishPrep and the article->cover workflow guard read:
|
|
884
993
|
// `${runDate}_07_cover_cover.png` (a plain `cover-${runDate}.png` left options.coverPath
|
|
885
994
|
// undefined and silently dropped the cover from both podcast and surfdash — 2026-09-16 run).
|
|
886
995
|
const out = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
996
|
+
const images = metadataOf(readJson(join(workDir, '2-cover', 'content.json'))).images;
|
|
997
|
+
const first = Array.isArray(images) ? images[0] : undefined;
|
|
998
|
+
const recorded =
|
|
999
|
+
first !== null && typeof first === 'object' && typeof (first as Record<string, unknown>).path === 'string'
|
|
1000
|
+
? ((first as Record<string, unknown>).path as string)
|
|
1001
|
+
: '';
|
|
1002
|
+
if (recorded === '' || !isFile(recorded)) {
|
|
1003
|
+
failWith(1, 'cover normalize: no image artifact in 2-cover/content.json');
|
|
1004
|
+
}
|
|
1005
|
+
const source = recorded;
|
|
1006
|
+
if (source.endsWith('.jpg') || source.endsWith('.jpeg')) {
|
|
890
1007
|
const sips = spawnSync('sips', ['-s', 'format', 'png', source, '--out', out], { stdio: 'ignore' });
|
|
891
1008
|
// Bun reports a missing binary as `status: undefined` (older versions: `null`) plus an
|
|
892
1009
|
// `error` object — the pre-0133 shell's `command -v sips` failed the same way.
|
|
@@ -899,16 +1016,236 @@ function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
|
899
1016
|
} else {
|
|
900
1017
|
sted('mv', source, () => renameSync(source, out));
|
|
901
1018
|
}
|
|
902
|
-
for (const stale of [
|
|
1019
|
+
for (const stale of new Set([legacyJpg, legacyPng, source])) {
|
|
903
1020
|
try {
|
|
904
1021
|
unlinkSync(stale);
|
|
905
1022
|
} catch {
|
|
906
|
-
// `rm -f` is silent when the file is already gone.
|
|
1023
|
+
// `rm -f` is silent when the file is already gone (a renamed source lands here too).
|
|
907
1024
|
}
|
|
908
1025
|
}
|
|
909
1026
|
console.log(`cover normalized: ${runDate}_07_cover_cover.png`);
|
|
910
1027
|
}
|
|
911
1028
|
|
|
1029
|
+
/** `metadata.images[0]` — the artifact the image-gen plugin recorded for one render. */
|
|
1030
|
+
function firstArtifact(content: Record<string, unknown> | null): Record<string, unknown> | null {
|
|
1031
|
+
const images = metadataOf(content).images;
|
|
1032
|
+
const first = Array.isArray(images) ? images[0] : undefined;
|
|
1033
|
+
return first !== null && typeof first === 'object' ? (first as Record<string, unknown>) : null;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* E8: validate the agent-written XHS card plan. The plan is the only model-authored input —
|
|
1038
|
+
* format, style, provider and model are pinned by `dailyXhsRender`, matching F614's cover. Every
|
|
1039
|
+
* check fails loud naming the field, so an unusable plan skips the XHS target visibly instead of
|
|
1040
|
+
* publishing a partial card series (the owning state carries `onError: continue`).
|
|
1041
|
+
*/
|
|
1042
|
+
function xhsPlan(planPath: string, count: number): { title: string; body: string; cards: XhsCard[] } {
|
|
1043
|
+
const parsed = requireJson(planPath, 'xhs plan');
|
|
1044
|
+
const title = typeof parsed.title === 'string' ? parsed.title.trim() : '';
|
|
1045
|
+
if (title === '' || title.length > XHS_TITLE_CAP) {
|
|
1046
|
+
failWith(1, `xhs plan: title must be 1..${XHS_TITLE_CAP} chars, got ${title.length}`);
|
|
1047
|
+
}
|
|
1048
|
+
const body = typeof parsed.body === 'string' ? parsed.body.trim() : '';
|
|
1049
|
+
if (body === '' || body.length > XHS_BODY_CAP) {
|
|
1050
|
+
failWith(1, `xhs plan: body must be 1..${XHS_BODY_CAP} chars, got ${body.length}`);
|
|
1051
|
+
}
|
|
1052
|
+
const raw = parsed.cards;
|
|
1053
|
+
if (!Array.isArray(raw) || raw.length !== count) {
|
|
1054
|
+
failWith(1, `xhs plan: expected ${count} cards (xhs_cards), got ${Array.isArray(raw) ? raw.length : 0}`);
|
|
1055
|
+
}
|
|
1056
|
+
const cards = raw.map((entry, position): XhsCard => {
|
|
1057
|
+
const card = (entry !== null && typeof entry === 'object' ? entry : {}) as Record<string, unknown>;
|
|
1058
|
+
const label = `xhs plan: card ${position + 1}`;
|
|
1059
|
+
const text = (key: string): string => {
|
|
1060
|
+
const value = card[key];
|
|
1061
|
+
if (typeof value !== 'string' || value.trim() === '') failWith(1, `${label} needs a non-empty '${key}'`);
|
|
1062
|
+
return value.trim();
|
|
1063
|
+
};
|
|
1064
|
+
const role = text('role');
|
|
1065
|
+
if (!XHS_ROLES.includes(role)) {
|
|
1066
|
+
failWith(1, `${label} role must be one of ${XHS_ROLES.join('|')}, got '${role}'`);
|
|
1067
|
+
}
|
|
1068
|
+
if (position === 0 && role !== 'cover') failWith(1, `${label} must be role 'cover' (the series opens on it)`);
|
|
1069
|
+
const points = card.points;
|
|
1070
|
+
if (!Array.isArray(points) || points.length < 1 || points.length > 4) {
|
|
1071
|
+
failWith(1, `${label} needs 1..4 'points', got ${Array.isArray(points) ? points.length : 0}`);
|
|
1072
|
+
}
|
|
1073
|
+
const bullets = points.map((point) => {
|
|
1074
|
+
if (typeof point !== 'string' || point.trim() === '') failWith(1, `${label} has an empty point`);
|
|
1075
|
+
return point.trim();
|
|
1076
|
+
});
|
|
1077
|
+
return {
|
|
1078
|
+
index: `${position + 1}`.padStart(2, '0'),
|
|
1079
|
+
role,
|
|
1080
|
+
copy: text('copy'),
|
|
1081
|
+
points: bullets,
|
|
1082
|
+
layout: text('layout'),
|
|
1083
|
+
subject: text('subject'),
|
|
1084
|
+
sourcePoint: text('sourcePoint'),
|
|
1085
|
+
};
|
|
1086
|
+
});
|
|
1087
|
+
return { title, body, cards };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* E8: build one `image-gen` envelope per planned card, render them sequentially, and package the
|
|
1092
|
+
* ordered card manifest + `Content` that `xhs-pub` consumes. Format, style, provider and model
|
|
1093
|
+
* are pinned here (never model-authored), so the series cannot drift from its preset. Resume-safe:
|
|
1094
|
+
* a card whose recorded artifact is still on disk is not re-rendered — and not re-billed.
|
|
1095
|
+
*/
|
|
1096
|
+
function dailyXhsRender(args: string[]): void {
|
|
1097
|
+
const [workDir = '', runDate = '', pluginsPath = '', countArg = '', style = '', provider = '', model = ''] = args;
|
|
1098
|
+
const count = Number.parseInt(countArg, 10);
|
|
1099
|
+
const planPath = join(workDir, '2-xhs', `${runDate}_18_xhs_plan.json`);
|
|
1100
|
+
const cardsDir = join(workDir, '2-xhs', 'cards');
|
|
1101
|
+
const manifestPath = join(workDir, '2-xhs', 'cards.json');
|
|
1102
|
+
const contentPath = join(workDir, '2-xhs', 'content.json');
|
|
1103
|
+
timed(workDir, 'xhs-render', () => {
|
|
1104
|
+
if (!Number.isInteger(count) || count < 1 || count > XHS_MAX_CARDS) {
|
|
1105
|
+
failWith(1, `xhs render: xhs_cards must be 1..${XHS_MAX_CARDS}, got '${countArg}'`);
|
|
1106
|
+
}
|
|
1107
|
+
const plan = xhsPlan(planPath, count);
|
|
1108
|
+
sted('mkdir', cardsDir, () => mkdirSync(cardsDir, { recursive: true }));
|
|
1109
|
+
const images: Record<string, unknown>[] = [];
|
|
1110
|
+
const cards: Record<string, unknown>[] = [];
|
|
1111
|
+
for (const card of plan.cards) {
|
|
1112
|
+
const out = join(cardsDir, `card-${card.index}.json`);
|
|
1113
|
+
const promptPath = join(cardsDir, `card-${card.index}.prompt.txt`);
|
|
1114
|
+
let artifact = firstArtifact(readJson(out));
|
|
1115
|
+
if (artifact === null || !isFile(typeof artifact.path === 'string' ? artifact.path : '')) {
|
|
1116
|
+
const envPath = join(cardsDir, `card-${card.index}.in.json`);
|
|
1117
|
+
const imageGen: Record<string, unknown> = {
|
|
1118
|
+
job: 'free',
|
|
1119
|
+
format: XHS_FORMAT,
|
|
1120
|
+
style,
|
|
1121
|
+
vars: {
|
|
1122
|
+
role: card.role,
|
|
1123
|
+
layout: card.layout,
|
|
1124
|
+
headline: card.copy,
|
|
1125
|
+
points: card.points.join('\n'),
|
|
1126
|
+
subject: card.subject,
|
|
1127
|
+
language: '简体中文',
|
|
1128
|
+
index: card.index,
|
|
1129
|
+
},
|
|
1130
|
+
maxImages: 1,
|
|
1131
|
+
};
|
|
1132
|
+
if (provider !== '') imageGen.provider = provider;
|
|
1133
|
+
if (model !== '') imageGen.model = model;
|
|
1134
|
+
sted('write', envPath, () =>
|
|
1135
|
+
writeJsonText(envPath, [
|
|
1136
|
+
{ id: `xhs-card-${card.index}`, title: runDate, body: '', metadata: { imageGen } },
|
|
1137
|
+
]),
|
|
1138
|
+
);
|
|
1139
|
+
// Sequential on purpose: one provider call per card, and the first failure stops
|
|
1140
|
+
// the spend — later cards stay unrendered for a resume run to pick up.
|
|
1141
|
+
kk(['executor', 'run', 'image-gen', '--in', envPath, '--out', out, ...pluginsArgs(pluginsPath)]);
|
|
1142
|
+
artifact = firstArtifact(requireJson(out, 'xhs render'));
|
|
1143
|
+
}
|
|
1144
|
+
if (artifact === null) failWith(1, `xhs render: card ${card.index} recorded no image artifact (${out})`);
|
|
1145
|
+
const recorded = artifact.path;
|
|
1146
|
+
if (typeof recorded !== 'string' || !isFile(recorded)) {
|
|
1147
|
+
failWith(1, `xhs render: card ${card.index} image artifact is not on disk: '${String(recorded)}'`);
|
|
1148
|
+
}
|
|
1149
|
+
// `image-gen` writes the artifact beside `--out`; the publisher requires absolute paths.
|
|
1150
|
+
const imagePath = resolve(recorded);
|
|
1151
|
+
sted('write', promptPath, () =>
|
|
1152
|
+
writeRaw(promptPath, typeof artifact.prompt === 'string' ? artifact.prompt : ''),
|
|
1153
|
+
);
|
|
1154
|
+
images.push({ ...artifact, path: imagePath });
|
|
1155
|
+
cards.push({
|
|
1156
|
+
index: card.index,
|
|
1157
|
+
role: card.role,
|
|
1158
|
+
sourcePoint: card.sourcePoint,
|
|
1159
|
+
copy: card.copy,
|
|
1160
|
+
layout: card.layout,
|
|
1161
|
+
promptPath,
|
|
1162
|
+
imagePath,
|
|
1163
|
+
status: 'rendered',
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
sted('write', manifestPath, () => writeJsonText(manifestPath, { source: planPath, cards }));
|
|
1167
|
+
sted('write', contentPath, () =>
|
|
1168
|
+
writeJsonText(contentPath, {
|
|
1169
|
+
title: plan.title,
|
|
1170
|
+
body: plan.body,
|
|
1171
|
+
format: 'text',
|
|
1172
|
+
metadata: { images, publishStatus: 'draft' },
|
|
1173
|
+
}),
|
|
1174
|
+
);
|
|
1175
|
+
console.log(`xhs render: ${cards.length} cards packaged in card order (${contentPath})`);
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** A single-target xhs Result, not the fan-out aggregate: `ok` plus `metadata.publishStatus`. */
|
|
1180
|
+
function xhsStatus(path: string): string {
|
|
1181
|
+
const parsed = readJson(path);
|
|
1182
|
+
if (parsed?.ok !== true) return '';
|
|
1183
|
+
const status = metadataOf(parsed).publishStatus;
|
|
1184
|
+
return status === 'public' || status === 'draft' ? status : '';
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* E8: publish the packaged card series through `xhs-pub`'s script driver. Draft by default; live
|
|
1189
|
+
* only on an explicit launch-time `xhs_live=true` (ADR-022 — an unattended run never posts live on
|
|
1190
|
+
* its own). Non-blocking by operator decision: a missing/expired XHS session or a changed console
|
|
1191
|
+
* logs a loud failure and leaves the run alive, and the outcome never enters `publish_status`
|
|
1192
|
+
* (surfdash + podcast remain the episode's publication contract).
|
|
1193
|
+
*/
|
|
1194
|
+
function dailyPublishXhs(args: string[]): void {
|
|
1195
|
+
const [workDir = '', runDate = '', pluginsPath = '', live = 'false'] = args;
|
|
1196
|
+
const content = join(workDir, '2-xhs', 'content.json');
|
|
1197
|
+
const input = join(workDir, '3-publish', `${runDate}_19_publish-xhs_content.json`);
|
|
1198
|
+
const result = join(workDir, '3-publish', `${runDate}_19_publish-xhs_result.json`);
|
|
1199
|
+
const want = live === 'true' ? 'public' : 'draft';
|
|
1200
|
+
timed(workDir, 'publish-xhs', () => {
|
|
1201
|
+
// Cache on the *requested* status: a cached draft must not silently satisfy a later
|
|
1202
|
+
// xhs_live=true re-run (that would report a live post that never happened).
|
|
1203
|
+
if (xhsStatus(result) === want) {
|
|
1204
|
+
console.log(`publish-xhs cached: ${want} already recorded in ${result}`);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (!isFile(content)) {
|
|
1208
|
+
console.log(`publish-xhs: no card series at ${content} (render packaged none) — skip`);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const packaged = requireJson(content, 'publish-xhs');
|
|
1212
|
+
sted('write', input, () =>
|
|
1213
|
+
writeJsonText(input, { ...packaged, metadata: { ...metadataOf(packaged), publishStatus: want } }),
|
|
1214
|
+
);
|
|
1215
|
+
// `soft` keeps the XHS target non-blocking: the plugin still writes its own failed Result
|
|
1216
|
+
// and prints the failing step (login gate, upload, body, submit control), which is what the
|
|
1217
|
+
// operator needs to re-run — never a silent skip.
|
|
1218
|
+
const status = kk(['executor', 'run', 'xhs-pub', '--in', input, '--out', result, ...pluginsArgs(pluginsPath)], {
|
|
1219
|
+
soft: true,
|
|
1220
|
+
});
|
|
1221
|
+
console.log(
|
|
1222
|
+
status === 0
|
|
1223
|
+
? `publish-xhs: ${want} dispatched; outcome recorded in ${result}`
|
|
1224
|
+
: `WARNING: publish-xhs — xhs-pub exited ${status}; XHS target skipped (run continues, see ${result})`,
|
|
1225
|
+
);
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* F613 R1: snapshot the shipped style contract into the run's work dir. Spur workflows have no
|
|
1231
|
+
* workflow-dir var, so the script-write prompt reads this copy — and the snapshot freezes the
|
|
1232
|
+
* exact contract the run's plan was written against. Idempotent (an existing snapshot is kept);
|
|
1233
|
+
* a missing sidecar is a broken install, so `requireSidecar` fails loud.
|
|
1234
|
+
*/
|
|
1235
|
+
function dailyScriptStyle(args: string[]): void {
|
|
1236
|
+
const [workDir = '', runDate = ''] = args;
|
|
1237
|
+
const snapshot = join(workDir, '2-script', `${runDate}_08_script-write_style.md`);
|
|
1238
|
+
if (isFile(snapshot)) {
|
|
1239
|
+
console.log(`script style cached: ${snapshot}`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const sidecar = requireSidecar('', STYLE_SIDECAR, 'daily-voice-style');
|
|
1243
|
+
const scriptDir = join(workDir, '2-script');
|
|
1244
|
+
sted('mkdir', scriptDir, () => mkdirSync(scriptDir, { recursive: true }));
|
|
1245
|
+
sted('cp', snapshot, () => copyFileSync(sidecar, snapshot));
|
|
1246
|
+
console.log(`script style snapshot: ${snapshot}`);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
912
1249
|
function dailyScript(args: string[]): void {
|
|
913
1250
|
const [workDir = '', runDate = '', pluginsPath = '', language = '', title = '', voiceProfile = ''] = args;
|
|
914
1251
|
const yamlOut = join(workDir, '2-script', `${runDate}_08_script_voicescript.yaml`);
|
|
@@ -920,7 +1257,16 @@ function dailyScript(args: string[]): void {
|
|
|
920
1257
|
return;
|
|
921
1258
|
}
|
|
922
1259
|
kk(['executor', 'run', 'dailynews-gen', '--in', planIn, '--out', contentOut, ...pluginsArgs(pluginsPath)], {
|
|
923
|
-
env: {
|
|
1260
|
+
env: {
|
|
1261
|
+
DAILYNEWS_LANGUAGE: language,
|
|
1262
|
+
DAILYNEWS_TITLE: title,
|
|
1263
|
+
VOICEBOX_DEFAULT_PROFILE: voiceProfile,
|
|
1264
|
+
// F613 R4: always passed as absolute paths — spawnPluginEntry runs plugins with
|
|
1265
|
+
// cwd = the plugin package dir, so a relative workDir would silently miss the
|
|
1266
|
+
// plan; a plan file absent at build time reports missing-plan (template fallback).
|
|
1267
|
+
DAILYNEWS_SCRIPT_PLAN: resolve(join(workDir, '2-script', `${runDate}_08_script-write_plan.json`)),
|
|
1268
|
+
DAILYNEWS_STYLE_FILE: resolve(resolveSidecar('', STYLE_SIDECAR)),
|
|
1269
|
+
},
|
|
924
1270
|
});
|
|
925
1271
|
const body = readJson(contentOut)?.body;
|
|
926
1272
|
if (typeof body !== 'string') failWith(1, `script: ${contentOut} has no string body`);
|
|
@@ -965,7 +1311,11 @@ function dailyGenerate(args: string[]): void {
|
|
|
965
1311
|
const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '', vdriver = ''] = args;
|
|
966
1312
|
// 20260917 review: the workflow picks a voice driver by short name; both drivers' envs are
|
|
967
1313
|
// passed unconditionally so switching `vdriver` needs no shell changes. Unknown -> fail loud.
|
|
968
|
-
const VOICE_DRIVERS: Record<string, string> = {
|
|
1314
|
+
const VOICE_DRIVERS: Record<string, string> = {
|
|
1315
|
+
ominivoice: 'omni-voice-gen',
|
|
1316
|
+
voicebox: 'voice-gen',
|
|
1317
|
+
gemini: 'gemini-tts',
|
|
1318
|
+
};
|
|
969
1319
|
const voiceGenerator = VOICE_DRIVERS[vdriver];
|
|
970
1320
|
if (!voiceGenerator) {
|
|
971
1321
|
failWith(
|
|
@@ -1008,15 +1358,22 @@ function dailyIntro(args: string[]): void {
|
|
|
1008
1358
|
console.log(`intro-music cached: ${contentPath}`);
|
|
1009
1359
|
return;
|
|
1010
1360
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1361
|
+
// Merge from the raw lossless render; mp3Path is the derived publish artifact.
|
|
1362
|
+
// Preferring mp3Path here made `<stem>.mp3` the concat input while the output
|
|
1363
|
+
// suffix rewrite (`.wav` -> `.intro.wav`) no longer matched, so ffmpeg saw one
|
|
1364
|
+
// path as both input #1 and output and aborted with "same as Input #1".
|
|
1365
|
+
const candidates = [meta.audioPath, meta.mp3Path].filter(
|
|
1366
|
+
(candidate): candidate is string => typeof candidate === 'string' && candidate !== '',
|
|
1367
|
+
);
|
|
1368
|
+
const rawFile = candidates.find((candidate) => existsSync(resolve(candidate))) ?? candidates[0];
|
|
1369
|
+
if (rawFile === undefined) {
|
|
1013
1370
|
failWith(1, 'intro-music: voice content carries no mp3Path/audioPath');
|
|
1014
1371
|
}
|
|
1015
1372
|
const introPath = resolve(introSrc);
|
|
1016
1373
|
if (!existsSync(introPath)) failWith(1, `intro-music: intro file not found: ${introPath}`);
|
|
1017
1374
|
const mainPath = resolve(rawFile);
|
|
1018
1375
|
if (!existsSync(mainPath)) failWith(1, `intro-music: generated audio not found: ${mainPath}`);
|
|
1019
|
-
const mergedPath = mainPath.replace(/\.wav
|
|
1376
|
+
const mergedPath = `${mainPath.replace(/\.(wav|mp3)$/i, '')}.intro.wav`;
|
|
1020
1377
|
const probe = (file: string, entries: string): string =>
|
|
1021
1378
|
spawnSync('ffprobe', [
|
|
1022
1379
|
'-v',
|
|
@@ -1110,8 +1467,50 @@ function dailyQualityReport(args: string[]): void {
|
|
|
1110
1467
|
});
|
|
1111
1468
|
}
|
|
1112
1469
|
|
|
1470
|
+
/**
|
|
1471
|
+
* 0171 R3: Apple Podcasts reads the enclosure's ID3 tags, so the published MP3 carries
|
|
1472
|
+
* title/artist/album/date. `-c copy` moves the audio frames without a re-encode, and the temp
|
|
1473
|
+
* file + rename keeps a failed ffmpeg from truncating the episode in place. An empty value
|
|
1474
|
+
* omits that tag rather than failing. The caller runs this after intro-music, whose mp3
|
|
1475
|
+
* re-transcode would otherwise strip tags stamped earlier.
|
|
1476
|
+
*/
|
|
1477
|
+
function stampId3Tags(audioFile: string, title: string, runDate: string, artist: string, album: string): void {
|
|
1478
|
+
const tmpFile = `${audioFile}.id3.tmp.mp3`;
|
|
1479
|
+
const metadata: string[] = [];
|
|
1480
|
+
const addTag = (name: string, value: string): void => {
|
|
1481
|
+
if (value.trim() !== '') metadata.push('-metadata', `${name}=${value}`);
|
|
1482
|
+
};
|
|
1483
|
+
addTag('title', title);
|
|
1484
|
+
addTag('artist', artist);
|
|
1485
|
+
addTag('album', album);
|
|
1486
|
+
// run_date is a digits-only stamp (`20260924`); ID3 wants `YYYY-MM-DD`.
|
|
1487
|
+
const isoDate = /^\d{8}$/.test(runDate)
|
|
1488
|
+
? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`
|
|
1489
|
+
: runDate;
|
|
1490
|
+
addTag('date', isoDate);
|
|
1491
|
+
const stamp = spawnSync('ffmpeg', [
|
|
1492
|
+
'-y',
|
|
1493
|
+
'-hide_banner',
|
|
1494
|
+
'-loglevel',
|
|
1495
|
+
'error',
|
|
1496
|
+
'-i',
|
|
1497
|
+
audioFile,
|
|
1498
|
+
'-c',
|
|
1499
|
+
'copy',
|
|
1500
|
+
'-id3v2_version',
|
|
1501
|
+
'3',
|
|
1502
|
+
...metadata,
|
|
1503
|
+
tmpFile,
|
|
1504
|
+
]);
|
|
1505
|
+
if (stamp.status !== 0 || !existsSync(tmpFile)) {
|
|
1506
|
+
if (existsSync(tmpFile)) unlinkSync(tmpFile); // never leave a partial file beside the episode
|
|
1507
|
+
failWith(1, `publish-prep: ffmpeg ID3 stamp failed: ${stamp.stderr?.toString().trim()}`);
|
|
1508
|
+
}
|
|
1509
|
+
sted('rename', audioFile, () => renameSync(tmpFile, audioFile));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1113
1512
|
function dailyPublishPrep(args: string[]): void {
|
|
1114
|
-
const [workDir = '', runDate = ''] = args;
|
|
1513
|
+
const [workDir = '', runDate = '', artist = '', album = ''] = args;
|
|
1115
1514
|
const articlePath = join(workDir, '2-article', `${runDate}_06_article_content.json`);
|
|
1116
1515
|
const audioPath = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
1117
1516
|
const prepPath = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
|
|
@@ -1124,6 +1523,12 @@ function dailyPublishPrep(args: string[]): void {
|
|
|
1124
1523
|
failWith(1, 'publish-prep: voice content carries no mp3Path/audioPath');
|
|
1125
1524
|
}
|
|
1126
1525
|
const audioFile = resolve(rawFile);
|
|
1526
|
+
// 0171 R2: publish-prep publishes metadata.mp3Path ?? audioPath, so a WAV here means
|
|
1527
|
+
// transcode_mp3 was turned off — the run would ship an enclosure Apple Podcasts rejects.
|
|
1528
|
+
if (!audioFile.endsWith('.mp3')) {
|
|
1529
|
+
failWith(1, `publish-prep: resolved audio is not an MP3 (transcode_mp3 must be "true"): ${audioFile}`);
|
|
1530
|
+
}
|
|
1531
|
+
stampId3Tags(audioFile, typeof article.title === 'string' ? article.title : '', runDate, artist, album);
|
|
1127
1532
|
const resolvedCover = resolve(coverPath);
|
|
1128
1533
|
const hasCover = existsSync(resolvedCover);
|
|
1129
1534
|
// podcast-pub's documented contract reads metadata.coverImage (map.ts); options.coverPath
|
|
@@ -1168,7 +1573,7 @@ function dailyPublishSurfdash(args: string[]): void {
|
|
|
1168
1573
|
...pluginsArgs(pluginsPath),
|
|
1169
1574
|
],
|
|
1170
1575
|
{
|
|
1171
|
-
unset:
|
|
1576
|
+
unset: NESTED_SPUR_UNSET,
|
|
1172
1577
|
soft: true,
|
|
1173
1578
|
},
|
|
1174
1579
|
);
|
|
@@ -1487,6 +1892,8 @@ function dailyRunReport(args: string[]): void {
|
|
|
1487
1892
|
// 20260917 funnel: the blended fan-in input, rendered as the 语料构成
|
|
1488
1893
|
// source/category composition (absent -> omitted).
|
|
1489
1894
|
NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '1-ingest', `${runDate}_02_collect_blended.json`),
|
|
1895
|
+
// F613 R5: the script stage's Content, read best-effort (absent -> 脚本来源 omitted).
|
|
1896
|
+
NEWS_REPORT_SCRIPT_FILE: join(absWorkDir, '2-script', `${runDate}_08_script_content.json`),
|
|
1490
1897
|
NEWS_REPORT_DATE: runDate,
|
|
1491
1898
|
},
|
|
1492
1899
|
});
|
|
@@ -1576,7 +1983,9 @@ function dailyTranslateSync(args: string[]): void {
|
|
|
1576
1983
|
sted('write', sync, () =>
|
|
1577
1984
|
writeJqText(sync, { body: '', options: { operation: 'sync', postPath: zhPostPath } }),
|
|
1578
1985
|
);
|
|
1579
|
-
kk(['executor', 'run', 'surfdash-pub', '--in', sync, '--out', syncResult, ...pluginsArgs(pluginsPath)]
|
|
1986
|
+
kk(['executor', 'run', 'surfdash-pub', '--in', sync, '--out', syncResult, ...pluginsArgs(pluginsPath)], {
|
|
1987
|
+
unset: NESTED_SPUR_UNSET,
|
|
1988
|
+
});
|
|
1580
1989
|
});
|
|
1581
1990
|
}
|
|
1582
1991
|
|
|
@@ -1650,9 +2059,18 @@ if (import.meta.main)
|
|
|
1650
2059
|
case 'daily-article':
|
|
1651
2060
|
dailyArticle(rest);
|
|
1652
2061
|
break;
|
|
2062
|
+
case 'daily-cover':
|
|
2063
|
+
dailyCover(rest);
|
|
2064
|
+
break;
|
|
1653
2065
|
case 'daily-cover-normalize':
|
|
1654
2066
|
dailyCoverNormalize(rest);
|
|
1655
2067
|
break;
|
|
2068
|
+
case 'daily-xhs-render':
|
|
2069
|
+
dailyXhsRender(rest);
|
|
2070
|
+
break;
|
|
2071
|
+
case 'daily-script-style':
|
|
2072
|
+
dailyScriptStyle(rest);
|
|
2073
|
+
break;
|
|
1656
2074
|
case 'daily-script':
|
|
1657
2075
|
dailyScript(rest);
|
|
1658
2076
|
break;
|
|
@@ -1680,6 +2098,9 @@ if (import.meta.main)
|
|
|
1680
2098
|
case 'daily-publish-podcast':
|
|
1681
2099
|
dailyPublishPodcast(rest);
|
|
1682
2100
|
break;
|
|
2101
|
+
case 'daily-publish-xhs':
|
|
2102
|
+
dailyPublishXhs(rest);
|
|
2103
|
+
break;
|
|
1683
2104
|
case 'daily-publish-classify':
|
|
1684
2105
|
dailyPublishClassify(rest);
|
|
1685
2106
|
break;
|
|
@@ -28,11 +28,34 @@ not add a `kk publish` noun.
|
|
|
28
28
|
| juejin | `juejin-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
29
29
|
| infoq | `infoq-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
30
30
|
| substack | `substack-pub` | `browser` / `agent` | workflow `agent.run` |
|
|
31
|
-
| xhs | `xhs-pub` | `browser` / `
|
|
31
|
+
| xhs | `xhs-pub` | `browser` / `script` | `kk executor run xhs-pub --in <content.json> --out <result.json>` — its default `script` driver uploads the ordered card images with Playwright `setInputFiles`; the `agent` driver fails loud for this channel (ADR-024) |
|
|
32
32
|
|
|
33
33
|
Already-harvested file-cli channels (`qiita-pub`, `zenn-pub`, `surfdash-pub`,
|
|
34
34
|
`emdash-pub`, `podcast-pub`) compose the same way.
|
|
35
35
|
|
|
36
|
+
## XHS profile setup (0177)
|
|
37
|
+
|
|
38
|
+
`xhs-pub` reuses the signed-in session at `~/.config/kk/browser-profiles/xhs`; kk stores no
|
|
39
|
+
credentials. Before the first script-driver run (and after any session expiry), do a one-time
|
|
40
|
+
headed login, then verify reuse read-only:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bun plugins/publishings/xhs-pub/src/profile-check.ts login # headed; sign in once in the window
|
|
44
|
+
bun plugins/publishings/xhs-pub/src/profile-check.ts check # headless; exit 0 = session verified
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`check` never signs in — with no saved profile it refuses (exit 1) instead of reporting success.
|
|
48
|
+
Overrides: `--profile <dir>`, `--timeout <ms>`; env fallback `XHS_PROFILE_DIR` / `KK_BROWSER_PROFILE_DIR`.
|
|
49
|
+
|
|
50
|
+
**ADR-025 (2026-09-28, after an XHS warning about automated activity).** Publish now runs this same
|
|
51
|
+
check as a **preflight** before opening anything headed — so a missing or expired session is a fast
|
|
52
|
+
refusal (`ok: false`) rather than a headed window sitting on the login gate for 120 s — and both
|
|
53
|
+
console visits are capped at **one headed publish cycle + one session check per 24 h**
|
|
54
|
+
(`~/.config/kk/xhs-automation.json`, override `KK_XHS_AUTOMATION_STATE`; a manual `check` draws on
|
|
55
|
+
the same `check` budget). A capped or sessionless publish fails with `ok: false` and a named reason.
|
|
56
|
+
`login` is never automated and never capped. Do not schedule XHS publishing: publish deliberately,
|
|
57
|
+
by hand, at most once a day, and prefer a draft.
|
|
58
|
+
|
|
36
59
|
## Do not
|
|
37
60
|
|
|
38
61
|
- Embed Playwright selectors or reimplement a transport here.
|