@gobing-ai/knowledge-kit 0.1.9 → 0.1.11

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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +132 -25
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +4 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  7. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  8. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  9. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  10. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  11. package/plugins/generations/image-gen/dist/index.js +4 -0
  12. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  13. package/plugins/generations/voice-gen/dist/index.js +4 -0
  14. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  15. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  16. package/plugins/ingestions/horizon-ingest/dist/index.js +4 -0
  17. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  18. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  19. package/plugins/ingestions/web-search/dist/index.js +4 -0
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  22. package/plugins/kk/scripts/daily-stages.ts +1083 -49
  23. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  24. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  25. package/plugins/kk/scripts/sentences.ts +134 -0
  26. package/plugins/kk/scripts/series-names.ts +124 -0
  27. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +194 -113
  28. package/plugins/kk/workflows/kk-daily-safety.yaml +160 -36
  29. package/plugins/publishings/bilibili-pub/dist/index.js +6 -1
  30. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/cnblogs-pub/dist/index.js +6 -1
  32. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  33. package/plugins/publishings/csdn-pub/dist/index.js +6 -1
  34. package/plugins/publishings/csdn-pub/dist/profile-check.js +6 -1
  35. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  36. package/plugins/publishings/infoq-pub/dist/index.js +6 -1
  37. package/plugins/publishings/infoq-pub/dist/profile-check.js +6 -1
  38. package/plugins/publishings/jike-pub/dist/index.js +6 -1
  39. package/plugins/publishings/jike-pub/dist/profile-check.js +6 -1
  40. package/plugins/publishings/juejin-pub/dist/index.js +6 -1
  41. package/plugins/publishings/juejin-pub/dist/profile-check.js +6 -1
  42. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  43. package/plugins/publishings/podcast-pub/dist/index.js +151 -16
  44. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  45. package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
  46. package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
  47. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  48. package/plugins/publishings/shipinhao-pub/dist/index.js +6 -1
  49. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  50. package/plugins/publishings/sspai-pub/dist/index.js +6 -1
  51. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  52. package/plugins/publishings/substack-pub/dist/index.js +6 -1
  53. package/plugins/publishings/substack-pub/dist/profile-check.js +6 -1
  54. package/plugins/publishings/surfdash-pub/dist/index.js +691 -63
  55. package/plugins/publishings/surfdash-pub/src/digest-check.ts +284 -0
  56. package/plugins/publishings/surfdash-pub/src/index.ts +656 -84
  57. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  58. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +937 -98
  60. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +64 -3
  61. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  62. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  63. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  64. package/plugins/publishings/weibo-pub/dist/index.js +6 -1
  65. package/plugins/publishings/weibo-pub/dist/profile-check.js +6 -1
  66. package/plugins/publishings/x-pub/dist/index.js +6 -1
  67. package/plugins/publishings/x-pub/dist/profile-check.js +6 -1
  68. package/plugins/publishings/xhs-pub/dist/index.js +6 -1
  69. package/plugins/publishings/xhs-pub/dist/profile-check.js +6 -1
  70. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +6 -1
  71. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
  72. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Fast offline pre-check for post-refine digest contract (2026-10-24).
