zengen 0.1.34 → 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 +58 -9
- 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 +3 -2
- 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 +69 -9
- 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/template.ts
CHANGED
|
@@ -259,7 +259,7 @@ export class TemplateEngine {
|
|
|
259
259
|
</article>
|
|
260
260
|
|
|
261
261
|
<footer class="footer">
|
|
262
|
-
<p>Generated by <strong>ZEN</strong> • <a href="https://github.com/
|
|
262
|
+
<p>Generated by <strong>ZEN</strong> • <a href="https://github.com/zccz14/ZEN" target="_blank">View on GitHub</a></p>
|
|
263
263
|
</footer>
|
|
264
264
|
</main>
|
|
265
265
|
|
|
@@ -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(
|
|
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
|
|
389
|
+
title,
|
|
338
390
|
content: fileInfo.html || '',
|
|
339
391
|
navigation,
|
|
340
|
-
metadata: fileInfo.
|
|
341
|
-
currentPath: `/${fileInfo.
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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?:
|
|
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();
|
package/docs/ci/github-ci-cd.md
DELETED
|
@@ -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)
|