@gobing-ai/knowledge-kit 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/board/collect/index.js +744 -1107
- package/dist/board/daily-voice/index.js +741 -107
- package/dist/board/itc/index.js +32 -6
- package/dist/index.js +1793 -103
- package/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +24 -1
- package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
- package/plugins/kk/config.example.yaml +8 -0
- package/plugins/kk/decisions/extract/ai-news.yaml +7 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
- package/plugins/kk/scripts/daily-stages.ts +664 -83
- package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
- package/plugins/kk/scripts/topic-boundary.json +18 -83
- package/plugins/kk/workflows/kk-collect.yaml +2 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +82 -8
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
- package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
- package/plugins/publishings/podcast-pub/README.md +65 -0
- package/plugins/publishings/podcast-pub/dist/index.js +1709 -274
- package/plugins/publishings/podcast-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
- package/plugins/publishings/podcast-pub/src/index.ts +211 -34
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
- package/plugins/publishings/podcast-pub/src/seo.ts +1081 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +71 -6
- package/plugins/publishings/surfdash-pub/dist/index.js +66 -5
- package/plugins/publishings/surfdash-pub/src/index.ts +94 -10
- package/plugins/publishings/wechatmp-pub/dist/index.js +54 -30
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +8 -3
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +58 -38
- package/plugins/publishings/wechatmp-pub/src/flow.ts +18 -1
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -1
|
@@ -4866,10 +4866,13 @@ function writeLine(message, target) {
|
|
|
4866
4866
|
target.write(`${message}
|
|
4867
4867
|
`);
|
|
4868
4868
|
}
|
|
4869
|
+
function echo(message, target = defaultStdoutTarget ?? processStream("stdout")) {
|
|
4870
|
+
writeLine(message, target);
|
|
4871
|
+
}
|
|
4869
4872
|
function echoError(message, target = defaultStderrTarget ?? processStream("stderr")) {
|
|
4870
4873
|
writeLine(message, target);
|
|
4871
4874
|
}
|
|
4872
|
-
var defaultStderrTarget;
|
|
4875
|
+
var defaultStdoutTarget, defaultStderrTarget;
|
|
4873
4876
|
|
|
4874
4877
|
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/index.js
|
|
4875
4878
|
var init_dist = __esm(() => {
|
|
@@ -11981,7 +11984,7 @@ function findProjectRoot(startDir) {
|
|
|
11981
11984
|
var init_file_system_node = () => {};
|
|
11982
11985
|
|
|
11983
11986
|
// ../../plugins/publishings/podcast-pub/src/index.ts
|
|
11984
|
-
import { readFileSync as
|
|
11987
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
11985
11988
|
import { readFile } from "fs/promises";
|
|
11986
11989
|
import { basename, dirname as dirname2, extname as extname2 } from "path";
|
|
11987
11990
|
import { parseArgs } from "util";
|
|
@@ -12254,6 +12257,59 @@ var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx)
|
|
|
12254
12257
|
init_file_system_node();
|
|
12255
12258
|
init_config();
|
|
12256
12259
|
|
|
12260
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.32+180a6e424da77181/node_modules/@gobing-ai/ts-runtime/dist/fs.js
|
|
12261
|
+
init_file_system_node();
|
|
12262
|
+
|
|
12263
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.32+180a6e424da77181/node_modules/@gobing-ai/ts-runtime/dist/path.js
|
|
12264
|
+
function normalizeSeparators(path) {
|
|
12265
|
+
return path.replaceAll("\\", "/");
|
|
12266
|
+
}
|
|
12267
|
+
var SEP = globalThis.process?.platform === "win32" ? "\\" : "/";
|
|
12268
|
+
function splitRoot(path) {
|
|
12269
|
+
const normalized = normalizeSeparators(path);
|
|
12270
|
+
const drive = normalized.match(/^([A-Za-z]:)(?:\/|$)/);
|
|
12271
|
+
if (drive) {
|
|
12272
|
+
return { root: drive[1] ?? "", rest: normalized.slice((drive[1] ?? "").length).replace(/^\/+/, "") };
|
|
12273
|
+
}
|
|
12274
|
+
if (normalized.startsWith("/")) {
|
|
12275
|
+
return { root: "/", rest: normalized.replace(/^\/+/, "") };
|
|
12276
|
+
}
|
|
12277
|
+
return { root: "", rest: normalized };
|
|
12278
|
+
}
|
|
12279
|
+
function dirnamePath(path) {
|
|
12280
|
+
const input = normalizeSeparators(path);
|
|
12281
|
+
const { root } = splitRoot(input);
|
|
12282
|
+
if (root !== "" && input.replace(/\/+$/, "") === root)
|
|
12283
|
+
return root === "/" ? "/" : `${root}/`;
|
|
12284
|
+
if (/^\/+$/.test(input))
|
|
12285
|
+
return "/";
|
|
12286
|
+
const normalized = input.replace(/\/+$/, "");
|
|
12287
|
+
if (normalized === "" || normalized === "/")
|
|
12288
|
+
return normalized || ".";
|
|
12289
|
+
const index = normalized.lastIndexOf("/");
|
|
12290
|
+
if (index < 0)
|
|
12291
|
+
return ".";
|
|
12292
|
+
if (index === 0)
|
|
12293
|
+
return "/";
|
|
12294
|
+
return normalized.slice(0, index);
|
|
12295
|
+
}
|
|
12296
|
+
|
|
12297
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.32+180a6e424da77181/node_modules/@gobing-ai/ts-runtime/dist/fs.js
|
|
12298
|
+
async function ensureDirForFile(path, fs = createNodeFileSystem()) {
|
|
12299
|
+
await fs.ensureDir(dirnamePath(path));
|
|
12300
|
+
}
|
|
12301
|
+
async function readJsonFile(path, fs = createNodeFileSystem()) {
|
|
12302
|
+
const content = await fs.readFile(path);
|
|
12303
|
+
try {
|
|
12304
|
+
return JSON.parse(content);
|
|
12305
|
+
} catch (error) {
|
|
12306
|
+
throw new Error(`Failed to parse JSON file "${path}": ${error.message}`);
|
|
12307
|
+
}
|
|
12308
|
+
}
|
|
12309
|
+
async function writeJsonFile(path, value, fs = createNodeFileSystem()) {
|
|
12310
|
+
await fs.writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
12311
|
+
`);
|
|
12312
|
+
}
|
|
12257
12313
|
// ../../packages/kk-core/src/render-md.ts
|
|
12258
12314
|
if (false) {}
|
|
12259
12315
|
// ../../packages/publish-harness/src/index.ts
|
|
@@ -12305,104 +12361,1443 @@ class FakeFileCliTransport {
|
|
|
12305
12361
|
// ../../plugins/publishings/podcast-pub/src/index.ts
|
|
12306
12362
|
init_dist();
|
|
12307
12363
|
|
|
12308
|
-
// ../../plugins/publishings/podcast-pub/src/
|
|
12309
|
-
|
|
12364
|
+
// ../../plugins/publishings/podcast-pub/src/backfill.ts
|
|
12365
|
+
init_dist();
|
|
12310
12366
|
|
|
12311
|
-
// ../../plugins/publishings/podcast-pub/src/
|
|
12312
|
-
|
|
12313
|
-
|
|
12367
|
+
// ../../plugins/publishings/podcast-pub/src/microfeed-client.ts
|
|
12368
|
+
class MicrofeedRequestError extends Error {
|
|
12369
|
+
status;
|
|
12370
|
+
constructor(status, message) {
|
|
12371
|
+
super(message);
|
|
12372
|
+
this.name = "MicrofeedRequestError";
|
|
12373
|
+
this.status = status;
|
|
12374
|
+
}
|
|
12314
12375
|
}
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
break;
|
|
12322
|
-
out += `${rest.slice(0, match.index)}<a href="${match[2]}" target="_blank" rel="noopener">${match[1]}</a>`;
|
|
12323
|
-
rest = rest.slice(match.index + match[0].length);
|
|
12376
|
+
var MIN_TIMEOUT_MS = 1000;
|
|
12377
|
+
var DEFAULT_TIMEOUT_MS = 300000;
|
|
12378
|
+
function resolveTimeoutMs(raw) {
|
|
12379
|
+
const parsed = raw === undefined ? Number.NaN : Number(raw);
|
|
12380
|
+
if (!Number.isFinite(parsed) || parsed < MIN_TIMEOUT_MS) {
|
|
12381
|
+
return DEFAULT_TIMEOUT_MS;
|
|
12324
12382
|
}
|
|
12325
|
-
return
|
|
12383
|
+
return parsed;
|
|
12326
12384
|
}
|
|
12327
|
-
|
|
12328
|
-
|
|
12385
|
+
|
|
12386
|
+
class MicrofeedClient {
|
|
12387
|
+
baseUrl;
|
|
12388
|
+
apiKey;
|
|
12389
|
+
timeoutMs;
|
|
12390
|
+
constructor(baseUrl, apiKey, timeoutMs) {
|
|
12391
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
12392
|
+
this.apiKey = apiKey;
|
|
12393
|
+
this.timeoutMs = timeoutMs;
|
|
12394
|
+
}
|
|
12395
|
+
itemUrl(itemId) {
|
|
12396
|
+
return `${this.baseUrl}/items/${itemId}`;
|
|
12397
|
+
}
|
|
12398
|
+
async request(path, init) {
|
|
12399
|
+
let response;
|
|
12400
|
+
try {
|
|
12401
|
+
response = await fetch(`${this.baseUrl}${path}`, {
|
|
12402
|
+
...init,
|
|
12403
|
+
headers: {
|
|
12404
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
12405
|
+
Origin: this.baseUrl,
|
|
12406
|
+
...init.headers
|
|
12407
|
+
},
|
|
12408
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12409
|
+
});
|
|
12410
|
+
} catch (error) {
|
|
12411
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
12412
|
+
throw new Error(`microfeed transport error (${init.method ?? "GET"} ${path}): ${reason}`);
|
|
12413
|
+
}
|
|
12414
|
+
if (!response.ok) {
|
|
12415
|
+
const text = (await response.text()).slice(0, 1000);
|
|
12416
|
+
let detail = text.trim();
|
|
12417
|
+
try {
|
|
12418
|
+
const parsed = JSON.parse(text);
|
|
12419
|
+
if (typeof parsed.error === "string")
|
|
12420
|
+
detail = parsed.error;
|
|
12421
|
+
} catch {}
|
|
12422
|
+
throw new MicrofeedRequestError(response.status, `microfeed API request failed (HTTP ${response.status} ${response.statusText} ${path}): ${detail || "Unknown error"}`);
|
|
12423
|
+
}
|
|
12424
|
+
try {
|
|
12425
|
+
return await response.json();
|
|
12426
|
+
} catch {
|
|
12427
|
+
throw new Error(`microfeed API returned a non-JSON response from ${path}`);
|
|
12428
|
+
}
|
|
12429
|
+
}
|
|
12430
|
+
async createItem(input, idempotencyKey) {
|
|
12431
|
+
const data = await this.request("/api/v1/items", {
|
|
12432
|
+
method: "POST",
|
|
12433
|
+
headers: {
|
|
12434
|
+
"Content-Type": "application/json",
|
|
12435
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
12436
|
+
},
|
|
12437
|
+
body: JSON.stringify(input)
|
|
12438
|
+
});
|
|
12439
|
+
if (typeof data.id !== "string" || data.id.length === 0) {
|
|
12440
|
+
throw new Error("microfeed item create response is missing the item id");
|
|
12441
|
+
}
|
|
12442
|
+
return data.id;
|
|
12443
|
+
}
|
|
12444
|
+
async validateItem(input) {
|
|
12445
|
+
await this.request("/api/v1/items/validate", {
|
|
12446
|
+
method: "POST",
|
|
12447
|
+
headers: { "Content-Type": "application/json" },
|
|
12448
|
+
body: JSON.stringify(input)
|
|
12449
|
+
});
|
|
12450
|
+
}
|
|
12451
|
+
async getItem(itemId) {
|
|
12452
|
+
return await this.request(`/api/v1/items/${encodeURIComponent(itemId)}`, {
|
|
12453
|
+
method: "GET"
|
|
12454
|
+
});
|
|
12455
|
+
}
|
|
12456
|
+
async updateItem(itemId, input) {
|
|
12457
|
+
return await this.request(`/api/v1/items/${encodeURIComponent(itemId)}`, {
|
|
12458
|
+
method: "PUT",
|
|
12459
|
+
headers: { "Content-Type": "application/json" },
|
|
12460
|
+
body: JSON.stringify(input)
|
|
12461
|
+
});
|
|
12462
|
+
}
|
|
12463
|
+
async deleteItem(itemId) {
|
|
12464
|
+
await this.request(`/api/v1/items/${encodeURIComponent(itemId)}`, {
|
|
12465
|
+
method: "DELETE",
|
|
12466
|
+
headers: { "Content-Type": "application/json" }
|
|
12467
|
+
});
|
|
12468
|
+
}
|
|
12469
|
+
async listItems() {
|
|
12470
|
+
const first = await this.request("/api/v1/feed", { method: "GET" });
|
|
12471
|
+
const items = [...first.items ?? []];
|
|
12472
|
+
const seen = new Set(["/api/v1/feed"]);
|
|
12473
|
+
let next = typeof first.next_url === "string" ? first.next_url : "";
|
|
12474
|
+
for (let page = 1;page < 20 && next !== "" && !seen.has(next); page++) {
|
|
12475
|
+
seen.add(next);
|
|
12476
|
+
const path = next.startsWith(this.baseUrl) ? next.slice(this.baseUrl.length) || "/" : next;
|
|
12477
|
+
if (!path.startsWith("/"))
|
|
12478
|
+
break;
|
|
12479
|
+
const more = await this.request(path, { method: "GET" });
|
|
12480
|
+
items.push(...more.items ?? []);
|
|
12481
|
+
next = typeof more.next_url === "string" ? more.next_url : "";
|
|
12482
|
+
}
|
|
12483
|
+
return { ...first, items };
|
|
12484
|
+
}
|
|
12485
|
+
async prepareUpload(input) {
|
|
12486
|
+
const data = await this.request("/api/v1/media_files/presigned_urls", {
|
|
12487
|
+
method: "POST",
|
|
12488
|
+
headers: { "Content-Type": "application/json" },
|
|
12489
|
+
body: JSON.stringify(input)
|
|
12490
|
+
});
|
|
12491
|
+
if (typeof data.presigned_url !== "string" || typeof data.media_url !== "string") {
|
|
12492
|
+
throw new Error("microfeed upload-preparation response is missing presigned_url/media_url");
|
|
12493
|
+
}
|
|
12494
|
+
return { presignedUrl: data.presigned_url, mediaUrl: data.media_url };
|
|
12495
|
+
}
|
|
12496
|
+
async uploadBytes(presignedUrl, bytes, mimeType) {
|
|
12497
|
+
let response;
|
|
12498
|
+
try {
|
|
12499
|
+
response = await fetch(presignedUrl, {
|
|
12500
|
+
method: "PUT",
|
|
12501
|
+
headers: { "Content-Type": mimeType },
|
|
12502
|
+
body: bytes,
|
|
12503
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12504
|
+
});
|
|
12505
|
+
} catch (error) {
|
|
12506
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
12507
|
+
throw new Error(`microfeed transport error (PUT audio bytes): ${reason}`);
|
|
12508
|
+
}
|
|
12509
|
+
if (!response.ok) {
|
|
12510
|
+
const text = (await response.text()).slice(0, 1000);
|
|
12511
|
+
throw new Error(`microfeed audio upload failed (HTTP ${response.status} ${response.statusText}): ${text.trim() || "Unknown error"}`);
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12329
12514
|
}
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
return
|
|
12515
|
+
|
|
12516
|
+
// ../../plugins/publishings/podcast-pub/src/seo.ts
|
|
12517
|
+
init_dist();
|
|
12518
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
12519
|
+
function codePointLength(value) {
|
|
12520
|
+
return Array.from(value).length;
|
|
12521
|
+
}
|
|
12522
|
+
var WARN_PREFIX = "WARN podcast-pub:";
|
|
12523
|
+
function warn(message, sink) {
|
|
12524
|
+
const line = message.startsWith(WARN_PREFIX) ? message : `${WARN_PREFIX} ${message}`;
|
|
12525
|
+
echoError(line);
|
|
12526
|
+
sink?.push(line);
|
|
12527
|
+
}
|
|
12528
|
+
var SEO_DEFAULTS = {
|
|
12529
|
+
seo: true,
|
|
12530
|
+
titleMode: "as-is",
|
|
12531
|
+
titlePattern: "{series} \xB7 {date}\uFF1A{keywords}",
|
|
12532
|
+
seriesName: "\u6BCF\u65E5AI\u4E71\u7096",
|
|
12533
|
+
showName: "\u54B8\u8BDD\u54B8\u8BF4",
|
|
12534
|
+
titleMax: 55,
|
|
12535
|
+
titleMaxKeywords: 2,
|
|
12536
|
+
seoTitleSuffix: "\uFF5C\u54B8\u8BDD\u54B8\u8BF4",
|
|
12537
|
+
seoTitleMax: 60,
|
|
12538
|
+
descriptionMin: 120,
|
|
12539
|
+
descriptionMax: 155,
|
|
12540
|
+
descriptionSource: "plan",
|
|
12541
|
+
descriptionPattern: "{show}\xB7{series} {date}\uFF08\u7EA6 {minutes} \u5206\u949F\uFF09\uFF1A{clauses}\u3002",
|
|
12542
|
+
summaryParagraph: true,
|
|
12543
|
+
timelineStyle: "after",
|
|
12544
|
+
autoEpisodeNumber: true,
|
|
12545
|
+
titleSanitize: "normalize",
|
|
12546
|
+
linkField: "none",
|
|
12547
|
+
slugMode: "pattern",
|
|
12548
|
+
slugPattern: "daily-ai-{date}",
|
|
12549
|
+
socialImage: false,
|
|
12550
|
+
socialImageAltPattern: "{series} {date} \u5C01\u9762\uFF1A{keywords}",
|
|
12551
|
+
roundDuration: true
|
|
12552
|
+
};
|
|
12553
|
+
var ENV_KEYS = {
|
|
12554
|
+
seo: "PODCAST_PUB_SEO",
|
|
12555
|
+
titleMode: "PODCAST_PUB_EPISODE_TITLE_MODE",
|
|
12556
|
+
titlePattern: "PODCAST_PUB_EPISODE_TITLE_PATTERN",
|
|
12557
|
+
seriesName: "PODCAST_PUB_SERIES_NAME",
|
|
12558
|
+
showName: "PODCAST_PUB_SHOW_NAME",
|
|
12559
|
+
titleMax: "PODCAST_PUB_TITLE_MAX",
|
|
12560
|
+
titleMaxKeywords: "PODCAST_PUB_TITLE_MAX_KEYWORDS",
|
|
12561
|
+
seoTitleSuffix: "PODCAST_PUB_SEO_TITLE_SUFFIX",
|
|
12562
|
+
seoTitleMax: "PODCAST_PUB_SEO_TITLE_MAX",
|
|
12563
|
+
descriptionMin: "PODCAST_PUB_SEO_DESCRIPTION_MIN",
|
|
12564
|
+
descriptionMax: "PODCAST_PUB_SEO_DESCRIPTION_MAX",
|
|
12565
|
+
descriptionSource: "PODCAST_PUB_SEO_DESCRIPTION_SOURCE",
|
|
12566
|
+
descriptionPattern: "PODCAST_PUB_SEO_DESCRIPTION_PATTERN",
|
|
12567
|
+
summaryParagraph: "PODCAST_PUB_SUMMARY_PARAGRAPH",
|
|
12568
|
+
timelineStyle: "PODCAST_PUB_TIMELINE_INLINE_STYLE",
|
|
12569
|
+
autoEpisodeNumber: "PODCAST_PUB_AUTO_EPISODE_NUMBER",
|
|
12570
|
+
titleSanitize: "PODCAST_PUB_TITLE_SANITIZE",
|
|
12571
|
+
linkField: "PODCAST_PUB_LINK_FIELD",
|
|
12572
|
+
slugMode: "PODCAST_PUB_SLUG",
|
|
12573
|
+
slugPattern: "PODCAST_PUB_SLUG_PATTERN",
|
|
12574
|
+
socialImage: "PODCAST_PUB_SOCIAL_IMAGE",
|
|
12575
|
+
socialImageAltPattern: "PODCAST_PUB_SOCIAL_IMAGE_ALT_PATTERN",
|
|
12576
|
+
roundDuration: "PODCAST_PUB_ROUND_DURATION"
|
|
12577
|
+
};
|
|
12578
|
+
function asRecord(value) {
|
|
12579
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
12580
|
+
return value;
|
|
12581
|
+
}
|
|
12582
|
+
return;
|
|
12336
12583
|
}
|
|
12337
|
-
function
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12584
|
+
function podcastOf(metadata) {
|
|
12585
|
+
return asRecord(metadata?.podcast) ?? {};
|
|
12586
|
+
}
|
|
12587
|
+
function pick2(metadata, podcastKey, envKey, env) {
|
|
12588
|
+
const podcast = podcastOf(metadata);
|
|
12589
|
+
if (Object.hasOwn(podcast, podcastKey)) {
|
|
12590
|
+
const value = podcast[podcastKey];
|
|
12591
|
+
if (value !== undefined && value !== "")
|
|
12592
|
+
return { value, key: `metadata.podcast.${podcastKey}` };
|
|
12593
|
+
}
|
|
12594
|
+
const fromEnv = env[envKey];
|
|
12595
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
12596
|
+
return { value: fromEnv, key: envKey };
|
|
12597
|
+
return;
|
|
12598
|
+
}
|
|
12599
|
+
function invalid(key, value, expected) {
|
|
12600
|
+
return `invalid ${key} "${String(value)}": expected ${expected}`;
|
|
12601
|
+
}
|
|
12602
|
+
function parseBool(raw, fallback) {
|
|
12603
|
+
if (!raw)
|
|
12604
|
+
return fallback;
|
|
12605
|
+
const value = raw.value;
|
|
12606
|
+
if (value === true || value === "true" || value === "1")
|
|
12607
|
+
return true;
|
|
12608
|
+
if (value === false || value === "false" || value === "0")
|
|
12609
|
+
return false;
|
|
12610
|
+
return invalid(raw.key, value, "true or false");
|
|
12611
|
+
}
|
|
12612
|
+
function parseIntIn(raw, fallback, min) {
|
|
12613
|
+
if (!raw)
|
|
12614
|
+
return fallback;
|
|
12615
|
+
const value = typeof raw.value === "number" ? raw.value : Number(raw.value);
|
|
12616
|
+
if (!Number.isInteger(value) || value < min)
|
|
12617
|
+
return invalid(raw.key, raw.value, `an integer \u2265 ${min}`);
|
|
12618
|
+
return value;
|
|
12619
|
+
}
|
|
12620
|
+
function parseEnum(raw, allowed, fallback) {
|
|
12621
|
+
if (!raw)
|
|
12622
|
+
return fallback;
|
|
12623
|
+
const value = String(raw.value);
|
|
12624
|
+
if (allowed.includes(value))
|
|
12625
|
+
return value;
|
|
12626
|
+
return invalid(raw.key, raw.value, allowed.join(" or "));
|
|
12627
|
+
}
|
|
12628
|
+
function resolvePodcastSeoConfig(metadata, env = process.env) {
|
|
12629
|
+
const bool = (podcastKey, envKey, fallback) => parseBool(pick2(metadata, podcastKey, envKey, env), fallback);
|
|
12630
|
+
const int = (podcastKey, envKey, fallback, min) => parseIntIn(pick2(metadata, podcastKey, envKey, env), fallback, min);
|
|
12631
|
+
const text = (podcastKey, envKey, fallback) => {
|
|
12632
|
+
const raw = pick2(metadata, podcastKey, envKey, env);
|
|
12633
|
+
return raw ? String(raw.value) : fallback;
|
|
12348
12634
|
};
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
const
|
|
12352
|
-
|
|
12353
|
-
const
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12635
|
+
const seo = bool("seo", ENV_KEYS.seo, SEO_DEFAULTS.seo);
|
|
12636
|
+
const titleMode = parseEnum(pick2(metadata, "titleMode", ENV_KEYS.titleMode, env), ["as-is", "headline"], SEO_DEFAULTS.titleMode);
|
|
12637
|
+
const titleMax = int("titleMax", ENV_KEYS.titleMax, SEO_DEFAULTS.titleMax, 1);
|
|
12638
|
+
const titleMaxKeywords = int("titleMaxKeywords", ENV_KEYS.titleMaxKeywords, SEO_DEFAULTS.titleMaxKeywords, 1);
|
|
12639
|
+
const seoTitleMax = int("seoTitleMax", ENV_KEYS.seoTitleMax, SEO_DEFAULTS.seoTitleMax, 1);
|
|
12640
|
+
const descriptionMin = int("descriptionMin", ENV_KEYS.descriptionMin, SEO_DEFAULTS.descriptionMin, 1);
|
|
12641
|
+
const descriptionMax = int("descriptionMax", ENV_KEYS.descriptionMax, SEO_DEFAULTS.descriptionMax, 1);
|
|
12642
|
+
const descriptionSource = parseEnum(pick2(metadata, "descriptionSource", ENV_KEYS.descriptionSource, env), ["plan", "surfing"], SEO_DEFAULTS.descriptionSource);
|
|
12643
|
+
const summaryParagraph = bool("summaryParagraph", ENV_KEYS.summaryParagraph, SEO_DEFAULTS.summaryParagraph);
|
|
12644
|
+
const timelineStyle = parseEnum(pick2(metadata, "timelineInlineStyle", ENV_KEYS.timelineStyle, env), ["after", "before", "omit", "true", "false"], "after");
|
|
12645
|
+
const autoEpisodeNumber = bool("autoEpisodeNumber", ENV_KEYS.autoEpisodeNumber, SEO_DEFAULTS.autoEpisodeNumber);
|
|
12646
|
+
const titleSanitize = parseEnum(pick2(metadata, "titleSanitize", ENV_KEYS.titleSanitize, env), ["normalize", "reject"], SEO_DEFAULTS.titleSanitize);
|
|
12647
|
+
const linkField = parseEnum(pick2(metadata, "linkField", ENV_KEYS.linkField, env), ["none", "url"], SEO_DEFAULTS.linkField);
|
|
12648
|
+
const slugMode = parseEnum(pick2(metadata, "slug", ENV_KEYS.slugMode, env), ["auto", "pattern"], SEO_DEFAULTS.slugMode);
|
|
12649
|
+
const socialImage = bool("socialImageEnabled", ENV_KEYS.socialImage, SEO_DEFAULTS.socialImage);
|
|
12650
|
+
const roundDuration = bool("roundDuration", ENV_KEYS.roundDuration, SEO_DEFAULTS.roundDuration);
|
|
12651
|
+
const parsed = [
|
|
12652
|
+
seo,
|
|
12653
|
+
titleMode,
|
|
12654
|
+
titleMax,
|
|
12655
|
+
titleMaxKeywords,
|
|
12656
|
+
seoTitleMax,
|
|
12657
|
+
descriptionMin,
|
|
12658
|
+
descriptionMax,
|
|
12659
|
+
descriptionSource,
|
|
12660
|
+
summaryParagraph,
|
|
12661
|
+
timelineStyle,
|
|
12662
|
+
autoEpisodeNumber,
|
|
12663
|
+
titleSanitize,
|
|
12664
|
+
linkField,
|
|
12665
|
+
slugMode,
|
|
12666
|
+
socialImage,
|
|
12667
|
+
roundDuration
|
|
12668
|
+
];
|
|
12669
|
+
const failure = parsed.find((value) => typeof value === "string" && value.startsWith("invalid "));
|
|
12670
|
+
if (typeof failure === "string")
|
|
12671
|
+
return { error: failure };
|
|
12672
|
+
if (descriptionMin > descriptionMax) {
|
|
12673
|
+
return {
|
|
12674
|
+
error: `invalid ${ENV_KEYS.descriptionMin}: ${String(descriptionMin)} is greater than ${ENV_KEYS.descriptionMax} ${String(descriptionMax)}`
|
|
12675
|
+
};
|
|
12676
|
+
}
|
|
12677
|
+
const seoTitleSuffix = text("seoTitleSuffix", ENV_KEYS.seoTitleSuffix, SEO_DEFAULTS.seoTitleSuffix);
|
|
12678
|
+
if (codePointLength(seoTitleSuffix) >= seoTitleMax) {
|
|
12679
|
+
return {
|
|
12680
|
+
error: `invalid ${ENV_KEYS.seoTitleSuffix}: suffix is longer than ${ENV_KEYS.seoTitleMax} (${String(seoTitleMax)})`
|
|
12681
|
+
};
|
|
12682
|
+
}
|
|
12683
|
+
const styleRaw = timelineStyle;
|
|
12684
|
+
const timeline = styleRaw === "true" || styleRaw === "after" ? "after" : styleRaw === "false" || styleRaw === "omit" ? "omit" : "before";
|
|
12685
|
+
return {
|
|
12686
|
+
config: {
|
|
12687
|
+
seo,
|
|
12688
|
+
titleMode,
|
|
12689
|
+
titlePattern: text("titlePattern", ENV_KEYS.titlePattern, SEO_DEFAULTS.titlePattern),
|
|
12690
|
+
seriesName: text("seriesName", ENV_KEYS.seriesName, SEO_DEFAULTS.seriesName),
|
|
12691
|
+
showName: text("showName", ENV_KEYS.showName, SEO_DEFAULTS.showName),
|
|
12692
|
+
titleMax,
|
|
12693
|
+
titleMaxKeywords,
|
|
12694
|
+
seoTitleSuffix,
|
|
12695
|
+
seoTitleMax,
|
|
12696
|
+
descriptionMin,
|
|
12697
|
+
descriptionMax,
|
|
12698
|
+
descriptionSource,
|
|
12699
|
+
descriptionPattern: text("descriptionPattern", ENV_KEYS.descriptionPattern, SEO_DEFAULTS.descriptionPattern),
|
|
12700
|
+
summaryParagraph,
|
|
12701
|
+
timelineStyle: timeline,
|
|
12702
|
+
autoEpisodeNumber,
|
|
12703
|
+
titleSanitize,
|
|
12704
|
+
linkField,
|
|
12705
|
+
slugMode,
|
|
12706
|
+
slugPattern: text("slugPattern", ENV_KEYS.slugPattern, SEO_DEFAULTS.slugPattern),
|
|
12707
|
+
socialImage,
|
|
12708
|
+
socialImageAltPattern: text("socialImageAltPattern", ENV_KEYS.socialImageAltPattern, SEO_DEFAULTS.socialImageAltPattern),
|
|
12709
|
+
roundDuration
|
|
12368
12710
|
}
|
|
12711
|
+
};
|
|
12712
|
+
}
|
|
12713
|
+
function hasControlChar(value) {
|
|
12714
|
+
for (const ch of value) {
|
|
12715
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
12716
|
+
if (cp <= 31 || cp >= 127 && cp <= 159 || cp === 8232 || cp === 8233)
|
|
12717
|
+
return true;
|
|
12369
12718
|
}
|
|
12370
|
-
|
|
12371
|
-
return fallback();
|
|
12372
|
-
return starts;
|
|
12719
|
+
return false;
|
|
12373
12720
|
}
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
.
|
|
12377
|
-
.
|
|
12378
|
-
.
|
|
12379
|
-
.
|
|
12380
|
-
.
|
|
12381
|
-
.
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12721
|
+
function forbiddenChars(value) {
|
|
12722
|
+
const found = [];
|
|
12723
|
+
if (value.includes('"'))
|
|
12724
|
+
found.push('"');
|
|
12725
|
+
if (value.includes("\\"))
|
|
12726
|
+
found.push("\\");
|
|
12727
|
+
if (value.includes("<"))
|
|
12728
|
+
found.push("<");
|
|
12729
|
+
if (value.includes(">"))
|
|
12730
|
+
found.push(">");
|
|
12731
|
+
if (hasControlChar(value))
|
|
12732
|
+
found.push("control");
|
|
12733
|
+
return found;
|
|
12734
|
+
}
|
|
12735
|
+
function sanitizeTitle(value, mode) {
|
|
12736
|
+
const forbidden = forbiddenChars(value);
|
|
12737
|
+
if (mode === "reject" && forbidden.length > 0) {
|
|
12738
|
+
return { ok: false, error: `title contains forbidden characters: ${forbidden.join(", ")}` };
|
|
12739
|
+
}
|
|
12740
|
+
const chars = Array.from(value);
|
|
12741
|
+
const quoteAt = [];
|
|
12742
|
+
chars.forEach((ch, index) => {
|
|
12743
|
+
if (ch === '"')
|
|
12744
|
+
quoteAt.push(index);
|
|
12745
|
+
});
|
|
12746
|
+
const odd = quoteAt.length % 2 === 1;
|
|
12747
|
+
for (let n = 0;n < quoteAt.length; n++) {
|
|
12748
|
+
const index = quoteAt[n] ?? 0;
|
|
12749
|
+
const last = n === quoteAt.length - 1;
|
|
12750
|
+
chars[index] = odd && last ? "\u201D" : n % 2 === 0 ? "\u201C" : "\u201D";
|
|
12751
|
+
}
|
|
12752
|
+
let out = "";
|
|
12753
|
+
for (const ch of chars) {
|
|
12754
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
12755
|
+
if (cp <= 31 || cp >= 127 && cp <= 159 || cp === 8232 || cp === 8233)
|
|
12756
|
+
continue;
|
|
12757
|
+
if (ch === "\\")
|
|
12758
|
+
out += "\uFF3C";
|
|
12759
|
+
else if (ch === "<")
|
|
12760
|
+
out += "\u3008";
|
|
12761
|
+
else if (ch === ">")
|
|
12762
|
+
out += "\u3009";
|
|
12763
|
+
else
|
|
12764
|
+
out += ch;
|
|
12765
|
+
}
|
|
12766
|
+
return { ok: true, value: out.trim() };
|
|
12767
|
+
}
|
|
12768
|
+
function spaceCjkLatin(text) {
|
|
12769
|
+
return text.replace(/(\p{Script=Han})([A-Za-z0-9])/gu, "$1 $2").replace(/([A-Za-z0-9])(\p{Script=Han})/gu, "$1 $2");
|
|
12770
|
+
}
|
|
12771
|
+
function normalizeEpisodeTitle(title, seriesName) {
|
|
12772
|
+
let out = title.replace(/\s+/g, " ").trim();
|
|
12773
|
+
const legacy = /^(?:\u6BCF\u65E5 AI \u8D44\u8BAF|\u6BCF\u65E5AI\u8D44\u8BAF|AI \u4E71\u7096|AI\u4E71\u7096)/;
|
|
12774
|
+
if (legacy.test(out))
|
|
12775
|
+
out = out.replace(legacy, seriesName);
|
|
12776
|
+
const trailing = /\s\u00B7\s*(\d{4}-\d{2}-\d{2})\s*$/;
|
|
12777
|
+
const trail = trailing.exec(out);
|
|
12778
|
+
if (trail?.[1] && out.slice(0, trail.index).includes(trail[1]))
|
|
12779
|
+
out = out.slice(0, trail.index).trim();
|
|
12780
|
+
if (seriesName.length > 0 && out.startsWith(seriesName)) {
|
|
12781
|
+
let rest = spaceCjkLatin(out.slice(seriesName.length));
|
|
12782
|
+
const seriesEndsCjk = /\p{Script=Han}$/u.test(seriesName);
|
|
12783
|
+
if (seriesEndsCjk && /^[A-Za-z0-9]/.test(rest))
|
|
12784
|
+
rest = ` ${rest}`;
|
|
12785
|
+
out = seriesName + rest;
|
|
12786
|
+
} else {
|
|
12787
|
+
out = spaceCjkLatin(out);
|
|
12788
|
+
}
|
|
12789
|
+
return out.replace(/\s+/g, " ").trim();
|
|
12790
|
+
}
|
|
12791
|
+
function isRealDate(year, month, day) {
|
|
12792
|
+
if (month < 1 || month > 12 || day < 1 || day > 31)
|
|
12387
12793
|
return false;
|
|
12388
|
-
|
|
12794
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
12795
|
+
return utc.getUTCFullYear() === year && utc.getUTCMonth() === month - 1 && utc.getUTCDate() === day;
|
|
12389
12796
|
}
|
|
12390
|
-
function
|
|
12391
|
-
const
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12797
|
+
function isoFromRunDate(raw) {
|
|
12798
|
+
const digits = raw.replaceAll(/\D/g, "");
|
|
12799
|
+
if (digits.length < 8)
|
|
12800
|
+
return;
|
|
12801
|
+
const year = Number(digits.slice(0, 4));
|
|
12802
|
+
const month = Number(digits.slice(4, 6));
|
|
12803
|
+
const day = Number(digits.slice(6, 8));
|
|
12804
|
+
if (!isRealDate(year, month, day))
|
|
12805
|
+
return;
|
|
12806
|
+
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
12807
|
+
}
|
|
12808
|
+
function isoFromLosAngeles(ms) {
|
|
12809
|
+
if (!Number.isFinite(ms))
|
|
12810
|
+
return;
|
|
12811
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
12812
|
+
timeZone: "America/Los_Angeles",
|
|
12813
|
+
year: "numeric",
|
|
12814
|
+
month: "2-digit",
|
|
12815
|
+
day: "2-digit"
|
|
12816
|
+
}).formatToParts(new Date(ms));
|
|
12817
|
+
const year = parts.find((part) => part.type === "year")?.value;
|
|
12818
|
+
const month = parts.find((part) => part.type === "month")?.value;
|
|
12819
|
+
const day = parts.find((part) => part.type === "day")?.value;
|
|
12820
|
+
if (!year || !month || !day)
|
|
12821
|
+
return;
|
|
12822
|
+
return `${year}-${month}-${day}`;
|
|
12823
|
+
}
|
|
12824
|
+
function resolveEpisodeDate(metadata, publishedAtMs) {
|
|
12825
|
+
if (typeof metadata.runDate === "string") {
|
|
12826
|
+
const fromRun = isoFromRunDate(metadata.runDate);
|
|
12827
|
+
if (fromRun)
|
|
12828
|
+
return fromRun;
|
|
12829
|
+
}
|
|
12830
|
+
if (publishedAtMs !== undefined)
|
|
12831
|
+
return isoFromLosAngeles(publishedAtMs);
|
|
12832
|
+
const publishedAt = metadata.publishedAt;
|
|
12833
|
+
if (typeof publishedAt === "number")
|
|
12834
|
+
return isoFromLosAngeles(publishedAt);
|
|
12835
|
+
if (typeof publishedAt === "string" && publishedAt.length > 0) {
|
|
12836
|
+
const parsed = Date.parse(publishedAt);
|
|
12837
|
+
if (!Number.isNaN(parsed))
|
|
12838
|
+
return isoFromLosAngeles(parsed);
|
|
12839
|
+
}
|
|
12840
|
+
return;
|
|
12841
|
+
}
|
|
12842
|
+
function seasonAndEpisode(isoDate) {
|
|
12843
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
|
12844
|
+
if (!match)
|
|
12845
|
+
return;
|
|
12846
|
+
const year = Number(match[1]);
|
|
12847
|
+
const month = Number(match[2]);
|
|
12848
|
+
const day = Number(match[3]);
|
|
12849
|
+
if (!isRealDate(year, month, day))
|
|
12850
|
+
return;
|
|
12851
|
+
const episode = Math.round((Date.UTC(year, month - 1, day) - Date.UTC(year, 0, 1)) / 86400000) + 1;
|
|
12852
|
+
return { season: year, episode };
|
|
12853
|
+
}
|
|
12854
|
+
function isPositiveInt(value) {
|
|
12855
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1;
|
|
12856
|
+
}
|
|
12857
|
+
function keywordsFromRefinedTitle(title) {
|
|
12858
|
+
const index = title.indexOf("\uFF1A");
|
|
12859
|
+
if (index < 0)
|
|
12860
|
+
return [];
|
|
12861
|
+
return title.slice(index + 1).split("\u3001").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
12862
|
+
}
|
|
12863
|
+
function keywordFromDocTitle(title) {
|
|
12864
|
+
const cut = title.split(/\uFF0C|,|\uFF1A|\uFF1B|\u2014\u2014/)[0]?.trim() ?? "";
|
|
12865
|
+
return Array.from(cut).slice(0, 15).join("");
|
|
12866
|
+
}
|
|
12867
|
+
function clauseFromTitle(title) {
|
|
12868
|
+
const cut = (title.split(/\uFF0C|,/)[0] ?? title).replaceAll("\u300C", "").replaceAll("\u300D", "");
|
|
12869
|
+
return cut.replace(/\s+/g, " ").trim();
|
|
12870
|
+
}
|
|
12871
|
+
function firstSentence(text) {
|
|
12872
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
12873
|
+
if (trimmed.length === 0)
|
|
12874
|
+
return "";
|
|
12875
|
+
const parts = trimmed.split(/(?<=[\u3002\uFF01\uFF1F.!?])/);
|
|
12876
|
+
return (parts[0] ?? trimmed).trim();
|
|
12877
|
+
}
|
|
12878
|
+
function planItemsFromMetadata(metadata) {
|
|
12879
|
+
const raw = podcastOf(metadata).planItems;
|
|
12880
|
+
if (!Array.isArray(raw))
|
|
12881
|
+
return [];
|
|
12882
|
+
const items = [];
|
|
12883
|
+
for (const entry of raw) {
|
|
12884
|
+
const record = asRecord(entry);
|
|
12885
|
+
if (!record)
|
|
12886
|
+
continue;
|
|
12887
|
+
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
12888
|
+
const brief = typeof record.brief === "string" ? record.brief.trim() : "";
|
|
12889
|
+
if (title.length === 0 && brief.length === 0)
|
|
12890
|
+
continue;
|
|
12891
|
+
items.push(brief.length > 0 ? { title, brief } : { title });
|
|
12892
|
+
}
|
|
12893
|
+
return items;
|
|
12894
|
+
}
|
|
12895
|
+
function stringList(value) {
|
|
12896
|
+
if (!Array.isArray(value))
|
|
12897
|
+
return;
|
|
12898
|
+
const items = value.filter((entry) => typeof entry === "string").map((entry) => entry.trim());
|
|
12899
|
+
return items.filter((entry) => entry.length > 0);
|
|
12900
|
+
}
|
|
12901
|
+
function cutAtBoundary(value, room) {
|
|
12902
|
+
if (room <= 0)
|
|
12903
|
+
return "";
|
|
12904
|
+
const chars = Array.from(value);
|
|
12905
|
+
if (chars.length <= room)
|
|
12906
|
+
return value;
|
|
12907
|
+
const sliced = chars.slice(0, room);
|
|
12908
|
+
let cut = sliced.length;
|
|
12909
|
+
for (let index = sliced.length - 1;index >= 0; index--) {
|
|
12910
|
+
if (/[\uFF0C\u3002\uFF01\uFF1F\u3001\uFF1B\uFF1A,.\s]/.test(sliced[index] ?? "")) {
|
|
12911
|
+
cut = index;
|
|
12912
|
+
break;
|
|
12913
|
+
}
|
|
12914
|
+
}
|
|
12915
|
+
const trimmed = sliced.slice(0, cut).join("").trim();
|
|
12916
|
+
return trimmed.length > 0 ? trimmed : sliced.join("").trim();
|
|
12917
|
+
}
|
|
12918
|
+
function headlineFromKeywords(config, date, keywords, limit) {
|
|
12919
|
+
const head = normalizeEpisodeTitle(config.titlePattern.replaceAll("{series}", config.seriesName).replaceAll("{date}", date).replaceAll("{keywords}", ""), config.seriesName);
|
|
12920
|
+
const chosen = [];
|
|
12921
|
+
for (const keyword of keywords) {
|
|
12922
|
+
const title = normalizeEpisodeTitle(`${head}${[...chosen, keyword].join("\u3001")}`, config.seriesName);
|
|
12923
|
+
if (codePointLength(title) <= limit)
|
|
12924
|
+
chosen.push(keyword);
|
|
12925
|
+
else
|
|
12926
|
+
break;
|
|
12927
|
+
}
|
|
12928
|
+
if (chosen.length === 0 && keywords[0]) {
|
|
12929
|
+
const room = limit - codePointLength(head);
|
|
12930
|
+
const cut = cutAtBoundary(keywords[0], room);
|
|
12931
|
+
if (cut.length > 0)
|
|
12932
|
+
chosen.push(cut);
|
|
12933
|
+
}
|
|
12934
|
+
return normalizeEpisodeTitle(chosen.length > 0 ? `${head}${chosen.join("\u3001")}` : head, config.seriesName);
|
|
12935
|
+
}
|
|
12936
|
+
function pickKeywords(metadata, config) {
|
|
12937
|
+
const podcast = podcastOf(metadata);
|
|
12938
|
+
if (typeof podcast.title === "string" && podcast.title.trim().length > 0) {
|
|
12939
|
+
return { explicitTitle: podcast.title.trim(), keywords: [] };
|
|
12940
|
+
}
|
|
12941
|
+
const explicit = stringList(podcast.titleKeywords);
|
|
12942
|
+
if (explicit && explicit.length > 0)
|
|
12943
|
+
return { keywords: explicit.slice(0, config.titleMaxKeywords) };
|
|
12944
|
+
const refined = typeof podcast.refinedTitle === "string" ? keywordsFromRefinedTitle(podcast.refinedTitle) : [];
|
|
12945
|
+
if (refined.length > 0)
|
|
12946
|
+
return { keywords: refined.slice(0, config.titleMaxKeywords) };
|
|
12947
|
+
const fromPlan = planItemsFromMetadata(metadata).map((item) => keywordFromDocTitle(item.title)).filter((keyword) => keyword.length > 0);
|
|
12948
|
+
if (fromPlan.length > 0)
|
|
12949
|
+
return { keywords: fromPlan.slice(0, config.titleMaxKeywords) };
|
|
12950
|
+
return { keywords: [] };
|
|
12951
|
+
}
|
|
12952
|
+
function buildItemTitle(asIs, metadata, config, date, warnings) {
|
|
12953
|
+
const picked = pickKeywords(metadata, config);
|
|
12954
|
+
if (picked.explicitTitle)
|
|
12955
|
+
return { title: picked.explicitTitle, keywords: [], explicit: true };
|
|
12956
|
+
if (config.titleMode !== "headline")
|
|
12957
|
+
return { title: asIs, keywords: picked.keywords, explicit: false };
|
|
12958
|
+
if (!date) {
|
|
12959
|
+
warnings.push(`${WARN_PREFIX} headline title skipped: episode date is unknown`);
|
|
12960
|
+
return { title: asIs, keywords: [], explicit: false };
|
|
12961
|
+
}
|
|
12962
|
+
if (picked.keywords.length === 0) {
|
|
12963
|
+
warnings.push(`${WARN_PREFIX} no keywords for the headline title; keeping the as-is title`);
|
|
12964
|
+
return { title: asIs, keywords: [], explicit: false };
|
|
12965
|
+
}
|
|
12966
|
+
return {
|
|
12967
|
+
title: headlineFromKeywords(config, date, picked.keywords, config.titleMax),
|
|
12968
|
+
keywords: picked.keywords,
|
|
12969
|
+
explicit: false
|
|
12970
|
+
};
|
|
12971
|
+
}
|
|
12972
|
+
function fitSeoTitle(episodeTitle, keywords, config, date, explicit) {
|
|
12973
|
+
const suffix = config.seoTitleSuffix;
|
|
12974
|
+
let stem = episodeTitle;
|
|
12975
|
+
let remaining = keywords;
|
|
12976
|
+
let seo = `${stem}${suffix}`;
|
|
12977
|
+
while (codePointLength(seo) > config.seoTitleMax && remaining.length > 0 && date && !explicit) {
|
|
12978
|
+
remaining = remaining.slice(0, -1);
|
|
12979
|
+
stem = headlineFromKeywords(config, date, remaining, config.titleMax);
|
|
12980
|
+
seo = `${stem}${suffix}`;
|
|
12981
|
+
}
|
|
12982
|
+
if (codePointLength(seo) > config.seoTitleMax) {
|
|
12983
|
+
const room = config.seoTitleMax - codePointLength(suffix);
|
|
12984
|
+
stem = cutAtBoundary(stem, room);
|
|
12985
|
+
seo = `${stem}${suffix}`;
|
|
12986
|
+
}
|
|
12987
|
+
return { stem, seo };
|
|
12988
|
+
}
|
|
12989
|
+
function cleanClause(value) {
|
|
12990
|
+
return value.replace(/<[^>]*>/g, "").replace(/[*#`[\]]/g, "").replace(/[\u3002\uFF01\uFF1F.!?]+$/u, "").replace(/\s+/g, " ").trim();
|
|
12991
|
+
}
|
|
12992
|
+
function descriptionHead(config, date, minutes) {
|
|
12993
|
+
const pattern = config.descriptionPattern.includes("{clauses}") ? config.descriptionPattern.split("{clauses}")[0] ?? config.descriptionPattern : config.descriptionPattern;
|
|
12994
|
+
let head = pattern.replaceAll("{show}", config.showName).replaceAll("{series}", config.seriesName);
|
|
12995
|
+
if (date)
|
|
12996
|
+
head = head.replaceAll("{date}", date);
|
|
12997
|
+
else
|
|
12998
|
+
head = head.replaceAll(" {date}", "").replaceAll("{date}", "");
|
|
12999
|
+
if (minutes !== undefined && minutes > 0)
|
|
13000
|
+
head = head.replaceAll("{minutes}", String(minutes));
|
|
13001
|
+
else
|
|
13002
|
+
head = head.replaceAll("\uFF08\u7EA6 {minutes} \u5206\u949F\uFF09", "").replaceAll("{minutes}", "");
|
|
13003
|
+
return head.replace(/\u3002\s*$/, "");
|
|
13004
|
+
}
|
|
13005
|
+
function durationMinutes(seconds) {
|
|
13006
|
+
if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0)
|
|
13007
|
+
return;
|
|
13008
|
+
return Math.round(seconds / 60);
|
|
13009
|
+
}
|
|
13010
|
+
function buildSeoDescription(input) {
|
|
13011
|
+
const head = descriptionHead(input.config, input.date, input.minutes);
|
|
13012
|
+
const render = (parts) => parts.length === 0 ? head : `${head}${parts.join("\uFF1B")}\u3002`;
|
|
13013
|
+
const picked = [];
|
|
13014
|
+
const pushWhile = (candidates) => {
|
|
13015
|
+
for (const candidate of candidates) {
|
|
13016
|
+
const clause = cleanClause(candidate);
|
|
13017
|
+
if (clause.length === 0 || picked.includes(clause))
|
|
13018
|
+
continue;
|
|
13019
|
+
const next = render([...picked, clause]);
|
|
13020
|
+
if (codePointLength(next) <= input.config.descriptionMax)
|
|
13021
|
+
picked.push(clause);
|
|
13022
|
+
}
|
|
13023
|
+
};
|
|
13024
|
+
pushWhile(input.clauses);
|
|
13025
|
+
if (codePointLength(render(picked)) < input.config.descriptionMin)
|
|
13026
|
+
pushWhile(input.extraSentences);
|
|
13027
|
+
const description = render(picked);
|
|
13028
|
+
return { description, short: codePointLength(description) < input.config.descriptionMin };
|
|
13029
|
+
}
|
|
13030
|
+
function descriptionInputs(metadata, config, date, minutes) {
|
|
13031
|
+
const podcast = podcastOf(metadata);
|
|
13032
|
+
const plan = planItemsFromMetadata(metadata);
|
|
13033
|
+
const summary = stringList(podcast.summaryClauses);
|
|
13034
|
+
const surfing = config.descriptionSource === "surfing" && typeof podcast.refinedDescription === "string" ? podcast.refinedDescription.trim() : "";
|
|
13035
|
+
const clauses = surfing ? [surfing] : summary && summary.length > 0 ? summary : plan.map((item) => clauseFromTitle(item.title)).filter((clause) => clause.length > 0);
|
|
13036
|
+
const extras = plan.map((item) => item.brief ? firstSentence(item.brief) : "").filter((sentence) => sentence.length > 0);
|
|
13037
|
+
return buildSeoDescription({ config, date, minutes, clauses, extraSentences: extras });
|
|
13038
|
+
}
|
|
13039
|
+
function isValidItemSlug(slug) {
|
|
13040
|
+
return slug.length >= 1 && slug.length <= 120 && !slug.endsWith("-") && /^[a-z0-9][a-z0-9-]*$/.test(slug);
|
|
13041
|
+
}
|
|
13042
|
+
function renderSlug(pattern, date) {
|
|
13043
|
+
if (!date)
|
|
13044
|
+
return { warning: "skipping slug: episode date is unknown" };
|
|
13045
|
+
const slug = pattern.replaceAll("{date}", date);
|
|
13046
|
+
if (!isValidItemSlug(slug)) {
|
|
13047
|
+
return {
|
|
13048
|
+
error: `invalid PODCAST_PUB_SLUG_PATTERN output "${slug}": expected lowercase letters, digits and hyphens (^[a-z0-9][a-z0-9-]*$), at most 120 characters, and no trailing hyphen`
|
|
13049
|
+
};
|
|
13050
|
+
}
|
|
13051
|
+
return { slug };
|
|
13052
|
+
}
|
|
13053
|
+
function roundDurationSeconds(seconds, enabled) {
|
|
13054
|
+
if (seconds === undefined)
|
|
13055
|
+
return;
|
|
13056
|
+
return enabled ? Math.round(seconds) : seconds;
|
|
13057
|
+
}
|
|
13058
|
+
function imageMimeFromBytes(bytes) {
|
|
13059
|
+
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
13060
|
+
return "image/png";
|
|
13061
|
+
}
|
|
13062
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 216)
|
|
13063
|
+
return "image/jpeg";
|
|
13064
|
+
return;
|
|
13065
|
+
}
|
|
13066
|
+
function readImageDimensions(bytes) {
|
|
13067
|
+
const mime = imageMimeFromBytes(bytes);
|
|
13068
|
+
if (mime === "image/png") {
|
|
13069
|
+
if (bytes.length < 24)
|
|
13070
|
+
return;
|
|
13071
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
13072
|
+
const width = view.getUint32(16);
|
|
13073
|
+
const height = view.getUint32(20);
|
|
13074
|
+
return { width, height, mime };
|
|
13075
|
+
}
|
|
13076
|
+
if (mime === "image/jpeg") {
|
|
13077
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
13078
|
+
let offset = 2;
|
|
13079
|
+
while (offset + 3 < bytes.length) {
|
|
13080
|
+
if (bytes[offset] !== 255)
|
|
13081
|
+
return;
|
|
13082
|
+
const marker = bytes[offset + 1] ?? 0;
|
|
13083
|
+
if (marker === 216 || marker === 217) {
|
|
13084
|
+
offset += 2;
|
|
13085
|
+
continue;
|
|
13086
|
+
}
|
|
13087
|
+
if (marker >= 208 && marker <= 215) {
|
|
13088
|
+
offset += 2;
|
|
13089
|
+
continue;
|
|
13090
|
+
}
|
|
13091
|
+
if (offset + 4 > bytes.length)
|
|
13092
|
+
return;
|
|
13093
|
+
const length = view.getUint16(offset + 2);
|
|
13094
|
+
if (length < 2)
|
|
13095
|
+
return;
|
|
13096
|
+
const isSof = marker === 192 || marker === 193 || marker === 194 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207;
|
|
13097
|
+
if (isSof) {
|
|
13098
|
+
if (offset + 9 > bytes.length)
|
|
13099
|
+
return;
|
|
13100
|
+
return { height: view.getUint16(offset + 5), width: view.getUint16(offset + 7), mime };
|
|
13101
|
+
}
|
|
13102
|
+
offset += 2 + length;
|
|
13103
|
+
}
|
|
13104
|
+
}
|
|
13105
|
+
return;
|
|
13106
|
+
}
|
|
13107
|
+
function readLocalBytes(path) {
|
|
13108
|
+
try {
|
|
13109
|
+
return new Uint8Array(readFileSync2(path));
|
|
13110
|
+
} catch {
|
|
13111
|
+
return;
|
|
13112
|
+
}
|
|
13113
|
+
}
|
|
13114
|
+
function altText(config, date, keywords) {
|
|
13115
|
+
const rendered = config.socialImageAltPattern.replaceAll("{series}", config.seriesName).replaceAll("{date}", date ?? "").replaceAll("{keywords}", keywords.join("\u3001"));
|
|
13116
|
+
const sanitized = sanitizeTitle(rendered, "normalize");
|
|
13117
|
+
const value = sanitized.ok ? sanitized.value : rendered;
|
|
13118
|
+
if (codePointLength(value) <= 200)
|
|
13119
|
+
return value;
|
|
13120
|
+
return cutAtBoundary(value, 200);
|
|
13121
|
+
}
|
|
13122
|
+
function mustSanitize(value, mode, label) {
|
|
13123
|
+
const sanitized = sanitizeTitle(value, mode);
|
|
13124
|
+
if (!sanitized.ok)
|
|
13125
|
+
return { ok: false, error: `${label}: ${sanitized.error}` };
|
|
13126
|
+
return sanitized;
|
|
13127
|
+
}
|
|
13128
|
+
function prepareEpisodeFields(episode, metadata, config) {
|
|
13129
|
+
const warnings = [];
|
|
13130
|
+
const date = resolveEpisodeDate(metadata, episode.datePublishedMs);
|
|
13131
|
+
const built = buildItemTitle(episode.title, metadata, config, date, warnings);
|
|
13132
|
+
const sanitizedTitle = mustSanitize(built.title, config.titleSanitize, "item title");
|
|
13133
|
+
if (!sanitizedTitle.ok)
|
|
13134
|
+
return sanitizedTitle;
|
|
13135
|
+
let title = built.explicit ? sanitizedTitle.value : config.titleMode === "headline" ? normalizeEpisodeTitle(sanitizedTitle.value, config.seriesName) : sanitizedTitle.value;
|
|
13136
|
+
const itunes = { ...episode.itunes ?? {} };
|
|
13137
|
+
if (itunes.season !== undefined && !isPositiveInt(itunes.season)) {
|
|
13138
|
+
return {
|
|
13139
|
+
ok: false,
|
|
13140
|
+
error: `invalid metadata.itunes.season: expected an integer \u2265 1, got ${String(itunes.season)}`
|
|
13141
|
+
};
|
|
13142
|
+
}
|
|
13143
|
+
if (itunes.episode !== undefined && !isPositiveInt(itunes.episode)) {
|
|
13144
|
+
return {
|
|
13145
|
+
ok: false,
|
|
13146
|
+
error: `invalid metadata.itunes.episode: expected an integer \u2265 1, got ${String(itunes.episode)}`
|
|
13147
|
+
};
|
|
13148
|
+
}
|
|
13149
|
+
if (config.autoEpisodeNumber) {
|
|
13150
|
+
const derived = date ? seasonAndEpisode(date) : undefined;
|
|
13151
|
+
if (derived) {
|
|
13152
|
+
if (itunes.season === undefined)
|
|
13153
|
+
itunes.season = derived.season;
|
|
13154
|
+
if (itunes.episode === undefined)
|
|
13155
|
+
itunes.episode = derived.episode;
|
|
13156
|
+
} else if (itunes.season === undefined || itunes.episode === undefined) {
|
|
13157
|
+
warnings.push(`${WARN_PREFIX} itunes season/episode left unset: episode date is unknown`);
|
|
13158
|
+
}
|
|
13159
|
+
}
|
|
13160
|
+
if (itunes.title) {
|
|
13161
|
+
const sanitizedItunes = mustSanitize(itunes.title, config.titleSanitize, "itunes:title");
|
|
13162
|
+
if (!sanitizedItunes.ok)
|
|
13163
|
+
return sanitizedItunes;
|
|
13164
|
+
itunes.title = sanitizedItunes.value;
|
|
13165
|
+
}
|
|
13166
|
+
const minutes = durationMinutes(episode.durationSeconds);
|
|
13167
|
+
let seo;
|
|
13168
|
+
if (config.seo) {
|
|
13169
|
+
const fitted = fitSeoTitle(title, built.keywords, config, date, built.explicit);
|
|
13170
|
+
if (!built.explicit && config.titleMode === "headline" && fitted.stem.length > 0)
|
|
13171
|
+
title = fitted.stem;
|
|
13172
|
+
const seoTitle = mustSanitize(fitted.seo, config.titleSanitize, "seo.title");
|
|
13173
|
+
if (!seoTitle.ok)
|
|
13174
|
+
return seoTitle;
|
|
13175
|
+
const described = descriptionInputs(metadata, config, date, minutes);
|
|
13176
|
+
if (described.short) {
|
|
13177
|
+
warnings.push(`${WARN_PREFIX} seo.description is ${codePointLength(described.description)} code points, under the ${config.descriptionMin} minimum; left unpadded`);
|
|
13178
|
+
}
|
|
13179
|
+
seo = { title: seoTitle.value, description: described.description };
|
|
13180
|
+
}
|
|
13181
|
+
let slug;
|
|
13182
|
+
if (config.slugMode === "pattern") {
|
|
13183
|
+
const rendered = renderSlug(config.slugPattern, date);
|
|
13184
|
+
if (rendered.error)
|
|
13185
|
+
return { ok: false, error: rendered.error };
|
|
13186
|
+
if (rendered.warning)
|
|
13187
|
+
warnings.push(`${WARN_PREFIX} ${rendered.warning}`);
|
|
13188
|
+
slug = rendered.slug;
|
|
13189
|
+
}
|
|
13190
|
+
if (config.linkField === "url" && episode.link) {
|
|
13191
|
+
warnings.push(`${WARN_PREFIX} PODCAST_PUB_LINK_FIELD=url sends metadata.articleUrl as the item url; microfeed uses that URL as the canonical and drops the episode from the podcast sitemap`);
|
|
13192
|
+
}
|
|
13193
|
+
let socialAlt;
|
|
13194
|
+
let socialLocalPath;
|
|
13195
|
+
let socialMime;
|
|
13196
|
+
let socialRemoteUrl;
|
|
13197
|
+
if (config.socialImage) {
|
|
13198
|
+
const source = podcastOf(metadata).socialImage;
|
|
13199
|
+
const alt = altText(config, date, built.keywords);
|
|
13200
|
+
if (typeof source !== "string" || source.trim().length === 0) {
|
|
13201
|
+
warnings.push(`${WARN_PREFIX} PODCAST_PUB_SOCIAL_IMAGE is on but metadata.podcast.socialImage is empty; skipping alt text`);
|
|
13202
|
+
} else if (source.trim().startsWith("http")) {
|
|
13203
|
+
socialAlt = alt;
|
|
13204
|
+
socialRemoteUrl = source.trim();
|
|
13205
|
+
} else {
|
|
13206
|
+
const bytes = readLocalBytes(source.trim());
|
|
13207
|
+
const dims = bytes ? readImageDimensions(bytes) : undefined;
|
|
13208
|
+
if (dims?.width !== 1200 || dims?.height !== 630) {
|
|
13209
|
+
warnings.push(`${WARN_PREFIX} social image skipped: expected a 1200\xD7630 PNG or JPEG, got ${dims ? `${dims.width}\xD7${dims.height}` : "an unreadable file"}`);
|
|
13210
|
+
} else {
|
|
13211
|
+
socialAlt = alt;
|
|
13212
|
+
socialLocalPath = source.trim();
|
|
13213
|
+
socialMime = dims.mime;
|
|
13214
|
+
}
|
|
13215
|
+
}
|
|
13216
|
+
}
|
|
13217
|
+
for (const line of warnings)
|
|
13218
|
+
echoError(line);
|
|
13219
|
+
return {
|
|
13220
|
+
ok: true,
|
|
13221
|
+
fields: {
|
|
13222
|
+
title,
|
|
13223
|
+
itunes,
|
|
13224
|
+
durationSeconds: roundDurationSeconds(episode.durationSeconds, config.roundDuration),
|
|
13225
|
+
...config.linkField === "url" && episode.link ? { link: episode.link } : {},
|
|
13226
|
+
...seo ? { seo } : {},
|
|
13227
|
+
...slug ? { slug } : {},
|
|
13228
|
+
warnings,
|
|
13229
|
+
...socialAlt ? { socialAlt } : {},
|
|
13230
|
+
...socialLocalPath ? { socialLocalPath, socialMime } : {},
|
|
13231
|
+
...socialRemoteUrl ? { socialRemoteUrl } : {}
|
|
13232
|
+
}
|
|
13233
|
+
};
|
|
13234
|
+
}
|
|
13235
|
+
function showNotesSummaryParagraph(input) {
|
|
13236
|
+
const date = isoFromRunDate(input.runDate);
|
|
13237
|
+
const metadata = {
|
|
13238
|
+
...date ? { runDate: date } : { runDate: input.runDate },
|
|
13239
|
+
podcast: {
|
|
13240
|
+
...input.refinedDescription ? { refinedDescription: input.refinedDescription } : {},
|
|
13241
|
+
...input.summaryClauses ? { summaryClauses: input.summaryClauses } : {},
|
|
13242
|
+
planItems: input.plan
|
|
13243
|
+
}
|
|
13244
|
+
};
|
|
13245
|
+
const described = descriptionInputs(metadata, input.config, date, durationMinutes(input.durationSeconds));
|
|
13246
|
+
const warnings = [];
|
|
13247
|
+
if (described.short) {
|
|
13248
|
+
warnings.push(`${WARN_PREFIX} seo.description is ${codePointLength(described.description)} code points, under the ${input.config.descriptionMin} minimum; left unpadded`);
|
|
13249
|
+
}
|
|
13250
|
+
const prefix = `${input.config.showName}\xB7`;
|
|
13251
|
+
const stripped = described.description.startsWith(prefix) ? described.description.slice(prefix.length) : described.description;
|
|
13252
|
+
const safeLabel = input.linkText.replaceAll("[", "").replaceAll("]", "").replaceAll("(", "").replaceAll(")", "");
|
|
13253
|
+
const markdown = `${stripped} \u5B8C\u6574\u56FE\u6587\u7248\uFF1A[${safeLabel}](${input.articleUrl})`;
|
|
13254
|
+
return { markdown, description: described.description, warnings };
|
|
13255
|
+
}
|
|
13256
|
+
function summaryParagraphHtml(description, showName, articleUrl, linkText) {
|
|
13257
|
+
const prefix = `${showName}\xB7`;
|
|
13258
|
+
const stripped = description.startsWith(prefix) ? description.slice(prefix.length) : description;
|
|
13259
|
+
const text = stripped.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
13260
|
+
if (!articleUrl)
|
|
13261
|
+
return `<p>${text}</p>`;
|
|
13262
|
+
const href = articleUrl.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "");
|
|
13263
|
+
const label = linkText.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
13264
|
+
return `<p>${text} \u5B8C\u6574\u56FE\u6587\u7248\uFF1A<a href="${href}" target="_blank" rel="noopener">${label}</a></p>`;
|
|
13265
|
+
}
|
|
13266
|
+
function stripMarkupTitle(raw) {
|
|
13267
|
+
return raw.replace(/<[^>]+>/g, " ").replace(/\u6765\u6E90/g, "").replace(/^\s*\d+\s*[.\u3001]\s*/, "").replace(/\s+/g, " ").trim();
|
|
13268
|
+
}
|
|
13269
|
+
function titlesFromContentHtml(html) {
|
|
13270
|
+
const timeline = [...html.matchAll(/class="kk-tl-title">([\s\S]*?)<\/div>/gi)].map((match) => stripMarkupTitle(match[1] ?? "")).filter((title) => title.length > 0);
|
|
13271
|
+
if (timeline.length > 0)
|
|
13272
|
+
return timeline;
|
|
13273
|
+
return [...html.matchAll(/<h2[^>]*>([\s\S]*?)<\/h2>/gi)].map((match) => stripMarkupTitle(match[1] ?? "")).filter((title) => title.length > 0);
|
|
13274
|
+
}
|
|
13275
|
+
function insertAfterTimeline(html, insertion) {
|
|
13276
|
+
const start = html.indexOf('<div class="kk-tl">');
|
|
13277
|
+
if (start < 0)
|
|
13278
|
+
return `${html.trim()}
|
|
13279
|
+
${insertion}`;
|
|
13280
|
+
let depth = 0;
|
|
13281
|
+
const tags = /<\/?div\b[^>]*>/gi;
|
|
13282
|
+
tags.lastIndex = start;
|
|
13283
|
+
let match = tags.exec(html);
|
|
13284
|
+
while (match) {
|
|
13285
|
+
depth += match[0].startsWith("</") ? -1 : 1;
|
|
13286
|
+
if (depth === 0) {
|
|
13287
|
+
const at = match.index + match[0].length;
|
|
13288
|
+
const head = html.slice(0, at).replace(/\s*$/, "");
|
|
13289
|
+
const tail = html.slice(at).replace(/^\s*/, "");
|
|
13290
|
+
return tail.length > 0 ? `${head}
|
|
13291
|
+
${insertion}
|
|
13292
|
+
${tail}` : `${head}
|
|
13293
|
+
${insertion}`;
|
|
13294
|
+
}
|
|
13295
|
+
match = tags.exec(html);
|
|
13296
|
+
}
|
|
13297
|
+
return `${html.trim()}
|
|
13298
|
+
${insertion}`;
|
|
13299
|
+
}
|
|
13300
|
+
function rewriteEpisodeHtml(html, summaryHtml, moveStyle) {
|
|
13301
|
+
let body = html;
|
|
13302
|
+
if (moveStyle) {
|
|
13303
|
+
const styleMatch = body.match(/<style>[\s\S]*?<\/style>/i);
|
|
13304
|
+
if (styleMatch && styleMatch.index !== undefined) {
|
|
13305
|
+
const style = styleMatch[0];
|
|
13306
|
+
const without = `${body.slice(0, styleMatch.index)}${body.slice(styleMatch.index + style.length)}`;
|
|
13307
|
+
body = insertAfterTimeline(without, style);
|
|
13308
|
+
}
|
|
13309
|
+
}
|
|
13310
|
+
const trimmed = body.trim();
|
|
13311
|
+
if (summaryHtml.length > 0 && (trimmed === summaryHtml || trimmed.startsWith(`${summaryHtml}
|
|
13312
|
+
`) || trimmed.startsWith(`${summaryHtml}<`))) {
|
|
13313
|
+
return trimmed;
|
|
13314
|
+
}
|
|
13315
|
+
return `${summaryHtml}
|
|
13316
|
+
${trimmed}`;
|
|
13317
|
+
}
|
|
13318
|
+
function dateFromFeedItem(item) {
|
|
13319
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
13320
|
+
const inTitle = /(\d{4}-\d{2}-\d{2})/.exec(title);
|
|
13321
|
+
if (inTitle?.[1] && isoFromRunDate(inTitle[1]))
|
|
13322
|
+
return inTitle[1];
|
|
13323
|
+
const msField = item.date_published_ms;
|
|
13324
|
+
if (typeof msField === "number")
|
|
13325
|
+
return isoFromLosAngeles(msField);
|
|
13326
|
+
const published = item.date_published;
|
|
13327
|
+
if (typeof published === "number")
|
|
13328
|
+
return isoFromLosAngeles(published);
|
|
13329
|
+
if (typeof published === "string") {
|
|
13330
|
+
const parsed = Date.parse(published);
|
|
13331
|
+
if (!Number.isNaN(parsed))
|
|
13332
|
+
return isoFromLosAngeles(parsed);
|
|
13333
|
+
}
|
|
13334
|
+
return;
|
|
13335
|
+
}
|
|
13336
|
+
|
|
13337
|
+
// ../../plugins/publishings/podcast-pub/src/backfill.ts
|
|
13338
|
+
var TARGET = "podcast-pub";
|
|
13339
|
+
function fail(error) {
|
|
13340
|
+
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
13341
|
+
}
|
|
13342
|
+
function asRecord2(value) {
|
|
13343
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value))
|
|
13344
|
+
return value;
|
|
13345
|
+
return;
|
|
13346
|
+
}
|
|
13347
|
+
function flag(value) {
|
|
13348
|
+
return value === true || value === "true" || value === "1";
|
|
13349
|
+
}
|
|
13350
|
+
function positiveInt(value) {
|
|
13351
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1)
|
|
13352
|
+
return value;
|
|
13353
|
+
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
13354
|
+
const parsed = Number(value);
|
|
13355
|
+
if (Number.isInteger(parsed) && parsed >= 1)
|
|
13356
|
+
return parsed;
|
|
13357
|
+
}
|
|
13358
|
+
return;
|
|
13359
|
+
}
|
|
13360
|
+
function parseOverrides(raw) {
|
|
13361
|
+
if (raw === undefined)
|
|
13362
|
+
return { overrides: new Map };
|
|
13363
|
+
if (!Array.isArray(raw))
|
|
13364
|
+
return {
|
|
13365
|
+
error: "invalid options.overrides: expected an array of {id, title?, titleKeywords?, summaryClauses?}"
|
|
13366
|
+
};
|
|
13367
|
+
const overrides = new Map;
|
|
13368
|
+
for (const entry of raw) {
|
|
13369
|
+
const record = asRecord2(entry);
|
|
13370
|
+
if (!record || typeof record.id !== "string" || record.id.trim().length === 0) {
|
|
13371
|
+
return { error: "invalid options.overrides: each entry needs a string id" };
|
|
13372
|
+
}
|
|
13373
|
+
const keywords = record.titleKeywords;
|
|
13374
|
+
if (keywords !== undefined && (!Array.isArray(keywords) || keywords.some((keyword) => typeof keyword !== "string"))) {
|
|
13375
|
+
return { error: `invalid options.overrides titleKeywords for "${record.id}": expected string[]` };
|
|
13376
|
+
}
|
|
13377
|
+
const clauses = record.summaryClauses;
|
|
13378
|
+
if (clauses !== undefined && (!Array.isArray(clauses) || clauses.some((clause) => typeof clause !== "string"))) {
|
|
13379
|
+
return { error: `invalid options.overrides summaryClauses for "${record.id}": expected string[]` };
|
|
13380
|
+
}
|
|
13381
|
+
if (record.title !== undefined && typeof record.title !== "string") {
|
|
13382
|
+
return { error: `invalid options.overrides title for "${record.id}": expected a string` };
|
|
13383
|
+
}
|
|
13384
|
+
overrides.set(record.id, {
|
|
13385
|
+
id: record.id,
|
|
13386
|
+
...typeof record.title === "string" ? { title: record.title } : {},
|
|
13387
|
+
...Array.isArray(keywords) ? { titleKeywords: keywords.filter((keyword) => typeof keyword === "string") } : {},
|
|
13388
|
+
...Array.isArray(clauses) ? { summaryClauses: clauses.filter((clause) => typeof clause === "string") } : {}
|
|
13389
|
+
});
|
|
13390
|
+
}
|
|
13391
|
+
return { overrides };
|
|
13392
|
+
}
|
|
13393
|
+
function parseFlags(options) {
|
|
13394
|
+
const overrides = parseOverrides(options.overrides);
|
|
13395
|
+
if ("error" in overrides)
|
|
13396
|
+
return overrides;
|
|
13397
|
+
const since = options.since === undefined ? undefined : String(options.since);
|
|
13398
|
+
const until = options.until === undefined ? undefined : String(options.until);
|
|
13399
|
+
if (since !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(since)) {
|
|
13400
|
+
return { error: `invalid options.since "${since}": expected YYYY-MM-DD` };
|
|
13401
|
+
}
|
|
13402
|
+
if (until !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(until)) {
|
|
13403
|
+
return { error: `invalid options.until "${until}": expected YYYY-MM-DD` };
|
|
13404
|
+
}
|
|
13405
|
+
let ids;
|
|
13406
|
+
if (options.ids !== undefined) {
|
|
13407
|
+
if (!Array.isArray(options.ids) || options.ids.some((id) => typeof id !== "string")) {
|
|
13408
|
+
return { error: "invalid options.ids: expected an array of item id strings" };
|
|
13409
|
+
}
|
|
13410
|
+
ids = new Set(options.ids);
|
|
13411
|
+
}
|
|
13412
|
+
return {
|
|
13413
|
+
flags: {
|
|
13414
|
+
apply: flag(options.apply),
|
|
13415
|
+
retitle: flag(options.retitle),
|
|
13416
|
+
rewriteBody: flag(options.rewriteBody),
|
|
13417
|
+
applySlug: flag(options.applySlug),
|
|
13418
|
+
...since ? { since } : {},
|
|
13419
|
+
...until ? { until } : {},
|
|
13420
|
+
...ids ? { ids } : {},
|
|
13421
|
+
...typeof options.out === "string" && options.out.length > 0 ? { out: options.out } : {},
|
|
13422
|
+
...typeof options.plan === "string" && options.plan.length > 0 ? { planPath: options.plan } : {},
|
|
13423
|
+
overrides: overrides.overrides
|
|
13424
|
+
}
|
|
13425
|
+
};
|
|
13426
|
+
}
|
|
13427
|
+
function microfeedOf(item) {
|
|
13428
|
+
return asRecord2(item._microfeed) ?? {};
|
|
13429
|
+
}
|
|
13430
|
+
function durationOf(item) {
|
|
13431
|
+
const attachments = item.attachments;
|
|
13432
|
+
if (!Array.isArray(attachments))
|
|
13433
|
+
return;
|
|
13434
|
+
const first = asRecord2(attachments[0]);
|
|
13435
|
+
const seconds = first?.duration_in_seconds;
|
|
13436
|
+
return typeof seconds === "number" && Number.isFinite(seconds) ? seconds : undefined;
|
|
13437
|
+
}
|
|
13438
|
+
function articleUrlOf(html) {
|
|
13439
|
+
const marked = /\u5B8C\u6574\u56FE\u6587\u7248\uFF1A<a href="([^"]+)"/.exec(html);
|
|
13440
|
+
if (marked?.[1])
|
|
13441
|
+
return marked[1];
|
|
13442
|
+
const any = /href="(https?:\/\/[^"]+)"/.exec(html);
|
|
13443
|
+
return any?.[1];
|
|
13444
|
+
}
|
|
13445
|
+
function inWindow(date, flags) {
|
|
13446
|
+
if (!flags.since && !flags.until)
|
|
13447
|
+
return true;
|
|
13448
|
+
if (!date)
|
|
13449
|
+
return false;
|
|
13450
|
+
if (flags.since && date < flags.since)
|
|
13451
|
+
return false;
|
|
13452
|
+
if (flags.until && date > flags.until)
|
|
13453
|
+
return false;
|
|
13454
|
+
return true;
|
|
13455
|
+
}
|
|
13456
|
+
function planBackfillItem(item, config, flags, override) {
|
|
13457
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
13458
|
+
if (id.length === 0)
|
|
13459
|
+
return { error: "backfill-seo: a feed item is missing id" };
|
|
13460
|
+
const currentTitle = typeof item.title === "string" ? item.title : "";
|
|
13461
|
+
const micro = microfeedOf(item);
|
|
13462
|
+
const currentSeo = asRecord2(micro.seo);
|
|
13463
|
+
const currentSeoTitle = typeof currentSeo?.title === "string" ? currentSeo.title : undefined;
|
|
13464
|
+
const currentSeoDescription = typeof currentSeo?.description === "string" ? currentSeo.description : undefined;
|
|
13465
|
+
const currentSeason = positiveInt(micro["itunes:season"]);
|
|
13466
|
+
const currentEpisode = positiveInt(micro["itunes:episode"]);
|
|
13467
|
+
const currentSlug = typeof micro.slug === "string" ? micro.slug : undefined;
|
|
13468
|
+
const date = dateFromFeedItem(item);
|
|
13469
|
+
const html = typeof item.content_html === "string" ? item.content_html : "";
|
|
13470
|
+
const titles = titlesFromContentHtml(html);
|
|
13471
|
+
const clauses = override?.summaryClauses ?? titles.map((title) => clauseFromTitle(title)).filter((clause) => clause.length > 0);
|
|
13472
|
+
const keywords = override?.titleKeywords ?? titles.filter((title) => title.length > 0);
|
|
13473
|
+
const metadata = {
|
|
13474
|
+
...date ? { runDate: date } : {},
|
|
13475
|
+
...durationOf(item) !== undefined ? { durationSeconds: durationOf(item) } : {},
|
|
13476
|
+
podcast: {
|
|
13477
|
+
titleMode: "headline",
|
|
13478
|
+
...override?.title ? { title: override.title } : {},
|
|
13479
|
+
...keywords.length > 0 ? { titleKeywords: keywords.slice(0, config.titleMaxKeywords) } : {},
|
|
13480
|
+
...clauses.length > 0 ? { summaryClauses: clauses } : {}
|
|
13481
|
+
}
|
|
13482
|
+
};
|
|
13483
|
+
const prepared = prepareEpisodeFields({
|
|
13484
|
+
title: currentTitle,
|
|
13485
|
+
durationSeconds: durationOf(item),
|
|
13486
|
+
itunes: {
|
|
13487
|
+
...currentSeason !== undefined ? { season: currentSeason } : {},
|
|
13488
|
+
...currentEpisode !== undefined ? { episode: currentEpisode } : {}
|
|
13489
|
+
}
|
|
13490
|
+
}, metadata, {
|
|
13491
|
+
...config,
|
|
13492
|
+
seo: true,
|
|
13493
|
+
titleMode: "headline",
|
|
13494
|
+
slugMode: "auto",
|
|
13495
|
+
linkField: "none",
|
|
13496
|
+
socialImage: false,
|
|
13497
|
+
autoEpisodeNumber: true
|
|
13498
|
+
});
|
|
13499
|
+
if (!prepared.ok)
|
|
13500
|
+
return { error: `${id}: ${prepared.error}` };
|
|
13501
|
+
const violations = [];
|
|
13502
|
+
if (!date)
|
|
13503
|
+
violations.push("episode date is unknown");
|
|
13504
|
+
const patch = {};
|
|
13505
|
+
const nextMicro = {};
|
|
13506
|
+
if (flags.retitle && prepared.fields.title !== currentTitle)
|
|
13507
|
+
patch.title = prepared.fields.title;
|
|
13508
|
+
const seo = prepared.fields.seo;
|
|
13509
|
+
if (seo) {
|
|
13510
|
+
const seoPatch = {};
|
|
13511
|
+
if (seo.title && seo.title !== currentSeoTitle)
|
|
13512
|
+
seoPatch.title = seo.title;
|
|
13513
|
+
if (seo.description && seo.description !== currentSeoDescription)
|
|
13514
|
+
seoPatch.description = seo.description;
|
|
13515
|
+
if (seoPatch.title || seoPatch.description)
|
|
13516
|
+
nextMicro.seo = seoPatch;
|
|
13517
|
+
if (seo.description && codePointLength(seo.description) < config.descriptionMin) {
|
|
13518
|
+
violations.push(`seo.description is ${codePointLength(seo.description)} code points, under ${config.descriptionMin}`);
|
|
13519
|
+
}
|
|
13520
|
+
if (seo.description && /^\s*(\d|#|[-*])/.test(seo.description)) {
|
|
13521
|
+
violations.push("seo.description starts with a forbidden character");
|
|
13522
|
+
}
|
|
13523
|
+
}
|
|
13524
|
+
const numbered = prepared.fields.itunes;
|
|
13525
|
+
if (numbered?.season !== undefined && currentSeason === undefined)
|
|
13526
|
+
nextMicro["itunes:season"] = numbered.season;
|
|
13527
|
+
if (numbered?.episode !== undefined && currentEpisode === undefined)
|
|
13528
|
+
nextMicro["itunes:episode"] = numbered.episode;
|
|
13529
|
+
if (flags.applySlug && date) {
|
|
13530
|
+
const slug = renderSlug(config.slugPattern, date);
|
|
13531
|
+
if (slug.slug && slug.slug !== currentSlug && isValidItemSlug(slug.slug))
|
|
13532
|
+
nextMicro.slug = slug.slug;
|
|
13533
|
+
if (slug.error)
|
|
13534
|
+
violations.push(slug.error);
|
|
13535
|
+
}
|
|
13536
|
+
if (flags.rewriteBody && html.length > 0 && seo?.description) {
|
|
13537
|
+
const summary = summaryParagraphHtml(seo.description, config.showName, articleUrlOf(html), "\u672C\u671F\u5B8C\u6574\u6587\u7AE0");
|
|
13538
|
+
const rewritten = rewriteEpisodeHtml(html, summary, config.timelineStyle !== "omit");
|
|
13539
|
+
if (rewritten !== html.trim())
|
|
13540
|
+
patch.content_html = rewritten;
|
|
13541
|
+
}
|
|
13542
|
+
if (Object.keys(nextMicro).length > 0)
|
|
13543
|
+
patch._microfeed = nextMicro;
|
|
13544
|
+
const proposedTitle = typeof patch.title === "string" ? patch.title : currentTitle;
|
|
13545
|
+
const proposedSeo = asRecord2(nextMicro.seo);
|
|
13546
|
+
return {
|
|
13547
|
+
id,
|
|
13548
|
+
...date ? { date } : {},
|
|
13549
|
+
patch,
|
|
13550
|
+
counts: {
|
|
13551
|
+
...proposedTitle ? { title: codePointLength(proposedTitle) } : {},
|
|
13552
|
+
...typeof proposedSeo?.title === "string" ? { seoTitle: codePointLength(proposedSeo.title) } : {},
|
|
13553
|
+
...typeof proposedSeo?.description === "string" ? { seoDescription: codePointLength(proposedSeo.description) } : {}
|
|
13554
|
+
},
|
|
13555
|
+
violations,
|
|
13556
|
+
current: {
|
|
13557
|
+
...currentTitle ? { title: currentTitle } : {},
|
|
13558
|
+
...currentSeoTitle ? { seoTitle: currentSeoTitle } : {},
|
|
13559
|
+
...currentSeoDescription ? { seoDescription: currentSeoDescription } : {},
|
|
13560
|
+
...currentSeason !== undefined ? { season: currentSeason } : {},
|
|
13561
|
+
...currentEpisode !== undefined ? { episode: currentEpisode } : {},
|
|
13562
|
+
...currentSlug ? { slug: currentSlug } : {}
|
|
13563
|
+
}
|
|
13564
|
+
};
|
|
13565
|
+
}
|
|
13566
|
+
function describeEntry(entry) {
|
|
13567
|
+
const seo = asRecord2(asRecord2(entry.patch._microfeed)?.seo);
|
|
13568
|
+
const parts = [
|
|
13569
|
+
`${entry.id}:`,
|
|
13570
|
+
entry.date ? `date ${entry.date}` : "date unknown",
|
|
13571
|
+
`title ${JSON.stringify(entry.current.title ?? "")} \u2192 ${JSON.stringify(entry.patch.title ?? entry.current.title ?? "")} (${entry.counts.title ?? 0})`,
|
|
13572
|
+
`seo.title ${JSON.stringify(entry.current.seoTitle ?? "")} \u2192 ${JSON.stringify(seo?.title ?? entry.current.seoTitle ?? "")} (${entry.counts.seoTitle ?? codePointLength(String(entry.current.seoTitle ?? ""))})`,
|
|
13573
|
+
`seo.description (${entry.counts.seoDescription ?? codePointLength(String(entry.current.seoDescription ?? ""))})`
|
|
13574
|
+
];
|
|
13575
|
+
if (entry.violations.length > 0)
|
|
13576
|
+
parts.push(`violations: ${entry.violations.join("; ")}`);
|
|
13577
|
+
return parts.join(" | ");
|
|
13578
|
+
}
|
|
13579
|
+
async function writePlan(path, entries, apply) {
|
|
13580
|
+
await ensureDirForFile(path);
|
|
13581
|
+
await writeJsonFile(path, { dryRun: !apply, items: entries });
|
|
13582
|
+
}
|
|
13583
|
+
async function loadPlan(path) {
|
|
13584
|
+
try {
|
|
13585
|
+
const parsed = await readJsonFile(path);
|
|
13586
|
+
const record = asRecord2(parsed);
|
|
13587
|
+
const items = record?.items;
|
|
13588
|
+
if (!Array.isArray(items))
|
|
13589
|
+
return { error: `invalid backfill plan ${path}: expected { items: [...] }` };
|
|
13590
|
+
const entries = [];
|
|
13591
|
+
for (const item of items) {
|
|
13592
|
+
const row = asRecord2(item);
|
|
13593
|
+
if (!row || typeof row.id !== "string" || !asRecord2(row.patch)) {
|
|
13594
|
+
return { error: `invalid backfill plan ${path}: each item needs id and patch` };
|
|
13595
|
+
}
|
|
13596
|
+
entries.push(row);
|
|
13597
|
+
}
|
|
13598
|
+
return { entries };
|
|
13599
|
+
} catch (error) {
|
|
13600
|
+
return { error: `invalid backfill plan ${path}: ${error instanceof Error ? error.message : String(error)}` };
|
|
13601
|
+
}
|
|
13602
|
+
}
|
|
13603
|
+
function withoutSlug(micro) {
|
|
13604
|
+
const rest = {};
|
|
13605
|
+
for (const [key, value] of Object.entries(micro)) {
|
|
13606
|
+
if (key !== "slug")
|
|
13607
|
+
rest[key] = value;
|
|
13608
|
+
}
|
|
13609
|
+
return rest;
|
|
13610
|
+
}
|
|
13611
|
+
async function putPatch(client, entry, warnings) {
|
|
13612
|
+
const micro = asRecord2(entry.patch._microfeed);
|
|
13613
|
+
const slug = typeof micro?.slug === "string" ? micro.slug : undefined;
|
|
13614
|
+
if (!slug || !micro) {
|
|
13615
|
+
await client.updateItem(entry.id, entry.patch);
|
|
13616
|
+
return;
|
|
13617
|
+
}
|
|
13618
|
+
try {
|
|
13619
|
+
await client.updateItem(entry.id, entry.patch);
|
|
13620
|
+
} catch (error) {
|
|
13621
|
+
if (!(error instanceof MicrofeedRequestError) || error.status !== 409)
|
|
13622
|
+
throw error;
|
|
13623
|
+
const retry = `${slug}-2`;
|
|
13624
|
+
try {
|
|
13625
|
+
await client.updateItem(entry.id, { ...entry.patch, _microfeed: { ...micro, slug: retry } });
|
|
13626
|
+
warn(`slug "${slug}" for ${entry.id} conflicted (HTTP 409); retried as ${retry}`, warnings);
|
|
13627
|
+
} catch (retryError) {
|
|
13628
|
+
if (!(retryError instanceof MicrofeedRequestError) || retryError.status !== 409)
|
|
13629
|
+
throw retryError;
|
|
13630
|
+
const rest = withoutSlug(micro);
|
|
13631
|
+
if (Object.keys(rest).length === 0) {
|
|
13632
|
+
const { _microfeed: _dropped, ...other } = entry.patch;
|
|
13633
|
+
if (Object.keys(other).length === 0) {
|
|
13634
|
+
warn(`slug "${slug}" for ${entry.id} conflicted twice (HTTP 409); skipped the slug and no other fields changed`, warnings);
|
|
13635
|
+
return;
|
|
13636
|
+
}
|
|
13637
|
+
await client.updateItem(entry.id, other);
|
|
13638
|
+
} else {
|
|
13639
|
+
await client.updateItem(entry.id, { ...entry.patch, _microfeed: rest });
|
|
13640
|
+
}
|
|
13641
|
+
warn(`slug "${slug}" for ${entry.id} conflicted twice (HTTP 409); applied the patch without a slug`, warnings);
|
|
13642
|
+
}
|
|
13643
|
+
}
|
|
13644
|
+
}
|
|
13645
|
+
async function runBackfill(client, options, config) {
|
|
13646
|
+
const parsed = parseFlags(options);
|
|
13647
|
+
if ("error" in parsed)
|
|
13648
|
+
return fail(parsed.error);
|
|
13649
|
+
const flags = parsed.flags;
|
|
13650
|
+
const warnings = [];
|
|
13651
|
+
let entries;
|
|
13652
|
+
if (flags.apply && flags.planPath) {
|
|
13653
|
+
const loaded = await loadPlan(flags.planPath);
|
|
13654
|
+
if ("error" in loaded)
|
|
13655
|
+
return fail(loaded.error);
|
|
13656
|
+
entries = loaded.entries.filter((entry) => !flags.ids || flags.ids.has(entry.id));
|
|
13657
|
+
} else {
|
|
13658
|
+
const feed = await client.listItems();
|
|
13659
|
+
entries = [];
|
|
13660
|
+
for (const item of feed.items ?? []) {
|
|
13661
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
13662
|
+
if (flags.ids && !flags.ids.has(id))
|
|
13663
|
+
continue;
|
|
13664
|
+
const date = dateFromFeedItem(item);
|
|
13665
|
+
if (!inWindow(date, flags))
|
|
13666
|
+
continue;
|
|
13667
|
+
const planned = planBackfillItem(item, config, flags, flags.overrides.get(id));
|
|
13668
|
+
if ("error" in planned)
|
|
13669
|
+
return fail(planned.error);
|
|
13670
|
+
entries.push(planned);
|
|
13671
|
+
}
|
|
13672
|
+
}
|
|
13673
|
+
for (const entry of entries)
|
|
13674
|
+
echo(describeEntry(entry));
|
|
13675
|
+
if (flags.out)
|
|
13676
|
+
await writePlan(flags.out, entries, flags.apply);
|
|
13677
|
+
if (!flags.apply) {
|
|
13678
|
+
return ResultSchema.parse({
|
|
13679
|
+
ok: true,
|
|
13680
|
+
target: TARGET,
|
|
13681
|
+
message: `backfill-seo dry-run for ${entries.length} item(s); no writes`,
|
|
13682
|
+
metadata: { dryRun: true, items: entries, warnings }
|
|
13683
|
+
});
|
|
13684
|
+
}
|
|
13685
|
+
let written = 0;
|
|
13686
|
+
for (const entry of entries) {
|
|
13687
|
+
if (Object.keys(entry.patch).length === 0)
|
|
13688
|
+
continue;
|
|
13689
|
+
await putPatch(client, entry, warnings);
|
|
13690
|
+
written += 1;
|
|
13691
|
+
}
|
|
13692
|
+
return ResultSchema.parse({
|
|
13693
|
+
ok: true,
|
|
13694
|
+
target: TARGET,
|
|
13695
|
+
message: `backfill-seo applied ${written} item(s)`,
|
|
13696
|
+
metadata: { dryRun: false, applied: written, items: entries, warnings }
|
|
13697
|
+
});
|
|
13698
|
+
}
|
|
13699
|
+
|
|
13700
|
+
// ../../plugins/publishings/podcast-pub/src/map.ts
|
|
13701
|
+
import { extname } from "path";
|
|
13702
|
+
|
|
13703
|
+
// ../../plugins/publishings/podcast-pub/src/show-notes.ts
|
|
13704
|
+
function escapeHtml(value) {
|
|
13705
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
13706
|
+
}
|
|
13707
|
+
function renderInline(escaped) {
|
|
13708
|
+
let out = "";
|
|
13709
|
+
let rest = escaped;
|
|
13710
|
+
for (;; ) {
|
|
13711
|
+
const match = rest.match(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/);
|
|
13712
|
+
if (!match || match.index === undefined)
|
|
13713
|
+
break;
|
|
13714
|
+
out += `${rest.slice(0, match.index)}<a href="${match[2]}" target="_blank" rel="noopener">${match[1]}</a>`;
|
|
13715
|
+
rest = rest.slice(match.index + match[0].length);
|
|
13716
|
+
}
|
|
13717
|
+
return (out + rest).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
13718
|
+
}
|
|
13719
|
+
function normalizeForMatch(text) {
|
|
13720
|
+
return text.toLowerCase().replace(/[^\p{Script=Han}\p{L}\p{N}]+/gu, "");
|
|
13721
|
+
}
|
|
13722
|
+
function formatOffset(seconds) {
|
|
13723
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
13724
|
+
return "--:--";
|
|
13725
|
+
const m = Math.floor(seconds / 60);
|
|
13726
|
+
const s = Math.round(seconds % 60);
|
|
13727
|
+
return s === 60 ? `${m + 1}:00` : `${m}:${String(s).padStart(2, "0")}`;
|
|
13728
|
+
}
|
|
13729
|
+
function computeAudioOffsets(docs, segments) {
|
|
13730
|
+
const fallback = () => {
|
|
13731
|
+
const chars = docs.map((d) => Math.max((d.body ?? "").trim().length, 1));
|
|
13732
|
+
const total = chars.reduce((a, b) => a + b, 0);
|
|
13733
|
+
const sumDuration = (segments ?? []).reduce((a, s) => a + (s.duration ?? 0), 0);
|
|
13734
|
+
let acc = 0;
|
|
13735
|
+
return chars.map((c) => {
|
|
13736
|
+
const at = acc;
|
|
13737
|
+
acc += c / total * sumDuration;
|
|
13738
|
+
return at;
|
|
13739
|
+
});
|
|
13740
|
+
};
|
|
13741
|
+
if (!segments || segments.length < 2)
|
|
13742
|
+
return fallback();
|
|
13743
|
+
const starts = [];
|
|
13744
|
+
let elapsed = 0;
|
|
13745
|
+
const marks = segments.map((s) => {
|
|
13746
|
+
const at = elapsed;
|
|
13747
|
+
elapsed += s.duration ?? 0;
|
|
13748
|
+
return { at, text: normalizeForMatch(s.transcription ?? "") };
|
|
13749
|
+
});
|
|
13750
|
+
let cursor = 1;
|
|
13751
|
+
for (const d of docs) {
|
|
13752
|
+
const title = normalizeForMatch(d.title ?? "");
|
|
13753
|
+
const needle = title.slice(0, 12);
|
|
13754
|
+
const hit = needle.length >= 4 ? marks.findIndex((m, i) => i >= cursor && m.text.includes(needle)) : -1;
|
|
13755
|
+
if (hit >= 0) {
|
|
13756
|
+
starts.push(marks[hit]?.at ?? 0);
|
|
13757
|
+
cursor = hit + 1;
|
|
13758
|
+
} else {
|
|
13759
|
+
starts.push(Number.NaN);
|
|
13760
|
+
}
|
|
13761
|
+
}
|
|
13762
|
+
if (starts.some((v) => !Number.isFinite(v)))
|
|
13763
|
+
return fallback();
|
|
13764
|
+
return starts;
|
|
13765
|
+
}
|
|
13766
|
+
var isHttpUrl = (u) => /^https?:\/\//i.test(u.trim());
|
|
13767
|
+
var TIMELINE_STYLE = `<style>
|
|
13768
|
+
.kk-tl{margin:1em 0}
|
|
13769
|
+
.kk-tl-row{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid rgba(128,128,128,.25)}
|
|
13770
|
+
.kk-tl-t{flex:0 0 56px;font-variant-numeric:tabular-nums;font-weight:700;color:#0969da}
|
|
13771
|
+
.kk-tl-body{flex:1;min-width:0}
|
|
13772
|
+
.kk-tl-title{font-weight:600}
|
|
13773
|
+
.kk-tl-brief{margin:4px 0 0;color:#666;font-size:.95em}
|
|
13774
|
+
</style>`;
|
|
13775
|
+
var TIMELINE_TAG = /^<\/?(?:div|p|a)(?:\s+(?:class="kk-tl(?:-[a-z]+)?"|href="https?:\/\/[^"<>\s]*"|target="_blank"|rel="noopener"))*\s*>$/;
|
|
13776
|
+
function isGeneratedTimeline(block) {
|
|
13777
|
+
const rest = block.replace(TIMELINE_STYLE, "");
|
|
13778
|
+
if (rest.includes("<style") || rest.includes("<!--"))
|
|
13779
|
+
return false;
|
|
13780
|
+
return (rest.match(/<[^>]*>/g) ?? []).every((tag) => TIMELINE_TAG.test(tag)) && !/<(?![^>]*>)/.test(rest);
|
|
13781
|
+
}
|
|
13782
|
+
function renderTimelineHtml(docs, offsets, articleUrl, style) {
|
|
13783
|
+
const rows = docs.map((d, i) => {
|
|
13784
|
+
const title = escapeHtml(d.title ?? `\u6761\u76EE ${i + 1}`);
|
|
13785
|
+
const brief = escapeHtml(itemBrief(d));
|
|
13786
|
+
const at = formatOffset(offsets[i] ?? Number.NaN);
|
|
13787
|
+
const link = d.sourceUri && isHttpUrl(d.sourceUri) ? ` <a href="${escapeHtml(d.sourceUri)}" target="_blank" rel="noopener">\u6765\u6E90</a>` : "";
|
|
13788
|
+
return `
|
|
13789
|
+
<div class="kk-tl-row"><div class="kk-tl-t">${at}</div><div class="kk-tl-body"><div class="kk-tl-title">${i + 1}. ${title}${link}</div><p class="kk-tl-brief">${brief}</p></div></div>`;
|
|
13790
|
+
}).join("");
|
|
13791
|
+
const articleTail = isHttpUrl(articleUrl) ? `<p>\u5B8C\u6574\u56FE\u6587\u7248\uFF1A<a href="${escapeHtml(articleUrl)}" target="_blank" rel="noopener">${escapeHtml("\u672C\u671F\u5B8C\u6574\u6587\u7AE0")}</a></p>` : `<p>${escapeHtml("\u672C\u671F\u5B8C\u6574\u6587\u7AE0")}</p>`;
|
|
13792
|
+
const styleBlock = style === "omit" ? "" : `${TIMELINE_STYLE}
|
|
13793
|
+
`;
|
|
13794
|
+
const before = style === "before" ? styleBlock : "";
|
|
13795
|
+
const after = style === "after" ? styleBlock : "";
|
|
13796
|
+
return `<!--kk:timeline-->
|
|
13797
|
+
${before}<div class="kk-tl">${rows}
|
|
13798
|
+
</div>
|
|
13799
|
+
${after}${articleTail}
|
|
13800
|
+
<!--/kk:timeline-->`;
|
|
12406
13801
|
}
|
|
12407
13802
|
function renderShowNotesHtml(markdown) {
|
|
12408
13803
|
const blocks = [];
|
|
@@ -12476,7 +13871,7 @@ function itemBrief(d) {
|
|
|
12476
13871
|
return meta.brief.trim();
|
|
12477
13872
|
return (d.body ?? "").trim().split(/(?<=[\u3002\uFF01\uFF1F])\s*/).slice(0, 2).join("");
|
|
12478
13873
|
}
|
|
12479
|
-
function renderShowNotesMarkdown(docs, opts) {
|
|
13874
|
+
function renderShowNotesMarkdown(docs, opts, warnings) {
|
|
12480
13875
|
const slugify = (t) => {
|
|
12481
13876
|
const s = t.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
|
|
12482
13877
|
return s.length > 0 ? s : "post";
|
|
@@ -12487,6 +13882,9 @@ function renderShowNotesMarkdown(docs, opts) {
|
|
|
12487
13882
|
const postLocale = segments.find((s) => ["zh", "en", "ja"].includes(s)) ?? "zh";
|
|
12488
13883
|
const localePrefix = postLocale === "en" ? "" : `/${postLocale}`;
|
|
12489
13884
|
const articleUrl = postSlug.length > 0 ? `${baseUrl}${localePrefix}/posts/${postSlug}` : `${baseUrl}/zh/posts/${opts.runDate}-${slugify(opts.title)}`;
|
|
13885
|
+
if (opts.briefMode === true && postSlug.length === 0) {
|
|
13886
|
+
warn("brief mode requested but ARTICLE_POST_PATH is empty; using legacy layout", warnings);
|
|
13887
|
+
}
|
|
12490
13888
|
const briefMode = opts.briefMode === true && postSlug.length > 0;
|
|
12491
13889
|
const sourceLabel = (d) => {
|
|
12492
13890
|
const m = d.metadata ?? {};
|
|
@@ -12500,8 +13898,42 @@ function renderShowNotesMarkdown(docs, opts) {
|
|
|
12500
13898
|
};
|
|
12501
13899
|
const offsets = computeAudioOffsets(docs, opts.audioSegments);
|
|
12502
13900
|
const lines = [`# ${opts.title}`, ""];
|
|
13901
|
+
const summaryOn = opts.summaryParagraph !== false;
|
|
13902
|
+
if (summaryOn) {
|
|
13903
|
+
const segmentSeconds = opts.audioSegments?.reduce((sum, segment) => sum + (segment.duration ?? 0), 0);
|
|
13904
|
+
const durationSeconds = opts.durationSeconds ?? (segmentSeconds && segmentSeconds > 0 ? segmentSeconds : undefined);
|
|
13905
|
+
const plan = docs.map((doc) => {
|
|
13906
|
+
const title = doc.title?.trim() ?? "";
|
|
13907
|
+
const brief = itemBrief(doc);
|
|
13908
|
+
return brief.length > 0 ? { title, brief } : { title };
|
|
13909
|
+
});
|
|
13910
|
+
const config = {
|
|
13911
|
+
...SEO_DEFAULTS,
|
|
13912
|
+
...opts.seriesName ? { seriesName: opts.seriesName } : {},
|
|
13913
|
+
...opts.showName ? { showName: opts.showName } : {},
|
|
13914
|
+
...opts.descriptionMin !== undefined ? { descriptionMin: opts.descriptionMin } : {},
|
|
13915
|
+
...opts.descriptionMax !== undefined ? { descriptionMax: opts.descriptionMax } : {},
|
|
13916
|
+
...opts.descriptionSource ? { descriptionSource: opts.descriptionSource } : {},
|
|
13917
|
+
...opts.descriptionPattern ? { descriptionPattern: opts.descriptionPattern } : {},
|
|
13918
|
+
summaryParagraph: true,
|
|
13919
|
+
timelineStyle: opts.timelineStyle ?? "after"
|
|
13920
|
+
};
|
|
13921
|
+
const summary = showNotesSummaryParagraph({
|
|
13922
|
+
config,
|
|
13923
|
+
runDate: opts.runDate,
|
|
13924
|
+
...durationSeconds !== undefined ? { durationSeconds } : {},
|
|
13925
|
+
plan,
|
|
13926
|
+
...opts.refinedDescription ? { refinedDescription: opts.refinedDescription } : {},
|
|
13927
|
+
...opts.summaryClauses ? { summaryClauses: opts.summaryClauses } : {},
|
|
13928
|
+
articleUrl,
|
|
13929
|
+
linkText: opts.surfingTitle?.trim() || "\u672C\u671F\u5B8C\u6574\u6587\u7AE0"
|
|
13930
|
+
});
|
|
13931
|
+
for (const line of summary.warnings)
|
|
13932
|
+
warn(line.replace(/^WARN podcast-pub:\s*/, ""), warnings);
|
|
13933
|
+
lines.push(summary.markdown, "");
|
|
13934
|
+
}
|
|
12503
13935
|
if (briefMode) {
|
|
12504
|
-
lines.push(renderTimelineHtml(docs, offsets, articleUrl), "");
|
|
13936
|
+
lines.push(renderTimelineHtml(docs, offsets, articleUrl, opts.timelineStyle ?? "after"), "");
|
|
12505
13937
|
} else {
|
|
12506
13938
|
docs.forEach((d, i) => {
|
|
12507
13939
|
const body = (d.body ?? "").trim();
|
|
@@ -12625,134 +14057,6 @@ function mapContentToEpisode(content) {
|
|
|
12625
14057
|
};
|
|
12626
14058
|
}
|
|
12627
14059
|
|
|
12628
|
-
// ../../plugins/publishings/podcast-pub/src/microfeed-client.ts
|
|
12629
|
-
var MIN_TIMEOUT_MS = 1000;
|
|
12630
|
-
var DEFAULT_TIMEOUT_MS = 300000;
|
|
12631
|
-
function resolveTimeoutMs(raw) {
|
|
12632
|
-
const parsed = raw === undefined ? Number.NaN : Number(raw);
|
|
12633
|
-
if (!Number.isFinite(parsed) || parsed < MIN_TIMEOUT_MS) {
|
|
12634
|
-
return DEFAULT_TIMEOUT_MS;
|
|
12635
|
-
}
|
|
12636
|
-
return parsed;
|
|
12637
|
-
}
|
|
12638
|
-
|
|
12639
|
-
class MicrofeedClient {
|
|
12640
|
-
baseUrl;
|
|
12641
|
-
apiKey;
|
|
12642
|
-
timeoutMs;
|
|
12643
|
-
constructor(baseUrl, apiKey, timeoutMs) {
|
|
12644
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
12645
|
-
this.apiKey = apiKey;
|
|
12646
|
-
this.timeoutMs = timeoutMs;
|
|
12647
|
-
}
|
|
12648
|
-
itemUrl(itemId) {
|
|
12649
|
-
return `${this.baseUrl}/items/${itemId}`;
|
|
12650
|
-
}
|
|
12651
|
-
async request(path, init) {
|
|
12652
|
-
let response;
|
|
12653
|
-
try {
|
|
12654
|
-
response = await fetch(`${this.baseUrl}${path}`, {
|
|
12655
|
-
...init,
|
|
12656
|
-
headers: {
|
|
12657
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
12658
|
-
Origin: this.baseUrl,
|
|
12659
|
-
...init.headers
|
|
12660
|
-
},
|
|
12661
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12662
|
-
});
|
|
12663
|
-
} catch (error) {
|
|
12664
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
12665
|
-
throw new Error(`microfeed transport error (${init.method ?? "GET"} ${path}): ${reason}`);
|
|
12666
|
-
}
|
|
12667
|
-
if (!response.ok) {
|
|
12668
|
-
const text = (await response.text()).slice(0, 1000);
|
|
12669
|
-
let detail = text.trim();
|
|
12670
|
-
try {
|
|
12671
|
-
const parsed = JSON.parse(text);
|
|
12672
|
-
if (typeof parsed.error === "string")
|
|
12673
|
-
detail = parsed.error;
|
|
12674
|
-
} catch {}
|
|
12675
|
-
throw new Error(`microfeed API request failed (HTTP ${response.status} ${response.statusText} ${path}): ${detail || "Unknown error"}`);
|
|
12676
|
-
}
|
|
12677
|
-
try {
|
|
12678
|
-
return await response.json();
|
|
12679
|
-
} catch {
|
|
12680
|
-
throw new Error(`microfeed API returned a non-JSON response from ${path}`);
|
|
12681
|
-
}
|
|
12682
|
-
}
|
|
12683
|
-
async createItem(input, idempotencyKey) {
|
|
12684
|
-
const data = await this.request("/api/v1/items", {
|
|
12685
|
-
method: "POST",
|
|
12686
|
-
headers: {
|
|
12687
|
-
"Content-Type": "application/json",
|
|
12688
|
-
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
12689
|
-
},
|
|
12690
|
-
body: JSON.stringify(input)
|
|
12691
|
-
});
|
|
12692
|
-
if (typeof data.id !== "string" || data.id.length === 0) {
|
|
12693
|
-
throw new Error("microfeed item create response is missing the item id");
|
|
12694
|
-
}
|
|
12695
|
-
return data.id;
|
|
12696
|
-
}
|
|
12697
|
-
async validateItem(input) {
|
|
12698
|
-
await this.request("/api/v1/items/validate", {
|
|
12699
|
-
method: "POST",
|
|
12700
|
-
headers: { "Content-Type": "application/json" },
|
|
12701
|
-
body: JSON.stringify(input)
|
|
12702
|
-
});
|
|
12703
|
-
}
|
|
12704
|
-
async getItem(itemId) {
|
|
12705
|
-
return await this.request(`/api/v1/items/${encodeURIComponent(itemId)}`, {
|
|
12706
|
-
method: "GET"
|
|
12707
|
-
});
|
|
12708
|
-
}
|
|
12709
|
-
async updateItem(itemId, input) {
|
|
12710
|
-
return await this.request(`/api/v1/items/${encodeURIComponent(itemId)}`, {
|
|
12711
|
-
method: "PUT",
|
|
12712
|
-
headers: { "Content-Type": "application/json" },
|
|
12713
|
-
body: JSON.stringify(input)
|
|
12714
|
-
});
|
|
12715
|
-
}
|
|
12716
|
-
async deleteItem(itemId) {
|
|
12717
|
-
await this.request(`/api/v1/items/${encodeURIComponent(itemId)}`, {
|
|
12718
|
-
method: "DELETE",
|
|
12719
|
-
headers: { "Content-Type": "application/json" }
|
|
12720
|
-
});
|
|
12721
|
-
}
|
|
12722
|
-
async listItems() {
|
|
12723
|
-
return await this.request("/api/v1/feed", { method: "GET" });
|
|
12724
|
-
}
|
|
12725
|
-
async prepareUpload(input) {
|
|
12726
|
-
const data = await this.request("/api/v1/media_files/presigned_urls", {
|
|
12727
|
-
method: "POST",
|
|
12728
|
-
headers: { "Content-Type": "application/json" },
|
|
12729
|
-
body: JSON.stringify(input)
|
|
12730
|
-
});
|
|
12731
|
-
if (typeof data.presigned_url !== "string" || typeof data.media_url !== "string") {
|
|
12732
|
-
throw new Error("microfeed upload-preparation response is missing presigned_url/media_url");
|
|
12733
|
-
}
|
|
12734
|
-
return { presignedUrl: data.presigned_url, mediaUrl: data.media_url };
|
|
12735
|
-
}
|
|
12736
|
-
async uploadBytes(presignedUrl, bytes, mimeType) {
|
|
12737
|
-
let response;
|
|
12738
|
-
try {
|
|
12739
|
-
response = await fetch(presignedUrl, {
|
|
12740
|
-
method: "PUT",
|
|
12741
|
-
headers: { "Content-Type": mimeType },
|
|
12742
|
-
body: bytes,
|
|
12743
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12744
|
-
});
|
|
12745
|
-
} catch (error) {
|
|
12746
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
12747
|
-
throw new Error(`microfeed transport error (PUT audio bytes): ${reason}`);
|
|
12748
|
-
}
|
|
12749
|
-
if (!response.ok) {
|
|
12750
|
-
const text = (await response.text()).slice(0, 1000);
|
|
12751
|
-
throw new Error(`microfeed audio upload failed (HTTP ${response.status} ${response.statusText}): ${text.trim() || "Unknown error"}`);
|
|
12752
|
-
}
|
|
12753
|
-
}
|
|
12754
|
-
}
|
|
12755
|
-
|
|
12756
14060
|
// ../../plugins/publishings/podcast-pub/src/index.ts
|
|
12757
14061
|
function publicItemUrl(item, itemId, client) {
|
|
12758
14062
|
const microfeed = item?._microfeed;
|
|
@@ -12762,8 +14066,8 @@ function publicItemUrl(item, itemId, client) {
|
|
|
12762
14066
|
const feedUrl = typeof item?.url === "string" ? item.url : "";
|
|
12763
14067
|
return feedUrl !== "" ? feedUrl : client.itemUrl(itemId);
|
|
12764
14068
|
}
|
|
12765
|
-
var
|
|
12766
|
-
var OPERATIONS = ["publish", "show-notes", "list", "get", "update", "delete", "validate"];
|
|
14069
|
+
var TARGET2 = "podcast-pub";
|
|
14070
|
+
var OPERATIONS = ["publish", "show-notes", "list", "get", "update", "delete", "validate", "backfill-seo"];
|
|
12767
14071
|
function resolveClient() {
|
|
12768
14072
|
const baseUrl = process.env.MICROFEED_BASE_URL;
|
|
12769
14073
|
const apiKey = process.env.MICROFEED_API_KEY;
|
|
@@ -12781,8 +14085,8 @@ function resolveClient() {
|
|
|
12781
14085
|
client: new MicrofeedClient(baseUrl.trim(), apiKey.trim(), resolveTimeoutMs(process.env.MICROFEED_TIMEOUT_MS))
|
|
12782
14086
|
};
|
|
12783
14087
|
}
|
|
12784
|
-
function
|
|
12785
|
-
return ResultSchema.parse({ ok: false, target:
|
|
14088
|
+
function fail2(error) {
|
|
14089
|
+
return ResultSchema.parse({ ok: false, target: TARGET2, error });
|
|
12786
14090
|
}
|
|
12787
14091
|
var COVER_MIME = {
|
|
12788
14092
|
".png": "image/png",
|
|
@@ -12829,7 +14133,7 @@ function resolveShowNotesRenderOptions(options) {
|
|
|
12829
14133
|
const audioContentPath = resolveOptionString(undefined, "PODCAST_PUB_AUDIO_CONTENT");
|
|
12830
14134
|
if (audioContentPath !== undefined) {
|
|
12831
14135
|
try {
|
|
12832
|
-
const parsed = JSON.parse(
|
|
14136
|
+
const parsed = JSON.parse(readFileSync3(audioContentPath, "utf-8"));
|
|
12833
14137
|
const meta = parsed?.metadata;
|
|
12834
14138
|
if (Array.isArray(meta?.segments)) {
|
|
12835
14139
|
audioSegments = meta.segments.map((s) => ({
|
|
@@ -12839,6 +14143,10 @@ function resolveShowNotesRenderOptions(options) {
|
|
|
12839
14143
|
}
|
|
12840
14144
|
} catch {}
|
|
12841
14145
|
}
|
|
14146
|
+
const seoConfig = resolvePodcastSeoConfig(undefined);
|
|
14147
|
+
if ("error" in seoConfig)
|
|
14148
|
+
return { error: seoConfig.error };
|
|
14149
|
+
const config = seoConfig.config;
|
|
12842
14150
|
return {
|
|
12843
14151
|
renderOptions: {
|
|
12844
14152
|
title,
|
|
@@ -12847,7 +14155,15 @@ function resolveShowNotesRenderOptions(options) {
|
|
|
12847
14155
|
...briefMode ? { briefMode: true } : {},
|
|
12848
14156
|
...articlePostPath !== undefined ? { articlePostPath } : {},
|
|
12849
14157
|
...footerNote !== undefined ? { footerNote } : {},
|
|
12850
|
-
...audioSegments !== undefined ? { audioSegments } : {}
|
|
14158
|
+
...audioSegments !== undefined ? { audioSegments } : {},
|
|
14159
|
+
summaryParagraph: config.summaryParagraph,
|
|
14160
|
+
timelineStyle: config.timelineStyle,
|
|
14161
|
+
seriesName: config.seriesName,
|
|
14162
|
+
showName: config.showName,
|
|
14163
|
+
descriptionMin: config.descriptionMin,
|
|
14164
|
+
descriptionMax: config.descriptionMax,
|
|
14165
|
+
descriptionSource: config.descriptionSource,
|
|
14166
|
+
descriptionPattern: config.descriptionPattern
|
|
12851
14167
|
}
|
|
12852
14168
|
};
|
|
12853
14169
|
}
|
|
@@ -12860,7 +14176,8 @@ function renderShowNotesFromOptions(options) {
|
|
|
12860
14176
|
const renderOptions = resolveShowNotesRenderOptions(options);
|
|
12861
14177
|
if ("error" in renderOptions)
|
|
12862
14178
|
return { error: renderOptions.error };
|
|
12863
|
-
|
|
14179
|
+
const warnings = [];
|
|
14180
|
+
return { markdown: renderShowNotesMarkdown(docs.data, renderOptions.renderOptions, warnings), warnings };
|
|
12864
14181
|
}
|
|
12865
14182
|
function resolveAssemblyOptions(options) {
|
|
12866
14183
|
const assembly = {};
|
|
@@ -12895,27 +14212,124 @@ function audioAttachment(audio, mediaUrl, sizeInBytes, durationSeconds) {
|
|
|
12895
14212
|
...durationSeconds !== undefined ? { duration_in_seconds: durationSeconds } : {}
|
|
12896
14213
|
};
|
|
12897
14214
|
}
|
|
12898
|
-
function episodeExtras(episode) {
|
|
14215
|
+
function episodeExtras(episode, seo) {
|
|
12899
14216
|
return {
|
|
12900
14217
|
...episode.imageUrl ? { image: episode.imageUrl } : {},
|
|
12901
14218
|
...episode.datePublishedMs !== undefined ? { date_published_ms: episode.datePublishedMs } : {},
|
|
12902
14219
|
...episode.link ? { url: episode.link } : {},
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
}
|
|
14220
|
+
_microfeed: {
|
|
14221
|
+
"itunes:episodeType": episode.itunes?.episodeType ?? "full",
|
|
14222
|
+
...episode.itunes?.title ? { "itunes:title": episode.itunes.title } : {},
|
|
14223
|
+
...episode.itunes?.season !== undefined ? { "itunes:season": episode.itunes.season } : {},
|
|
14224
|
+
...episode.itunes?.episode !== undefined ? { "itunes:episode": episode.itunes.episode } : {},
|
|
14225
|
+
...episode.itunes?.explicit !== undefined ? { "itunes:explicit": episode.itunes.explicit } : {},
|
|
14226
|
+
...episode.itunes?.block !== undefined ? { "itunes:block": episode.itunes.block } : {},
|
|
14227
|
+
...seo ? { seo } : {}
|
|
12912
14228
|
}
|
|
12913
14229
|
};
|
|
12914
14230
|
}
|
|
14231
|
+
function shapedEpisode(episode, fields) {
|
|
14232
|
+
return {
|
|
14233
|
+
...episode,
|
|
14234
|
+
title: fields.title,
|
|
14235
|
+
...fields.itunes ? { itunes: fields.itunes } : {},
|
|
14236
|
+
...fields.durationSeconds !== undefined ? { durationSeconds: fields.durationSeconds } : { durationSeconds: undefined },
|
|
14237
|
+
link: fields.link
|
|
14238
|
+
};
|
|
14239
|
+
}
|
|
14240
|
+
async function createEpisodeItem(client, body, slug, warnings) {
|
|
14241
|
+
const withSlug = (itemSlug) => {
|
|
14242
|
+
const micro = body._microfeed ?? {};
|
|
14243
|
+
return { ...body, _microfeed: { ...micro, slug: itemSlug } };
|
|
14244
|
+
};
|
|
14245
|
+
if (!slug)
|
|
14246
|
+
return client.createItem(body, crypto.randomUUID());
|
|
14247
|
+
try {
|
|
14248
|
+
return await client.createItem(withSlug(slug), crypto.randomUUID());
|
|
14249
|
+
} catch (error) {
|
|
14250
|
+
if (!(error instanceof MicrofeedRequestError) || error.status !== 409)
|
|
14251
|
+
throw error;
|
|
14252
|
+
const retry = `${slug}-2`;
|
|
14253
|
+
try {
|
|
14254
|
+
return await client.createItem(withSlug(retry), crypto.randomUUID());
|
|
14255
|
+
} catch (retryError) {
|
|
14256
|
+
if (!(retryError instanceof MicrofeedRequestError) || retryError.status !== 409)
|
|
14257
|
+
throw retryError;
|
|
14258
|
+
warn(`slug "${slug}" conflicted (HTTP 409); publishing without a custom slug`, warnings);
|
|
14259
|
+
return client.createItem(body, crypto.randomUUID());
|
|
14260
|
+
}
|
|
14261
|
+
}
|
|
14262
|
+
}
|
|
14263
|
+
async function resolveSocialImage(client, fields, warnings) {
|
|
14264
|
+
if (!fields.socialAlt)
|
|
14265
|
+
return;
|
|
14266
|
+
if (fields.socialLocalPath && fields.socialMime) {
|
|
14267
|
+
const bytes = new Uint8Array(await readFile(fields.socialLocalPath));
|
|
14268
|
+
const prepared = await client.prepareUpload({
|
|
14269
|
+
category: "image",
|
|
14270
|
+
full_local_file_path: basename(fields.socialLocalPath),
|
|
14271
|
+
size: bytes.byteLength,
|
|
14272
|
+
type: fields.socialMime
|
|
14273
|
+
});
|
|
14274
|
+
await client.uploadBytes(prepared.presignedUrl, bytes, fields.socialMime);
|
|
14275
|
+
return {
|
|
14276
|
+
url: prepared.mediaUrl,
|
|
14277
|
+
width: 1200,
|
|
14278
|
+
height: 630,
|
|
14279
|
+
mime_type: fields.socialMime,
|
|
14280
|
+
alt: fields.socialAlt
|
|
14281
|
+
};
|
|
14282
|
+
}
|
|
14283
|
+
if (!fields.socialRemoteUrl)
|
|
14284
|
+
return;
|
|
14285
|
+
let bytes;
|
|
14286
|
+
try {
|
|
14287
|
+
const response = await fetch(fields.socialRemoteUrl);
|
|
14288
|
+
if (!response.ok)
|
|
14289
|
+
throw new Error(`HTTP ${response.status}`);
|
|
14290
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
14291
|
+
} catch (error) {
|
|
14292
|
+
warn(`social image skipped: could not read ${fields.socialRemoteUrl} (${error instanceof Error ? error.message : String(error)})`, warnings);
|
|
14293
|
+
return;
|
|
14294
|
+
}
|
|
14295
|
+
const dims = readImageDimensions(bytes);
|
|
14296
|
+
if (dims?.width !== 1200 || dims?.height !== 630) {
|
|
14297
|
+
warn(`social image skipped: expected a 1200\xD7630 PNG or JPEG, got ${dims ? `${dims.width}\xD7${dims.height}` : "an unreadable file"}`, warnings);
|
|
14298
|
+
return;
|
|
14299
|
+
}
|
|
14300
|
+
return {
|
|
14301
|
+
url: fields.socialRemoteUrl,
|
|
14302
|
+
width: 1200,
|
|
14303
|
+
height: 630,
|
|
14304
|
+
mime_type: dims.mime,
|
|
14305
|
+
alt: fields.socialAlt
|
|
14306
|
+
};
|
|
14307
|
+
}
|
|
14308
|
+
function mergeSocial(seo, social) {
|
|
14309
|
+
if (!social)
|
|
14310
|
+
return seo;
|
|
14311
|
+
return { ...seo ?? {}, social_image: social };
|
|
14312
|
+
}
|
|
14313
|
+
function mappedValidateInput(content) {
|
|
14314
|
+
const seoConfig = resolvePodcastSeoConfig(content.metadata);
|
|
14315
|
+
if ("error" in seoConfig)
|
|
14316
|
+
throw new Error(seoConfig.error);
|
|
14317
|
+
const episode = mapContentToEpisode(content);
|
|
14318
|
+
const policy = prepareEpisodeFields(episode, content.metadata ?? {}, seoConfig.config);
|
|
14319
|
+
if (!policy.ok)
|
|
14320
|
+
throw new Error(policy.error);
|
|
14321
|
+
const shaped = shapedEpisode(episode, policy.fields);
|
|
14322
|
+
return {
|
|
14323
|
+
title: shaped.title,
|
|
14324
|
+
content_html: shaped.contentHtml,
|
|
14325
|
+
status: shaped.status,
|
|
14326
|
+
...episodeExtras(shaped, policy.fields.seo)
|
|
14327
|
+
};
|
|
14328
|
+
}
|
|
12915
14329
|
async function publishEpisode(client, payload, options) {
|
|
12916
14330
|
const assembly = resolveAssemblyOptions(options);
|
|
12917
14331
|
if ("error" in assembly)
|
|
12918
|
-
return
|
|
14332
|
+
return fail2(assembly.error);
|
|
12919
14333
|
const { audioFile, coverPath, durationSec } = assembly.assembly;
|
|
12920
14334
|
const content = audioFile !== undefined || coverPath !== undefined || durationSec !== undefined ? {
|
|
12921
14335
|
...payload.content,
|
|
@@ -12930,17 +14344,31 @@ async function publishEpisode(client, payload, options) {
|
|
|
12930
14344
|
try {
|
|
12931
14345
|
episode = mapContentToEpisode(content);
|
|
12932
14346
|
} catch (error) {
|
|
12933
|
-
return
|
|
14347
|
+
return fail2(`Invalid Content: ${error instanceof Error ? error.message : String(error)}`);
|
|
14348
|
+
}
|
|
14349
|
+
const seoConfig = resolvePodcastSeoConfig(content.metadata);
|
|
14350
|
+
if ("error" in seoConfig)
|
|
14351
|
+
return fail2(seoConfig.error);
|
|
14352
|
+
const policy = prepareEpisodeFields(episode, content.metadata ?? {}, seoConfig.config);
|
|
14353
|
+
if (!policy.ok)
|
|
14354
|
+
return fail2(policy.error);
|
|
14355
|
+
const fields = policy.fields;
|
|
14356
|
+
let social;
|
|
14357
|
+
try {
|
|
14358
|
+
social = await resolveSocialImage(client, fields, fields.warnings);
|
|
14359
|
+
} catch (error) {
|
|
14360
|
+
return fail2(error instanceof Error ? error.message : String(error));
|
|
12934
14361
|
}
|
|
12935
|
-
const
|
|
14362
|
+
const seo = mergeSocial(fields.seo, social);
|
|
14363
|
+
episode = shapedEpisode(episode, fields);
|
|
12936
14364
|
if (episode.audio.kind === "url") {
|
|
12937
|
-
const itemId = await client
|
|
14365
|
+
const itemId = await createEpisodeItem(client, {
|
|
12938
14366
|
title: episode.title,
|
|
12939
14367
|
content_html: episode.contentHtml,
|
|
12940
14368
|
status: episode.status,
|
|
12941
14369
|
attachments: [audioAttachment(episode.audio, episode.audio.url, undefined, episode.durationSeconds)],
|
|
12942
|
-
...episodeExtras(episode)
|
|
12943
|
-
},
|
|
14370
|
+
...episodeExtras(episode, seo)
|
|
14371
|
+
}, fields.slug, fields.warnings);
|
|
12944
14372
|
let hostedItem;
|
|
12945
14373
|
try {
|
|
12946
14374
|
hostedItem = (await client.getItem(itemId)).items?.[0];
|
|
@@ -12949,17 +14377,18 @@ async function publishEpisode(client, payload, options) {
|
|
|
12949
14377
|
}
|
|
12950
14378
|
return ResultSchema.parse({
|
|
12951
14379
|
ok: true,
|
|
12952
|
-
target:
|
|
14380
|
+
target: TARGET2,
|
|
12953
14381
|
url: publicItemUrl(hostedItem, itemId, client),
|
|
12954
14382
|
id: itemId,
|
|
12955
|
-
message: "Published successfully to microfeed (hosted audio, single-call create)"
|
|
14383
|
+
message: "Published successfully to microfeed (hosted audio, single-call create)",
|
|
14384
|
+
...fields.warnings.length > 0 ? { metadata: { warnings: fields.warnings } } : {}
|
|
12956
14385
|
});
|
|
12957
14386
|
}
|
|
12958
|
-
const itemId = await client
|
|
14387
|
+
const itemId = await createEpisodeItem(client, {
|
|
12959
14388
|
title: episode.title,
|
|
12960
14389
|
content_html: episode.contentHtml,
|
|
12961
14390
|
status: "unpublished"
|
|
12962
|
-
},
|
|
14391
|
+
}, fields.slug, fields.warnings);
|
|
12963
14392
|
const bytes = new Uint8Array(await readFile(episode.audio.path));
|
|
12964
14393
|
const prepared = await client.prepareUpload({
|
|
12965
14394
|
category: "audio",
|
|
@@ -12988,15 +14417,19 @@ async function publishEpisode(client, payload, options) {
|
|
|
12988
14417
|
status: episode.status,
|
|
12989
14418
|
attachments: [audioAttachment(episode.audio, prepared.mediaUrl, bytes.byteLength, episode.durationSeconds)],
|
|
12990
14419
|
...imageUrl ? { image: imageUrl } : {},
|
|
12991
|
-
...episodeExtras(episode)
|
|
14420
|
+
...episodeExtras(episode, seo)
|
|
12992
14421
|
});
|
|
12993
14422
|
return ResultSchema.parse({
|
|
12994
14423
|
ok: true,
|
|
12995
|
-
target:
|
|
14424
|
+
target: TARGET2,
|
|
12996
14425
|
url: publicItemUrl(item, itemId, client),
|
|
12997
14426
|
id: itemId,
|
|
12998
14427
|
message: "Published successfully to microfeed (two-phase upload + publish)",
|
|
12999
|
-
metadata: {
|
|
14428
|
+
metadata: {
|
|
14429
|
+
itemId,
|
|
14430
|
+
imageUrl: imageUrl ?? null,
|
|
14431
|
+
...fields.warnings.length > 0 ? { warnings: fields.warnings } : {}
|
|
14432
|
+
}
|
|
13000
14433
|
});
|
|
13001
14434
|
}
|
|
13002
14435
|
|
|
@@ -13006,21 +14439,24 @@ class PodcastPubTransport {
|
|
|
13006
14439
|
const options = payload.options !== undefined && typeof payload.options === "object" && payload.options !== null ? payload.options : {};
|
|
13007
14440
|
const routed = resolveOperation(options);
|
|
13008
14441
|
if ("error" in routed)
|
|
13009
|
-
return
|
|
14442
|
+
return fail2(routed.error);
|
|
13010
14443
|
if (routed.operation === "show-notes") {
|
|
13011
14444
|
const rendered = renderShowNotesFromOptions(options);
|
|
13012
14445
|
if ("error" in rendered)
|
|
13013
|
-
return
|
|
14446
|
+
return fail2(rendered.error);
|
|
13014
14447
|
return ResultSchema.parse({
|
|
13015
14448
|
ok: true,
|
|
13016
|
-
target:
|
|
14449
|
+
target: TARGET2,
|
|
13017
14450
|
message: "Rendered show notes markdown (pure render; no microfeed API call)",
|
|
13018
|
-
metadata: {
|
|
14451
|
+
metadata: {
|
|
14452
|
+
markdown: rendered.markdown,
|
|
14453
|
+
...rendered.warnings.length > 0 ? { warnings: rendered.warnings } : {}
|
|
14454
|
+
}
|
|
13019
14455
|
});
|
|
13020
14456
|
}
|
|
13021
14457
|
const resolved = resolveClient();
|
|
13022
14458
|
if ("error" in resolved)
|
|
13023
|
-
return
|
|
14459
|
+
return fail2(resolved.error);
|
|
13024
14460
|
const { client } = resolved;
|
|
13025
14461
|
try {
|
|
13026
14462
|
switch (routed.operation) {
|
|
@@ -13030,7 +14466,7 @@ class PodcastPubTransport {
|
|
|
13030
14466
|
const feed = await client.listItems();
|
|
13031
14467
|
return ResultSchema.parse({
|
|
13032
14468
|
ok: true,
|
|
13033
|
-
target:
|
|
14469
|
+
target: TARGET2,
|
|
13034
14470
|
url: typeof feed.feed_url === "string" ? feed.feed_url : undefined,
|
|
13035
14471
|
message: `Listed ${feed.items?.length ?? 0} microfeed item(s)`,
|
|
13036
14472
|
metadata: { items: feed.items ?? [] }
|
|
@@ -13039,14 +14475,14 @@ class PodcastPubTransport {
|
|
|
13039
14475
|
case "get": {
|
|
13040
14476
|
const itemId = resolveItemId(options);
|
|
13041
14477
|
if ("error" in itemId)
|
|
13042
|
-
return
|
|
14478
|
+
return fail2(itemId.error);
|
|
13043
14479
|
const feed = await client.getItem(itemId.itemId);
|
|
13044
14480
|
const item = feed.items?.[0];
|
|
13045
14481
|
if (!item)
|
|
13046
|
-
return
|
|
14482
|
+
return fail2(`microfeed item "${itemId.itemId}" not found in the feed response`);
|
|
13047
14483
|
return ResultSchema.parse({
|
|
13048
14484
|
ok: true,
|
|
13049
|
-
target:
|
|
14485
|
+
target: TARGET2,
|
|
13050
14486
|
id: itemId.itemId,
|
|
13051
14487
|
url: typeof item.url === "string" ? item.url : undefined,
|
|
13052
14488
|
message: `Fetched microfeed item ${itemId.itemId}`,
|
|
@@ -13056,14 +14492,14 @@ class PodcastPubTransport {
|
|
|
13056
14492
|
case "update": {
|
|
13057
14493
|
const itemId = resolveItemId(options);
|
|
13058
14494
|
if ("error" in itemId)
|
|
13059
|
-
return
|
|
14495
|
+
return fail2(itemId.error);
|
|
13060
14496
|
if (options.item === undefined || typeof options.item !== "object" || options.item === null) {
|
|
13061
|
-
return
|
|
14497
|
+
return fail2("options.item (microfeed item input object) is required for the update operation");
|
|
13062
14498
|
}
|
|
13063
14499
|
const item = await client.updateItem(itemId.itemId, options.item);
|
|
13064
14500
|
return ResultSchema.parse({
|
|
13065
14501
|
ok: true,
|
|
13066
|
-
target:
|
|
14502
|
+
target: TARGET2,
|
|
13067
14503
|
id: itemId.itemId,
|
|
13068
14504
|
url: typeof item.url === "string" ? item.url : undefined,
|
|
13069
14505
|
message: `Updated microfeed item ${itemId.itemId}`,
|
|
@@ -13073,34 +14509,33 @@ class PodcastPubTransport {
|
|
|
13073
14509
|
case "delete": {
|
|
13074
14510
|
const itemId = resolveItemId(options);
|
|
13075
14511
|
if ("error" in itemId)
|
|
13076
|
-
return
|
|
14512
|
+
return fail2(itemId.error);
|
|
13077
14513
|
await client.deleteItem(itemId.itemId);
|
|
13078
14514
|
return ResultSchema.parse({
|
|
13079
14515
|
ok: true,
|
|
13080
|
-
target:
|
|
14516
|
+
target: TARGET2,
|
|
13081
14517
|
id: itemId.itemId,
|
|
13082
14518
|
message: `Deleted microfeed item ${itemId.itemId}`
|
|
13083
14519
|
});
|
|
13084
14520
|
}
|
|
14521
|
+
case "backfill-seo": {
|
|
14522
|
+
const config = resolvePodcastSeoConfig(payload.content.metadata);
|
|
14523
|
+
if ("error" in config)
|
|
14524
|
+
return fail2(config.error);
|
|
14525
|
+
return await runBackfill(client, options, config.config);
|
|
14526
|
+
}
|
|
13085
14527
|
case "validate": {
|
|
13086
|
-
const input = options.item !== undefined && typeof options.item === "object" && options.item !== null ? options.item : (
|
|
13087
|
-
const episode = mapContentToEpisode(payload.content);
|
|
13088
|
-
return {
|
|
13089
|
-
title: episode.title,
|
|
13090
|
-
content_html: episode.contentHtml,
|
|
13091
|
-
status: episode.status
|
|
13092
|
-
};
|
|
13093
|
-
})();
|
|
14528
|
+
const input = options.item !== undefined && typeof options.item === "object" && options.item !== null ? options.item : mappedValidateInput(payload.content);
|
|
13094
14529
|
await client.validateItem(input);
|
|
13095
14530
|
return ResultSchema.parse({
|
|
13096
14531
|
ok: true,
|
|
13097
|
-
target:
|
|
14532
|
+
target: TARGET2,
|
|
13098
14533
|
message: "microfeed accepted the item input (POST /api/v1/items/validate)"
|
|
13099
14534
|
});
|
|
13100
14535
|
}
|
|
13101
14536
|
}
|
|
13102
14537
|
} catch (error) {
|
|
13103
|
-
return
|
|
14538
|
+
return fail2(`${error instanceof Error ? error.message : String(error)}`);
|
|
13104
14539
|
}
|
|
13105
14540
|
}
|
|
13106
14541
|
}
|
|
@@ -13121,7 +14556,7 @@ async function renderShowNotesIO(docs, outputPath) {
|
|
|
13121
14556
|
const rendered = renderShowNotesFromOptions({ docs });
|
|
13122
14557
|
if ("error" in rendered) {
|
|
13123
14558
|
const message = `show-notes render failed: ${rendered.error}`;
|
|
13124
|
-
await writePublishResult(outputPath,
|
|
14559
|
+
await writePublishResult(outputPath, fail2(message));
|
|
13125
14560
|
throw new Error(message);
|
|
13126
14561
|
}
|
|
13127
14562
|
await createNodeFileSystem().writeFile(outputPath, rendered.markdown);
|
|
@@ -13143,7 +14578,7 @@ async function processPublishIO(inputPath, outputPath, transport = getPublishTra
|
|
|
13143
14578
|
}
|
|
13144
14579
|
} catch (error) {
|
|
13145
14580
|
const message = `Invalid Content input: ${error instanceof Error ? error.message : String(error)}`;
|
|
13146
|
-
await writePublishResult(outputPath, ResultSchema.parse({ ok: false, target:
|
|
14581
|
+
await writePublishResult(outputPath, ResultSchema.parse({ ok: false, target: TARGET2, error: message }));
|
|
13147
14582
|
throw new Error(message);
|
|
13148
14583
|
}
|
|
13149
14584
|
const result = await transport.publish({ content, options });
|