chore: remove all cn comments (#277)
This commit is contained in:
parent
875e97a40d
commit
f93f26fc48
|
|
@ -52,3 +52,5 @@ common/temp
|
||||||
|
|
||||||
|
|
||||||
backend/conf/model/*.yaml
|
backend/conf/model/*.yaml
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 <directory>`:需要处理的根目录
|
||||||
|
|
||||||
|
### 可选参数
|
||||||
|
- `--exts, -e <extensions>`:文件扩展名,如 "ts,js,go,md"
|
||||||
|
- `--openai-key <key>`:OpenAI API密钥
|
||||||
|
- `--model <model>`:OpenAI模型名称(默认:gpt-3.5-turbo)
|
||||||
|
- `--dry-run`:仅分析不实际修改文件
|
||||||
|
- `--backup`:创建文件备份(默认启用)
|
||||||
|
- `--verbose, -v`:详细输出模式
|
||||||
|
- `--output <file>`:报告输出文件路径
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```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! 🎉**
|
||||||
|
|
@ -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 <directory>', '需要处理的根目录')
|
||||||
|
.option('-e, --exts <extensions>', '文件扩展名,用逗号分隔 (例: ts,js,go)', '')
|
||||||
|
.option('--access-key-id <key>', '火山引擎 Access Key ID')
|
||||||
|
.option('--secret-access-key <key>', '火山引擎 Secret Access Key')
|
||||||
|
.option('--region <region>', '火山引擎服务区域', 'cn-beijing')
|
||||||
|
.option('--source-language <lang>', '源语言代码', 'zh')
|
||||||
|
.option('--target-language <lang>', '目标语言代码', 'en')
|
||||||
|
.option('--dry-run', '仅分析不实际修改文件')
|
||||||
|
.option('-v, --verbose', '详细输出模式')
|
||||||
|
.option('-o, --output <file>', '报告输出文件路径')
|
||||||
|
.option('-c, --config <file>', '配置文件路径')
|
||||||
|
.option('--concurrency <number>', '并发翻译数量', '3')
|
||||||
|
.option('--max-retries <number>', '最大重试次数', '3')
|
||||||
|
.option('--timeout <number>', '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 <YOUR_KEY_ID> --secret-access-key <YOUR_SECRET>
|
||||||
|
|
||||||
|
# 指定文件类型和翻译语言
|
||||||
|
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');
|
||||||
|
};
|
||||||
|
|
@ -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<Partial<AppConfig>> => {
|
||||||
|
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<AppConfig> => {
|
||||||
|
const config: Partial<AppConfig> = {};
|
||||||
|
|
||||||
|
// 翻译配置
|
||||||
|
if (options.accessKeyId || options.secretAccessKey || options.region || options.sourceLanguage || options.targetLanguage) {
|
||||||
|
config.translation = {} as Partial<TranslationConfig>;
|
||||||
|
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<ProcessingConfig>;
|
||||||
|
// 根据输出文件扩展名推断格式
|
||||||
|
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>[]): AppConfig => {
|
||||||
|
return configs.reduce(
|
||||||
|
(merged, config) => deepMerge(merged, config),
|
||||||
|
{ ...DEFAULT_CONFIG }
|
||||||
|
) as AppConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载完整配置
|
||||||
|
*/
|
||||||
|
export const loadConfig = async (options: CliOptions): Promise<AppConfig> => {
|
||||||
|
const configs: Partial<AppConfig>[] = [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 ? '是' : '否'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<string[]>`
|
||||||
|
- `filterFilesByExtensions(files: string[], extensions: string[]): string[]`
|
||||||
|
- `readSourceFiles(filePaths: string[]): Promise<SourceFile[]>`
|
||||||
|
|
||||||
|
### 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<TranslationResult>`
|
||||||
|
- `batchTranslate(comments: string[]): Promise<TranslationResult[]>`
|
||||||
|
- `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<void>`
|
||||||
|
- `createBackup(filePath: string): Promise<string>`
|
||||||
|
- `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 <directory> --exts <extensions> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
|
||||||
|
- `--root, -r <directory>`: 需要处理的根目录(必填)
|
||||||
|
- `--exts, -e <extensions>`: 文件扩展名数组,如 "ts,js,go,md"(可选,默认处理所有文本文件)
|
||||||
|
- `--openai-key <key>`: OpenAI API密钥(可选,也可通过环境变量提供)
|
||||||
|
- `--model <model>`: OpenAI模型名称(可选,默认gpt-3.5-turbo)
|
||||||
|
- `--dry-run`: 仅分析不实际修改文件(可选)
|
||||||
|
- `--backup`: 创建文件备份(可选,默认启用)
|
||||||
|
- `--verbose, -v`: 详细输出模式(可选)
|
||||||
|
- `--output <file>`: 报告输出文件(可选)
|
||||||
|
|
||||||
|
## 技术实现细节
|
||||||
|
|
||||||
|
### 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. 并发控制和进度显示
|
||||||
|
|
@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -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<CommentType, number>;
|
||||||
|
} => {
|
||||||
|
let totalComments = 0;
|
||||||
|
let chineseComments = 0;
|
||||||
|
let filesWithComments = 0;
|
||||||
|
const commentsByType: Record<CommentType, number> = {
|
||||||
|
'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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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<number, string>();
|
||||||
|
|
||||||
|
// 将翻译结果映射到对应的行
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
@ -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<Result<SourceFile>> => {
|
||||||
|
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<SourceFile[]> => {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
filePaths.map(path => readSourceFile(path))
|
||||||
|
);
|
||||||
|
|
||||||
|
return results
|
||||||
|
.filter((result): result is PromiseFulfilledResult<Result<SourceFile>> =>
|
||||||
|
result.status === 'fulfilled' && result.value.success
|
||||||
|
)
|
||||||
|
.map(result => (result.value as { success: true; data: SourceFile }).data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Git仓库中的源码文件
|
||||||
|
*/
|
||||||
|
export const getSourceFiles = async (config: FileScanConfig): Promise<Result<string[]>> => {
|
||||||
|
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<Result<SourceFile[]>> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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<boolean> =>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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<string, FileProcessingDetail> = 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<void> => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<string, TranslationResult>();
|
||||||
|
|
||||||
|
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<string[]> {
|
||||||
|
const volcConfig = this.toVolcConfig();
|
||||||
|
const response = await volcTranslate(texts, volcConfig);
|
||||||
|
|
||||||
|
return response.TranslationList.map(item => item.Translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译单个注释
|
||||||
|
*/
|
||||||
|
async translateComment(
|
||||||
|
comment: string,
|
||||||
|
context?: TranslationContext,
|
||||||
|
): Promise<TranslationResult> {
|
||||||
|
// 检查缓存
|
||||||
|
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<TranslationResult[]> {
|
||||||
|
// 提取未缓存的注释
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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, // 需要实际统计命中率
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 编写代码
|
||||||
|
|
@ -0,0 +1,636 @@
|
||||||
|
# 中文备注转换为英文 - 技术规格说明
|
||||||
|
|
||||||
|
## 1. 文件扫描模块详细设计
|
||||||
|
|
||||||
|
### 1.1 Git文件获取
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import simpleGit from 'simple-git';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Git仓库中的所有已跟踪文件
|
||||||
|
*/
|
||||||
|
export const getGitTrackedFiles = async (root: string): Promise<string[]> => {
|
||||||
|
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<string, SourceFileLanguage> = {
|
||||||
|
'.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<string, CommentPattern> = {
|
||||||
|
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,
|
||||||
|
multiEnd: /-->/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<TranslationResult> {
|
||||||
|
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<TranslationResult[]> => {
|
||||||
|
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<string> => {
|
||||||
|
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<void> => {
|
||||||
|
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<string, FileProcessingDetail> = 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 = <T>(...fns: Function[]) => (value: T) =>
|
||||||
|
fns.reduce((acc, fn) => fn(acc), value);
|
||||||
|
|
||||||
|
export const compose = <T>(...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 <T, U>(fn: (item: T) => Promise<U>, items: T[]): Promise<U[]> =>
|
||||||
|
Promise.all(items.map(fn))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const asyncFilter = curry(
|
||||||
|
async <T>(predicate: (item: T) => Promise<boolean>, items: T[]): Promise<T[]> => {
|
||||||
|
const results = await Promise.all(items.map(predicate));
|
||||||
|
return items.filter((_, index) => results[index]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const asyncReduce = curry(
|
||||||
|
async <T, U>(
|
||||||
|
fn: (acc: U, item: T) => Promise<U>,
|
||||||
|
initial: U,
|
||||||
|
items: T[]
|
||||||
|
): Promise<U> => {
|
||||||
|
let result = initial;
|
||||||
|
for (const item of items) {
|
||||||
|
result = await fn(result, item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type Result<T, E = Error> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: E };
|
||||||
|
|
||||||
|
export const success = <T>(data: T): Result<T> => ({ success: true, data });
|
||||||
|
export const failure = <E>(error: E): Result<never, E> => ({ success: false, error });
|
||||||
|
|
||||||
|
export const tryCatch = async <T>(
|
||||||
|
fn: () => Promise<T>
|
||||||
|
): Promise<Result<T>> => {
|
||||||
|
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<AppConfig> => {
|
||||||
|
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<void> {
|
||||||
|
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<string, TranslationResult>();
|
||||||
|
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<void> {
|
||||||
|
const data = Object.fromEntries(this.cache);
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(filePath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
||||||
|
this.cache = new Map(Object.entries(data));
|
||||||
|
} catch {
|
||||||
|
// 缓存文件不存在或损坏,忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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<T, E = Error> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: E };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译错误
|
||||||
|
*/
|
||||||
|
export class TranslationError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public originalComment: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'TranslationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(/^<!--\s*/, '').replace(/\s*-->$/, '');
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { Result } from '../types/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数组合 - 从左到右执行
|
||||||
|
*/
|
||||||
|
export const pipe =
|
||||||
|
<T>(...fns: Function[]) =>
|
||||||
|
(value: T) =>
|
||||||
|
fns.reduce((acc, fn) => fn(acc), value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数组合 - 从右到左执行
|
||||||
|
*/
|
||||||
|
export const compose =
|
||||||
|
<T>(...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 <T, U>(fn: (item: T) => Promise<U>, items: T[]): Promise<U[]> =>
|
||||||
|
Promise.all(items.map(fn)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步过滤
|
||||||
|
*/
|
||||||
|
export const asyncFilter = curry(
|
||||||
|
async <T>(
|
||||||
|
predicate: (item: T) => Promise<boolean>,
|
||||||
|
items: T[],
|
||||||
|
): Promise<T[]> => {
|
||||||
|
const results = await Promise.all(items.map(predicate));
|
||||||
|
return items.filter((_, index) => results[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步归约
|
||||||
|
*/
|
||||||
|
export const asyncReduce = curry(
|
||||||
|
async <T, U>(
|
||||||
|
fn: (acc: U, item: T) => Promise<U>,
|
||||||
|
initial: U,
|
||||||
|
items: T[],
|
||||||
|
): Promise<U> => {
|
||||||
|
let result = initial;
|
||||||
|
for (const item of items) {
|
||||||
|
result = await fn(result, item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建成功结果
|
||||||
|
*/
|
||||||
|
export const success = <T>(data: T): Result<T> => ({ success: true, data });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建失败结果
|
||||||
|
*/
|
||||||
|
export const failure = <E>(error: E): Result<never, E> => ({
|
||||||
|
success: false,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全的异步操作包装
|
||||||
|
*/
|
||||||
|
export const tryCatch = async <T>(fn: () => Promise<T>): Promise<Result<T>> => {
|
||||||
|
try {
|
||||||
|
const data = await fn();
|
||||||
|
return success(data);
|
||||||
|
} catch (error) {
|
||||||
|
return failure(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步版本的安全操作包装
|
||||||
|
*/
|
||||||
|
export const tryCatchSync = <T>(fn: () => T): Result<T> => {
|
||||||
|
try {
|
||||||
|
const data = fn();
|
||||||
|
return success(data);
|
||||||
|
} catch (error) {
|
||||||
|
return failure(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组分块
|
||||||
|
*/
|
||||||
|
export const chunk = <T>(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<void> =>
|
||||||
|
new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试机制
|
||||||
|
*/
|
||||||
|
export const retry = async <T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxAttempts: number = 3,
|
||||||
|
delayMs: number = 1000,
|
||||||
|
): Promise<T> => {
|
||||||
|
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 = <T extends Record<string, any>>(
|
||||||
|
target: T,
|
||||||
|
source: Partial<T>,
|
||||||
|
): 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;
|
||||||
|
};
|
||||||
|
|
@ -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<Result<string[]>> => {
|
||||||
|
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<Result<string[]>> => {
|
||||||
|
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<Result<boolean>> => {
|
||||||
|
return tryCatch(async () => {
|
||||||
|
const git = simpleGit(root);
|
||||||
|
await git.status();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Git仓库的根目录
|
||||||
|
*/
|
||||||
|
export const getGitRoot = async (cwd: string): Promise<Result<string>> => {
|
||||||
|
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<Result<boolean>> => {
|
||||||
|
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; // 文件未被忽略
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -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<string, SourceFileLanguage> = {
|
||||||
|
'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<SourceFileLanguage, CommentPattern> = {
|
||||||
|
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,
|
||||||
|
multiEnd: /-->/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,
|
||||||
|
multiEnd: /-->/g
|
||||||
|
},
|
||||||
|
xml: {
|
||||||
|
single: /^$/g, // XML不支持单行注释
|
||||||
|
multiStart: /<!--/g,
|
||||||
|
multiEnd: /-->/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)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* 信号量并发控制类
|
||||||
|
*/
|
||||||
|
export class Semaphore {
|
||||||
|
private permits: number;
|
||||||
|
private waiting: (() => void)[] = [];
|
||||||
|
|
||||||
|
constructor(permits: number) {
|
||||||
|
this.permits = permits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取许可
|
||||||
|
*/
|
||||||
|
async acquire(): Promise<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<string, string>;
|
||||||
|
query?: Record<string, string | string[] | undefined>;
|
||||||
|
region?: string;
|
||||||
|
serviceName?: string;
|
||||||
|
method?: string;
|
||||||
|
pathName?: string;
|
||||||
|
accessKeyId?: string;
|
||||||
|
secretAccessKey?: string;
|
||||||
|
needSignHeaderKeys?: string[];
|
||||||
|
bodySha?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询参数类型
|
||||||
|
*/
|
||||||
|
export type QueryParams = Record<string, string | string[] | undefined | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求头类型
|
||||||
|
*/
|
||||||
|
export type Headers = Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译请求参数接口
|
||||||
|
*/
|
||||||
|
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<TranslateConfig>,
|
||||||
|
): Promise<VolcTranslateResponse> {
|
||||||
|
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<string, string>,
|
||||||
|
).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');
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,10 @@ const spawn = require('cross-spawn')
|
||||||
const defaultConfig = require('cz-customizable');
|
const defaultConfig = require('cz-customizable');
|
||||||
const { getChangedPackages } = require('./utils')
|
const { getChangedPackages } = require('./utils')
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对不同类型的 commit prefix message
|
||||||
|
*/
|
||||||
const typesConfig = [
|
const typesConfig = [
|
||||||
{ value: 'feat', name: 'A new feature' },
|
{ value: 'feat', name: 'A new feature' },
|
||||||
{ value: 'fix', name: 'A bug fix' },
|
{ value: 'fix', name: 'A bug fix' },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue