@gobing-ai/knowledge-kit 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/board/collect/index.js +725 -1101
- package/dist/board/daily-voice/index.js +722 -101
- package/dist/board/itc/index.js +13 -0
- package/dist/index.js +1796 -102
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +4 -0
- package/plugins/generations/core-facts-gen/dist/index.js +4 -0
- package/plugins/generations/daily-article-gen/dist/index.js +4 -0
- package/plugins/generations/dailynews-gen/dist/index.js +4 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
- package/plugins/generations/gemini-tts/dist/index.js +4 -0
- package/plugins/generations/image-gen/dist/index.js +4 -0
- package/plugins/generations/news-report-gen/dist/index.js +4 -0
- package/plugins/generations/voice-gen/dist/index.js +4 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +28 -1
- package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
- package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
- package/plugins/ingestions/web-search/dist/index.js +4 -0
- package/plugins/kk/config.example.yaml +8 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
- package/plugins/kk/scripts/daily-stages.ts +665 -78
- package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
- package/plugins/kk/scripts/sentences.ts +134 -0
- package/plugins/kk/scripts/series-names.ts +124 -0
- package/plugins/kk/scripts/topic-boundary.json +18 -83
- package/plugins/kk/workflows/kk-collect.yaml +2 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +112 -9
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
- package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
- package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
- package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
- package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
- package/plugins/publishings/csdn-pub/dist/index.js +4 -0
- package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/emdash-pub/dist/index.js +4 -0
- package/plugins/publishings/infoq-pub/dist/index.js +4 -0
- package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/jike-pub/dist/index.js +4 -0
- package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/juejin-pub/dist/index.js +4 -0
- package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/medium-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/README.md +65 -0
- package/plugins/publishings/podcast-pub/dist/index.js +1906 -374
- package/plugins/publishings/podcast-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
- package/plugins/publishings/podcast-pub/src/index.ts +211 -34
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
- package/plugins/publishings/podcast-pub/src/seo.ts +1079 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +74 -14
- package/plugins/publishings/qiita-pub/dist/index.js +4 -0
- package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
- package/plugins/publishings/sspai-pub/dist/index.js +4 -0
- package/plugins/publishings/substack-pub/dist/index.js +4 -0
- package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +313 -32
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +170 -39
- package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
- package/plugins/publishings/threads-pub/dist/index.js +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/weibo-pub/dist/index.js +4 -0
- package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/x-pub/dist/index.js +4 -0
- package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/xhs-pub/dist/index.js +4 -0
- package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
- package/plugins/publishings/zenn-pub/dist/index.js +4 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-refine digest contract check (2026-10-24).
|
|
3
|
+
*
|
|
4
|
+
* The surfdash refine step (scripts/lib/seo-digest.ts) owns the daily-digest title, description
|
|
5
|
+
* and key_points (ADR-008). `requireRefined` only proves refine *ran*; this module re-verifies the
|
|
6
|
+
* values against the site's published contract (exact `<series> · <date>:h1、h2` title shape,
|
|
7
|
+
* `title | Surfing` ≤ 60 code points, description length window + terminal punctuation, no
|
|
8
|
+
* ellipsis, `label — sentence` key_points) so a regressed derivation fails the publish loud
|
|
9
|
+
* instead of shipping broken SEO metadata.
|
|
10
|
+
*
|
|
11
|
+
* Pure, line-based frontmatter parsing — no YAML dependency (same stance as
|
|
12
|
+
* {@link normalizeLocaleFrontmatter} and the site's own check-digest script). Handles `"..."`,
|
|
13
|
+
* `'...'`, plain and `>-`-folded scalars, flow arrays and ` - "..."` block lists.
|
|
14
|
+
*/
|
|
15
|
+
import { SERIES_NAMES } from './patch';
|
|
16
|
+
|
|
17
|
+
/** Locales the digest series publishes in (zh primary; en/ja translated siblings). */
|
|
18
|
+
export type DigestLocale = keyof typeof SERIES_NAMES;
|
|
19
|
+
|
|
20
|
+
/** The tag that marks a post as one of the daily digest series (site DIGEST_TAG). */
|
|
21
|
+
export const DIGEST_TAG = 'sub-category:ai-daily';
|
|
22
|
+
|
|
23
|
+
/** Podcast-intro openings a digest description must not start with (case-insensitive). */
|
|
24
|
+
const PODCAST_INTROS = ['这里是由Robin', '咸话咸说', 'This is Robin', 'Robin がお届け'] as const;
|
|
25
|
+
|
|
26
|
+
/** Rendered `<title>` budget: title + " | Surfing" must fit (site SEO_TITLE_LIMIT). */
|
|
27
|
+
const SEO_TITLE_LIMIT = 60;
|
|
28
|
+
|
|
29
|
+
/** One parsed frontmatter entry: a scalar string or a block/flow list of strings. */
|
|
30
|
+
type FrontmatterValue = string | string[];
|
|
31
|
+
|
|
32
|
+
/** Opening `key:` line: bare key, no leading whitespace (frontmatter is never indented). */
|
|
33
|
+
const KEY_LINE = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/;
|
|
34
|
+
|
|
35
|
+
const escapeRe = (token: string): string => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
|
|
37
|
+
/** Unquotes a YAML scalar: `"…"` via JSON (mapContent writes JSON.stringify), `'…'` with '' unescape. */
|
|
38
|
+
function unquote(value: string): string {
|
|
39
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(value) as string;
|
|
42
|
+
} catch {
|
|
43
|
+
return value.slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
47
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Line-based frontmatter parse: scalars (plain/quoted/folded) and string lists (block `- item`
|
|
54
|
+
* or flow `[a, b]`). Unknown shapes degrade to the raw text; a missing frontmatter yields {}.
|
|
55
|
+
*/
|
|
56
|
+
export function parseFrontmatterScalars(markdown: string): Map<string, FrontmatterValue> {
|
|
57
|
+
const out = new Map<string, FrontmatterValue>();
|
|
58
|
+
const lines = markdown.split(/\r?\n/);
|
|
59
|
+
if (lines[0]?.trim() !== '---') return out;
|
|
60
|
+
const end = lines.findIndex((line, i) => i > 0 && line.trim() === '---');
|
|
61
|
+
if (end < 0) return out;
|
|
62
|
+
for (let i = 1; i < end; i++) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
if (!line) continue;
|
|
65
|
+
const match = line.match(KEY_LINE);
|
|
66
|
+
if (!match?.[1] || match[2] === undefined) continue;
|
|
67
|
+
const key = match[1];
|
|
68
|
+
const value = match[2].trim();
|
|
69
|
+
if (value === '>-' || value === '>' || value === '>+' || value === '|-' || value === '|') {
|
|
70
|
+
// Folded scalar: indented continuation lines, joined with spaces (block scalars too —
|
|
71
|
+
// descriptions are prose, a stray newline matters less than losing the text).
|
|
72
|
+
const parts: string[] = [];
|
|
73
|
+
let j = i + 1;
|
|
74
|
+
while (j < end) {
|
|
75
|
+
const continuation = lines[j];
|
|
76
|
+
if (!continuation || !/^[ \t]+\S/.test(continuation)) break;
|
|
77
|
+
parts.push(continuation.trim());
|
|
78
|
+
j++;
|
|
79
|
+
}
|
|
80
|
+
out.set(key, parts.join(' '));
|
|
81
|
+
i = j - 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (value === '') {
|
|
85
|
+
// Block list: `key:` followed by ` - item` lines; an empty key stays ''.
|
|
86
|
+
const items: string[] = [];
|
|
87
|
+
let j = i + 1;
|
|
88
|
+
while (j < end) {
|
|
89
|
+
const itemLine = lines[j];
|
|
90
|
+
if (!itemLine || !/^\s*-\s+/.test(itemLine)) break;
|
|
91
|
+
items.push(unquote(itemLine.replace(/^\s*-\s+/, '').trim()));
|
|
92
|
+
j++;
|
|
93
|
+
}
|
|
94
|
+
if (items.length > 0) {
|
|
95
|
+
out.set(key, items);
|
|
96
|
+
i = j - 1;
|
|
97
|
+
} else {
|
|
98
|
+
out.set(key, '');
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
103
|
+
out.set(
|
|
104
|
+
key,
|
|
105
|
+
value
|
|
106
|
+
.slice(1, -1)
|
|
107
|
+
.split(',')
|
|
108
|
+
.map((item) => unquote(item.trim()))
|
|
109
|
+
.filter((item) => item !== ''),
|
|
110
|
+
);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
out.set(key, unquote(value));
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The scalar at `key`: a list joins with ' ' (never a shape the digest contract accepts). */
|
|
119
|
+
function scalar(frontmatter: Map<string, FrontmatterValue>, key: string): string | undefined {
|
|
120
|
+
const value = frontmatter.get(key);
|
|
121
|
+
if (value === undefined) return undefined;
|
|
122
|
+
return Array.isArray(value) ? value.join(' ') : value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Terminal punctuation per locale: terminator, optionally followed by closing quotes/brackets. */
|
|
126
|
+
function endsAsSentence(text: string, locale: DigestLocale): boolean {
|
|
127
|
+
return locale === 'en' ? /[.!?]["')”’]*$/u.test(text.trimEnd()) : /[。!?][」』”)]*$/u.test(text.trimEnd());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validates a refined post's frontmatter against the digest contract. Returns the problems
|
|
132
|
+
* (empty = ok); non-digest posts (no `sub-category:ai-daily` tag) return [] unchecked.
|
|
133
|
+
*/
|
|
134
|
+
export function checkDigestFrontmatter(markdown: string, locale: DigestLocale): string[] {
|
|
135
|
+
const frontmatter = parseFrontmatterScalars(markdown);
|
|
136
|
+
const tags = frontmatter.get('tags');
|
|
137
|
+
const tagList = Array.isArray(tags) ? tags : typeof tags === 'string' && tags !== '' ? [tags] : [];
|
|
138
|
+
if (!tagList.some((tag) => tag.includes(DIGEST_TAG))) return [];
|
|
139
|
+
|
|
140
|
+
const problems: string[] = [];
|
|
141
|
+
const series = SERIES_NAMES[locale];
|
|
142
|
+
const titleSep = locale === 'en' ? ': ' : ':';
|
|
143
|
+
const listSep = locale === 'en' ? ', ' : '、';
|
|
144
|
+
|
|
145
|
+
const title = scalar(frontmatter, 'title');
|
|
146
|
+
if (title === undefined || title === '') {
|
|
147
|
+
problems.push('title: missing');
|
|
148
|
+
} else {
|
|
149
|
+
const match = title.match(
|
|
150
|
+
new RegExp(`^${escapeRe(series)} · (\\d{4}-\\d{2}-\\d{2})${escapeRe(titleSep)}(.+)$`),
|
|
151
|
+
);
|
|
152
|
+
if (!match) {
|
|
153
|
+
problems.push(
|
|
154
|
+
`title: expected "<series> · YYYY-MM-DD${titleSep}<h1>${listSep}<h2>" for ${series}, got "${title}"`,
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
const rest = match[2] ?? '';
|
|
158
|
+
const heads = locale === 'en' ? rest.split(', ') : rest.split('、');
|
|
159
|
+
const seps = locale === 'en' ? (rest.match(/, /g) ?? []).length : (rest.match(/、/g) ?? []).length;
|
|
160
|
+
if (seps !== 1 || heads.some((head) => head.trim() === '')) {
|
|
161
|
+
problems.push(`title: expected exactly two non-empty headlines joined "${listSep}", got "${rest}"`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const seoTitle = `${title} | Surfing`;
|
|
165
|
+
const seoLength = Array.from(seoTitle).length;
|
|
166
|
+
if (seoLength > SEO_TITLE_LIMIT) {
|
|
167
|
+
problems.push(`title: "${seoTitle}" is ${seoLength} code points (max ${SEO_TITLE_LIMIT})`);
|
|
168
|
+
}
|
|
169
|
+
if (title.includes('…') || title.includes('...')) problems.push('title: contains "…" or "..."');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const description = scalar(frontmatter, 'description');
|
|
173
|
+
const [min, max] = locale === 'en' ? [120, 155] : [70, 100];
|
|
174
|
+
if (description === undefined || description === '') {
|
|
175
|
+
problems.push('description: missing');
|
|
176
|
+
} else {
|
|
177
|
+
const length = Array.from(description).length;
|
|
178
|
+
if (length < min || length > max) {
|
|
179
|
+
problems.push(`description: ${length} code points (expected ${min}–${max} for ${locale})`);
|
|
180
|
+
}
|
|
181
|
+
if (!endsAsSentence(description, locale)) {
|
|
182
|
+
problems.push('description: does not end with sentence punctuation');
|
|
183
|
+
}
|
|
184
|
+
const lower = description.toLowerCase();
|
|
185
|
+
const intro = PODCAST_INTROS.find((candidate) => lower.startsWith(candidate.toLowerCase()));
|
|
186
|
+
if (intro) problems.push(`description: starts with the podcast intro ("${intro}")`);
|
|
187
|
+
if (description.includes('…') || description.includes('...')) {
|
|
188
|
+
problems.push('description: contains "…" or "..."');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const points = frontmatter.get('key_points');
|
|
193
|
+
if (!Array.isArray(points) || points.length === 0) {
|
|
194
|
+
problems.push('key_points: missing');
|
|
195
|
+
} else {
|
|
196
|
+
const labelMax = locale === 'en' ? 28 : 14;
|
|
197
|
+
points.forEach((point, index) => {
|
|
198
|
+
const sep = point.indexOf(' — ');
|
|
199
|
+
if (sep < 0) {
|
|
200
|
+
problems.push(`key_points[${index}]: missing " — " separator`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const label = point.slice(0, sep);
|
|
204
|
+
const text = point.slice(sep + 3);
|
|
205
|
+
const labelLength = Array.from(label).length;
|
|
206
|
+
if (labelLength === 0 || labelLength > labelMax) {
|
|
207
|
+
problems.push(`key_points[${index}]: label "${label}" is ${labelLength} code points (max ${labelMax})`);
|
|
208
|
+
}
|
|
209
|
+
if (!endsAsSentence(text, locale)) {
|
|
210
|
+
problems.push(`key_points[${index}]: text does not end with sentence punctuation`);
|
|
211
|
+
}
|
|
212
|
+
if (point.includes('…') || point.includes('...')) {
|
|
213
|
+
problems.push(`key_points[${index}]: contains "…" or "..."`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return problems;
|
|
218
|
+
}
|
|
@@ -6,8 +6,12 @@ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-
|
|
|
6
6
|
import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
7
7
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
8
8
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
9
|
+
// The plugin's public digest-contract surface (2026-10-24).
|
|
10
|
+
import { checkDigestFrontmatter, DIGEST_TAG, type DigestLocale } from './digest-check';
|
|
9
11
|
import { localeIndexPath, PATCH_LOCALES, type PatchPostOutcome, patchPostFiles } from './patch';
|
|
10
12
|
|
|
13
|
+
export { checkDigestFrontmatter, DIGEST_TAG, type DigestLocale } from './digest-check';
|
|
14
|
+
|
|
11
15
|
/** Result target name for this plugin. */
|
|
12
16
|
const TARGET = 'surfdash';
|
|
13
17
|
|
|
@@ -76,28 +80,14 @@ export function normalizeTag(raw: string): string {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
|
-
*
|
|
83
|
+
* The content's tag sources (metadata.tags, keywords, subCategory) as normalized frontmatter tags.
|
|
84
|
+
*
|
|
85
|
+
* `slugify` drops non-ASCII, so a Chinese tag (`硬件发布`) collapsed to '' and shipped as an empty
|
|
86
|
+
* YAML tag — the site renders it as a blank chip (seen on 20261011-ai-2026-10-11). Case variants
|
|
87
|
+
* also collapsed to the same slug twice (`AI` + `ai`), so the set dedupes. `normalizeTag` preserves
|
|
88
|
+
* `sub-category:<slug>` colon prefixes so sub-category series tags survive into frontmatter.
|
|
80
89
|
*/
|
|
81
|
-
export function
|
|
82
|
-
let title = content.title;
|
|
83
|
-
if (!title) {
|
|
84
|
-
const match = content.body.match(/^#\s+(.+)$/m);
|
|
85
|
-
title = match?.[1] ? match[1].trim() : 'Untitled Post';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let description: string | undefined;
|
|
89
|
-
if (typeof content.metadata?.description === 'string') {
|
|
90
|
-
description = content.metadata.description;
|
|
91
|
-
} else {
|
|
92
|
-
const paragraphs = content.body
|
|
93
|
-
.split(/\n\s*\n/)
|
|
94
|
-
.map((p) => p.replace(/^#+\s+.*$/, '').trim())
|
|
95
|
-
.filter((p) => p.length > 0);
|
|
96
|
-
if (paragraphs.length > 0) {
|
|
97
|
-
description = paragraphs[0];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
90
|
+
export function contentTags(content: Content): string[] {
|
|
101
91
|
const rawTags: unknown[] = Array.isArray(content.metadata?.tags)
|
|
102
92
|
? [...content.metadata.tags]
|
|
103
93
|
: Array.isArray(content.metadata?.keywords)
|
|
@@ -107,36 +97,51 @@ export function mapContentToSurfingMarkdown(content: Content, options: MapConten
|
|
|
107
97
|
const subRaw = content.metadata.subCategory.trim();
|
|
108
98
|
rawTags.push(/^sub-category\s*:/i.test(subRaw) ? subRaw : `sub-category:${subRaw}`);
|
|
109
99
|
}
|
|
110
|
-
|
|
111
|
-
// YAML tag — the site renders it as a blank chip (seen on 20261011-ai-2026-10-11). Case variants
|
|
112
|
-
// also collapsed to the same slug twice (`AI` + `ai`), so the set dedupes. `normalizeTag` preserves
|
|
113
|
-
// `sub-category:<slug>` colon prefixes so sub-category series tags survive into frontmatter.
|
|
114
|
-
const tags = [
|
|
100
|
+
return [
|
|
115
101
|
...new Set(
|
|
116
102
|
rawTags
|
|
117
103
|
.filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
|
118
104
|
.map((t: string) => normalizeTag(t)),
|
|
119
105
|
),
|
|
120
106
|
].filter((tag) => tag.length > 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Maps Content object into Surfing frontmatter and body per the publish spec.
|
|
111
|
+
*/
|
|
112
|
+
export function mapContentToSurfingMarkdown(content: Content, options: MapContentOptions = {}): string {
|
|
113
|
+
let title = content.title;
|
|
114
|
+
if (!title) {
|
|
115
|
+
const match = content.body.match(/^#\s+(.+)$/m);
|
|
116
|
+
title = match?.[1] ? match[1].trim() : 'Untitled Post';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ADR-008: the site's refine step owns the digest/SEO description; the generator's
|
|
120
|
+
// first-paragraph fallback shipped the raw lede as SEO metadata before refine existed.
|
|
121
|
+
const description =
|
|
122
|
+
typeof content.metadata?.description === 'string' && content.metadata.description.length > 0
|
|
123
|
+
? content.metadata.description
|
|
124
|
+
: '';
|
|
125
|
+
|
|
126
|
+
const tags = contentTags(content);
|
|
121
127
|
|
|
122
128
|
const isoDate = options.publishDate ?? new Date().toISOString().slice(0, 10);
|
|
123
|
-
const firstRefUrl = content.references?.[0]?.url;
|
|
124
|
-
const canonical =
|
|
125
|
-
typeof firstRefUrl === 'string'
|
|
126
|
-
? firstRefUrl
|
|
127
|
-
: typeof content.metadata?.canonicalUrl === 'string'
|
|
128
|
-
? content.metadata.canonicalUrl
|
|
129
|
-
: undefined;
|
|
130
129
|
const draft = typeof content.metadata?.published === 'boolean' ? !content.metadata.published : false;
|
|
131
130
|
|
|
132
131
|
// Strict surfdash PostSchema (surfdash/src/content/schema.ts): publishDate (ISO date) is
|
|
133
132
|
// required; unknown keys (`date`, `canonical_url`, `published`) are rejected.
|
|
133
|
+
//
|
|
134
|
+
// `canonical` is deliberately NOT written here. The canonical URL is the site's own route
|
|
135
|
+
// for this slug, which only the site can compute (locale prefix + collection segment); the
|
|
136
|
+
// previous `references[0].url` fallback pointed the canonical at an unrelated *external*
|
|
137
|
+
// source and would have dropped the post out of the sitemap if surfdash's refine step had
|
|
138
|
+
// not overwritten it. See docs/00_ADR.md ADR-008: the generator supplies content, the site
|
|
139
|
+
// supplies identity.
|
|
134
140
|
const frontmatterObj: Record<string, unknown> = {
|
|
135
141
|
title,
|
|
136
|
-
description
|
|
142
|
+
description,
|
|
137
143
|
tags,
|
|
138
144
|
publishDate: isoDate,
|
|
139
|
-
...(canonical ? { canonical } : {}),
|
|
140
145
|
draft,
|
|
141
146
|
...(options.image ? { image: options.image, og_image: options.image } : {}),
|
|
142
147
|
};
|
|
@@ -145,6 +150,11 @@ export function mapContentToSurfingMarkdown(content: Content, options: MapConten
|
|
|
145
150
|
return `${yamlLines.join('\n')}\n\n${content.body.trim()}\n`;
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
/** True when the content's tag sources (contentTags) mark it as a daily digest post. */
|
|
154
|
+
function isDigestContent(content: Content): boolean {
|
|
155
|
+
return contentTags(content).some((tag) => tag.includes(DIGEST_TAG));
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
/** `Result` failure helper: ok false + error (podcast-pub pattern). */
|
|
149
159
|
function fail(error: string): Result {
|
|
150
160
|
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
@@ -251,6 +261,7 @@ async function spawnCli(cmd: string[], cwd: string): Promise<CliSpawnResult> {
|
|
|
251
261
|
cwd,
|
|
252
262
|
stdout: 'pipe',
|
|
253
263
|
stderr: 'pipe',
|
|
264
|
+
env: process.env,
|
|
254
265
|
});
|
|
255
266
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
256
267
|
proc.exited,
|
|
@@ -273,6 +284,64 @@ export function withEmdashTokenHint(errText: string): string {
|
|
|
273
284
|
: errText;
|
|
274
285
|
}
|
|
275
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Post-conditions the site guarantees after its publishing pipeline ran.
|
|
289
|
+
*
|
|
290
|
+
* The publisher does not derive SEO metadata any more (ADR-008): surfdash's refine step owns
|
|
291
|
+
* `refined`, `seo_title`, `canonical` and the digest title/description. Verifying the marker
|
|
292
|
+
* here turns a silently-regressed site pipeline into a loud publisher failure instead of a post
|
|
293
|
+
* that ships with a date-only title — which is exactly how the 2026-10-21 regression went
|
|
294
|
+
* unnoticed.
|
|
295
|
+
*/
|
|
296
|
+
export interface RefinedSnapshot {
|
|
297
|
+
title?: string;
|
|
298
|
+
seoTitle?: string;
|
|
299
|
+
description?: string;
|
|
300
|
+
canonical?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Reads the frontmatter scalar fields the site owns; undefined when refine never ran. */
|
|
304
|
+
export function readRefinedSnapshot(filePath: string): RefinedSnapshot | undefined {
|
|
305
|
+
if (!existsSync(filePath)) return undefined;
|
|
306
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
307
|
+
const block = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
308
|
+
const fm = block?.[1] ?? '';
|
|
309
|
+
if (!fm || !/^refined:\s*true$/m.test(fm)) return undefined;
|
|
310
|
+
const read = (key: string): string | undefined => {
|
|
311
|
+
const m = fm.match(new RegExp(`^${key}:\\s*(?:>-?\n((?:[ \\t]+.*\n?)+)|(.*))$`, 'm'));
|
|
312
|
+
if (!m) return undefined;
|
|
313
|
+
const value = (m[1] ?? m[2] ?? '').replace(/\n[ \t]+/g, ' ').trim();
|
|
314
|
+
return value.replace(/^["']|["']$/g, '') || undefined;
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
title: read('title'),
|
|
318
|
+
seoTitle: read('seo_title'),
|
|
319
|
+
description: read('description'),
|
|
320
|
+
canonical: read('canonical'),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Error text when the site pipeline did not mark the post refined, else undefined. */
|
|
325
|
+
export function requireRefined(filePath: string): string | undefined {
|
|
326
|
+
if (readRefinedSnapshot(filePath)) return undefined;
|
|
327
|
+
return `surfdash refine step did not run for ${filePath} (no \`refined: true\`): the publishing pipeline is broken — the post would ship with generator-supplied SEO metadata`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Post-refine digest contract gate (2026-10-24): `requireRefined` proves refine ran; this proves
|
|
332
|
+
* the refined values still satisfy the digest contract. Returns the fail-loud error text, or
|
|
333
|
+
* undefined when ok / downgraded. `SURFDASH_DIGEST_CHECK=warn` echoes each problem and continues.
|
|
334
|
+
*/
|
|
335
|
+
export function digestCheckGate(filePath: string, locale: DigestLocale): string | undefined {
|
|
336
|
+
const problems = checkDigestFrontmatter(readFileSync(filePath, 'utf-8'), locale);
|
|
337
|
+
if (problems.length === 0) return undefined;
|
|
338
|
+
if (process.env.SURFDASH_DIGEST_CHECK === 'warn') {
|
|
339
|
+
for (const problem of problems) echoError(`digest check (warn): ${problem}`);
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return `digest frontmatter check failed: ${problems.join('; ')}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
276
345
|
/** Outcome of {@link pushPostGit} — the union keeps fail-loud errors typed at the call site. */
|
|
277
346
|
export type GitPushOutcome = { ok: true; hash: string } | { ok: false; error: string };
|
|
278
347
|
|
|
@@ -300,6 +369,41 @@ async function assertMainBranch(surfdashRoot: string): Promise<{ ok: true } | {
|
|
|
300
369
|
return { ok: true };
|
|
301
370
|
}
|
|
302
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Fast-forward the checkout onto `origin/main` before anything is written (2026-10-22).
|
|
374
|
+
*
|
|
375
|
+
* Three consecutive publish days failed with `! [rejected] main -> main (fetch first)`: this
|
|
376
|
+
* checkout sat behind a remote another machine had pushed to, so the publish commit landed on a
|
|
377
|
+
* stale base and the push was rejected — after the article had already been written and committed,
|
|
378
|
+
* and after the podcast episode had already published, so the run closed `partial`/`failed` while a
|
|
379
|
+
* real episode was live. A fast-forward is lossless (HEAD moves only when local carries no commit of
|
|
380
|
+
* its own), which is exactly the case here; a DIVERGED checkout is the operator's call and fails
|
|
381
|
+
* loud with both counts. An unreachable remote is not fatal: the fetch error is swallowed so the
|
|
382
|
+
* push below reports the real reason.
|
|
383
|
+
*/
|
|
384
|
+
export async function fastForwardToOrigin(surfdashRoot: string): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
385
|
+
const fetched = await spawnCli(['git', 'fetch', '--quiet', 'origin', 'main'], surfdashRoot);
|
|
386
|
+
if (fetched.exitCode !== 0) return { ok: true };
|
|
387
|
+
const counts = await spawnCli(['git', 'rev-list', '--left-right', '--count', 'origin/main...HEAD'], surfdashRoot);
|
|
388
|
+
if (counts.exitCode !== 0) return { ok: true };
|
|
389
|
+
const [behindRaw = '0', aheadRaw = '0'] = counts.stdout.trim().split(/\s+/);
|
|
390
|
+
const behind = Number(behindRaw);
|
|
391
|
+
const ahead = Number(aheadRaw);
|
|
392
|
+
if (!Number.isFinite(behind) || !Number.isFinite(ahead) || behind === 0) return { ok: true };
|
|
393
|
+
if (ahead > 0) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
error: `surfdash checkout diverged from origin/main (${ahead} local, ${behind} remote commit(s)): reconcile it by hand before publishing`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const merged = await spawnCli(['git', 'merge', '--ff-only', '--quiet', 'origin/main'], surfdashRoot);
|
|
400
|
+
if (merged.exitCode !== 0) {
|
|
401
|
+
return { ok: false, error: `surfdash fast-forward to origin/main failed: ${cliErrorText(merged)}` };
|
|
402
|
+
}
|
|
403
|
+
echoError(`surfdash: fast-forwarded ${behind} commit(s) to origin/main before writing`);
|
|
404
|
+
return { ok: true };
|
|
405
|
+
}
|
|
406
|
+
|
|
303
407
|
/**
|
|
304
408
|
* Commits several paths of the surfdash checkout and pushes them to `main` — the plural form of
|
|
305
409
|
* {@link pushPostGit} the post-patch needs (it writes the posts and any copied cover). Same
|
|
@@ -315,6 +419,8 @@ export async function commitPostPaths(
|
|
|
315
419
|
if (relPaths.length === 0) return { ok: true, hash: 'unchanged' };
|
|
316
420
|
const branch = await assertMainBranch(surfdashRoot);
|
|
317
421
|
if (!branch.ok) return branch;
|
|
422
|
+
const base = await fastForwardToOrigin(surfdashRoot);
|
|
423
|
+
if (!base.ok) return base;
|
|
318
424
|
const add = await spawnCli(['git', 'add', '--', ...relPaths], surfdashRoot);
|
|
319
425
|
if (add.exitCode !== 0) return { ok: false, error: `surfdash git add failed: ${cliErrorText(add)}` };
|
|
320
426
|
const staged = await spawnCli(['git', 'diff', '--cached', '--quiet', '--', ...relPaths], surfdashRoot);
|
|
@@ -484,6 +590,13 @@ async function syncPost(options: SurfdashPubOptions): Promise<Result> {
|
|
|
484
590
|
`publish-orchestrator sync failed (exit ${spawned.exitCode}): ${withEmdashTokenHint(cliErrorText(spawned))}`,
|
|
485
591
|
);
|
|
486
592
|
}
|
|
593
|
+
// Digest contract (2026-10-24): verify the synced post's frontmatter for its own locale;
|
|
594
|
+
// paths outside content/posts/articles/<zh|en|ja>/ carry no digest series to check.
|
|
595
|
+
const localeInPath = postPath.postPath.match(/[\\/]content[\\/]posts[\\/]articles[\\/](zh|en|ja)[\\/]/);
|
|
596
|
+
if (localeInPath) {
|
|
597
|
+
const digestProblem = digestCheckGate(postPath.postPath, localeInPath[1] as DigestLocale);
|
|
598
|
+
if (digestProblem) return fail(digestProblem);
|
|
599
|
+
}
|
|
487
600
|
return ResultSchema.parse({
|
|
488
601
|
ok: true,
|
|
489
602
|
target: TARGET,
|
|
@@ -621,16 +734,26 @@ async function publishPost(payload: PublishTransportPayload): Promise<Result> {
|
|
|
621
734
|
return fail(`post path escapes ${articlesRoot}`);
|
|
622
735
|
}
|
|
623
736
|
const coverPath = payload.content.metadata?.coverImage;
|
|
624
|
-
|
|
737
|
+
// R4 + 2026-10-24: a coverImage that does not exist must fail before anything is written —
|
|
738
|
+
// the old silent skip published a coverless post while the run believed it had one.
|
|
739
|
+
if (typeof coverPath === 'string' && !existsSync(coverPath)) {
|
|
740
|
+
return fail(`metadata.coverImage not found: ${coverPath}`);
|
|
741
|
+
}
|
|
742
|
+
if (typeof coverPath === 'string' && !COVER_EXTS.has(extname(coverPath).toLowerCase())) {
|
|
625
743
|
return fail(`metadata.coverImage must be one of ${[...COVER_EXTS].join(', ')}`);
|
|
626
744
|
}
|
|
745
|
+
// Daily digest posts are the series' public face: a missing cover ships a blank OG card.
|
|
746
|
+
// The check uses the same tag sources as mapContentToSurfingMarkdown (contentTags).
|
|
747
|
+
if (isDigestContent(payload.content) && typeof coverPath !== 'string') {
|
|
748
|
+
return fail('digest post has no cover image (metadata.coverImage)');
|
|
749
|
+
}
|
|
627
750
|
|
|
628
751
|
await createNodeFileSystem().ensureDir(postDir);
|
|
629
752
|
|
|
630
753
|
// Cover image: metadata.coverImage (absolute local path) is copied into the post's
|
|
631
754
|
// assets dir and referenced relatively — the surfdash PostSchema accepts ./assets/<file>.
|
|
632
755
|
let imageFrontmatter: string | undefined;
|
|
633
|
-
if (typeof coverPath === 'string'
|
|
756
|
+
if (typeof coverPath === 'string') {
|
|
634
757
|
const assetsDir = join(postDir, 'assets');
|
|
635
758
|
await createNodeFileSystem().ensureDir(assetsDir);
|
|
636
759
|
const coverName = `cover${extname(coverPath).toLowerCase()}`;
|
|
@@ -653,6 +776,14 @@ async function publishPost(payload: PublishTransportPayload): Promise<Result> {
|
|
|
653
776
|
);
|
|
654
777
|
}
|
|
655
778
|
|
|
779
|
+
// Contract check (ADR-008): the site, not this plugin, owns SEO metadata.
|
|
780
|
+
const notRefined = requireRefined(filePath);
|
|
781
|
+
if (notRefined) return fail(notRefined);
|
|
782
|
+
// Digest contract (2026-10-24): the refined values themselves must hold, before any push.
|
|
783
|
+
const digestProblem = digestCheckGate(filePath, locale as DigestLocale);
|
|
784
|
+
if (digestProblem) return fail(digestProblem);
|
|
785
|
+
const refined = readRefinedSnapshot(filePath);
|
|
786
|
+
|
|
656
787
|
// Deploy seam: commit + push the post so deploy.yml builds it, then wait for
|
|
657
788
|
// the live URL — downstream show-notes advertise a real page, not a 404.
|
|
658
789
|
// Tests (and offline runs) opt out via SURFDASH_AUTO_PUSH=0.
|
|
@@ -669,7 +800,7 @@ async function publishPost(payload: PublishTransportPayload): Promise<Result> {
|
|
|
669
800
|
message: 'Published via postsurfing CLI (auto-push disabled)',
|
|
670
801
|
// Absolute post path lets downstream steps (locale translation + CMS sync)
|
|
671
802
|
// derive the surfdash checkout root without extra configuration.
|
|
672
|
-
metadata: { postPath: filePath },
|
|
803
|
+
metadata: { postPath: filePath, refinedTitle: refined?.title, refinedCanonical: refined?.canonical },
|
|
673
804
|
});
|
|
674
805
|
}
|
|
675
806
|
const relPostDir = join('content', 'posts', 'articles', locale, slug);
|
|
@@ -684,7 +815,7 @@ async function publishPost(payload: PublishTransportPayload): Promise<Result> {
|
|
|
684
815
|
message: live
|
|
685
816
|
? 'Published, committed, pushed, and live on the site'
|
|
686
817
|
: 'Published and pushed; URL not live within the verify window — check the surfdash deploy workflow',
|
|
687
|
-
metadata: { postPath: filePath, gitCommit: pushed.hash, live },
|
|
818
|
+
metadata: { postPath: filePath, gitCommit: pushed.hash, live, refinedTitle: refined?.title },
|
|
688
819
|
});
|
|
689
820
|
} catch (err: unknown) {
|
|
690
821
|
return fail(`Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -60,13 +60,23 @@ export function localeIndexPath(postPath: string, locale: PatchLocale): string |
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* The podcast cross-link line per locale (task 0189 R4: the column's episode-title pattern, not the
|
|
63
|
-
* retired news-bulletin name).
|
|
63
|
+
* retired news-bulletin name). The series names are fixed per-locale product identity (2026-10-23:
|
|
64
|
+
* the translate agent had been inventing them) and must agree with the pipeline's single table in
|
|
65
|
+
* plugins/kk/scripts/series-names.ts — the two packages cannot import each other, so tests assert
|
|
66
|
+
* the agreement. digest-check.ts imports this table as the title-contract series names.
|
|
64
67
|
*/
|
|
68
|
+
export const SERIES_NAMES: Record<PatchLocale, string> = {
|
|
69
|
+
zh: '每日AI乱炖',
|
|
70
|
+
en: 'Daily AI Stew',
|
|
71
|
+
ja: 'デイリーAIシチュー',
|
|
72
|
+
};
|
|
73
|
+
|
|
65
74
|
export function crossLinkFor(locale: PatchLocale, dateIso: string, showNotesUrl: string): string {
|
|
75
|
+
const series = SERIES_NAMES[locale];
|
|
66
76
|
const links: Record<PatchLocale, string> = {
|
|
67
|
-
zh: `🎧 本期已同步制作为播客节目,[
|
|
68
|
-
en: `🎧 This episode is also available as a podcast: [listen to
|
|
69
|
-
ja: `🎧 本エピソードはポッドキャストでも配信中:[
|
|
77
|
+
zh: `🎧 本期已同步制作为播客节目,[点击收听《${series} · ${dateIso}》](${showNotesUrl})。`,
|
|
78
|
+
en: `🎧 This episode is also available as a podcast: [listen to ${series} · ${dateIso}](${showNotesUrl}).`,
|
|
79
|
+
ja: `🎧 本エピソードはポッドキャストでも配信中:[${series} · ${dateIso} を聴く](${showNotesUrl})。`,
|
|
70
80
|
};
|
|
71
81
|
return links[locale];
|
|
72
82
|
}
|
|
@@ -118,11 +128,23 @@ export async function patchPostFiles(request: PatchPostRequest): Promise<PatchPo
|
|
|
118
128
|
const end = frontmatterEnd(md);
|
|
119
129
|
const fm = end < 0 ? md : md.slice(0, end);
|
|
120
130
|
if (request.coverUrl !== '') {
|
|
131
|
+
// A relative or http:// cover would ship an unservable OG card — the deployed worker
|
|
132
|
+
// only serves absolute https URLs (prod feedback 2026-09-29).
|
|
133
|
+
if (!/^https:\/\//.test(request.coverUrl)) {
|
|
134
|
+
throw new Error(`coverUrl must be an absolute https:// URL, got: ${request.coverUrl}`);
|
|
135
|
+
}
|
|
121
136
|
md = upsertFrontmatterKey(md, 'image', request.coverUrl);
|
|
122
137
|
md = upsertFrontmatterKey(md, 'og_image', request.coverUrl);
|
|
123
138
|
images[locale] = request.coverUrl;
|
|
124
|
-
} else if (!/^image:/m.test(fm)
|
|
139
|
+
} else if (!/^image:/m.test(fm)) {
|
|
125
140
|
// Fallback for a run without a podcast cover: ship the rendered cover with the post.
|
|
141
|
+
// A locale with no cover anywhere must fail naming the locale, not silently skip —
|
|
142
|
+
// the CMS card would carry no servable image at all.
|
|
143
|
+
if (!(await fs.exists(request.localCover))) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`${locale} post has no cover: coverUrl is empty, the frontmatter has no image key, and localCover does not exist (${request.localCover})`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
126
148
|
const asset = join(dir, 'assets', 'cover.png');
|
|
127
149
|
await fs.ensureDir(dirname(asset));
|
|
128
150
|
await fs.copy(request.localCover, asset);
|
|
@@ -131,8 +153,23 @@ export async function patchPostFiles(request: PatchPostRequest): Promise<PatchPo
|
|
|
131
153
|
md = upsertFrontmatterKey(md, 'og_image', './assets/cover.png');
|
|
132
154
|
images[locale] = './assets/cover.png';
|
|
133
155
|
}
|
|
134
|
-
if (request.showNotesUrl !== ''
|
|
135
|
-
|
|
156
|
+
if (request.showNotesUrl !== '') {
|
|
157
|
+
// 2026-10-23: a previous run may have left a 🎧 cross-link line carrying a retired
|
|
158
|
+
// series name — refresh that line in place instead of keeping it forever (the URL it
|
|
159
|
+
// carries made a fresh append a no-op). A non-cross-link line mentioning the URL is
|
|
160
|
+
// left alone; the fresh link is only appended when no line carries the URL at all.
|
|
161
|
+
const link = crossLinkFor(locale, request.dateIso, request.showNotesUrl);
|
|
162
|
+
const lines = md.split('\n');
|
|
163
|
+
const carriesUrl = lines.some((line) => line.includes(request.showNotesUrl));
|
|
164
|
+
if (!carriesUrl) {
|
|
165
|
+
md = `${md.replace(/\s*$/, '')}\n\n${link}\n`;
|
|
166
|
+
} else {
|
|
167
|
+
md = lines
|
|
168
|
+
.map((line) =>
|
|
169
|
+
line.includes(request.showNotesUrl) && line.trimStart().startsWith('🎧') ? link : line,
|
|
170
|
+
)
|
|
171
|
+
.join('\n');
|
|
172
|
+
}
|
|
136
173
|
}
|
|
137
174
|
if (md !== before) {
|
|
138
175
|
await fs.writeFile(indexMd, md);
|
|
@@ -12254,6 +12254,10 @@ init_config();
|
|
|
12254
12254
|
|
|
12255
12255
|
// ../../packages/kk-core/src/render-md.ts
|
|
12256
12256
|
if (false) {}
|
|
12257
|
+
// ../../packages/kk-core/src/sentences.ts
|
|
12258
|
+
var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
|
|
12259
|
+
var LATIN_TERMINATORS = new Set([".", "!", "?"]);
|
|
12260
|
+
var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
|
|
12257
12261
|
// ../../packages/publish-harness/src/index.ts
|
|
12258
12262
|
class FakeTransportCore {
|
|
12259
12263
|
callLogs = [];
|
|
@@ -20162,6 +20162,10 @@ init_runtime_node_bun();
|
|
|
20162
20162
|
|
|
20163
20163
|
// ../../packages/kk-core/src/render-md.ts
|
|
20164
20164
|
if (false) {}
|
|
20165
|
+
// ../../packages/kk-core/src/sentences.ts
|
|
20166
|
+
var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
|
|
20167
|
+
var LATIN_TERMINATORS = new Set([".", "!", "?"]);
|
|
20168
|
+
var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
|
|
20165
20169
|
// ../../packages/publish-harness/src/index.ts
|
|
20166
20170
|
import { homedir } from "os";
|
|
20167
20171
|
import { join, resolve } from "path";
|
|
@@ -20682,6 +20682,10 @@ var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx)
|
|
|
20682
20682
|
});
|
|
20683
20683
|
// ../../packages/kk-core/src/render-md.ts
|
|
20684
20684
|
if (false) {}
|
|
20685
|
+
// ../../packages/kk-core/src/sentences.ts
|
|
20686
|
+
var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
|
|
20687
|
+
var LATIN_TERMINATORS = new Set([".", "!", "?"]);
|
|
20688
|
+
var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
|
|
20685
20689
|
// ../../plugins/publishings/wechatmp-pub/src/cgi.ts
|
|
20686
20690
|
var MP_ORIGIN = "https://mp.weixin.qq.com";
|
|
20687
20691
|
var FILETRANSFER_URL = `${MP_ORIGIN}/cgi-bin/filetransfer`;
|