@gobing-ai/knowledge-kit 0.0.27 → 0.0.29
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/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +27 -4
- package/plugins/generations/daily-article-gen/src/index.ts +34 -3
- package/plugins/generations/dailynews-gen/dist/index.js +42 -19
- package/plugins/generations/dailynews-gen/src/index.ts +29 -1
- package/plugins/generations/dailynews-gen/src/script-builder.ts +33 -21
- package/plugins/generations/episode-plan-gen/dist/index.js +7 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +12 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/OFL.txt +92 -0
- package/plugins/generations/image-gen/assets/fonts/SOURCE.md +15 -0
- package/plugins/generations/image-gen/dist/index.js +597 -16
- package/plugins/generations/image-gen/package.json +1 -0
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +2 -2
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +19 -11
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -1
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +135 -0
- package/plugins/generations/image-gen/src/bytes.ts +28 -0
- package/plugins/generations/image-gen/src/compose/card.ts +294 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +147 -0
- package/plugins/generations/image-gen/src/compose/cmap.ts +177 -0
- package/plugins/generations/image-gen/src/index.ts +167 -9
- package/plugins/generations/image-gen/src/job.ts +6 -0
- package/plugins/generations/image-gen/src/presets.ts +53 -13
- package/plugins/generations/image-gen/src/providers/google.ts +9 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +25 -12
- package/plugins/kk/scripts/kk-workflow-stages.ts +344 -11
- package/plugins/kk/scripts/topic-boundary.json +105 -0
- package/plugins/kk/scripts/validate-voicescript.ts +15 -0
- package/plugins/kk/skills/content-safety/SKILL.md +163 -0
- package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/netease-music.md +31 -0
- package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +32 -0
- package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +35 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +11 -7
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +4 -4
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +118 -16
- package/plugins/publishings/podcast-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/src/index.ts +2 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +272 -115
- package/plugins/publishings/wechatmp-pub/src/index.ts +336 -40
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +21 -9
- package/plugins/publishings/xhs-pub/dist/index.js +28 -22
- package/plugins/publishings/xhs-pub/src/index.ts +30 -22
package/package.json
CHANGED
|
@@ -22035,13 +22035,19 @@ ${metaLine ? `${metaLine}
|
|
|
22035
22035
|
|
|
22036
22036
|
` : ""}${stripScaffold(doc2.body ?? "").trim()}`;
|
|
22037
22037
|
}
|
|
22038
|
+
function episodeTitle(docs, date6) {
|
|
22039
|
+
const lead = docs[0]?.title?.trim() ?? "";
|
|
22040
|
+
const theme = lead !== "" ? lead : "\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5";
|
|
22041
|
+
return `AI \u4E71\u7096\uFF5C${theme} \xB7 ${date6}`;
|
|
22042
|
+
}
|
|
22038
22043
|
function renderDailyArticle(docs, date6) {
|
|
22039
22044
|
const items = docs.map(renderDoc).join(`
|
|
22040
22045
|
|
|
22041
22046
|
`);
|
|
22042
|
-
|
|
22047
|
+
const intro = docs.length > 0 ? `\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u5171 ${docs.length} \u6761\u5185\u5BB9\uFF0C\u5747\u9644\u53EF\u70B9\u51FB\u6765\u6E90\u94FE\u63A5\u3002` : "\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002";
|
|
22048
|
+
return `# ${episodeTitle(docs, date6)}
|
|
22043
22049
|
|
|
22044
|
-
|
|
22050
|
+
${intro}
|
|
22045
22051
|
|
|
22046
22052
|
${items}
|
|
22047
22053
|
`;
|
|
@@ -22054,10 +22060,26 @@ function tagsFor(docs) {
|
|
|
22054
22060
|
});
|
|
22055
22061
|
return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
|
|
22056
22062
|
}
|
|
22063
|
+
var LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
22057
22064
|
function docsToContent(docs, date6, bodyOverride) {
|
|
22065
|
+
const title = episodeTitle(docs, date6);
|
|
22066
|
+
let body = renderDailyArticle(docs, date6);
|
|
22067
|
+
if (bodyOverride) {
|
|
22068
|
+
const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), "");
|
|
22069
|
+
body = `# ${title}
|
|
22070
|
+
|
|
22071
|
+
${stripped}
|
|
22072
|
+
`;
|
|
22073
|
+
}
|
|
22074
|
+
body = `${body.trimEnd()}
|
|
22075
|
+
|
|
22076
|
+
---
|
|
22077
|
+
|
|
22078
|
+
*\u6587\u5B57\u7531 AI \u8F85\u52A9\u6574\u7406\uFF0C\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002*
|
|
22079
|
+
`;
|
|
22058
22080
|
return ContentSchema.parse({
|
|
22059
|
-
title
|
|
22060
|
-
body
|
|
22081
|
+
title,
|
|
22082
|
+
body,
|
|
22061
22083
|
format: "markdown",
|
|
22062
22084
|
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
22063
22085
|
metadata: { generator: "kk:daily-article", date: date6, doc_count: docs.length, tags: tagsFor(docs) }
|
|
@@ -22110,5 +22132,6 @@ export {
|
|
|
22110
22132
|
renderDailyArticle,
|
|
22111
22133
|
processGeneratorIO,
|
|
22112
22134
|
main2 as main,
|
|
22135
|
+
episodeTitle,
|
|
22113
22136
|
docsToContent
|
|
22114
22137
|
};
|
|
@@ -102,10 +102,27 @@ function renderDoc(doc: Doc, index: number): string {
|
|
|
102
102
|
return `## ${index + 1}. ${heading}\n\n${metaLine ? `${metaLine}\n\n` : ''}${stripScaffold(doc.body ?? '').trim()}`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Episode title for the 「AI 乱炖」 column (task 0189 R1/R4): `AI 乱炖|<本期实际技术主题> · YYYY-MM-DD`.
|
|
107
|
+
* The deterministic proxy for 本期实际技术主题 is the lead plan item's title — the same doc the
|
|
108
|
+
* cover brief and XHS cards already treat as the episode's lead story. Pure function so the
|
|
109
|
+
* agent-written and template article paths, ID3, RSS and show notes all derive one identical
|
|
110
|
+
* title; never re-introduces the retired news-bulletin positioning (task 0189 R4).
|
|
111
|
+
*/
|
|
112
|
+
export function episodeTitle(docs: Doc[], date: string): string {
|
|
113
|
+
const lead = docs[0]?.title?.trim() ?? '';
|
|
114
|
+
const theme = lead !== '' ? lead : '工具、论文与实践';
|
|
115
|
+
return `AI 乱炖|${theme} · ${date}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
/** Render the article markdown for a merged Doc[] (pure — exported for tests). */
|
|
106
119
|
export function renderDailyArticle(docs: Doc[], date: string): string {
|
|
107
120
|
const items = docs.map(renderDoc).join('\n\n');
|
|
108
|
-
|
|
121
|
+
const intro =
|
|
122
|
+
docs.length > 0
|
|
123
|
+
? `「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期共 ${docs.length} 条内容,均附可点击来源链接。`
|
|
124
|
+
: '「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期没有足够扎实的可聊内容。';
|
|
125
|
+
return `# ${episodeTitle(docs, date)}\n\n${intro}\n\n${items}\n`;
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
/** Single-source daily tag: stamped on every Content this generator emits (task 0119). */
|
|
@@ -120,11 +137,25 @@ function tagsFor(docs: Doc[]): string[] {
|
|
|
120
137
|
return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
/** A leading H1 line — agent bodies used to carry their own title; the canonical title is computed (task 0189 R4). */
|
|
141
|
+
const LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
142
|
+
|
|
123
143
|
/** Build the article Content from a merged Doc[]; `bodyOverride` replaces the mechanical render (agent-written prose), still scaffold-stripped as a safety net. */
|
|
124
144
|
export function docsToContent(docs: Doc[], date: string, bodyOverride?: string): Content {
|
|
145
|
+
const title = episodeTitle(docs, date);
|
|
146
|
+
let body = renderDailyArticle(docs, date);
|
|
147
|
+
if (bodyOverride) {
|
|
148
|
+
// The canonical episode title is computed from the lead item; a leading H1 the agent may
|
|
149
|
+
// still have written is replaced (not duplicated) so body and metadata agree on every path.
|
|
150
|
+
const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), '');
|
|
151
|
+
body = `# ${title}\n\n${stripped}\n`;
|
|
152
|
+
}
|
|
153
|
+
// Task 0189 R1: the AI-production note rides on every article path (mechanical and agent
|
|
154
|
+
// prose). Podcast show notes are rendered separately by podcast-pub and do not inherit it.
|
|
155
|
+
body = `${body.trimEnd()}\n\n---\n\n*文字由 AI 辅助整理,音频为 AI 合成配音。*\n`;
|
|
125
156
|
return ContentSchema.parse({
|
|
126
|
-
title
|
|
127
|
-
body
|
|
157
|
+
title,
|
|
158
|
+
body,
|
|
128
159
|
format: 'markdown',
|
|
129
160
|
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
130
161
|
metadata: { generator: 'kk:daily-article', date, doc_count: docs.length, tags: tagsFor(docs) },
|
|
@@ -22064,19 +22064,22 @@ function stripLeadingTitleEcho(body, title) {
|
|
|
22064
22064
|
const wanted = title.replace(TITLE_ECHO_NOISE_RE, "");
|
|
22065
22065
|
if (wanted.length < 4)
|
|
22066
22066
|
return body;
|
|
22067
|
-
let
|
|
22068
|
-
|
|
22069
|
-
|
|
22070
|
-
|
|
22071
|
-
|
|
22072
|
-
|
|
22073
|
-
|
|
22074
|
-
|
|
22075
|
-
|
|
22067
|
+
let text = body;
|
|
22068
|
+
for (;; ) {
|
|
22069
|
+
let matched = "";
|
|
22070
|
+
let end = -1;
|
|
22071
|
+
for (let i = 0;i < text.length && matched.length < wanted.length; i++) {
|
|
22072
|
+
const kept = (text[i] ?? "").replace(TITLE_ECHO_NOISE_RE, "");
|
|
22073
|
+
if (kept === "")
|
|
22074
|
+
continue;
|
|
22075
|
+
matched += kept;
|
|
22076
|
+
if (matched.length >= wanted.length)
|
|
22077
|
+
end = i + 1;
|
|
22078
|
+
}
|
|
22079
|
+
if (end === -1 || matched !== wanted)
|
|
22080
|
+
return text;
|
|
22081
|
+
text = text.slice(end).replace(/^[\uFF0C\u3002\u3001\uFF1A:\uFF1B;!\uFF01?\uFF1F\u2026\s\u300C\u300D\u300E\u300F\u3010\u3011]+/, "");
|
|
22076
22082
|
}
|
|
22077
|
-
if (end === -1 || matched !== wanted)
|
|
22078
|
-
return body;
|
|
22079
|
-
return body.slice(end).replace(/^[\uFF0C\u3002\u3001\uFF1A:\uFF1B;!\uFF01?\uFF1F\u2026\s\u300C\u300D\u300E\u300F\u3010\u3011]+/, "");
|
|
22080
22083
|
}
|
|
22081
22084
|
var TAG_METADATA_LINE_RE = /^\s*\**\s*\u6807\u7B7E\s*\**\s*[\uFF1A:\uFF0C,]/;
|
|
22082
22085
|
function stripTagMetadataLines(body) {
|
|
@@ -22113,10 +22116,10 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22113
22116
|
const broadcastDate = options?.date ?? new Date;
|
|
22114
22117
|
const dateStr = formatBroadcastDate(broadcastDate, language);
|
|
22115
22118
|
if (docs.length === 0) {
|
|
22116
|
-
const title2 = options?.title || (isZh ? "\
|
|
22119
|
+
const title2 = options?.title || (isZh ? "AI \u4E71\u7096\uFF08\u6682\u65E0\u66F4\u65B0\uFF09" : "AI \u4E71\u7096 (No Updates)");
|
|
22117
22120
|
const segments2 = isZh ? [
|
|
22118
22121
|
{
|
|
22119
|
-
text: `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\u3002\u6B22\u8FCE\u6536\u542C\
|
|
22122
|
+
text: `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\u3002\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${voiceProfile}\u3002\u4ECA\u5929\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002`,
|
|
22120
22123
|
gap_ms: 400,
|
|
22121
22124
|
emotion: "calm"
|
|
22122
22125
|
},
|
|
@@ -22127,7 +22130,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22127
22130
|
}
|
|
22128
22131
|
] : [
|
|
22129
22132
|
{
|
|
22130
|
-
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with
|
|
22133
|
+
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with AI \u4E71\u7096, our chat about AI tools, papers, and hands-on practice. There is nothing solid enough to share today.`,
|
|
22131
22134
|
gap_ms: 400,
|
|
22132
22135
|
emotion: "calm"
|
|
22133
22136
|
},
|
|
@@ -22148,7 +22151,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22148
22151
|
segments: segments2
|
|
22149
22152
|
});
|
|
22150
22153
|
}
|
|
22151
|
-
const title = options?.title || (isZh ?
|
|
22154
|
+
const title = options?.title || (isZh ? `AI \u4E71\u7096\uFF08\u5171${docs.length}\u6761\uFF09` : `AI \u4E71\u7096 (${docs.length} stories)`);
|
|
22152
22155
|
const segments = [];
|
|
22153
22156
|
let hostName = options?.speakers?.host?.name || voiceProfile.replace(/[-_]?(news|voice|bot|profile)$/i, "") || voiceProfile;
|
|
22154
22157
|
if (hostName.length > 0) {
|
|
@@ -22158,7 +22161,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22158
22161
|
const planItems = new Map((scriptPlan?.items ?? []).map((item) => [item.id, item]));
|
|
22159
22162
|
const countText = isZh ? `${formatChineseCount(docs.length)}\u6761` : `${docs.length}`;
|
|
22160
22163
|
const titlesSummary = docs.slice(0, 3).map((d) => (d.title ?? "").trim()).filter(Boolean).join("\u3001");
|
|
22161
|
-
const introText = isZh ? scriptPlan ? `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\
|
|
22164
|
+
const introText = isZh ? scriptPlan ? `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${hostName}\u3002${scriptPlan.intro}` : `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${hostName}\u3002\u4ECA\u5929\u6211\u4ECE AI \u5DE5\u5177\u3001\u8BBA\u6587\u548C\u5B9E\u8DF5\u4E2D\u6574\u7406\u4E86${countText}\u503C\u5F97\u804A\u7684\u5185\u5BB9${titlesSummary ? `\uFF0C\u5305\u62EC${titlesSummary}` : ""}\uFF0C\u54B1\u4EEC\u4E00\u6761\u6761\u6765\u804A\u3002` : `Hello and welcome. Today is ${dateStr}. This is ${hostName} with AI \u4E71\u7096, where we talk about AI tools, papers, and hands-on practice. In today's episode, we have ${countText} stories. Let's dive straight in.`;
|
|
22162
22165
|
segments.push({
|
|
22163
22166
|
text: normalizeBroadcastText(introText, isZh),
|
|
22164
22167
|
gap_ms: 700,
|
|
@@ -22193,7 +22196,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22193
22196
|
});
|
|
22194
22197
|
}
|
|
22195
22198
|
});
|
|
22196
|
-
const outroText = isZh ? scriptPlan ? `${scriptPlan.outro}\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `\u597D\u4E86\uFF0C\
|
|
22199
|
+
const outroText = isZh ? scriptPlan ? `${scriptPlan.outro}\u672C\u671F\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `\u597D\u4E86\uFF0C\u672C\u671FAI\u4E71\u7096\u5C31\u804A\u5230\u8FD9\u91CC\u3002\u5E0C\u671B\u4ECA\u5929\u8FD9\u4E9B\u5DE5\u5177\u3001\u8BBA\u6587\u548C\u5B9E\u8DF5\u7684\u7ECF\u9A8C\u5BF9\u60A8\u6709\u542F\u53D1\u3002\u672C\u671F\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `That wraps up this episode of AI \u4E71\u7096. I hope today's takeaways were useful. The audio in this episode is AI-synthesized. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
22197
22200
|
segments.push({
|
|
22198
22201
|
text: normalizeBroadcastText(outroText, isZh),
|
|
22199
22202
|
gap_ms: 300,
|
|
@@ -22453,6 +22456,24 @@ async function resolveScriptPlan(fs, docs) {
|
|
|
22453
22456
|
}
|
|
22454
22457
|
return { scriptPlan: validated.plan, scriptSource: "plan", scriptPlanErrors: [] };
|
|
22455
22458
|
}
|
|
22459
|
+
function broadcastDateFromEnv(raw) {
|
|
22460
|
+
if (!raw)
|
|
22461
|
+
return;
|
|
22462
|
+
const match = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})$/);
|
|
22463
|
+
if (match === null) {
|
|
22464
|
+
echoError(`dailynews-gen: ignoring malformed DAILYNEWS_DATE "${raw}" (expected YYYYMMDD or YYYY-MM-DD)`);
|
|
22465
|
+
return;
|
|
22466
|
+
}
|
|
22467
|
+
const year = Number(match[1]);
|
|
22468
|
+
const month = Number(match[2]) - 1;
|
|
22469
|
+
const day = Number(match[3]);
|
|
22470
|
+
const date6 = new Date(year, month, day);
|
|
22471
|
+
if (date6.getFullYear() !== year || date6.getMonth() !== month || date6.getDate() !== day) {
|
|
22472
|
+
echoError(`dailynews-gen: ignoring out-of-range DAILYNEWS_DATE "${raw}"`);
|
|
22473
|
+
return;
|
|
22474
|
+
}
|
|
22475
|
+
return date6;
|
|
22476
|
+
}
|
|
22456
22477
|
async function processGeneratorIO(inputPath, outputPath, options) {
|
|
22457
22478
|
const fs = createNodeFileSystem();
|
|
22458
22479
|
let docs;
|
|
@@ -22472,7 +22493,8 @@ async function processGeneratorIO(inputPath, outputPath, options) {
|
|
|
22472
22493
|
}
|
|
22473
22494
|
const isZh = (options?.language || "zh").toLowerCase() === "zh";
|
|
22474
22495
|
const planOutcome = isZh ? await resolveScriptPlan(fs, docs) : { scriptSource: "template", scriptPlanErrors: [] };
|
|
22475
|
-
const
|
|
22496
|
+
const date6 = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
|
|
22497
|
+
const script = buildNewsVoiceScript(docs, { ...options, date: date6, scriptPlan: planOutcome.scriptPlan });
|
|
22476
22498
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
22477
22499
|
const content = {
|
|
22478
22500
|
title: script.title,
|
|
@@ -22553,6 +22575,7 @@ export {
|
|
|
22553
22575
|
formatChineseCount,
|
|
22554
22576
|
formatBroadcastDate,
|
|
22555
22577
|
buildNewsVoiceScript,
|
|
22578
|
+
broadcastDateFromEnv,
|
|
22556
22579
|
VoiceSpeakerSchema,
|
|
22557
22580
|
VoiceScriptSegmentSchema,
|
|
22558
22581
|
VoiceScriptSchema,
|
|
@@ -66,6 +66,32 @@ async function resolveScriptPlan(fs: FileSystem, docs: Doc[]): Promise<ScriptPla
|
|
|
66
66
|
return { scriptPlan: validated.plan, scriptSource: 'plan', scriptPlanErrors: [] };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* `DAILYNEWS_DATE` (YYYYMMDD or `YYYY-MM-DD`) → a local-midnight Date. Task 0189: the broadcast
|
|
71
|
+
* greeting must carry the episode's run date, not the render date — a 9/29 episode rebuilt on 9/30
|
|
72
|
+
* must not greet "今天是9月30日". Parsed as local calendar parts, never UTC (`new Date('2026-09-29')`
|
|
73
|
+
* is UTC midnight and renders the previous day west of UTC). Absent → undefined; garbled or
|
|
74
|
+
* out-of-range (the Date constructor rolls over, e.g. month 13) → undefined with a warn so a
|
|
75
|
+
* caller bug surfaces instead of silently broadcasting the wrong day.
|
|
76
|
+
*/
|
|
77
|
+
export function broadcastDateFromEnv(raw: string | undefined): Date | undefined {
|
|
78
|
+
if (!raw) return undefined;
|
|
79
|
+
const match = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})$/);
|
|
80
|
+
if (match === null) {
|
|
81
|
+
echoError(`dailynews-gen: ignoring malformed DAILYNEWS_DATE "${raw}" (expected YYYYMMDD or YYYY-MM-DD)`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const year = Number(match[1]);
|
|
85
|
+
const month = Number(match[2]) - 1;
|
|
86
|
+
const day = Number(match[3]);
|
|
87
|
+
const date = new Date(year, month, day);
|
|
88
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
89
|
+
echoError(`dailynews-gen: ignoring out-of-range DAILYNEWS_DATE "${raw}"`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return date;
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
export async function processGeneratorIO(
|
|
70
96
|
inputPath: string,
|
|
71
97
|
outputPath: string,
|
|
@@ -101,7 +127,9 @@ export async function processGeneratorIO(
|
|
|
101
127
|
const planOutcome = isZh
|
|
102
128
|
? await resolveScriptPlan(fs, docs)
|
|
103
129
|
: { scriptSource: 'template' as const, scriptPlanErrors: [] as ScriptPlanError[] };
|
|
104
|
-
|
|
130
|
+
// Explicit option wins; else DAILYNEWS_DATE (workflow backfill/retry); else the builder's now.
|
|
131
|
+
const date = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
|
|
132
|
+
const script = buildNewsVoiceScript(docs, { ...options, date, scriptPlan: planOutcome.scriptPlan });
|
|
105
133
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
106
134
|
|
|
107
135
|
const content: Content = {
|
|
@@ -232,20 +232,29 @@ const TITLE_ECHO_NOISE_RE = /[,。、::;;!!??…\s「」『』【】]/
|
|
|
232
232
|
* character prefix compare, never a fuzzy/LLM similarity match (anti-pattern list). The body is
|
|
233
233
|
* returned untouched whenever the normalized prefix does not equal the normalized title, and a
|
|
234
234
|
* body that is *only* the restatement collapses to the empty string (the lead alone carries it).
|
|
235
|
+
*
|
|
236
|
+
* 20260929 dogfood: plan bodies can restate the title MORE THAN once — a bare first line plus a
|
|
237
|
+
* restated line carrying a parenthetical extension (`title(来源标注日期为…)`). A single pass
|
|
238
|
+
* strips only the first, so the second becomes the new head and the VoiceScript validator rejects
|
|
239
|
+
* the segment. Keep stripping until the head no longer echoes; every accepted pass strictly
|
|
240
|
+
* shortens the body, so the loop terminates.
|
|
235
241
|
*/
|
|
236
242
|
export function stripLeadingTitleEcho(body: string, title: string): string {
|
|
237
243
|
const wanted = title.replace(TITLE_ECHO_NOISE_RE, '');
|
|
238
244
|
if (wanted.length < 4) return body; // a short title would match too much of the body
|
|
239
|
-
let
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
let text = body;
|
|
246
|
+
for (;;) {
|
|
247
|
+
let matched = '';
|
|
248
|
+
let end = -1;
|
|
249
|
+
for (let i = 0; i < text.length && matched.length < wanted.length; i++) {
|
|
250
|
+
const kept = (text[i] ?? '').replace(TITLE_ECHO_NOISE_RE, '');
|
|
251
|
+
if (kept === '') continue; // drift punctuation between title and echo
|
|
252
|
+
matched += kept;
|
|
253
|
+
if (matched.length >= wanted.length) end = i + 1;
|
|
254
|
+
}
|
|
255
|
+
if (end === -1 || matched !== wanted) return text;
|
|
256
|
+
text = text.slice(end).replace(/^[,。、::;;!!??…\s「」『』【】]+/, '');
|
|
246
257
|
}
|
|
247
|
-
if (end === -1 || matched !== wanted) return body;
|
|
248
|
-
return body.slice(end).replace(/^[,。、::;;!!??…\s「」『』【】]+/, '');
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
/**
|
|
@@ -304,11 +313,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
304
313
|
const dateStr = formatBroadcastDate(broadcastDate, language);
|
|
305
314
|
|
|
306
315
|
if (docs.length === 0) {
|
|
307
|
-
const title = options?.title || (isZh ? '
|
|
316
|
+
const title = options?.title || (isZh ? 'AI 乱炖(暂无更新)' : 'AI 乱炖 (No Updates)');
|
|
308
317
|
const segments: VoiceScriptSegment[] = isZh
|
|
309
318
|
? [
|
|
310
319
|
{
|
|
311
|
-
|
|
320
|
+
// Task 0189 R1/R4: honest no-content episode — the copy names the show
|
|
321
|
+
// (咸话咸说) and the column (AI 乱炖), and never claims a news digest happened.
|
|
322
|
+
text: `大家好,今天是${dateStr}。欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${voiceProfile}。今天没有足够扎实的可聊内容。`,
|
|
312
323
|
gap_ms: 400,
|
|
313
324
|
emotion: 'calm',
|
|
314
325
|
},
|
|
@@ -320,7 +331,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
320
331
|
]
|
|
321
332
|
: [
|
|
322
333
|
{
|
|
323
|
-
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with
|
|
334
|
+
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with AI 乱炖, our chat about AI tools, papers, and hands-on practice. There is nothing solid enough to share today.`,
|
|
324
335
|
gap_ms: 400,
|
|
325
336
|
emotion: 'calm',
|
|
326
337
|
},
|
|
@@ -343,9 +354,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
343
354
|
});
|
|
344
355
|
}
|
|
345
356
|
|
|
346
|
-
const title =
|
|
347
|
-
options?.title ||
|
|
348
|
-
(isZh ? `今日AI科技漫谈(共${docs.length}条)` : `AI Daily Briefing (${docs.length} stories)`);
|
|
357
|
+
const title = options?.title || (isZh ? `AI 乱炖(共${docs.length}条)` : `AI 乱炖 (${docs.length} stories)`);
|
|
349
358
|
const segments: VoiceScriptSegment[] = [];
|
|
350
359
|
|
|
351
360
|
let hostName =
|
|
@@ -373,9 +382,10 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
373
382
|
? scriptPlan
|
|
374
383
|
? // F613: fixed greeting stays builder-owned; the plan only fills the slot between
|
|
375
384
|
// greeting and first story (the template tail 从「今天科技圈有几条…正题。」 is replaced).
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
// Task 0189 R1: the greeting names the show (咸话咸说) and the column (AI 乱炖).
|
|
386
|
+
`大家好,今天是${dateStr},欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${hostName}。${scriptPlan.intro}`
|
|
387
|
+
: `大家好,今天是${dateStr},欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${hostName}。今天我从 AI 工具、论文和实践中整理了${countText}值得聊的内容${titlesSummary ? `,包括${titlesSummary}` : ''},咱们一条条来聊。`
|
|
388
|
+
: `Hello and welcome. Today is ${dateStr}. This is ${hostName} with AI 乱炖, where we talk about AI tools, papers, and hands-on practice. In today's episode, we have ${countText} stories. Let's dive straight in.`;
|
|
379
389
|
|
|
380
390
|
segments.push({
|
|
381
391
|
text: normalizeBroadcastText(introText, isZh),
|
|
@@ -440,11 +450,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
440
450
|
|
|
441
451
|
// 3. Outro segment: Warm, conversational wrap-up. F613: the plan outro replaces the template
|
|
442
452
|
// wrap-up head; the fixed sign-off stays builder-owned.
|
|
453
|
+
// Task 0189 R1: the AI-voice disclosure is part of the builder-owned sign-off (not the plan),
|
|
454
|
+
// so it is spoken in every episode — Apple Podcasts guideline 1.11 requires it in the content.
|
|
443
455
|
const outroText = isZh
|
|
444
456
|
? scriptPlan
|
|
445
|
-
? `${scriptPlan.outro}
|
|
446
|
-
:
|
|
447
|
-
: `That wraps up
|
|
457
|
+
? `${scriptPlan.outro}本期音频为 AI 合成配音。感谢您的收听,我是${hostName},祝您拥有充实高效的一天,我们下期节目不见不散!`
|
|
458
|
+
: `好了,本期AI乱炖就聊到这里。希望今天这些工具、论文和实践的经验对您有启发。本期音频为 AI 合成配音。感谢您的收听,我是${hostName},祝您拥有充实高效的一天,我们下期节目不见不散!`
|
|
459
|
+
: `That wraps up this episode of AI 乱炖. I hope today's takeaways were useful. The audio in this episode is AI-synthesized. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
448
460
|
|
|
449
461
|
segments.push({
|
|
450
462
|
text: normalizeBroadcastText(outroText, isZh),
|
|
@@ -22101,7 +22101,14 @@ function docTimestamp(doc2) {
|
|
|
22101
22101
|
}
|
|
22102
22102
|
return null;
|
|
22103
22103
|
}
|
|
22104
|
+
var CHINA_POLITICS_RE = /\u4E60\u8FD1\u5E73|\u674E\u514B\u5F3A|\u674E\u5F3A|\u515A\u4E2D\u592E|\u653F\u6CBB\u5C40|\u4E2D\u5357\u6D77|\u653F\u5E9C\u5DE5\u4F5C\u62A5\u544A|\u56FD\u53F0\u529E|\u5916\u4EA4\u90E8\u53D1\u8A00\u4EBA|\u4E24\u4F1A/;
|
|
22105
|
+
function isChinaPolitics(doc2) {
|
|
22106
|
+
return CHINA_POLITICS_RE.test(`${doc2.title ?? ""}
|
|
22107
|
+
${(doc2.body ?? "").slice(0, 2000)}`);
|
|
22108
|
+
}
|
|
22104
22109
|
function passesGates(doc2, planDate) {
|
|
22110
|
+
if (isChinaPolitics(doc2))
|
|
22111
|
+
return false;
|
|
22105
22112
|
const meta3 = asRecord(doc2.metadata);
|
|
22106
22113
|
const tags = Array.isArray(meta3.tags) ? meta3.tags.filter((t) => typeof t === "string") : [];
|
|
22107
22114
|
if (tags.some((tag) => NON_NEWS_TAGS.has(tag.toLowerCase())))
|
|
@@ -227,7 +227,19 @@ function docTimestamp(doc: Doc): number | null {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/** Freshness + non-news gates applied before scoring/dedup (archived or off-topic items never enter the pool). */
|
|
230
|
+
/**
|
|
231
|
+
* Platform-safety gate (operator rule 2026-09-29): China domestic politics stories never enter
|
|
232
|
+
* the plan. Keyword heuristic — high-precision markers + top-leader names only; widen after
|
|
233
|
+
* reviewing false negatives in the audit trail rather than pre-emptively (overblocking kills
|
|
234
|
+
* legitimate AI-industry coverage).
|
|
235
|
+
*/
|
|
236
|
+
const CHINA_POLITICS_RE = /习近平|李克强|李强|党中央|政治局|中南海|政府工作报告|国台办|外交部发言人|两会/;
|
|
237
|
+
function isChinaPolitics(doc: Doc): boolean {
|
|
238
|
+
return CHINA_POLITICS_RE.test(`${doc.title ?? ''}\n${(doc.body ?? '').slice(0, 2000)}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
230
241
|
function passesGates(doc: Doc, planDate: string): boolean {
|
|
242
|
+
if (isChinaPolitics(doc)) return false;
|
|
231
243
|
const meta = asRecord(doc.metadata);
|
|
232
244
|
const tags = Array.isArray(meta.tags) ? meta.tags.filter((t): t is string => typeof t === 'string') : [];
|
|
233
245
|
if (tags.some((tag) => NON_NEWS_TAGS.has(tag.toLowerCase()))) return false;
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
This Font Software is licensed under the SIL Open Font License,
|
|
2
|
+
Version 1.1.
|
|
3
|
+
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font
|
|
14
|
+
creation efforts of academic and linguistic communities, and to
|
|
15
|
+
provide a free and open framework in which fonts may be shared and
|
|
16
|
+
improved in partnership with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply to
|
|
25
|
+
any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software
|
|
36
|
+
components as distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to,
|
|
39
|
+
deleting, or substituting -- in part or in whole -- any of the
|
|
40
|
+
components of the Original Version, by changing formats or by porting
|
|
41
|
+
the Font Software to a new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION & CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
49
|
+
modify, redistribute, and sell modified and unmodified copies of the
|
|
50
|
+
Font Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components, in
|
|
53
|
+
Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the
|
|
64
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
65
|
+
primary font name as presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created using
|
|
77
|
+
the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Noto Sans SC — provenance
|
|
2
|
+
|
|
3
|
+
Shipped static SubsetOTF/SC fonts used by the `compose` text-card composer (task 0180, ADR-028).
|
|
4
|
+
Output is reproducible on every install because these bytes ship with the plugin — no runtime
|
|
5
|
+
download and no system-font lookup.
|
|
6
|
+
|
|
7
|
+
- **Upstream:** https://github.com/notofonts/noto-cjk (Noto Sans CJK SC, static SubsetOTF)
|
|
8
|
+
- **Version:** tag `Sans2.004` (https://github.com/notofonts/noto-cjk/releases/tag/Sans2.004)
|
|
9
|
+
- **Licence:** SIL Open Font License 1.1 (`OFL.txt`, from the repo root `LICENSE` at the same tag)
|
|
10
|
+
|
|
11
|
+
| File | Upstream path at `Sans2.004` | SHA-256 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `NotoSansSC-Regular.otf` | `Sans/SubsetOTF/SC/NotoSansSC-Regular.otf` | `faa6c9df652116dde789d351359f3d7e5d2285a2b2a1f04a2d7244df706d5ea9` |
|
|
14
|
+
| `NotoSansSC-Bold.otf` | `Sans/SubsetOTF/SC/NotoSansSC-Bold.otf` | `c6cb5a93abaa9edc8ee7463b7ebb7f42d618d40e6ed2f7a5371c97b0b64767c0` |
|
|
15
|
+
| `OFL.txt` | `LICENSE` | `6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2` |
|