diff --git a/README-zh.md b/README-zh.md index f028243..e2a79c8 100644 --- a/README-zh.md +++ b/README-zh.md @@ -18,19 +18,23 @@ ## 特性 -- 将 Markdown 文件翻译成 OpenAI 模型支持的任何语言。 -- 在翻译过程中保留 Markdown 语法。 -- 通过命令行参数或环境变量灵活配置。 +- 将 Markdown 文件翻译为 OpenAI 模型支持的任何语言 +- 在翻译过程中保留 Markdown 语法 +- 支持递归目录翻译 +- 失败翻译的自动重试机制 +- 全面的日志记录系统 +- 目录结构可视化 +- 文件失败跟踪和恢复 ## 先决条件 -- Node.js (v14 或更高版本) +- Node.js (v14 或更高) - npm (通常与 Node.js 一起安装) -- 一个 OpenAI API 密钥 +- OpenAI API 密钥 ## 安装 -1. 克隆此存储库或下载源代码。 +1. 克隆此库或下载源代码。 2. 在终端中导航到项目目录。 3. 安装依赖项: @@ -46,31 +50,31 @@ npm run build ## 脚本 -- `build`: 编译 TypeScript 文件到 JavaScript。 +- `build`: 将 TypeScript 文件编译为 JavaScript。 - `start`: 使用 Node.js 运行编译后的 JavaScript。 - `lint`: 运行 ESLint 检查 TypeScript 文件中的代码质量问题。 - `lint:fix`: 自动修复 TypeScript 文件中的 lint 问题。 -- `format`: 使用 Prettier 格式化 `src` 目录中各种文件类型的代码。 -- `format:check`: 不进行更改地检查 `src` 目录中各种文件类型的代码格式。 +- `format`: 使用 Prettier 格式化 `src` 目录中的不同文件类型的代码。 +- `format:check`: 检查代码格式而不进行更改,适用于 `src` 目录中的不同文件类型。 - `postbuild`: 使编译后的 `index.js` 文件可执行。 -- `changelog`: 根据常规提交生成变更日志。 -- `version`: 在版本管理时更新变更日志并将其暂存以供提交。 +- `changelog`: 基于常规提交生成变更日志。 +- `version`: 更新变更日志并在版本更新时将其暂存以供提交。 - `test`: 构建项目并运行测试。 ## 用法 -您可以使用 Node.js、`npx`,或作为独立可执行文件(如果您打包了它)来运行 CLI 工具。 +您可以使用 Node.js、`npx` 或作为独立可执行文件(如果您已打包)来运行 CLI 工具。 ### 使用 Node.js ```bash -node dist/index.js --input <输入文件> --output <输出文件> --language <目标语言> [选项] +node dist/index.js --input --output --language [options] ``` ### 使用 npx ```bash -npx ai-markdown-translator -i <输入文件> -o <输出文件> -l <目标语言> [选项] +npx ai-markdown-translator -i -o -l [options] ``` 例如: @@ -82,33 +86,45 @@ npx ai-markdown-translator -u https://gitee.com/h7ml/ai-markdown-translator/raw/ ### 使用独立可执行文件 ```bash -./ai-markdown-translator --input <输入文件> --output <输出文件> --language <目标语言> [选项] +./ai-markdown-translator --input --output --language [options] ``` ## 选项 - `--input`, `-i`: 输入的 Markdown 文件或目录(替代 `--url`)。此选项允许您指定要翻译的 Markdown 文件或目录的路径。 -- `--url`, `-u`: 要翻译的 Markdown 文件的 URL(替代 `--input`)。使用此选项提供要翻译的 Markdown 文件的直接链接。 +- `--url`, `-u`: 要翻译的 Markdown 文件的 URL(替代 `--input`)。使用此选项提供一个直接链接到您想要翻译的 Markdown 文件。 -- `--extension`, `-e`: 指定要翻译的文件扩展名(例如,`md`)。如果未提供,将处理所有文件。此选项允许您根据文件扩展名过滤要翻译的文件。 +- `--extension`, `-e`: 指定要翻译的文件扩展名(例如,`md`)。如果未提供,所有文件将被处理。此选项允许您根据文件的扩展名过滤要翻译的文件。 -- `--rename`: 是否修改文件名。如果为 true,输出文件将命名为 `<原始文件名>-translated.<扩展名>`。此选项允许您指定是否希望在翻译后的文件名中附加后缀。 +- `--rename`: 是否修改文件名。如果为 true,输出文件将命名为 `-translated.`。此选项允许您指定是否要在翻译后的文件名中附加后缀。 -- `--output`, `-o`: 输出的 Markdown 文件(如果未提供,默认为输入文件名)。此选项允许您指定翻译内容将保存到的输出文件的名称。 +- `--output`, `-o`: 输出的 Markdown 文件(如果未提供,默认为输入文件名)。此选项允许您指定翻译内容将被保存的输出文件的名称。 -- `--language`, `-l`: 翻译的目标语言(必需)。此选项指定您希望将 Markdown 内容翻译成的语言。 +- `--language`, `-l`: 翻译的目标语言(必需)。此选项指定您想要将 Markdown 内容翻译成的语言。 - `--openai-url`: OpenAI API URL(默认:使用 `OPENAI_URL` 环境变量)。此选项允许您在需要时指定 OpenAI API 的自定义 URL。 - `--api-key`: OpenAI API 密钥(默认:使用 `API_KEY` 环境变量)。此选项用于提供您的 OpenAI API 密钥以进行身份验证。 -- `--model`: 要使用的 OpenAI 模型(默认:使用 `MODEL` 环境变量或 `gpt-3.5-turbo`)。此选项允许您指定要用于翻译的 OpenAI 模型。 +- `--model`: 使用的 OpenAI 模型(默认:使用 `MODEL` 环境变量或 `gpt-3.5-turbo`)。此选项允许您指定要用于翻译的 OpenAI 模型。 - `--help`, `-h`: 显示帮助。此选项显示命令行工具的帮助信息。 - `--show-version`, `-v`: 显示版本。此选项显示工具的当前版本。 -> 注意:`--input` 和 `--url` 是互斥的;您必须提供其中一个。 +- `--log`: 启用日志记录(默认:false)。启用翻译过程的详细日志记录,包括成功和失败信息。 + +- `--log-file`: 指定日志文件路径(默认:`/log/translator-err.log`)。用于记录翻译错误和失败的文件。 + +- `--log-dir`: 指定日志目录(默认:`/log`)。所有日志文件将存储的目录。 + +- `--retry-count`: 失败翻译的重试尝试次数(默认:3)。翻译器应尝试重试失败翻译的次数。 + +- `--retry-delay`: 重试尝试之间的延迟(默认:10)。重试尝试之间等待的时间。 + +- `--path`, `-p`: 显示目录结构(默认:当前脚本目录)。显示指定目录结构的树视图。 + +> 注意:`--input` 和 `--url` 是互斥的;您必须提供其中之一。 ## 环境变量 @@ -118,7 +134,7 @@ npx ai-markdown-translator -u https://gitee.com/h7ml/ai-markdown-translator/raw/ - `API_KEY`: 您的 OpenAI API 密钥。 - `MODEL`: 要使用的 OpenAI 模型(例如,`'gpt-3.5-turbo'`)。 -您可以在项目根目录中的 `.env` 文件中设置这些,或在您的 shell 中导出它们。 +您可以在项目根目录中的 `.env` 文件中设置这些环境变量,或在 shell 中导出它们。 ## 示例 @@ -146,38 +162,101 @@ npx ai-markdown-translator -i input.md -o output.md -l "德语" --openai-url "ht npx ai-markdown-translator -u https://gitee.com/h7ml/ai-markdown-translator/raw/main/README.md -o output.md -l "意大利语" ``` -5. **翻译目录中的所有 Markdown 文件并重命名它们:** +5. **翻译目录中的所有 Markdown 文件并重命名:** ```bash npx ai-markdown-translator -i ./markdown-files -l "中文" --rename ``` -6. **翻译 Markdown 文件并指定输出文件名:** +6. **翻译 Markdown 文件并指定输出文件名称:** ```bash npx ai-markdown-translator -i example.md -o translated_example.md -l "日语" ``` +7. **使用日志记录和重试选项进行翻译:** + +```bash +npx ai-markdown-translator -i ./docs -o ./translated -l "中文" --log --retry-count 5 --retry-delay 15 +``` + +8. **使用自定义日志目录进行翻译:** + +```bash +npx ai-markdown-translator -i input.md -o output.md -l "日语" --log --log-dir "./custom-logs" +``` + +9. **使用所有日志记录和重试选项进行翻译:** + +```bash +npx ai-markdown-translator -i ./markdown-files -l "法语" \ + --log \ + --log-dir "./logs" \ + --log-file "./logs/translation.log" \ + --retry-count 3 \ + --retry-delay 5 +``` + +10. **显示目录结构:** + +```bash +npx ai-markdown-translator -p ./src +``` + +输出示例: + +``` +📂 目录结构: /path/to/src +. +├── 📁 components +│ ├── 📄 Button.tsx +│ └── 📄 Input.tsx +├── 📁 utils +│ ├── 📄 logger.ts +│ └── 📄 translator.ts +└── 📄 index.ts +``` + +11. **使用自动重试和日志记录进行翻译:** + +```bash +npx ai-markdown-translator -i ./docs -o ./translated -l "中文" \ + --log \ + --retry-count 5 \ + --retry-delay 15 \ + --log-file "./logs/translation.log" +``` + +12. **翻译目录并跟踪失败:** + +```bash +npx ai-markdown-translator -i ./markdown-files -o ./output -l "日语" \ + --log \ + --log-dir "./logs" \ + --retry-count 3 \ + --retry-delay 10 +``` + ## 许可证 -[MIT 许可证](LICENSE) +[MIT License](LICENSE) ## Git 信息 -- **存储库**: [h7ml/ai-markdown-translator](https://github.com/h7ml/ai-markdown-translator) +- **仓库**: [h7ml/ai-markdown-translator](https://github.com/h7ml/ai-markdown-translator) - **问题**: [报告问题](https://github.com/h7ml/ai-markdown-translator/issues) ## 版本信息 -- **当前版本**: 1.0.11 +- **当前版本**: 1.0.12 - **NPM 包**: [ai-markdown-translator](https://www.npmjs.com/package/ai-markdown-translator) ## CI 信息 -此项目使用 GitHub Actions 进行持续集成。CI 工作流包括: +此项目使用 GitHub Actions 进行持续集成。CI 工作流程包含: -- 使用 ESLint 对代码进行 lint 检查 -- 运行测试(如果适用) +- 使用 ESLint 对代码进行 lint +- 运行测试(如适用) - 构建项目 - 缓存依赖项以加快构建速度 @@ -187,4 +266,4 @@ npx ai-markdown-translator -i example.md -o translated_example.md -l "日语" ## 支持 -如果您遇到任何问题或有任何疑问,请在此存储库中打开一个问题。 \ No newline at end of file +如果您遇到任何问题或有任何疑问,请在此仓库中打开一个问题。 diff --git a/README.md b/README.md index a641021..808b0a8 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,13 @@ ## Features -- Translate Markdown files to any language supported by OpenAI's models. -- Preserve Markdown syntax during translation. -- Flexible configuration through command-line arguments or environment variables. +- Translate Markdown files to any language supported by OpenAI's models +- Preserve Markdown syntax during translation +- Support for recursive directory translation +- Automatic retry mechanism for failed translations +- Comprehensive logging system +- Directory structure visualization +- File failure tracking and recovery ## Prerequisites @@ -108,6 +112,18 @@ npx ai-markdown-translator -u https://gitee.com/h7ml/ai-markdown-translator/raw/ - `--show-version`, `-v`: Show version. This option displays the current version of the tool. +- `--log`: Enable logging (default: false). Enables detailed logging of the translation process, including success and failure information. + +- `--log-file`: Specify the log file path (default: `/log/translator-err.log`). The file where translation errors and failures will be logged. + +- `--log-dir`: Specify the log directory (default: `/log`). The directory where all log files will be stored. + +- `--retry-count`: Number of retry attempts for failed translations (default: 3). How many times the translator should attempt to retry failed translations. + +- `--retry-delay`: Delay in seconds between retry attempts (default: 10). How long to wait between retry attempts. + +- `--path`, `-p`: Display directory structure (default: current script directory). Shows a tree view of the specified directory structure. + > Note: `--input` and `--url` are mutually exclusive; you must provide one or the other. ## Environment Variables @@ -158,6 +174,69 @@ npx ai-markdown-translator -i ./markdown-files -l "Chinese" --rename npx ai-markdown-translator -i example.md -o translated_example.md -l "Japanese" ``` +7. **Translate with logging and retry options:** + +```bash +npx ai-markdown-translator -i ./docs -o ./translated -l "Chinese" --log --retry-count 5 --retry-delay 15 +``` + +8. **Translate with custom log directory:** + +```bash +npx ai-markdown-translator -i input.md -o output.md -l "Japanese" --log --log-dir "./custom-logs" +``` + +9. **Translate with all logging and retry options:** + +```bash +npx ai-markdown-translator -i ./markdown-files -l "French" \ + --log \ + --log-dir "./logs" \ + --log-file "./logs/translation.log" \ + --retry-count 3 \ + --retry-delay 5 +``` + +10. **Display directory structure:** + +```bash +npx ai-markdown-translator -p ./src +``` + +Output example: + +``` +📂 Directory structure: /path/to/src +. +├── 📁 components +│ ├── 📄 Button.tsx +│ └── 📄 Input.tsx +├── 📁 utils +│ ├── 📄 logger.ts +│ └── 📄 translator.ts +└── 📄 index.ts +``` + +11. **Translate with automatic retry and logging:** + +```bash +npx ai-markdown-translator -i ./docs -o ./translated -l "Chinese" \ + --log \ + --retry-count 5 \ + --retry-delay 15 \ + --log-file "./logs/translation.log" +``` + +12. **Translate directory with failure tracking:** + +```bash +npx ai-markdown-translator -i ./markdown-files -o ./output -l "Japanese" \ + --log \ + --log-dir "./logs" \ + --retry-count 3 \ + --retry-delay 10 +``` + ## License [MIT License](LICENSE) @@ -169,7 +248,7 @@ npx ai-markdown-translator -i example.md -o translated_example.md -l "Japanese" ## Version Information -- **Current Version**: 1.0.11 +- **Current Version**: 1.0.12 - **NPM Package**: [ai-markdown-translator](https://www.npmjs.com/package/ai-markdown-translator) ## CI Information diff --git a/src/index.ts b/src/index.ts index 302a19c..3aa7245 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,11 @@ config(); const __filename = fileURLToPath(import.meta.url); // 当前脚本的文件路径 const __dirname = path.dirname(__filename); // 当前文件所在目录 + +// 日志文件路径 +const LOG_DIR = path.join(__dirname, './log'); +const FAIL_LOG = path.join(LOG_DIR, 'translator-err.log'); + // 添加常量配置 const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const ALLOWED_CONTENT_TYPES = [ @@ -23,6 +28,10 @@ const ALLOWED_CONTENT_TYPES = [ 'application/octet-stream', ]; +// 确保日志目录存在 +if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); +} // 验证URL是否有效 function isValidUrl(urlString: string): boolean { try { @@ -95,44 +104,97 @@ async function getDefaultApiKey(): Promise { return ''; } } +// 记录翻译失败的文件路径 +function logFailedFile(filePath: string) { + fs.appendFileSync(FAIL_LOG, `${filePath}\n`, 'utf-8'); +} + +// 从日志文件加载失败文件路径 +function getFailedFiles(): string[] { + if (fs.existsSync(FAIL_LOG)) { + const content = fs.readFileSync(FAIL_LOG, 'utf-8'); + return content.split('\n').filter((line) => line.trim() !== ''); + } + return []; +} +// 清除某些已翻译成功的文件记录 +function clearLogFile(succeededFiles: string[]) { + if (!fs.existsSync(FAIL_LOG)) return; + const failedFiles = getFailedFiles(); + const remainingFiles = failedFiles.filter((file) => !succeededFiles.includes(file)); + fs.writeFileSync(FAIL_LOG, remainingFiles.join('\n') + '\n', 'utf-8'); +} + +// 添加新的工具函数 +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// 添加日志记录函数 +function logMessage(message: string, options: { log: boolean; logFile: string }) { + if (options.log) { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + fs.appendFileSync(options.logFile, logMessage); + console.log(message); + } +} async function translateText( text: string, targetLanguage: string, openaiUrl = 'https://api.302.ai/v1/chat/completions', apiKey: string, model = 'gpt-4o-mini', + retryOptions = { count: 3, delay: 10, log: false, logFile: '' }, ): Promise { - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }; + for (let attempt = 1; attempt <= retryOptions.count; attempt++) { + try { + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }; + + const prompt = `将以下文本翻译成${targetLanguage}。请保持格式不变:\n\n${text}`; + const systemContent = await getFileContent('system.md'); + const translateContent = await getFileContent('translate.md'); + const assistantContent = await getFileContent('assistant.md'); + + const data = { + model: model, + messages: [ + { role: 'system', content: systemContent }, + { + role: 'user', + content: `请将以下文本翻译成英文。请保持格式不变:\n\n${translateContent}`, + }, + { role: 'assistant', content: assistantContent }, + { role: 'user', content: prompt }, + ], + }; + + const response = await axios.post(openaiUrl, data, { headers }); + + if (response.status === 200) { + logMessage(`翻译成功 (尝试 ${attempt}/${retryOptions.count})`, retryOptions); + return response.data.choices[0].message.content; + } - const prompt = `将以下文本翻译成${targetLanguage}。请保持格式不变:\n\n${text}`; - const systemContent = await getFileContent('system.md'); - const translateContent = await getFileContent('translate.md'); - const assistantContent = await getFileContent('assistant.md'); - const data = { - model: model, - messages: [ - { role: 'system', content: systemContent }, - { role: 'user', content: `请将以下文本翻译成英文。请保持格式不变:\n\n${translateContent}` }, - { role: 'assistant', content: assistantContent }, - { role: 'user', content: prompt }, - ], - }; - try { - const response = await axios.post(openaiUrl, data, { headers }); - if (response.status === 200) { - return response.data.choices[0].message.content; - } else { - console.error(`错误: ${response.status} - ${response.statusText}`); - return null; + logMessage( + `请求失败 (尝试 ${attempt}/${retryOptions.count}): ${response.status} - ${response.statusText}`, + retryOptions, + ); + } catch (error) { + logMessage(`翻译错误 (尝试 ${attempt}/${retryOptions.count}): ${error}`, retryOptions); + + if (attempt < retryOptions.count) { + logMessage(`等待 ${retryOptions.delay} 秒后重试...`, retryOptions); + await sleep(retryOptions.delay * 1000); + } } - } catch (error) { - console.error(`请求失败: ${error}`); - return null; } + + return null; } async function getContentFromUrl(urlString: string): Promise { @@ -221,6 +283,7 @@ async function getContentFromUrl(urlString: string): Promise { } } +// 更新 translateDirectory 函数 async function translateDirectory( inputDir: string, outputDir: string, @@ -230,63 +293,141 @@ async function translateDirectory( model: string, fileExtension: string | null, rename: string | null, + options: { + log: boolean; + logFile: string; + logDir: string; + retryCount: number; + retryDelay: number; + }, ) { - // 使用glob递归获取所有文件 + // 确保日志目录存在 + if (options.log && !fs.existsSync(options.logDir)) { + fs.mkdirSync(options.logDir, { recursive: true }); + } + const pattern = fileExtension ? `**/*.${fileExtension}` : '**/*'; const markdownFiles = glob.sync(`${inputDir}/${pattern}`, { nodir: true }); + const successfulFiles: string[] = []; // 添加成功翻译文件的数组 + for (const file of markdownFiles) { - const relativePath = path.relative(inputDir, file); // 获取相对路径 - const content = readMarkdownFile(file); + const relativePath = path.relative(inputDir, file); + logMessage(`开始处理文件: ${file}`, options); + const content = readMarkdownFile(file); const translatedContent = await translateText( content, targetLanguage, openaiUrl, apiKey, model, + { + count: options.retryCount, + delay: options.retryDelay, + log: options.log, + logFile: options.logFile, + }, ); if (translatedContent) { - let modifiedContent = translatedContent; // 创建可修改的变量 - // 校验是否含有代码块标记,如果有则删除第一行和最后一行 - if (modifiedContent.startsWith('```')) { - const endOfFirstLine = modifiedContent.indexOf('\n'); - modifiedContent = modifiedContent.slice(endOfFirstLine + 1).trim(); - } + try { + let modifiedContent = translatedContent; + if (modifiedContent.startsWith('```')) { + const endOfFirstLine = modifiedContent.indexOf('\n'); + modifiedContent = modifiedContent.slice(endOfFirstLine + 1).trim(); + } - if (modifiedContent.endsWith('```')) { - const endIndex = modifiedContent.lastIndexOf('```'); - if (endIndex !== -1) { - modifiedContent = modifiedContent.slice(0, endIndex); + if (modifiedContent.endsWith('```')) { + const startOfLastLine = modifiedContent.lastIndexOf('\n'); + modifiedContent = modifiedContent.slice(0, startOfLastLine).trim(); + } + + const outputFileName = rename + ? path.join( + outputDir, + path.dirname(relativePath), + `${path.basename(file, path.extname(file))}-${rename}${path.extname(file)}`, + ) + : path.join(outputDir, relativePath); + + const outputDirPath = path.dirname(outputFileName); + if (!fs.existsSync(outputDirPath)) { + fs.mkdirSync(outputDirPath, { recursive: true }); } - } - // 根据相对路径生成输出文件路径 - const outputFileName = rename - ? path.join( - outputDir, - path.dirname(relativePath), - `${path.basename(file, path.extname(file))}-${rename}${path.extname(file)}`, - ) - : path.join( - outputDir, - path.dirname(relativePath), - `${path.basename(file, path.extname(file))}${path.extname(file)}`, - ); - - // 确保输出目录存在 - const outputDirPath = path.dirname(outputFileName); - if (!fs.existsSync(outputDirPath)) { - fs.mkdirSync(outputDirPath, { recursive: true }); - } - // 写入翻译后的文件 - writeMarkdownFile(outputFileName, modifiedContent); - console.log(`翻译完成。输出已保存到 ${outputFileName}`); + writeMarkdownFile(outputFileName, modifiedContent); + logMessage(`翻译完成: ${file} -> ${outputFileName}`, options); + successfulFiles.push(file); // 添加到成功列表 + } catch (writeError) { + logMessage(`写入文件失败: ${file}`, options); + logFailedFile(file); + } } else { - console.log('翻译失败。'); + logMessage(`翻译失败: ${file}`, options); + logFailedFile(file); } } + + // 清理已成功翻译的文件记录 + if (successfulFiles.length > 0) { + clearLogFile(successfulFiles); + logMessage(`已清理 ${successfulFiles.length} 个成功翻译文件的记录`, options); + } +} + +// 优化 printDirectoryStructure 函数,添加文件过滤和图标支持 +function printDirectoryStructure( + dirPath: string, + prefix = '', + options = { + showHidden: false, + showFiles: true, + maxDepth: Infinity, + currentDepth: 0, + fileFilter: (filename: string) => true, + }, +): void { + if (options.currentDepth > options.maxDepth) return; + + const items = fs + .readdirSync(dirPath) + .filter((item) => (options.showHidden ? true : !item.startsWith('.'))) + .filter((item) => { + const fullPath = path.join(dirPath, item); + const stats = fs.statSync(fullPath); + return stats.isDirectory() || (options.showFiles && options.fileFilter(item)); + }) + .sort((a, b) => { + const aPath = path.join(dirPath, a); + const bPath = path.join(dirPath, b); + const aIsDir = fs.statSync(aPath).isDirectory(); + const bIsDir = fs.statSync(bPath).isDirectory(); + if (aIsDir && !bIsDir) return -1; + if (!aIsDir && bIsDir) return 1; + return a.localeCompare(b); + }); + + items.forEach((item, index) => { + const isLast = index === items.length - 1; + const fullPath = path.join(dirPath, item); + const stats = fs.statSync(fullPath); + const isDir = stats.isDirectory(); + + // 添加图标 + const icon = isDir ? '📁' : '📄'; + const displayPrefix = prefix + (isLast ? '└── ' : '├── '); + + console.log(`${displayPrefix}${icon} ${item}`); + + if (isDir) { + const newPrefix = prefix + (isLast ? ' ' : '│ '); + printDirectoryStructure(fullPath, newPrefix, { + ...options, + currentDepth: options.currentDepth + 1, + }); + } + }); } async function main() { @@ -356,6 +497,41 @@ async function main() { description: '显示版本号', type: 'boolean', }) + .option('retry', { + description: '是否重试翻译失败的文件', + type: 'boolean', + default: false, + }) + .option('log', { + description: '是否显示日志', + type: 'boolean', + default: false, + }) + .option('log-file', { + description: '日志文件路径', + type: 'string', + default: path.join(__dirname, '..', 'log', 'translator-err.log'), + }) + .option('log-dir', { + description: '日志目录', + type: 'string', + default: path.join(__dirname, '..', 'log'), + }) + .option('retry-count', { + description: '重试次数', + type: 'number', + default: 3, + }) + .option('retry-delay', { + description: '重试延迟时间(秒)', + type: 'number', + default: 10, + }) + .option('path', { + description: '当前文件所在的目录', + type: 'string', + default: __dirname, + }) .help() .alias('help', 'h').argv; @@ -374,7 +550,36 @@ async function main() { throw new Error('需要提供API Key。请通过--api-key参数或API_KEY环境变量提供。'); } + // 如果指定了 path 参数,显示目录结构 + if (argv.path) { + const pathToShow = path.resolve(argv.path as string); + console.log(`\n📂 目录结构: ${pathToShow}`); + console.log('.'); + + // 使用增强的目录打印选项 + printDirectoryStructure(pathToShow, '', { + showHidden: false, + showFiles: true, + maxDepth: 5, // 限制最大深度 + currentDepth: 0, + fileFilter: (filename: string) => { + // 可以根据需要过滤文件 + return true; + }, + }); + console.log('\n'); + } + let markdownContent: string | null = null; + const options = { + log: argv.log as boolean, + logFile: argv['log-file'] as string, + logDir: argv['log-dir'] as string, + retryCount: argv['retry-count'] as number, + retryDelay: argv['retry-delay'] as number, + path: argv.path as string, + }; + if (argv.url) { markdownContent = await getContentFromUrl(argv.url as string); } else if (argv.input) { @@ -391,6 +596,7 @@ async function main() { argv.model as string, argv.extension || null, argv.rename, + options, ); } else { markdownContent = readMarkdownFile(inputPath); @@ -421,6 +627,12 @@ async function main() { argv['openai-url'] as string, argv['api-key'] as string, argv.model as string, + { + count: options.retryCount, + delay: options.retryDelay, + log: options.log, + logFile: options.logFile, + }, ); if (translatedContent) { @@ -431,15 +643,21 @@ async function main() { } if (modifiedContent.endsWith('```')) { - const endIndex = modifiedContent.lastIndexOf('```'); - if (endIndex !== -1) { - modifiedContent = modifiedContent.slice(0, endIndex); - } + const startOfLastLine = modifiedContent.lastIndexOf('\n'); + modifiedContent = modifiedContent.slice(0, startOfLastLine).trim(); } writeMarkdownFile(argv.output, modifiedContent); console.log(`翻译 ${argv.input} 完成。输出已保存到 ${argv.output}`); + + // 如果是单文件翻译,也清理日志 + if (typeof argv.input === 'string') { + clearLogFile([argv.input]); + } } else { console.log('翻译失败。'); + if (typeof argv.input === 'string') { + logFailedFile(argv.input); + } } } } catch (error: Error | unknown) {