zengen 0.1.35 → 0.1.36
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/bump-version.yml +112 -0
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/pages.yml +1 -7
- package/.zen/meta.json +57 -0
- package/.zen/translations.json +51 -0
- package/dist/ai-client.d.ts +34 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +180 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-processor.d.ts +51 -0
- package/dist/ai-processor.d.ts.map +1 -0
- package/dist/ai-processor.js +215 -0
- package/dist/ai-processor.js.map +1 -0
- package/dist/ai-service.d.ts +79 -0
- package/dist/ai-service.d.ts.map +1 -0
- package/dist/ai-service.js +257 -0
- package/dist/ai-service.js.map +1 -0
- package/dist/builder.d.ts +26 -2
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +420 -9
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +45 -3
- package/dist/cli.js.map +1 -1
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.d.ts.map +1 -1
- package/dist/gitignore.js +21 -3
- package/dist/gitignore.js.map +1 -1
- package/dist/gitignore.test.js +82 -17
- package/dist/gitignore.test.js.map +1 -1
- package/dist/markdown.d.ts +6 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +31 -9
- package/dist/markdown.js.map +1 -1
- package/dist/navigation.js +5 -5
- package/dist/navigation.js.map +1 -1
- package/dist/scanner.d.ts +26 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +190 -0
- package/dist/scanner.js.map +1 -0
- package/dist/template.d.ts +6 -2
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +57 -8
- package/dist/template.js.map +1 -1
- package/dist/translation-service.d.ts +72 -0
- package/dist/translation-service.d.ts.map +1 -0
- package/dist/translation-service.js +291 -0
- package/dist/translation-service.js.map +1 -0
- package/dist/types.d.ts +35 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/advanced-usage.md +39 -0
- package/docs/getting-started.md +26 -0
- package/docs/guides/best-practices.md +0 -113
- package/docs/guides/config.md +0 -233
- package/package.json +2 -1
- package/src/ai-client.ts +227 -0
- package/src/ai-processor.ts +243 -0
- package/src/ai-service.ts +281 -0
- package/src/builder.ts +543 -10
- package/src/cli.ts +49 -3
- package/src/gitignore.test.ts +82 -17
- package/src/gitignore.ts +23 -3
- package/src/markdown.ts +39 -11
- package/src/navigation.ts +5 -5
- package/src/scanner.ts +180 -0
- package/src/template.ts +68 -8
- package/src/translation-service.ts +350 -0
- package/src/types.ts +39 -3
- package/test-multilang.js +44 -0
- package/docs/ci/github-ci-cd.md +0 -127
- package/docs/guides/api.md +0 -277
package/src/builder.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BuildOptions,
|
|
3
|
+
FileInfo,
|
|
4
|
+
NavigationItem,
|
|
5
|
+
ZenConfig,
|
|
6
|
+
ScannedFile,
|
|
7
|
+
MultiLangBuildOptions,
|
|
8
|
+
} from './types';
|
|
2
9
|
import { MarkdownConverter } from './markdown';
|
|
3
10
|
import { TemplateEngine } from './template';
|
|
4
11
|
import { NavigationGenerator } from './navigation';
|
|
5
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';
|
|
6
17
|
import * as fs from 'fs/promises';
|
|
7
18
|
import * as path from 'path';
|
|
8
19
|
import * as chokidar from 'chokidar';
|
|
@@ -13,13 +24,32 @@ export class ZenBuilder {
|
|
|
13
24
|
private markdownConverter: MarkdownConverter;
|
|
14
25
|
private templateEngine: TemplateEngine;
|
|
15
26
|
private navigationGenerator: NavigationGenerator;
|
|
27
|
+
private scanner: Scanner;
|
|
28
|
+
private aiProcessor: AIProcessor;
|
|
29
|
+
private translationService: TranslationService;
|
|
16
30
|
private config: ZenConfig = {};
|
|
17
31
|
|
|
18
32
|
constructor(config: ZenConfig = {}) {
|
|
19
33
|
this.config = config;
|
|
20
|
-
|
|
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);
|
|
21
50
|
this.templateEngine = new TemplateEngine();
|
|
22
51
|
this.navigationGenerator = new NavigationGenerator(config.baseUrl);
|
|
52
|
+
this.scanner = new Scanner(config);
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
/**
|
|
@@ -27,13 +57,16 @@ export class ZenBuilder {
|
|
|
27
57
|
*/
|
|
28
58
|
async build(options: BuildOptions): Promise<void> {
|
|
29
59
|
const startTime = Date.now();
|
|
30
|
-
const { srcDir, outDir, template, verbose = false, baseUrl } = options;
|
|
60
|
+
const { srcDir, outDir, template, verbose = false, baseUrl, langs } = options;
|
|
31
61
|
|
|
32
62
|
if (verbose) {
|
|
33
63
|
console.log(`🚀 Starting ZEN build...`);
|
|
34
64
|
console.log(`📁 Source: ${srcDir}`);
|
|
35
65
|
console.log(`📁 Output: ${outDir}`);
|
|
36
66
|
console.log(`🔗 Base URL: ${baseUrl || '(not set)'}`);
|
|
67
|
+
if (langs && langs.length > 0) {
|
|
68
|
+
console.log(`🌐 Target languages: ${langs.join(', ')}`);
|
|
69
|
+
}
|
|
37
70
|
console.log(`🔍 Verbose mode enabled`);
|
|
38
71
|
}
|
|
39
72
|
|
|
@@ -47,16 +80,76 @@ export class ZenBuilder {
|
|
|
47
80
|
// 确保输出目录存在
|
|
48
81
|
await fs.mkdir(outDir, { recursive: true });
|
|
49
82
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
const
|
|
83
|
+
// 确保 .zen/.gitignore 文件存在且内容正确
|
|
84
|
+
const zenDir = path.dirname(outDir); // .zen 目录
|
|
85
|
+
const zenGitignorePath = path.join(zenDir, '.gitignore');
|
|
86
|
+
const gitignoreContent = 'dist\n';
|
|
53
87
|
|
|
54
|
-
|
|
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) {
|
|
55
109
|
console.warn(`⚠️ No Markdown files found in ${srcDir}`);
|
|
56
110
|
return;
|
|
57
111
|
}
|
|
58
112
|
|
|
59
|
-
if (verbose) console.log(`✅ Found ${
|
|
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
|
+
}
|
|
60
153
|
|
|
61
154
|
// 更新导航生成器的 baseUrl(优先使用命令行参数)
|
|
62
155
|
if (baseUrl !== undefined) {
|
|
@@ -95,7 +188,7 @@ export class ZenBuilder {
|
|
|
95
188
|
console.log(` Processed ${processedCount}/${files.length} files...`);
|
|
96
189
|
}
|
|
97
190
|
} catch (error) {
|
|
98
|
-
console.error(`❌ Failed to process ${file.
|
|
191
|
+
console.error(`❌ Failed to process ${file.path}:`, error);
|
|
99
192
|
}
|
|
100
193
|
}
|
|
101
194
|
|
|
@@ -125,6 +218,346 @@ export class ZenBuilder {
|
|
|
125
218
|
}
|
|
126
219
|
}
|
|
127
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
|
+
|
|
128
561
|
/**
|
|
129
562
|
* 监听文件变化并自动重建
|
|
130
563
|
*/
|
|
@@ -430,8 +863,91 @@ export class ZenBuilder {
|
|
|
430
863
|
}
|
|
431
864
|
|
|
432
865
|
/**
|
|
433
|
-
*
|
|
866
|
+
* 存储母语文件到 .zen/src 目录
|
|
434
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
|
+
|
|
435
951
|
validateConfig(config: ZenConfig): string[] {
|
|
436
952
|
const errors: string[] = [];
|
|
437
953
|
|
|
@@ -453,6 +969,23 @@ export class ZenBuilder {
|
|
|
453
969
|
}
|
|
454
970
|
}
|
|
455
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
|
+
|
|
456
989
|
return errors;
|
|
457
990
|
}
|
|
458
991
|
}
|