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.
Files changed (150) hide show
  1. package/.github/workflows/pages.yml +1 -1
  2. package/.zen/meta.json +112 -32
  3. package/.zen/src/en-US/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
  4. package/.zen/src/en-US/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +75 -0
  5. package/.zen/src/en-US/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
  6. package/.zen/src/en-US/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +53 -0
  7. package/.zen/src/en-US/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
  8. package/.zen/src/en-US/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +61 -0
  9. package/.zen/src/en-US/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +40 -0
  10. package/.zen/src/en-US/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
  11. package/.zen/src/zh-Hans/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
  12. package/.zen/src/zh-Hans/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +77 -0
  13. package/.zen/src/zh-Hans/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
  14. package/.zen/src/zh-Hans/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +55 -0
  15. package/.zen/src/zh-Hans/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
  16. package/.zen/src/zh-Hans/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +63 -0
  17. package/.zen/src/zh-Hans/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +40 -0
  18. package/.zen/src/zh-Hans/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
  19. package/assets/templates/default/layout.html +274 -0
  20. package/dist/ai/extractMetadataFromMarkdown.d.ts +8 -0
  21. package/dist/ai/extractMetadataFromMarkdown.d.ts.map +1 -0
  22. package/dist/ai/extractMetadataFromMarkdown.js +88 -0
  23. package/dist/ai/extractMetadataFromMarkdown.js.map +1 -0
  24. package/dist/ai/translateMarkdown.d.ts +8 -0
  25. package/dist/ai/translateMarkdown.d.ts.map +1 -0
  26. package/dist/ai/translateMarkdown.js +29 -0
  27. package/dist/ai/translateMarkdown.js.map +1 -0
  28. package/dist/build/pipeline.d.ts +6 -0
  29. package/dist/build/pipeline.d.ts.map +1 -0
  30. package/dist/build/pipeline.js +219 -0
  31. package/dist/build/pipeline.js.map +1 -0
  32. package/dist/cli.js +10 -118
  33. package/dist/cli.js.map +1 -1
  34. package/dist/findEntries.d.ts +10 -0
  35. package/dist/findEntries.d.ts.map +1 -0
  36. package/dist/findEntries.js +38 -0
  37. package/dist/findEntries.js.map +1 -0
  38. package/dist/index.d.ts +1 -32
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -35
  41. package/dist/index.js.map +1 -1
  42. package/dist/metadata.d.ts +14 -0
  43. package/dist/metadata.d.ts.map +1 -0
  44. package/dist/metadata.js +78 -0
  45. package/dist/metadata.js.map +1 -0
  46. package/dist/paths.d.ts +6 -0
  47. package/dist/paths.d.ts.map +1 -0
  48. package/dist/paths.js +10 -0
  49. package/dist/paths.js.map +1 -0
  50. package/dist/process/extractMetadataByAI.d.ts +5 -0
  51. package/dist/process/extractMetadataByAI.d.ts.map +1 -0
  52. package/dist/process/extractMetadataByAI.js +31 -0
  53. package/dist/process/extractMetadataByAI.js.map +1 -0
  54. package/dist/process/template.d.ts +5 -0
  55. package/dist/process/template.d.ts.map +1 -0
  56. package/dist/process/template.js +188 -0
  57. package/dist/process/template.js.map +1 -0
  58. package/dist/scan/files.d.ts +7 -0
  59. package/dist/scan/files.d.ts.map +1 -0
  60. package/dist/scan/files.js +54 -0
  61. package/dist/scan/files.js.map +1 -0
  62. package/dist/services/openai.d.ts +41 -0
  63. package/dist/services/openai.d.ts.map +1 -0
  64. package/dist/services/openai.js +54 -0
  65. package/dist/services/openai.js.map +1 -0
  66. package/dist/types.d.ts +16 -67
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/utils/convertMarkdownToHtml.d.ts +7 -0
  69. package/dist/utils/convertMarkdownToHtml.d.ts.map +1 -0
  70. package/dist/utils/convertMarkdownToHtml.js +39 -0
  71. package/dist/utils/convertMarkdownToHtml.js.map +1 -0
  72. package/dist/utils/frontmatter.d.ts +6 -0
  73. package/dist/utils/frontmatter.d.ts.map +1 -0
  74. package/dist/utils/frontmatter.js +22 -0
  75. package/dist/utils/frontmatter.js.map +1 -0
  76. package/docs/deployment/github-pages.md +1 -2
  77. package/docs/guides/best-practices.md +4 -4
  78. package/docs/guides/config.md +0 -5
  79. package/package.json +4 -2
  80. package/src/ai/extractMetadataFromMarkdown.ts +95 -0
  81. package/src/ai/translateMarkdown.ts +29 -0
  82. package/src/build/pipeline.ts +217 -0
  83. package/src/cli.ts +10 -132
  84. package/src/findEntries.ts +37 -0
  85. package/src/index.ts +1 -40
  86. package/src/metadata.ts +44 -0
  87. package/src/paths.ts +7 -0
  88. package/src/process/extractMetadataByAI.ts +31 -0
  89. package/src/process/template.ts +201 -0
  90. package/src/scan/files.ts +17 -0
  91. package/src/services/openai.ts +92 -0
  92. package/src/types.ts +18 -72
  93. package/src/utils/convertMarkdownToHtml.ts +32 -0
  94. package/src/utils/frontmatter.ts +18 -0
  95. package/.zen/translations.json +0 -51
  96. package/dist/ai-client.d.ts +0 -34
  97. package/dist/ai-client.d.ts.map +0 -1
  98. package/dist/ai-client.js +0 -180
  99. package/dist/ai-client.js.map +0 -1
  100. package/dist/ai-processor.d.ts +0 -51
  101. package/dist/ai-processor.d.ts.map +0 -1
  102. package/dist/ai-processor.js +0 -215
  103. package/dist/ai-processor.js.map +0 -1
  104. package/dist/ai-service.d.ts +0 -79
  105. package/dist/ai-service.d.ts.map +0 -1
  106. package/dist/ai-service.js +0 -257
  107. package/dist/ai-service.js.map +0 -1
  108. package/dist/builder.d.ts +0 -70
  109. package/dist/builder.d.ts.map +0 -1
  110. package/dist/builder.js +0 -854
  111. package/dist/builder.js.map +0 -1
  112. package/dist/gitignore.d.ts +0 -41
  113. package/dist/gitignore.d.ts.map +0 -1
  114. package/dist/gitignore.js +0 -202
  115. package/dist/gitignore.js.map +0 -1
  116. package/dist/gitignore.test.d.ts +0 -2
  117. package/dist/gitignore.test.d.ts.map +0 -1
  118. package/dist/gitignore.test.js +0 -309
  119. package/dist/gitignore.test.js.map +0 -1
  120. package/dist/markdown.d.ts +0 -35
  121. package/dist/markdown.d.ts.map +0 -1
  122. package/dist/markdown.js +0 -221
  123. package/dist/markdown.js.map +0 -1
  124. package/dist/navigation.d.ts +0 -46
  125. package/dist/navigation.d.ts.map +0 -1
  126. package/dist/navigation.js +0 -196
  127. package/dist/navigation.js.map +0 -1
  128. package/dist/scanner.d.ts +0 -26
  129. package/dist/scanner.d.ts.map +0 -1
  130. package/dist/scanner.js +0 -190
  131. package/dist/scanner.js.map +0 -1
  132. package/dist/template.d.ts +0 -33
  133. package/dist/template.d.ts.map +0 -1
  134. package/dist/template.js +0 -434
  135. package/dist/template.js.map +0 -1
  136. package/dist/translation-service.d.ts +0 -72
  137. package/dist/translation-service.d.ts.map +0 -1
  138. package/dist/translation-service.js +0 -291
  139. package/dist/translation-service.js.map +0 -1
  140. package/src/ai-client.ts +0 -227
  141. package/src/ai-processor.ts +0 -243
  142. package/src/ai-service.ts +0 -281
  143. package/src/builder.ts +0 -991
  144. package/src/gitignore.test.ts +0 -318
  145. package/src/gitignore.ts +0 -193
  146. package/src/markdown.ts +0 -212
  147. package/src/navigation.ts +0 -237
  148. package/src/scanner.ts +0 -180
  149. package/src/template.ts +0 -425
  150. package/src/translation-service.ts +0 -350
