diff --git a/README-zh.md b/README-zh.md index 590f169..1c538c5 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,28 +1,28 @@ # ai-markdown-translator -[![NPM 版本](https://img.shields.io/npm/v/ai-markdown-translator.svg?style=flat)](https://www.npmjs.org/package/ai-markdown-translator) +[![NPM version](https://img.shields.io/npm/v/ai-markdown-translator.svg?style=flat)](https://www.npmjs.org/package/ai-markdown-translator) [![CI](https://github.com/h7ml/ai-markdown-translator/actions/workflows/ci.yml/badge.svg)](https://github.com/h7ml/ai-markdown-translator/actions/workflows/ci.yml) -![NPM 下载量](https://img.shields.io/npm/dw/ai-markdown-translator) -![GitHub 许可证](https://img.shields.io/github/license/h7ml/ai-markdown-translator) +![NPM Downloads](https://img.shields.io/npm/dw/ai-markdown-translator) +![GitHub License](https://img.shields.io/github/license/h7ml/ai-markdown-translator) -ai-markdown-translator 是一个命令行工具,使用 OpenAI 的语言模型将 Markdown 文件从一种语言翻译成另一种语言。它在翻译内容的同时保留了 Markdown 语法。 +ai-markdown-translator是一个命令行工具,使用OpenAI的语言模型将Markdown文件从一种语言翻译成另一种语言。在翻译内容的同时,它会保留Markdown语法。 ## 特性 -- 将 Markdown 文件翻译成 OpenAI 模型支持的任意语言 -- 在翻译过程中保留 Markdown 语法 -- 通过命令行参数或环境变量进行灵活配置 +- 将Markdown文件翻译为OpenAI模型支持的任何语言 +- 在翻译过程中保留Markdown语法 +- 通过命令行参数或环境变量灵活配置 - 跨平台支持(Windows,macOS,Linux) -## 预备条件 +## 先决条件 -- Node.js(v14 或更高版本) -- npm(通常与 Node.js 一起提供) -- 一个 OpenAI API 密钥 +- Node.js(v14或更高版本) +- npm(通常与Node.js一起提供) +- OpenAI API密钥 ## 安装 -1. 克隆此仓库或下载源代码。 +1. 克隆此库或下载源代码。 2. 在终端中导航到项目目录。 3. 安装依赖项: @@ -36,97 +36,106 @@ npm install npm run build ``` -5. (可选) 将 CLI 打包成一个独立的可执行文件: +5. (可选)将CLI打包为独立可执行文件: ```bash npm run package ``` -在 `bin` 目录中为 Windows、macOS 和 Linux 创建可执行文件。 +这将在`bin`目录中为Windows、macOS和Linux创建可执行文件。 ## 脚本 -- **build**:将 TypeScript 文件编译为 JavaScript。 -- **start**:使用 Node.js 运行 CLI 工具。 -- **package**:为 CLI 创建独立的可执行文件。 -- **lint**:运行 ESLint 检查代码质量问题。 -- **lint:fix**:自动修复 linting 问题。 -- **format**:使用 Prettier 格式化代码。 -- **format:check**:检查代码格式,不作更改。 +- **build**:将TypeScript文件编译为JavaScript。 +- **start**:使用Node.js运行CLI工具。 +- **package**:为CLI创建独立的可执行文件。 +- **lint**:运行ESLint检查代码质量问题。 +- **lint:fix**:自动修复代码问题。 +- **format**:使用Prettier格式化代码。 +- **format:check**:检查代码格式而不做更改。 ## 用法 -可以使用 Node.js,`npx`,或作为一个独立的可执行程序运行 CLI 工具(如果你已经打包了它)。 +您可以使用Node.js、`npx`或作为独立可执行文件(如果您已经打包)来运行CLI工具。 -### 使用 Node.js +### 使用Node.js ```bash -node dist/index.js --input <输入文件> --output <输出文件> --language <目标语言> [选项] +node dist/index.js --input --output --language [options] ``` -### 使用 npx +### 使用npx ```bash -npx ai-markdown-translator -i <输入文件> -o <输出文件> -l <目标语言> [选项] +npx ai-markdown-translator -i -o -l [options] ``` -### 使用独立的可执行程序 +### 使用独立可执行文件 ```bash -./ai-markdown-translator --input <输入文件> --output <输出文件> --language <目标语言> [选项] +./ai-markdown-translator --input --output --language [options] ``` ### 选项 -- `--input`, `-i`:输入 Markdown 文件(必需) -- `--output`, `-o`:输出 Markdown 文件(必需) -- `--language`, `-l`:目标翻译语言(必需) -- `--openai-url`:OpenAI API URL(默认:使用环境变量 OPENAI_URL) -- `--api-key`:OpenAI API 密钥(默认:使用环境变量 API_KEY) -- `--model`:使用的 OpenAI 模型(默认:使用环境变量 MODEL 或 'gpt-3.5-turbo') +- `--input`, `-i`:输入Markdown文件(替代--url) +- `--url`, `-u`:要翻译的Markdown文件的URL(替代--input) +- `--output`, `-o`:输出Markdown文件(必填) +- `--language`, `-l`:翻译的目标语言(必填) +- `--openai-url`:OpenAI API URL(默认:使用OPENAI_URL环境变量) +- `--api-key`:OpenAI API密钥(默认:使用API_KEY环境变量) +- `--model`:使用的OpenAI模型(默认:使用MODEL环境变量或'gpt-3.5-turbo') - `--help`, `-h`:显示帮助 +注意:`--input`和`--url`是互斥的——您必须提供其中一个。 + ## 环境变量 -可以设置以下环境变量,而无需将它们作为命令行参数传递: +您可以设置以下环境变量,而不是作为命令行参数传递它们: -- `OPENAI_URL`:OpenAI API 的 URL -- `API_KEY`:你的 OpenAI API 密钥 -- `MODEL`:要使用的 OpenAI 模型(例如,'gpt-3.5-turbo') +- `OPENAI_URL`:OpenAI API的URL +- `API_KEY`:您的OpenAI API密钥 +- `MODEL`:使用的OpenAI模型(例如,'gpt-3.5-turbo') -你可以在项目根目录的 `.env` 文件中设置这些参数,或在你的 shell 中导出它们。 +您可以在项目根目录中的`.env`文件中设置这些变量,或在您的Shell中导出它们。 ## 示例 -1. 将 Markdown 文件从英语翻译成西班牙语: +1. 将Markdown文件从英语翻译成西班牙语: ```bash ./ai-markdown-translator --input english.md --output spanish.md --language "Spanish" ``` -2. 使用指定的 OpenAI 模型进行翻译: +2. 使用特定的OpenAI模型翻译: ```bash ./ai-markdown-translator --input input.md --output output.md --language "French" --model "gpt-4" ``` -3. 使用自定义的 OpenAI URL 和 API 密钥进行翻译: +3. 使用自定义OpenAI URL和API密钥翻译: ```bash ./ai-markdown-translator --input input.md --output output.md --language "German" --openai-url "https://api.302.ai/v1/chat/completions" --api-key "sk-302-api-key" ``` -4. 使用 `npx` 翻译 Markdown 文件: +4. 使用`npx`翻译Markdown文件: ```bash npx ai-markdown-translator -i input.md -o output.md -l "Italian" ``` -## 许可证 +5. 翻译URL的Markdown内容: + +```bash +./ai-markdown-translator -u https://gitee.com/h7ml/ai-markdown-translator/raw/main/README.md -o output.md -l "Italian" +``` + +## 许可 -[MIT 许可证](LICENSE) +[MIT License](LICENSE) -## Git 信息 +## Git信息 - **仓库**:[h7ml/ai-markdown-translator](https://github.com/h7ml/ai-markdown-translator) - **问题**:[报告问题](https://github.com/h7ml/ai-markdown-translator/issues) @@ -134,21 +143,21 @@ npx ai-markdown-translator -i input.md -o output.md -l "Italian" ## 版本信息 - **当前版本**:1.0.4 -- **NPM 包**:[ai-markdown-translator](https://www.npmjs.com/package/ai-markdown-translator) +- **NPM包**:[ai-markdown-translator](https://www.npmjs.com/package/ai-markdown-translator) -## CI 信息 +## CI信息 -此项目使用 GitHub Actions 进行持续集成。CI 工作流包括: +此项目使用GitHub Actions进行持续集成。CI工作流包括: -- 使用 ESLint 检查代码 +- 使用ESLint对代码进行Lint检查 - 运行测试(如果适用) - 构建项目 -- 缓存依赖项以加快构建速度 +- 缓存依赖项以加速构建 ## 贡献 -欢迎贡献!请随时提交拉取请求。 +欢迎贡献!请随时提交Pull Request。 ## 支持 -如果遇到任何问题或有任何问题,请在此库开一个问题。 +如果您遇到任何问题或有任何疑问,请在此仓库中提出问题。 \ No newline at end of file diff --git a/README.md b/README.md index 19f54e9..4e07c6a 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,8 @@ npx ai-markdown-translator -i -o -l ### Options -- `--input`, `-i`: Input Markdown file (required) +- `--input`, `-i`: Input Markdown file (alternative to --url) +- `--url`, `-u`: URL of a Markdown file to translate (alternative to --input) - `--output`, `-o`: Output Markdown file (required) - `--language`, `-l`: Target language for translation (required) - `--openai-url`: OpenAI API URL (default: uses OPENAI_URL environment variable) @@ -86,6 +87,8 @@ npx ai-markdown-translator -i -o -l - `--model`: OpenAI Model to use (default: uses MODEL environment variable or 'gpt-3.5-turbo') - `--help`, `-h`: Show help +Note: `--input` and `--url` are mutually exclusive - you must provide one or the other. + ## Environment Variables You can set the following environment variables instead of passing them as command-line arguments: @@ -122,6 +125,12 @@ You can set these in a `.env` file in the project root or export them in your sh npx ai-markdown-translator -i input.md -o output.md -l "Italian" ``` +5. Translate the Markdown content of the URL: + +```bash +./ai-markdown-translator -u https://gitee.com/h7ml/ai-markdown-translator/raw/main/README.md -o output.md -l "Italian" +``` + ## License [MIT License](LICENSE) diff --git a/src/index.ts b/src/index.ts index 91f1635..853aa0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,9 +5,47 @@ import axios from 'axios'; import { config } from 'dotenv'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import * as path from 'path'; +import * as os from 'os'; config(); +// 添加常量配置 +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB +const ALLOWED_CONTENT_TYPES = [ + 'text/markdown', + 'text/plain', + 'text/x-markdown', + 'application/octet-stream', +]; + +// 验证URL是否有效 +function isValidUrl(urlString: string): boolean { + try { + // 支持标准协议 + if (urlString.match(/^(http|https|ftp|ssh|file):\/\//)) { + new URL(urlString); + return true; + } // 支持 scp 格式的 SSH URL + + if (urlString.match(/^git@[^:]+:/)) { + return true; + } // 支持本地文件路径 + + if ( + urlString.startsWith('file://') || + urlString.startsWith('/') || + /^[a-zA-Z]:\\/.test(urlString) + ) { + return true; + } + + return false; + } catch { + return false; + } +} + function readMarkdownFile(filePath: string): string { if (!fs.existsSync(filePath)) { throw new Error(`输入文件 ${filePath} 不存在。`); @@ -79,6 +117,92 @@ async function translateText( } } +async function getContentFromUrl(urlString: string): Promise { + const tempDir = os.tmpdir(); + const tempFile = path.join(tempDir, `md_${Date.now()}.md`); + + const validateContent = (content: string): boolean => { + // 基本的 Markdown 格式验证 + const hasMarkdownSyntax = /[#*_[\]()-`]/.test(content); + const hasText = /[a-zA-Z\u4e00-\u9fa5]/.test(content); + return hasMarkdownSyntax && hasText; + }; + + try { + const response = await axios({ + method: 'get', + url: urlString, + headers: { + 'User-Agent': 'Mozilla/5.0', + Accept: 'text/markdown,text/plain,*/*', + }, + responseType: 'arraybuffer', + timeout: 5000, + maxContentLength: MAX_FILE_SIZE, + validateStatus: (status) => status === 200, + }); + + // 验证内容类型 + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (!ALLOWED_CONTENT_TYPES.some((type) => contentType.includes(type))) { + throw new Error(`不支持的内容类型: ${contentType}`); + } + + const content = response.data.toString('utf-8'); + + // 验证内容是否为有效的 Markdown + if (!validateContent(content)) { + throw new Error('内容不是有效的 Markdown 格式'); + } + + return content; + } catch (firstError) { + console.log('直接获取失败,尝试下载方式:', firstError); + try { + const response = await axios({ + method: 'get', + url: urlString, + headers: { + 'User-Agent': 'Mozilla/5.0', + Accept: 'application/octet-stream', + }, + responseType: 'stream', + timeout: 5000, + maxContentLength: MAX_FILE_SIZE, + }); + + // 验证内容类型 + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (!ALLOWED_CONTENT_TYPES.some((type) => contentType.includes(type))) { + throw new Error(`不支持的内容类型: ${contentType}`); + } + + const writer = fs.createWriteStream(tempFile); + response.data.pipe(writer); + + await new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); + + const content = fs.readFileSync(tempFile, 'utf-8'); + + // 验证内容是否为有效的 Markdown + if (!validateContent(content)) { + throw new Error('内容不是有效的 Markdown 格式'); + } + + return content; + } catch (secondError) { + throw new Error(`无法从 URL 获取内容: ${urlString}`); + } finally { + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + } + } +} + async function main() { const defaultApiKey = await getDefaultApiKey(); @@ -87,7 +211,23 @@ async function main() { alias: 'i', description: '输入的Markdown文件', type: 'string', - demandOption: true, + }) + .option('url', { + alias: 'u', + description: '输入的Markdown URL地址', + type: 'string', + }) + .check((argv) => { + if (!argv.input && !argv.url) { + throw new Error('必须提供 --input 或 --url 参数之一'); + } + if (argv.input && argv.url) { + throw new Error('--input 和 --url 参数不能同时使用'); + } + if (argv.url && !isValidUrl(argv.url)) { + throw new Error('提供的URL格式不正确'); + } + return true; }) .option('output', { alias: 'o', @@ -127,7 +267,14 @@ async function main() { throw new Error('需要提供API Key。请通过--api-key参数或API_KEY环境变量提供。'); } - let markdownContent = readMarkdownFile(argv.input); + let markdownContent: string; + if (argv.url) { + markdownContent = await getContentFromUrl(argv.url as string); + } else if (argv.input) { + markdownContent = readMarkdownFile(argv.input as string); + } else { + throw new Error('必须提供 --input 或 --url 参数之一'); + } if (markdownContent.startsWith('```')) { markdownContent = markdownContent.slice(3).trim();