3
+ *
4
+ * NOTE: This module is a fast, offline pre-check run before the slower site gate to catch
5
+ * frontmatter shape errors early in-process. The authoritative source of truth is the surfdash
6
+ * site gate (`bun run sd check digest` and `bun run check:content`), which runs in the surfdash
7
+ * checkout before commit+push.
8
+ *
9
+ * The surfdash refine step (scripts/lib/seo-digest.ts) owns the daily-digest title, description
10
+ * and key_points (ADR-008). `requireRefined` only proves refine *ran*; this module re-verifies the
11
+ * values against the site's published contract (exact `<series> · <date>:h1、h2` title shape,
12
+ * `title | Surfing` ≤ 60 code points, description length window + terminal punctuation, no
13
+ * ellipsis, `label — sentence` key_points) so a regressed derivation fails the publish loud
14
+ * instead of shipping broken SEO metadata.
15
+ *
16
+ * Line-based frontmatter parsing with Bun's stdlib YAML renderer for block scalars (no package
17
+ * dependency; same field compatibility as the site's own check-digest script). Handles `"..."`,
18
+ * `'...'`, plain and folded/literal block scalars, flow arrays and ` - "..."` block lists.
19
+ */
20
+ import { SERIES_NAMES } from './patch';
21
+
22
+ /** Locales the digest series publishes in (zh primary; en/ja translated siblings). */
23
+ export type DigestLocale = keyof typeof SERIES_NAMES;
24
+
25
+ /** The tag that marks a post as one of the daily digest series (site DIGEST_TAG). */
26
+ export const DIGEST_TAG = 'sub-category:ai-daily';
27
+
28
+ /** Podcast-intro openings a digest description must not start with (case-insensitive). */
29
+ const PODCAST_INTROS = ['这里是由Robin', '咸话咸说', 'This is Robin', 'Robin がお届け'] as const;
30
+
31
+ /** Rendered `<title>` budget: title + " | Surfing" must fit (site SEO_TITLE_LIMIT). */
32
+ const SEO_TITLE_LIMIT = 60;
33
+
34
+ /** One parsed frontmatter entry: a scalar string or a block/flow list of strings. */
35
+ type FrontmatterValue = string | string[];
36
+
37
+ /** Opening `key:` line: bare key, no leading whitespace (frontmatter is never indented). */
38
+ const KEY_LINE = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/;
39
+ const BLOCK_SCALAR = /^[>|][+-]?$/;
40
+
41
+ const escapeRe = (token: string): string => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
+
43
+ /** Number of leading indentation characters; YAML block indentation is spaces in refined posts. */
44
+ function leadingIndent(line: string): number {
45
+ return line.match(/^[ \t]*/)?.[0].length ?? 0;
46
+ }
47
+
48
+ /**
49
+ * Reads one block scalar without allowing a same-indentation sibling list item to become text.
50
+ * Bun's YAML renderer supplies folded/literal and blank-line semantics once the indentation
51
+ * boundary is known. The fallback keeps malformed frontmatter non-fatal, matching the old
52
+ * line parser's permissive behavior.
53
+ */
54
+ function readBlockScalar(
55
+ lines: string[],
56
+ start: number,
57
+ end: number,
58
+ parentIndent: number,
59
+ indicator: string,
60
+ ): { value: string; nextIndex: number } {
61
+ const content: string[] = [];
62
+ let j = start;
63
+ while (j < end) {
64
+ const line = lines[j] ?? '';
65
+ if (line.trim() === '') {
66
+ content.push('');
67
+ j++;
68
+ continue;
69
+ }
70
+ if (leadingIndent(line) <= parentIndent) break;
71
+ content.push(line);
72
+ j++;
73
+ }
74
+ if (content.length === 0) return { value: '', nextIndex: j };
75
+
76
+ const normalized = content.map((line) =>
77
+ line === '' ? '' : line.slice(Math.min(parentIndent, leadingIndent(line))),
78
+ );
79
+ try {
80
+ const parsed = Bun.YAML.parse([`value: ${indicator}`, ...normalized, 'sentinel: null'].join('\n')) as {
81
+ value?: unknown;
82
+ };
83
+ if (typeof parsed.value === 'string') return { value: parsed.value, nextIndex: j };
84
+ } catch {
85
+ // Keep the checker useful for malformed drafts; the contract gate reports the field issue.
86
+ }
87
+ const folded = indicator.startsWith('>');
88
+ const parts: string[] = [];
89
+ for (const line of normalized) {
90
+ if (line !== '') parts.push(line.trim());
91
+ }
92
+ return {
93
+ value: parts.join(folded ? ' ' : '\n'),
94
+ nextIndex: j,
95
+ };
96
+ }
97
+
98
+ /** Unquotes a YAML scalar: `"…"` via JSON (mapContent writes JSON.stringify), `'…'` with '' unescape. */
99
+ function unquote(value: string): string {
100
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
101
+ try {
102
+ return JSON.parse(value) as string;
103
+ } catch {
104
+ return value.slice(1, -1);
105
+ }
106
+ }
107
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
108
+ return value.slice(1, -1).replaceAll("''", "'");
109
+ }
110
+ return value;
111
+ }
112
+
113
+ /**
114
+ * Line-based frontmatter parse: scalars (plain/quoted/folded/literal) and string lists (block `- item`
115
+ * or flow `[a, b]`). Unknown shapes degrade to the raw text; a missing frontmatter yields {}.
116
+ */
117
+ export function parseFrontmatterScalars(markdown: string): Map<string, FrontmatterValue> {
118
+ const out = new Map<string, FrontmatterValue>();
119
+ const lines = markdown.split(/\r?\n/);
120
+ if (lines[0]?.trim() !== '---') return out;
121
+ const end = lines.findIndex((line, i) => i > 0 && line.trim() === '---');
122
+ if (end < 0) return out;
123
+ for (let i = 1; i < end; i++) {
124
+ const line = lines[i];
125
+ if (!line) continue;
126
+ const match = line.match(KEY_LINE);
127
+ if (!match?.[1] || match[2] === undefined) continue;
128
+ const key = match[1];
129
+ const value = match[2].trim();
130
+ if (BLOCK_SCALAR.test(value)) {
131
+ const block = readBlockScalar(lines, i + 1, end, leadingIndent(line), value);
132
+ out.set(key, block.value);
133
+ i = block.nextIndex - 1;
134
+ continue;
135
+ }
136
+ if (value === '') {
137
+ // Block list: `key:` followed by ` - item` lines; an empty key stays ''.
138
+ const items: string[] = [];
139
+ let listIndent: number | undefined;
140
+ let j = i + 1;
141
+ while (j < end) {
142
+ const itemLine = lines[j];
143
+ if (!itemLine || !/^\s*-\s+/.test(itemLine)) break;
144
+ const itemIndent = leadingIndent(itemLine);
145
+ if (listIndent === undefined) listIndent = itemIndent;
146
+ if (itemIndent !== listIndent) break;
147
+ const raw = itemLine.replace(/^\s*-\s+/, '').trim();
148
+ // Folded item (`- >-`): the refine writer folds long CJK bullets, and a bare
149
+ // ">-" item would fail the " — " separator check downstream — read the
150
+ // indented continuation lines into the item with YAML's block-scalar semantics.
151
+ if (BLOCK_SCALAR.test(raw)) {
152
+ const block = readBlockScalar(lines, j + 1, end, itemIndent, raw);
153
+ items.push(block.value);
154
+ j = block.nextIndex;
155
+ continue;
156
+ }
157
+ items.push(unquote(raw));
158
+ j++;
159
+ }
160
+ if (items.length > 0) {
161
+ out.set(key, items);
162
+ i = j - 1;
163
+ } else {
164
+ out.set(key, '');
165
+ }
166
+ continue;
167
+ }
168
+ if (value.startsWith('[') && value.endsWith(']')) {
169
+ out.set(
170
+ key,
171
+ value
172
+ .slice(1, -1)
173
+ .split(',')
174
+ .map((item) => unquote(item.trim()))
175
+ .filter((item) => item !== ''),
176
+ );
177
+ continue;
178
+ }
179
+ out.set(key, unquote(value));
180
+ }
181
+ return out;
182
+ }
183
+
184
+ /** The scalar at `key`: a list joins with ' ' (never a shape the digest contract accepts). */
185
+ function scalar(frontmatter: Map<string, FrontmatterValue>, key: string): string | undefined {
186
+ const value = frontmatter.get(key);
187
+ if (value === undefined) return undefined;
188
+ return Array.isArray(value) ? value.join(' ') : value;
189
+ }
190
+
191
+ /** Terminal punctuation per locale: terminator, optionally followed by closing quotes/brackets. */
192
+ function endsAsSentence(text: string, locale: DigestLocale): boolean {
193
+ return locale === 'en' ? /[.!?]["')”’]*$/u.test(text.trimEnd()) : /[。!?][」』”)]*$/u.test(text.trimEnd());
194
+ }
195
+
196
+ /**
197
+ * Validates a refined post's frontmatter against the digest contract. Returns the problems
198
+ * (empty = ok); non-digest posts (no `sub-category:ai-daily` tag) return [] unchecked.
199
+ */
200
+ export function checkDigestFrontmatter(markdown: string, locale: DigestLocale): string[] {
201
+ const frontmatter = parseFrontmatterScalars(markdown);
202
+ const tags = frontmatter.get('tags');
203
+ const tagList = Array.isArray(tags) ? tags : typeof tags === 'string' && tags !== '' ? [tags] : [];
204
+ if (!tagList.some((tag) => tag.includes(DIGEST_TAG))) return [];
205
+
206
+ const problems: string[] = [];
207
+ const series = SERIES_NAMES[locale];
208
+ const titleSep = locale === 'en' ? ': ' : ':';
209
+ const listSep = locale === 'en' ? ', ' : '、';
210
+
211
+ const title = scalar(frontmatter, 'title');
212
+ if (title === undefined || title === '') {
213
+ problems.push('title: missing');
214
+ } else {
215
+ const match = title.match(
216
+ new RegExp(`^${escapeRe(series)} · (\\d{4}-\\d{2}-\\d{2})${escapeRe(titleSep)}(.+)$`),
217
+ );
218
+ if (!match) {
219
+ problems.push(
220
+ `title: expected "<series> · YYYY-MM-DD${titleSep}<h1>${listSep}<h2>" for ${series}, got "${title}"`,
221
+ );
222
+ } else {
223
+ const rest = match[2] ?? '';
224
+ const heads = locale === 'en' ? rest.split(', ') : rest.split('、');
225
+ const seps = locale === 'en' ? (rest.match(/, /g) ?? []).length : (rest.match(/、/g) ?? []).length;
226
+ if (seps !== 1 || heads.some((head) => head.trim() === '')) {
227
+ problems.push(`title: expected exactly two non-empty headlines joined "${listSep}", got "${rest}"`);
228
+ }
229
+ }
230
+ const seoTitle = `${title} | Surfing`;
231
+ const seoLength = Array.from(seoTitle).length;
232
+ if (seoLength > SEO_TITLE_LIMIT) {
233
+ problems.push(`title: "${seoTitle}" is ${seoLength} code points (max ${SEO_TITLE_LIMIT})`);
234
+ }
235
+ if (title.includes('…') || title.includes('...')) problems.push('title: contains "…" or "..."');
236
+ }
237
+
238
+ const description = scalar(frontmatter, 'description');
239
+ const [min, max] = locale === 'en' ? [120, 155] : [70, 100];
240
+ if (description === undefined || description === '') {
241
+ problems.push('description: missing');
242
+ } else {
243
+ const length = Array.from(description).length;
244
+ if (length < min || length > max) {
245
+ problems.push(`description: ${length} code points (expected ${min}–${max} for ${locale})`);
246
+ }
247
+ if (!endsAsSentence(description, locale)) {
248
+ problems.push('description: does not end with sentence punctuation');
249
+ }
250
+ const lower = description.toLowerCase();
251
+ const intro = PODCAST_INTROS.find((candidate) => lower.startsWith(candidate.toLowerCase()));
252
+ if (intro) problems.push(`description: starts with the podcast intro ("${intro}")`);
253
+ if (description.includes('…') || description.includes('...')) {
254
+ problems.push('description: contains "…" or "..."');
255
+ }
256
+ }
257
+
258
+ const points = frontmatter.get('key_points');
259
+ if (!Array.isArray(points) || points.length === 0) {
260
+ problems.push('key_points: missing');
261
+ } else {
262
+ const labelMax = locale === 'en' ? 28 : 14;
263
+ points.forEach((point, index) => {
264
+ const sep = point.indexOf(' — ');
265
+ if (sep < 0) {
266
+ problems.push(`key_points[${index}]: missing " — " separator`);
267
+ return;
268
+ }
269
+ const label = point.slice(0, sep);
270
+ const text = point.slice(sep + 3);
271
+ const labelLength = Array.from(label).length;
272
+ if (labelLength === 0 || labelLength > labelMax) {
273
+ problems.push(`key_points[${index}]: label "${label}" is ${labelLength} code points (max ${labelMax})`);
274
+ }
275
+ if (!endsAsSentence(text, locale)) {
276
+ problems.push(`key_points[${index}]: text does not end with sentence punctuation`);
277
+ }
278
+ if (point.includes('…') || point.includes('...')) {
279
+ problems.push(`key_points[${index}]: contains "…" or "..."`);
280
+ }
281
+ });
282
+ }
283
+ return problems;
284
+ }