zengen 0.1.36 → 0.2.1
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/.github/workflows/pages.yml +1 -1
- package/.zen/meta.json +112 -32
- package/.zen/src/en-US/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
- package/.zen/src/en-US/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +75 -0
- package/.zen/src/en-US/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
- package/.zen/src/en-US/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +53 -0
- package/.zen/src/en-US/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
- package/.zen/src/en-US/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +61 -0
- package/.zen/src/en-US/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +40 -0
- package/.zen/src/en-US/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
- package/.zen/src/zh-Hans/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
- package/.zen/src/zh-Hans/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +77 -0
- package/.zen/src/zh-Hans/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
- package/.zen/src/zh-Hans/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +55 -0
- package/.zen/src/zh-Hans/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
- package/.zen/src/zh-Hans/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +63 -0
- package/.zen/src/zh-Hans/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +40 -0
- package/.zen/src/zh-Hans/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
- package/assets/templates/default/layout.html +274 -0
- package/dist/ai/extractMetadataFromMarkdown.d.ts +8 -0
- package/dist/ai/extractMetadataFromMarkdown.d.ts.map +1 -0
- package/dist/ai/extractMetadataFromMarkdown.js +88 -0
- package/dist/ai/extractMetadataFromMarkdown.js.map +1 -0
- package/dist/ai/translateMarkdown.d.ts +8 -0
- package/dist/ai/translateMarkdown.d.ts.map +1 -0
- package/dist/ai/translateMarkdown.js +29 -0
- package/dist/ai/translateMarkdown.js.map +1 -0
- package/dist/build/pipeline.d.ts +6 -0
- package/dist/build/pipeline.d.ts.map +1 -0
- package/dist/build/pipeline.js +219 -0
- package/dist/build/pipeline.js.map +1 -0
- package/dist/cli.js +10 -118
- package/dist/cli.js.map +1 -1
- package/dist/findEntries.d.ts +10 -0
- package/dist/findEntries.d.ts.map +1 -0
- package/dist/findEntries.js +38 -0
- package/dist/findEntries.js.map +1 -0
- package/dist/index.d.ts +1 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -35
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +14 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +78 -0
- package/dist/metadata.js.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -0
- package/dist/process/extractMetadataByAI.d.ts +5 -0
- package/dist/process/extractMetadataByAI.d.ts.map +1 -0
- package/dist/process/extractMetadataByAI.js +31 -0
- package/dist/process/extractMetadataByAI.js.map +1 -0
- package/dist/process/template.d.ts +5 -0
- package/dist/process/template.d.ts.map +1 -0
- package/dist/process/template.js +188 -0
- package/dist/process/template.js.map +1 -0
- package/dist/scan/files.d.ts +7 -0
- package/dist/scan/files.d.ts.map +1 -0
- package/dist/scan/files.js +54 -0
- package/dist/scan/files.js.map +1 -0
- package/dist/services/openai.d.ts +41 -0
- package/dist/services/openai.d.ts.map +1 -0
- package/dist/services/openai.js +54 -0
- package/dist/services/openai.js.map +1 -0
- package/dist/types.d.ts +16 -67
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/convertMarkdownToHtml.d.ts +7 -0
- package/dist/utils/convertMarkdownToHtml.d.ts.map +1 -0
- package/dist/utils/convertMarkdownToHtml.js +39 -0
- package/dist/utils/convertMarkdownToHtml.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +6 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +22 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/docs/deployment/github-pages.md +1 -2
- package/docs/guides/best-practices.md +4 -4
- package/docs/guides/config.md +0 -5
- package/package.json +4 -2
- package/src/ai/extractMetadataFromMarkdown.ts +95 -0
- package/src/ai/translateMarkdown.ts +29 -0
- package/src/build/pipeline.ts +217 -0
- package/src/cli.ts +10 -132
- package/src/findEntries.ts +37 -0
- package/src/index.ts +1 -40
- package/src/metadata.ts +44 -0
- package/src/paths.ts +7 -0
- package/src/process/extractMetadataByAI.ts +31 -0
- package/src/process/template.ts +201 -0
- package/src/scan/files.ts +17 -0
- package/src/services/openai.ts +92 -0
- package/src/types.ts +18 -72
- package/src/utils/convertMarkdownToHtml.ts +32 -0
- package/src/utils/frontmatter.ts +18 -0
- package/.zen/translations.json +0 -51
- package/dist/ai-client.d.ts +0 -34
- package/dist/ai-client.d.ts.map +0 -1
- package/dist/ai-client.js +0 -180
- package/dist/ai-client.js.map +0 -1
- package/dist/ai-processor.d.ts +0 -51
- package/dist/ai-processor.d.ts.map +0 -1
- package/dist/ai-processor.js +0 -215
- package/dist/ai-processor.js.map +0 -1
- package/dist/ai-service.d.ts +0 -79
- package/dist/ai-service.d.ts.map +0 -1
- package/dist/ai-service.js +0 -257
- package/dist/ai-service.js.map +0 -1
- package/dist/builder.d.ts +0 -70
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -854
- package/dist/builder.js.map +0 -1
- package/dist/gitignore.d.ts +0 -41
- package/dist/gitignore.d.ts.map +0 -1
- package/dist/gitignore.js +0 -202
- package/dist/gitignore.js.map +0 -1
- package/dist/gitignore.test.d.ts +0 -2
- package/dist/gitignore.test.d.ts.map +0 -1
- package/dist/gitignore.test.js +0 -309
- package/dist/gitignore.test.js.map +0 -1
- package/dist/markdown.d.ts +0 -35
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js +0 -221
- package/dist/markdown.js.map +0 -1
- package/dist/navigation.d.ts +0 -46
- package/dist/navigation.d.ts.map +0 -1
- package/dist/navigation.js +0 -196
- package/dist/navigation.js.map +0 -1
- package/dist/scanner.d.ts +0 -26
- package/dist/scanner.d.ts.map +0 -1
- package/dist/scanner.js +0 -190
- package/dist/scanner.js.map +0 -1
- package/dist/template.d.ts +0 -33
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -434
- package/dist/template.js.map +0 -1
- package/dist/translation-service.d.ts +0 -72
- package/dist/translation-service.d.ts.map +0 -1
- package/dist/translation-service.js +0 -291
- package/dist/translation-service.js.map +0 -1
- package/src/ai-client.ts +0 -227
- package/src/ai-processor.ts +0 -243
- package/src/ai-service.ts +0 -281
- package/src/builder.ts +0 -991
- package/src/gitignore.test.ts +0 -318
- package/src/gitignore.ts +0 -193
- package/src/markdown.ts +0 -212
- package/src/navigation.ts +0 -237
- package/src/scanner.ts +0 -180
- package/src/template.ts +0 -425
- package/src/translation-service.ts +0 -350
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateFrontmatter = exports.parseFrontmatter = void 0;
|
|
4
|
+
const yaml_1 = require("yaml");
|
|
5
|
+
const parseFrontmatter = (content) => {
|
|
6
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
7
|
+
const match = content.match(frontmatterRegex);
|
|
8
|
+
if (match) {
|
|
9
|
+
const frontmatterContent = match[1];
|
|
10
|
+
const body = content.slice(match[0].length).trim();
|
|
11
|
+
return { frontmatter: (0, yaml_1.parse)(frontmatterContent.trim()), body };
|
|
12
|
+
}
|
|
13
|
+
return { frontmatter: {}, body: content };
|
|
14
|
+
};
|
|
15
|
+
exports.parseFrontmatter = parseFrontmatter;
|
|
16
|
+
const updateFrontmatter = (content, newFrontmatter) => {
|
|
17
|
+
const { body } = (0, exports.parseFrontmatter)(content);
|
|
18
|
+
const frontmatterContent = `---\n${(0, yaml_1.stringify)(newFrontmatter)}---\n\n`;
|
|
19
|
+
return frontmatterContent + body;
|
|
20
|
+
};
|
|
21
|
+
exports.updateFrontmatter = updateFrontmatter;
|
|
22
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/utils/frontmatter.ts"],"names":[],"mappings":";;;AAAA,+BAAwC;AAEjC,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAAsC,EAAE;IACtF,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAC9C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,kBAAkB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACnD,OAAO,EAAE,WAAW,EAAE,IAAA,YAAK,EAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC5C,CAAC,CAAC;AATW,QAAA,gBAAgB,oBAS3B;AAEK,MAAM,iBAAiB,GAAG,CAAC,OAAe,EAAE,cAAmB,EAAU,EAAE;IAChF,MAAM,EAAE,IAAI,EAAE,GAAG,IAAA,wBAAgB,EAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,kBAAkB,GAAG,QAAQ,IAAA,gBAAS,EAAC,cAAc,CAAC,SAAS,CAAC;IACtE,OAAO,kBAAkB,GAAG,IAAI,CAAC;AACnC,CAAC,CAAC;AAJW,QAAA,iBAAiB,qBAI5B"}
|
|
@@ -52,12 +52,11 @@ echo "docs.example.com" > docs-dist/CNAME
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
cd demo/src
|
|
55
|
-
zengen build --
|
|
55
|
+
zengen build --verbose
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
可用的选项:
|
|
59
59
|
|
|
60
|
-
- `--clean`:在构建前清理输出目录
|
|
61
60
|
- `--verbose`:显示详细输出
|
|
62
61
|
- `--watch`:监听模式(不适用于 CI/CD)
|
|
63
62
|
- `--template`:指定自定义模板文件
|
|
@@ -30,7 +30,7 @@ npx zengen build --watch
|
|
|
30
30
|
npx zengen build --watch --serve
|
|
31
31
|
|
|
32
32
|
# 生产构建
|
|
33
|
-
npx zengen build
|
|
33
|
+
npx zengen build
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## 部署策略
|
|
@@ -63,7 +63,7 @@ jobs:
|
|
|
63
63
|
- name: Build documentation
|
|
64
64
|
run: |
|
|
65
65
|
cd docs
|
|
66
|
-
npx zengen build --
|
|
66
|
+
npx zengen build --base-url /my-docs
|
|
67
67
|
|
|
68
68
|
- name: Deploy to GitHub Pages
|
|
69
69
|
uses: peaceiris/actions-gh-pages@v3
|
|
@@ -81,8 +81,8 @@ jobs:
|
|
|
81
81
|
# 切换到文档目录
|
|
82
82
|
cd docs
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
npx zengen build
|
|
84
|
+
# 构建文档
|
|
85
|
+
npx zengen build
|
|
86
86
|
|
|
87
87
|
# 同步到服务器
|
|
88
88
|
rsync -avz .zen/dist/ user@server:/var/www/docs/
|
package/docs/guides/config.md
CHANGED
|
@@ -19,9 +19,6 @@ npx zengen build --watch --serve
|
|
|
19
19
|
# 自定义端口
|
|
20
20
|
npx zengen build --watch --serve --port 8080
|
|
21
21
|
|
|
22
|
-
# 清理输出目录
|
|
23
|
-
npx zengen build --clean
|
|
24
|
-
|
|
25
22
|
# 显示详细日志
|
|
26
23
|
npx zengen build --verbose
|
|
27
24
|
|
|
@@ -47,7 +44,5 @@ npx zengen
|
|
|
47
44
|
| `--port` | `-p` | 开发服务器端口 | `3000` |
|
|
48
45
|
| `--host` | | 开发服务器主机 | `localhost` |
|
|
49
46
|
| `--verbose` | `-v` | 显示详细日志 | `false` |
|
|
50
|
-
| `--clean` | | 清理输出目录 | `false` |
|
|
51
47
|
| `--base-url` | | 站点基础 URL | 无 |
|
|
52
48
|
| `--help` | `-h` | 显示帮助信息 | 无 |
|
|
53
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zengen",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "ZEN - A minimalist Markdown documentation site builder",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "npx rimraf dist && tsc",
|
|
12
12
|
"dev": "ts-node src/cli.ts",
|
|
13
|
+
"build:doc": "npm run build && node dist/cli.js build --lang zh-Hans --lang en-US --verbose",
|
|
13
14
|
"test": "npm run build && node --test dist/**/*.test.js",
|
|
14
15
|
"test:types": "tsc --noEmit",
|
|
15
16
|
"test:build": "npm run build && test -f dist/index.js && test -f dist/cli.js",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"express": "^4.21.2",
|
|
59
60
|
"highlight.js": "^11.11.1",
|
|
60
61
|
"marked": "^17.0.1",
|
|
61
|
-
"minimatch": "^10.1.1"
|
|
62
|
+
"minimatch": "^10.1.1",
|
|
63
|
+
"yaml": "^2.8.2"
|
|
62
64
|
}
|
|
63
65
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { completeMessages, OpenAIMessage } from '../services/openai';
|
|
2
|
+
import { AIMetadata } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 从 markdown 内容中提取 metadata
|
|
6
|
+
* @param content Markdown 内容
|
|
7
|
+
* @returns Promise<AIMetadata> 提取的元数据,失败时抛出错误
|
|
8
|
+
*/
|
|
9
|
+
export async function extractMetadataFromMarkdown(content: string): Promise<AIMetadata> {
|
|
10
|
+
const prompt = buildMetadataPrompt(content);
|
|
11
|
+
const messages: OpenAIMessage[] = [
|
|
12
|
+
{
|
|
13
|
+
role: 'system',
|
|
14
|
+
content:
|
|
15
|
+
'你是一个专业的文档分析助手,擅长从文档中提取结构化信息。请严格按照要求的 JSON 格式返回结果。',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
role: 'user',
|
|
19
|
+
content: prompt,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const response = await completeMessages(messages, {
|
|
24
|
+
response_format: { type: 'json_object' },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const metadata = parseMetadataResponse(response.choices[0].message.content);
|
|
28
|
+
|
|
29
|
+
// 添加 tokens 使用情况
|
|
30
|
+
metadata.tokens_used = {
|
|
31
|
+
prompt: response.usage.prompt_tokens,
|
|
32
|
+
completion: response.usage.completion_tokens,
|
|
33
|
+
total: response.usage.total_tokens,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return metadata;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 构建提取 metadata 的 prompt
|
|
41
|
+
*/
|
|
42
|
+
function buildMetadataPrompt(content: string): string {
|
|
43
|
+
// 限制内容长度以避免 token 超限
|
|
44
|
+
const maxContentLength = 8000;
|
|
45
|
+
const truncatedContent =
|
|
46
|
+
content.length > maxContentLength
|
|
47
|
+
? content.substring(0, maxContentLength) + '... [内容已截断]'
|
|
48
|
+
: content;
|
|
49
|
+
|
|
50
|
+
return `请分析以下文档内容,提取以下信息并返回 JSON 格式:
|
|
51
|
+
|
|
52
|
+
文档内容:
|
|
53
|
+
"""
|
|
54
|
+
${truncatedContent}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
请提取:
|
|
58
|
+
1. title: 文档的标题(简洁明了,不超过 20 个字)
|
|
59
|
+
2. summary: 文档摘要(控制在 100 字以内,概括主要内容)
|
|
60
|
+
3. tags: 关键词列表(3-8 个关键词,使用中文或英文)
|
|
61
|
+
4. inferred_date: 文档中隐含的创建日期(如果有的话,格式:YYYY-MM-DD,没有就留空字符串)
|
|
62
|
+
5. inferred_lang: 文档使用的语言代码(例如:zh-Hans 表示简体中文,en-US 表示美式英语)
|
|
63
|
+
|
|
64
|
+
请严格按照以下 JSON 格式返回,不要包含任何其他文本:
|
|
65
|
+
{
|
|
66
|
+
"title": "文档标题",
|
|
67
|
+
"summary": "文档摘要...",
|
|
68
|
+
"tags": ["关键词1", "关键词2", "关键词3"],
|
|
69
|
+
"inferred_date": "2023-01-01",
|
|
70
|
+
"inferred_lang": "zh-Hans"
|
|
71
|
+
}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 解析 AI 返回的 metadata
|
|
76
|
+
*/
|
|
77
|
+
function parseMetadataResponse(responseContent: string): AIMetadata {
|
|
78
|
+
try {
|
|
79
|
+
const metadata = JSON.parse(responseContent);
|
|
80
|
+
|
|
81
|
+
// 验证和清理数据
|
|
82
|
+
return {
|
|
83
|
+
title: metadata.title?.trim() || '未命名文档',
|
|
84
|
+
summary: metadata.summary?.trim() || '',
|
|
85
|
+
tags: Array.isArray(metadata.tags)
|
|
86
|
+
? metadata.tags.map((tag: string) => tag.trim()).filter(Boolean)
|
|
87
|
+
: [],
|
|
88
|
+
inferred_date: metadata.inferred_date?.trim() || undefined,
|
|
89
|
+
inferred_lang: metadata.inferred_lang?.trim() || 'zh-Hans',
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('❌ Failed to parse AI response:', error, 'Response:', responseContent);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { completeMessages, OpenAIMessage } from '../services/openai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 将 markdown 翻译为指定的语言
|
|
5
|
+
* @param content Markdown 内容
|
|
6
|
+
* @param targetLang 目标语言代码(例如:zh-Hans, en-US)
|
|
7
|
+
* @returns Promise<string> 翻译后的 Markdown 内容
|
|
8
|
+
*/
|
|
9
|
+
export async function translateMarkdown(content: string, targetLang: string): Promise<string> {
|
|
10
|
+
const messages: OpenAIMessage[] = [
|
|
11
|
+
{
|
|
12
|
+
role: 'system',
|
|
13
|
+
content: `你是一个专业的翻译助手,擅长将文档翻译成不同语言,同时保持原有的格式和结构。请将用户输入翻译成 ${targetLang},注意保持 Markdown 格式不变,链接不变,不要翻译代码,但是可以翻译代码中的注释。`,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
role: 'user',
|
|
17
|
+
content: content,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const response = await completeMessages(messages);
|
|
22
|
+
const translatedContent = response.choices[0]?.message?.content?.trim() || '';
|
|
23
|
+
|
|
24
|
+
if (!translatedContent) {
|
|
25
|
+
throw new Error('Empty translation response');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return translatedContent;
|
|
29
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { translateMarkdown } from '../ai/translateMarkdown';
|
|
4
|
+
import { findMarkdownEntries } from '../findEntries';
|
|
5
|
+
import { loadMetaData, MetaData, saveMetaData } from '../metadata';
|
|
6
|
+
import { INPUT_DIR, ZEN_DIR, ZEN_DIST_DIR, ZEN_SRC_DIR } from '../paths';
|
|
7
|
+
import { extractMetadataByAI } from '../process/extractMetadataByAI';
|
|
8
|
+
import { renderTemplates } from '../process/template';
|
|
9
|
+
import { calculateFileHash } from '../scan/files';
|
|
10
|
+
import { BuildOptions } from '../types';
|
|
11
|
+
import { updateFrontmatter } from '../utils/frontmatter';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 验证构建配置
|
|
15
|
+
*/
|
|
16
|
+
async function validateConfig(options: BuildOptions): Promise<void> {
|
|
17
|
+
const { verbose = false } = options;
|
|
18
|
+
|
|
19
|
+
if (verbose) {
|
|
20
|
+
console.log(`🚀 Starting ZEN build...`);
|
|
21
|
+
console.log(`🔗 Base URL: ${options.baseUrl || '(not set)'}`);
|
|
22
|
+
if (options.langs && options.langs.length > 0) {
|
|
23
|
+
console.log(`🌐 Target languages: ${options.langs.join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`🔍 Verbose mode enabled`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
MetaData.options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 扫描源文件
|
|
33
|
+
*/
|
|
34
|
+
async function scanSourceFiles(): Promise<void> {
|
|
35
|
+
console.log(`🔍 Scanning source directory...`);
|
|
36
|
+
const markdownFiles = await findMarkdownEntries(INPUT_DIR);
|
|
37
|
+
const hashes = new Set<string>();
|
|
38
|
+
|
|
39
|
+
for (const relativePath of markdownFiles) {
|
|
40
|
+
const fullPath = path.join(INPUT_DIR, relativePath);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// 检查文件是否存在
|
|
44
|
+
await fs.access(fullPath);
|
|
45
|
+
|
|
46
|
+
const hash = await calculateFileHash(fullPath);
|
|
47
|
+
|
|
48
|
+
hashes.add(hash);
|
|
49
|
+
|
|
50
|
+
const metaWithSameHash = MetaData.files.find(f => f.hash === hash);
|
|
51
|
+
if (metaWithSameHash) {
|
|
52
|
+
metaWithSameHash.path = relativePath;
|
|
53
|
+
} else {
|
|
54
|
+
// 如果没有相同哈希的元数据,则添加一个新的占位符
|
|
55
|
+
MetaData.files.push({
|
|
56
|
+
hash,
|
|
57
|
+
path: relativePath,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(`⚠️ File not found or inaccessible: ${fullPath}`, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 移除不再存在的文件元数据
|
|
65
|
+
MetaData.files = MetaData.files.filter(f => hashes.has(f.hash));
|
|
66
|
+
|
|
67
|
+
console.log(`✅ Found ${MetaData.files.length} Markdown files`);
|
|
68
|
+
|
|
69
|
+
if (MetaData.files.length === 0) {
|
|
70
|
+
console.warn(`⚠️ No Markdown files found in ${INPUT_DIR}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 存储母语文件到 .zen/src
|
|
76
|
+
*/
|
|
77
|
+
async function storeNativeFiles(): Promise<void> {
|
|
78
|
+
const {
|
|
79
|
+
options: { verbose },
|
|
80
|
+
files,
|
|
81
|
+
} = MetaData;
|
|
82
|
+
for (const file of MetaData.files) {
|
|
83
|
+
try {
|
|
84
|
+
if (!file.hash) throw new Error(`Missing hash`);
|
|
85
|
+
if (!file.metadata?.inferred_lang) throw new Error(`Missing inferred language`);
|
|
86
|
+
const filePath = path.join(ZEN_SRC_DIR, file.metadata.inferred_lang, file.hash + '.md');
|
|
87
|
+
const originalContent = await fs.readFile(path.join(INPUT_DIR, file.path), 'utf-8');
|
|
88
|
+
|
|
89
|
+
const enhancedContent = updateFrontmatter(originalContent, {
|
|
90
|
+
title: file.metadata.title,
|
|
91
|
+
summary: file.metadata.summary,
|
|
92
|
+
tags: file.metadata.tags,
|
|
93
|
+
inferred_date: file.metadata.inferred_date,
|
|
94
|
+
inferred_lang: file.metadata.inferred_lang,
|
|
95
|
+
});
|
|
96
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
97
|
+
|
|
98
|
+
await fs.writeFile(filePath, enhancedContent, 'utf-8');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.warn(`⚠️ Failed to store native file ${file.path}:`, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (verbose && files.length > 0) {
|
|
105
|
+
console.log(`💾 Stored ${files.length} native language files to .zen/src`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 处理翻译
|
|
111
|
+
*/
|
|
112
|
+
async function processTranslations(): Promise<void> {
|
|
113
|
+
const {
|
|
114
|
+
files,
|
|
115
|
+
options: { langs = [], verbose },
|
|
116
|
+
} = MetaData;
|
|
117
|
+
|
|
118
|
+
await Promise.all(
|
|
119
|
+
files.flatMap(async file => {
|
|
120
|
+
return Promise.all(
|
|
121
|
+
langs.map(async lang => {
|
|
122
|
+
if (verbose) console.info(`📄 Processing file for translation: ${file.path}`);
|
|
123
|
+
if (!file.metadata) {
|
|
124
|
+
console.warn(`⚠️ Missing metadata for file: ${file.path}, skipping translation.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (verbose) console.log(`🌐 Translating to ${lang}...`);
|
|
128
|
+
// 存储翻译文件到 .zen/src/{lang}
|
|
129
|
+
const sourcePath = path.join(ZEN_SRC_DIR, file.metadata.inferred_lang, file.hash + '.md'); // 使用已经加强的母语文件路径
|
|
130
|
+
const targetPath = path.join(ZEN_SRC_DIR, lang, file.hash + '.md');
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const content = await fs.readFile(sourcePath, 'utf-8');
|
|
134
|
+
if (file.metadata.inferred_lang === lang) {
|
|
135
|
+
if (verbose)
|
|
136
|
+
console.log(`ℹ️ Skipping translation for ${file.path}, already in target language`);
|
|
137
|
+
return;
|
|
138
|
+
} else {
|
|
139
|
+
// 翻译
|
|
140
|
+
// 先检查是否已经有翻译文件存在
|
|
141
|
+
|
|
142
|
+
const exists = await fs.access(targetPath).then(
|
|
143
|
+
() => true,
|
|
144
|
+
() => false
|
|
145
|
+
);
|
|
146
|
+
if (exists) {
|
|
147
|
+
if (verbose)
|
|
148
|
+
console.log(`ℹ️ Translation already exists for ${file.path} in ${lang}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const translatedContent = await translateMarkdown(content, lang);
|
|
154
|
+
|
|
155
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
156
|
+
await fs.writeFile(targetPath, translatedContent, 'utf-8');
|
|
157
|
+
|
|
158
|
+
if (verbose) console.log(`✅ Translated file saved: ${targetPath}`);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`❌ Failed to translate to ${lang}:`, error);
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 构建管道(函数组合)
|
|
170
|
+
*/
|
|
171
|
+
async function buildPipeline(options: BuildOptions): Promise<void> {
|
|
172
|
+
// 验证配置
|
|
173
|
+
await validateConfig(options);
|
|
174
|
+
|
|
175
|
+
// 清理输出目录
|
|
176
|
+
await fs.rm(ZEN_DIST_DIR, { recursive: true, force: true });
|
|
177
|
+
|
|
178
|
+
// 确保 .zen/.gitignore 文件
|
|
179
|
+
await fs.mkdir(ZEN_DIR, { recursive: true });
|
|
180
|
+
await fs.writeFile(path.join(ZEN_DIR, '.gitignore'), 'dist\n', 'utf-8');
|
|
181
|
+
|
|
182
|
+
// 扫描源文件
|
|
183
|
+
await scanSourceFiles();
|
|
184
|
+
|
|
185
|
+
// 运行 AI 元数据提取
|
|
186
|
+
await extractMetadataByAI();
|
|
187
|
+
|
|
188
|
+
// 存储母语文件
|
|
189
|
+
await storeNativeFiles();
|
|
190
|
+
|
|
191
|
+
// 处理翻译
|
|
192
|
+
await processTranslations();
|
|
193
|
+
|
|
194
|
+
// 渲染模板
|
|
195
|
+
await renderTemplates();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 主构建函数
|
|
200
|
+
*/
|
|
201
|
+
export async function buildSite(options: BuildOptions): Promise<void> {
|
|
202
|
+
const startTime = Date.now();
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await loadMetaData();
|
|
206
|
+
await buildPipeline(options);
|
|
207
|
+
|
|
208
|
+
const endTime = Date.now();
|
|
209
|
+
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
210
|
+
console.log(`🎉 Build completed in ${duration}s`);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(`❌ Build failed:`, error);
|
|
213
|
+
throw error;
|
|
214
|
+
} finally {
|
|
215
|
+
await saveMetaData();
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,65 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { config } from 'dotenv';
|
|
4
3
|
import { Cli, Command, Option } from 'clipanion';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
4
|
+
import { config } from 'dotenv';
|
|
5
|
+
import * as fs from 'fs';
|
|
7
6
|
import * as path from 'path';
|
|
8
|
-
import
|
|
9
|
-
import * as fsSync from 'fs';
|
|
10
|
-
import * as url from 'url';
|
|
7
|
+
import { buildSite } from './build/pipeline';
|
|
11
8
|
|
|
12
9
|
// 加载 .env 文件中的环境变量
|
|
13
10
|
config();
|
|
14
11
|
|
|
15
12
|
// 获取版本号 - 从 package.json 读取
|
|
16
13
|
function getVersion(): string {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return packageJson.version;
|
|
21
|
-
} catch {
|
|
22
|
-
return '0.1.32';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 基础命令类
|
|
27
|
-
abstract class BaseCommand extends Command {
|
|
28
|
-
protected async loadConfig(configPath?: string): Promise<ZenConfig> {
|
|
29
|
-
if (!configPath) {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const resolvedPath = path.resolve(configPath);
|
|
35
|
-
const configContent = await fs.readFile(resolvedPath, 'utf-8');
|
|
36
|
-
return JSON.parse(configContent);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
this.context.stderr.write(`❌ Failed to load config file: ${error}\n`);
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
protected getOutDir(): string {
|
|
44
|
-
const currentDir = process.cwd();
|
|
45
|
-
return path.join(currentDir, '.zen', 'dist');
|
|
46
|
-
}
|
|
14
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
15
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
16
|
+
return packageJson.version;
|
|
47
17
|
}
|
|
48
18
|
|
|
49
19
|
// Build 命令
|
|
50
|
-
class BuildCommand extends
|
|
20
|
+
class BuildCommand extends Command {
|
|
51
21
|
static paths = [['build']];
|
|
52
22
|
|
|
53
23
|
template = Option.String('-t,--template');
|
|
54
|
-
watch = Option.Boolean('-w,--watch');
|
|
55
|
-
serve = Option.Boolean('-s,--serve');
|
|
56
|
-
port = Option.String('-p,--port', '3000');
|
|
57
|
-
host = Option.String('--host', 'localhost');
|
|
58
24
|
verbose = Option.Boolean('-v,--verbose');
|
|
59
|
-
config = Option.String('-c,--config');
|
|
60
25
|
baseUrl = Option.String('--base-url');
|
|
61
|
-
clean = Option.Boolean('--clean');
|
|
62
|
-
ai = Option.Boolean('--ai', { description: 'Enable AI metadata extraction' });
|
|
63
26
|
lang = Option.Array('--lang', {
|
|
64
27
|
description: 'Target languages for translation (e.g., en-US, ja-JP)',
|
|
65
28
|
});
|
|
@@ -72,103 +35,18 @@ class BuildCommand extends BaseCommand {
|
|
|
72
35
|
|
|
73
36
|
Examples:
|
|
74
37
|
$ zengen build
|
|
75
|
-
$ zengen build --watch
|
|
76
|
-
$ zengen build --watch --serve
|
|
77
|
-
$ zengen build --watch --serve --port 8080
|
|
78
|
-
$ zengen build --config zen.config.json
|
|
79
|
-
$ zengen build --clean
|
|
80
|
-
$ zengen build --ai (requires OPENAI_API_KEY environment variable)
|
|
81
38
|
$ zengen build --lang en-US --lang ja-JP (translate to English and Japanese)
|
|
82
39
|
`,
|
|
83
40
|
});
|
|
84
41
|
|
|
85
42
|
async execute() {
|
|
86
43
|
try {
|
|
87
|
-
|
|
88
|
-
const config = await this.loadConfig(this.config);
|
|
89
|
-
|
|
90
|
-
// 强制使用当前目录作为 src 目录,输出到 .zen/dist 目录
|
|
91
|
-
const currentDir = process.cwd();
|
|
92
|
-
const outDir = this.getOutDir();
|
|
93
|
-
|
|
94
|
-
// 处理 AI 配置:如果指定了 --ai 参数,启用 AI;否则使用配置中的设置
|
|
95
|
-
const aiConfig = {
|
|
96
|
-
...config.ai,
|
|
97
|
-
enabled: this.ai ? true : config.ai?.enabled,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// 处理语言配置:命令行参数优先于配置文件
|
|
101
|
-
const targetLangs = this.lang && this.lang.length > 0 ? this.lang : config.i18n?.targetLangs;
|
|
102
|
-
const i18nConfig =
|
|
103
|
-
targetLangs && targetLangs.length > 0
|
|
104
|
-
? {
|
|
105
|
-
...config.i18n,
|
|
106
|
-
sourceLang: config.i18n?.sourceLang || 'zh-Hans', // 默认源语言为中文
|
|
107
|
-
targetLangs,
|
|
108
|
-
}
|
|
109
|
-
: undefined;
|
|
110
|
-
|
|
111
|
-
// 合并命令行参数和配置
|
|
112
|
-
const buildOptions = {
|
|
113
|
-
srcDir: currentDir,
|
|
114
|
-
outDir: outDir,
|
|
44
|
+
await buildSite({
|
|
115
45
|
template: this.template ? path.resolve(this.template) : undefined,
|
|
116
|
-
watch: this.watch,
|
|
117
|
-
serve: this.serve,
|
|
118
|
-
port: parseInt(this.port, 10),
|
|
119
|
-
host: this.host,
|
|
120
46
|
verbose: this.verbose,
|
|
121
|
-
baseUrl: this.baseUrl
|
|
47
|
+
baseUrl: this.baseUrl,
|
|
122
48
|
langs: this.lang,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// 创建最终的配置,包含 AI 和 i18n 设置
|
|
126
|
-
const finalConfig = {
|
|
127
|
-
...config,
|
|
128
|
-
ai: aiConfig.enabled ? aiConfig : undefined,
|
|
129
|
-
i18n: i18nConfig,
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const builder = new ZenBuilder(finalConfig);
|
|
133
|
-
|
|
134
|
-
// 验证配置
|
|
135
|
-
const errors = builder.validateConfig(finalConfig);
|
|
136
|
-
if (errors.length > 0) {
|
|
137
|
-
this.context.stderr.write('❌ Configuration errors:\n');
|
|
138
|
-
errors.forEach(error => this.context.stderr.write(` - ${error}\n`));
|
|
139
|
-
return 1;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 警告 --serve 选项需要 --watch 选项
|
|
143
|
-
if (this.serve && !this.watch) {
|
|
144
|
-
this.context.stdout.write(
|
|
145
|
-
'⚠️ Warning: --serve option requires --watch option, ignoring --serve\n'
|
|
146
|
-
);
|
|
147
|
-
buildOptions.serve = false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 清理输出目录
|
|
151
|
-
if (this.clean) {
|
|
152
|
-
await builder.clean(buildOptions.outDir);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 构建或监听
|
|
156
|
-
if (this.watch) {
|
|
157
|
-
await builder.watch(buildOptions);
|
|
158
|
-
} else {
|
|
159
|
-
// 如果指定了语言参数,使用多语言构建
|
|
160
|
-
if (this.lang && this.lang.length > 0) {
|
|
161
|
-
const multiLangOptions = {
|
|
162
|
-
...buildOptions,
|
|
163
|
-
langs: this.lang,
|
|
164
|
-
useMetaData: true,
|
|
165
|
-
filterOrphans: true,
|
|
166
|
-
};
|
|
167
|
-
await builder.buildMultiLang(multiLangOptions);
|
|
168
|
-
} else {
|
|
169
|
-
await builder.build(buildOptions);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
49
|
+
});
|
|
172
50
|
|
|
173
51
|
return 0;
|
|
174
52
|
} catch (error) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 使用git命令查找所有Markdown文件
|
|
8
|
+
* 使用git ls-files --others --cached --exclude-standard获取所有文件
|
|
9
|
+
* 然后过滤掉.zen目录和只保留.md文件
|
|
10
|
+
*
|
|
11
|
+
* @param dirPath 要扫描的目录路径
|
|
12
|
+
* @returns Promise<string[]> 返回Markdown文件的相对路径数组
|
|
13
|
+
*/
|
|
14
|
+
export const findMarkdownEntries = async (dirPath: string): Promise<string[]> => {
|
|
15
|
+
try {
|
|
16
|
+
// 使用git命令获取所有文件(包括已跟踪和未跟踪的文件)
|
|
17
|
+
// 在指定的目录下执行git命令
|
|
18
|
+
const { stdout } = await execAsync('git ls-files --others --cached --exclude-standard', {
|
|
19
|
+
cwd: dirPath,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 按行分割并过滤
|
|
23
|
+
const files = stdout
|
|
24
|
+
.split('\n')
|
|
25
|
+
.filter(line => line.trim() !== '') // 移除空行
|
|
26
|
+
.filter(file => !file.startsWith('.zen')) // 过滤掉.zen目录下的文件
|
|
27
|
+
.filter(file => file.endsWith('.md')); // 只保留.md文件
|
|
28
|
+
|
|
29
|
+
return files;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Error finding markdown entries:', error);
|
|
32
|
+
|
|
33
|
+
// 如果git命令失败,返回空数组
|
|
34
|
+
// 这可以处理没有git仓库或git不可用的情况
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
};
|