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/template.ts CHANGED
@@ -268,6 +268,35 @@ export class TemplateEngine {
268
268
  </body>
269
269
  </html>`;
270
270
 
271
+ /**
272
+ * 生成语言切换器 HTML
273
+ */
274
+ private generateLanguageSwitcher(currentLang: string, availableLangs: string[]): string {
275
+ const langNames: Record<string, string> = {
276
+ 'zh-Hans': '简体中文',
277
+ 'en-US': 'English',
278
+ 'ja-JP': '日本語',
279
+ 'ko-KR': '한국어',
280
+ };
281
+
282
+ const items = availableLangs
283
+ .map(lang => {
284
+ const langName = langNames[lang] || lang;
285
+ const isCurrent = lang === currentLang;
286
+ const activeClass = isCurrent ? 'active' : '';
287
+
288
+ return `<li class="lang-item ${activeClass}">
289
+ <a href="?lang=${lang}" class="lang-link">${langName}</a>
290
+ </li>`;
291
+ })
292
+ .join('');
293
+
294
+ return `<div class="language-switcher">
295
+ <span class="lang-label">Language:</span>
296
+ <ul class="lang-list">${items}</ul>
297
+ </div>`;
298
+ }
299
+
271
300
  /**
272
301
  * 生成导航 HTML
273
302
  */
@@ -306,6 +335,21 @@ export class TemplateEngine {
306
335
  result = result.replace(/{{title}}/g, data.title || 'Untitled');
307
336
  result = result.replace(/{{{content}}}/g, data.content || '');
308
337
 
338
+ // 替换元数据变量
339
+ if (data.metadata) {
340
+ result = result.replace(/{{summary}}/g, data.metadata.summary || '');
341
+ result = result.replace(/{{tags}}/g, data.metadata.tags?.join(', ') || '');
342
+ result = result.replace(/{{inferred_date}}/g, data.metadata.inferred_date || '');
343
+ result = result.replace(/{{inferred_lang}}/g, data.metadata.inferred_lang || '');
344
+ }
345
+
346
+ // 替换语言相关变量
347
+ result = result.replace(/{{lang}}/g, data.lang || '');
348
+ if (data.availableLangs && data.availableLangs.length > 0 && data.lang) {
349
+ const langSwitcher = this.generateLanguageSwitcher(data.lang, data.availableLangs);
350
+ result = result.replace('{{language_switcher}}', langSwitcher);
351
+ }
352
+
309
353
  return result;
310
354
  }
311
355
 
@@ -332,23 +376,39 @@ export class TemplateEngine {
332
376
  /**
333
377
  * 从文件信息生成模板数据
334
378
  */
335
- generateTemplateData(fileInfo: FileInfo, navigation: NavigationItem[]): TemplateData {
379
+ generateTemplateData(
380
+ fileInfo: FileInfo,
381
+ navigation: NavigationItem[],
382
+ lang?: string,
383
+ availableLangs?: string[]
384
+ ): TemplateData {
385
+ // 优先使用 AI 元数据中的标题,其次使用文件元数据,最后使用文件名
386
+ const title = fileInfo.aiMetadata?.title || fileInfo.metadata?.title || fileInfo.name;
387
+
336
388
  return {
337
- title: fileInfo.metadata?.title || fileInfo.name, // 优先使用提取的标题,如果没有则使用文件名
389
+ title,
338
390
  content: fileInfo.html || '',
339
391
  navigation,
340
- metadata: fileInfo.metadata,
341
- currentPath: `/${fileInfo.relativePath.replace(/\.md$/, '.html')}`,
392
+ metadata: fileInfo.aiMetadata, // 使用完整的 AI 元数据
393
+ currentPath: `/${fileInfo.path.replace(/\.md$/, '.html')}`,
394
+ lang,
395
+ availableLangs,
342
396
  };
343
397
  }
344
398
 
345
399
  /**
346
400
  * 生成输出文件路径
347
401
  */
348
- getOutputPath(fileInfo: FileInfo, outDir: string): string {
349
- const htmlFileName = `${fileInfo.name}.html`;
350
- const relativeDir = path.dirname(fileInfo.relativePath);
351
- return path.join(outDir, relativeDir, htmlFileName);
402
+ getOutputPath(fileInfo: FileInfo, outDir: string, lang?: string, hash?: string): string {
403
+ if (lang && hash) {
404
+ // 多语言模式:.zen/dist/{lang}/{hash}.html
405
+ return path.join(outDir, lang, `${hash}.html`);
406
+ } else {
407
+ // 传统模式:保持目录结构
408
+ const htmlFileName = `${fileInfo.name}.html`;
409
+ const relativeDir = path.dirname(fileInfo.path);
410
+ return path.join(outDir, relativeDir, htmlFileName);
411
+ }
352
412
  }
353
413
 
354
414
  /**
@@ -0,0 +1,350 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import * as crypto from 'crypto';
4
+ import { AIMetadata, FileInfo } from './types';
5
+ import { AIService } from './ai-service';
6
+
7
+ /**
8
+ * 翻译缓存项
9
+ */
10
+ export interface TranslationCache {
11
+ sourceHash: string; // 源文件hash
12
+ sourceLang: string; // 源语言
13
+ targetLang: string; // 目标语言
14
+ translatedContent: string; // 翻译后的内容
15
+ lastUpdated: string; // 最后更新时间
16
+ }
17
+
18
+ /**
19
+ * 翻译服务配置
20
+ */
21
+ export interface TranslationConfig {
22
+ enabled: boolean;
23
+ apiKey: string;
24
+ baseUrl: string;
25
+ model: string;
26
+ temperature: number;
27
+ maxTokens: number;
28
+ }
29
+
30
+ /**
31
+ * 翻译服务类
32
+ */
33
+ export class TranslationService {
34
+ private config: TranslationConfig;
35
+ private aiService: AIService;
36
+ private translationCachePath: string;
37
+
38
+ constructor(config: Partial<TranslationConfig> = {}) {
39
+ // 从环境变量读取配置
40
+ const apiKey = process.env.OPENAI_API_KEY || '';
41
+ const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
42
+
43
+ // 配置优先级:构造函数参数 > 环境变量 > 默认值
44
+ const model = config.model || process.env.OPENAI_MODEL || 'gpt-3.5-turbo';
45
+
46
+ this.config = {
47
+ enabled: config.enabled ?? apiKey !== '',
48
+ apiKey,
49
+ baseUrl,
50
+ model,
51
+ temperature: 0, // 总是设置为 0,翻译不需要随机性
52
+ maxTokens: config.maxTokens || 2000,
53
+ };
54
+
55
+ this.aiService = new AIService();
56
+ this.translationCachePath = path.join(process.cwd(), '.zen', 'translations.json');
57
+ }
58
+
59
+ /**
60
+ * 检查是否启用翻译功能
61
+ */
62
+ isEnabled(): boolean {
63
+ const enabled = this.config.enabled && this.config.apiKey !== '';
64
+ if (!enabled && this.config.enabled) {
65
+ console.warn(
66
+ '⚠️ Translation is enabled but API key is missing. Please set OPENAI_API_KEY environment variable.'
67
+ );
68
+ }
69
+ return enabled;
70
+ }
71
+
72
+ /**
73
+ * 加载翻译缓存
74
+ */
75
+ async loadTranslationCache(): Promise<TranslationCache[]> {
76
+ try {
77
+ await fs.access(this.translationCachePath);
78
+ const content = await fs.readFile(this.translationCachePath, 'utf-8');
79
+ return JSON.parse(content);
80
+ } catch (error) {
81
+ // 如果文件不存在,返回空数组
82
+ return [];
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 保存翻译缓存
88
+ */
89
+ async saveTranslationCache(cache: TranslationCache[]): Promise<void> {
90
+ // 确保 .zen 目录存在
91
+ const zenDir = path.dirname(this.translationCachePath);
92
+ await fs.mkdir(zenDir, { recursive: true });
93
+
94
+ // 保存文件
95
+ await fs.writeFile(this.translationCachePath, JSON.stringify(cache, null, 2), 'utf-8');
96
+ }
97
+
98
+ /**
99
+ * 获取缓存的翻译
100
+ */
101
+ async getCachedTranslation(
102
+ sourceHash: string,
103
+ sourceLang: string,
104
+ targetLang: string
105
+ ): Promise<string | null> {
106
+ if (!this.isEnabled()) {
107
+ return null;
108
+ }
109
+
110
+ try {
111
+ const cache = await this.loadTranslationCache();
112
+ const cachedTranslation = cache.find(
113
+ item =>
114
+ item.sourceHash === sourceHash &&
115
+ item.sourceLang === sourceLang &&
116
+ item.targetLang === targetLang
117
+ );
118
+
119
+ if (cachedTranslation) {
120
+ console.log(`📚 Using cached translation for ${sourceHash} (${sourceLang} → ${targetLang})`);
121
+ return cachedTranslation.translatedContent;
122
+ }
123
+ } catch (error) {
124
+ console.warn(`⚠️ Failed to load translation cache:`, error);
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * 缓存翻译结果
132
+ */
133
+ async cacheTranslation(
134
+ sourceHash: string,
135
+ sourceLang: string,
136
+ targetLang: string,
137
+ translatedContent: string
138
+ ): Promise<void> {
139
+ if (!this.isEnabled()) {
140
+ return;
141
+ }
142
+
143
+ try {
144
+ const cache = await this.loadTranslationCache();
145
+
146
+ // 查找是否已存在相同翻译
147
+ const existingIndex = cache.findIndex(
148
+ item =>
149
+ item.sourceHash === sourceHash &&
150
+ item.sourceLang === sourceLang &&
151
+ item.targetLang === targetLang
152
+ );
153
+
154
+ if (existingIndex >= 0) {
155
+ // 更新现有缓存
156
+ cache[existingIndex] = {
157
+ sourceHash,
158
+ sourceLang,
159
+ targetLang,
160
+ translatedContent,
161
+ lastUpdated: new Date().toISOString(),
162
+ };
163
+ } else {
164
+ // 添加新缓存
165
+ cache.push({
166
+ sourceHash,
167
+ sourceLang,
168
+ targetLang,
169
+ translatedContent,
170
+ lastUpdated: new Date().toISOString(),
171
+ });
172
+ }
173
+
174
+ await this.saveTranslationCache(cache);
175
+ console.log(`💾 Cached translation for ${sourceHash} (${sourceLang} → ${targetLang})`);
176
+ } catch (error) {
177
+ console.warn(`⚠️ Failed to cache translation:`, error);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 使用AI翻译内容
183
+ */
184
+ async translateWithAI(
185
+ content: string,
186
+ sourceLang: string,
187
+ targetLang: string
188
+ ): Promise<string> {
189
+ if (!this.isEnabled()) {
190
+ throw new Error('Translation service is not enabled');
191
+ }
192
+
193
+ const prompt = `请将以下${sourceLang}文本翻译成${targetLang}。保持Markdown格式不变,只翻译文本内容:
194
+
195
+ ${content}
196
+
197
+ 翻译结果(保持原格式):`;
198
+
199
+ try {
200
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
201
+ method: 'POST',
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ 'Authorization': `Bearer ${this.config.apiKey}`,
205
+ },
206
+ body: JSON.stringify({
207
+ model: this.config.model,
208
+ messages: [
209
+ {
210
+ role: 'system',
211
+ content: '你是一个专业的翻译助手,擅长将文档翻译成不同语言,同时保持原有的格式和结构。'
212
+ },
213
+ {
214
+ role: 'user',
215
+ content: prompt
216
+ }
217
+ ],
218
+ temperature: this.config.temperature,
219
+ max_tokens: this.config.maxTokens,
220
+ }),
221
+ });
222
+
223
+ if (!response.ok) {
224
+ const errorText = await response.text();
225
+ throw new Error(`Translation API error: ${response.status} ${errorText}`);
226
+ }
227
+
228
+ const data = await response.json();
229
+ const translatedContent = data.choices[0]?.message?.content?.trim() || '';
230
+
231
+ if (!translatedContent) {
232
+ throw new Error('Empty translation response');
233
+ }
234
+
235
+ return translatedContent;
236
+ } catch (error) {
237
+ console.error(`❌ Translation failed:`, error);
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 翻译文件
244
+ */
245
+ async translateFile(
246
+ fileInfo: FileInfo,
247
+ sourceLang: string,
248
+ targetLang: string
249
+ ): Promise<string> {
250
+ const sourceHash = fileInfo.hash || this.aiService.calculateFileHash(fileInfo.content);
251
+
252
+ // 检查缓存
253
+ const cachedTranslation = await this.getCachedTranslation(sourceHash, sourceLang, targetLang);
254
+ if (cachedTranslation) {
255
+ return cachedTranslation;
256
+ }
257
+
258
+ // 如果目标语言与源语言相同,直接返回原内容
259
+ if (sourceLang === targetLang) {
260
+ console.log(`📝 Skipping translation (same language): ${sourceLang} → ${targetLang}`);
261
+ await this.cacheTranslation(sourceHash, sourceLang, targetLang, fileInfo.content);
262
+ return fileInfo.content;
263
+ }
264
+
265
+ // 使用AI翻译
266
+ console.log(`🌐 Translating from ${sourceLang} to ${targetLang}...`);
267
+ const translatedContent = await this.translateWithAI(fileInfo.content, sourceLang, targetLang);
268
+
269
+ // 缓存结果
270
+ await this.cacheTranslation(sourceHash, sourceLang, targetLang, translatedContent);
271
+
272
+ return translatedContent;
273
+ }
274
+
275
+ /**
276
+ * 生成翻译后的文件路径
277
+ */
278
+ getTranslatedFilePath(
279
+ originalPath: string,
280
+ targetLang: string,
281
+ nativeHash: string
282
+ ): string {
283
+ const zenSrcDir = path.join(process.cwd(), '.zen', 'src');
284
+ const langDir = path.join(zenSrcDir, targetLang);
285
+ const fileName = `${nativeHash}.md`;
286
+ return path.join(langDir, fileName);
287
+ }
288
+
289
+ /**
290
+ * 确保翻译文件存在
291
+ */
292
+ async ensureTranslatedFile(
293
+ fileInfo: FileInfo,
294
+ sourceLang: string,
295
+ targetLang: string,
296
+ nativeHash: string
297
+ ): Promise<string> {
298
+ const translatedFilePath = this.getTranslatedFilePath(fileInfo.path, targetLang, nativeHash);
299
+
300
+ try {
301
+ // 检查文件是否已存在
302
+ await fs.access(translatedFilePath);
303
+ console.log(`📄 Translation file already exists: ${translatedFilePath}`);
304
+
305
+ // 读取现有内容
306
+ const existingContent = await fs.readFile(translatedFilePath, 'utf-8');
307
+ return existingContent;
308
+ } catch (error) {
309
+ // 文件不存在,需要翻译
310
+ console.log(`🔄 Creating translation file: ${translatedFilePath}`);
311
+
312
+ // 翻译内容
313
+ const translatedContent = await this.translateFile(fileInfo, sourceLang, targetLang);
314
+
315
+ // 确保目录存在
316
+ const dirPath = path.dirname(translatedFilePath);
317
+ await fs.mkdir(dirPath, { recursive: true });
318
+
319
+ // 保存翻译文件
320
+ await fs.writeFile(translatedFilePath, translatedContent, 'utf-8');
321
+
322
+ return translatedContent;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * 清理过期的翻译缓存
328
+ */
329
+ async cleanupCache(maxAgeDays: number = 30): Promise<void> {
330
+ try {
331
+ const cache = await this.loadTranslationCache();
332
+ const cutoffTime = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
333
+ const originalCount = cache.length;
334
+
335
+ // 过滤掉过期的缓存
336
+ const filteredCache = cache.filter(item => {
337
+ const itemTime = new Date(item.lastUpdated).getTime();
338
+ return itemTime >= cutoffTime;
339
+ });
340
+
341
+ const cleanedCount = originalCount - filteredCache.length;
342
+ if (cleanedCount > 0) {
343
+ await this.saveTranslationCache(filteredCache);
344
+ console.log(`🧹 Cleaned ${cleanedCount} expired translation cache entries`);
345
+ }
346
+ } catch (error) {
347
+ console.warn(`⚠️ Failed to cleanup translation cache:`, error);
348
+ }
349
+ }
350
+ }
package/src/types.ts CHANGED
@@ -8,16 +8,38 @@ export interface BuildOptions {
8
8
  port?: number;
9
9
  host?: string;
10
10
  baseUrl?: string;
11
+ langs?: string[]; // 目标语言数组
12
+ }
13
+
14
+ export interface ScannedFile {
15
+ path: string; // 相对路径
16
+ name: string;
17
+ ext: string;
18
+ hash?: string; // 文件内容的 sha256 hash
11
19
  }
12
20
 
13
21
  export interface FileInfo {
14
- path: string;
15
- relativePath: string;
22
+ path: string; // 相对路径
16
23
  name: string;
17
24
  ext: string;
18
25
  content: string;
19
26
  html?: string;
20
27
  metadata?: { title: string }; // 简化,只保留标题
28
+ hash?: string; // 文件内容的 sha256 hash
29
+ aiMetadata?: AIMetadata; // AI 提取的元数据
30
+ }
31
+
32
+ export interface AIMetadata {
33
+ title: string; // AI 提取的标题
34
+ summary: string; // AI 提取的摘要,控制在 100字以内
35
+ tags: string[]; // AI 提取的关键字
36
+ inferred_date?: string; // 正文中隐含的文档创建日期,没有就留空
37
+ inferred_lang: string; // 文章使用的语言,例如 zh-Hans 或者 en-US
38
+ tokens_used?: {
39
+ prompt: number;
40
+ completion: number;
41
+ total: number;
42
+ }; // tokens 使用情况
21
43
  }
22
44
 
23
45
  export interface NavigationItem {
@@ -30,8 +52,10 @@ export interface TemplateData {
30
52
  title: string;
31
53
  content: string;
32
54
  navigation: NavigationItem[];
33
- metadata?: { title: string }; // 简化,只保留标题
55
+ metadata?: AIMetadata; // 使用完整的 AI 元数据
34
56
  currentPath?: string;
57
+ lang?: string; // 当前语言
58
+ availableLangs?: string[]; // 可用的语言列表
35
59
  }
36
60
 
37
61
  export interface MarkdownProcessor {
@@ -49,7 +73,19 @@ export interface ZenConfig {
49
73
  targetLangs: string[];
50
74
  apiKey?: string;
51
75
  };
76
+ ai?: {
77
+ enabled?: boolean;
78
+ model?: string;
79
+ temperature?: number;
80
+ maxTokens?: number;
81
+ };
52
82
  processors?: MarkdownProcessor[];
53
83
  includePattern?: string;
54
84
  excludePattern?: string;
55
85
  }
86
+
87
+ export interface MultiLangBuildOptions extends BuildOptions {
88
+ langs: string[]; // 必须指定目标语言
89
+ useMetaData?: boolean; // 是否使用 meta.json 中的元数据
90
+ filterOrphans?: boolean; // 是否过滤孤儿文件
91
+ }
@@ -0,0 +1,44 @@
1
+ const { ZenBuilder } = require('./dist/builder');
2
+
3
+ async function testMultiLangBuild() {
4
+ console.log('Testing multi-language build...\n');
5
+
6
+ const builder = new ZenBuilder();
7
+
8
+ try {
9
+ await builder.buildMultiLang({
10
+ srcDir: './docs',
11
+ outDir: './.zen/dist',
12
+ langs: ['zh-Hans', 'en-US'],
13
+ verbose: true,
14
+ useMetaData: true,
15
+ filterOrphans: true,
16
+ });
17
+
18
+ console.log('\n✅ Multi-language build test completed successfully!');
19
+
20
+ // 检查生成的文件
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const distDir = './.zen/dist';
25
+ if (fs.existsSync(distDir)) {
26
+ console.log('\n📁 Generated files:');
27
+ const listFiles = (dir, indent = '') => {
28
+ const items = fs.readdirSync(dir, { withFileTypes: true });
29
+ for (const item of items) {
30
+ console.log(`${indent}${item.name}${item.isDirectory() ? '/' : ''}`);
31
+ if (item.isDirectory()) {
32
+ listFiles(path.join(dir, item.name), indent + ' ');
33
+ }
34
+ }
35
+ };
36
+ listFiles(distDir);
37
+ }
38
+ } catch (error) {
39
+ console.error('\n❌ Multi-language build test failed:', error);
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ testMultiLangBuild();
@@ -1,127 +0,0 @@
1
- # GitHub CI/CD 配置指南
2
-
3
- 本文档描述了如何为 zengen 项目配置 GitHub Actions 进行可信发布。
4
-
5
- ## 概述
6
-
7
- 我们配置了三个 GitHub Actions workflows:
8
-
9
- 1. **CI** (`ci.yml`) - 代码质量检查和测试
10
- 2. **Version Management** (`version.yml`) - 版本管理和自动创建 Release
11
- 3. **Publish to npm** (`publish.yml`) - 可信发布到 npm registry
12
-
13
- ## 配置步骤
14
-
15
- ### 1. 设置 npm 认证令牌
16
-
17
- 由于 npm 经典令牌已被撤销,需要使用新的认证方式:
18
-
19
- #### 选项 A: 使用细粒度访问令牌(推荐)
20
-
21
- 1. 登录 [npmjs.com](https://www.npmjs.com)
22
- 2. 进入 Account Settings → Access Tokens
23
- 3. 点击 "Create New Token"
24
- 4. 选择 "Automation" 类型
25
- 5. 配置权限:
26
- - 读取和写入包
27
- - 绕过 2FA(用于自动化工作流)
28
- 6. 设置有效期(最长 90 天)
29
- 7. 复制生成的令牌
30
-
31
- #### 选项 B: 使用 OIDC 可信发布
32
-
33
- 1. 确保在 GitHub 仓库设置中启用了 OIDC
34
- 2. 配置 npm 以信任 GitHub Actions 的 OIDC 令牌
35
-
36
- ### 2. 配置 GitHub Actions 权限
37
-
38
- 确保仓库有以下权限设置:
39
-
40
- - Settings → Actions → General
41
- - Workflow permissions: 选择 "Read and write permissions"
42
- - 确保启用了 OIDC 支持
43
-
44
- **注意**:对于 npm OIDC 发布,不需要配置 `NPM_TOKEN` secret。GitHub Actions 会自动使用 OIDC 令牌进行认证。
45
-
46
- ## 工作流程
47
-
48
- ### CI 流程
49
-
50
- 1. 当有 push 到 main 分支或创建 PR 时触发
51
- 2. 在多个 Node.js 版本上运行测试
52
- 3. 执行代码质量检查
53
- 4. 构建包并验证
54
-
55
- ### 版本管理流程
56
-
57
- 1. 当 package.json 版本变更时触发
58
- 2. 自动创建 GitHub Release
59
- 3. 生成基于提交历史的 changelog
60
-
61
- ### 发布流程
62
-
63
- 1. 当创建 GitHub Release 时触发
64
- 2. 使用 OIDC 进行可信认证
65
- 3. 构建并发布到 npm
66
- 4. 生成来源证明(provenance)
67
-
68
- ## 手动发布
69
-
70
- 如果需要手动发布:
71
-
72
- 1. 更新 `package.json` 中的版本号
73
- 2. 提交并推送到 main 分支
74
- 3. 版本管理 workflow 会自动创建 Release
75
- 4. 发布 workflow 会自动发布到 npm
76
-
77
- 或者使用 workflow_dispatch:
78
-
79
- 1. 前往 Actions → Publish to npm
80
- 2. 点击 "Run workflow"
81
- 3. 选择分支并运行
82
-
83
- ## 安全注意事项
84
-
85
- 1. **令牌安全**:
86
- - 永远不要在代码中硬编码令牌
87
- - 使用 GitHub Secrets 存储敏感信息
88
- - 定期轮换令牌
89
-
90
- 2. **OIDC 优势**:
91
- - 消除长期令牌的管理
92
- - 自动化的短期会话令牌
93
- - 更好的安全审计
94
-
95
- 3. **来源证明**:
96
- - 使用 `--provenance` 标志发布
97
- - 验证包的构建环境和来源
98
- - 增加用户信任
99
-
100
- ## 故障排除
101
-
102
- ### 常见问题
103
-
104
- 1. **认证失败**:
105
- - 确保仓库启用了 OIDC 支持
106
- - 检查 workflow 中设置了 `permissions: id-token: write`
107
- - 验证 npm 版本是否支持 OIDC(需要 npm 9.0.0+)
108
-
109
- 2. **版本检测失败**:
110
- - 确保 `fetch-depth: 0` 以获取完整历史
111
- - 检查 package.json 格式是否正确
112
-
113
- 3. **构建失败**:
114
- - 检查 Node.js 版本兼容性
115
- - 验证 TypeScript 配置
116
- - 确保所有依赖项已安装
117
-
118
- ### 调试
119
-
120
- - 查看 GitHub Actions 日志
121
- - 启用调试日志:在仓库 Settings → Actions → Runner 中设置 secret `ACTIONS_STEP_DEBUG` 为 `true`
122
-
123
- ## 相关链接
124
-
125
- - [npm 认证变更公告](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/)
126
- - [GitHub Actions npm 发布文档](https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages)
127
- - [npm 可信发布文档](https://docs.npmjs.com/trusted-publishing-with-oidc)