@gobing-ai/knowledge-kit 0.0.19 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +161 -142
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +20 -3
- package/plugins/generations/core-facts-gen/dist/index.js +20 -3
- package/plugins/generations/daily-article-gen/dist/index.js +20 -3
- package/plugins/generations/dailynews-gen/dist/index.js +29 -4
- package/plugins/generations/dailynews-gen/src/script-builder.ts +19 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +20 -3
- package/plugins/generations/image-gen/dist/index.js +147 -69
- package/plugins/generations/image-gen/src/presets.ts +2 -2
- package/plugins/generations/image-gen/src/providers/agnes.ts +6 -2
- package/plugins/generations/image-gen/src/providers/azure.ts +6 -2
- package/plugins/generations/image-gen/src/providers/dashscope.ts +10 -3
- package/plugins/generations/image-gen/src/providers/google.ts +70 -29
- package/plugins/generations/image-gen/src/providers/huggingface.ts +3 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +9 -2
- package/plugins/generations/image-gen/src/providers/minimax.ts +10 -2
- package/plugins/generations/image-gen/src/providers/openai.ts +9 -3
- package/plugins/generations/image-gen/src/providers/openrouter.ts +16 -4
- package/plugins/generations/image-gen/src/providers/replicate.ts +13 -3
- package/plugins/generations/image-gen/src/providers/seedream.ts +4 -1
- package/plugins/generations/image-gen/src/providers/timeouts.ts +14 -0
- package/plugins/generations/image-gen/src/providers/types.ts +10 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +4 -1
- package/plugins/generations/news-report-gen/dist/index.js +20 -3
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +13 -5
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +23 -15
- package/plugins/generations/voice-gen/dist/index.js +36 -10
- package/plugins/generations/voice-gen/src/voicebox-client.ts +18 -4
- package/plugins/generations/voice-gen/src/voicebox-server.ts +2 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +26 -6
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/aihot-ingest/src/client.ts +5 -0
- package/plugins/ingestions/aihot-ingest/src/rss.ts +12 -2
- package/plugins/ingestions/horizon-ingest/dist/index.js +20 -3
- package/plugins/ingestions/last30days-ingest/dist/index.js +20 -3
- package/plugins/ingestions/web-search/dist/index.js +22 -3
- package/plugins/ingestions/web-search/src/index.ts +4 -0
- package/plugins/kk/commands/workflow-run.md +20 -6
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/itc-stages.ts +563 -0
- package/plugins/kk/scripts/kk-workflow-stages.ts +125 -18
- package/plugins/kk/scripts/render-md.ts +7 -1
- package/plugins/kk/skills/article-adapt/SKILL.md +85 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +26 -16
- package/plugins/kk/workflows/kk-itc.yaml +461 -12
- package/plugins/publishings/emdash-pub/dist/index.js +76 -19
- package/plugins/publishings/emdash-pub/src/index.ts +4 -0
- package/plugins/publishings/infoq-pub/dist/index.js +74 -21
- package/plugins/publishings/juejin-pub/dist/index.js +74 -21
- package/plugins/publishings/medium-pub/dist/index.js +77 -19
- package/plugins/publishings/medium-pub/src/index.ts +5 -0
- package/plugins/publishings/podcast-pub/dist/index.js +130 -34
- package/plugins/publishings/podcast-pub/src/index.ts +20 -5
- package/plugins/publishings/podcast-pub/src/map.ts +11 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +66 -13
- package/plugins/publishings/qiita-pub/dist/index.js +75 -18
- package/plugins/publishings/qiita-pub/src/index.ts +4 -0
- package/plugins/publishings/substack-pub/dist/index.js +74 -21
- package/plugins/publishings/surfdash-pub/dist/index.js +99 -62
- package/plugins/publishings/surfdash-pub/src/index.ts +60 -19
- package/plugins/publishings/wechatmp-pub/dist/index.js +79 -22
- package/plugins/publishings/wechatmp-pub/src/index.ts +12 -3
- package/plugins/publishings/x-pub/dist/index.js +56 -37
- package/plugins/publishings/x-pub/src/index.ts +4 -0
- package/plugins/publishings/xhs-pub/dist/index.js +74 -21
- package/plugins/publishings/zenn-pub/dist/index.js +103 -25
- package/plugins/publishings/zenn-pub/src/index.ts +43 -9
|
@@ -21746,7 +21746,7 @@ var init_file_system_node = () => {};
|
|
|
21746
21746
|
// ../../plugins/publishings/podcast-pub/src/index.ts
|
|
21747
21747
|
import { readFileSync as readFileSync2 } from "fs";
|
|
21748
21748
|
import { readFile } from "fs/promises";
|
|
21749
|
-
import { dirname as dirname2 } from "path";
|
|
21749
|
+
import { basename, dirname as dirname2, extname as extname2 } from "path";
|
|
21750
21750
|
import { parseArgs } from "util";
|
|
21751
21751
|
|
|
21752
21752
|
// ../../packages/kk-core/src/kinds.ts
|
|
@@ -21783,6 +21783,22 @@ var ResultSchema = exports_external.object({
|
|
|
21783
21783
|
error: exports_external.string().optional(),
|
|
21784
21784
|
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21785
21785
|
});
|
|
21786
|
+
var TargetOutcomeSchema = exports_external.object({
|
|
21787
|
+
target: exports_external.string(),
|
|
21788
|
+
ok: exports_external.boolean(),
|
|
21789
|
+
status: exports_external.enum(["published", "failed", "skipped"]),
|
|
21790
|
+
url: exports_external.string().optional(),
|
|
21791
|
+
id: exports_external.string().optional(),
|
|
21792
|
+
error: exports_external.string().optional(),
|
|
21793
|
+
durationMs: exports_external.number().optional(),
|
|
21794
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21795
|
+
});
|
|
21796
|
+
var AggregateResultSchema = exports_external.object({
|
|
21797
|
+
ok: exports_external.boolean(),
|
|
21798
|
+
summary: exports_external.object({ total: exports_external.number(), succeeded: exports_external.number(), failed: exports_external.number(), skipped: exports_external.number() }),
|
|
21799
|
+
targets: exports_external.array(TargetOutcomeSchema),
|
|
21800
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21801
|
+
});
|
|
21786
21802
|
var DocListSchema = exports_external.array(DocSchema);
|
|
21787
21803
|
var omniVoiceGenContentSchema = ContentSchema.extend({
|
|
21788
21804
|
format: exports_external.literal("audio"),
|
|
@@ -21811,14 +21827,15 @@ var PluginKindSchema = exports_external.enum(PLUGIN_KINDS);
|
|
|
21811
21827
|
// ../../packages/kk-core/src/manifest.ts
|
|
21812
21828
|
init_zod();
|
|
21813
21829
|
var OperationIOSchema = exports_external.enum(["doc-list", "markdown"]);
|
|
21814
|
-
var
|
|
21830
|
+
var PluginOperationSchema = exports_external.object({ input: OperationIOSchema, output: OperationIOSchema });
|
|
21831
|
+
var PluginManifestSchema = exports_external.looseObject({
|
|
21815
21832
|
name: exports_external.string().min(1),
|
|
21816
21833
|
kind: PluginKindSchema,
|
|
21817
21834
|
entry: exports_external.string().min(1),
|
|
21818
21835
|
version: exports_external.string().optional(),
|
|
21819
21836
|
description: exports_external.string().optional(),
|
|
21820
|
-
operations: exports_external.record(exports_external.string(),
|
|
21821
|
-
})
|
|
21837
|
+
operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
|
|
21838
|
+
});
|
|
21822
21839
|
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/index.js
|
|
21823
21840
|
init_file_system_node();
|
|
21824
21841
|
init_config();
|
|
@@ -21844,41 +21861,82 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
|
|
|
21844
21861
|
// ../../packages/kk-core/src/render-md.ts
|
|
21845
21862
|
if (false) {}
|
|
21846
21863
|
// ../../packages/publish-harness/src/index.ts
|
|
21847
|
-
class
|
|
21848
|
-
|
|
21849
|
-
callLogs;
|
|
21864
|
+
class FakeTransportCore {
|
|
21865
|
+
callLogs = [];
|
|
21850
21866
|
nextResult;
|
|
21851
|
-
constructor() {
|
|
21852
|
-
this.callLogs = [];
|
|
21853
|
-
}
|
|
21854
21867
|
setNextResult(result) {
|
|
21855
21868
|
this.nextResult = result;
|
|
21856
21869
|
}
|
|
21857
|
-
async publish(payload) {
|
|
21870
|
+
async publish(payload, defaultResult) {
|
|
21858
21871
|
this.callLogs.push({
|
|
21859
21872
|
timestamp: new Date().toISOString(),
|
|
21860
21873
|
payload
|
|
21861
21874
|
});
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
return
|
|
21875
|
+
return this.nextResult ?? defaultResult;
|
|
21876
|
+
}
|
|
21877
|
+
getCallLogs() {
|
|
21878
|
+
return this.callLogs;
|
|
21879
|
+
}
|
|
21880
|
+
reset() {
|
|
21881
|
+
this.callLogs = [];
|
|
21882
|
+
this.nextResult = undefined;
|
|
21883
|
+
}
|
|
21884
|
+
}
|
|
21885
|
+
|
|
21886
|
+
class FakeFileCliTransport {
|
|
21887
|
+
kind = "file-cli";
|
|
21888
|
+
core = new FakeTransportCore;
|
|
21889
|
+
setNextResult(result) {
|
|
21890
|
+
this.core.setNextResult(result);
|
|
21891
|
+
}
|
|
21892
|
+
async publish(payload) {
|
|
21893
|
+
return this.core.publish(payload, {
|
|
21866
21894
|
ok: true,
|
|
21867
21895
|
target: "fake-file-cli",
|
|
21868
21896
|
url: "https://example.com/published-post",
|
|
21869
21897
|
id: "fake-id-123",
|
|
21870
21898
|
message: "Published offline via FakeFileCliTransport"
|
|
21871
|
-
};
|
|
21899
|
+
});
|
|
21872
21900
|
}
|
|
21873
21901
|
getCallLogs() {
|
|
21874
|
-
return this.
|
|
21902
|
+
return this.core.getCallLogs();
|
|
21875
21903
|
}
|
|
21876
21904
|
reset() {
|
|
21877
|
-
this.
|
|
21878
|
-
this.nextResult = undefined;
|
|
21905
|
+
this.core.reset();
|
|
21879
21906
|
}
|
|
21880
21907
|
}
|
|
21881
21908
|
|
|
21909
|
+
class FakeBrowserTransport {
|
|
21910
|
+
kind = "browser";
|
|
21911
|
+
driver;
|
|
21912
|
+
core = new FakeTransportCore;
|
|
21913
|
+
constructor(driver = "agent") {
|
|
21914
|
+
this.driver = driver;
|
|
21915
|
+
}
|
|
21916
|
+
setNextResult(result) {
|
|
21917
|
+
this.core.setNextResult(result);
|
|
21918
|
+
}
|
|
21919
|
+
async publish(payload) {
|
|
21920
|
+
return this.core.publish(payload, {
|
|
21921
|
+
ok: true,
|
|
21922
|
+
target: "fake-browser",
|
|
21923
|
+
url: "https://example.com/browser-draft",
|
|
21924
|
+
id: "fake-browser-id-123",
|
|
21925
|
+
message: "Published offline via FakeBrowserTransport",
|
|
21926
|
+
metadata: { driver: this.driver }
|
|
21927
|
+
});
|
|
21928
|
+
}
|
|
21929
|
+
getCallLogs() {
|
|
21930
|
+
return this.core.getCallLogs();
|
|
21931
|
+
}
|
|
21932
|
+
reset() {
|
|
21933
|
+
this.core.reset();
|
|
21934
|
+
}
|
|
21935
|
+
}
|
|
21936
|
+
|
|
21937
|
+
// ../../plugins/publishings/podcast-pub/src/map.ts
|
|
21938
|
+
import { extname } from "path";
|
|
21939
|
+
|
|
21882
21940
|
// ../../plugins/publishings/podcast-pub/src/show-notes.ts
|
|
21883
21941
|
function escapeHtml(value) {
|
|
21884
21942
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
@@ -21942,27 +22000,37 @@ function computeAudioOffsets(docs, segments) {
|
|
|
21942
22000
|
return fallback();
|
|
21943
22001
|
return starts;
|
|
21944
22002
|
}
|
|
22003
|
+
var isHttpUrl = (u) => /^https?:\/\//i.test(u.trim());
|
|
22004
|
+
var TIMELINE_STYLE = `<style>
|
|
22005
|
+
.kk-tl{margin:1em 0}
|
|
22006
|
+
.kk-tl-row{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid rgba(128,128,128,.25)}
|
|
22007
|
+
.kk-tl-t{flex:0 0 56px;font-variant-numeric:tabular-nums;font-weight:700;color:#0969da}
|
|
22008
|
+
.kk-tl-body{flex:1;min-width:0}
|
|
22009
|
+
.kk-tl-title{font-weight:600}
|
|
22010
|
+
.kk-tl-brief{margin:4px 0 0;color:#666;font-size:.95em}
|
|
22011
|
+
</style>`;
|
|
22012
|
+
var TIMELINE_TAG = /^<\/?(?:div|p|a)(?:\s+(?:class="kk-tl(?:-[a-z]+)?"|href="https?:\/\/[^"<>\s]*"|target="_blank"|rel="noopener"))*\s*>$/;
|
|
22013
|
+
function isGeneratedTimeline(block) {
|
|
22014
|
+
const rest = block.replace(TIMELINE_STYLE, "");
|
|
22015
|
+
if (rest.includes("<style") || rest.includes("<!--"))
|
|
22016
|
+
return false;
|
|
22017
|
+
return (rest.match(/<[^>]*>/g) ?? []).every((tag) => TIMELINE_TAG.test(tag)) && !/<(?![^>]*>)/.test(rest);
|
|
22018
|
+
}
|
|
21945
22019
|
function renderTimelineHtml(docs, offsets, articleUrl) {
|
|
21946
22020
|
const rows = docs.map((d, i) => {
|
|
21947
22021
|
const title = escapeHtml(d.title ?? `\u6761\u76EE ${i + 1}`);
|
|
21948
22022
|
const brief = escapeHtml(itemBrief(d));
|
|
21949
22023
|
const at = formatOffset(offsets[i] ?? Number.NaN);
|
|
21950
|
-
const link = d.sourceUri ? ` <a href="${escapeHtml(d.sourceUri)}" target="_blank" rel="noopener">\u6765\u6E90</a>` : "";
|
|
22024
|
+
const link = d.sourceUri && isHttpUrl(d.sourceUri) ? ` <a href="${escapeHtml(d.sourceUri)}" target="_blank" rel="noopener">\u6765\u6E90</a>` : "";
|
|
21951
22025
|
return `
|
|
21952
22026
|
<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>`;
|
|
21953
22027
|
}).join("");
|
|
22028
|
+
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>`;
|
|
21954
22029
|
return `<!--kk:timeline-->
|
|
21955
|
-
|
|
21956
|
-
.kk-tl{margin:1em 0}
|
|
21957
|
-
.kk-tl-row{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid rgba(128,128,128,.25)}
|
|
21958
|
-
.kk-tl-t{flex:0 0 56px;font-variant-numeric:tabular-nums;font-weight:700;color:#0969da}
|
|
21959
|
-
.kk-tl-body{flex:1;min-width:0}
|
|
21960
|
-
.kk-tl-title{font-weight:600}
|
|
21961
|
-
.kk-tl-brief{margin:4px 0 0;color:#666;font-size:.95em}
|
|
21962
|
-
</style>
|
|
22030
|
+
${TIMELINE_STYLE}
|
|
21963
22031
|
<div class="kk-tl">${rows}
|
|
21964
22032
|
</div>
|
|
21965
|
-
|
|
22033
|
+
${articleTail}
|
|
21966
22034
|
<!--/kk:timeline-->`;
|
|
21967
22035
|
}
|
|
21968
22036
|
function renderShowNotesHtml(markdown) {
|
|
@@ -21976,6 +22044,15 @@ function renderShowNotesHtml(markdown) {
|
|
|
21976
22044
|
paragraph = [];
|
|
21977
22045
|
}
|
|
21978
22046
|
};
|
|
22047
|
+
const escapeBufferedTimeline = () => {
|
|
22048
|
+
for (const l of timeline) {
|
|
22049
|
+
const t = l.trim();
|
|
22050
|
+
if (t.length > 0)
|
|
22051
|
+
paragraph.push(t);
|
|
22052
|
+
}
|
|
22053
|
+
timeline.length = 0;
|
|
22054
|
+
flush();
|
|
22055
|
+
};
|
|
21979
22056
|
for (const rawLine of markdown.split(`
|
|
21980
22057
|
`)) {
|
|
21981
22058
|
const line = rawLine.trim();
|
|
@@ -21987,6 +22064,14 @@ function renderShowNotesHtml(markdown) {
|
|
|
21987
22064
|
if (inTimeline) {
|
|
21988
22065
|
if (line === "<!--/kk:timeline-->") {
|
|
21989
22066
|
inTimeline = false;
|
|
22067
|
+
if (isGeneratedTimeline(timeline.join(`
|
|
22068
|
+
`))) {
|
|
22069
|
+
blocks.push(timeline.join(`
|
|
22070
|
+
`));
|
|
22071
|
+
timeline.length = 0;
|
|
22072
|
+
} else {
|
|
22073
|
+
escapeBufferedTimeline();
|
|
22074
|
+
}
|
|
21990
22075
|
continue;
|
|
21991
22076
|
}
|
|
21992
22077
|
timeline.push(rawLine);
|
|
@@ -22010,8 +22095,7 @@ function renderShowNotesHtml(markdown) {
|
|
|
22010
22095
|
}
|
|
22011
22096
|
flush();
|
|
22012
22097
|
if (timeline.length > 0)
|
|
22013
|
-
|
|
22014
|
-
`));
|
|
22098
|
+
escapeBufferedTimeline();
|
|
22015
22099
|
return blocks.join(`
|
|
22016
22100
|
`);
|
|
22017
22101
|
}
|
|
@@ -22059,6 +22143,7 @@ function renderShowNotesMarkdown(docs, opts) {
|
|
|
22059
22143
|
}
|
|
22060
22144
|
|
|
22061
22145
|
// ../../plugins/publishings/podcast-pub/src/map.ts
|
|
22146
|
+
var COVER_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
|
|
22062
22147
|
var AUDIO_MIME_BY_EXTENSION = {
|
|
22063
22148
|
".mp3": "audio/mpeg",
|
|
22064
22149
|
".wav": "audio/wav",
|
|
@@ -22108,6 +22193,9 @@ function mapContentToEpisode(content) {
|
|
|
22108
22193
|
const link = articleUrl && articleUrl.length > 0 ? articleUrl : undefined;
|
|
22109
22194
|
const coverImage = typeof metadata.coverImage === "string" ? metadata.coverImage.trim() : undefined;
|
|
22110
22195
|
const imageUrl = coverImage?.startsWith("http") ? coverImage : undefined;
|
|
22196
|
+
if (coverImage && coverImage.length > 0 && !imageUrl && !COVER_EXTS.has(extname(coverImage).toLowerCase())) {
|
|
22197
|
+
throw new Error(`invalid metadata.coverImage: expected an http(s) URL or a local image (${[...COVER_EXTS].join(", ")}), got '${coverImage}'`);
|
|
22198
|
+
}
|
|
22111
22199
|
const imagePath = !imageUrl && coverImage && coverImage.length > 0 ? coverImage : undefined;
|
|
22112
22200
|
let datePublishedMs;
|
|
22113
22201
|
const publishedAt = metadata.publishedAt;
|
|
@@ -22314,6 +22402,13 @@ function resolveClient() {
|
|
|
22314
22402
|
function fail(error51) {
|
|
22315
22403
|
return ResultSchema.parse({ ok: false, target: TARGET, error: error51 });
|
|
22316
22404
|
}
|
|
22405
|
+
var COVER_MIME = {
|
|
22406
|
+
".png": "image/png",
|
|
22407
|
+
".jpg": "image/jpeg",
|
|
22408
|
+
".jpeg": "image/jpeg",
|
|
22409
|
+
".webp": "image/webp",
|
|
22410
|
+
".gif": "image/gif"
|
|
22411
|
+
};
|
|
22317
22412
|
function resolveOperation(options) {
|
|
22318
22413
|
const raw = options.operation ?? "publish";
|
|
22319
22414
|
if (typeof raw !== "string" || !OPERATIONS.includes(raw)) {
|
|
@@ -22492,14 +22587,15 @@ async function publishEpisode(client, payload, options) {
|
|
|
22492
22587
|
await client.uploadBytes(prepared.presignedUrl, bytes, episode.audio.mimeType);
|
|
22493
22588
|
let imageUrl = episode.imageUrl;
|
|
22494
22589
|
if (episode.imagePath && !imageUrl) {
|
|
22590
|
+
const imageMime = COVER_MIME[extname2(episode.imagePath).toLowerCase()] ?? "image/png";
|
|
22495
22591
|
const imageBytes = new Uint8Array(await readFile(episode.imagePath));
|
|
22496
22592
|
const imagePrepared = await client.prepareUpload({
|
|
22497
22593
|
category: "image",
|
|
22498
|
-
full_local_file_path: episode.imagePath,
|
|
22594
|
+
full_local_file_path: basename(episode.imagePath),
|
|
22499
22595
|
size: imageBytes.byteLength,
|
|
22500
|
-
type:
|
|
22596
|
+
type: imageMime
|
|
22501
22597
|
});
|
|
22502
|
-
await client.uploadBytes(imagePrepared.presignedUrl, imageBytes,
|
|
22598
|
+
await client.uploadBytes(imagePrepared.presignedUrl, imageBytes, imageMime);
|
|
22503
22599
|
imageUrl = imagePrepared.mediaUrl;
|
|
22504
22600
|
}
|
|
22505
22601
|
const item = await client.updateItem(itemId, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname } from 'node:path';
|
|
3
|
+
import { basename, dirname, extname } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { type Content, ContentSchema, DocListSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
6
6
|
import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
@@ -75,6 +75,18 @@ function fail(error: string): Result {
|
|
|
75
75
|
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Local-cover MIME by extension. `map.ts` has already rejected anything outside this set (R4), so
|
|
80
|
+
* the fallback is unreachable in practice; it keeps the field non-empty for the type system.
|
|
81
|
+
*/
|
|
82
|
+
const COVER_MIME: Record<string, string> = {
|
|
83
|
+
'.png': 'image/png',
|
|
84
|
+
'.jpg': 'image/jpeg',
|
|
85
|
+
'.jpeg': 'image/jpeg',
|
|
86
|
+
'.webp': 'image/webp',
|
|
87
|
+
'.gif': 'image/gif',
|
|
88
|
+
};
|
|
89
|
+
|
|
78
90
|
/** Parses and routes `options.operation`; unknown or misplaced values fail loud. */
|
|
79
91
|
function resolveOperation(options: PodcastPubOptions): { operation: Operation } | { error: string } {
|
|
80
92
|
const raw = options.operation ?? 'publish';
|
|
@@ -333,17 +345,20 @@ async function publishEpisode(
|
|
|
333
345
|
});
|
|
334
346
|
await client.uploadBytes(prepared.presignedUrl, bytes, episode.audio.mimeType);
|
|
335
347
|
|
|
336
|
-
// Phase 2b: upload a local cover via the presigned image flow (no item_id needed).
|
|
348
|
+
// Phase 2b: upload a local cover via the presigned image flow (no item_id needed). Only the
|
|
349
|
+
// basename is sent as `full_local_file_path`, and the MIME follows the real extension rather
|
|
350
|
+
// than assuming PNG (R4).
|
|
337
351
|
let imageUrl = episode.imageUrl;
|
|
338
352
|
if (episode.imagePath && !imageUrl) {
|
|
353
|
+
const imageMime = COVER_MIME[extname(episode.imagePath).toLowerCase()] ?? 'image/png';
|
|
339
354
|
const imageBytes = new Uint8Array(await readFile(episode.imagePath));
|
|
340
355
|
const imagePrepared = await client.prepareUpload({
|
|
341
356
|
category: 'image',
|
|
342
|
-
full_local_file_path: episode.imagePath,
|
|
357
|
+
full_local_file_path: basename(episode.imagePath),
|
|
343
358
|
size: imageBytes.byteLength,
|
|
344
|
-
type:
|
|
359
|
+
type: imageMime,
|
|
345
360
|
});
|
|
346
|
-
await client.uploadBytes(imagePrepared.presignedUrl, imageBytes,
|
|
361
|
+
await client.uploadBytes(imagePrepared.presignedUrl, imageBytes, imageMime);
|
|
347
362
|
imageUrl = imagePrepared.mediaUrl;
|
|
348
363
|
}
|
|
349
364
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
1
2
|
import type { Content } from '@gobing-ai/kk-core';
|
|
2
3
|
import { renderShowNotesHtml } from './show-notes';
|
|
3
4
|
|
|
5
|
+
/** Local-cover extensions accepted from `metadata.coverImage` (R4); anything else is rejected. */
|
|
6
|
+
const COVER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
7
|
+
|
|
4
8
|
/** Resolved episode fields derived from a Content payload (design D6). */
|
|
5
9
|
export interface EpisodeMapping {
|
|
6
10
|
/** Episode title: `content.title`, else the first `#` heading, else validation error. */
|
|
@@ -107,6 +111,13 @@ export function mapContentToEpisode(content: Content): EpisodeMapping {
|
|
|
107
111
|
// microfeed keeps remote image URLs verbatim (`image` field); a local cover file
|
|
108
112
|
// goes through the presigned image upload flow (no item_id required) instead.
|
|
109
113
|
const imageUrl = coverImage?.startsWith('http') ? coverImage : undefined;
|
|
114
|
+
// `coverImage` is agent-steerable via article metadata, so a local path must be a known image
|
|
115
|
+
// extension — otherwise an arbitrary file (/etc/passwd) would be read and uploaded (R4).
|
|
116
|
+
if (coverImage && coverImage.length > 0 && !imageUrl && !COVER_EXTS.has(extname(coverImage).toLowerCase())) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`invalid metadata.coverImage: expected an http(s) URL or a local image (${[...COVER_EXTS].join(', ')}), got '${coverImage}'`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
110
121
|
const imagePath = !imageUrl && coverImage && coverImage.length > 0 ? coverImage : undefined;
|
|
111
122
|
|
|
112
123
|
let datePublishedMs: number | undefined;
|
|
@@ -95,6 +95,37 @@ export function computeAudioOffsets(
|
|
|
95
95
|
return starts;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const isHttpUrl = (u: string): boolean => /^https?:\/\//i.test(u.trim());
|
|
99
|
+
|
|
100
|
+
/** The exact `<style>` block {@link renderTimelineHtml} emits; reused by the marker validator. */
|
|
101
|
+
const TIMELINE_STYLE = `<style>
|
|
102
|
+
.kk-tl{margin:1em 0}
|
|
103
|
+
.kk-tl-row{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid rgba(128,128,128,.25)}
|
|
104
|
+
.kk-tl-t{flex:0 0 56px;font-variant-numeric:tabular-nums;font-weight:700;color:#0969da}
|
|
105
|
+
.kk-tl-body{flex:1;min-width:0}
|
|
106
|
+
.kk-tl-title{font-weight:600}
|
|
107
|
+
.kk-tl-brief{margin:4px 0 0;color:#666;font-size:.95em}
|
|
108
|
+
</style>`;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Tags {@link renderTimelineHtml} can emit: `div`/`p`/`a` carrying only the exact class, href,
|
|
112
|
+
* target, and rel attributes it produces. Anything else inside a marker block is attacker-supplied.
|
|
113
|
+
*/
|
|
114
|
+
const TIMELINE_TAG =
|
|
115
|
+
/^<\/?(?:div|p|a)(?:\s+(?:class="kk-tl(?:-[a-z]+)?"|href="https?:\/\/[^"<>\s]*"|target="_blank"|rel="noopener"))*\s*>$/;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A marker block is trusted only if it is markup {@link renderTimelineHtml} could have produced.
|
|
119
|
+
* The markers are a legitimate cross-process channel (see {@link renderShowNotesHtml}), so the
|
|
120
|
+
* block is validated rather than stripped: a comment or `<style` inside it is an injection attempt,
|
|
121
|
+
* and any leftover stray `<` means the block is not well-formed generator output.
|
|
122
|
+
*/
|
|
123
|
+
function isGeneratedTimeline(block: string): boolean {
|
|
124
|
+
const rest = block.replace(TIMELINE_STYLE, '');
|
|
125
|
+
if (rest.includes('<style') || rest.includes('<!--')) return false;
|
|
126
|
+
return (rest.match(/<[^>]*>/g) ?? []).every((tag) => TIMELINE_TAG.test(tag)) && !/<(?![^>]*>)/.test(rest);
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
/**
|
|
99
130
|
* Brief-mode timeline block: two-column HTML (offset left, title + briefing right),
|
|
100
131
|
* marked `<!--kk:timeline-->` so {@link renderShowNotesHtml} passes it through
|
|
@@ -108,25 +139,24 @@ function renderTimelineHtml(docs: Doc[], offsets: number[], articleUrl: string):
|
|
|
108
139
|
const title = escapeHtml(d.title ?? `条目 ${i + 1}`);
|
|
109
140
|
const brief = escapeHtml(itemBrief(d));
|
|
110
141
|
const at = formatOffset(offsets[i] ?? Number.NaN);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
// A `sourceUri` that is not http(s) (e.g. `javascript:`) must not become a live href;
|
|
143
|
+
// escaping alone does not neutralise a scheme, so the link is dropped instead (R2).
|
|
144
|
+
const link =
|
|
145
|
+
d.sourceUri && isHttpUrl(d.sourceUri)
|
|
146
|
+
? ` <a href="${escapeHtml(d.sourceUri)}" target="_blank" rel="noopener">来源</a>`
|
|
147
|
+
: '';
|
|
114
148
|
return `
|
|
115
149
|
<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>`;
|
|
116
150
|
})
|
|
117
151
|
.join('');
|
|
152
|
+
const articleTail = isHttpUrl(articleUrl)
|
|
153
|
+
? `<p>完整图文版:<a href="${escapeHtml(articleUrl)}" target="_blank" rel="noopener">${escapeHtml('本期完整文章')}</a></p>`
|
|
154
|
+
: `<p>${escapeHtml('本期完整文章')}</p>`;
|
|
118
155
|
return `<!--kk:timeline-->
|
|
119
|
-
|
|
120
|
-
.kk-tl{margin:1em 0}
|
|
121
|
-
.kk-tl-row{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid rgba(128,128,128,.25)}
|
|
122
|
-
.kk-tl-t{flex:0 0 56px;font-variant-numeric:tabular-nums;font-weight:700;color:#0969da}
|
|
123
|
-
.kk-tl-body{flex:1;min-width:0}
|
|
124
|
-
.kk-tl-title{font-weight:600}
|
|
125
|
-
.kk-tl-brief{margin:4px 0 0;color:#666;font-size:.95em}
|
|
126
|
-
</style>
|
|
156
|
+
${TIMELINE_STYLE}
|
|
127
157
|
<div class="kk-tl">${rows}
|
|
128
158
|
</div>
|
|
129
|
-
|
|
159
|
+
${articleTail}
|
|
130
160
|
<!--/kk:timeline-->`;
|
|
131
161
|
}
|
|
132
162
|
|
|
@@ -150,6 +180,17 @@ export function renderShowNotesHtml(markdown: string): string {
|
|
|
150
180
|
}
|
|
151
181
|
};
|
|
152
182
|
|
|
183
|
+
// Escape every buffered timeline line through the normal paragraph path. Used when the block
|
|
184
|
+
// fails {@link isGeneratedTimeline} and for a marker that is never closed.
|
|
185
|
+
const escapeBufferedTimeline = () => {
|
|
186
|
+
for (const l of timeline) {
|
|
187
|
+
const t = l.trim();
|
|
188
|
+
if (t.length > 0) paragraph.push(t);
|
|
189
|
+
}
|
|
190
|
+
timeline.length = 0;
|
|
191
|
+
flush();
|
|
192
|
+
};
|
|
193
|
+
|
|
153
194
|
for (const rawLine of markdown.split('\n')) {
|
|
154
195
|
const line = rawLine.trim();
|
|
155
196
|
if (line === '<!--kk:timeline-->') {
|
|
@@ -160,6 +201,17 @@ export function renderShowNotesHtml(markdown: string): string {
|
|
|
160
201
|
if (inTimeline) {
|
|
161
202
|
if (line === '<!--/kk:timeline-->') {
|
|
162
203
|
inTimeline = false;
|
|
204
|
+
// The marker is a legitimate cross-process channel: renderShowNotesMarkdown writes
|
|
205
|
+
// the timeline into markdown, and that markdown later arrives as metadata.showNotes.
|
|
206
|
+
// Stripping the markers would break the feature, so the block is validated against
|
|
207
|
+
// the generator's own grammar instead — anything renderTimelineHtml could not have
|
|
208
|
+
// produced is escaped line by line rather than passed through (R2).
|
|
209
|
+
if (isGeneratedTimeline(timeline.join('\n'))) {
|
|
210
|
+
blocks.push(timeline.join('\n'));
|
|
211
|
+
timeline.length = 0;
|
|
212
|
+
} else {
|
|
213
|
+
escapeBufferedTimeline();
|
|
214
|
+
}
|
|
163
215
|
continue;
|
|
164
216
|
}
|
|
165
217
|
timeline.push(rawLine);
|
|
@@ -182,7 +234,8 @@ export function renderShowNotesHtml(markdown: string): string {
|
|
|
182
234
|
paragraph.push(line);
|
|
183
235
|
}
|
|
184
236
|
flush();
|
|
185
|
-
|
|
237
|
+
// An unclosed marker never reaches the closing branch above — escape whatever it buffered.
|
|
238
|
+
if (timeline.length > 0) escapeBufferedTimeline();
|
|
186
239
|
return blocks.join('\n');
|
|
187
240
|
}
|
|
188
241
|
|
|
@@ -21781,6 +21781,22 @@ var ResultSchema = exports_external.object({
|
|
|
21781
21781
|
error: exports_external.string().optional(),
|
|
21782
21782
|
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21783
21783
|
});
|
|
21784
|
+
var TargetOutcomeSchema = exports_external.object({
|
|
21785
|
+
target: exports_external.string(),
|
|
21786
|
+
ok: exports_external.boolean(),
|
|
21787
|
+
status: exports_external.enum(["published", "failed", "skipped"]),
|
|
21788
|
+
url: exports_external.string().optional(),
|
|
21789
|
+
id: exports_external.string().optional(),
|
|
21790
|
+
error: exports_external.string().optional(),
|
|
21791
|
+
durationMs: exports_external.number().optional(),
|
|
21792
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21793
|
+
});
|
|
21794
|
+
var AggregateResultSchema = exports_external.object({
|
|
21795
|
+
ok: exports_external.boolean(),
|
|
21796
|
+
summary: exports_external.object({ total: exports_external.number(), succeeded: exports_external.number(), failed: exports_external.number(), skipped: exports_external.number() }),
|
|
21797
|
+
targets: exports_external.array(TargetOutcomeSchema),
|
|
21798
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
21799
|
+
});
|
|
21784
21800
|
var DocListSchema = exports_external.array(DocSchema);
|
|
21785
21801
|
var omniVoiceGenContentSchema = ContentSchema.extend({
|
|
21786
21802
|
format: exports_external.literal("audio"),
|
|
@@ -21809,14 +21825,15 @@ var PluginKindSchema = exports_external.enum(PLUGIN_KINDS);
|
|
|
21809
21825
|
// ../../packages/kk-core/src/manifest.ts
|
|
21810
21826
|
init_zod();
|
|
21811
21827
|
var OperationIOSchema = exports_external.enum(["doc-list", "markdown"]);
|
|
21812
|
-
var
|
|
21828
|
+
var PluginOperationSchema = exports_external.object({ input: OperationIOSchema, output: OperationIOSchema });
|
|
21829
|
+
var PluginManifestSchema = exports_external.looseObject({
|
|
21813
21830
|
name: exports_external.string().min(1),
|
|
21814
21831
|
kind: PluginKindSchema,
|
|
21815
21832
|
entry: exports_external.string().min(1),
|
|
21816
21833
|
version: exports_external.string().optional(),
|
|
21817
21834
|
description: exports_external.string().optional(),
|
|
21818
|
-
operations: exports_external.record(exports_external.string(),
|
|
21819
|
-
})
|
|
21835
|
+
operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
|
|
21836
|
+
});
|
|
21820
21837
|
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/index.js
|
|
21821
21838
|
init_file_system_node();
|
|
21822
21839
|
init_config();
|
|
@@ -21842,43 +21859,82 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
|
|
|
21842
21859
|
// ../../packages/kk-core/src/render-md.ts
|
|
21843
21860
|
if (false) {}
|
|
21844
21861
|
// ../../packages/publish-harness/src/index.ts
|
|
21845
|
-
class
|
|
21846
|
-
|
|
21847
|
-
callLogs;
|
|
21862
|
+
class FakeTransportCore {
|
|
21863
|
+
callLogs = [];
|
|
21848
21864
|
nextResult;
|
|
21849
|
-
constructor() {
|
|
21850
|
-
this.callLogs = [];
|
|
21851
|
-
}
|
|
21852
21865
|
setNextResult(result) {
|
|
21853
21866
|
this.nextResult = result;
|
|
21854
21867
|
}
|
|
21855
|
-
async publish(payload) {
|
|
21868
|
+
async publish(payload, defaultResult) {
|
|
21856
21869
|
this.callLogs.push({
|
|
21857
21870
|
timestamp: new Date().toISOString(),
|
|
21858
21871
|
payload
|
|
21859
21872
|
});
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
return
|
|
21873
|
+
return this.nextResult ?? defaultResult;
|
|
21874
|
+
}
|
|
21875
|
+
getCallLogs() {
|
|
21876
|
+
return this.callLogs;
|
|
21877
|
+
}
|
|
21878
|
+
reset() {
|
|
21879
|
+
this.callLogs = [];
|
|
21880
|
+
this.nextResult = undefined;
|
|
21881
|
+
}
|
|
21882
|
+
}
|
|
21883
|
+
|
|
21884
|
+
class FakeFileCliTransport {
|
|
21885
|
+
kind = "file-cli";
|
|
21886
|
+
core = new FakeTransportCore;
|
|
21887
|
+
setNextResult(result) {
|
|
21888
|
+
this.core.setNextResult(result);
|
|
21889
|
+
}
|
|
21890
|
+
async publish(payload) {
|
|
21891
|
+
return this.core.publish(payload, {
|
|
21864
21892
|
ok: true,
|
|
21865
21893
|
target: "fake-file-cli",
|
|
21866
21894
|
url: "https://example.com/published-post",
|
|
21867
21895
|
id: "fake-id-123",
|
|
21868
21896
|
message: "Published offline via FakeFileCliTransport"
|
|
21869
|
-
};
|
|
21897
|
+
});
|
|
21870
21898
|
}
|
|
21871
21899
|
getCallLogs() {
|
|
21872
|
-
return this.
|
|
21900
|
+
return this.core.getCallLogs();
|
|
21873
21901
|
}
|
|
21874
21902
|
reset() {
|
|
21875
|
-
this.
|
|
21876
|
-
|
|
21903
|
+
this.core.reset();
|
|
21904
|
+
}
|
|
21905
|
+
}
|
|
21906
|
+
|
|
21907
|
+
class FakeBrowserTransport {
|
|
21908
|
+
kind = "browser";
|
|
21909
|
+
driver;
|
|
21910
|
+
core = new FakeTransportCore;
|
|
21911
|
+
constructor(driver = "agent") {
|
|
21912
|
+
this.driver = driver;
|
|
21913
|
+
}
|
|
21914
|
+
setNextResult(result) {
|
|
21915
|
+
this.core.setNextResult(result);
|
|
21916
|
+
}
|
|
21917
|
+
async publish(payload) {
|
|
21918
|
+
return this.core.publish(payload, {
|
|
21919
|
+
ok: true,
|
|
21920
|
+
target: "fake-browser",
|
|
21921
|
+
url: "https://example.com/browser-draft",
|
|
21922
|
+
id: "fake-browser-id-123",
|
|
21923
|
+
message: "Published offline via FakeBrowserTransport",
|
|
21924
|
+
metadata: { driver: this.driver }
|
|
21925
|
+
});
|
|
21926
|
+
}
|
|
21927
|
+
getCallLogs() {
|
|
21928
|
+
return this.core.getCallLogs();
|
|
21929
|
+
}
|
|
21930
|
+
reset() {
|
|
21931
|
+
this.core.reset();
|
|
21877
21932
|
}
|
|
21878
21933
|
}
|
|
21879
21934
|
|
|
21880
21935
|
// ../../plugins/publishings/qiita-pub/src/index.ts
|
|
21881
21936
|
var QIITA_ITEMS_URL = "https://qiita.com/api/v2/items";
|
|
21937
|
+
var REQUEST_TIMEOUT_MS = 120000;
|
|
21882
21938
|
function slugify2(str) {
|
|
21883
21939
|
return str.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
|
|
21884
21940
|
}
|
|
@@ -21944,6 +22000,7 @@ class QiitaApiTransport {
|
|
|
21944
22000
|
Authorization: `Bearer ${token}`,
|
|
21945
22001
|
"Content-Type": "application/json"
|
|
21946
22002
|
},
|
|
22003
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
21947
22004
|
body: JSON.stringify({
|
|
21948
22005
|
title: fields.title,
|
|
21949
22006
|
body: payload.content.body,
|
|
@@ -8,6 +8,9 @@ import { echoError } from '@gobing-ai/ts-utils';
|
|
|
8
8
|
/** Qiita API v2 article-create endpoint (audited from wt-publish-to-qiita). */
|
|
9
9
|
const QIITA_ITEMS_URL = 'https://qiita.com/api/v2/items';
|
|
10
10
|
|
|
11
|
+
/** Per-request timeout (ms) for the Qiita call — Bun's `fetch` has no default deadline (R5). */
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Converts a string into a lowercase hyphenated tag (slug) per the publish spec tags convention.
|
|
13
16
|
* Qiita tag names allow only alphanumeric, hyphen, and underscore, so the slugified form is
|
|
@@ -131,6 +134,7 @@ export class QiitaApiTransport implements PublishTransport {
|
|
|
131
134
|
Authorization: `Bearer ${token}`,
|
|
132
135
|
'Content-Type': 'application/json',
|
|
133
136
|
},
|
|
137
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
134
138
|
body: JSON.stringify({
|
|
135
139
|
title: fields.title,
|
|
136
140
|
body: payload.content.body,
|