package/src/builder.ts DELETED
@@ -1,991 +0,0 @@
1
- import {
2
- BuildOptions,
3
- FileInfo,
4
- NavigationItem,
5
- ZenConfig,
6
- ScannedFile,
7
- MultiLangBuildOptions,
8
- } from './types';
9
- import { MarkdownConverter } from './markdown';
10
- import { TemplateEngine } from './template';
11
- import { NavigationGenerator } from './navigation';
12
- import { GitIgnoreProcessor } from './gitignore';
13
- import { Scanner } from './scanner';
14
- import { AIProcessor } from './ai-processor';
15
- import { TranslationService } from './translation-service';
16
- import { AIService } from './ai-service';
17
- import * as fs from 'fs/promises';
18
- import * as path from 'path';
19
- import * as chokidar from 'chokidar';
20
- import express from 'express';
21
- import * as http from 'http';
22
-
23
- export class ZenBuilder {
24
- private markdownConverter: MarkdownConverter;
25
- private templateEngine: TemplateEngine;
26
- private navigationGenerator: NavigationGenerator;
27
- private scanner: Scanner;
28
- private aiProcessor: AIProcessor;
29
- private translationService: TranslationService;
30
- private config: ZenConfig = {};
31
-
32
- constructor(config: ZenConfig = {}) {
33
- this.config = config;
34
-
35
- // 创建 AI 处理器
36
- this.aiProcessor = new AIProcessor(config);
37
-
38
- // 创建翻译服务
39
- this.translationService = new TranslationService(config.ai);
40
-
41
- // 获取现有的 processors 或创建空数组
42
- const existingProcessors = config.processors || [];
43
-
44
- // 如果 AI 处理器启用,将其添加到 processors 列表的开头
45
- const processors = this.aiProcessor.isEnabled()
46
- ? [this.aiProcessor, ...existingProcessors]
47
- : existingProcessors;
48
-
49
- this.markdownConverter = new MarkdownConverter(processors);
50
- this.templateEngine = new TemplateEngine();
51
- this.navigationGenerator = new NavigationGenerator(config.baseUrl);
52
- this.scanner = new Scanner(config);
53
- }
54
-
55
- /**
56
- * 构建文档站点
57
- */
58
- async build(options: BuildOptions): Promise<void> {
59
- const startTime = Date.now();
60
- const { srcDir, outDir, template, verbose = false, baseUrl, langs } = options;
61
-
62
- if (verbose) {
63
- console.log(`🚀 Starting ZEN build...`);
64
- console.log(`📁 Source: ${srcDir}`);
65
- console.log(`📁 Output: ${outDir}`);
66
- console.log(`🔗 Base URL: ${baseUrl || '(not set)'}`);
67
- if (langs && langs.length > 0) {
68
- console.log(`🌐 Target languages: ${langs.join(', ')}`);
69
- }
70
- console.log(`🔍 Verbose mode enabled`);
71
- }
72
-
73
- // 验证源目录
74
- try {
75
- await fs.access(srcDir);
76
- } catch (error) {
77
- throw new Error(`Source directory does not exist: ${srcDir}`);
78
- }
79
-
80
- // 确保输出目录存在
81
- await fs.mkdir(outDir, { recursive: true });
82
-
83
- // 确保 .zen/.gitignore 文件存在且内容正确
84
- const zenDir = path.dirname(outDir); // .zen 目录
85
- const zenGitignorePath = path.join(zenDir, '.gitignore');
86
- const gitignoreContent = 'dist\n';
87
-
88
- try {
89
- // 检查 .gitignore 文件是否存在
90
- await fs.access(zenGitignorePath);
91
-
92
- // 如果存在,检查内容是否正确
93
- const existingContent = await fs.readFile(zenGitignorePath, 'utf-8');
94
- if (existingContent.trim() !== 'dist') {
95
- if (verbose) console.log(`📝 Updating .zen/.gitignore content...`);
96
- await fs.writeFile(zenGitignorePath, gitignoreContent, 'utf-8');
97
- }
98
- } catch (error) {
99
- // 文件不存在,创建它
100
- if (verbose) console.log(`📝 Creating .zen/.gitignore file...`);
101
- await fs.writeFile(zenGitignorePath, gitignoreContent, 'utf-8');
102
- }
103
-
104
- // 扫描阶段:生成文件列表
105
- if (verbose) console.log(`🔍 Scanning source directory...`);
106
- const scannedFiles = await this.scanner.scanDirectory(srcDir);
107
-
108
- if (scannedFiles.length === 0) {
109
- console.warn(`⚠️ No Markdown files found in ${srcDir}`);
110
- return;
111
- }
112
-
113
- if (verbose) console.log(`✅ Found ${scannedFiles.length} Markdown files`);
114
-
115
- // 清理 meta.json 中的孤儿条目(文件已删除但缓存仍存在)
116
- if (this.aiProcessor.isEnabled()) {
117
- if (verbose) console.log(`🧹 Cleaning orphan entries in meta.json...`);
118
- const aiService = new AIService();
119
- const existingFilePaths = scannedFiles.map(file => file.path);
120
- await aiService.removeOrphanEntries(existingFilePaths);
121
- }
122
-
123
- // 保存扫描结果到 .zen/dist 目录
124
- const zenDistDir = path.join(path.dirname(outDir), 'dist');
125
- const scanResultPath = path.join(zenDistDir, 'scan-result.json');
126
- if (verbose) console.log(`💾 Saving scan result to ${scanResultPath}...`);
127
- await this.scanner.saveScanResult(scannedFiles, scanResultPath);
128
-
129
- // 构建阶段:读取文件内容并转换
130
- if (verbose) console.log(`📄 Reading and converting Markdown files...`);
131
- const files = await this.markdownConverter.convertScannedFiles(scannedFiles, srcDir);
132
-
133
- if (files.length === 0) {
134
- console.warn(`⚠️ Failed to read any Markdown files`);
135
- return;
136
- }
137
-
138
- // AI 批量处理(如果启用)
139
- if (this.aiProcessor.isEnabled()) {
140
- if (verbose) console.log(`🤖 Running AI metadata extraction...`);
141
- await this.aiProcessor.processBatch(files);
142
- }
143
-
144
- // 存储母语文件到 .zen/src
145
- if (verbose) console.log(`💾 Storing native language files...`);
146
- await this.storeNativeFiles(files, verbose);
147
-
148
- // 处理翻译(如果指定了目标语言)
149
- if (langs && langs.length > 0 && this.translationService.isEnabled()) {
150
- if (verbose) console.log(`🌐 Processing translations...`);
151
- await this.processTranslations(files, langs, verbose);
152
- }
153
-
154
- // 更新导航生成器的 baseUrl(优先使用命令行参数)
155
- if (baseUrl !== undefined) {
156
- if (verbose) console.log(`🔗 Using baseUrl: ${baseUrl}`);
157
- this.navigationGenerator.setBaseUrl(baseUrl);
158
- } else if (this.config.baseUrl) {
159
- if (verbose) console.log(`🔗 Using config baseUrl: ${this.config.baseUrl}`);
160
- this.navigationGenerator.setBaseUrl(this.config.baseUrl);
161
- }
162
-
163
- // 生成导航
164
- if (verbose) console.log(`🗺️ Generating navigation...`);
165
- const navigation = this.navigationGenerator.generate(files);
166
-
167
- // 处理每个文件
168
- if (verbose) console.log(`⚡ Processing files...`);
169
- let processedCount = 0;
170
-
171
- for (const file of files) {
172
- try {
173
- // 生成模板数据
174
- const templateData = this.templateEngine.generateTemplateData(file, navigation);
175
-
176
- // 渲染模板
177
- const html = await this.templateEngine.render(templateData, template);
178
-
179
- // 生成输出路径
180
- const outputPath = this.templateEngine.getOutputPath(file, outDir);
181
-
182
- // 保存文件
183
- await this.templateEngine.saveToFile(html, outputPath);
184
-
185
- processedCount++;
186
-
187
- if (verbose && processedCount % 10 === 0) {
188
- console.log(` Processed ${processedCount}/${files.length} files...`);
189
- }
190
- } catch (error) {
191
- console.error(`❌ Failed to process ${file.path}:`, error);
192
- }
193
- }
194
-
195
- // 生成站点地图
196
- if (verbose) console.log(`🗺️ Generating sitemap...`);
197
- await this.generateSitemap(files, outDir);
198
-
199
- // 生成导航 JSON 文件
200
- if (verbose) console.log(`📊 Generating navigation data...`);
201
- await this.generateNavigationJson(files, outDir);
202
-
203
- // 复制静态资源(如果存在)
204
- await this.copyStaticAssets(srcDir, outDir);
205
-
206
- // 确保每个目录都有 index.html
207
- if (verbose) console.log(`📁 Ensuring index.html in all directories...`);
208
- await this.ensureDirectoryIndexHtml(outDir);
209
-
210
- const duration = Date.now() - startTime;
211
- if (verbose) {
212
- console.log(`🎉 Build completed!`);
213
- console.log(` Files processed: ${processedCount}/${files.length}`);
214
- console.log(` Duration: ${duration}ms`);
215
- console.log(` Output directory: ${outDir}`);
216
- } else {
217
- console.log(`✅ Built ${processedCount} files to ${outDir} in ${duration}ms`);
218
- }
219
- }
220
-
221
- /**
222
- * 多语言构建:基于 meta.json 构建多语言版本
223
- */
224
- async buildMultiLang(options: MultiLangBuildOptions): Promise<void> {
225
- const startTime = Date.now();
226
- const {
227
- srcDir,
228
- outDir,
229
- template,
230
- verbose = false,
231
- baseUrl,
232
- langs,
233
- useMetaData = true,
234
- filterOrphans = true,
235
- } = options;
236
-
237
- if (!langs || langs.length === 0) {
238
- throw new Error('At least one language must be specified for multi-language build');
239
- }
240
-
241
- if (verbose) {
242
- console.log(`🚀 Starting ZEN multi-language build...`);
243
- console.log(`📁 Source: ${srcDir}`);
244
- console.log(`📁 Output: ${outDir}`);
245
- console.log(`🌐 Target languages: ${langs.join(', ')}`);
246
- console.log(`📊 Using meta.json: ${useMetaData}`);
247
- console.log(`🧹 Filter orphans: ${filterOrphans}`);
248
- console.log(`🔗 Base URL: ${baseUrl || '(not set)'}`);
249
- console.log(`🔍 Verbose mode enabled`);
250
- }
251
-
252
- // 验证源目录
253
- try {
254
- await fs.access(srcDir);
255
- } catch (error) {
256
- throw new Error(`Source directory does not exist: ${srcDir}`);
257
- }
258
-
259
- // 确保输出目录存在
260
- await fs.mkdir(outDir, { recursive: true });
261
-
262
- // 加载 meta.json
263
- const aiService = new AIService();
264
- const metaData = await aiService.loadMetaData();
265
-
266
- if (verbose) {
267
- console.log(`📊 Loaded ${metaData.files.length} entries from meta.json`);
268
- }
269
-
270
- // 过滤有效的文件项
271
- let validFiles = metaData.files;
272
-
273
- if (filterOrphans) {
274
- const originalCount = validFiles.length;
275
- validFiles = await this.filterValidFiles(validFiles, srcDir, verbose);
276
- if (verbose) {
277
- console.log(`🧹 Filtered ${originalCount - validFiles.length} orphan files`);
278
- }
279
- }
280
-
281
- if (validFiles.length === 0) {
282
- console.warn(`⚠️ No valid files found in meta.json`);
283
- return;
284
- }
285
-
286
- if (verbose) {
287
- console.log(`✅ Found ${validFiles.length} valid files to build`);
288
- }
289
-
290
- // 为每个语言构建
291
- let totalProcessed = 0;
292
- for (const lang of langs) {
293
- if (verbose) {
294
- console.log(`\n🌐 Building for language: ${lang}`);
295
- }
296
-
297
- const langProcessed = await this.buildForLanguage(
298
- validFiles,
299
- lang,
300
- srcDir,
301
- outDir,
302
- template,
303
- baseUrl,
304
- verbose,
305
- langs
306
- );
307
-
308
- totalProcessed += langProcessed;
309
- }
310
-
311
- // 生成语言索引页面
312
- if (verbose) {
313
- console.log(`\n📄 Generating language index...`);
314
- }
315
- await this.generateLanguageIndex(langs, outDir, verbose);
316
-
317
- const duration = Date.now() - startTime;
318
- console.log(`🎉 Multi-language build completed!`);
319
- console.log(` Languages: ${langs.join(', ')}`);
320
- console.log(` Total files built: ${totalProcessed}`);
321
- console.log(` Duration: ${duration}ms`);
322
- console.log(` Output directory: ${outDir}`);
323
- }
324
-
325
- /**
326
- * 过滤有效的文件(移除 path 不存在的孤儿文件)
327
- */
328
- private async filterValidFiles(files: any[], srcDir: string, verbose?: boolean): Promise<any[]> {
329
- const validFiles: any[] = [];
330
-
331
- for (const file of files) {
332
- // 如果文件路径已经是绝对路径或包含目录,直接使用
333
- const filePath = file.path.startsWith('/') ? file.path : path.join(process.cwd(), file.path);
334
- try {
335
- await fs.access(filePath);
336
- validFiles.push(file);
337
- } catch (error) {
338
- // 文件不存在,跳过
339
- if (verbose) {
340
- console.log(` ⚠️ Orphan file skipped: ${file.path} (path: ${filePath})`);
341
- }
342
- }
343
- }
344
-
345
- return validFiles;
346
- }
347
-
348
- /**
349
- * 为特定语言构建文件
350
- */
351
- private async buildForLanguage(
352
- files: any[],
353
- lang: string,
354
- srcDir: string,
355
- outDir: string,
356
- template?: string,
357
- baseUrl?: string,
358
- verbose?: boolean,
359
- allLangs?: string[]
360
- ): Promise<number> {
361
- const aiService = new AIService();
362
- const langDir = path.join(outDir, lang);
363
- await fs.mkdir(langDir, { recursive: true });
364
-
365
- let processedCount = 0;
366
-
367
- // 更新导航生成器的 baseUrl
368
- if (baseUrl !== undefined) {
369
- this.navigationGenerator.setBaseUrl(baseUrl);
370
- } else if (this.config.baseUrl) {
371
- this.navigationGenerator.setBaseUrl(this.config.baseUrl);
372
- }
373
-
374
- // 为当前语言生成导航
375
- const navigation = this.navigationGenerator.generate([]); // 暂时使用空导航
376
-
377
- for (const file of files) {
378
- try {
379
- let content: string;
380
- let filePath: string;
381
- let finalHash = file.hash;
382
- let finalMetadata = file.metadata;
383
-
384
- // 获取源语言
385
- const sourceLang = file.metadata?.inferred_lang || 'zh-Hans';
386
-
387
- if (lang === sourceLang) {
388
- // 如果是源语言,读取原始文件
389
- filePath = file.path.startsWith('/') ? file.path : path.join(process.cwd(), file.path);
390
- content = await fs.readFile(filePath, 'utf-8');
391
- } else {
392
- // 如果是目标语言,尝试读取翻译文件
393
- const translationService = new TranslationService();
394
- try {
395
- // 创建临时 FileInfo 对象用于获取翻译
396
- const tempFileInfo: FileInfo = {
397
- path: file.path,
398
- name: path.basename(file.path, '.md'),
399
- ext: '.md',
400
- content: '', // 临时内容
401
- hash: file.hash,
402
- aiMetadata: file.metadata,
403
- };
404
-
405
- // 确保翻译文件存在并获取内容
406
- content = await translationService.ensureTranslatedFile(
407
- tempFileInfo,
408
- sourceLang,
409
- lang,
410
- file.hash
411
- );
412
-
413
- // 翻译文件的路径
414
- filePath = translationService.getTranslatedFilePath(file.path, lang, file.hash);
415
-
416
- // 对于翻译文件,我们可以使用相同的 hash,或者生成新的 hash
417
- // 这里我们使用相同的 hash,因为翻译是基于原始内容的
418
- } catch (translationError) {
419
- console.warn(
420
- `⚠️ Failed to get translation for ${file.path} to ${lang}, using source file:`,
421
- translationError
422
- );
423
- // 如果翻译失败,回退到源文件
424
- filePath = file.path.startsWith('/') ? file.path : path.join(process.cwd(), file.path);
425
- content = await fs.readFile(filePath, 'utf-8');
426
- }
427
- }
428
-
429
- // 创建 FileInfo 对象
430
- const fileInfo: FileInfo = {
431
- path: file.path,
432
- name: path.basename(file.path, '.md'),
433
- ext: '.md',
434
- content,
435
- hash: finalHash,
436
- aiMetadata: finalMetadata,
437
- };
438
-
439
- // 转换为 HTML
440
- const convertedFileInfo = await this.markdownConverter.convert(fileInfo);
441
- const html = convertedFileInfo.html || '';
442
-
443
- // 更新文件信息中的 HTML 内容
444
- const finalFileInfo: FileInfo = {
445
- ...fileInfo,
446
- html,
447
- };
448
-
449
- // 生成模板数据
450
- const templateData = this.templateEngine.generateTemplateData(
451
- finalFileInfo,
452
- navigation,
453
- lang,
454
- allLangs
455
- );
456
-
457
- // 渲染模板
458
- const renderedHtml = await this.templateEngine.render(templateData, template);
459
-
460
- // 生成输出路径
461
- const outputPath = this.templateEngine.getOutputPath(
462
- finalFileInfo,
463
- outDir,
464
- lang,
465
- file.hash
466
- );
467
-
468
- // 保存文件
469
- await this.templateEngine.saveToFile(renderedHtml, outputPath);
470
-
471
- processedCount++;
472
-
473
- if (verbose && processedCount % 5 === 0) {
474
- console.log(` Processed ${processedCount}/${files.length} files for ${lang}...`);
475
- }
476
- } catch (error) {
477
- console.error(`❌ Failed to process ${file.path} for ${lang}:`, error);
478
- }
479
- }
480
-
481
- if (verbose) {
482
- console.log(` ✅ Built ${processedCount} files for ${lang}`);
483
- }
484
-
485
- return processedCount;
486
- }
487
-
488
- /**
489
- * 生成语言索引页面
490
- */
491
- private async generateLanguageIndex(
492
- langs: string[],
493
- outDir: string,
494
- verbose?: boolean
495
- ): Promise<void> {
496
- try {
497
- const indexHtml = `<!DOCTYPE html>
498
- <html lang="en">
499
- <head>
500
- <meta charset="UTF-8">
501
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
502
- <title>ZEN Documentation - Language Selection</title>
503
- <style>
504
- * { margin: 0; padding: 0; box-sizing: border-box; }
505
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
506
- line-height: 1.6; color: #333; background: #f8f9fa;
507
- display: flex; justify-content: center; align-items: center; min-height: 100vh; }
508
- .container { text-align: center; padding: 3rem; max-width: 600px; }
509
- h1 { font-size: 2.5rem; margin-bottom: 1rem; color: #212529; }
510
- p { color: #6c757d; margin-bottom: 2rem; font-size: 1.125rem; }
511
- .lang-list { list-style: none; display: flex; flex-direction: column; gap: 1rem; }
512
- .lang-item { margin: 0; }
513
- .lang-link { display: block; padding: 1rem 2rem; background: #fff; border: 2px solid #007bff;
514
- color: #007bff; text-decoration: none; border-radius: 8px;
515
- font-size: 1.25rem; font-weight: 500; transition: all 0.2s; }
516
- .lang-link:hover { background: #007bff; color: white; transform: translateY(-2px);
517
- box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2); }
518
- .footer { margin-top: 3rem; color: #6c757d; font-size: 0.875rem; }
519
- </style>
520
- </head>
521
- <body>
522
- <div class="container">
523
- <h1>ZEN Documentation</h1>
524
- <p>Select your preferred language:</p>
525
-
526
- <ul class="lang-list">
527
- ${langs
528
- .map(lang => {
529
- const langNames: Record<string, string> = {
530
- 'zh-Hans': '简体中文',
531
- 'en-US': 'English',
532
- 'ja-JP': '日本語',
533
- 'ko-KR': '한국어',
534
- };
535
- const langName = langNames[lang] || lang;
536
- return `<li class="lang-item">
537
- <a href="${lang}/" class="lang-link">${langName}</a>
538
- </li>`;
539
- })
540
- .join('')}
541
- </ul>
542
-
543
- <div class="footer">
544
- <p>Generated by <strong>ZEN</strong> • <a href="https://github.com/zccz14/ZEN" target="_blank">View on GitHub</a></p>
545
- </div>
546
- </div>
547
- </body>
548
- </html>`;
549
-
550
- const indexPath = path.join(outDir, 'index.html');
551
- await fs.writeFile(indexPath, indexHtml, 'utf-8');
552
-
553
- if (verbose) {
554
- console.log(` ✅ Generated language index at ${indexPath}`);
555
- }
556
- } catch (error) {
557
- console.warn(`⚠️ Failed to generate language index:`, error);
558
- }
559
- }
560
-
561
- /**
562
- * 监听文件变化并自动重建
563
- */
564
- async watch(options: BuildOptions): Promise<void> {
565
- const {
566
- srcDir,
567
- outDir,
568
- template,
569
- verbose = false,
570
- serve = false,
571
- port = 3000,
572
- host = 'localhost',
573
- baseUrl,
574
- } = options;
575
-
576
- console.log(`👀 Watching for changes in ${srcDir}...`);
577
- console.log(`Press Ctrl+C to stop watching`);
578
-
579
- // 初始构建
580
- await this.build(options);
581
-
582
- // 启动 HTTP 服务器(如果启用)
583
- let server: http.Server | null = null;
584
- if (serve) {
585
- server = await this.startHttpServer(outDir, port, host);
586
- console.log(`🌐 HTTP server started at http://${host}:${port}`);
587
- }
588
-
589
- // 创建 GitIgnoreProcessor 并加载 .gitignore 文件
590
- const gitignoreProcessor = new GitIgnoreProcessor(srcDir);
591
- await gitignoreProcessor.loadFromFile();
592
-
593
- // 获取 .gitignore 模式并转换为 chokidar 兼容的正则表达式
594
- const gitignorePatterns = gitignoreProcessor.getPatterns();
595
- const gitignoreRegexes = gitignorePatterns.map(pattern => {
596
- // 将 glob 模式转换为正则表达式
597
- // 注意:这是一个简化的转换,对于复杂的 glob 模式可能需要更复杂的处理
598
- const regexPattern = pattern
599
- .replace(/\./g, '\\.')
600
- .replace(/\*/g, '.*')
601
- .replace(/\?/g, '.')
602
- .replace(/\*\*/g, '.*');
603
- return new RegExp(`(^|[\\/\\\\])${regexPattern}([\\/\\\\].*)?$`);
604
- });
605
-
606
- // 设置文件监听,忽略隐藏文件、.zen 目录和 .gitignore 中的文件
607
- const watcher = chokidar.watch(srcDir, {
608
- ignored: [
609
- /(^|[\/\\])\../, // 忽略隐藏文件
610
- /(^|[\/\\])\.zen($|[\/\\])/, // 忽略 .zen 目录
611
- ...gitignoreRegexes, // 忽略 .gitignore 中的文件
612
- ],
613
- persistent: true,
614
- ignoreInitial: true,
615
- });
616
-
617
- let isBuilding = false;
618
- let buildQueue: string[] = [];
619
-
620
- const debouncedBuild = async () => {
621
- if (isBuilding) {
622
- return;
623
- }
624
-
625
- isBuilding = true;
626
- const changedFiles = [...buildQueue];
627
- buildQueue = [];
628
-
629
- try {
630
- if (verbose) {
631
- console.log(`\n🔄 Rebuilding due to changes in: ${changedFiles.join(', ')}`);
632
- } else {
633
- console.log(`\n🔄 Rebuilding...`);
634
- }
635
-
636
- await this.build(options);
637
- console.log(`✅ Rebuild complete. Watching for changes...`);
638
- } catch (error) {
639
- console.error(`❌ Rebuild failed:`, error);
640
- } finally {
641
- isBuilding = false;
642
-
643
- // 如果队列中有新文件,立即处理
644
- if (buildQueue.length > 0) {
645
- setTimeout(debouncedBuild, 100);
646
- }
647
- }
648
- };
649
-
650
- watcher
651
- .on('add', (filePath: string) => {
652
- // 双重检查:确保文件是 .md 文件且不被 .gitignore 忽略
653
- if (filePath.endsWith('.md') && !gitignoreProcessor.shouldIgnore(filePath)) {
654
- if (verbose) console.log(`📄 File added: ${filePath}`);
655
- buildQueue.push(filePath);
656
- setTimeout(debouncedBuild, 300);
657
- }
658
- })
659
- .on('change', (filePath: string) => {
660
- // 双重检查:确保文件是 .md 文件且不被 .gitignore 忽略
661
- if (filePath.endsWith('.md') && !gitignoreProcessor.shouldIgnore(filePath)) {
662
- if (verbose) console.log(`📄 File changed: ${filePath}`);
663
- buildQueue.push(filePath);
664
- setTimeout(debouncedBuild, 300);
665
- }
666
- })
667
- .on('unlink', (filePath: string) => {
668
- // 双重检查:确保文件是 .md 文件且不被 .gitignore 忽略
669
- if (filePath.endsWith('.md') && !gitignoreProcessor.shouldIgnore(filePath)) {
670
- if (verbose) console.log(`📄 File removed: ${filePath}`);
671
- buildQueue.push(filePath);
672
- setTimeout(debouncedBuild, 300);
673
- }
674
- })
675
- .on('error', (error: unknown) => {
676
- console.error(`❌ Watcher error:`, error);
677
- });
678
-
679
- // 处理退出信号
680
- process.on('SIGINT', () => {
681
- console.log(`\n👋 Stopping watcher...`);
682
- watcher.close();
683
-
684
- // 关闭 HTTP 服务器(如果存在)
685
- if (server) {
686
- console.log(`🌐 Stopping HTTP server...`);
687
- server.close(() => {
688
- console.log(`✅ HTTP server stopped`);
689
- process.exit(0);
690
- });
691
- } else {
692
- process.exit(0);
693
- }
694
- });
695
- }
696
-
697
- /**
698
- * 启动 HTTP 服务器
699
- */
700
- private async startHttpServer(outDir: string, port: number, host: string): Promise<http.Server> {
701
- return new Promise((resolve, reject) => {
702
- const app = express();
703
-
704
- // 提供静态文件服务
705
- app.use(express.static(outDir));
706
-
707
- // 处理 SPA 路由 - 所有未找到的路径返回 index.html
708
- app.get('*', (req: express.Request, res: express.Response) => {
709
- res.sendFile(path.join(outDir, 'index.html'));
710
- });
711
-
712
- const server = app.listen(port, host, () => {
713
- resolve(server);
714
- });
715
-
716
- server.on('error', (error: Error) => {
717
- reject(error);
718
- });
719
- });
720
- }
721
-
722
- /**
723
- * 生成站点地图
724
- */
725
- private async generateSitemap(files: FileInfo[], outDir: string): Promise<void> {
726
- try {
727
- const sitemapXml = this.navigationGenerator.generateSitemap(files, this.config.baseUrl);
728
- const sitemapPath = path.join(outDir, 'sitemap.xml');
729
- await fs.writeFile(sitemapPath, sitemapXml, 'utf-8');
730
- } catch (error) {
731
- console.warn(`⚠️ Failed to generate sitemap:`, error);
732
- }
733
- }
734
-
735
- /**
736
- * 生成导航 JSON 文件
737
- */
738
- private async generateNavigationJson(files: FileInfo[], outDir: string): Promise<void> {
739
- try {
740
- const navigationJson = this.navigationGenerator.generateJsonNavigation(files);
741
- const navPath = path.join(outDir, 'navigation.json');
742
- await fs.writeFile(navPath, navigationJson, 'utf-8');
743
- } catch (error) {
744
- console.warn(`⚠️ Failed to generate navigation JSON:`, error);
745
- }
746
- }
747
-
748
- /**
749
- * 复制静态资源
750
- */
751
- private async copyStaticAssets(srcDir: string, outDir: string): Promise<void> {
752
- const staticDir = path.join(srcDir, 'static');
753
-
754
- try {
755
- await fs.access(staticDir);
756
-
757
- // 简单的递归复制
758
- async function copyDir(source: string, target: string) {
759
- await fs.mkdir(target, { recursive: true });
760
- const entries = await fs.readdir(source, { withFileTypes: true });
761
-
762
- for (const entry of entries) {
763
- const srcPath = path.join(source, entry.name);
764
- const destPath = path.join(target, entry.name);
765
-
766
- if (entry.isDirectory()) {
767
- await copyDir(srcPath, destPath);
768
- } else {
769
- await fs.copyFile(srcPath, destPath);
770
- }
771
- }
772
- }
773
-
774
- await copyDir(staticDir, path.join(outDir, 'static'));
775
- } catch (error) {
776
- // 静态目录不存在是正常的,忽略错误
777
- }
778
- }
779
-
780
- /**
781
- * 确保每个目录都有 index.html 文件
782
- * 为缺少 index.html 的目录创建重定向页面
783
- */
784
- private async ensureDirectoryIndexHtml(outDir: string): Promise<void> {
785
- try {
786
- // 递归遍历所有目录
787
- async function processDirectory(dirPath: string): Promise<void> {
788
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
789
-
790
- // 检查当前目录是否有 index.html
791
- const hasIndexHtml = entries.some(entry => entry.isFile() && entry.name === 'index.html');
792
-
793
- if (!hasIndexHtml) {
794
- // 查找当前目录下的第一个 .html 文件(不包括 index.html)
795
- const htmlFiles = entries
796
- .filter(
797
- entry => entry.isFile() && entry.name.endsWith('.html') && entry.name !== 'index.html'
798
- )
799
- .map(entry => entry.name)
800
- .sort();
801
-
802
- let redirectTarget: string;
803
-
804
- if (htmlFiles.length > 0) {
805
- // 重定向到第一个 .html 文件
806
- redirectTarget = htmlFiles[0];
807
- } else {
808
- // 如果没有 .html 文件,重定向到父目录
809
- const parentDir = path.dirname(dirPath);
810
- if (parentDir === dirPath) {
811
- // 已经是根目录,重定向到根目录的 index.html(如果存在)
812
- redirectTarget = 'index.html';
813
- } else {
814
- // 计算相对路径到父目录
815
- const relativePath = path.relative(dirPath, parentDir);
816
- redirectTarget = path.join(relativePath, 'index.html');
817
- }
818
- }
819
-
820
- // 创建重定向 HTML
821
- const redirectHtml = `<!DOCTYPE html>
822
- <html lang="zh-CN">
823
- <head>
824
- <meta charset="UTF-8">
825
- <meta http-equiv="refresh" content="0; url=${redirectTarget}">
826
- <title>Redirecting...</title>
827
- <script>
828
- window.location.href = "${redirectTarget}";
829
- </script>
830
- </head>
831
- <body>
832
- <p>正在重定向到 <a href="${redirectTarget}">${redirectTarget}</a>...</p>
833
- </body>
834
- </html>`;
835
-
836
- await fs.writeFile(path.join(dirPath, 'index.html'), redirectHtml, 'utf-8');
837
- }
838
-
839
- // 递归处理子目录
840
- for (const entry of entries) {
841
- if (entry.isDirectory()) {
842
- await processDirectory(path.join(dirPath, entry.name));
843
- }
844
- }
845
- }
846
-
847
- await processDirectory(outDir);
848
- } catch (error) {
849
- console.warn(`⚠️ Failed to ensure index.html in directories:`, error);
850
- }
851
- }
852
-
853
- /**
854
- * 清理输出目录
855
- */
856
- async clean(outDir: string): Promise<void> {
857
- try {
858
- await fs.rm(outDir, { recursive: true, force: true });
859
- console.log(`🧹 Cleaned output directory: ${outDir}`);
860
- } catch (error) {
861
- console.error(`❌ Failed to clean output directory:`, error);
862
- }
863
- }
864
-
865
- /**
866
- * 存储母语文件到 .zen/src 目录
867
- */
868
- private async storeNativeFiles(files: FileInfo[], verbose: boolean): Promise<void> {
869
- const aiService = new AIService();
870
-
871
- for (const file of files) {
872
- try {
873
- // 获取源语言(从AI元数据或默认值)
874
- const sourceLang = file.aiMetadata?.inferred_lang || 'zh-Hans';
875
- const nativeHash = file.hash || aiService.calculateFileHash(file.content);
876
-
877
- if (verbose) {
878
- console.log(`📄 Storing native file: ${file.path} (${sourceLang})`);
879
- }
880
-
881
- // 生成母语文件路径
882
- const zenSrcDir = path.join(process.cwd(), '.zen', 'src');
883
- const sourceLangDir = path.join(zenSrcDir, sourceLang);
884
- const nativeFilePath = path.join(sourceLangDir, `${nativeHash}.md`);
885
-
886
- // 确保目录存在
887
- await fs.mkdir(sourceLangDir, { recursive: true });
888
-
889
- // 检查文件是否已存在
890
- try {
891
- await fs.access(nativeFilePath);
892
- if (verbose) {
893
- console.log(` ✅ Native file already exists: ${nativeFilePath}`);
894
- }
895
- } catch (error) {
896
- // 文件不存在,保存母语文件
897
- await fs.writeFile(nativeFilePath, file.content, 'utf-8');
898
- if (verbose) {
899
- console.log(` 💾 Saved native file: ${nativeFilePath}`);
900
- }
901
- }
902
- } catch (error) {
903
- console.error(`❌ Failed to store native file for ${file.path}:`, error);
904
- }
905
- }
906
- }
907
-
908
- /**
909
- * 处理文件翻译
910
- */
911
- private async processTranslations(
912
- files: FileInfo[],
913
- targetLangs: string[],
914
- verbose: boolean
915
- ): Promise<void> {
916
- const aiService = new AIService();
917
-
918
- for (const file of files) {
919
- try {
920
- // 获取文件的AI元数据(包含inferred_lang)
921
- const sourceLang = file.aiMetadata?.inferred_lang || 'zh-Hans';
922
- const nativeHash = file.hash || aiService.calculateFileHash(file.content);
923
-
924
- if (verbose) {
925
- console.log(`📄 Processing translations for: ${file.path} (${sourceLang})`);
926
- }
927
-
928
- for (const targetLang of targetLangs) {
929
- try {
930
- // 确保翻译文件存在
931
- await this.translationService.ensureTranslatedFile(
932
- file,
933
- sourceLang,
934
- targetLang,
935
- nativeHash
936
- );
937
-
938
- if (verbose) {
939
- console.log(` ✅ Translated to ${targetLang}`);
940
- }
941
- } catch (error) {
942
- console.error(` ❌ Failed to translate to ${targetLang}:`, error);
943
- }
944
- }
945
- } catch (error) {
946
- console.error(`❌ Failed to process translations for ${file.path}:`, error);
947
- }
948
- }
949
- }
950
-
951
- validateConfig(config: ZenConfig): string[] {
952
- const errors: string[] = [];
953
-
954
- if (config.srcDir && !path.isAbsolute(config.srcDir)) {
955
- errors.push('srcDir must be an absolute path');
956
- }
957
-
958
- if (config.outDir && !path.isAbsolute(config.outDir)) {
959
- errors.push('outDir must be an absolute path');
960
- }
961
-
962
- if (config.i18n) {
963
- if (!config.i18n.sourceLang) {
964
- errors.push('i18n.sourceLang is required');
965
- }
966
-
967
- if (!config.i18n.targetLangs || config.i18n.targetLangs.length === 0) {
968
- errors.push('i18n.targetLangs must have at least one language');
969
- }
970
- }
971
-
972
- if (config.ai) {
973
- if (config.ai.enabled && !process.env.OPENAI_API_KEY && !config.i18n?.apiKey) {
974
- errors.push('OPENAI_API_KEY environment variable is required when AI is enabled');
975
- }
976
-
977
- if (
978
- config.ai.temperature !== undefined &&
979
- (config.ai.temperature < 0 || config.ai.temperature > 2)
980
- ) {
981
- errors.push('ai.temperature must be between 0 and 2');
982
- }
983
-
984
- if (config.ai.maxTokens !== undefined && config.ai.maxTokens < 1) {
985
- errors.push('ai.maxTokens must be greater than 0');
986
- }
987
- }
988
-
989
- return errors;
990
- }
991
- }