@gobing-ai/knowledge-kit 0.0.11 → 0.0.12
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/dist/index.js +363 -138
- package/package.json +1 -1
- package/plugins/generations/content-gen/src/storm.ts +99 -50
- package/plugins/generations/core-facts-gen/package.json +17 -0
- package/plugins/generations/core-facts-gen/plugin.json +7 -0
- package/plugins/generations/core-facts-gen/src/index.ts +116 -0
- package/plugins/generations/core-facts-gen/tsconfig.json +4 -0
- package/plugins/generations/daily-article-gen/package.json +17 -0
- package/plugins/generations/daily-article-gen/plugin.json +7 -0
- package/plugins/generations/daily-article-gen/src/index.ts +91 -0
- package/plugins/generations/daily-article-gen/tsconfig.json +4 -0
- package/plugins/generations/dailynews-gen/src/index.ts +11 -0
- package/plugins/generations/dailynews-gen/src/script-builder.ts +1 -1
- package/plugins/generations/episode-plan-gen/package.json +17 -0
- package/plugins/generations/episode-plan-gen/plugin.json +7 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +726 -0
- package/plugins/generations/episode-plan-gen/tsconfig.json +4 -0
- package/plugins/generations/voice-gen/src/index.ts +102 -11
- package/plugins/generations/voice-gen/src/qc.ts +154 -9
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/aihot-ingest/src/index.ts +72 -13
- package/plugins/ingestions/aihot-ingest/src/mapper.ts +1 -0
- package/plugins/ingestions/aihot-ingest/src/rss.ts +151 -0
- package/plugins/ingestions/horizon-ingest/package.json +17 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +7 -0
- package/plugins/ingestions/horizon-ingest/src/index.ts +205 -0
- package/plugins/ingestions/horizon-ingest/tsconfig.json +4 -0
- package/plugins/ingestions/last30days-ingest/package.json +17 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +7 -0
- package/plugins/ingestions/last30days-ingest/src/index.ts +148 -0
- package/plugins/ingestions/last30days-ingest/tsconfig.json +4 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +12 -6
- package/plugins/kk/skills/taste-unslop/references/pattern-guide.md +128 -48
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +432 -19
- package/plugins/publishings/podcast-pub/package.json +17 -0
- package/plugins/publishings/podcast-pub/plugin.json +7 -0
- package/plugins/publishings/podcast-pub/src/index.ts +538 -0
- package/plugins/publishings/podcast-pub/src/map.ts +165 -0
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +196 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +132 -0
- package/plugins/publishings/podcast-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +328 -62
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Content, ContentSchema, type Doc } from '@gobing-ai/kk-core';
|
|
2
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { parseAgentJson } from './agent-json';
|
|
4
5
|
|
|
@@ -21,60 +22,97 @@ export interface StormStageRunner {
|
|
|
21
22
|
runStage(prompt: string): Promise<AgentRunEnvelope>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
interface SpawnOutcome {
|
|
26
|
+
kind: 'ok' | 'crash' | 'malformed';
|
|
27
|
+
envelope: AgentRunEnvelope;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One `spur agent run` invocation + envelope unwrap. */
|
|
31
|
+
async function spawnAgentOnce(prompt: string): Promise<SpawnOutcome> {
|
|
32
|
+
const proc = Bun.spawn(
|
|
33
|
+
[
|
|
34
|
+
process.env.KNOWLEDGE_KIT_SPUR_BIN ?? 'spur',
|
|
35
|
+
'agent',
|
|
36
|
+
'run',
|
|
37
|
+
prompt,
|
|
38
|
+
'--agent',
|
|
39
|
+
'auto',
|
|
40
|
+
'--mode',
|
|
41
|
+
'text',
|
|
42
|
+
'--json',
|
|
43
|
+
'--cwd',
|
|
44
|
+
// Anchor the agent's cwd to the project root (set by the executor, whose own
|
|
45
|
+
// cwd is the invocation root). process.cwd() here is the plugin package dir —
|
|
46
|
+
// using it makes spur auto-init a stray `.spur/` and drops agent files there.
|
|
47
|
+
process.env.KNOWLEDGE_KIT_PROJECT_ROOT ?? process.cwd(),
|
|
48
|
+
],
|
|
49
|
+
{ stdout: 'pipe', stderr: 'pipe' },
|
|
50
|
+
);
|
|
51
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
52
|
+
proc.exited,
|
|
53
|
+
new Response(proc.stdout).text(),
|
|
54
|
+
new Response(proc.stderr).text(),
|
|
55
|
+
]);
|
|
56
|
+
if (exitCode !== 0) {
|
|
57
|
+
// CLI-level failure (spawn/argv): no envelope to unwrap; stages prefix the diagnostic.
|
|
58
|
+
return { kind: 'crash', envelope: { exitCode, stdout, stderr } };
|
|
59
|
+
}
|
|
60
|
+
// The CLI writes the agent envelope {exitCode, stdout, stderr, durationMs} to stdout.
|
|
61
|
+
// Unwrap it so stages consume the agent output.
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(stdout) as Partial<AgentRunEnvelope>;
|
|
64
|
+
if (
|
|
65
|
+
typeof parsed.exitCode !== 'number' ||
|
|
66
|
+
typeof parsed.stdout !== 'string' ||
|
|
67
|
+
typeof parsed.stderr !== 'string'
|
|
68
|
+
) {
|
|
69
|
+
throw new Error('expected envelope {exitCode:number, stdout:string, stderr:string}');
|
|
51
70
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
typeof parsed.stdout !== 'string' ||
|
|
60
|
-
typeof parsed.stderr !== 'string'
|
|
61
|
-
) {
|
|
62
|
-
throw new Error('expected envelope {exitCode:number, stdout:string, stderr:string}');
|
|
63
|
-
}
|
|
64
|
-
envelope = parsed as AgentRunEnvelope;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
// Exit-0 but non-envelope stdout: return a runner-level diagnostic so the calling
|
|
67
|
-
// stage prefixes it ({Stage} stage failed: …) instead of leaking a raw SyntaxError.
|
|
68
|
-
return {
|
|
71
|
+
const envelope = parsed as AgentRunEnvelope;
|
|
72
|
+
return { kind: envelope.exitCode === 0 ? 'ok' : 'crash', envelope };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Exit-0 but non-envelope stdout: deterministic contract failure — never retried.
|
|
75
|
+
return {
|
|
76
|
+
kind: 'malformed',
|
|
77
|
+
envelope: {
|
|
69
78
|
exitCode: 1,
|
|
70
79
|
stdout: '',
|
|
71
80
|
stderr: `spur agent run returned malformed output: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
-
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Concrete runner invoking the existing `spur agent run` boundary (same call shape as the D2 stand-in). */
|
|
87
|
+
export const spurStageRunner: StormStageRunner = {
|
|
88
|
+
async runStage(prompt: string): Promise<AgentRunEnvelope> {
|
|
89
|
+
// A crashed agent subprocess (e.g. polish stage exit 1, dogfood daily-ai-news-20260903)
|
|
90
|
+
// must not kill a 90-minute pipeline run — retry once. Malformed envelopes are a
|
|
91
|
+
// deterministic contract failure and are not retried.
|
|
92
|
+
let last: AgentRunEnvelope = { exitCode: 1, stdout: '', stderr: 'no attempt executed' };
|
|
93
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
94
|
+
const outcome = await spawnAgentOnce(prompt);
|
|
95
|
+
if (outcome.kind !== 'crash') return outcome.envelope;
|
|
96
|
+
last = outcome.envelope;
|
|
97
|
+
if (attempt === 1) {
|
|
98
|
+
echoError(
|
|
99
|
+
`spurStageRunner: agent run exited ${last.exitCode}; retrying once (${last.stderr.trim().slice(0, 200)})`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
73
102
|
}
|
|
74
|
-
return
|
|
103
|
+
return last;
|
|
75
104
|
},
|
|
76
105
|
};
|
|
77
106
|
|
|
107
|
+
/** Language directive appended to outline/article/polish prompts when CONTENT_LANGUAGE is set. */
|
|
108
|
+
function languageDirective(): string {
|
|
109
|
+
const lang = process.env.CONTENT_LANGUAGE?.trim();
|
|
110
|
+
if (!lang) return '';
|
|
111
|
+
return lang === 'zh'
|
|
112
|
+
? 'Write everything in Simplified Chinese (简体中文), including the title and all section headings.'
|
|
113
|
+
: `Write everything in ${lang}, including the title and all section headings.`;
|
|
114
|
+
}
|
|
115
|
+
|
|
78
116
|
const PerspectiveSchema = z.object({
|
|
79
117
|
id: z.string().min(1),
|
|
80
118
|
name: z.string().min(1),
|
|
@@ -166,7 +204,9 @@ export async function curationStage(runner: StormStageRunner, docs: Doc[], input
|
|
|
166
204
|
[
|
|
167
205
|
'Develop multiple distinct perspectives (personas or angles) on the source material. For each perspective, list the questions it would ask and cite the supplied document IDs that ground it.',
|
|
168
206
|
'Return only a JSON object matching {"perspectives":[{"id":string,"name":string,"questions":[string],"evidence":[string]}]} where every evidence entry is one of the allowed document IDs.',
|
|
169
|
-
]
|
|
207
|
+
]
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join('\n'),
|
|
170
210
|
);
|
|
171
211
|
const envelope = await runner.runStage(prompt);
|
|
172
212
|
if (envelope.exitCode !== 0) {
|
|
@@ -204,8 +244,11 @@ export async function outlineStage(
|
|
|
204
244
|
[
|
|
205
245
|
`The validated curation result for the article is: ${JSON.stringify(curation)}`,
|
|
206
246
|
'Produce a structured markdown outline for the article derived from that curation result.',
|
|
247
|
+
languageDirective(),
|
|
207
248
|
'Return only a JSON object matching {"title":string,"outline":string,"references":[string]} where outline is markdown and every references entry is one of the allowed document IDs.',
|
|
208
|
-
]
|
|
249
|
+
]
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join('\n'),
|
|
209
252
|
);
|
|
210
253
|
const envelope = await runner.runStage(prompt);
|
|
211
254
|
if (envelope.exitCode !== 0) {
|
|
@@ -242,9 +285,12 @@ export async function articleStage(
|
|
|
242
285
|
`The validated curation result for the article is: ${JSON.stringify(curation)}`,
|
|
243
286
|
`The validated outline for the article is: ${JSON.stringify(outline)}`,
|
|
244
287
|
'Write the complete article as markdown, one section per outline heading. Every citation must reference one of the allowed document IDs.',
|
|
288
|
+
languageDirective(),
|
|
245
289
|
'Synthesize: the body must draw findings from across the supplied documents, compare them, and state conclusions. Never list sources one per line or produce an index of references — references are evidence, not the deliverable.',
|
|
246
290
|
'Return only a JSON object matching {"title":string,"body":string,"citations":[string]} where body is the full markdown draft and every citations entry is one of the allowed document IDs.',
|
|
247
|
-
]
|
|
291
|
+
]
|
|
292
|
+
.filter(Boolean)
|
|
293
|
+
.join('\n'),
|
|
248
294
|
);
|
|
249
295
|
const envelope = await runner.runStage(prompt);
|
|
250
296
|
if (envelope.exitCode !== 0) {
|
|
@@ -278,8 +324,11 @@ export async function polishStage(
|
|
|
278
324
|
[
|
|
279
325
|
`The drafted article is: ${JSON.stringify(article)}`,
|
|
280
326
|
'Polish the draft: preserve every heading and citation exactly, remove repetition, and tighten the prose. Do not cite any document outside the allowed IDs.',
|
|
327
|
+
languageDirective(),
|
|
281
328
|
'Return only a JSON object matching {"title":string,"body":string,"outline":string} where body is the polished markdown (headings and citations preserved) and outline is a markdown outline of the final article.',
|
|
282
|
-
]
|
|
329
|
+
]
|
|
330
|
+
.filter(Boolean)
|
|
331
|
+
.join('\n'),
|
|
283
332
|
);
|
|
284
333
|
const envelope = await runner.runStage(prompt);
|
|
285
334
|
if (envelope.exitCode !== 0) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobing-ai/core-facts-gen",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"typecheck": "tsc --noEmit"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@gobing-ai/kk-core": "workspace:*",
|
|
10
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
11
|
+
"@gobing-ai/ts-utils": "catalog:",
|
|
12
|
+
"zod": "4.4.3"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.14"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { type Content, ContentSchema, type Doc, DocListSchema } from '@gobing-ai/kk-core';
|
|
4
|
+
import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
|
|
5
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* core-facts-gen (task 0090 contract, docs/design/core-facts-template.md):
|
|
9
|
+
* render a merged `Doc[]` (from `kk executor fan-in`) into one dated core-facts
|
|
10
|
+
* markdown `Content`. Mechanical template fill — no LLM.
|
|
11
|
+
*
|
|
12
|
+
* Run date: `CORE_FACTS_DATE` env var, else today (UTC).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Narrow a metadata.sources[] entry to its `file` string (provenance shape written by fan-in). */
|
|
16
|
+
function sourceFileOf(entry: unknown): string | null {
|
|
17
|
+
if (entry && typeof entry === 'object' && 'file' in entry) {
|
|
18
|
+
const file = entry.file;
|
|
19
|
+
return typeof file === 'string' ? file : null;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Render the facts markdown for a merged Doc[] (pure — exported for tests). */
|
|
25
|
+
export function renderCoreFacts(docs: Doc[], date: string): string {
|
|
26
|
+
const sources = new Set<string>();
|
|
27
|
+
for (const doc of docs) {
|
|
28
|
+
const entries = doc.metadata?.sources;
|
|
29
|
+
if (Array.isArray(entries)) {
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const file = sourceFileOf(entry);
|
|
32
|
+
if (file) sources.add(file);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const roster = [...sources].map((file) => ` - ${basename(file)} (${file})`).join('\n');
|
|
37
|
+
|
|
38
|
+
const labelOf = (d: Doc): string => {
|
|
39
|
+
const source = d.metadata?.source;
|
|
40
|
+
if (typeof source === 'string' && source) return source;
|
|
41
|
+
const entries = d.metadata?.sources;
|
|
42
|
+
if (Array.isArray(entries)) {
|
|
43
|
+
const file = entries.length > 0 ? sourceFileOf(entries[0]) : null;
|
|
44
|
+
if (file) return basename(file);
|
|
45
|
+
}
|
|
46
|
+
return '';
|
|
47
|
+
};
|
|
48
|
+
const scoreOf = (d: Doc): unknown => d.metadata?.score ?? d.metadata?.relevance_score ?? '';
|
|
49
|
+
const dateOf = (d: Doc): string => {
|
|
50
|
+
const published = d.metadata?.published_at;
|
|
51
|
+
return typeof published === 'string' ? published : '';
|
|
52
|
+
};
|
|
53
|
+
const headlines = docs
|
|
54
|
+
.map(
|
|
55
|
+
(d) =>
|
|
56
|
+
`### ${d.title ?? ''}\n` +
|
|
57
|
+
`**Source:** ${labelOf(d)} · **URI:** ${d.sourceUri ?? ''} · **ID:** \`${d.id}\`\n` +
|
|
58
|
+
`**Date:** ${dateOf(d)} · **Score:** ${String(scoreOf(d))}\n` +
|
|
59
|
+
`${d.body}`,
|
|
60
|
+
)
|
|
61
|
+
.join('\n\n');
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
`---\ndate: ${date}\nsources:\n${roster}\ndedup_rule: id\ndoc_count: ${docs.length}\n---\n\n` +
|
|
65
|
+
`# Core Facts — ${date}\n\n` +
|
|
66
|
+
'> Daily AI news core-facts digest. Merge: `kk executor fan-in` (id dedupe, first-seen wins).\n' +
|
|
67
|
+
'> Render: core-facts-gen generator (mechanical; no LLM).\n\n' +
|
|
68
|
+
`## Headlines\n\n${headlines}\n`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function docsToContent(docs: Doc[], date: string): Content {
|
|
73
|
+
return ContentSchema.parse({
|
|
74
|
+
title: `Core Facts — ${date}`,
|
|
75
|
+
body: renderCoreFacts(docs, date),
|
|
76
|
+
format: 'markdown',
|
|
77
|
+
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
78
|
+
metadata: { generator: 'kk:core-facts', date, doc_count: docs.length },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function processGeneratorIO(inputPath: string, outputPath: string): Promise<Content> {
|
|
83
|
+
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
84
|
+
const date = process.env.CORE_FACTS_DATE ?? new Date().toISOString().slice(0, 10);
|
|
85
|
+
const content = docsToContent(docs, date);
|
|
86
|
+
const fs = createNodeFileSystem();
|
|
87
|
+
const outDir = dirname(outputPath);
|
|
88
|
+
if (outDir && outDir !== '.') await fs.ensureDir(outDir);
|
|
89
|
+
await atomicWriteJson(outputPath, content, fs);
|
|
90
|
+
return content;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function main(): Promise<number> {
|
|
94
|
+
let values: { in?: string; out?: string };
|
|
95
|
+
try {
|
|
96
|
+
({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
echoError(`core-facts-gen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (!values.in || !values.out) {
|
|
102
|
+
echoError('core-facts-gen failed: Missing required arguments: --in <docs.json> --out <content.json>');
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
await processGeneratorIO(values.in, values.out);
|
|
107
|
+
return 0;
|
|
108
|
+
} catch (err: unknown) {
|
|
109
|
+
echoError(`core-facts-gen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (import.meta.main) {
|
|
115
|
+
process.exit(await main());
|
|
116
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobing-ai/daily-article-gen",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"typecheck": "tsc --noEmit"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@gobing-ai/kk-core": "workspace:*",
|
|
10
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
11
|
+
"@gobing-ai/ts-utils": "catalog:",
|
|
12
|
+
"zod": "4.4.3"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.14"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "daily-article-gen",
|
|
3
|
+
"kind": "generator",
|
|
4
|
+
"entry": "./src/index.ts",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Renders a merged Doc[] into a readable daily AI-news article with per-item source links (mechanical template, no LLM) — replaces the heavy STORM flow"
|
|
7
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { type Content, ContentSchema, type Doc, DocListSchema } from '@gobing-ai/kk-core';
|
|
4
|
+
import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
|
|
5
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* daily-article-gen: render a merged `Doc[]` into one readable daily AI-news
|
|
9
|
+
* article. Mechanical template fill — no LLM, no STORM. Every news item keeps
|
|
10
|
+
* a human-clickable source link (`[来源](sourceUri)`) so readers can trace
|
|
11
|
+
* claims to their origin.
|
|
12
|
+
*
|
|
13
|
+
* Run date: `ARTICLE_DATE` env var (YYYYMMDD or ISO), else today (UTC).
|
|
14
|
+
* Language-neutral: the Doc bodies already carry the summary language.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Pretty source label for an item line. */
|
|
18
|
+
function sourceLabelOf(doc: Doc): string {
|
|
19
|
+
const meta = doc.metadata as Record<string, unknown> | undefined;
|
|
20
|
+
const name = meta?.sourceName;
|
|
21
|
+
if (typeof name === 'string' && name.trim().length > 0) return name.trim();
|
|
22
|
+
try {
|
|
23
|
+
return new URL(doc.sourceUri ?? '').hostname.replace(/^www\./, '');
|
|
24
|
+
} catch {
|
|
25
|
+
return '来源';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Render one news item section: heading, source link, and summary body. */
|
|
30
|
+
function renderDoc(doc: Doc, index: number): string {
|
|
31
|
+
const heading = doc.title?.trim() || `新闻 ${index + 1}`;
|
|
32
|
+
const link = doc.sourceUri ? `[来源:${sourceLabelOf(doc)}](${doc.sourceUri})` : '';
|
|
33
|
+
const meta = doc.metadata as Record<string, unknown> | undefined;
|
|
34
|
+
const publishedAt = typeof meta?.publishedAt === 'string' ? meta.publishedAt.slice(0, 10) : '';
|
|
35
|
+
const metaLine = [link, publishedAt].filter(Boolean).join(' · ');
|
|
36
|
+
return `## ${index + 1}. ${heading}\n\n${metaLine ? `${metaLine}\n\n` : ''}${(doc.body ?? '').trim()}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Render the article markdown for a merged Doc[] (pure — exported for tests). */
|
|
40
|
+
export function renderDailyArticle(docs: Doc[], date: string): string {
|
|
41
|
+
const items = docs.map(renderDoc).join('\n\n');
|
|
42
|
+
return `# 每日 AI 资讯 — ${date}\n\n本期共 ${docs.length} 条新闻,均附可点击来源链接。\n\n${items}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build the article Content from a merged Doc[]. */
|
|
46
|
+
export function docsToContent(docs: Doc[], date: string): Content {
|
|
47
|
+
return ContentSchema.parse({
|
|
48
|
+
title: `每日 AI 资讯 — ${date}`,
|
|
49
|
+
body: renderDailyArticle(docs, date),
|
|
50
|
+
format: 'markdown',
|
|
51
|
+
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
52
|
+
metadata: { generator: 'kk:daily-article', date, doc_count: docs.length },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function processGeneratorIO(inputPath: string, outputPath: string): Promise<Content> {
|
|
57
|
+
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
58
|
+
const raw = process.env.ARTICLE_DATE ?? new Date().toISOString().slice(0, 10);
|
|
59
|
+
const date = raw.length === 8 ? `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}` : raw;
|
|
60
|
+
const content = docsToContent(docs, date);
|
|
61
|
+
const fs = createNodeFileSystem();
|
|
62
|
+
const outDir = dirname(outputPath);
|
|
63
|
+
if (outDir && outDir !== '.') await fs.ensureDir(outDir);
|
|
64
|
+
await atomicWriteJson(outputPath, content, fs);
|
|
65
|
+
return content;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function main(): Promise<number> {
|
|
69
|
+
let values: { in?: string; out?: string };
|
|
70
|
+
try {
|
|
71
|
+
({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
|
|
72
|
+
} catch (err: unknown) {
|
|
73
|
+
echoError(`daily-article-gen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (!values.in || !values.out) {
|
|
77
|
+
echoError('daily-article-gen failed: Missing required arguments: --in <docs.json> --out <content.json>');
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await processGeneratorIO(values.in, values.out);
|
|
82
|
+
return 0;
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
echoError(`daily-article-gen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (import.meta.main) {
|
|
90
|
+
process.exit(await main());
|
|
91
|
+
}
|
|
@@ -27,6 +27,17 @@ export async function processGeneratorIO(
|
|
|
27
27
|
throw new Error(`Invalid DocList input: ${err instanceof Error ? err.message : String(err)}`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Optional story cap (daily pipeline: the blend carries 50+ docs; a broadcast needs a bound).
|
|
31
|
+
// Input order is curated-first (fan-in first-seen), so a head-cap keeps the best items.
|
|
32
|
+
const maxItemsEnv = process.env.DAILYNEWS_MAX_ITEMS;
|
|
33
|
+
if (maxItemsEnv) {
|
|
34
|
+
const maxItems = Number.parseInt(maxItemsEnv, 10);
|
|
35
|
+
if (Number.isInteger(maxItems) && maxItems > 0 && docs.length > maxItems) {
|
|
36
|
+
echoError(`dailynews-gen: capping ${docs.length} docs to ${maxItems} (DAILYNEWS_MAX_ITEMS)`);
|
|
37
|
+
docs = docs.slice(0, maxItems);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
const script = buildNewsVoiceScript(docs, options);
|
|
31
42
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
32
43
|
|
|
@@ -269,7 +269,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
269
269
|
? idx === 0
|
|
270
270
|
? `首先来聊聊大家非常关注的【${cleanTitle}】。`
|
|
271
271
|
: isLast
|
|
272
|
-
?
|
|
272
|
+
? `最后,来看看今天的最后一条资讯,【${cleanTitle}】。`
|
|
273
273
|
: `接着我们把目光转向另一条重要进展,【${cleanTitle}】。`
|
|
274
274
|
: idx === 0
|
|
275
275
|
? `First up today, let's look at ${cleanTitle}.`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobing-ai/episode-plan-gen",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"typecheck": "tsc --noEmit"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@gobing-ai/kk-core": "workspace:*",
|
|
10
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
11
|
+
"@gobing-ai/ts-utils": "catalog:",
|
|
12
|
+
"zod": "4.4.3"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.14"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "episode-plan-gen",
|
|
3
|
+
"kind": "generator",
|
|
4
|
+
"entry": "./src/index.ts",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Plans the daily episode: scores blended Doc[] (source weight + normalized relevance + log-scaled engagement), dedups by URL and title bigram, selects top N, flags English items, strips rating/heading leftovers (mechanical, no LLM)"
|
|
7
|
+
}
|