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.
Files changed (70) hide show
  1. package/.github/workflows/bump-version.yml +112 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/pages.yml +1 -7
  4. package/.zen/meta.json +57 -0
  5. package/.zen/translations.json +51 -0
  6. package/dist/ai-client.d.ts +34 -0
  7. package/dist/ai-client.d.ts.map +1 -0
  8. package/dist/ai-client.js +180 -0
  9. package/dist/ai-client.js.map +1 -0
  10. package/dist/ai-processor.d.ts +51 -0
  11. package/dist/ai-processor.d.ts.map +1 -0
  12. package/dist/ai-processor.js +215 -0
  13. package/dist/ai-processor.js.map +1 -0
  14. package/dist/ai-service.d.ts +79 -0
  15. package/dist/ai-service.d.ts.map +1 -0
  16. package/dist/ai-service.js +257 -0
  17. package/dist/ai-service.js.map +1 -0
  18. package/dist/builder.d.ts +26 -2
  19. package/dist/builder.d.ts.map +1 -1
  20. package/dist/builder.js +420 -9
  21. package/dist/builder.js.map +1 -1
  22. package/dist/cli.js +45 -3
  23. package/dist/cli.js.map +1 -1
  24. package/dist/gitignore.d.ts +2 -1
  25. package/dist/gitignore.d.ts.map +1 -1
  26. package/dist/gitignore.js +21 -3
  27. package/dist/gitignore.js.map +1 -1
  28. package/dist/gitignore.test.js +82 -17
  29. package/dist/gitignore.test.js.map +1 -1
  30. package/dist/markdown.d.ts +6 -1
  31. package/dist/markdown.d.ts.map +1 -1
  32. package/dist/markdown.js +31 -9
  33. package/dist/markdown.js.map +1 -1
  34. package/dist/navigation.js +5 -5
  35. package/dist/navigation.js.map +1 -1
  36. package/dist/scanner.d.ts +26 -0
  37. package/dist/scanner.d.ts.map +1 -0
  38. package/dist/scanner.js +190 -0
  39. package/dist/scanner.js.map +1 -0
  40. package/dist/template.d.ts +6 -2
  41. package/dist/template.d.ts.map +1 -1
  42. package/dist/template.js +57 -8
  43. package/dist/template.js.map +1 -1
  44. package/dist/translation-service.d.ts +72 -0
  45. package/dist/translation-service.d.ts.map +1 -0
  46. package/dist/translation-service.js +291 -0
  47. package/dist/translation-service.js.map +1 -0
  48. package/dist/types.d.ts +35 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/docs/advanced-usage.md +39 -0
  51. package/docs/getting-started.md +26 -0
  52. package/docs/guides/best-practices.md +0 -113
  53. package/docs/guides/config.md +0 -233
  54. package/package.json +2 -1
  55. package/src/ai-client.ts +227 -0
  56. package/src/ai-processor.ts +243 -0
  57. package/src/ai-service.ts +281 -0
  58. package/src/builder.ts +543 -10
  59. package/src/cli.ts +49 -3
  60. package/src/gitignore.test.ts +82 -17
  61. package/src/gitignore.ts +23 -3
  62. package/src/markdown.ts +39 -11
  63. package/src/navigation.ts +5 -5
  64. package/src/scanner.ts +180 -0
  65. package/src/template.ts +68 -8
  66. package/src/translation-service.ts +350 -0
  67. package/src/types.ts +39 -3
  68. package/test-multilang.js +44 -0
  69. package/docs/ci/github-ci-cd.md +0 -127
  70. package/docs/guides/api.md +0 -277
package/src/builder.ts CHANGED
@@ -1,8 +1,19 @@
1
- import { BuildOptions, FileInfo, NavigationItem, ZenConfig } from './types';
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
- this.markdownConverter = new MarkdownConverter(config.processors || []);
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
- // 读取并转换 Markdown 文件
51
- if (verbose) console.log(`📄 Reading Markdown files...`);
52
- const files = await this.markdownConverter.convertDirectory(srcDir);
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
- if (files.length === 0) {
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 ${files.length} Markdown files`);
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.relativePath}:`, error);
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
  }