czon 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/extractMetadataFromMarkdown.js +1 -1
- package/dist/build/pipeline.js +3 -0
- package/dist/process/enhanceMarkdownSource.js +2 -11
- package/dist/process/template.js +1 -2
- package/dist/process/translateMetadata.js +172 -0
- package/dist/ssg/ContentPage.js +14 -10
- package/dist/ssg/components/ContentMeta.js +16 -8
- package/dist/ssg/components/Navigator.js +3 -2
- package/dist/ssg/utils/getLocalizedMetadata.js +20 -0
- package/dist/utils/frontmatter.js +12 -7
- package/package.json +1 -1
- package/prompts/summary-base.md +3 -0
- package/prompts/summary-psychological.md +1 -1
- package/.opencode/skills/article-summarizer/SKILL.md +0 -123
|
@@ -21,7 +21,7 @@ async function extractMetadataFromMarkdown(filePath, content) {
|
|
|
21
21
|
'summary: 文档中型摘要(用一段话总结文章,包含关键论点和结论,控制在 300 字以内),用于 邮件推送内容,newsletter 介绍',
|
|
22
22
|
'inferred_date: 文档中隐含的创建日期(如果有的话,格式:YYYY-MM-DD,没有就留空字符串)',
|
|
23
23
|
'inferred_lang: 文档使用的语言代码(例如:zh-Hans 表示简体中文,en-US 表示美式英语)',
|
|
24
|
-
'key_points:
|
|
24
|
+
'key_points: 文章的关键要点列表(最重要的 3-5 个要点,简洁明了)',
|
|
25
25
|
'audience: 目标读者描述(简短描述,50 字以内)',
|
|
26
26
|
'short_summary: 文档的超短摘要(用 2-3 句话概括文章主要内容,突出核心观点),用于文章列表页摘要,RSS feed 描述',
|
|
27
27
|
...(hasExistingSlug
|
package/dist/build/pipeline.js
CHANGED
|
@@ -40,6 +40,7 @@ const metadata_1 = require("../metadata");
|
|
|
40
40
|
const paths_1 = require("../paths");
|
|
41
41
|
const category_1 = require("../process/category");
|
|
42
42
|
const translateCategories_1 = require("../process/translateCategories");
|
|
43
|
+
const translateMetadata_1 = require("../process/translateMetadata");
|
|
43
44
|
const translateNavLinks_1 = require("../process/translateNavLinks");
|
|
44
45
|
const enhanceMarkdownSource_1 = require("../process/enhanceMarkdownSource");
|
|
45
46
|
const extractMetadataByAI_1 = require("../process/extractMetadataByAI");
|
|
@@ -103,6 +104,8 @@ async function buildPipeline(options) {
|
|
|
103
104
|
}
|
|
104
105
|
// 运行 AI 元数据提取
|
|
105
106
|
await (0, extractMetadataByAI_1.extractMetadataByAI)();
|
|
107
|
+
// 翻译 AI 提取的 metadata JSON
|
|
108
|
+
await (0, translateMetadata_1.processTranslateMetadata)();
|
|
106
109
|
// 提取分类信息
|
|
107
110
|
await (0, category_1.processExtractCategory)();
|
|
108
111
|
// 翻译分类
|
|
@@ -8,10 +8,10 @@ const promises_1 = require("fs/promises");
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const metadata_1 = require("../metadata");
|
|
10
10
|
const paths_1 = require("../paths");
|
|
11
|
-
const frontmatter_1 = require("../utils/frontmatter");
|
|
12
11
|
const writeFile_1 = require("../utils/writeFile");
|
|
13
12
|
/**
|
|
14
13
|
* 存储母语文件到 .czon/src
|
|
14
|
+
* 直接复制原始文件,metadata 从 meta.json 获取
|
|
15
15
|
*/
|
|
16
16
|
async function storeNativeFiles() {
|
|
17
17
|
const { files } = metadata_1.MetaData;
|
|
@@ -25,16 +25,7 @@ async function storeNativeFiles() {
|
|
|
25
25
|
throw new Error(`Missing inferred language`);
|
|
26
26
|
const filePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.path);
|
|
27
27
|
const originalContent = await (0, promises_1.readFile)(path_1.default.join(paths_1.INPUT_DIR, file.path), 'utf-8');
|
|
28
|
-
|
|
29
|
-
const enhancedContent = (0, frontmatter_1.updateFrontmatter)(originalContent, {
|
|
30
|
-
title: file.metadata.title,
|
|
31
|
-
summary: file.metadata.summary,
|
|
32
|
-
tags: file.metadata.tags,
|
|
33
|
-
date: file.metadata.inferred_date,
|
|
34
|
-
});
|
|
35
|
-
// 进行内链接替换, 将相对链接替换为基于 czon://hash 的链接
|
|
36
|
-
// const replacedContent = replaceInnerLinks(file, enhancedContent);
|
|
37
|
-
await (0, writeFile_1.writeFile)(filePath, enhancedContent);
|
|
28
|
+
await (0, writeFile_1.writeFile)(filePath, originalContent);
|
|
38
29
|
}
|
|
39
30
|
catch (error) {
|
|
40
31
|
console.warn(`⚠️ Failed to store native file ${file.path}:`, error);
|
package/dist/process/template.js
CHANGED
|
@@ -102,12 +102,11 @@ const spiderStaticSiteGenerator = async () => {
|
|
|
102
102
|
continue;
|
|
103
103
|
for (const lang of metadata_1.MetaData.options.langs || []) {
|
|
104
104
|
const markdown = await fs.readFile(path.join(paths_1.CZON_SRC_DIR, lang, file.path), 'utf-8');
|
|
105
|
-
const
|
|
105
|
+
const body = (0, frontmatter_1.stripFrontmatter)(markdown);
|
|
106
106
|
const article = {
|
|
107
107
|
lang,
|
|
108
108
|
file,
|
|
109
109
|
body: '',
|
|
110
|
-
frontmatter,
|
|
111
110
|
headings: [],
|
|
112
111
|
};
|
|
113
112
|
(0, convertMarkdownToHtml_1.convertMarkdownToHtml)(article, file.path, lang, body);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processTranslateMetadata = processTranslateMetadata;
|
|
4
|
+
const metadata_1 = require("../metadata");
|
|
5
|
+
const openai_1 = require("../services/openai");
|
|
6
|
+
const languages_1 = require("../languages");
|
|
7
|
+
const sha256_1 = require("../utils/sha256");
|
|
8
|
+
/**
|
|
9
|
+
* 计算 metadata 中需要翻译字段的哈希值
|
|
10
|
+
* 用于增量检测
|
|
11
|
+
*/
|
|
12
|
+
function computeTranslatableFieldsHash(metadata) {
|
|
13
|
+
const translatableFields = {
|
|
14
|
+
title: metadata.title,
|
|
15
|
+
description: metadata.description,
|
|
16
|
+
summary: metadata.summary,
|
|
17
|
+
short_summary: metadata.short_summary,
|
|
18
|
+
key_points: metadata.key_points,
|
|
19
|
+
audience: metadata.audience,
|
|
20
|
+
tags: metadata.tags,
|
|
21
|
+
};
|
|
22
|
+
return (0, sha256_1.sha256)(JSON.stringify(translatableFields));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 检查单个文件的单个语言是否需要翻译
|
|
26
|
+
*/
|
|
27
|
+
function needsTranslationForLang(file, lang) {
|
|
28
|
+
if (!file.metadata)
|
|
29
|
+
return false;
|
|
30
|
+
// 没有该语言的翻译 -> 需要翻译
|
|
31
|
+
if (!file.metadataTranslations?.[lang])
|
|
32
|
+
return true;
|
|
33
|
+
// hash 变化 -> 源 metadata 已更新,需要重新翻译
|
|
34
|
+
const currentHash = computeTranslatableFieldsHash(file.metadata);
|
|
35
|
+
return file.metadataTranslationHash !== currentHash;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 翻译单个文件的 metadata 到单个目标语言
|
|
39
|
+
*/
|
|
40
|
+
async function translateFileMetadataToLang(file, targetLang) {
|
|
41
|
+
const { metadata } = file;
|
|
42
|
+
if (!metadata)
|
|
43
|
+
return;
|
|
44
|
+
// 提取需要翻译的字段
|
|
45
|
+
const sourceMetadata = {
|
|
46
|
+
title: metadata.title,
|
|
47
|
+
description: metadata.description,
|
|
48
|
+
summary: metadata.summary,
|
|
49
|
+
short_summary: metadata.short_summary,
|
|
50
|
+
key_points: metadata.key_points,
|
|
51
|
+
audience: metadata.audience,
|
|
52
|
+
tags: metadata.tags,
|
|
53
|
+
};
|
|
54
|
+
const sourceLangName = languages_1.LANGUAGE_NAMES[metadata.inferred_lang] || metadata.inferred_lang;
|
|
55
|
+
const targetLangName = languages_1.LANGUAGE_NAMES[targetLang] || targetLang;
|
|
56
|
+
const response = await (0, openai_1.completeMessages)([
|
|
57
|
+
{
|
|
58
|
+
role: 'system',
|
|
59
|
+
content: [
|
|
60
|
+
'你是专业的多语言翻译助手,擅长准确翻译技术文档的元数据。',
|
|
61
|
+
'',
|
|
62
|
+
'任务:将给定的 metadata JSON 翻译成目标语言。',
|
|
63
|
+
'',
|
|
64
|
+
'要求:',
|
|
65
|
+
'1. 保持 JSON 结构完全不变,只翻译文本内容',
|
|
66
|
+
'2. title 翻译要简洁明了,不超过 30 个字',
|
|
67
|
+
'3. description 和 summary 翻译要准确传达原意',
|
|
68
|
+
'4. tags 翻译要符合目标语言的常用术语',
|
|
69
|
+
'5. key_points 数组中的每个要点都需要翻译',
|
|
70
|
+
'6. 技术术语保持准确,必要时可保留原文',
|
|
71
|
+
'',
|
|
72
|
+
'直接返回翻译后的 JSON 对象,格式如下:',
|
|
73
|
+
'{',
|
|
74
|
+
' "title": "...",',
|
|
75
|
+
' "description": "...",',
|
|
76
|
+
' "summary": "...",',
|
|
77
|
+
' "short_summary": "...",',
|
|
78
|
+
' "key_points": ["...", "..."],',
|
|
79
|
+
' "audience": "...",',
|
|
80
|
+
' "tags": ["...", "..."]',
|
|
81
|
+
'}',
|
|
82
|
+
].join('\n'),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: [
|
|
87
|
+
`源语言: ${metadata.inferred_lang} (${sourceLangName})`,
|
|
88
|
+
`目标语言: ${targetLang} (${targetLangName})`,
|
|
89
|
+
'',
|
|
90
|
+
'待翻译的 metadata:',
|
|
91
|
+
JSON.stringify(sourceMetadata, null, 2),
|
|
92
|
+
].join('\n'),
|
|
93
|
+
},
|
|
94
|
+
], {
|
|
95
|
+
response_format: { type: 'json_object' },
|
|
96
|
+
task_id: `translate-metadata:${file.path}:${targetLang}`,
|
|
97
|
+
});
|
|
98
|
+
const translated = JSON.parse(response.choices[0].message.content);
|
|
99
|
+
// 初始化存储结构
|
|
100
|
+
if (!file.metadataTranslations) {
|
|
101
|
+
file.metadataTranslations = {};
|
|
102
|
+
}
|
|
103
|
+
// 验证并存储翻译结果
|
|
104
|
+
if (translated && translated.title) {
|
|
105
|
+
file.metadataTranslations[targetLang] = {
|
|
106
|
+
title: translated.title?.trim() || '',
|
|
107
|
+
description: translated.description?.trim() || '',
|
|
108
|
+
summary: translated.summary?.trim() || '',
|
|
109
|
+
short_summary: translated.short_summary?.trim() || '',
|
|
110
|
+
key_points: Array.isArray(translated.key_points)
|
|
111
|
+
? translated.key_points.map((p) => p?.trim()).filter(Boolean)
|
|
112
|
+
: [],
|
|
113
|
+
audience: translated.audience?.trim() || '',
|
|
114
|
+
tags: Array.isArray(translated.tags)
|
|
115
|
+
? translated.tags.map((t) => t?.trim()).filter(Boolean)
|
|
116
|
+
: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 处理所有文件的 metadata 翻译
|
|
122
|
+
* 采用逐文件逐语言的翻译策略,避免输出 JSON 过长被截断
|
|
123
|
+
*/
|
|
124
|
+
async function processTranslateMetadata() {
|
|
125
|
+
const langs = metadata_1.MetaData.options.langs || [];
|
|
126
|
+
if (langs.length === 0) {
|
|
127
|
+
console.info('ℹ️ No target languages configured, skipping metadata translation.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// 收集所有需要翻译的任务 (file, lang) 组合
|
|
131
|
+
const tasks = [];
|
|
132
|
+
for (const file of metadata_1.MetaData.files) {
|
|
133
|
+
if (!file.path.endsWith('.md') || !file.metadata)
|
|
134
|
+
continue;
|
|
135
|
+
for (const lang of langs) {
|
|
136
|
+
if (needsTranslationForLang(file, lang)) {
|
|
137
|
+
tasks.push({ file, lang });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (tasks.length === 0) {
|
|
142
|
+
console.info('ℹ️ All metadata already translated, skipping.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const fileCount = new Set(tasks.map(t => t.file.path)).size;
|
|
146
|
+
console.info(`🌐 Translating metadata: ${tasks.length} tasks (${fileCount} files × up to ${langs.length} languages)...`);
|
|
147
|
+
// 并行执行所有翻译任务
|
|
148
|
+
const results = await Promise.allSettled(tasks.map(async ({ file, lang }) => {
|
|
149
|
+
await translateFileMetadataToLang(file, lang);
|
|
150
|
+
console.info(`✅ Translated metadata: ${file.path} -> ${lang}`);
|
|
151
|
+
}));
|
|
152
|
+
// 统计结果
|
|
153
|
+
const succeeded = results.filter(r => r.status === 'fulfilled').length;
|
|
154
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
155
|
+
if (failed > 0) {
|
|
156
|
+
console.warn(`⚠️ ${failed} translation tasks failed:`);
|
|
157
|
+
results.forEach((r, i) => {
|
|
158
|
+
if (r.status === 'rejected') {
|
|
159
|
+
console.error(` ❌ ${tasks[i].file.path} -> ${tasks[i].lang}:`, r.reason?.message || r.reason);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// 更新所有成功翻译文件的 hash
|
|
164
|
+
const translatedFiles = new Set(tasks.filter((_, i) => results[i].status === 'fulfilled').map(t => t.file));
|
|
165
|
+
for (const file of translatedFiles) {
|
|
166
|
+
if (file.metadata) {
|
|
167
|
+
file.metadataTranslationHash = computeTranslatableFieldsHash(file.metadata);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
console.info(`✅ Metadata translation completed: ${succeeded}/${tasks.length} succeeded`);
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=translateMetadata.js.map
|
package/dist/ssg/ContentPage.js
CHANGED
|
@@ -15,12 +15,13 @@ const Navigator_1 = require("./components/Navigator");
|
|
|
15
15
|
const PageLayout_1 = require("./layouts/PageLayout");
|
|
16
16
|
const resourceMap_1 = require("./resourceMap");
|
|
17
17
|
const style_1 = require("./style");
|
|
18
|
+
const getLocalizedMetadata_1 = require("./utils/getLocalizedMetadata");
|
|
18
19
|
const ContentPage = props => {
|
|
19
|
-
const
|
|
20
|
-
const title =
|
|
21
|
-
const summary =
|
|
22
|
-
const date =
|
|
23
|
-
const tags =
|
|
20
|
+
const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
|
|
21
|
+
const title = metadata?.title || '(no title)';
|
|
22
|
+
const summary = metadata?.summary || '';
|
|
23
|
+
const date = props.file.metadata?.inferred_date || '--';
|
|
24
|
+
const tags = metadata?.tags || [];
|
|
24
25
|
const category = props.file.category;
|
|
25
26
|
const relatedContents = props.ctx.site.files.filter(f => f.category === category && f.path !== props.file.path);
|
|
26
27
|
// 查找指向当前文章的其他文章
|
|
@@ -34,7 +35,10 @@ const ContentPage = props => {
|
|
|
34
35
|
react_1.default.createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
35
36
|
react_1.default.createElement("title", null, title),
|
|
36
37
|
react_1.default.createElement("link", { rel: "icon", href: faviconUrl, type: "image/x-icon" }),
|
|
37
|
-
|
|
38
|
+
// 使用 short_summary 作为 description,有利于 SEO 和社交分享预览
|
|
39
|
+
metadata?.short_summary && (react_1.default.createElement("meta", { name: "description", content: metadata?.short_summary.slice(0, 150) })),
|
|
40
|
+
// Keywords meta tag for SEO (using tags, if available)
|
|
41
|
+
tags.length > 0 && react_1.default.createElement("meta", { name: "keywords", content: tags.join(', ') }),
|
|
38
42
|
react_1.default.createElement(Analytics_1.Analytics, { ctx: props.ctx }),
|
|
39
43
|
react_1.default.createElement("script", { src: (0, resourceMap_1.getResourceUrlFrom)(props.ctx.path, 'tailwindcss.js') }),
|
|
40
44
|
react_1.default.createElement("style", null, style_1.style),
|
|
@@ -69,16 +73,16 @@ const ContentPage = props => {
|
|
|
69
73
|
relatedContents.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
70
74
|
react_1.default.createElement("h2", null, "See Also"),
|
|
71
75
|
react_1.default.createElement("ul", null, relatedContents.map(f => {
|
|
72
|
-
const
|
|
76
|
+
const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(f, props.lang);
|
|
73
77
|
return (react_1.default.createElement("li", { key: f.path },
|
|
74
|
-
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` },
|
|
78
|
+
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theMetadata?.title || '(no title)')));
|
|
75
79
|
})))),
|
|
76
80
|
referencedFiles.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
77
81
|
react_1.default.createElement("h2", null, "Referenced By"),
|
|
78
82
|
react_1.default.createElement("ul", null, referencedFiles.map(f => {
|
|
79
|
-
const
|
|
83
|
+
const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(f, props.lang);
|
|
80
84
|
return (react_1.default.createElement("li", { key: f.path },
|
|
81
|
-
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` },
|
|
85
|
+
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theMetadata?.title || '(no title)')));
|
|
82
86
|
}))))),
|
|
83
87
|
react_1.default.createElement("footer", { className: "footer" },
|
|
84
88
|
react_1.default.createElement(LanguageSwitcher_1.LanguageSwitcher, { ctx: props.ctx, lang: props.lang, file: props.file }),
|
|
@@ -6,21 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ContentMeta = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
|
|
9
|
+
const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
|
|
9
10
|
const TagList_1 = require("./TagList");
|
|
10
11
|
const ContentMeta = props => {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
12
|
+
const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
|
|
13
|
+
const title = metadata?.title || '(no title)';
|
|
14
|
+
const summary = metadata?.summary || '';
|
|
15
|
+
const audience = metadata?.audience || '';
|
|
16
|
+
const keyPoints = metadata?.key_points || [];
|
|
17
|
+
const date = props.file.metadata?.inferred_date || '--';
|
|
18
|
+
const tags = metadata?.tags || [];
|
|
17
19
|
const category = props.file.category;
|
|
18
20
|
return (react_1.default.createElement("header", { className: "content-header mb-4 pb-2 border-b" },
|
|
19
21
|
react_1.default.createElement("h2", { className: "text-2xl font-bold mb-2" },
|
|
20
22
|
react_1.default.createElement("a", { href: `${props.file.metadata?.slug}.html` }, title)),
|
|
21
|
-
react_1.default.createElement("p", { className: "font-semibold" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
|
|
23
|
+
react_1.default.createElement("p", { className: "font-semibold mb-2" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
|
|
24
|
+
audience && react_1.default.createElement("div", null,
|
|
25
|
+
"\uD83D\uDC64 ",
|
|
26
|
+
audience),
|
|
22
27
|
react_1.default.createElement("blockquote", null, summary),
|
|
23
|
-
react_1.default.createElement("
|
|
28
|
+
keyPoints.length > 0 && (react_1.default.createElement("ul", { className: "key-points list-inside my-2" }, keyPoints.slice(0, 5).map((point, index) => (react_1.default.createElement("li", { key: index },
|
|
29
|
+
"\u2728 ",
|
|
30
|
+
point))))),
|
|
31
|
+
date && date !== '--' && react_1.default.createElement("div", null,
|
|
24
32
|
"\uD83D\uDCC5 ",
|
|
25
33
|
date),
|
|
26
34
|
react_1.default.createElement("div", { className: "tags" },
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.Navigator = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
|
|
9
|
+
const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
|
|
9
10
|
const sortBy_1 = require("../../utils/sortBy");
|
|
10
11
|
const Navigator = props => {
|
|
11
12
|
const categories = [...new Set(props.ctx.site.files.map(f => f.category))];
|
|
@@ -24,8 +25,8 @@ const Navigator = props => {
|
|
|
24
25
|
filesInCategory.map(file => {
|
|
25
26
|
const link = file.metadata.slug + '.html';
|
|
26
27
|
const isActive = props.file === file;
|
|
27
|
-
const
|
|
28
|
-
const theTitle =
|
|
28
|
+
const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(file, props.lang);
|
|
29
|
+
const theTitle = theMetadata?.title || '(no title)';
|
|
29
30
|
return (react_1.default.createElement("li", { className: "nav-item", key: file.path },
|
|
30
31
|
react_1.default.createElement("a", { href: link, className: `nav-link ${isActive ? 'active' : ''}` },
|
|
31
32
|
theTitle,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLocalizedMetadata = getLocalizedMetadata;
|
|
4
|
+
/**
|
|
5
|
+
* 获取文件在指定语言下的 metadata
|
|
6
|
+
* - 如果是原语言,返回 file.metadata
|
|
7
|
+
* - 如果是翻译语言,返回 file.metadataTranslations[lang]
|
|
8
|
+
*/
|
|
9
|
+
function getLocalizedMetadata(file, lang) {
|
|
10
|
+
const metadata = file.metadata;
|
|
11
|
+
if (!metadata)
|
|
12
|
+
return undefined;
|
|
13
|
+
// 如果是原语言,直接返回原始 metadata
|
|
14
|
+
if (lang === metadata.inferred_lang) {
|
|
15
|
+
return metadata;
|
|
16
|
+
}
|
|
17
|
+
// 否则返回翻译版本
|
|
18
|
+
return file.metadataTranslations?.[lang];
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=getLocalizedMetadata.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.stripFrontmatter = exports.parseFrontmatter = void 0;
|
|
4
4
|
const yaml_1 = require("yaml");
|
|
5
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
5
6
|
const parseFrontmatter = (content) => {
|
|
6
|
-
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
7
7
|
const match = content.match(frontmatterRegex);
|
|
8
8
|
if (match) {
|
|
9
9
|
const frontmatterContent = match[1];
|
|
@@ -13,10 +13,15 @@ const parseFrontmatter = (content) => {
|
|
|
13
13
|
return { frontmatter: {}, body: content };
|
|
14
14
|
};
|
|
15
15
|
exports.parseFrontmatter = parseFrontmatter;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 移除 Markdown 内容中的 YAML FrontMatter,只返回正文
|
|
18
|
+
*/
|
|
19
|
+
const stripFrontmatter = (content) => {
|
|
20
|
+
const match = content.match(frontmatterRegex);
|
|
21
|
+
if (match) {
|
|
22
|
+
return content.slice(match[0].length).trim();
|
|
23
|
+
}
|
|
24
|
+
return content;
|
|
20
25
|
};
|
|
21
|
-
exports.
|
|
26
|
+
exports.stripFrontmatter = stripFrontmatter;
|
|
22
27
|
//# sourceMappingURL=frontmatter.js.map
|
package/package.json
CHANGED
package/prompts/summary-base.md
CHANGED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: article-summarizer
|
|
3
|
-
description: 'Guide for generating multi-style article summaries from Markdown files in a repository. Use when user requests article summarization, multi-perspective analysis, or content summarization with different writing styles. Triggers on keywords: article summary, markdown summary, multi-style summary, SUMMARY directory, git ls-files, 7 styles.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 文章总结器
|
|
7
|
-
|
|
8
|
-
## 概述
|
|
9
|
-
|
|
10
|
-
本skill指导代理从仓库中的Markdown文章生成全面的多风格摘要。它提供一个结构化工作流程:清空SUMMARY目录、通过git命令识别目标Markdown文件、读取所有.md文件(排除.czon)、并以7种不同写作风格生成摘要,附带正确的引用规则。
|
|
11
|
-
|
|
12
|
-
## 工作流程
|
|
13
|
-
|
|
14
|
-
### 1. 准备SUMMARY目录
|
|
15
|
-
|
|
16
|
-
首先,确保SUMMARY目录存在并清空之前的内容:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
mkdir -p SUMMARY
|
|
20
|
-
rm -rf SUMMARY/*
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### 2. 识别目标Markdown文件
|
|
24
|
-
|
|
25
|
-
使用以下git命令列出仓库中的所有Markdown文件(已跟踪和未跟踪),排除.czon目录中的文件:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
git ls-files --others --cached --exclude-standard -z -x ".czon" | tr '\0' '\n' | grep -v ".czon" | grep ".md$"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
将输出捕获为文件路径列表。验证每个文件存在且可读。
|
|
32
|
-
|
|
33
|
-
### 3. 读取和处理Markdown内容
|
|
34
|
-
|
|
35
|
-
对于每个Markdown文件:
|
|
36
|
-
|
|
37
|
-
- 使用`Read`工具读取完整内容
|
|
38
|
-
- 提取元数据:标题(第一个H1)、修改日期(git log)、字数统计
|
|
39
|
-
- 解析结构:标题、链接、代码块
|
|
40
|
-
- 注意指向其他Markdown文件的内部链接
|
|
41
|
-
|
|
42
|
-
### 4. 生成多风格摘要
|
|
43
|
-
|
|
44
|
-
在`SUMMARY/`目录中为7种风格创建单独的摘要文件。每个摘要必须严格基于Markdown文件的事实内容,不得虚构。
|
|
45
|
-
|
|
46
|
-
**风格指南:**
|
|
47
|
-
|
|
48
|
-
1. **客观中立风格**(基于事实,保持简洁,就像名片和履历一样)
|
|
49
|
-
- 呈现关键事实、日期、角色、成就
|
|
50
|
-
- 避免主观语言;坚持可验证的信息
|
|
51
|
-
- 格式:项目符号或简短段落
|
|
52
|
-
|
|
53
|
-
2. **客观批判风格**(基于事实的批判,反思视角)
|
|
54
|
-
- 基于证据识别优势和劣势
|
|
55
|
-
- 提出改进领域或替代方法
|
|
56
|
-
- 保持专业语气;避免人身攻击
|
|
57
|
-
|
|
58
|
-
3. **赞扬鼓励风格**(基于事实的积极强化)
|
|
59
|
-
- 突出成功、创新元素、有价值的贡献
|
|
60
|
-
- 以建设性的方式构建为"哪些做得好以及为什么"
|
|
61
|
-
- 为未来工作提供激励视角
|
|
62
|
-
|
|
63
|
-
4. **幽默调侃风格**(轻松愉快,面向普通受众)
|
|
64
|
-
- 用通俗语言解释复杂概念("说人话")
|
|
65
|
-
- 使用温和的幽默、类比、相关例子
|
|
66
|
-
- 跳过过于技术性或敏感、可能被误解的内容
|
|
67
|
-
- 目标:引发会心一笑,而非嘲笑
|
|
68
|
-
|
|
69
|
-
5. **文艺感性风格**(叙事性,引发共鸣,故事驱动)
|
|
70
|
-
- 创造生动的意象,情感共鸣
|
|
71
|
-
- 将事实编织成引人入胜的叙事弧线
|
|
72
|
-
- 旨在激发共情和连接
|
|
73
|
-
|
|
74
|
-
6. **心理分析风格**(例如,基于证据的MBTI分析)
|
|
75
|
-
- 分配心理特质(如INTJ、ENFP),并提供明确推理
|
|
76
|
-
- 为每个特质维度提供内容中的具体例子
|
|
77
|
-
- 确保分析基于可观察的模式
|
|
78
|
-
|
|
79
|
-
7. **历史时间跨度风格**(按时间发展视角)
|
|
80
|
-
- 沿时间线组织内容
|
|
81
|
-
- 展示演变、转折点、随时间趋势
|
|
82
|
-
- 在更广泛的历史或项目叙事中情境化
|
|
83
|
-
|
|
84
|
-
### 5. 输出格式规则
|
|
85
|
-
|
|
86
|
-
- **文件命名**: `SUMMARY/neutral.md`, `SUMMARY/critical.md`, `SUMMARY/praise.md`, `SUMMARY/humorous.md`, `SUMMARY/literary.md`, `SUMMARY/psychological.md`, `SUMMARY/historical.md`
|
|
87
|
-
- **头部**: 每个文件必须以以下内容开头:
|
|
88
|
-
```
|
|
89
|
-
# [风格名称] 摘要
|
|
90
|
-
*由AI生成于YYYY-MM-DD HH:MM UTC*
|
|
91
|
-
*内容为AI生成,仅供参考*
|
|
92
|
-
```
|
|
93
|
-
- **引用链接**: 当引用源Markdown文件时:
|
|
94
|
-
- 使用相对路径链接到具体文件:`../path/to/file.md`
|
|
95
|
-
- 锚文本应该是文章的标题(第一个H1),而不是文件名
|
|
96
|
-
- 确保链接有效(如有需要,使用`Read`工具测试)
|
|
97
|
-
- 永远不要链接到目录——始终链接到具体的.md文件
|
|
98
|
-
- **权重分配**: 给予最近修改的文章更高的权重(使用git时间戳)
|
|
99
|
-
- **事实核查**: 跨文件交叉引用以确保一致性
|
|
100
|
-
|
|
101
|
-
### 6. 质量保证
|
|
102
|
-
|
|
103
|
-
生成所有摘要后:
|
|
104
|
-
|
|
105
|
-
- 验证没有摘要与事实源材料相矛盾
|
|
106
|
-
- 检查所有内部链接是否正确解析
|
|
107
|
-
- 确保每种风格保持其独特的语调
|
|
108
|
-
- 确认SUMMARY目录恰好包含7个文件
|
|
109
|
-
|
|
110
|
-
## 常见陷阱
|
|
111
|
-
|
|
112
|
-
- **虚构**: 绝不添加源Markdown文件中不存在的信息
|
|
113
|
-
- **链接错误**: 从SUMMARY目录链接时始终使用`../`前缀
|
|
114
|
-
- **风格混杂**: 保持每个摘要严格在其指定的语调范围内
|
|
115
|
-
- **新近度偏差**: 虽然近期文章获得更高权重,但不要忽略较旧的有价值内容
|
|
116
|
-
- **过度解读**: 在心理分析中,仅基于明确的证据得出结论
|
|
117
|
-
|
|
118
|
-
## 示例用户请求
|
|
119
|
-
|
|
120
|
-
- "以7种不同风格总结所有Markdown文章"
|
|
121
|
-
- "生成我们文档的多视角分析"
|
|
122
|
-
- "为我们的知识库创建文章摘要"
|
|
123
|
-
- "生成我们内容的幽默和严肃版本"
|