@gobing-ai/knowledge-kit 0.1.13 → 0.1.15
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 +796 -176
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +2 -2
- package/plugins/generations/core-facts-gen/dist/index.js +2 -2
- package/plugins/generations/daily-article-gen/dist/index.js +2 -2
- package/plugins/generations/dailynews-gen/dist/index.js +72 -9
- package/plugins/generations/dailynews-gen/src/index.ts +23 -1
- package/plugins/generations/dailynews-gen/src/script-plan.ts +103 -5
- package/plugins/generations/episode-plan-gen/dist/index.js +2 -2
- package/plugins/generations/gemini-tts/dist/index.js +2 -2
- package/plugins/generations/image-gen/dist/index.js +2 -2
- package/plugins/generations/news-report-gen/dist/index.js +2 -2
- package/plugins/generations/voice-gen/dist/index.js +2 -2
- package/plugins/generations/voxcpm-tts/dist/index.js +2 -2
- package/plugins/ingestions/aihot-ingest/dist/index.js +2 -2
- package/plugins/ingestions/horizon-ingest/dist/index.js +2 -2
- package/plugins/ingestions/last30days-ingest/dist/index.js +2 -2
- package/plugins/ingestions/web-search/dist/index.js +2 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-scheduled-run.ts +53 -5
- package/plugins/kk/scripts/daily-stages.ts +476 -12
- package/plugins/kk/scripts/kk-workflow-stages.ts +20 -4
- package/plugins/kk/skills/content-judge/SKILL.md +5 -4
- package/plugins/kk/skills/content-judge/references/rubrics.md +14 -0
- package/plugins/kk/skills/news-editorial/SKILL.md +122 -0
- package/plugins/kk/skills/news-editorial/references/selection.md +47 -0
- package/plugins/kk/skills/news-editorial/references/style-contract.zh.md +99 -0
- package/plugins/kk/skills/publish/SKILL.md +3 -3
- package/plugins/kk/skills/web-automation/SKILL.md +229 -77
- package/plugins/kk/skills/web-automation/references/browser-launch.md +127 -36
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +4 -6
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +183 -222
- package/plugins/kk/workflows/kk-daily-publish3.yaml +16 -8
- package/plugins/publishings/bilibili-pub/dist/index.js +5 -3
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +5 -3
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/csdn-pub/dist/index.js +5 -3
- package/plugins/publishings/csdn-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/emdash-pub/dist/index.js +2 -2
- package/plugins/publishings/infoq-pub/dist/index.js +5 -3
- package/plugins/publishings/infoq-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/jike-pub/dist/index.js +5 -3
- package/plugins/publishings/jike-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/juejin-pub/dist/index.js +5 -3
- package/plugins/publishings/juejin-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/linkedin-pub/dist/index.js +5 -3
- package/plugins/publishings/linkedin-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/medium-pub/dist/index.js +2 -2
- package/plugins/publishings/podcast-pub/dist/index.js +147 -4
- package/plugins/publishings/podcast-pub/src/index.ts +71 -2
- package/plugins/publishings/podcast-pub/src/map.ts +3 -0
- package/plugins/publishings/podcast-pub/src/transcript.ts +195 -0
- package/plugins/publishings/qiita-pub/dist/index.js +2 -2
- package/plugins/publishings/shipinhao-pub/dist/index.js +5 -3
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/sspai-pub/dist/index.js +5 -3
- package/plugins/publishings/sspai-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/substack-pub/dist/index.js +5 -3
- package/plugins/publishings/substack-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/surfdash-pub/dist/index.js +2 -2
- package/plugins/publishings/threads-pub/dist/index.js +2 -2
- package/plugins/publishings/wechatmp-pub/dist/index.js +52 -33
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +70 -33
- package/plugins/publishings/weibo-pub/dist/index.js +21 -6
- package/plugins/publishings/weibo-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/weibo-pub/src/index.ts +31 -7
- package/plugins/publishings/x-pub/dist/index.js +5 -3
- package/plugins/publishings/x-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/xhs-pub/dist/index.js +5 -3
- package/plugins/publishings/xhs-pub/dist/profile-check.js +5 -3
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +5 -3
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +3 -1
- package/plugins/publishings/zenn-pub/dist/index.js +2 -2
- package/plugins/kk/scripts/daily-voice-style.zh.md +0 -70
|
@@ -11985,9 +11985,9 @@ var init_file_system_node = () => {};
|
|
|
11985
11985
|
|
|
11986
11986
|
// ../../plugins/publishings/podcast-pub/src/index.ts
|
|
11987
11987
|
import { createHash } from "crypto";
|
|
11988
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
11988
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
11989
11989
|
import { readFile } from "fs/promises";
|
|
11990
|
-
import { basename, dirname as dirname2, extname as extname2 } from "path";
|
|
11990
|
+
import { basename, dirname as dirname2, extname as extname2, join } from "path";
|
|
11991
11991
|
import { parseArgs } from "util";
|
|
11992
11992
|
|
|
11993
11993
|
// ../../packages/kk-core/src/content-limits.ts
|
|
@@ -12231,11 +12231,11 @@ var PluginManifestSchemaBase = looseObject({
|
|
|
12231
12231
|
});
|
|
12232
12232
|
var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
|
|
12233
12233
|
if (manifest.browserSession !== undefined) {
|
|
12234
|
-
if (manifest.kind !== "publish") {
|
|
12234
|
+
if (manifest.kind !== "publish" && manifest.kind !== "ingestion") {
|
|
12235
12235
|
ctx.addIssue({
|
|
12236
12236
|
code: "custom",
|
|
12237
12237
|
path: ["browserSession"],
|
|
12238
|
-
message: `browserSession requires kind "publish", not "${manifest.kind}"`
|
|
12238
|
+
message: `browserSession requires kind "publish" or "ingestion", not "${manifest.kind}"`
|
|
12239
12239
|
});
|
|
12240
12240
|
}
|
|
12241
12241
|
if (manifest.check === undefined) {
|
|
@@ -14155,6 +14155,101 @@ function mapContentToEpisode(content) {
|
|
|
14155
14155
|
};
|
|
14156
14156
|
}
|
|
14157
14157
|
|
|
14158
|
+
// ../../plugins/publishings/podcast-pub/src/transcript.ts
|
|
14159
|
+
function isMeasured(value) {
|
|
14160
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
14161
|
+
}
|
|
14162
|
+
function buildTranscriptSegments(segments) {
|
|
14163
|
+
if (segments.length === 0)
|
|
14164
|
+
return;
|
|
14165
|
+
for (const segment of segments) {
|
|
14166
|
+
if (!isMeasured(segment.startSeconds) || segment.startSeconds < 0)
|
|
14167
|
+
return;
|
|
14168
|
+
const text = typeof segment.transcription === "string" ? segment.transcription.trim() : "";
|
|
14169
|
+
if (text.length === 0)
|
|
14170
|
+
return;
|
|
14171
|
+
}
|
|
14172
|
+
for (let index = 1;index < segments.length; index += 1) {
|
|
14173
|
+
if (segments[index].startSeconds <= segments[index - 1].startSeconds) {
|
|
14174
|
+
return;
|
|
14175
|
+
}
|
|
14176
|
+
}
|
|
14177
|
+
const last = segments[segments.length - 1];
|
|
14178
|
+
if (!isMeasured(last.endSeconds) || last.endSeconds <= last.startSeconds)
|
|
14179
|
+
return;
|
|
14180
|
+
return segments.map((segment, index) => ({
|
|
14181
|
+
start: segment.startSeconds,
|
|
14182
|
+
end: index === segments.length - 1 ? segment.endSeconds : segments[index + 1].startSeconds,
|
|
14183
|
+
text: segment.transcription.trim()
|
|
14184
|
+
}));
|
|
14185
|
+
}
|
|
14186
|
+
function splitTimestamp(seconds) {
|
|
14187
|
+
const totalMs = Math.round((Number.isFinite(seconds) && seconds > 0 ? seconds : 0) * 1000);
|
|
14188
|
+
return {
|
|
14189
|
+
hours: Math.floor(totalMs / 3600000),
|
|
14190
|
+
minutes: String(Math.floor(totalMs % 3600000 / 60000)).padStart(2, "0"),
|
|
14191
|
+
secs: String(Math.floor(totalMs % 60000 / 1000)).padStart(2, "0"),
|
|
14192
|
+
fraction: String(totalMs % 1000).padStart(3, "0")
|
|
14193
|
+
};
|
|
14194
|
+
}
|
|
14195
|
+
function vttTimestamp(seconds) {
|
|
14196
|
+
const t = splitTimestamp(seconds);
|
|
14197
|
+
return `${String(t.hours).padStart(2, "0")}:${t.minutes}:${t.secs}.${t.fraction}`;
|
|
14198
|
+
}
|
|
14199
|
+
function escapeVttText(text) {
|
|
14200
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replace(/\r+/g, "");
|
|
14201
|
+
}
|
|
14202
|
+
function renderWebVtt(cues) {
|
|
14203
|
+
const lines = ["WEBVTT", ""];
|
|
14204
|
+
for (const cue of cues) {
|
|
14205
|
+
lines.push(`${vttTimestamp(cue.start)} --> ${vttTimestamp(cue.end)}`, escapeVttText(cue.text), "");
|
|
14206
|
+
}
|
|
14207
|
+
return lines.join(`
|
|
14208
|
+
`);
|
|
14209
|
+
}
|
|
14210
|
+
function podcastRecord(metadata) {
|
|
14211
|
+
if (metadata !== null && typeof metadata === "object") {
|
|
14212
|
+
const podcast = metadata.podcast;
|
|
14213
|
+
if (podcast !== null && typeof podcast === "object" && !Array.isArray(podcast)) {
|
|
14214
|
+
return podcast;
|
|
14215
|
+
}
|
|
14216
|
+
}
|
|
14217
|
+
return {};
|
|
14218
|
+
}
|
|
14219
|
+
function pick3(metadata, key, envValue) {
|
|
14220
|
+
if (Object.hasOwn(metadata, key)) {
|
|
14221
|
+
const value = metadata[key];
|
|
14222
|
+
if (value !== undefined && value !== "")
|
|
14223
|
+
return value;
|
|
14224
|
+
}
|
|
14225
|
+
return envValue !== undefined && envValue !== "" ? envValue : undefined;
|
|
14226
|
+
}
|
|
14227
|
+
function resolveTranscriptConfig(metadata, env = process.env) {
|
|
14228
|
+
const podcast = podcastRecord(metadata);
|
|
14229
|
+
const rawEnabled = pick3(podcast, "transcriptEnabled", env.PODCAST_PUB_TRANSCRIPT);
|
|
14230
|
+
const enabled = rawEnabled === true || rawEnabled === "true" || rawEnabled === "1";
|
|
14231
|
+
if (rawEnabled !== undefined && !enabled) {
|
|
14232
|
+
if (rawEnabled !== false && rawEnabled !== "false" && rawEnabled !== "0") {
|
|
14233
|
+
return {
|
|
14234
|
+
error: `invalid transcript enable flag "${String(rawEnabled)}": expected PODCAST_PUB_TRANSCRIPT or metadata.podcast.transcriptEnabled to be true or false`
|
|
14235
|
+
};
|
|
14236
|
+
}
|
|
14237
|
+
}
|
|
14238
|
+
const rawUrl = pick3(podcast, "transcriptUrl", env.PODCAST_PUB_TRANSCRIPT_URL);
|
|
14239
|
+
if (rawUrl === undefined)
|
|
14240
|
+
return { config: { enabled } };
|
|
14241
|
+
const url = typeof rawUrl === "string" ? rawUrl.trim() : String(rawUrl).trim();
|
|
14242
|
+
if (!url.startsWith("https://")) {
|
|
14243
|
+
return {
|
|
14244
|
+
error: `invalid transcript URL "${url}": microfeed hosts only HTTPS transcript files (set metadata.podcast.transcriptUrl or PODCAST_PUB_TRANSCRIPT_URL to an https:// URL)`
|
|
14245
|
+
};
|
|
14246
|
+
}
|
|
14247
|
+
return { config: { enabled, url } };
|
|
14248
|
+
}
|
|
14249
|
+
function transcriptReferences(url) {
|
|
14250
|
+
return [{ url, type: "text/vtt", language: "zh-cn" }];
|
|
14251
|
+
}
|
|
14252
|
+
|
|
14158
14253
|
// ../../plugins/publishings/podcast-pub/src/index.ts
|
|
14159
14254
|
function publicItemUrl(item, itemId, client) {
|
|
14160
14255
|
const microfeed = item?._microfeed;
|
|
@@ -14301,6 +14396,48 @@ function resolveAssemblyOptions(options) {
|
|
|
14301
14396
|
}
|
|
14302
14397
|
return { assembly };
|
|
14303
14398
|
}
|
|
14399
|
+
function prepareTranscript(metadata, warnings, emit = (message) => warn(message, warnings)) {
|
|
14400
|
+
const config = resolveTranscriptConfig(metadata);
|
|
14401
|
+
if ("error" in config)
|
|
14402
|
+
throw new Error(config.error);
|
|
14403
|
+
if (!config.config.enabled)
|
|
14404
|
+
return;
|
|
14405
|
+
if (!config.config.url) {
|
|
14406
|
+
emit("transcript skipped: PODCAST_PUB_TRANSCRIPT is on but no transcriptUrl was provided");
|
|
14407
|
+
return;
|
|
14408
|
+
}
|
|
14409
|
+
const meta = metadata ?? {};
|
|
14410
|
+
const segments = Array.isArray(meta.segments) ? meta.segments.map((s) => ({
|
|
14411
|
+
startSeconds: typeof s.startSeconds === "number" ? s.startSeconds : undefined,
|
|
14412
|
+
endSeconds: typeof s.endSeconds === "number" ? s.endSeconds : undefined,
|
|
14413
|
+
transcription: typeof s.transcription === "string" ? s.transcription : undefined
|
|
14414
|
+
})) : [];
|
|
14415
|
+
const cues = buildTranscriptSegments(segments);
|
|
14416
|
+
if (!cues) {
|
|
14417
|
+
emit("transcript skipped: no measured absolute offsets recorded for every segment (concat does not record startSeconds/endSeconds yet); summed per-clip durations are never used for cue timing");
|
|
14418
|
+
return;
|
|
14419
|
+
}
|
|
14420
|
+
const source = (() => {
|
|
14421
|
+
for (const key of ["mp3Path", "audioPath", "audioFile"]) {
|
|
14422
|
+
const value = meta[key];
|
|
14423
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
14424
|
+
return value.trim();
|
|
14425
|
+
}
|
|
14426
|
+
return;
|
|
14427
|
+
})();
|
|
14428
|
+
if (!source) {
|
|
14429
|
+
emit("transcript skipped: no local mp3Path/audioPath/audioFile to anchor the .vtt artifact next to");
|
|
14430
|
+
return;
|
|
14431
|
+
}
|
|
14432
|
+
const vttPath = `${join(dirname2(source), basename(source).replace(/\.[^.]+$/, ""))}.vtt`;
|
|
14433
|
+
try {
|
|
14434
|
+
writeFileSync2(vttPath, renderWebVtt(cues), "utf-8");
|
|
14435
|
+
} catch (error) {
|
|
14436
|
+
emit(`transcript skipped: could not write ${vttPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
14437
|
+
return;
|
|
14438
|
+
}
|
|
14439
|
+
return transcriptReferences(config.config.url);
|
|
14440
|
+
}
|
|
14304
14441
|
function audioAttachment(audio, mediaUrl, sizeInBytes, durationSeconds) {
|
|
14305
14442
|
return {
|
|
14306
14443
|
category: "audio",
|
|
@@ -14316,6 +14453,7 @@ function episodeExtras(episode, seo) {
|
|
|
14316
14453
|
...episode.datePublishedMs !== undefined ? { date_published_ms: episode.datePublishedMs } : {},
|
|
14317
14454
|
...episode.link ? { url: episode.link } : {},
|
|
14318
14455
|
_microfeed: {
|
|
14456
|
+
...episode.transcripts ? { podcast: { transcripts: episode.transcripts } } : {},
|
|
14319
14457
|
"itunes:episodeType": episode.itunes?.episodeType ?? "full",
|
|
14320
14458
|
...episode.itunes?.title ? { "itunes:title": episode.itunes.title } : {},
|
|
14321
14459
|
...episode.itunes?.season !== undefined ? { "itunes:season": episode.itunes.season } : {},
|
|
@@ -14478,6 +14616,11 @@ async function publishEpisode(client, payload, options) {
|
|
|
14478
14616
|
if (!policy.ok)
|
|
14479
14617
|
return fail2(policy.error);
|
|
14480
14618
|
const fields = policy.fields;
|
|
14619
|
+
try {
|
|
14620
|
+
episode.transcripts = prepareTranscript(content.metadata, fields.warnings);
|
|
14621
|
+
} catch (error) {
|
|
14622
|
+
return fail2(`Invalid transcript config: ${error instanceof Error ? error.message : String(error)}`);
|
|
14623
|
+
}
|
|
14481
14624
|
let social;
|
|
14482
14625
|
try {
|
|
14483
14626
|
social = await resolveSocialImage(client, fields, fields.warnings);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { basename, dirname, extname } from 'node:path';
|
|
4
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { type Content, ContentSchema, DocListSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
7
7
|
import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
warn,
|
|
26
26
|
} from './seo';
|
|
27
27
|
import { renderShowNotesMarkdown, type ShowNotesRenderOptions } from './show-notes';
|
|
28
|
+
import { buildTranscriptSegments, renderWebVtt, resolveTranscriptConfig, transcriptReferences } from './transcript';
|
|
28
29
|
|
|
29
30
|
/** Result target name for this plugin. */
|
|
30
31
|
/**
|
|
@@ -261,6 +262,65 @@ function resolveAssemblyOptions(options: PodcastPubOptions): { assembly: Assembl
|
|
|
261
262
|
return { assembly };
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Builds `_microfeed.podcast.transcripts` behind `PODCAST_PUB_TRANSCRIPT` (design
|
|
267
|
+
* docs/design/podcast-transcript.md): builds the WebVTT from `metadata.segments[]` measured
|
|
268
|
+
* absolute offsets and writes it as a local dry-run artifact next to the episode's audio
|
|
269
|
+
* source — transcript files are never uploaded here (microfeed hosts only already-external
|
|
270
|
+
* HTTPS URLs). Inert today: the audio chain records no measured offsets, so this returns
|
|
271
|
+
* undefined and the payload omits `podcast` entirely so a later update cannot erase a stored
|
|
272
|
+
* transcript list.
|
|
273
|
+
*/
|
|
274
|
+
function prepareTranscript(
|
|
275
|
+
metadata: Record<string, unknown> | undefined,
|
|
276
|
+
warnings: string[],
|
|
277
|
+
emit: (message: string) => void = (message) => warn(message, warnings),
|
|
278
|
+
): EpisodeMapping['transcripts'] {
|
|
279
|
+
const config = resolveTranscriptConfig(metadata);
|
|
280
|
+
if ('error' in config) throw new Error(config.error);
|
|
281
|
+
if (!config.config.enabled) return undefined;
|
|
282
|
+
if (!config.config.url) {
|
|
283
|
+
emit('transcript skipped: PODCAST_PUB_TRANSCRIPT is on but no transcriptUrl was provided');
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const meta = metadata ?? {};
|
|
287
|
+
const segments = Array.isArray(meta.segments)
|
|
288
|
+
? (meta.segments as Array<Record<string, unknown>>).map((s) => ({
|
|
289
|
+
startSeconds: typeof s.startSeconds === 'number' ? s.startSeconds : undefined,
|
|
290
|
+
endSeconds: typeof s.endSeconds === 'number' ? s.endSeconds : undefined,
|
|
291
|
+
transcription: typeof s.transcription === 'string' ? s.transcription : undefined,
|
|
292
|
+
}))
|
|
293
|
+
: [];
|
|
294
|
+
const cues = buildTranscriptSegments(segments);
|
|
295
|
+
if (!cues) {
|
|
296
|
+
emit(
|
|
297
|
+
'transcript skipped: no measured absolute offsets recorded for every segment (concat does not record startSeconds/endSeconds yet); summed per-clip durations are never used for cue timing',
|
|
298
|
+
);
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const source = (() => {
|
|
302
|
+
for (const key of ['mp3Path', 'audioPath', 'audioFile'] as const) {
|
|
303
|
+
const value = meta[key];
|
|
304
|
+
if (typeof value === 'string' && value.trim().length > 0) return value.trim();
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
})();
|
|
308
|
+
if (!source) {
|
|
309
|
+
emit('transcript skipped: no local mp3Path/audioPath/audioFile to anchor the .vtt artifact next to');
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
const vttPath = `${join(dirname(source), basename(source).replace(/\.[^.]+$/, ''))}.vtt`;
|
|
313
|
+
try {
|
|
314
|
+
writeFileSync(vttPath, renderWebVtt(cues), 'utf-8');
|
|
315
|
+
} catch (error: unknown) {
|
|
316
|
+
emit(
|
|
317
|
+
`transcript skipped: could not write ${vttPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
318
|
+
);
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
return transcriptReferences(config.config.url);
|
|
322
|
+
}
|
|
323
|
+
|
|
264
324
|
/** Builds the RSS-enclosure attachment from the presigned-upload media URL (design D2). */
|
|
265
325
|
function audioAttachment(
|
|
266
326
|
audio: ReturnType<typeof mapContentToEpisode>['audio'],
|
|
@@ -284,6 +344,7 @@ function episodeExtras(episode: EpisodeMapping, seo?: SeoBlock): MicrofeedItemIn
|
|
|
284
344
|
...(episode.datePublishedMs !== undefined ? { date_published_ms: episode.datePublishedMs } : {}),
|
|
285
345
|
...(episode.link ? { url: episode.link } : {}),
|
|
286
346
|
_microfeed: {
|
|
347
|
+
...(episode.transcripts ? { podcast: { transcripts: episode.transcripts } } : {}),
|
|
287
348
|
'itunes:episodeType': episode.itunes?.episodeType ?? 'full',
|
|
288
349
|
...(episode.itunes?.title ? { 'itunes:title': episode.itunes.title } : {}),
|
|
289
350
|
...(episode.itunes?.season !== undefined ? { 'itunes:season': episode.itunes.season } : {}),
|
|
@@ -499,6 +560,14 @@ async function publishEpisode(
|
|
|
499
560
|
const policy = prepareEpisodeFields(episode, content.metadata ?? {}, seoConfig.config);
|
|
500
561
|
if (!policy.ok) return fail(policy.error);
|
|
501
562
|
const fields = policy.fields;
|
|
563
|
+
// Transcript (PODCAST_PUB_TRANSCRIPT): built and written to disk before any create call so
|
|
564
|
+
// the `_microfeed` payload in both create branches carries the same field when it exists.
|
|
565
|
+
try {
|
|
566
|
+
episode.transcripts = prepareTranscript(content.metadata, fields.warnings);
|
|
567
|
+
} catch (error: unknown) {
|
|
568
|
+
return fail(`Invalid transcript config: ${error instanceof Error ? error.message : String(error)}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
502
571
|
let social: NonNullable<SeoBlock['social_image']> | undefined;
|
|
503
572
|
try {
|
|
504
573
|
social = await resolveSocialImage(client, fields, fields.warnings);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { extname } from 'node:path';
|
|
2
2
|
import type { Content } from '@gobing-ai/kk-core';
|
|
3
3
|
import { renderShowNotesHtml } from './show-notes';
|
|
4
|
+
import type { TranscriptReference } from './transcript';
|
|
4
5
|
|
|
5
6
|
/** Local-cover extensions accepted from `metadata.coverImage` (R4); anything else is rejected. */
|
|
6
7
|
const COVER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
@@ -25,6 +26,8 @@ export interface EpisodeMapping {
|
|
|
25
26
|
datePublishedMs?: number;
|
|
26
27
|
/** Optional public link for the episode (`metadata.articleUrl` → microfeed `url`). */
|
|
27
28
|
link?: string;
|
|
29
|
+
/** Transcript references (`PODCAST_PUB_TRANSCRIPT`, wired in index.ts; empty when disabled/unavailable). */
|
|
30
|
+
transcripts?: TranscriptReference[];
|
|
28
31
|
/** Podcast-directory fields (`metadata.itunes`) passed through to `_microfeed`. */
|
|
29
32
|
itunes?: {
|
|
30
33
|
title?: string;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript cues from real measured data (design docs/design/podcast-transcript.md).
|
|
3
|
+
*
|
|
4
|
+
* Inert today: the audio chain (voice-gen `concatWavs`, the daily sectioned ffmpeg join,
|
|
5
|
+
* `dailyIntro`) does not record measured absolute offsets, and the intro sting plus
|
|
6
|
+
* inter-segment silences are invisible to per-clip durations — so summed durations cannot be
|
|
7
|
+
* used for cue timing. A transcript is built ONLY when every segment carries a measured
|
|
8
|
+
* absolute `startSeconds`/`endSeconds` recorded against the final mixed episode plus real
|
|
9
|
+
* text. Until the concat step records those offsets, this returns undefined and the publish
|
|
10
|
+
* payload simply omits `_microfeed.podcast.transcripts`. Nothing here touches the network.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One segment from `metadata.segments[]` in **final-mixed-episode coordinates** (the shape the
|
|
15
|
+
* concat step must record; per-clip `duration` values are deliberately NOT an input here —
|
|
16
|
+
* summed durations never derive cue starts).
|
|
17
|
+
*/
|
|
18
|
+
export interface TranscriptSegmentInput {
|
|
19
|
+
/** Measured absolute start of the segment's speech in the mixed episode. */
|
|
20
|
+
startSeconds?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Optional measured absolute end. Only the LAST segment's `endSeconds` is read (the final
|
|
23
|
+
* cue boundary); earlier cue ends come from the next segment's measured start.
|
|
24
|
+
*/
|
|
25
|
+
endSeconds?: number;
|
|
26
|
+
transcription?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Framed cue: `[start, end)` seconds in final-mixed coordinates and the caption text. */
|
|
30
|
+
export interface TranscriptCue {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isMeasured(value: unknown): value is number {
|
|
37
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds one cue per segment ONLY when every segment carries a measured absolute
|
|
42
|
+
* `startSeconds` (strictly increasing) and the LAST segment carries a measured absolute
|
|
43
|
+
* `endSeconds`, plus a non-empty transcription on every segment. Each cue start is the
|
|
44
|
+
* segment's own measured offset — never a sum of per-clip durations — and each end is the
|
|
45
|
+
* next measured boundary. Any missing/invalid value discards the whole transcript
|
|
46
|
+
* (undefined); a partially timed transcript is not shippable.
|
|
47
|
+
*/
|
|
48
|
+
export function buildTranscriptSegments(segments: TranscriptSegmentInput[]): TranscriptCue[] | undefined {
|
|
49
|
+
if (segments.length === 0) return undefined;
|
|
50
|
+
for (const segment of segments) {
|
|
51
|
+
if (!isMeasured(segment.startSeconds) || segment.startSeconds < 0) return undefined;
|
|
52
|
+
const text = typeof segment.transcription === 'string' ? segment.transcription.trim() : '';
|
|
53
|
+
if (text.length === 0) return undefined;
|
|
54
|
+
}
|
|
55
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
56
|
+
if (
|
|
57
|
+
(segments[index] as { startSeconds: number }).startSeconds <=
|
|
58
|
+
(segments[index - 1] as { startSeconds: number }).startSeconds
|
|
59
|
+
) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const last = segments[segments.length - 1] as { startSeconds: number; endSeconds?: number };
|
|
64
|
+
if (!isMeasured(last.endSeconds) || last.endSeconds <= last.startSeconds) return undefined;
|
|
65
|
+
return segments.map((segment, index) => ({
|
|
66
|
+
start: segment.startSeconds as number,
|
|
67
|
+
end:
|
|
68
|
+
index === segments.length - 1
|
|
69
|
+
? (segment.endSeconds as number)
|
|
70
|
+
: (segments[index + 1] as { startSeconds: number }).startSeconds,
|
|
71
|
+
text: (segment.transcription as string).trim(),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function splitTimestamp(seconds: number): { hours: number; minutes: string; secs: string; fraction: string } {
|
|
76
|
+
const totalMs = Math.round((Number.isFinite(seconds) && seconds > 0 ? seconds : 0) * 1000);
|
|
77
|
+
return {
|
|
78
|
+
hours: Math.floor(totalMs / 3_600_000),
|
|
79
|
+
minutes: String(Math.floor((totalMs % 3_600_000) / 60_000)).padStart(2, '0'),
|
|
80
|
+
secs: String(Math.floor((totalMs % 60_000) / 1000)).padStart(2, '0'),
|
|
81
|
+
fraction: String(totalMs % 1000).padStart(3, '0'),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `HH:MM:SS.mmm` — WebVTT requires two-digit hours. */
|
|
86
|
+
function vttTimestamp(seconds: number): string {
|
|
87
|
+
const t = splitTimestamp(seconds);
|
|
88
|
+
return `${String(t.hours).padStart(2, '0')}:${t.minutes}:${t.secs}.${t.fraction}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `HH:MM:SS,mmm` — SubRip twin of the VTT stamp with a comma decimal separator. */
|
|
92
|
+
function srtTimestamp(seconds: number): string {
|
|
93
|
+
const t = splitTimestamp(seconds);
|
|
94
|
+
return `${String(t.hours).padStart(2, '0')}:${t.minutes}:${t.secs},${t.fraction}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* WebVTT escaping per spec: `&` first, then `<`/`>` (the two characters a WebVTT parser treats
|
|
99
|
+
* as markup). CR characters are dropped so text lines stay inline.
|
|
100
|
+
*/
|
|
101
|
+
function escapeVttText(text: string): string {
|
|
102
|
+
return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replace(/\r+/g, '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** WebVTT document: `WEBVTT` header, then one cue per segment (overview cues omitted deliberately). */
|
|
106
|
+
export function renderWebVtt(cues: TranscriptCue[]): string {
|
|
107
|
+
const lines = ['WEBVTT', ''];
|
|
108
|
+
for (const cue of cues) {
|
|
109
|
+
lines.push(`${vttTimestamp(cue.start)} --> ${vttTimestamp(cue.end)}`, escapeVttText(cue.text), '');
|
|
110
|
+
}
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** SubRip twin of {@link renderWebVtt}: numbered cues, `,` decimal separator, no header. */
|
|
115
|
+
export function renderSrt(cues: TranscriptCue[]): string {
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
let number = 1;
|
|
118
|
+
for (const cue of cues) {
|
|
119
|
+
lines.push(
|
|
120
|
+
String(number),
|
|
121
|
+
`${srtTimestamp(cue.start)} --> ${srtTimestamp(cue.end)}`,
|
|
122
|
+
escapeVttText(cue.text),
|
|
123
|
+
'',
|
|
124
|
+
);
|
|
125
|
+
number += 1;
|
|
126
|
+
}
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** microfeed 1.0.13 transcript entry: HTTPS URL only, `text/plain` is not accepted. */
|
|
131
|
+
export interface TranscriptReference {
|
|
132
|
+
url: string;
|
|
133
|
+
type: 'text/vtt' | 'application/x-subrip';
|
|
134
|
+
language: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface TranscriptConfig {
|
|
138
|
+
/** `PODCAST_PUB_TRANSCRIPT` / `metadata.podcast.transcriptEnabled` — default false. */
|
|
139
|
+
enabled: boolean;
|
|
140
|
+
/** Already-hosted HTTPS URL (`PODCAST_PUB_TRANSCRIPT_URL` / `metadata.podcast.transcriptUrl`). */
|
|
141
|
+
url?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function podcastRecord(metadata: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
145
|
+
if (metadata !== null && typeof metadata === 'object') {
|
|
146
|
+
const podcast = metadata.podcast;
|
|
147
|
+
if (podcast !== null && typeof podcast === 'object' && !Array.isArray(podcast)) {
|
|
148
|
+
return podcast as Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function pick(metadata: Record<string, unknown>, key: string, envValue: string | undefined): unknown {
|
|
155
|
+
if (Object.hasOwn(metadata, key)) {
|
|
156
|
+
const value = metadata[key];
|
|
157
|
+
if (value !== undefined && value !== '') return value;
|
|
158
|
+
}
|
|
159
|
+
return envValue !== undefined && envValue !== '' ? envValue : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `metadata.podcast.*` wins over env (the plugin's established precedence, `src/seo.ts` pick).
|
|
164
|
+
* A non-HTTPS or non-string URL fails loud before any request; flag on without a URL disables
|
|
165
|
+
* the transcript with a warning instead of an error.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveTranscriptConfig(
|
|
168
|
+
metadata: Record<string, unknown> | undefined,
|
|
169
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
170
|
+
): { config: TranscriptConfig } | { error: string } {
|
|
171
|
+
const podcast = podcastRecord(metadata);
|
|
172
|
+
const rawEnabled = pick(podcast, 'transcriptEnabled', env.PODCAST_PUB_TRANSCRIPT);
|
|
173
|
+
const enabled = rawEnabled === true || rawEnabled === 'true' || rawEnabled === '1';
|
|
174
|
+
if (rawEnabled !== undefined && !enabled) {
|
|
175
|
+
if (rawEnabled !== false && rawEnabled !== 'false' && rawEnabled !== '0') {
|
|
176
|
+
return {
|
|
177
|
+
error: `invalid transcript enable flag "${String(rawEnabled)}": expected PODCAST_PUB_TRANSCRIPT or metadata.podcast.transcriptEnabled to be true or false`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const rawUrl = pick(podcast, 'transcriptUrl', env.PODCAST_PUB_TRANSCRIPT_URL);
|
|
182
|
+
if (rawUrl === undefined) return { config: { enabled } };
|
|
183
|
+
const url = typeof rawUrl === 'string' ? rawUrl.trim() : String(rawUrl).trim();
|
|
184
|
+
if (!url.startsWith('https://')) {
|
|
185
|
+
return {
|
|
186
|
+
error: `invalid transcript URL "${url}": microfeed hosts only HTTPS transcript files (set metadata.podcast.transcriptUrl or PODCAST_PUB_TRANSCRIPT_URL to an https:// URL)`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { config: { enabled, url } };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The `_microfeed.podcast.transcripts` value for a real hosted VTT (zh-cn episodes). */
|
|
193
|
+
export function transcriptReferences(url: string): TranscriptReference[] {
|
|
194
|
+
return [{ url, type: 'text/vtt', language: 'zh-cn' }];
|
|
195
|
+
}
|
|
@@ -12225,11 +12225,11 @@ var PluginManifestSchemaBase = looseObject({
|
|
|
12225
12225
|
});
|
|
12226
12226
|
var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
|
|
12227
12227
|
if (manifest.browserSession !== undefined) {
|
|
12228
|
-
if (manifest.kind !== "publish") {
|
|
12228
|
+
if (manifest.kind !== "publish" && manifest.kind !== "ingestion") {
|
|
12229
12229
|
ctx.addIssue({
|
|
12230
12230
|
code: "custom",
|
|
12231
12231
|
path: ["browserSession"],
|
|
12232
|
-
message: `browserSession requires kind "publish", not "${manifest.kind}"`
|
|
12232
|
+
message: `browserSession requires kind "publish" or "ingestion", not "${manifest.kind}"`
|
|
12233
12233
|
});
|
|
12234
12234
|
}
|
|
12235
12235
|
if (manifest.check === undefined) {
|
|
@@ -20073,11 +20073,11 @@ var PluginManifestSchemaBase = looseObject({
|
|
|
20073
20073
|
});
|
|
20074
20074
|
var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
|
|
20075
20075
|
if (manifest.browserSession !== undefined) {
|
|
20076
|
-
if (manifest.kind !== "publish") {
|
|
20076
|
+
if (manifest.kind !== "publish" && manifest.kind !== "ingestion") {
|
|
20077
20077
|
ctx.addIssue({
|
|
20078
20078
|
code: "custom",
|
|
20079
20079
|
path: ["browserSession"],
|
|
20080
|
-
message: `browserSession requires kind "publish", not "${manifest.kind}"`
|
|
20080
|
+
message: `browserSession requires kind "publish" or "ingestion", not "${manifest.kind}"`
|
|
20081
20081
|
});
|
|
20082
20082
|
}
|
|
20083
20083
|
if (manifest.check === undefined) {
|
|
@@ -20683,10 +20683,12 @@ function sha256Hex(value) {
|
|
|
20683
20683
|
var DEFAULT_BOT_PROFILE_ROOT = join4(homedir4(), ".config", "kk", "browser-profiles");
|
|
20684
20684
|
class BotError extends Error {
|
|
20685
20685
|
code;
|
|
20686
|
-
|
|
20686
|
+
details;
|
|
20687
|
+
constructor(code, message, details = {}) {
|
|
20687
20688
|
super(message);
|
|
20688
20689
|
this.name = "BotError";
|
|
20689
20690
|
this.code = code;
|
|
20691
|
+
this.details = details;
|
|
20690
20692
|
}
|
|
20691
20693
|
}
|
|
20692
20694
|
// ../../packages/publish-harness/src/profile-dir.ts
|
|
@@ -20097,10 +20097,12 @@ function normalizeSessionIdentity(value) {
|
|
|
20097
20097
|
}
|
|
20098
20098
|
class BotError extends Error {
|
|
20099
20099
|
code;
|
|
20100
|
-
|
|
20100
|
+
details;
|
|
20101
|
+
constructor(code, message, details = {}) {
|
|
20101
20102
|
super(message);
|
|
20102
20103
|
this.name = "BotError";
|
|
20103
20104
|
this.code = code;
|
|
20105
|
+
this.details = details;
|
|
20104
20106
|
}
|
|
20105
20107
|
}
|
|
20106
20108
|
// ../../packages/publish-harness/src/index.ts
|
|
@@ -20073,11 +20073,11 @@ var PluginManifestSchemaBase = looseObject({
|
|
|
20073
20073
|
});
|
|
20074
20074
|
var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
|
|
20075
20075
|
if (manifest.browserSession !== undefined) {
|
|
20076
|
-
if (manifest.kind !== "publish") {
|
|
20076
|
+
if (manifest.kind !== "publish" && manifest.kind !== "ingestion") {
|
|
20077
20077
|
ctx.addIssue({
|
|
20078
20078
|
code: "custom",
|
|
20079
20079
|
path: ["browserSession"],
|
|
20080
|
-
message: `browserSession requires kind "publish", not "${manifest.kind}"`
|
|
20080
|
+
message: `browserSession requires kind "publish" or "ingestion", not "${manifest.kind}"`
|
|
20081
20081
|
});
|
|
20082
20082
|
}
|
|
20083
20083
|
if (manifest.check === undefined) {
|
|
@@ -20707,10 +20707,12 @@ function sha256Hex(value) {
|
|
|
20707
20707
|
var DEFAULT_BOT_PROFILE_ROOT = join4(homedir4(), ".config", "kk", "browser-profiles");
|
|
20708
20708
|
class BotError extends Error {
|
|
20709
20709
|
code;
|
|
20710
|
-
|
|
20710
|
+
details;
|
|
20711
|
+
constructor(code, message, details = {}) {
|
|
20711
20712
|
super(message);
|
|
20712
20713
|
this.name = "BotError";
|
|
20713
20714
|
this.code = code;
|
|
20715
|
+
this.details = details;
|
|
20714
20716
|
}
|
|
20715
20717
|
}
|
|
20716
20718
|
// ../../packages/publish-harness/src/profile-dir.ts
|
|
@@ -20097,10 +20097,12 @@ function normalizeSessionIdentity(value) {
|
|
|
20097
20097
|
}
|
|
20098
20098
|
class BotError extends Error {
|
|
20099
20099
|
code;
|
|
20100
|
-
|
|
20100
|
+
details;
|
|
20101
|
+
constructor(code, message, details = {}) {
|
|
20101
20102
|
super(message);
|
|
20102
20103
|
this.name = "BotError";
|
|
20103
20104
|
this.code = code;
|
|
20105
|
+
this.details = details;
|
|
20104
20106
|
}
|
|
20105
20107
|
}
|
|
20106
20108
|
// ../../packages/publish-harness/src/index.ts
|
|
@@ -20073,11 +20073,11 @@ var PluginManifestSchemaBase = looseObject({
|
|
|
20073
20073
|
});
|
|
20074
20074
|
var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
|
|
20075
20075
|
if (manifest.browserSession !== undefined) {
|
|
20076
|
-
if (manifest.kind !== "publish") {
|
|
20076
|
+
if (manifest.kind !== "publish" && manifest.kind !== "ingestion") {
|
|
20077
20077
|
ctx.addIssue({
|
|
20078
20078
|
code: "custom",
|
|
20079
20079
|
path: ["browserSession"],
|
|
20080
|
-
message: `browserSession requires kind "publish", not "${manifest.kind}"`
|
|
20080
|
+
message: `browserSession requires kind "publish" or "ingestion", not "${manifest.kind}"`
|
|
20081
20081
|
});
|
|
20082
20082
|
}
|
|
20083
20083
|
if (manifest.check === undefined) {
|
|
@@ -20701,10 +20701,12 @@ function sha256Hex(value) {
|
|
|
20701
20701
|
var DEFAULT_BOT_PROFILE_ROOT = join4(homedir4(), ".config", "kk", "browser-profiles");
|
|
20702
20702
|
class BotError extends Error {
|
|
20703
20703
|
code;
|
|
20704
|
-
|
|
20704
|
+
details;
|
|
20705
|
+
constructor(code, message, details = {}) {
|
|
20705
20706
|
super(message);
|
|
20706
20707
|
this.name = "BotError";
|
|
20707
20708
|
this.code = code;
|
|
20709
|
+
this.details = details;
|
|
20708
20710
|
}
|
|
20709
20711
|
}
|
|
20710
20712
|
// ../../packages/publish-harness/src/profile-dir.ts
|
|
@@ -20369,10 +20369,12 @@ function normalizeSessionIdentity(value) {
|
|
|
20369
20369
|
}
|
|
20370
20370
|
class BotError extends Error {
|
|
20371
20371
|
code;
|
|
20372
|
-
|
|
20372
|
+
details;
|
|
20373
|
+
constructor(code, message, details = {}) {
|
|
20373
20374
|
super(message);
|
|
20374
20375
|
this.name = "BotError";
|
|
20375
20376
|
this.code = code;
|
|
20377
|
+
this.details = details;
|
|
20376
20378
|
}
|
|
20377
20379
|
}
|
|
20378
20380
|
// ../../packages/publish-harness/src/index.ts
|
|
@@ -20948,11 +20950,11 @@ var PluginManifestSchemaBase = looseObject({
|
|
|
20948
20950
|
});
|
|
20949
20951
|
var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
|
|
20950
20952
|
if (manifest.browserSession !== undefined) {
|
|
20951
|
-
if (manifest.kind !== "publish") {
|
|
20953
|
+
if (manifest.kind !== "publish" && manifest.kind !== "ingestion") {
|
|
20952
20954
|
ctx.addIssue({
|
|
20953
20955
|
code: "custom",
|
|
20954
20956
|
path: ["browserSession"],
|
|
20955
|
-
message: `browserSession requires kind "publish", not "${manifest.kind}"`
|
|
20957
|
+
message: `browserSession requires kind "publish" or "ingestion", not "${manifest.kind}"`
|
|
20956
20958
|
});
|
|
20957
20959
|
}
|
|
20958
20960
|
if (manifest.check === undefined) {
|