From f93f26fc48d5c5c94ff4e1d575712f9acce335a0 Mon Sep 17 00:00:00 2001 From: tecvan <84165678+Tecvan-fe@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:52:35 +0800 Subject: [PATCH] chore: remove all cn comments (#277) --- .gitignore | 2 + .../autoinstallers/rush-commands/package.json | 19 + .../rush-commands/pnpm-lock.yaml | 382 +++++++++++ .../src/convert-comments/README.md | 284 ++++++++ .../src/convert-comments/cli/command.ts | 108 +++ .../src/convert-comments/cli/config.ts | 182 +++++ .../src/convert-comments/example/config.json | 25 + .../convert-comments/implementation-plan.md | 306 +++++++++ .../src/convert-comments/index.ts | 232 +++++++ .../modules/chinese-detection.ts | 470 +++++++++++++ .../modules/file-replacement.ts | 421 ++++++++++++ .../src/convert-comments/modules/file-scan.ts | 123 ++++ .../src/convert-comments/modules/report.ts | 302 +++++++++ .../convert-comments/modules/translation.ts | 239 +++++++ .../src/convert-comments/requirements.md | 27 + .../technical-specification.md | 636 ++++++++++++++++++ .../src/convert-comments/types/config.ts | 65 ++ .../src/convert-comments/types/index.ts | 192 ++++++ .../src/convert-comments/utils/chinese.ts | 120 ++++ .../src/convert-comments/utils/fp.ts | 173 +++++ .../src/convert-comments/utils/git.ts | 94 +++ .../src/convert-comments/utils/language.ts | 227 +++++++ .../src/convert-comments/utils/semaphore.ts | 51 ++ .../src/convert-comments/volc/translate.ts | 387 +++++++++++ .../rush-commands/tsconfig.json | 38 ++ .../rush-commitlint/.cz-config.js | 4 + 26 files changed, 5109 insertions(+) create mode 100644 common/autoinstallers/rush-commands/package.json create mode 100644 common/autoinstallers/rush-commands/pnpm-lock.yaml create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/README.md create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/cli/command.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/cli/config.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/example/config.json create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/implementation-plan.md create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/index.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/modules/chinese-detection.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/modules/file-replacement.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/modules/file-scan.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/modules/report.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/modules/translation.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/requirements.md create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/technical-specification.md create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/types/config.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/types/index.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/utils/chinese.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/utils/fp.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/utils/git.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/utils/language.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/utils/semaphore.ts create mode 100644 common/autoinstallers/rush-commands/src/convert-comments/volc/translate.ts create mode 100644 common/autoinstallers/rush-commands/tsconfig.json diff --git a/.gitignore b/.gitignore index 9c0c556d..328ee469 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ common/temp backend/conf/model/*.yaml + +*.tsbuildinfo diff --git a/common/autoinstallers/rush-commands/package.json b/common/autoinstallers/rush-commands/package.json new file mode 100644 index 00000000..2a13276e --- /dev/null +++ b/common/autoinstallers/rush-commands/package.json @@ -0,0 +1,19 @@ +{ + "name": "rush-commands", + "version": "1.0.0", + "description": "", + "keywords": [], + "license": "Apache-2.0", + "author": "", + "main": "index.js", + "scripts": {}, + "dependencies": { + "commander": "^11.0.0", + "simple-git": "^3.20.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.19.2", + "typescript": "^5.0.0" + } +} diff --git a/common/autoinstallers/rush-commands/pnpm-lock.yaml b/common/autoinstallers/rush-commands/pnpm-lock.yaml new file mode 100644 index 00000000..7f136701 --- /dev/null +++ b/common/autoinstallers/rush-commands/pnpm-lock.yaml @@ -0,0 +1,382 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + commander: + specifier: ^11.0.0 + version: 11.1.0 + simple-git: + specifier: ^3.20.0 + version: 3.28.0 + +devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.9 + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^5.0.0 + version: 5.8.3 + +packages: + + /@esbuild/aix-ppc64@0.25.8: + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.25.8: + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.25.8: + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.25.8: + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.25.8: + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.25.8: + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.25.8: + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.25.8: + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.25.8: + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.25.8: + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.25.8: + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.25.8: + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.25.8: + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.25.8: + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.25.8: + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.25.8: + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.25.8: + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.25.8: + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.25.8: + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-arm64@0.25.8: + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.25.8: + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openharmony-arm64@0.25.8: + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.25.8: + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.25.8: + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.25.8: + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.25.8: + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@kwsites/file-exists@1.1.1: + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@kwsites/promise-deferred@1.1.1: + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + dev: false + + /@types/node@20.19.9: + resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} + dependencies: + undici-types: 6.21.0 + dev: true + + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false + + /debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + + /esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + + /simple-git@3.28.0: + resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + dev: false + + /tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.25.8 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + dev: true diff --git a/common/autoinstallers/rush-commands/src/convert-comments/README.md b/common/autoinstallers/rush-commands/src/convert-comments/README.md new file mode 100644 index 00000000..24103635 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/README.md @@ -0,0 +1,284 @@ +# 中文备注转换为英文 - 项目概览 + +## 📖 项目简介 + +本项目是一个TypeScript命令行工具,用于自动将代码仓库中的中文注释翻译为英文。通过调用OpenAI API,实现高质量的代码注释翻译,同时保持原有的代码格式和结构。 + +## 🎯 功能特性 + +- ✅ **智能文件扫描**:自动识别Git仓库中的源码文件 +- ✅ **多语言支持**:支持TypeScript、JavaScript、Go、Markdown等文件格式 +- ✅ **精确注释解析**:准确定位和提取不同语言的注释内容 +- ✅ **高质量翻译**:集成OpenAI API,提供专业的翻译服务 +- ✅ **格式保持**:保持原有的缩进、换行和注释结构 +- ✅ **安全备份**:自动创建文件备份,支持回滚操作 +- ✅ **并发处理**:支持并发翻译,提高处理效率 +- ✅ **详细报告**:生成完整的处理报告和统计信息 +- ✅ **函数式设计**:采用FP编程范式,代码简洁易维护 + +## 🚀 快速开始 + +### 安装依赖 + +```bash +npm install +``` + +### 基本使用 + +```bash +# 翻译指定目录下的所有支持文件 +ai-translate --root ./src + +# 指定文件扩展名 +ai-translate --root ./src --exts ts,js,go + +# 仅分析不修改(预览模式) +ai-translate --root ./src --dry-run + +# 详细输出模式 +ai-translate --root ./src --verbose +``` + +### 配置OpenAI API + +```bash +# 通过环境变量设置 +export OPENAI_API_KEY=your-api-key + +# 或通过命令行参数 +ai-translate --root ./src --openai-key your-api-key +``` + +## 📁 项目结构 + +``` +src/convert-comments/ +├── 📄 requirements.md # 需求文档 +├── 📄 implementation-plan.md # 实现方案 +├── 📄 technical-specification.md # 技术规格 +├── 📄 README.md # 项目概览(本文件) +├── 📦 index.ts # 主入口文件 +├── 🗂️ cli/ # 命令行接口 +│ ├── command.ts # Commander.js命令定义 +│ └── config.ts # 配置管理 +├── 🗂️ modules/ # 核心功能模块 +│ ├── file-scan.ts # 文件扫描模块 +│ ├── chinese-detection.ts # 中文检测模块 +│ ├── translation.ts # 翻译服务模块 +│ ├── file-replacement.ts # 文件替换模块 +│ └── report.ts # 报告生成模块 +├── 🗂️ utils/ # 工具函数 +│ ├── git.ts # Git操作工具 +│ ├── language.ts # 编程语言识别 +│ ├── chinese.ts # 中文字符检测 +│ └── fp.ts # 函数式编程工具 +├── 🗂️ types/ # TypeScript类型定义 +│ ├── index.ts # 主要类型定义 +│ └── config.ts # 配置类型 +└── 🗂️ __tests__/ # 测试文件 + ├── unit/ # 单元测试 + └── integration/ # 集成测试 +``` + +## 🔧 核心模块 + +### 1. 文件扫描模块 (FileScanModule) +- 调用Git命令获取仓库文件列表 +- 根据扩展名过滤目标文件 +- 识别编程语言类型 + +### 2. 中文检测模块 (ChineseDetectionModule) +- 解析不同语言的注释语法 +- 识别包含中文字符的注释 +- 提取注释的精确位置信息 + +### 3. 翻译服务模块 (TranslationModule) +- 调用OpenAI API进行翻译 +- 处理翻译错误和重试机制 +- 优化翻译提示词和上下文 + +### 4. 文件替换模块 (FileReplacementModule) +- 精确替换文件中的中文注释 +- 保持代码格式和缩进 +- 实现备份和回滚机制 + +### 5. 报告生成模块 (ReportModule) +- 收集处理过程的统计信息 +- 生成详细的处理报告 +- 支持多种输出格式 + +## ⚡ 技术亮点 + +### 函数式编程范式 +采用纯函数设计和不可变数据结构: +```typescript +const processRepository = pipe( + getGitTrackedFiles, + asyncMap(readFile), + asyncFilter(hasChineseComments), + asyncMap(extractChineseComments), + asyncMap(translateComments), + asyncMap(applyTranslations), + generateReport +); +``` + +### 性能优化 +- **并发控制**:使用Semaphore控制API调用频率 +- **缓存机制**:避免重复翻译相同内容 +- **增量处理**:仅处理修改过的文件 +- **流式处理**:支持大文件分块处理 + +### 错误处理 +- **Result模式**:使用函数式错误处理 +- **重试机制**:自动重试失败的API调用 +- **部分失败**:支持部分文件失败时继续处理 + +## 🛠️ 开发指南 + +### 环境准备 + +1. **Node.js 环境**:建议使用 Node.js 16+ +2. **TypeScript**:项目使用TypeScript开发 +3. **OpenAI API Key**:需要有效的OpenAI API密钥 + +### 开发流程 + +1. **安装依赖** +```bash +npm install +``` + +2. **开发模式运行** +```bash +npm run dev +``` + +3. **运行测试** +```bash +npm test +``` + +4. **构建项目** +```bash +npm run build +``` + +### 贡献指南 + +1. **Fork 项目**到自己的GitHub账号 +2. **创建功能分支**:`git checkout -b feature/new-feature` +3. **提交更改**:`git commit -am 'Add new feature'` +4. **推送分支**:`git push origin feature/new-feature` +5. **创建 Pull Request** + +### 代码规范 + +- **TypeScript严格模式**:启用所有严格类型检查 +- **ESLint规则**:遵循项目ESLint配置 +- **Prettier格式化**:保持代码格式一致 +- **单元测试**:新功能需要对应的单元测试 + +## 📋 命令行参数 + +### 必需参数 +- `--root, -r `:需要处理的根目录 + +### 可选参数 +- `--exts, -e `:文件扩展名,如 "ts,js,go,md" +- `--openai-key `:OpenAI API密钥 +- `--model `:OpenAI模型名称(默认:gpt-3.5-turbo) +- `--dry-run`:仅分析不实际修改文件 +- `--backup`:创建文件备份(默认启用) +- `--verbose, -v`:详细输出模式 +- `--output `:报告输出文件路径 + +### 使用示例 + +```bash +# 基本使用 +ai-translate --root ./src --exts ts,js + +# 预览模式(不修改文件) +ai-translate --root ./src --dry-run --verbose + +# 使用GPT-4模型 +ai-translate --root ./src --model gpt-4 + +# 生成JSON格式报告 +ai-translate --root ./src --output report.json +``` + +## 🔍 配置文件 + +支持使用配置文件来管理默认设置: + +```json +{ + "translation": { + "model": "gpt-3.5-turbo", + "maxRetries": 3, + "timeout": 30000, + "concurrency": 3 + }, + "processing": { + "defaultExtensions": ["ts", "js", "go", "md"], + "createBackup": true, + "outputFormat": "console" + }, + "git": { + "ignorePatterns": ["node_modules/**", ".git/**", "dist/**"], + "includeUntracked": false + } +} +``` + +## 📊 输出报告 + +处理完成后会生成详细的统计报告: + +``` +📊 翻译处理报告 +================== +总文件数: 45 +处理成功: 42 +跳过文件: 3 +翻译注释: 128 +错误数量: 0 +处理时间: 45.32秒 + +✅ 处理完成,无错误 +``` + +## ⚠️ 注意事项 + +### API限制 +- OpenAI API有调用频率限制,建议合理设置并发数量 +- 长时间运行可能消耗较多API配额 + +### 翻译质量 +- 自动翻译可能不够准确,建议人工审核重要注释 +- 提供dry-run模式预览翻译结果 + +### 文件安全 +- 默认创建备份文件,避免意外损失 +- 建议在版本控制环境下使用 + +## 🔗 相关文档 + +- [需求文档](./requirements.md) - 详细的功能需求说明 +- [实现方案](./implementation-plan.md) - 整体架构和设计方案 +- [技术规格](./technical-specification.md) - 详细的技术实现规格 + +## 📞 问题反馈 + +如有问题或建议,请通过以下方式联系: + +- 创建 GitHub Issue +- 提交 Pull Request +- 发送邮件至开发团队 + +--- + +**Happy Coding! 🎉** diff --git a/common/autoinstallers/rush-commands/src/convert-comments/cli/command.ts b/common/autoinstallers/rush-commands/src/convert-comments/cli/command.ts new file mode 100644 index 00000000..dfa79347 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/cli/command.ts @@ -0,0 +1,108 @@ +import { Command } from 'commander'; +import { CliOptions } from '../types/config'; + +/** + * 创建命令行程序 + */ +export const createProgram = (): Command => { + const program = new Command(); + + program + .name('ai-translate') + .description('将代码仓库中的中文注释翻译为英文') + .version('1.0.0'); + + program + .requiredOption('-r, --root ', '需要处理的根目录') + .option('-e, --exts ', '文件扩展名,用逗号分隔 (例: ts,js,go)', '') + .option('--access-key-id ', '火山引擎 Access Key ID') + .option('--secret-access-key ', '火山引擎 Secret Access Key') + .option('--region ', '火山引擎服务区域', 'cn-beijing') + .option('--source-language ', '源语言代码', 'zh') + .option('--target-language ', '目标语言代码', 'en') + .option('--dry-run', '仅分析不实际修改文件') + .option('-v, --verbose', '详细输出模式') + .option('-o, --output ', '报告输出文件路径') + .option('-c, --config ', '配置文件路径') + .option('--concurrency ', '并发翻译数量', '3') + .option('--max-retries ', '最大重试次数', '3') + .option('--timeout ', 'API超时时间(毫秒)', '30000'); + + return program; +}; + +/** + * 解析命令行选项 + */ +export const parseOptions = (program: Command): CliOptions => { + const options = program.opts(); + + return { + root: options.root, + exts: options.exts, + accessKeyId: options.accessKeyId, + secretAccessKey: options.secretAccessKey, + region: options.region, + sourceLanguage: options.sourceLanguage, + targetLanguage: options.targetLanguage, + dryRun: options.dryRun, + verbose: options.verbose, + output: options.output, + config: options.config + }; +}; + +/** + * 显示帮助信息 + */ +export const showHelp = (): void => { + console.log(` +🤖 AI翻译工具 - 中文注释转英文(基于火山引擎翻译) + +使用方法: + ai-translate --root <目录> [选项] + +示例: + # 基本使用 + ai-translate --root ./src --access-key-id --secret-access-key + + # 指定文件类型和翻译语言 + ai-translate --root ./src --exts ts,js,go --source-language zh --target-language en + + # 仅预览,不修改文件 + ai-translate --root ./src --dry-run + + # 指定区域和并发数 + ai-translate --root ./src --region ap-southeast-1 --concurrency 5 + + # 生成详细报告 + ai-translate --root ./src --verbose --output report.json + +环境变量: + VOLC_ACCESS_KEY_ID 火山引擎 Access Key ID(必需) + VOLC_SECRET_ACCESS_KEY 火山引擎 Secret Access Key(必需) + +配置文件示例 (config.json): +{ + "translation": { + "accessKeyId": "your-access-key-id", + "secretAccessKey": "your-secret-access-key", + "region": "cn-beijing", + "sourceLanguage": "zh", + "targetLanguage": "en", + "maxRetries": 3, + "concurrency": 3 + }, + "processing": { + "defaultExtensions": ["ts", "js", "go", "md"] + } +} +`); +}; + +/** + * 显示版本信息 + */ +export const showVersion = (): void => { + console.log('ai-translate version 1.0.0'); +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/cli/config.ts b/common/autoinstallers/rush-commands/src/convert-comments/cli/config.ts new file mode 100644 index 00000000..654ec1c7 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/cli/config.ts @@ -0,0 +1,182 @@ +import { AppConfig, CliOptions, TranslationConfig, ProcessingConfig } from '../types/config'; +import { deepMerge } from '../utils/fp'; + +/** + * 默认配置 + */ +const DEFAULT_CONFIG: AppConfig = { + translation: { + accessKeyId: process.env.VOLC_ACCESS_KEY_ID || '', + secretAccessKey: process.env.VOLC_SECRET_ACCESS_KEY || '', + region: 'cn-beijing', + sourceLanguage: 'zh', + targetLanguage: 'en', + maxRetries: 3, + timeout: 30000, + concurrency: 3 + }, + processing: { + defaultExtensions: [ + 'ts', 'tsx', 'js', 'jsx', 'go', 'md', 'txt', 'json', + 'yaml', 'yml', 'toml', 'ini', 'conf', 'config', + 'sh', 'bash', 'zsh', 'fish', 'py', 'css', 'scss', 'sass', 'less', + 'html', 'htm', 'xml', 'php', 'rb', 'rs', 'java', 'c', 'h', + 'cpp', 'cxx', 'cc', 'hpp', 'cs' + ], + outputFormat: 'console' + }, + git: { + ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'], + includeUntracked: false + } +}; + +/** + * 从文件加载配置 + */ +export const loadConfigFromFile = async (configPath: string): Promise> => { + try { + const fs = await import('fs/promises'); + const configContent = await fs.readFile(configPath, 'utf-8'); + return JSON.parse(configContent); + } catch (error) { + console.warn(`配置文件加载失败: ${configPath}`, error); + return {}; + } +}; + +/** + * 从命令行选项创建配置 + */ +export const createConfigFromOptions = (options: CliOptions): Partial => { + const config: Partial = {}; + + // 翻译配置 + if (options.accessKeyId || options.secretAccessKey || options.region || options.sourceLanguage || options.targetLanguage) { + config.translation = {} as Partial; + if (options.accessKeyId) { + config.translation!.accessKeyId = options.accessKeyId; + } + if (options.secretAccessKey) { + config.translation!.secretAccessKey = options.secretAccessKey; + } + if (options.region) { + config.translation!.region = options.region; + } + if (options.sourceLanguage) { + config.translation!.sourceLanguage = options.sourceLanguage; + } + if (options.targetLanguage) { + config.translation!.targetLanguage = options.targetLanguage; + } + } + + // 处理配置 + if (options.output) { + config.processing = {} as Partial; + // 根据输出文件扩展名推断格式 + const ext = options.output.toLowerCase().split('.').pop(); + if (ext === 'json') { + config.processing!.outputFormat = 'json'; + } else if (ext === 'md') { + config.processing!.outputFormat = 'markdown'; + } + } + + return config; +}; + +/** + * 合并配置 + */ +export const mergeConfigs = (...configs: Partial[]): AppConfig => { + return configs.reduce( + (merged, config) => deepMerge(merged, config), + { ...DEFAULT_CONFIG } + ) as AppConfig; +}; + +/** + * 加载完整配置 + */ +export const loadConfig = async (options: CliOptions): Promise => { + const configs: Partial[] = [DEFAULT_CONFIG]; + + // 加载配置文件 + if (options.config) { + const fileConfig = await loadConfigFromFile(options.config); + configs.push(fileConfig); + } + + // 加载命令行选项配置 + const optionsConfig = createConfigFromOptions(options); + configs.push(optionsConfig); + + return mergeConfigs(...configs); +}; + +/** + * 验证配置 + */ +export const validateConfig = (config: AppConfig): { valid: boolean; errors: string[] } => { + const errors: string[] = []; + + // 验证火山引擎 Access Key ID + if (!config.translation.accessKeyId) { + errors.push('火山引擎 Access Key ID 未设置,请通过环境变量VOLC_ACCESS_KEY_ID或--access-key-id参数提供'); + } + + // 验证火山引擎 Secret Access Key + if (!config.translation.secretAccessKey) { + errors.push('火山引擎 Secret Access Key 未设置,请通过环境变量VOLC_SECRET_ACCESS_KEY或--secret-access-key参数提供'); + } + + // 验证区域 + const validRegions = ['cn-beijing', 'ap-southeast-1', 'us-east-1']; + if (!validRegions.includes(config.translation.region)) { + console.warn(`未知的区域: ${config.translation.region},建议使用: ${validRegions.join(', ')}`); + } + + // 验证语言代码 + const validLanguages = ['zh', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'pt', 'ru']; + if (!validLanguages.includes(config.translation.sourceLanguage)) { + console.warn(`未知的源语言: ${config.translation.sourceLanguage},建议使用: ${validLanguages.join(', ')}`); + } + if (!validLanguages.includes(config.translation.targetLanguage)) { + console.warn(`未知的目标语言: ${config.translation.targetLanguage},建议使用: ${validLanguages.join(', ')}`); + } + + // 验证并发数 + if (config.translation.concurrency < 1 || config.translation.concurrency > 10) { + errors.push('并发数应该在1-10之间'); + } + + // 验证超时时间 + if (config.translation.timeout < 1000 || config.translation.timeout > 300000) { + errors.push('超时时间应该在1000-300000毫秒之间'); + } + + return { valid: errors.length === 0, errors }; +}; + +/** + * 打印配置信息 + */ +export const printConfigInfo = (config: AppConfig, verbose: boolean = false): void => { + console.log('🔧 当前配置:'); + console.log(` 区域: ${config.translation.region}`); + console.log(` 源语言: ${config.translation.sourceLanguage}`); + console.log(` 目标语言: ${config.translation.targetLanguage}`); + console.log(` 并发数: ${config.translation.concurrency}`); + console.log(` 重试次数: ${config.translation.maxRetries}`); + console.log(` 输出格式: ${config.processing.outputFormat}`); + + if (verbose) { + console.log(` Access Key ID: ${config.translation.accessKeyId ? '已设置' : '未设置'}`); + console.log(` Secret Access Key: ${config.translation.secretAccessKey ? '已设置' : '未设置'}`); + console.log(` 超时时间: ${config.translation.timeout}ms`); + console.log(` 默认扩展名: ${config.processing.defaultExtensions.join(', ')}`); + console.log(` 忽略模式: ${config.git.ignorePatterns.join(', ')}`); + console.log(` 包含未跟踪文件: ${config.git.includeUntracked ? '是' : '否'}`); + } +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/example/config.json b/common/autoinstallers/rush-commands/src/convert-comments/example/config.json new file mode 100644 index 00000000..03366219 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/example/config.json @@ -0,0 +1,25 @@ +{ + "translation": { + "model": "gpt-3.5-turbo", + "maxRetries": 3, + "timeout": 30000, + "concurrency": 3 + }, + "processing": { + "defaultExtensions": ["ts", "tsx", "js", "jsx", "go", "md"], + "createBackup": true, + "outputFormat": "console" + }, + "git": { + "ignorePatterns": [ + "node_modules/**", + ".git/**", + "dist/**", + "build/**", + "coverage/**", + "*.min.js", + "*.bundle.js" + ], + "includeUntracked": false + } +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/implementation-plan.md b/common/autoinstallers/rush-commands/src/convert-comments/implementation-plan.md new file mode 100644 index 00000000..2f8de3c4 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/implementation-plan.md @@ -0,0 +1,306 @@ +# 中文备注转换为英文 - 实现方案 + +## 项目概述 + +基于需求文档,本项目需要实现一个TypeScript脚本,用于将代码仓库内的中文备注自动转换为英文备注。 + +## 核心模块设计 + +### 1. 文件扫描模块 (FileScanModule) + +```typescript +interface FileScanConfig { + root: string; + extensions: string[]; +} + +interface SourceFile { + path: string; + content: string; + language: 'typescript' | 'javascript' | 'go' | 'markdown' | 'other'; +} +``` + +**功能职责:** +- 调用git命令获取仓库所有源码文件 +- 根据文件扩展名过滤目标文件 +- 读取文件内容并识别编程语言类型 + +**核心函数:** +- `getGitTrackedFiles(root: string): Promise` +- `filterFilesByExtensions(files: string[], extensions: string[]): string[]` +- `readSourceFiles(filePaths: string[]): Promise` + +### 2. 中文检测模块 (ChineseDetectionModule) + +```typescript +interface ChineseComment { + content: string; + startLine: number; + endLine: number; + startColumn: number; + endColumn: number; + type: 'single-line' | 'multi-line' | 'documentation'; +} + +interface FileWithComments { + file: SourceFile; + chineseComments: ChineseComment[]; +} +``` + +**功能职责:** +- 解析不同语言的注释语法 +- 识别包含中文字符的注释 +- 提取注释的精确位置信息 + +**核心函数:** +- `detectChineseInComments(file: SourceFile): ChineseComment[]` +- `parseCommentsByLanguage(content: string, language: string): Comment[]` +- `containsChinese(text: string): boolean` + +### 3. 翻译服务模块 (TranslationModule) + +```typescript +interface TranslationConfig { + apiKey: string; + model: string; + maxRetries: number; + timeout: number; +} + +interface TranslationResult { + original: string; + translated: string; + confidence: number; +} +``` + +**功能职责:** +- 调用OpenAI API进行翻译 +- 处理翻译错误和重试 +- 保持代码注释的格式和结构 + +**核心函数:** +- `translateComment(comment: string, context?: string): Promise` +- `batchTranslate(comments: string[]): Promise` +- `createTranslationPrompt(comment: string, language: string): string` + +### 4. 文件替换模块 (FileReplacementModule) + +```typescript +interface ReplacementOperation { + file: string; + replacements: Array<{ + start: number; + end: number; + original: string; + replacement: string; + }>; +} +``` + +**功能职责:** +- 精确替换文件中的中文注释 +- 保持代码格式和缩进 +- 创建备份机制 + +**核心函数:** +- `replaceCommentsInFile(file: SourceFile, replacements: ReplacementOperation): Promise` +- `createBackup(filePath: string): Promise` +- `applyReplacements(content: string, replacements: Replacement[]): string` + +### 5. 报告生成模块 (ReportModule) + +```typescript +interface ProcessingReport { + totalFiles: number; + processedFiles: number; + translatedComments: number; + errors: Error[]; + duration: number; + details: FileProcessingDetail[]; +} + +interface FileProcessingDetail { + file: string; + commentCount: number; + status: 'success' | 'error' | 'skipped'; + errorMessage?: string; +} +``` + +**功能职责:** +- 记录处理过程中的统计信息 +- 生成详细的处理报告 +- 记录错误和异常情况 + +## 命令行接口设计 + +### 主命令 + +```bash +ai-translate --root --exts [options] +``` + +### 参数说明 + +- `--root, -r `: 需要处理的根目录(必填) +- `--exts, -e `: 文件扩展名数组,如 "ts,js,go,md"(可选,默认处理所有文本文件) +- `--openai-key `: OpenAI API密钥(可选,也可通过环境变量提供) +- `--model `: OpenAI模型名称(可选,默认gpt-3.5-turbo) +- `--dry-run`: 仅分析不实际修改文件(可选) +- `--backup`: 创建文件备份(可选,默认启用) +- `--verbose, -v`: 详细输出模式(可选) +- `--output `: 报告输出文件(可选) + +## 技术实现细节 + +### 1. 函数式编程范式 + +采用FP风格,主要体现在: +- 纯函数设计:无副作用的数据转换 +- 不可变数据结构:使用immutable.js或原生不可变操作 +- 函数组合:通过pipe和compose构建处理流水线 +- 错误处理:使用Either/Maybe模式处理异常 + +### 2. 异步处理策略 + +```typescript +// 使用函数式风格的异步处理管道 +const processRepository = pipe( + getGitTrackedFiles, + asyncMap(readFile), + asyncFilter(hasChineseComments), + asyncMap(extractChineseComments), + asyncMap(translateComments), + asyncMap(applyTranslations), + generateReport +); +``` + +### 3. 错误处理机制 + +- 使用Result类型封装成功/失败状态 +- 实现重试机制处理网络错误 +- 记录详细的错误日志和上下文 +- 支持部分失败的情况下继续处理 + +### 4. 性能优化 + +- 并发处理:合理控制并发数量避免API限制 +- 缓存机制:避免重复翻译相同内容 +- 增量处理:仅处理修改过的文件 +- 流式处理:大文件分块处理 + +## 项目结构 + +``` +src/convert-comments/ +├── index.ts # 主入口文件 +├── cli/ +│ ├── command.ts # Commander.js命令定义 +│ └── config.ts # 配置管理 +├── modules/ +│ ├── file-scan.ts # 文件扫描模块 +│ ├── chinese-detection.ts # 中文检测模块 +│ ├── translation.ts # 翻译服务模块 +│ ├── file-replacement.ts # 文件替换模块 +│ └── report.ts # 报告生成模块 +├── utils/ +│ ├── git.ts # Git操作工具 +│ ├── language.ts # 编程语言识别 +│ ├── chinese.ts # 中文字符检测 +│ └── fp.ts # 函数式编程工具 +├── types/ +│ ├── index.ts # 类型定义 +│ └── config.ts # 配置类型 +└── __tests__/ + ├── unit/ # 单元测试 + └── integration/ # 集成测试 +``` + +## 依赖包选择 + +### 核心依赖 +- `commander`: 命令行接口 +- `openai`: OpenAI API客户端 +- `simple-git`: Git操作 +- `ramda` 或 `lodash/fp`: 函数式编程工具 + +### 开发依赖 +- `typescript`: TypeScript编译器 +- `@types/node`: Node.js类型定义 +- `jest`: 测试框架 +- `prettier`: 代码格式化 +- `eslint`: 代码检查 + +## 开发阶段规划 + +### 阶段1:基础框架搭建 +1. 项目初始化和依赖安装 +2. TypeScript配置和构建脚本 +3. 命令行接口框架 +4. 基础类型定义 + +### 阶段2:核心功能实现 +1. 文件扫描模块 +2. 中文检测模块 +3. 翻译服务模块 +4. 文件替换模块 + +### 阶段3:完善和优化 +1. 报告生成模块 +2. 错误处理和重试机制 +3. 性能优化 +4. 单元测试和集成测试 + +### 阶段4:发布准备 +1. 文档完善 +2. 使用示例 +3. 打包和发布脚本 +4. CI/CD配置 + +## 使用示例 + +```bash +# 基本使用 +ai-translate --root ./src --exts ts,js,go + +# 仅分析不修改 +ai-translate --root ./src --dry-run + +# 指定OpenAI配置 +ai-translate --root ./src --openai-key sk-... --model gpt-4 + +# 生成详细报告 +ai-translate --root ./src --verbose --output report.json +``` + +## 质量保证 + +### 测试策略 +- 单元测试:覆盖所有核心函数 +- 集成测试:端到端流程验证 +- 性能测试:大型仓库处理能力 +- 安全测试:API密钥保护 + +### 代码质量 +- TypeScript严格模式 +- ESLint规则检查 +- Prettier代码格式化 +- 代码覆盖率要求>90% + +## 风险和缓解措施 + +### 主要风险 +1. **API配额限制**:OpenAI API调用频率限制 +2. **翻译质量**:自动翻译可能不够准确 +3. **文件损坏**:替换操作可能破坏文件 +4. **性能问题**:大型仓库处理时间过长 + +### 缓解措施 +1. 实现智能重试和降级机制 +2. 提供人工审核和修正功能 +3. 强制备份和回滚机制 +4. 并发控制和进度显示 diff --git a/common/autoinstallers/rush-commands/src/convert-comments/index.ts b/common/autoinstallers/rush-commands/src/convert-comments/index.ts new file mode 100644 index 00000000..51d7a374 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/index.ts @@ -0,0 +1,232 @@ +#!/usr/bin/env node + +import { createProgram, parseOptions, showHelp } from './cli/command'; +import { loadConfig, validateConfig, printConfigInfo } from './cli/config'; +import { scanSourceFiles } from './modules/file-scan'; +import { detectChineseInFiles } from './modules/chinese-detection'; +import { TranslationService } from './modules/translation'; +import { + createReplacements, + replaceCommentsInFile, +} from './modules/file-replacement'; +import { + ReportCollector, + ProgressDisplay, + generateReport, + saveReportToFile, +} from './modules/report'; +import { FileScanConfig } from './types/index'; + +/** + * 主处理函数 + */ +async function processRepository( + rootPath: string, + extensions: string[], + config: any, + dryRun: boolean = false, + verbose: boolean = false, +): Promise { + const reportCollector = new ReportCollector(); + + try { + console.log('🚀 开始处理代码仓库...'); + + if (verbose) { + printConfigInfo(config, true); + } + + // 1. 扫描源文件 + console.log('\n📁 扫描源文件...'); + const scanConfig: FileScanConfig = { + root: rootPath, + extensions, + ignorePatterns: config.git.ignorePatterns, + includeUntracked: config.git.includeUntracked, + }; + + const filesResult = await scanSourceFiles(scanConfig); + if (!filesResult.success) { + throw new Error(`文件扫描失败: ${filesResult.error}`); + } + + const sourceFiles = filesResult.data; + console.log(`✅ 找到 ${sourceFiles.length} 个源文件`); + + if (sourceFiles.length === 0) { + console.log('⚠️ 未找到任何源文件,请检查根目录和文件扩展名设置'); + return; + } + + // 2. 检测中文注释 + console.log('\n🔍 检测中文注释...'); + const filesWithComments = detectChineseInFiles(sourceFiles); + + const totalComments = filesWithComments.reduce( + (sum, file) => sum + file.chineseComments.length, + 0, + ); + + console.log( + `✅ 在 ${filesWithComments.length} 个文件中找到 ${totalComments} 条中文注释`, + ); + + if (totalComments === 0) { + console.log('✅ 未发现中文注释,无需处理'); + return; + } + + // 3. 初始化翻译服务 + console.log('\n🤖 初始化翻译服务...'); + const translationService = new TranslationService(config.translation); + + // 4. 处理文件 + console.log('\n🔄 开始翻译处理...'); + const progressDisplay = new ProgressDisplay(filesWithComments.length); + + for (let i = 0; i < filesWithComments.length; i++) { + const fileWithComments = filesWithComments[i]; + const { file, chineseComments } = fileWithComments; + + progressDisplay.update(i + 1, file.path); + reportCollector.recordFileStart(file.path); + + try { + // 翻译注释 + const translations = await translationService.batchTranslate( + chineseComments, + config.translation.concurrency, + ); + + if (verbose) { + console.log(`\n📝 ${file.path}:`); + translations.forEach((translation, index) => { + console.log( + ` ${index + 1}. "${translation.original}" → "${translation.translated}"`, + ); + }); + } + + // 如果不是干运行模式,则替换文件内容 + if (!dryRun) { + const replacements = createReplacements( + file, + chineseComments, + translations, + ); + const operation = { file: file.path, replacements }; + + const result = await replaceCommentsInFile( + file, + operation, + ); + + if (!result.success) { + throw new Error(result.error || '文件替换失败'); + } + } + + reportCollector.recordFileComplete(file.path, chineseComments.length); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error(`\n❌ 处理文件失败: ${file.path} - ${errorMessage}`); + reportCollector.recordError( + file.path, + error instanceof Error ? error : new Error(errorMessage), + ); + } + } + + progressDisplay.complete(); + + // 5. 生成报告 + console.log('\n📊 生成处理报告...'); + const report = reportCollector.generateReport(); + + if (dryRun) { + console.log('\n🔍 预览模式 - 未实际修改文件'); + } + + // 显示报告 + const reportText = generateReport(report, 'console'); + console.log(reportText); + + // 保存报告到文件(如果指定了输出路径) + if (config.outputFile) { + await saveReportToFile( + report, + config.outputFile, + config.processing.outputFormat, + ); + console.log(`📄 报告已保存到: ${config.outputFile}`); + } + } catch (error) { + console.error('\n💥 处理过程中发生错误:', error); + process.exit(1); + } +} + +/** + * 主函数 + */ +async function main(): Promise { + try { + const program = createProgram(); + + // 解析命令行参数 + program.parse(); + const options = parseOptions(program); + + // 加载配置 + const config = await loadConfig(options); + + // 验证配置 + const validation = validateConfig(config); + if (!validation.valid) { + console.error('❌ 配置验证失败:'); + validation.errors.forEach(error => console.error(` - ${error}`)); + showHelp(); + process.exit(1); + } + + // 解析文件扩展名 + const extensions = options.exts + ? options.exts.split(',').map(ext => ext.trim()) + : config.processing.defaultExtensions; + + // 添加输出文件配置 + const fullConfig = { + ...config, + outputFile: options.output, + }; + + // 执行处理 + await processRepository( + options.root, + extensions, + fullConfig, + options.dryRun || false, + options.verbose || false, + ); + } catch (error) { + console.error('💥 程序执行失败:', error); + process.exit(1); + } +} + +// 处理未捕获的异常 +process.on('unhandledRejection', (reason, promise) => { + console.error('未处理的Promise拒绝:', reason); + process.exit(1); +}); + +process.on('uncaughtException', error => { + console.error('未捕获的异常:', error); + process.exit(1); +}); + +// 运行主函数 +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/modules/chinese-detection.ts b/common/autoinstallers/rush-commands/src/convert-comments/modules/chinese-detection.ts new file mode 100644 index 00000000..b1ea6deb --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/modules/chinese-detection.ts @@ -0,0 +1,470 @@ +import { + SourceFile, + ChineseComment, + ParsedComment, + FileWithComments, + CommentType, + MultiLineContext +} from '../types/index'; +import { getCommentPatterns } from '../utils/language'; +import { containsChinese, cleanCommentText } from '../utils/chinese'; + +/** + * 检查指定位置是否在字符串字面量内部 + */ +const isInsideStringLiteral = (line: string, position: number): boolean => { + let insideDoubleQuote = false; + let insideSingleQuote = false; + let insideBacktick = false; + let escapeNext = false; + + for (let i = 0; i < position; i++) { + const char = line[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (char === '"' && !insideSingleQuote && !insideBacktick) { + insideDoubleQuote = !insideDoubleQuote; + } else if (char === "'" && !insideDoubleQuote && !insideBacktick) { + insideSingleQuote = !insideSingleQuote; + } else if (char === '`' && !insideDoubleQuote && !insideSingleQuote) { + insideBacktick = !insideBacktick; + } + } + + return insideDoubleQuote || insideSingleQuote || insideBacktick; +}; + +/** + * 解析单行注释 + */ +const parseSingleLineComments = ( + content: string, + pattern: RegExp, + language?: string, +): ParsedComment[] => { + const comments: ParsedComment[] = []; + const lines = content.split('\n'); + + // 添加安全检查 + const maxLines = 5000; // 降低到5000行 + if (lines.length > maxLines) { + console.warn(`⚠️ 文件行数过多 (${lines.length}行),跳过单行注释解析`); + return comments; + } + + lines.forEach((line, index) => { + pattern.lastIndex = 0; // 重置正则表达式索引 + let match: RegExpExecArray | null; + + // 查找所有匹配,但只保留不在字符串内的 + let matchCount = 0; + const maxMatches = 100; // 限制每行最多匹配100次 + let lastIndex = 0; + + while ((match = pattern.exec(line)) !== null) { + // 防止无限循环的多重保护 + matchCount++; + if (matchCount > maxMatches) { + console.warn(`⚠️ 单行匹配次数过多,中断处理: ${line.substring(0, 50)}...`); + break; + } + + // 检查 lastIndex 是否前进,防止无限循环 + if (pattern.global) { + if (pattern.lastIndex <= lastIndex) { + // 如果 lastIndex 没有前进,手动前进一位避免无限循环 + pattern.lastIndex = lastIndex + 1; + if (pattern.lastIndex >= line.length) { + break; + } + } + lastIndex = pattern.lastIndex; + } + + if (match[1]) { + const commentContent = match[1]; + let commentStartIndex = match.index!; + let commentLength = 2; // 默认为 // + + // 根据语言确定注释符号 + if ( + language === 'yaml' || + language === 'toml' || + language === 'shell' || + language === 'python' || + language === 'ruby' + ) { + commentStartIndex = line.indexOf('#', match.index!); + commentLength = 1; // # 长度为 1 + } else if (language === 'ini') { + // INI 文件可能使用 # 或 ; + const hashIndex = line.indexOf('#', match.index!); + const semicolonIndex = line.indexOf(';', match.index!); + if ( + hashIndex >= 0 && + (semicolonIndex < 0 || hashIndex < semicolonIndex) + ) { + commentStartIndex = hashIndex; + commentLength = 1; + } else if (semicolonIndex >= 0) { + commentStartIndex = semicolonIndex; + commentLength = 1; + } + } else if (language === 'php') { + // PHP 可能使用 // 或 # + const slashIndex = line.indexOf('//', match.index!); + const hashIndex = line.indexOf('#', match.index!); + if (slashIndex >= 0 && (hashIndex < 0 || slashIndex < hashIndex)) { + commentStartIndex = slashIndex; + commentLength = 2; + } else if (hashIndex >= 0) { + commentStartIndex = hashIndex; + commentLength = 1; + } + } else { + // JavaScript/TypeScript/Go/Java/C/C++/C# style + commentStartIndex = line.indexOf('//', match.index!); + commentLength = 2; + } + + const startColumn = commentStartIndex; + const endColumn = startColumn + commentLength + commentContent.length; + + // 检查注释开始位置是否在字符串内部 + if ( + commentStartIndex >= 0 && + !isInsideStringLiteral(line, commentStartIndex) + ) { + comments.push({ + content: commentContent, + startLine: index + 1, + endLine: index + 1, + startColumn, + endColumn, + type: 'single-line', + }); + } + } + + // 防止无限循环 + if (!pattern.global) break; + } + }); + + return comments; +}; + +/** + * 解析多行注释 + */ +const parseMultiLineComments = ( + content: string, + startPattern: RegExp, + endPattern: RegExp, +): ParsedComment[] => { + const comments: ParsedComment[] = []; + const lines = content.split('\n'); + let inComment = false; + let commentStart: { line: number; column: number } | null = null; + let commentLines: string[] = []; + + // 添加安全检查 + const maxLines = 5000; // 降低到5000行 + if (lines.length > maxLines) { + console.warn(`⚠️ 文件行数过多 (${lines.length}行),跳过多行注释解析`); + return comments; + } + + // 添加处理计数器,防止无限循环 + let processedLines = 0; + const maxProcessedLines = 10000; + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + + // 防止无限处理 + processedLines++; + if (processedLines > maxProcessedLines) { + console.warn(`⚠️ 处理行数超限,中断解析`); + break; + } + + if (!inComment) { + startPattern.lastIndex = 0; + const startMatch = startPattern.exec(line); + + if (startMatch && !isInsideStringLiteral(line, startMatch.index!)) { + inComment = true; + commentStart = { line: lineIndex + 1, column: startMatch.index! }; + + // 检查是否在同一行结束 + endPattern.lastIndex = startMatch.index! + startMatch[0].length; + const endMatch = endPattern.exec(line); + + if (endMatch) { + // 单行多行注释 + const commentContent = line.substring( + startMatch.index! + startMatch[0].length, + endMatch.index!, + ); + + comments.push({ + content: commentContent, + startLine: lineIndex + 1, + endLine: lineIndex + 1, + startColumn: startMatch.index!, + endColumn: endMatch.index! + endMatch[0].length, + type: 'multi-line', + }); + + inComment = false; + commentStart = null; + } else { + // 多行注释开始 + const commentContent = line.substring( + startMatch.index! + startMatch[0].length, + ); + commentLines = [commentContent]; + } + } + } else { + // 在多行注释中 + endPattern.lastIndex = 0; + const endMatch = endPattern.exec(line); + + if (endMatch) { + // 多行注释结束 + const commentContent = line.substring(0, endMatch.index!); + commentLines.push(commentContent); + + + comments.push({ + content: commentLines.join('\n'), + startLine: commentStart!.line, + endLine: lineIndex + 1, + startColumn: commentStart!.column, + endColumn: endMatch.index! + endMatch[0].length, + type: 'multi-line', + }); + + inComment = false; + commentStart = null; + commentLines = []; + } else { + // 继续多行注释 + commentLines.push(line); + } + } + } + + return comments; +}; + +/** + * 解析文件中的所有注释 + */ +export const parseComments = (file: SourceFile): ParsedComment[] => { + const patterns = getCommentPatterns(file.language); + if (!patterns) return []; + + const singleLineComments = parseSingleLineComments( + file.content, + patterns.single, + file.language, + ); + const multiLineComments = parseMultiLineComments( + file.content, + patterns.multiStart, + patterns.multiEnd, + ); + + return [...singleLineComments, ...multiLineComments]; +}; + +/** + * 过滤包含中文的注释,对多行注释进行逐行处理 + */ +export const filterChineseComments = ( + comments: ParsedComment[], + language?: string, +): ChineseComment[] => { + const result: ChineseComment[] = []; + + for (const comment of comments) { + if (comment.type === 'multi-line' && comment.content.includes('\n')) { + // 多行注释:逐行处理 + const multiLineResults = processMultiLineCommentForChinese(comment, language); + result.push(...multiLineResults); + } else if (containsChinese(comment.content)) { + // 单行注释或单行多行注释 + result.push({ + ...comment, + content: cleanCommentText( + comment.content, + comment.type === 'documentation' ? 'multi-line' : comment.type, + language, + ), + }); + } + } + + return result; +}; + +/** + * 处理多行注释,提取含中文的行作为独立的注释单元 + */ +const processMultiLineCommentForChinese = ( + comment: ParsedComment, + language?: string, +): ChineseComment[] => { + const lines = comment.content.split('\n'); + const result: ChineseComment[] = []; + + lines.forEach((line, lineIndex) => { + const cleanedLine = cleanCommentText(line, 'multi-line', language); + + if (containsChinese(cleanedLine)) { + // 计算这一行在原始文件中的位置 + const actualLineNumber = comment.startLine + lineIndex; + + // 创建一个表示这一行的注释对象 + const lineComment: ChineseComment = { + content: cleanedLine, + startLine: actualLineNumber, + endLine: actualLineNumber, + startColumn: 0, // 这个值需要更精确计算,但对于多行注释内的行处理暂时用0 + endColumn: line.length, + type: 'multi-line', + // 添加多行注释的元数据,用于后续处理 + multiLineContext: { + isPartOfMultiLine: true, + originalComment: comment, + lineIndexInComment: lineIndex, + totalLinesInComment: lines.length + } + }; + + result.push(lineComment); + } + }); + + return result; +}; + +/** + * 检测文件中的中文注释 + */ +export const detectChineseInFile = (file: SourceFile): ChineseComment[] => { + try { + // 简单防护:跳过大文件 + if (file.content.length > 500000) { + // 500KB + console.warn( + `⚠️ 跳过大文件: ${file.path} (${file.content.length} 字符)`, + ); + return []; + } + + // 简单防护:跳过行数过多的文件 + const lines = file.content.split('\n'); + if (lines.length > 10000) { + console.warn(`⚠️ 跳过多行文件: ${file.path} (${lines.length} 行)`); + return []; + } + + const allComments = parseComments(file); + return filterChineseComments(allComments, file.language); + } catch (error) { + console.error(`❌ 文件处理失败: ${file.path} - ${error}`); + return []; + } +}; + +/** + * 批量检测多个文件中的中文注释 + */ +export const detectChineseInFiles = (files: SourceFile[]): FileWithComments[] => { + const results: FileWithComments[] = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const fileName = file.path.split('/').pop() || file.path; + + console.log(`🔍 检测进度: ${i + 1}/${files.length} (当前: ${fileName})`); + + try { + const chineseComments = detectChineseInFile(file); + + if (chineseComments.length > 0) { + results.push({ + file, + chineseComments, + }); + } + + console.log( + `✅ 完成: ${fileName} (找到 ${chineseComments.length} 条中文注释)`, + ); + } catch (error) { + console.error(`❌ 处理文件失败: ${fileName} - ${error}`); + // 继续处理其他文件 + continue; + } + } + + return results; +}; + +/** + * 获取注释统计信息 + */ +export const getCommentStats = (files: SourceFile[]): { + totalFiles: number; + filesWithComments: number; + totalComments: number; + chineseComments: number; + commentsByType: Record; +} => { + let totalComments = 0; + let chineseComments = 0; + let filesWithComments = 0; + const commentsByType: Record = { + 'single-line': 0, + 'multi-line': 0, + 'documentation': 0 + }; + + files.forEach(file => { + const allComments = parseComments(file); + const chineseCommentsInFile = filterChineseComments(allComments, file.language); + + if (chineseCommentsInFile.length > 0) { + filesWithComments++; + } + + totalComments += allComments.length; + chineseComments += chineseCommentsInFile.length; + + chineseCommentsInFile.forEach(comment => { + commentsByType[comment.type]++; + }); + }); + + return { + totalFiles: files.length, + filesWithComments, + totalComments, + chineseComments, + commentsByType + }; +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/modules/file-replacement.ts b/common/autoinstallers/rush-commands/src/convert-comments/modules/file-replacement.ts new file mode 100644 index 00000000..a5d40c3d --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/modules/file-replacement.ts @@ -0,0 +1,421 @@ +import { + Replacement, + ReplacementOperation, + SourceFile, + ChineseComment, + TranslationResult, +} from '../types/index'; +import { tryCatch } from '../utils/fp'; + + +/** + * 检查字符串是否包含中文字符 + */ +const containsChinese = (text: string): boolean => { + return /[\u4e00-\u9fff]/.test(text); +}; + +/** + * 保持注释的原始格式,支持逐行翻译多行注释 + */ +export const preserveCommentFormat = ( + originalComment: string, + translatedComment: string, + commentType: 'single-line' | 'multi-line', +): string => { + if (commentType === 'single-line') { + // 保持单行注释的前缀空格和注释符 - 支持多种语言 + let match = originalComment.match(/^(\s*\/\/\s*)/); // JavaScript/TypeScript style + if (match) { + return match[1] + translatedComment.trim(); + } + + match = originalComment.match(/^(\s*#\s*)/); // Shell/Python/YAML style + if (match) { + return match[1] + translatedComment.trim(); + } + + match = originalComment.match(/^(\s*;\s*)/); // Some config files + if (match) { + return match[1] + translatedComment.trim(); + } + + // 如果无法识别,尝试从原始内容推断 + if (originalComment.includes('#')) { + const hashMatch = originalComment.match(/^(\s*#\s*)/); + return (hashMatch ? hashMatch[1] : '# ') + translatedComment.trim(); + } + + // 默认使用 JavaScript 风格 + return '// ' + translatedComment.trim(); + } + + if (commentType === 'multi-line') { + const lines = originalComment.split('\n'); + + if (lines.length === 1) { + // 单行多行注释 /* ... */ 或 /** ... */ + const startMatch = originalComment.match(/^(\s*\/\*\*?\s*)/); + const endMatch = originalComment.match(/(\s*\*\/\s*)$/); + + let prefix = '/* '; + let suffix = ' */'; + + if (startMatch) { + prefix = startMatch[1]; + } + + if (endMatch) { + suffix = endMatch[1]; + } + + return prefix + translatedComment.trim() + suffix; + } else { + // 多行注释 - 需要逐行处理 + return processMultiLineComment(originalComment, translatedComment); + } + } + + return translatedComment; +}; + +/** + * 处理多行注释,逐行翻译含中文的行,保持其他行原样 + */ +export const processMultiLineComment = ( + originalComment: string, + translatedContent: string, +): string => { + const originalLines = originalComment.split('\n'); + + // 提取每行的注释内容(去除 /** * 等前缀) + const extractedLines = originalLines.map(line => { + // 匹配不同类型的注释行 + if (line.match(/^\s*\/\*\*?\s*/)) { + // 开始行: /** 或 /* + return { prefix: line.match(/^\s*\/\*\*?\s*/)![0], content: line.replace(/^\s*\/\*\*?\s*/, '') }; + } else if (line.match(/^\s*\*\/\s*$/)) { + // 结束行: */ + return { prefix: line.match(/^\s*\*\/\s*$/)![0], content: '' }; + } else if (line.match(/^\s*\*\s*/)) { + // 中间行: * content + const match = line.match(/^(\s*\*\s*)(.*)/); + return { prefix: match![1], content: match![2] }; + } else { + // 其他情况 + return { prefix: '', content: line }; + } + }); + + // 收集需要翻译的行 + const linesToTranslate = extractedLines + .map((line, index) => ({ index, content: line.content })) + .filter(item => containsChinese(item.content)); + + // 如果没有中文内容,返回原始注释 + if (linesToTranslate.length === 0) { + return originalComment; + } + + // 解析翻译结果 - 假设翻译服务按顺序返回翻译后的行 + const translatedLines = translatedContent.split('\n'); + const translations = new Map(); + + // 将翻译结果映射到对应的行 + linesToTranslate.forEach((item, transIndex) => { + if (transIndex < translatedLines.length) { + translations.set(item.index, translatedLines[transIndex].trim()); + } + }); + + // 重建注释,保持原始结构 + return extractedLines + .map((line, index) => { + if (translations.has(index)) { + // 使用翻译内容,保持原始前缀 + return line.prefix + translations.get(index); + } else { + // 保持原样 + return originalLines[index]; + } + }) + .join('\n'); +}; + +/** + * 创建替换操作 + */ +export const createReplacements = ( + file: SourceFile, + comments: ChineseComment[], + translations: TranslationResult[], +): Replacement[] => { + const replacements: Replacement[] = []; + + comments.forEach((comment, index) => { + const translation = translations[index]; + if (!translation) return; + + if (comment.multiLineContext?.isPartOfMultiLine) { + // 处理多行注释中的单行 + const replacement = createMultiLineReplacement(file, comment, translation); + if (replacement) { + replacements.push(replacement); + } + } else { + // 处理普通注释(单行注释或整个多行注释) + const replacement = createRegularReplacement(file, comment, translation); + if (replacement) { + replacements.push(replacement); + } + } + }); + + return replacements; +}; + +/** + * 为多行注释中的单行创建替换操作 + */ +const createMultiLineReplacement = ( + file: SourceFile, + comment: ChineseComment, + translation: TranslationResult, +): Replacement | null => { + const lines = file.content.split('\n'); + const lineIndex = comment.startLine - 1; + + if (lineIndex >= lines.length) return null; + + const originalLine = lines[lineIndex]; + + // 查找这一行中中文内容的位置 + const cleanedContent = comment.content; + + // 更精确地查找中文内容在原始行中的位置 + const commentContentRegex = new RegExp(cleanedContent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + const contentMatch = originalLine.match(commentContentRegex); + + if (!contentMatch) { + return null; + } + + const chineseStart = contentMatch.index!; + const chineseEnd = chineseStart + contentMatch[0].length; + + // 计算在整个文件中的位置 + let start = 0; + for (let i = 0; i < lineIndex; i++) { + start += lines[i].length + 1; // +1 for newline + } + start += chineseStart; + + const end = start + (chineseEnd - chineseStart); + + return { + start, + end, + original: originalLine.substring(chineseStart, chineseEnd), + replacement: translation.translated, + }; +}; + +/** + * 为普通注释创建替换操作 + */ +const createRegularReplacement = ( + file: SourceFile, + comment: ChineseComment, + translation: TranslationResult, +): Replacement | null => { + const lines = file.content.split('\n'); + const startLineIndex = comment.startLine - 1; + const endLineIndex = comment.endLine - 1; + + // 计算原始注释在文件中的精确位置 + let start = 0; + for (let i = 0; i < startLineIndex; i++) { + start += lines[i].length + 1; // +1 for newline + } + start += comment.startColumn; + + let end = start; + if (comment.startLine === comment.endLine) { + // 同一行 + end = start + (comment.endColumn - comment.startColumn); + } else { + // 跨行 - 重新计算end位置 + end = 0; + for (let i = 0; i < endLineIndex; i++) { + end += lines[i].length + 1; // +1 for newline + } + end += comment.endColumn; + } + + // 获取原始注释文本 + const originalText = file.content.substring(start, end); + + // 应用格式保持 + const formattedTranslation = preserveCommentFormat( + originalText, + translation.translated, + comment.type === 'documentation' ? 'multi-line' : comment.type, + ); + + return { + start, + end, + original: originalText, + replacement: formattedTranslation, + }; +}; + +/** + * 应用替换操作到文本内容 + */ +export const applyReplacements = ( + content: string, + replacements: Replacement[], +): string => { + // 按位置倒序排列,避免替换后位置偏移 + const sortedReplacements = [...replacements].sort( + (a, b) => b.start - a.start, + ); + + let result = content; + + for (const replacement of sortedReplacements) { + const before = result.substring(0, replacement.start); + const after = result.substring(replacement.end); + result = before + replacement.replacement + after; + } + + return result; +}; + +/** + * 替换文件中的注释 + */ +export const replaceCommentsInFile = async ( + file: SourceFile, + operation: ReplacementOperation, +): Promise<{ success: boolean; error?: string }> => { + return tryCatch(async () => { + const fs = await import('fs/promises'); + + // 应用替换 + const newContent = applyReplacements( + file.content, + operation.replacements, + ); + + // 写入文件 + await fs.writeFile(file.path, newContent, 'utf-8'); + + return { success: true }; + }).then(result => { + if (result.success) { + return result.data; + } else { + return { + success: false, + error: + result.error instanceof Error + ? result.error.message + : String(result.error), + }; + } + }); +}; + +/** + * 批量替换多个文件 + */ +export const batchReplaceFiles = async ( + operations: ReplacementOperation[], +): Promise< + Array<{ + file: string; + success: boolean; + error?: string; + }> +> => { + const results = await Promise.allSettled( + operations.map(async operation => { + const fs = await import('fs/promises'); + const content = await fs.readFile(operation.file, 'utf-8'); + const sourceFile: SourceFile = { + path: operation.file, + content, + language: 'other', // 临时值,实际应该检测 + }; + + const result = await replaceCommentsInFile( + sourceFile, + operation, + ); + return { file: operation.file, ...result }; + }), + ); + + return results.map((result, index) => { + if (result.status === 'fulfilled') { + return result.value; + } else { + return { + file: operations[index].file, + success: false, + error: + result.reason instanceof Error + ? result.reason.message + : String(result.reason), + }; + } + }); +}; + +/** + * 验证替换操作 + */ +export const validateReplacements = ( + content: string, + replacements: Replacement[], +): { valid: boolean; errors: string[] } => { + const errors: string[] = []; + + // 检查位置是否有效 + replacements.forEach((replacement, index) => { + if (replacement.start < 0 || replacement.end > content.length) { + errors.push(`Replacement ${index}: Invalid position range`); + } + + if (replacement.start >= replacement.end) { + errors.push( + `Replacement ${index}: Start position must be less than end position`, + ); + } + + // 检查原文是否匹配 + const actualText = content.substring(replacement.start, replacement.end); + if (actualText !== replacement.original) { + errors.push(`Replacement ${index}: Original text mismatch`); + } + }); + + // 检查是否有重叠 + const sortedReplacements = [...replacements].sort( + (a, b) => a.start - b.start, + ); + for (let i = 0; i < sortedReplacements.length - 1; i++) { + const current = sortedReplacements[i]; + const next = sortedReplacements[i + 1]; + + if (current.end > next.start) { + errors.push( + `Overlapping replacements at positions ${current.start}-${current.end} and ${next.start}-${next.end}`, + ); + } + } + + return { valid: errors.length === 0, errors }; +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/modules/file-scan.ts b/common/autoinstallers/rush-commands/src/convert-comments/modules/file-scan.ts new file mode 100644 index 00000000..46c83742 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/modules/file-scan.ts @@ -0,0 +1,123 @@ +import { promises as fs } from 'fs'; +import { SourceFile, FileScanConfig, Result } from '../types/index'; +import { detectLanguage, filterFilesByExtensions, isTextFile } from '../utils/language'; +import { getGitTrackedFiles, getAllGitFiles } from '../utils/git'; +import { tryCatch } from '../utils/fp'; + +/** + * 读取文件内容并创建SourceFile对象 + */ +export const readSourceFile = async (filePath: string): Promise> => { + return tryCatch(async () => { + const content = await fs.readFile(filePath, 'utf-8'); + const language = detectLanguage(filePath); + + return { + path: filePath, + content, + language + }; + }); +}; + +/** + * 批量读取源文件 + */ +export const readSourceFiles = async (filePaths: string[]): Promise => { + const results = await Promise.allSettled( + filePaths.map(path => readSourceFile(path)) + ); + + return results + .filter((result): result is PromiseFulfilledResult> => + result.status === 'fulfilled' && result.value.success + ) + .map(result => (result.value as { success: true; data: SourceFile }).data); +}; + +/** + * 获取Git仓库中的源码文件 + */ +export const getSourceFiles = async (config: FileScanConfig): Promise> => { + const { root, extensions, includeUntracked } = config; + + return tryCatch(async () => { + // 获取Git文件列表 + const gitFilesResult = includeUntracked + ? await getAllGitFiles(root) + : await getGitTrackedFiles(root); + + if (!gitFilesResult.success) { + throw gitFilesResult.error; + } + + let files = gitFilesResult.data; + + // 过滤文本文件 + files = files.filter(isTextFile); + + // 根据扩展名过滤 + if (extensions.length > 0) { + files = filterFilesByExtensions(files, extensions); + } + + return files; + }); +}; + +/** + * 扫描并读取所有源码文件 + */ +export const scanSourceFiles = async (config: FileScanConfig): Promise> => { + return tryCatch(async () => { + const filesResult = await getSourceFiles(config); + + if (!filesResult.success) { + throw filesResult.error; + } + + const sourceFiles = await readSourceFiles(filesResult.data); + return sourceFiles; + }); +}; + +/** + * 检查文件是否存在且可读 + */ +export const isFileAccessible = async (filePath: string): Promise => { + try { + await fs.access(filePath, fs.constants.R_OK); + return true; + } catch { + return false; + } +}; + +/** + * 获取文件统计信息 + */ +export const getFileStats = async (filePaths: string[]): Promise<{ + total: number; + accessible: number; + textFiles: number; + supportedFiles: number; +}> => { + const accessibilityResults = await Promise.allSettled( + filePaths.map(isFileAccessible) + ); + + const accessible = accessibilityResults.filter( + (result): result is PromiseFulfilledResult => + result.status === 'fulfilled' && result.value + ).length; + + const textFiles = filePaths.filter(isTextFile).length; + const supportedFiles = filePaths.filter(path => detectLanguage(path) !== 'other').length; + + return { + total: filePaths.length, + accessible, + textFiles, + supportedFiles + }; +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/modules/report.ts b/common/autoinstallers/rush-commands/src/convert-comments/modules/report.ts new file mode 100644 index 00000000..d12e6769 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/modules/report.ts @@ -0,0 +1,302 @@ +import { + ProcessingReport, + ProcessingStats, + FileProcessingDetail, +} from '../types/index.js'; + +/** + * 报告收集器类 + */ +export class ReportCollector { + private stats: ProcessingStats = { + totalFiles: 0, + processedFiles: 0, + translatedComments: 0, + skippedFiles: 0, + errors: [], + startTime: Date.now(), + endTime: 0, + }; + + private fileDetails: Map = new Map(); + + /** + * 记录文件处理开始 + */ + recordFileStart(filePath: string): void { + this.stats.totalFiles++; + this.fileDetails.set(filePath, { + file: filePath, + commentCount: 0, + status: 'processing', + startTime: Date.now(), + }); + } + + /** + * 记录文件处理完成 + */ + recordFileComplete(filePath: string, commentCount: number): void { + const detail = this.fileDetails.get(filePath); + if (detail) { + detail.status = 'success'; + detail.commentCount = commentCount; + detail.endTime = Date.now(); + this.stats.processedFiles++; + this.stats.translatedComments += commentCount; + } + } + + /** + * 记录文件跳过 + */ + recordFileSkipped(filePath: string, reason?: string): void { + const detail = this.fileDetails.get(filePath); + if (detail) { + detail.status = 'skipped'; + detail.errorMessage = reason; + detail.endTime = Date.now(); + this.stats.skippedFiles++; + } + } + + /** + * 记录处理错误 + */ + recordError(filePath: string, error: Error): void { + const detail = this.fileDetails.get(filePath); + if (detail) { + detail.status = 'error'; + detail.errorMessage = error.message; + detail.endTime = Date.now(); + } + this.stats.errors.push({ file: filePath, error: error.message }); + } + + /** + * 完成统计 + */ + finalize(): void { + this.stats.endTime = Date.now(); + } + + /** + * 获取统计信息 + */ + getStats(): ProcessingStats { + return { ...this.stats }; + } + + /** + * 获取文件详情 + */ + getFileDetails(): FileProcessingDetail[] { + return Array.from(this.fileDetails.values()); + } + + /** + * 生成完整报告 + */ + generateReport(): ProcessingReport { + this.finalize(); + const duration = (this.stats.endTime - this.stats.startTime) / 1000; + + return { + stats: this.getStats(), + details: this.getFileDetails(), + duration, + }; + } + + /** + * 重置收集器 + */ + reset(): void { + this.stats = { + totalFiles: 0, + processedFiles: 0, + translatedComments: 0, + skippedFiles: 0, + errors: [], + startTime: Date.now(), + endTime: 0, + }; + this.fileDetails.clear(); + } +} + +/** + * 生成控制台报告 + */ +export const generateConsoleReport = (report: ProcessingReport): string => { + const { stats, duration } = report; + const successRate = + stats.totalFiles > 0 + ? ((stats.processedFiles / stats.totalFiles) * 100).toFixed(1) + : '0'; + + let output = ` +📊 翻译处理报告 +================== +总文件数: ${stats.totalFiles} +处理成功: ${stats.processedFiles} +跳过文件: ${stats.skippedFiles} +翻译注释: ${stats.translatedComments} +错误数量: ${stats.errors.length} +成功率: ${successRate}% +处理时间: ${duration.toFixed(2)}秒 +`; + + if (stats.errors.length > 0) { + output += '\n❌ 错误详情:\n'; + stats.errors.forEach(error => { + output += ` ${error.file}: ${error.error}\n`; + }); + } else { + output += '\n✅ 处理完成,无错误'; + } + + return output; +}; + +/** + * 生成Markdown报告 + */ +export const generateMarkdownReport = (report: ProcessingReport): string => { + const { stats, details, duration } = report; + const successRate = + stats.totalFiles > 0 + ? ((stats.processedFiles / stats.totalFiles) * 100).toFixed(1) + : '0'; + + let markdown = `# 中文注释翻译报告 + +## 📊 统计概览 + +| 指标 | 数值 | +|------|------| +| 总文件数 | ${stats.totalFiles} | +| 处理成功 | ${stats.processedFiles} | +| 跳过文件 | ${stats.skippedFiles} | +| 翻译注释 | ${stats.translatedComments} | +| 错误数量 | ${stats.errors.length} | +| 成功率 | ${successRate}% | +| 处理时间 | ${duration.toFixed(2)}秒 | + +## 📁 文件详情 + +| 文件路径 | 状态 | 注释数量 | 耗时(ms) | 备注 | +|----------|------|----------|----------|------| +`; + + details.forEach(detail => { + const duration = + detail.endTime && detail.startTime + ? detail.endTime - detail.startTime + : 0; + const status = + detail.status === 'success' + ? '✅' + : detail.status === 'error' + ? '❌' + : detail.status === 'skipped' + ? '⏭️' + : '🔄'; + + markdown += `| ${detail.file} | ${status} | ${detail.commentCount} | ${duration} | ${detail.errorMessage || '-'} |\n`; + }); + + if (stats.errors.length > 0) { + markdown += '\n## ❌ 错误详情\n\n'; + stats.errors.forEach((error, index) => { + markdown += `${index + 1}. **${error.file}**\n \`\`\`\n ${error.error}\n \`\`\`\n\n`; + }); + } + + return markdown; +}; + +/** + * 生成JSON报告 + */ +export const generateJsonReport = (report: ProcessingReport): string => { + return JSON.stringify(report, null, 2); +}; + +/** + * 根据格式生成报告 + */ +export const generateReport = ( + report: ProcessingReport, + format: 'json' | 'markdown' | 'console' = 'console', +): string => { + switch (format) { + case 'json': + return generateJsonReport(report); + case 'markdown': + return generateMarkdownReport(report); + case 'console': + default: + return generateConsoleReport(report); + } +}; + +/** + * 保存报告到文件 + */ +export const saveReportToFile = async ( + report: ProcessingReport, + filePath: string, + format: 'json' | 'markdown' | 'console' = 'json', +): Promise => { + const content = generateReport(report, format); + const fs = await import('fs/promises'); + await fs.writeFile(filePath, content, 'utf-8'); +}; + +/** + * 在控制台显示实时进度 + */ +export class ProgressDisplay { + private total: number = 0; + private current: number = 0; + private startTime: number = Date.now(); + + constructor(total: number) { + this.total = total; + } + + /** + * 更新进度 + */ + update(current: number, currentFile?: string): void { + this.current = current; + const percentage = ((current / this.total) * 100).toFixed(1); + const elapsed = (Date.now() - this.startTime) / 1000; + const speed = current / elapsed; + const eta = speed > 0 ? (this.total - current) / speed : 0; + + let line = `进度: ${current}/${this.total} (${percentage}%) | 耗时: ${elapsed.toFixed(1)}s`; + + if (eta > 0) { + line += ` | 预计剩余: ${eta.toFixed(1)}s`; + } + + if (currentFile) { + line += ` | 当前: ${currentFile}`; + } + + // 清除当前行并输出新进度 + process.stdout.write( + '\r' + ' '.repeat(process.stdout.columns || 80) + '\r', + ); + process.stdout.write(line); + } + + /** + * 完成进度显示 + */ + complete(): void { + process.stdout.write('\n'); + } +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/modules/translation.ts b/common/autoinstallers/rush-commands/src/convert-comments/modules/translation.ts new file mode 100644 index 00000000..c0462e93 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/modules/translation.ts @@ -0,0 +1,239 @@ +import { + TranslationResult, + TranslationContext, + ChineseComment, + TranslationError, +} from '../types/index'; +import { TranslationConfig } from '../types/config'; +import { retry, chunk } from '../utils/fp'; +import { isValidTranslation } from '../utils/chinese'; +import { translate as volcTranslate, TranslateConfig as VolcTranslateConfig } from '../volc/translate'; + +/** + * 翻译服务类 + */ +export class TranslationService { + private config: TranslationConfig; + private cache = new Map(); + + constructor(config: TranslationConfig) { + this.config = config; + } + + /** + * 转换为火山引擎翻译配置 + */ + private toVolcConfig(): VolcTranslateConfig { + return { + accessKeyId: this.config.accessKeyId, + secretAccessKey: this.config.secretAccessKey, + region: this.config.region, + sourceLanguage: this.config.sourceLanguage, + targetLanguage: this.config.targetLanguage, + }; + } + + /** + * 计算翻译置信度(简单实现) + */ + private calculateConfidence(translated: string, original: string): number { + // 基于长度比例和有效性的简单置信度计算 + const lengthRatio = translated.length / original.length; + + if (!isValidTranslation(original, translated)) { + return 0; + } + + // 理想的长度比例在0.8-2.0之间 + let confidence = 0.8; + if (lengthRatio >= 0.8 && lengthRatio <= 2.0) { + confidence = 0.9; + } + + return confidence; + } + + /** + * 调用火山引擎API进行翻译 + */ + private async callVolcTranslate(texts: string[]): Promise { + const volcConfig = this.toVolcConfig(); + const response = await volcTranslate(texts, volcConfig); + + return response.TranslationList.map(item => item.Translation); + } + + /** + * 翻译单个注释 + */ + async translateComment( + comment: string, + context?: TranslationContext, + ): Promise { + // 检查缓存 + const cacheKey = this.getCacheKey(comment, context); + const cached = this.cache.get(cacheKey); + if (cached) { + return cached; + } + + try { + const translations = await retry( + () => this.callVolcTranslate([comment]), + this.config.maxRetries, + 1000, + ); + + const translated = translations[0]; + if (!translated) { + throw new Error('Empty translation response'); + } + + const result: TranslationResult = { + original: comment, + translated, + confidence: this.calculateConfidence(translated, comment), + }; + + // 缓存结果 + this.cache.set(cacheKey, result); + + return result; + } catch (error) { + throw new TranslationError( + `Translation failed: ${error instanceof Error ? error.message : String(error)}`, + comment, + ); + } + } + + /** + * 生成缓存键 + */ + private getCacheKey(comment: string, context?: TranslationContext): string { + const contextStr = context + ? `${context.language}-${context.commentType}-${context.nearbyCode || ''}` + : ''; + return `${comment}|${contextStr}`; + } + + /** + * 批量翻译注释 + */ + async batchTranslate( + comments: ChineseComment[], + concurrency: number = this.config.concurrency, + ): Promise { + // 提取未缓存的注释 + const uncachedComments: { comment: ChineseComment; index: number }[] = []; + const results: TranslationResult[] = new Array(comments.length); + + // 检查缓存 + comments.forEach((comment, index) => { + const cacheKey = this.getCacheKey(comment.content); + const cached = this.cache.get(cacheKey); + if (cached) { + results[index] = cached; + } else { + uncachedComments.push({ comment, index }); + } + }); + + // 如果所有注释都已缓存,直接返回 + if (uncachedComments.length === 0) { + return results; + } + + // 分批翻译未缓存的注释 + const chunks = chunk(uncachedComments, concurrency); + + for (const chunkItems of chunks) { + try { + const textsToTranslate = chunkItems.map(item => item.comment.content); + const translations = await retry( + () => this.callVolcTranslate(textsToTranslate), + this.config.maxRetries, + 1000, + ); + + // 处理翻译结果 + chunkItems.forEach((item, chunkIndex) => { + const translated = translations[chunkIndex]; + if (translated) { + const result: TranslationResult = { + original: item.comment.content, + translated, + confidence: this.calculateConfidence(translated, item.comment.content), + }; + + // 缓存结果 + const cacheKey = this.getCacheKey(item.comment.content); + this.cache.set(cacheKey, result); + + results[item.index] = result; + } else { + // 如果翻译失败,创建一个错误结果 + results[item.index] = { + original: item.comment.content, + translated: item.comment.content, // 翻译失败时保持原文 + confidence: 0, + }; + } + }); + } catch (error) { + // 如果整个批次翻译失败,为这个批次的所有注释创建错误结果 + chunkItems.forEach(item => { + results[item.index] = { + original: item.comment.content, + translated: item.comment.content, // 翻译失败时保持原文 + confidence: 0, + }; + }); + + console.warn(`批量翻译失败: ${error instanceof Error ? error.message : String(error)}`); + } + } + + return results; + } + + /** + * 保存翻译缓存到文件 + */ + async saveCache(filePath: string): Promise { + const cacheData = Object.fromEntries(this.cache); + const fs = await import('fs/promises'); + await fs.writeFile(filePath, JSON.stringify(cacheData, null, 2)); + } + + /** + * 从文件加载翻译缓存 + */ + async loadCache(filePath: string): Promise { + try { + const fs = await import('fs/promises'); + const data = await fs.readFile(filePath, 'utf-8'); + const cacheData = JSON.parse(data); + this.cache = new Map(Object.entries(cacheData)); + } catch { + // 缓存文件不存在或损坏,忽略 + } + } + + /** + * 清空缓存 + */ + clearCache(): void { + this.cache.clear(); + } + + /** + * 获取缓存统计 + */ + getCacheStats(): { size: number; hitRate: number } { + return { + size: this.cache.size, + hitRate: 0, // 需要实际统计命中率 + }; + } +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/requirements.md b/common/autoinstallers/rush-commands/src/convert-comments/requirements.md new file mode 100644 index 00000000..5294871e --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/requirements.md @@ -0,0 +1,27 @@ +# 代码仓库中的中文备注转换为英文 + +## 简介 + +一段 JS 脚本,用于将代码仓库内的中文备注转换为英文。 + +## 执行逻辑: + +1. 调用 git 命令,先查出本仓库下所有源码文件 +2. 读取文件内容,分析文件中(可能包含 go 和ts、js、md 等代码,主要关注文本型文件)是否包含中文备注,是的话将文件目录加进待处理数组 tasks 中 +3. 遍历 tasks 中的文件,提取出中文代码备注,调用 openai 接口,进行翻译,翻译后插入源位置 +4. 所有文件均处理完毕后,输出报告 + +## 命令行参数: + +命令名称:ai-translate +参数: + +- `root`: 需要执行命令的目录 +- `exts`: 需要进行处理的文件扩展名,支持数组,默认为空 + +## 技术约束 + +- 使用 ts 编写代码 +- 调用 openai api 做翻译 +- 使用 commander 实现命令行交互 +- 优先使用 FP 编写代码 diff --git a/common/autoinstallers/rush-commands/src/convert-comments/technical-specification.md b/common/autoinstallers/rush-commands/src/convert-comments/technical-specification.md new file mode 100644 index 00000000..5c73cf02 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/technical-specification.md @@ -0,0 +1,636 @@ +# 中文备注转换为英文 - 技术规格说明 + +## 1. 文件扫描模块详细设计 + +### 1.1 Git文件获取 + +```typescript +import simpleGit from 'simple-git'; + +/** + * 获取Git仓库中的所有已跟踪文件 + */ +export const getGitTrackedFiles = async (root: string): Promise => { + const git = simpleGit(root); + const files = await git.raw(['ls-files']); + return files + .split('\n') + .filter(Boolean) + .map(file => path.resolve(root, file)); +}; +``` + +### 1.2 文件扩展名过滤 + +```typescript +/** + * 根据扩展名过滤文件 + */ +export const filterFilesByExtensions = ( + files: string[], + extensions: string[] +): string[] => { + if (extensions.length === 0) { + // 默认支持的文本文件扩展名 + const defaultExtensions = ['.ts', '.js', '.jsx', '.tsx', '.go', '.md', '.txt', '.json']; + return files.filter(file => + defaultExtensions.some(ext => file.endsWith(ext)) + ); + } + + return files.filter(file => + extensions.some(ext => file.endsWith(`.${ext}`)) + ); +}; +``` + +### 1.3 编程语言识别 + +```typescript +export const detectLanguage = (filePath: string): SourceFileLanguage => { + const ext = path.extname(filePath).toLowerCase(); + + const languageMap: Record = { + '.ts': 'typescript', + '.tsx': 'typescript', + '.js': 'javascript', + '.jsx': 'javascript', + '.go': 'go', + '.md': 'markdown', + '.txt': 'text', + '.json': 'json' + }; + + return languageMap[ext] || 'other'; +}; +``` + +## 2. 中文检测模块详细设计 + +### 2.1 注释解析器 + +```typescript +interface CommentPattern { + single: RegExp; + multiStart: RegExp; + multiEnd: RegExp; +} + +const commentPatterns: Record = { + typescript: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + javascript: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + go: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + markdown: { + single: //g, + multiStart: //g + } +}; +``` + +### 2.2 中文字符检测 + +```typescript +/** + * 检测文本是否包含中文字符 + */ +export const containsChinese = (text: string): boolean => { + // Unicode范围:中日韩统一表意文字 + const chineseRegex = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/; + return chineseRegex.test(text); +}; + +/** + * 提取文本中的中文部分 + */ +export const extractChineseParts = (text: string): string[] => { + const chineseRegex = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]+/g; + return text.match(chineseRegex) || []; +}; +``` + +### 2.3 注释位置定位 + +```typescript +export const parseComments = (content: string, language: string): ParsedComment[] => { + const pattern = commentPatterns[language]; + if (!pattern) return []; + + const comments: ParsedComment[] = []; + const lines = content.split('\n'); + + // 解析单行注释 + lines.forEach((line, index) => { + const match = pattern.single.exec(line); + if (match && containsChinese(match[1])) { + comments.push({ + content: match[1].trim(), + startLine: index + 1, + endLine: index + 1, + startColumn: match.index, + endColumn: match.index + match[0].length, + type: 'single-line' + }); + } + }); + + // 解析多行注释 + const multiLineComments = parseMultiLineComments(content, pattern); + comments.push(...multiLineComments); + + return comments; +}; +``` + +## 3. 翻译服务模块详细设计 + +### 3.1 OpenAI API集成 + +```typescript +import OpenAI from 'openai'; + +export class TranslationService { + private openai: OpenAI; + private config: TranslationConfig; + + constructor(config: TranslationConfig) { + this.config = config; + this.openai = new OpenAI({ + apiKey: config.apiKey, + timeout: config.timeout, + }); + } + + async translateComment( + comment: string, + context?: TranslationContext + ): Promise { + const prompt = this.createTranslationPrompt(comment, context); + + try { + const response = await this.openai.chat.completions.create({ + model: this.config.model, + messages: [ + { + role: 'system', + content: 'You are a professional code comment translator. Translate Chinese comments to English while preserving the original meaning and code formatting.' + }, + { + role: 'user', + content: prompt + } + ], + temperature: 0.3, + max_tokens: 200 + }); + + const translated = response.choices[0]?.message?.content?.trim(); + if (!translated) { + throw new Error('Empty translation response'); + } + + return { + original: comment, + translated, + confidence: this.calculateConfidence(response) + }; + } catch (error) { + throw new TranslationError(`Translation failed: ${error.message}`, comment); + } + } +} +``` + +### 3.2 翻译提示词优化 + +```typescript +private createTranslationPrompt( + comment: string, + context?: TranslationContext +): string { + const basePrompt = ` +Translate the following Chinese code comment to English: + +Chinese comment: "${comment}" + +Requirements: +1. Keep the same tone and style +2. Preserve any code-related terminology +3. Maintain brevity and clarity +4. Return only the translated text without quotes +`; + + if (context) { + return basePrompt + ` +Context: +- File type: ${context.language} +- Function/Variable name: ${context.nearbyCode} +- Comment type: ${context.commentType} +`; + } + + return basePrompt; +} +``` + +### 3.3 批量翻译优化 + +```typescript +export const batchTranslate = async ( + comments: ChineseComment[], + service: TranslationService, + concurrency: number = 3 +): Promise => { + const semaphore = new Semaphore(concurrency); + + return Promise.all( + comments.map(async (comment) => { + await semaphore.acquire(); + try { + return await service.translateComment(comment.content); + } finally { + semaphore.release(); + } + }) + ); +}; +``` + +## 4. 文件替换模块详细设计 + +### 4.1 精确位置替换 + +```typescript +export const applyReplacements = ( + content: string, + replacements: Replacement[] +): string => { + // 按位置倒序排列,避免替换后位置偏移 + const sortedReplacements = replacements.sort((a, b) => b.start - a.start); + + let result = content; + + for (const replacement of sortedReplacements) { + const before = result.substring(0, replacement.start); + const after = result.substring(replacement.end); + result = before + replacement.replacement + after; + } + + return result; +}; +``` + +### 4.2 备份机制 + +```typescript +export const createBackup = async (filePath: string): Promise => { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = `${filePath}.backup.${timestamp}`; + + await fs.copyFile(filePath, backupPath); + return backupPath; +}; + +export const restoreFromBackup = async ( + originalPath: string, + backupPath: string +): Promise => { + await fs.copyFile(backupPath, originalPath); + await fs.unlink(backupPath); +}; +``` + +### 4.3 格式保持 + +```typescript +/** + * 保持注释的原始缩进和格式 + */ +export const preserveCommentFormat = ( + originalComment: string, + translatedComment: string, + commentType: CommentType +): string => { + const originalLines = originalComment.split('\n'); + const translatedLines = translatedComment.split('\n'); + + if (commentType === 'single-line') { + // 保持单行注释的前缀空格 + const leadingSpaces = originalComment.match(/^(\s*)/)?.[1] || ''; + return leadingSpaces + translatedComment.trim(); + } + + if (commentType === 'multi-line') { + // 保持多行注释的对齐 + return translatedLines + .map((line, index) => { + const originalLine = originalLines[index]; + if (originalLine) { + const leadingSpaces = originalLine.match(/^(\s*)/)?.[1] || ''; + return leadingSpaces + line.trim(); + } + return line; + }) + .join('\n'); + } + + return translatedComment; +}; +``` + +## 5. 报告生成模块详细设计 + +### 5.1 统计数据收集 + +```typescript +export class ReportCollector { + private stats: ProcessingStats = { + totalFiles: 0, + processedFiles: 0, + translatedComments: 0, + skippedFiles: 0, + errors: [], + startTime: Date.now(), + endTime: 0 + }; + + private fileDetails: Map = new Map(); + + recordFileStart(filePath: string): void { + this.stats.totalFiles++; + this.fileDetails.set(filePath, { + file: filePath, + commentCount: 0, + status: 'processing', + startTime: Date.now() + }); + } + + recordFileComplete(filePath: string, commentCount: number): void { + const detail = this.fileDetails.get(filePath); + if (detail) { + detail.status = 'success'; + detail.commentCount = commentCount; + detail.endTime = Date.now(); + this.stats.processedFiles++; + this.stats.translatedComments += commentCount; + } + } + + recordError(filePath: string, error: Error): void { + const detail = this.fileDetails.get(filePath); + if (detail) { + detail.status = 'error'; + detail.errorMessage = error.message; + detail.endTime = Date.now(); + } + this.stats.errors.push({ file: filePath, error: error.message }); + } +} +``` + +### 5.2 报告格式化 + +```typescript +export const generateReport = ( + collector: ReportCollector, + format: 'json' | 'markdown' | 'console' = 'console' +): string => { + const stats = collector.getStats(); + + switch (format) { + case 'json': + return JSON.stringify(stats, null, 2); + + case 'markdown': + return generateMarkdownReport(stats); + + case 'console': + default: + return generateConsoleReport(stats); + } +}; + +const generateConsoleReport = (stats: ProcessingStats): string => { + const duration = (stats.endTime - stats.startTime) / 1000; + + return ` +📊 翻译处理报告 +================== +总文件数: ${stats.totalFiles} +处理成功: ${stats.processedFiles} +跳过文件: ${stats.skippedFiles} +翻译注释: ${stats.translatedComments} +错误数量: ${stats.errors.length} +处理时间: ${duration.toFixed(2)}秒 + +${stats.errors.length > 0 ? '❌ 错误详情:\n' + stats.errors.map(e => ` ${e.file}: ${e.error}`).join('\n') : '✅ 处理完成,无错误'} +`; +}; +``` + +## 6. 函数式编程工具 + +### 6.1 基础FP工具 + +```typescript +export const pipe = (...fns: Function[]) => (value: T) => + fns.reduce((acc, fn) => fn(acc), value); + +export const compose = (...fns: Function[]) => (value: T) => + fns.reduceRight((acc, fn) => fn(acc), value); + +export const curry = (fn: Function) => (...args: any[]) => + args.length >= fn.length + ? fn(...args) + : (...more: any[]) => curry(fn)(...args, ...more); +``` + +### 6.2 异步处理工具 + +```typescript +export const asyncMap = curry( + async (fn: (item: T) => Promise, items: T[]): Promise => + Promise.all(items.map(fn)) +); + +export const asyncFilter = curry( + async (predicate: (item: T) => Promise, items: T[]): Promise => { + const results = await Promise.all(items.map(predicate)); + return items.filter((_, index) => results[index]); + } +); + +export const asyncReduce = curry( + async ( + fn: (acc: U, item: T) => Promise, + initial: U, + items: T[] + ): Promise => { + let result = initial; + for (const item of items) { + result = await fn(result, item); + } + return result; + } +); +``` + +### 6.3 错误处理 + +```typescript +export type Result = + | { success: true; data: T } + | { success: false; error: E }; + +export const success = (data: T): Result => ({ success: true, data }); +export const failure = (error: E): Result => ({ success: false, error }); + +export const tryCatch = async ( + fn: () => Promise +): Promise> => { + try { + const data = await fn(); + return success(data); + } catch (error) { + return failure(error instanceof Error ? error : new Error(String(error))); + } +}; +``` + +## 7. 配置管理 + +### 7.1 配置文件结构 + +```typescript +interface AppConfig { + translation: { + apiKey: string; + model: string; + maxRetries: number; + timeout: number; + concurrency: number; + }; + processing: { + defaultExtensions: string[]; + createBackup: boolean; + outputFormat: 'json' | 'markdown' | 'console'; + }; + git: { + ignorePatterns: string[]; + includeUntracked: boolean; + }; +} +``` + +### 7.2 配置加载 + +```typescript +export const loadConfig = async (configPath?: string): Promise => { + const defaultConfig: AppConfig = { + translation: { + apiKey: process.env.OPENAI_API_KEY || '', + model: 'gpt-3.5-turbo', + maxRetries: 3, + timeout: 30000, + concurrency: 3 + }, + processing: { + defaultExtensions: ['ts', 'js', 'go', 'md'], + createBackup: true, + outputFormat: 'console' + }, + git: { + ignorePatterns: ['node_modules/**', '.git/**', 'dist/**'], + includeUntracked: false + } + }; + + if (configPath && await fs.access(configPath).then(() => true).catch(() => false)) { + const userConfig = JSON.parse(await fs.readFile(configPath, 'utf-8')); + return deepMerge(defaultConfig, userConfig); + } + + return defaultConfig; +}; +``` + +## 8. 性能优化策略 + +### 8.1 并发控制 + +```typescript +export class Semaphore { + private permits: number; + private waiting: (() => void)[] = []; + + constructor(permits: number) { + this.permits = permits; + } + + async acquire(): Promise { + if (this.permits > 0) { + this.permits--; + return; + } + + return new Promise(resolve => { + this.waiting.push(resolve); + }); + } + + release(): void { + this.permits++; + const next = this.waiting.shift(); + if (next) { + this.permits--; + next(); + } + } +} +``` + +### 8.2 缓存机制 + +```typescript +export class TranslationCache { + private cache = new Map(); + private hashFn = (text: string) => crypto.createHash('md5').update(text).digest('hex'); + + get(comment: string): TranslationResult | undefined { + const key = this.hashFn(comment); + return this.cache.get(key); + } + + set(comment: string, result: TranslationResult): void { + const key = this.hashFn(comment); + this.cache.set(key, result); + } + + async save(filePath: string): Promise { + const data = Object.fromEntries(this.cache); + await fs.writeFile(filePath, JSON.stringify(data, null, 2)); + } + + async load(filePath: string): Promise { + try { + const data = JSON.parse(await fs.readFile(filePath, 'utf-8')); + this.cache = new Map(Object.entries(data)); + } catch { + // 缓存文件不存在或损坏,忽略 + } + } +} +``` diff --git a/common/autoinstallers/rush-commands/src/convert-comments/types/config.ts b/common/autoinstallers/rush-commands/src/convert-comments/types/config.ts new file mode 100644 index 00000000..9624390f --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/types/config.ts @@ -0,0 +1,65 @@ +/** + * 翻译配置 + */ +export interface TranslationConfig { + accessKeyId: string; + secretAccessKey: string; + region: string; + sourceLanguage: string; + targetLanguage: string; + maxRetries: number; + timeout: number; + concurrency: number; +} + +/** + * 文件扫描配置 + */ +export interface FileScanConfig { + root: string; + extensions: string[]; + ignorePatterns: string[]; + includeUntracked: boolean; +} + +/** + * 处理配置 + */ +export interface ProcessingConfig { + defaultExtensions: string[]; + outputFormat: 'json' | 'markdown' | 'console'; +} + +/** + * Git配置 + */ +export interface GitConfig { + ignorePatterns: string[]; + includeUntracked: boolean; +} + +/** + * 应用配置 + */ +export interface AppConfig { + translation: TranslationConfig; + processing: ProcessingConfig; + git: GitConfig; +} + +/** + * 命令行选项 + */ +export interface CliOptions { + root: string; + exts?: string; + accessKeyId?: string; + secretAccessKey?: string; + region?: string; + sourceLanguage?: string; + targetLanguage?: string; + dryRun?: boolean; + verbose?: boolean; + output?: string; + config?: string; +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/types/index.ts b/common/autoinstallers/rush-commands/src/convert-comments/types/index.ts new file mode 100644 index 00000000..df7db492 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/types/index.ts @@ -0,0 +1,192 @@ +/** + * 源文件语言类型 + */ +export type SourceFileLanguage = + | 'typescript' + | 'javascript' + | 'go' + | 'markdown' + | 'text' + | 'json' + | 'yaml' + | 'toml' + | 'ini' + | 'shell' + | 'python' + | 'css' + | 'html' + | 'xml' + | 'php' + | 'ruby' + | 'rust' + | 'java' + | 'c' + | 'cpp' + | 'csharp' + | 'other'; + +/** + * 注释类型 + */ +export type CommentType = 'single-line' | 'multi-line' | 'documentation'; + +/** + * 源文件信息 + */ +export interface SourceFile { + path: string; + content: string; + language: SourceFileLanguage; +} + +/** + * 多行注释上下文信息 + */ +export interface MultiLineContext { + isPartOfMultiLine: boolean; + originalComment: ParsedComment; + lineIndexInComment: number; + totalLinesInComment: number; +} + +/** + * 中文注释信息 + */ +export interface ChineseComment { + content: string; + startLine: number; + endLine: number; + startColumn: number; + endColumn: number; + type: CommentType; + multiLineContext?: MultiLineContext; +} + +/** + * 包含中文注释的文件 + */ +export interface FileWithComments { + file: SourceFile; + chineseComments: ChineseComment[]; +} + +/** + * 翻译结果 + */ +export interface TranslationResult { + original: string; + translated: string; + confidence: number; +} + +/** + * 翻译上下文 + */ +export interface TranslationContext { + language: string; + nearbyCode?: string; + commentType: CommentType; +} + +/** + * 替换操作 + */ +export interface Replacement { + start: number; + end: number; + original: string; + replacement: string; +} + +/** + * 文件替换操作 + */ +export interface ReplacementOperation { + file: string; + replacements: Replacement[]; +} + +/** + * 文件处理详情 + */ +export interface FileProcessingDetail { + file: string; + commentCount: number; + status: 'processing' | 'success' | 'error' | 'skipped'; + errorMessage?: string; + startTime?: number; + endTime?: number; +} + +/** + * 处理统计信息 + */ +export interface ProcessingStats { + totalFiles: number; + processedFiles: number; + translatedComments: number; + skippedFiles: number; + errors: Array<{ file: string; error: string }>; + startTime: number; + endTime: number; +} + +/** + * 处理报告 + */ +export interface ProcessingReport { + stats: ProcessingStats; + details: FileProcessingDetail[]; + duration: number; +} + +/** + * 文件扫描配置 + */ +export interface FileScanConfig { + root: string; + extensions: string[]; + ignorePatterns: string[]; + includeUntracked: boolean; +} + +/** + * 解析的注释 + */ +export interface ParsedComment { + content: string; + startLine: number; + endLine: number; + startColumn: number; + endColumn: number; + type: CommentType; +} + +/** + * 注释模式配置 + */ +export interface CommentPattern { + single: RegExp; + multiStart: RegExp; + multiEnd: RegExp; +} + +/** + * 函数式编程结果类型 + */ +export type Result = + | { success: true; data: T } + | { success: false; error: E }; + +/** + * 翻译错误 + */ +export class TranslationError extends Error { + constructor( + message: string, + public originalComment: string, + ) { + super(message); + this.name = 'TranslationError'; + } +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/utils/chinese.ts b/common/autoinstallers/rush-commands/src/convert-comments/utils/chinese.ts new file mode 100644 index 00000000..af0df45f --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/utils/chinese.ts @@ -0,0 +1,120 @@ +/** + * 中文字符的Unicode范围正则表达式 + */ +const CHINESE_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/; +const CHINESE_EXTRACT_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]+/g; + +/** + * 检测文本是否包含中文字符 + */ +export const containsChinese = (text: string): boolean => { + return CHINESE_REGEX.test(text); +}; + +/** + * 提取文本中的中文部分 + */ +export const extractChineseParts = (text: string): string[] => { + return text.match(CHINESE_EXTRACT_REGEX) || []; +}; + +/** + * 计算文本中中文字符的数量 + */ +export const countChineseCharacters = (text: string): number => { + const matches = text.match(CHINESE_EXTRACT_REGEX); + if (!matches) return 0; + + return matches.reduce((count, match) => count + match.length, 0); +}; + +/** + * 检测文本是否主要由中文组成 + */ +export const isPrimarilyChinese = (text: string, threshold: number = 0.5): boolean => { + const totalLength = text.length; + if (totalLength === 0) return false; + + const chineseLength = countChineseCharacters(text); + return chineseLength / totalLength >= threshold; +}; + +/** + * 清理注释文本,移除注释符号和多余空格 + */ +export const cleanCommentText = ( + text: string, + commentType: 'single-line' | 'multi-line', + language?: string +): string => { + let cleaned = text; + + if (commentType === 'single-line') { + // 根据语言类型移除不同的单行注释符号 + switch (language) { + case 'yaml': + case 'toml': + case 'shell': + case 'python': + case 'ruby': + cleaned = cleaned.replace(/^#\s*/, ''); + break; + case 'ini': + cleaned = cleaned.replace(/^[;#]\s*/, ''); + break; + case 'php': + cleaned = cleaned.replace(/^(?:\/\/|#)\s*/, ''); + break; + default: + // JavaScript/TypeScript/Go/Java/C/C++/C# style + cleaned = cleaned.replace(/^\/\/\s*/, ''); + } + } else if (commentType === 'multi-line') { + // 根据语言类型移除不同的多行注释符号 + switch (language) { + case 'html': + case 'xml': + case 'markdown': + cleaned = cleaned.replace(/^$/, ''); + break; + case 'python': + cleaned = cleaned.replace(/^"""\s*/, '').replace(/\s*"""$/, ''); + break; + case 'ruby': + cleaned = cleaned.replace(/^=begin\s*/, '').replace(/\s*=end$/, ''); + break; + default: + // JavaScript/TypeScript/Go/Java/C/C++/C#/CSS style + cleaned = cleaned.replace(/^\/\*\s*/, '').replace(/\s*\*\/$/, ''); + // 移除每行开头的 * 符号 + cleaned = cleaned.replace(/^\s*\*\s?/gm, ''); + } + } + + // 移除多余的空格和换行 + cleaned = cleaned.trim(); + + return cleaned; +}; + +/** + * 验证翻译结果是否有效 + */ +export const isValidTranslation = (original: string, translated: string): boolean => { + // 基本验证 + if (!translated || translated.trim().length === 0) { + return false; + } + + // 检查是否还包含中文(可能翻译失败) + if (containsChinese(translated)) { + return false; + } + + // 检查长度是否合理(翻译后的文本不应该比原文长太多) + if (translated.length > original.length * 3) { + return false; + } + + return true; +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/utils/fp.ts b/common/autoinstallers/rush-commands/src/convert-comments/utils/fp.ts new file mode 100644 index 00000000..bf9cbf93 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/utils/fp.ts @@ -0,0 +1,173 @@ +import { Result } from '../types/index'; + +/** + * 函数组合 - 从左到右执行 + */ +export const pipe = + (...fns: Function[]) => + (value: T) => + fns.reduce((acc, fn) => fn(acc), value); + +/** + * 函数组合 - 从右到左执行 + */ +export const compose = + (...fns: Function[]) => + (value: T) => + fns.reduceRight((acc, fn) => fn(acc), value); + +/** + * 柯里化函数 + */ +export const curry = + (fn: Function) => + (...args: any[]) => + args.length >= fn.length + ? fn(...args) + : (...more: any[]) => curry(fn)(...args, ...more); + +/** + * 异步映射 + */ +export const asyncMap = curry( + async (fn: (item: T) => Promise, items: T[]): Promise => + Promise.all(items.map(fn)), +); + +/** + * 异步过滤 + */ +export const asyncFilter = curry( + async ( + predicate: (item: T) => Promise, + items: T[], + ): Promise => { + const results = await Promise.all(items.map(predicate)); + return items.filter((_, index) => results[index]); + }, +); + +/** + * 异步归约 + */ +export const asyncReduce = curry( + async ( + fn: (acc: U, item: T) => Promise, + initial: U, + items: T[], + ): Promise => { + let result = initial; + for (const item of items) { + result = await fn(result, item); + } + return result; + }, +); + +/** + * 创建成功结果 + */ +export const success = (data: T): Result => ({ success: true, data }); + +/** + * 创建失败结果 + */ +export const failure = (error: E): Result => ({ + success: false, + error, +}); + +/** + * 安全的异步操作包装 + */ +export const tryCatch = async (fn: () => Promise): Promise> => { + try { + const data = await fn(); + return success(data); + } catch (error) { + return failure(error instanceof Error ? error : new Error(String(error))); + } +}; + +/** + * 同步版本的安全操作包装 + */ +export const tryCatchSync = (fn: () => T): Result => { + try { + const data = fn(); + return success(data); + } catch (error) { + return failure(error instanceof Error ? error : new Error(String(error))); + } +}; + +/** + * 数组分块 + */ +export const chunk = (array: T[], size: number): T[][] => { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +}; + +/** + * 延迟执行 + */ +export const delay = (ms: number): Promise => + new Promise(resolve => setTimeout(resolve, ms)); + +/** + * 重试机制 + */ +export const retry = async ( + fn: () => Promise, + maxAttempts: number = 3, + delayMs: number = 1000, +): Promise => { + let lastError: Error; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + if (attempt < maxAttempts) { + await delay(delayMs * attempt); // 指数退避 + } + } + } + + throw lastError!; +}; + +/** + * 深度合并对象 + */ +export const deepMerge = >( + target: T, + source: Partial, +): T => { + const result = { ...target } as T; + + for (const key in source) { + if (source[key] !== undefined) { + if ( + typeof source[key] === 'object' && + source[key] !== null && + !Array.isArray(source[key]) && + typeof target[key] === 'object' && + target[key] !== null && + !Array.isArray(target[key]) + ) { + (result as any)[key] = deepMerge(target[key], source[key]!); + } else { + (result as any)[key] = source[key]!; + } + } + } + + return result; +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/utils/git.ts b/common/autoinstallers/rush-commands/src/convert-comments/utils/git.ts new file mode 100644 index 00000000..0d4eeb00 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/utils/git.ts @@ -0,0 +1,94 @@ +import { simpleGit } from 'simple-git'; +import * as path from 'path'; +import { tryCatch } from './fp'; +import { Result } from '../types/index'; + +/** + * 获取Git仓库中的所有已跟踪文件 + */ +export const getGitTrackedFiles = async ( + root: string, +): Promise> => { + return tryCatch(async () => { + const git = simpleGit(root); + const files = await git.raw(['ls-files']); + + return files + .split('\n') + .filter(Boolean) + .map(file => path.resolve(root, file)); + }); +}; + +/** + * 获取Git仓库中的所有文件(包括未跟踪的) + */ +export const getAllGitFiles = async ( + root: string, +): Promise> => { + return tryCatch(async () => { + const git = simpleGit(root); + + // 获取已跟踪的文件 + const trackedFiles = await git.raw(['ls-files']); + const trackedFilesArray = trackedFiles + .split('\n') + .filter(Boolean) + .map(file => path.resolve(root, file)); + + // 获取未跟踪的文件 + const status = await git.status(); + const untrackedFiles = status.not_added.map(file => + path.resolve(root, file), + ); + + // 合并并去重 + const allFiles = [...new Set([...trackedFilesArray, ...untrackedFiles])]; + + return allFiles; + }); +}; + +/** + * 检查目录是否是Git仓库 + */ +export const isGitRepository = async ( + root: string, +): Promise> => { + return tryCatch(async () => { + const git = simpleGit(root); + await git.status(); + return true; + }); +}; + +/** + * 获取Git仓库的根目录 + */ +export const getGitRoot = async (cwd: string): Promise> => { + return tryCatch(async () => { + const git = simpleGit(cwd); + const root = await git.revparse(['--show-toplevel']); + return root.trim(); + }); +}; + +/** + * 检查文件是否被Git忽略 + */ +export const isIgnoredByGit = async ( + root: string, + filePath: string, +): Promise> => { + return tryCatch(async () => { + const git = simpleGit(root); + const relativePath = path.relative(root, filePath); + + try { + await git.raw(['check-ignore', relativePath]); + return true; // 文件被忽略 + } catch { + return false; // 文件未被忽略 + } + }); +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/utils/language.ts b/common/autoinstallers/rush-commands/src/convert-comments/utils/language.ts new file mode 100644 index 00000000..9236cb64 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/utils/language.ts @@ -0,0 +1,227 @@ +import { SourceFileLanguage, CommentPattern } from '../types/index'; + +/** + * 根据文件扩展名识别编程语言 + */ +export const detectLanguage = (filePath: string): SourceFileLanguage => { + const ext = filePath.toLowerCase().split('.').pop(); + + const languageMap: Record = { + 'ts': 'typescript', + 'tsx': 'typescript', + 'js': 'javascript', + 'jsx': 'javascript', + 'go': 'go', + 'md': 'markdown', + 'txt': 'text', + 'json': 'json', + 'yaml': 'yaml', + 'yml': 'yaml', + 'toml': 'toml', + 'ini': 'ini', + 'conf': 'ini', + 'config': 'ini', + 'sh': 'shell', + 'bash': 'shell', + 'zsh': 'shell', + 'fish': 'shell', + 'py': 'python', + 'css': 'css', + 'scss': 'css', + 'sass': 'css', + 'less': 'css', + 'html': 'html', + 'htm': 'html', + 'xml': 'xml', + 'php': 'php', + 'rb': 'ruby', + 'rs': 'rust', + 'java': 'java', + 'c': 'c', + 'h': 'c', + 'cpp': 'cpp', + 'cxx': 'cpp', + 'cc': 'cpp', + 'hpp': 'cpp', + 'cs': 'csharp' + }; + + return languageMap[ext || ''] || 'other'; +}; + +/** + * 根据文件扩展名过滤文件 + */ +export const filterFilesByExtensions = ( + files: string[], + extensions: string[] +): string[] => { + if (extensions.length === 0) { + // 默认支持的文本文件扩展名 + const defaultExtensions = [ + '.ts', '.tsx', '.js', '.jsx', '.go', '.md', '.txt', '.json', + '.yaml', '.yml', '.toml', '.ini', '.conf', '.config', + '.sh', '.bash', '.zsh', '.fish', '.py', '.css', '.scss', '.sass', '.less', + '.html', '.htm', '.xml', '.php', '.rb', '.rs', '.java', '.c', '.h', + '.cpp', '.cxx', '.cc', '.hpp', '.cs' + ]; + return files.filter(file => + defaultExtensions.some(ext => file.toLowerCase().endsWith(ext)) + ); + } + + return files.filter(file => { + const lowerFile = file.toLowerCase(); + return extensions.some(ext => { + const lowerExt = ext.toLowerCase(); + // 如果扩展名已经有点号,直接使用;否则添加点号 + const extWithDot = lowerExt.startsWith('.') ? lowerExt : `.${lowerExt}`; + return lowerFile.endsWith(extWithDot); + }); + }); +}; + +/** + * 获取不同编程语言的注释模式 + */ +export const getCommentPatterns = (language: SourceFileLanguage): CommentPattern | null => { + const commentPatterns: Record = { + typescript: { + single: /(?:^|[^:])\s*\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + javascript: { + single: /(?:^|[^:])\s*\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + go: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + markdown: { + single: //g, + multiStart: //g + }, + text: { + single: /^(.*)$/gm, // 文本文件每行都可能是注释 + multiStart: /^/g, + multiEnd: /$/g + }, + json: { + single: /\/\/(.*)$/gm, // JSON通常不支持注释,但一些工具支持 + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + yaml: { + single: /#(.*)$/gm, + multiStart: /^$/g, // YAML不支持多行注释 + multiEnd: /^$/g + }, + toml: { + single: /#(.*)$/gm, + multiStart: /^$/g, // TOML不支持多行注释 + multiEnd: /^$/g + }, + ini: { + single: /[;#](.*)$/gm, // INI文件支持 ; 和 # 作为注释 + multiStart: /^$/g, // INI不支持多行注释 + multiEnd: /^$/g + }, + shell: { + single: /#(.*)$/gm, + multiStart: /^$/g, // Shell脚本不支持多行注释 + multiEnd: /^$/g + }, + python: { + single: /#(.*)$/gm, + multiStart: /"""[\s\S]*?$/gm, // Python docstring + multiEnd: /[\s\S]*?"""/gm + }, + css: { + single: /^$/g, // CSS不支持单行注释 + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + html: { + single: /^$/g, // HTML不支持单行注释 + multiStart: //g + }, + xml: { + single: /^$/g, // XML不支持单行注释 + multiStart: //g + }, + php: { + single: /(?:\/\/|#)(.*)$/gm, // PHP支持 // 和 # 作为单行注释 + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + ruby: { + single: /#(.*)$/gm, + multiStart: /=begin/g, + multiEnd: /=end/g + }, + rust: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + java: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + c: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + cpp: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + csharp: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + }, + other: { + single: /\/\/(.*)$/gm, + multiStart: /\/\*/g, + multiEnd: /\*\//g + } + }; + + return commentPatterns[language] || null; +}; + +/** + * 检查文件是否支持处理 + */ +export const isSupportedFile = (filePath: string): boolean => { + const language = detectLanguage(filePath); + return language !== 'other'; +}; + +/** + * 获取文件的MIME类型(用于判断是否为文本文件) + */ +export const isTextFile = (filePath: string): boolean => { + const textExtensions = [ + '.ts', '.tsx', '.js', '.jsx', '.go', '.md', '.txt', '.json', + '.css', '.scss', '.sass', '.less', '.html', '.htm', '.xml', + '.yaml', '.yml', '.toml', '.ini', '.conf', '.config', + '.sh', '.bash', '.zsh', '.fish', '.py', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', + '.php', '.rb', '.rs', '.kt', '.swift', '.dart', '.scala' + ]; + + return textExtensions.some(ext => + filePath.toLowerCase().endsWith(ext) + ); +}; diff --git a/common/autoinstallers/rush-commands/src/convert-comments/utils/semaphore.ts b/common/autoinstallers/rush-commands/src/convert-comments/utils/semaphore.ts new file mode 100644 index 00000000..369c4061 --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/utils/semaphore.ts @@ -0,0 +1,51 @@ +/** + * 信号量并发控制类 + */ +export class Semaphore { + private permits: number; + private waiting: (() => void)[] = []; + + constructor(permits: number) { + this.permits = permits; + } + + /** + * 获取许可 + */ + async acquire(): Promise { + if (this.permits > 0) { + this.permits--; + return; + } + + return new Promise(resolve => { + this.waiting.push(resolve); + }); + } + + /** + * 释放许可 + */ + release(): void { + this.permits++; + const next = this.waiting.shift(); + if (next) { + this.permits--; + next(); + } + } + + /** + * 获取当前可用许可数 + */ + available(): number { + return this.permits; + } + + /** + * 获取等待队列长度 + */ + waitingCount(): number { + return this.waiting.length; + } +} diff --git a/common/autoinstallers/rush-commands/src/convert-comments/volc/translate.ts b/common/autoinstallers/rush-commands/src/convert-comments/volc/translate.ts new file mode 100644 index 00000000..e524cddb --- /dev/null +++ b/common/autoinstallers/rush-commands/src/convert-comments/volc/translate.ts @@ -0,0 +1,387 @@ +/* +Copyright (year) Beijing Volcano Engine Technology Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import crypto from 'crypto'; +import util from 'util'; +import { URLSearchParams } from 'url'; + +const debuglog = util.debuglog('signer'); + +/** + * 签名参数接口 + */ +export interface SignParams { + headers?: Record; + query?: Record; + region?: string; + serviceName?: string; + method?: string; + pathName?: string; + accessKeyId?: string; + secretAccessKey?: string; + needSignHeaderKeys?: string[]; + bodySha?: string; +} + +/** + * 查询参数类型 + */ +export type QueryParams = Record; + +/** + * 请求头类型 + */ +export type Headers = Record; + +/** + * 翻译请求参数接口 + */ +export interface TranslateRequest { + /** 源语言代码 */ + SourceLanguage: string; + /** 目标语言代码 */ + TargetLanguage: string; + /** 要翻译的文本列表 */ + TextList: string[]; +} + +/** + * 翻译结果中的额外信息 + */ +export interface TranslationExtra { + /** 输入字符数 */ + input_characters: string; + /** 源语言 */ + source_language: string; +} + +/** + * 单个翻译结果 + */ +export interface TranslationItem { + /** 翻译结果 */ + Translation: string; + /** 检测到的源语言 */ + DetectedSourceLanguage: string; + /** 额外信息 */ + Extra: TranslationExtra; +} + +/** + * 响应元数据 + */ +export interface ResponseMetadata { + /** 请求ID */ + RequestId: string; + /** 操作名称 */ + Action: string; + /** API版本 */ + Version: string; + /** 服务名称 */ + Service: string; + /** 区域 */ + Region: string; +} + +/** + * 火山引擎翻译API响应接口 + */ +export interface VolcTranslateResponse { + /** 翻译结果列表 */ + TranslationList: TranslationItem[]; + /** 响应元数据 */ + ResponseMetadata: ResponseMetadata; + /** 响应元数据(备用字段) */ + ResponseMetaData?: ResponseMetadata; +} + +/** + * 翻译配置参数 + */ +export interface TranslateConfig { + /** 访问密钥ID */ + accessKeyId: string; + /** 秘密访问密钥 */ + secretAccessKey: string; + /** 服务区域 */ + region?: string; + /** 源语言代码 */ + sourceLanguage?: string; + /** 目标语言代码 */ + targetLanguage?: string; +} + +/** + * 不参与加签过程的 header key + */ +const HEADER_KEYS_TO_IGNORE = new Set([ + 'authorization', + 'content-type', + 'content-length', + 'user-agent', + 'presigned-expires', + 'expect', +]); + +/** + * 火山引擎翻译接口 + * @param textArray 要翻译的文本数组 + * @param config 翻译配置参数,如果不提供则使用默认配置 + * @returns 翻译响应结果 + */ +export async function translate( + textArray: string[], + config?: Partial, +): Promise { + const translateConfig: TranslateConfig = { + accessKeyId: config?.accessKeyId!, + secretAccessKey: config?.secretAccessKey!, + region: config?.region!, + sourceLanguage: 'zh', + targetLanguage: 'en', + ...config, + }; + + const requestBody: TranslateRequest = { + SourceLanguage: translateConfig.sourceLanguage!, + TargetLanguage: translateConfig.targetLanguage!, + TextList: textArray, + }; + + const requestBodyString = JSON.stringify(requestBody); + const signParams: SignParams = { + headers: { + // x-date header 是必传的 + 'X-Date': getDateTimeNow(), + 'content-type': 'application/json', + }, + method: 'POST', + query: { + Version: '2020-06-01', + Action: 'TranslateText', + }, + accessKeyId: translateConfig.accessKeyId, + secretAccessKey: translateConfig.secretAccessKey, + serviceName: 'translate', + region: translateConfig.region!, + bodySha: getBodySha(requestBodyString), + }; + + // 正规化 query object, 防止串化后出现 query 值为 undefined 情况 + if (signParams.query) { + for (const [key, val] of Object.entries(signParams.query)) { + if (val === undefined || val === null) { + signParams.query[key] = ''; + } + } + } + + const authorization = sign(signParams); + const queryString = new URLSearchParams( + signParams.query as Record, + ).toString(); + + const res = await fetch( + `https://translate.volcengineapi.com/?${queryString}`, + { + headers: { + ...signParams.headers, + Authorization: authorization, + }, + body: requestBodyString, + method: signParams.method, + }, + ); + + if (!res.ok) { + throw new Error( + `Translation request failed: ${res.status} ${res.statusText}`, + ); + } + + const result: VolcTranslateResponse = await res.json(); + return result; +} + +/** + * 生成签名 + */ +function sign(params: SignParams): string { + const { + headers = {}, + query = {}, + region = '', + serviceName = '', + method = '', + pathName = '/', + accessKeyId = '', + secretAccessKey = '', + needSignHeaderKeys = [], + bodySha, + } = params; + const datetime = headers['X-Date']; + const date = datetime.substring(0, 8); // YYYYMMDD + // 创建正规化请求 + const [signedHeaders, canonicalHeaders] = getSignHeaders( + headers, + needSignHeaderKeys, + ); + const canonicalRequest = [ + method.toUpperCase(), + pathName, + queryParamsToString(query) || '', + `${canonicalHeaders}\n`, + signedHeaders, + bodySha || hash(''), + ].join('\n'); + const credentialScope = [date, region, serviceName, 'request'].join('/'); + // 创建签名字符串 + const stringToSign = [ + 'HMAC-SHA256', + datetime, + credentialScope, + hash(canonicalRequest), + ].join('\n'); + // 计算签名 + const kDate = hmac(secretAccessKey, date); + const kRegion = hmac(kDate, region); + const kService = hmac(kRegion, serviceName); + const kSigning = hmac(kService, 'request'); + const signature = hmac(kSigning, stringToSign).toString('hex'); + debuglog( + '--------CanonicalString:\n%s\n--------SignString:\n%s', + canonicalRequest, + stringToSign, + ); + + return [ + 'HMAC-SHA256', + `Credential=${accessKeyId}/${credentialScope},`, + `SignedHeaders=${signedHeaders},`, + `Signature=${signature}`, + ].join(' '); +} + +/** + * HMAC-SHA256 加密 + */ +function hmac(secret: string | Buffer, s: string): Buffer { + return crypto.createHmac('sha256', secret).update(s, 'utf8').digest(); +} + +/** + * SHA256 哈希 + */ +function hash(s: string): string { + return crypto.createHash('sha256').update(s, 'utf8').digest('hex'); +} + +/** + * 查询参数转字符串 + */ +function queryParamsToString(params: QueryParams): string { + return Object.keys(params) + .sort() + .map(key => { + const val = params[key]; + if (typeof val === 'undefined' || val === null) { + return undefined; + } + const escapedKey = uriEscape(key); + if (!escapedKey) { + return undefined; + } + if (Array.isArray(val)) { + return `${escapedKey}=${val.map(uriEscape).sort().join(`&${escapedKey}=`)}`; + } + return `${escapedKey}=${uriEscape(val)}`; + }) + .filter(v => v) + .join('&'); +} + +/** + * 获取签名头 + */ +function getSignHeaders( + originHeaders: Headers, + needSignHeaders: string[], +): [string, string] { + function trimHeaderValue(header: string): string { + return header.toString?.().trim().replace(/\s+/g, ' ') ?? ''; + } + + let h = Object.keys(originHeaders); + // 根据 needSignHeaders 过滤 + if (Array.isArray(needSignHeaders)) { + const needSignSet = new Set( + [...needSignHeaders, 'x-date', 'host'].map(k => k.toLowerCase()), + ); + h = h.filter(k => needSignSet.has(k.toLowerCase())); + } + // 根据 ignore headers 过滤 + h = h.filter(k => !HEADER_KEYS_TO_IGNORE.has(k.toLowerCase())); + const signedHeaderKeys = h + .slice() + .map(k => k.toLowerCase()) + .sort() + .join(';'); + const canonicalHeaders = h + .sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)) + .map(k => `${k.toLowerCase()}:${trimHeaderValue(originHeaders[k])}`) + .join('\n'); + return [signedHeaderKeys, canonicalHeaders]; +} + +/** + * URI 转义 + */ +function uriEscape(str: string): string { + try { + return encodeURIComponent(str) + .replace(/[^A-Za-z0-9_.~\-%]+/g, match => + match + .split('') + .map(c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`) + .join(''), + ) + .replace(/[*]/g, ch => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`); + } catch (e) { + return ''; + } +} + +/** + * 获取当前时间格式化字符串 + */ +export function getDateTimeNow(): string { + const now = new Date(); + return now.toISOString().replace(/[:-]|\.\d{3}/g, ''); +} + +/** + * 获取 body 的 SHA256 值 + */ +export function getBodySha(body: string | URLSearchParams | Buffer): string { + const hashInstance = crypto.createHash('sha256'); + if (typeof body === 'string') { + hashInstance.update(body); + } else if (body instanceof URLSearchParams) { + hashInstance.update(body.toString()); + } else if (Buffer.isBuffer(body)) { + hashInstance.update(body); + } + return hashInstance.digest('hex'); +} diff --git a/common/autoinstallers/rush-commands/tsconfig.json b/common/autoinstallers/rush-commands/tsconfig.json new file mode 100644 index 00000000..86ba2d06 --- /dev/null +++ b/common/autoinstallers/rush-commands/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "declaration": true, + "composite": true, + "incremental": true, + "strictNullChecks": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "esModuleInterop": true, + "experimentalDecorators": true, + "outDir": "./dist", + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "module": "es2020", + "noFallthroughCasesInSwitch": true, + // This general feedback will make the code verbose, tentatively follow the original bot's settings, close + "noImplicitReturns": false, + "removeComments": false, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "disableReferencedProjectLoad": true, + // "disableSolutionSearching": true, + // "disableSourceOfProjectReferenceRedirect": true, + "target": "es2018" + }, + "watchOptions": { + "fallbackPolling": "dynamicpriority", + "synchronousWatchDirectory": false, + "watchDirectory": "fixedChunkSizePolling", + "watchFile": "useFsEventsOnParentDirectory" + } +} diff --git a/common/autoinstallers/rush-commitlint/.cz-config.js b/common/autoinstallers/rush-commitlint/.cz-config.js index 5d0679d8..9002c1f4 100644 --- a/common/autoinstallers/rush-commitlint/.cz-config.js +++ b/common/autoinstallers/rush-commitlint/.cz-config.js @@ -3,6 +3,10 @@ const spawn = require('cross-spawn') const defaultConfig = require('cz-customizable'); const { getChangedPackages } = require('./utils') + +/** + * 针对不同类型的 commit prefix message + */ const typesConfig = [ { value: 'feat', name: 'A new feature' }, { value: 'fix', name: 'A bug fix' },