@ansstory/hias 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -1
- package/.vscode/settings.json +8 -0
- package/LICENSE +22 -22
- package/README.md +533 -11
- package/README.zh-CN.md +531 -0
- package/lib/config.js +22 -18
- package/lib/core/action.js +95 -68
- package/lib/core/close-port.js +173 -0
- package/lib/core/commander.js +106 -40
- package/lib/core/download.js +32 -19
- package/lib/core/help.js +17 -6
- package/lib/core/lang.js +50 -0
- package/lib/core/translate.js +1049 -0
- package/lib/extractors/index.js +694 -0
- package/lib/i18n/index.js +77 -0
- package/lib/i18n/resources/en.json +91 -0
- package/lib/i18n/resources/zh-CN.json +91 -0
- package/lib/i18n/store.js +85 -0
- package/lib/index.js +14 -6
- package/lib/template/component.jsx.ejs +11 -11
- package/lib/template/component.tsx.ejs +12 -12
- package/lib/template/component.vue.ejs +13 -13
- package/lib/template/reduxStore.jsx.ejs +16 -16
- package/lib/template/reduxTsStore.tsx.ejs +22 -22
- package/lib/utils/baidu-translate.js +78 -0
- package/lib/utils/compile-ejs.js +28 -21
- package/lib/utils/tencent-translate.js +187 -0
- package/lib/utils/translation-cache.js +73 -0
- package/lib/utils/write-file.js +16 -10
- package/package.json +11 -3
- package/test/close-port.test.js +88 -0
- package/test/translate.test.js +545 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// 腾讯翻译 API 封装 - 提供文本翻译能力
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const axios = require('axios')
|
|
5
|
+
const chalk = require('chalk')
|
|
6
|
+
|
|
7
|
+
const SERVICE = 'tmt'
|
|
8
|
+
const HOST = 'tmt.tencentcloudapi.com'
|
|
9
|
+
const ENDPOINT = 'https://tmt.tencentcloudapi.com'
|
|
10
|
+
const VERSION = '2018-03-21'
|
|
11
|
+
const REGION = 'ap-guangzhou'
|
|
12
|
+
const MAX_RETRIES = 3
|
|
13
|
+
const RETRY_DELAY = 1000
|
|
14
|
+
|
|
15
|
+
async function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function withRetry(fn, retries = MAX_RETRIES) {
|
|
20
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
return await fn()
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const isRetryable = err.message && (
|
|
25
|
+
err.message.includes('ResourceExhausted') ||
|
|
26
|
+
err.message.includes('LimitExceeded') ||
|
|
27
|
+
err.message.includes('InternalError') ||
|
|
28
|
+
err.message.includes('request failed: 5')
|
|
29
|
+
)
|
|
30
|
+
if (attempt < retries - 1 && isRetryable) {
|
|
31
|
+
const delay = RETRY_DELAY * Math.pow(2, attempt)
|
|
32
|
+
console.warn(chalk.yellow(` ⚠ Tencent API attempt ${attempt + 1} failed, retrying in ${delay}ms...`))
|
|
33
|
+
await sleep(delay)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sha256Hex(message) {
|
|
42
|
+
return crypto.createHash('sha256').update(message, 'utf-8').digest('hex')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hmacSha256(key, message) {
|
|
46
|
+
return crypto.createHmac('sha256', key).update(message, 'utf-8').digest()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 生成腾讯云 TC3-HMAC-SHA256 签名认证头
|
|
51
|
+
* 仅对 content-type 和 host 两个头签名(与官方 SDK 一致)
|
|
52
|
+
*/
|
|
53
|
+
function buildAuthorization(secretId, secretKey, timestamp, contentType, host, payload) {
|
|
54
|
+
const date = new Date(timestamp * 1000).toISOString().slice(0, 10)
|
|
55
|
+
|
|
56
|
+
const canonicalHeaders = `content-type:${contentType}\nhost:${host}\n`
|
|
57
|
+
const signedHeaders = 'content-type;host'
|
|
58
|
+
const hashedRequestPayload = sha256Hex(JSON.stringify(payload))
|
|
59
|
+
|
|
60
|
+
const canonicalRequest = [
|
|
61
|
+
'POST',
|
|
62
|
+
'/',
|
|
63
|
+
'',
|
|
64
|
+
canonicalHeaders,
|
|
65
|
+
signedHeaders,
|
|
66
|
+
hashedRequestPayload,
|
|
67
|
+
].join('\n')
|
|
68
|
+
|
|
69
|
+
const algorithm = 'TC3-HMAC-SHA256'
|
|
70
|
+
const credentialScope = `${date}/${SERVICE}/tc3_request`
|
|
71
|
+
const hashedCanonicalRequest = sha256Hex(canonicalRequest)
|
|
72
|
+
|
|
73
|
+
const stringToSign = [
|
|
74
|
+
algorithm,
|
|
75
|
+
String(timestamp),
|
|
76
|
+
credentialScope,
|
|
77
|
+
hashedCanonicalRequest,
|
|
78
|
+
].join('\n')
|
|
79
|
+
|
|
80
|
+
const kDate = hmacSha256(`TC3${secretKey}`, date)
|
|
81
|
+
const kService = hmacSha256(kDate, SERVICE)
|
|
82
|
+
const kSigning = hmacSha256(kService, 'tc3_request')
|
|
83
|
+
const signature = hmacSha256(kSigning, stringToSign).toString('hex')
|
|
84
|
+
|
|
85
|
+
return `${algorithm} Credential=${secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 调用腾讯云翻译 API 批量翻译中文文本为英文
|
|
90
|
+
* 使用 TextTranslateBatch 支持单次请求多个文本
|
|
91
|
+
* @param {string[]} texts - 待翻译文本数组
|
|
92
|
+
* @param {string} secretId - 腾讯云 SecretId
|
|
93
|
+
* @param {string} secretKey - 腾讯云 SecretKey
|
|
94
|
+
* @param {string} [from='zh'] - 源语言
|
|
95
|
+
* @param {string} [to='en'] - 目标语言
|
|
96
|
+
* @returns {Promise<string[]>} - 翻译结果数组
|
|
97
|
+
*/
|
|
98
|
+
async function tencentTranslate(texts, secretId, secretKey, from = 'zh', to = 'en') {
|
|
99
|
+
if (texts.length === 0) return []
|
|
100
|
+
|
|
101
|
+
// Tencent TMT 单条文本限制 2000 字节,超长时按句子边界截断并警告
|
|
102
|
+
const MAX_ITEM_BYTES = 2000
|
|
103
|
+
const truncated = []
|
|
104
|
+
for (let i = 0; i < texts.length; i++) {
|
|
105
|
+
const bytes = Buffer.byteLength(texts[i], 'utf-8')
|
|
106
|
+
if (bytes > MAX_ITEM_BYTES) {
|
|
107
|
+
console.warn(chalk.yellow(` ⚠ Text #${i} exceeds ${MAX_ITEM_BYTES} bytes (${bytes}), truncating at sentence boundary`))
|
|
108
|
+
let buf = Buffer.from(texts[i], 'utf-8')
|
|
109
|
+
buf = buf.subarray(0, MAX_ITEM_BYTES)
|
|
110
|
+
let truncatedStr = buf.toString('utf-8').replace(/\uFFFD/g, '')
|
|
111
|
+
// 从末尾向前查找句子边界(句号、换行、感叹号、问号、分号)
|
|
112
|
+
const boundaryMatch = truncatedStr.match(/.*[。!?\n;;]/)
|
|
113
|
+
if (boundaryMatch) {
|
|
114
|
+
truncatedStr = boundaryMatch[0]
|
|
115
|
+
}
|
|
116
|
+
truncated.push(truncatedStr)
|
|
117
|
+
} else {
|
|
118
|
+
truncated.push(texts[i])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const BATCH_LIMIT = 200
|
|
123
|
+
const allResults = []
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < truncated.length; i += BATCH_LIMIT) {
|
|
126
|
+
const batch = truncated.slice(i, i + BATCH_LIMIT)
|
|
127
|
+
|
|
128
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
129
|
+
const action = batch.length > 1 ? 'TextTranslateBatch' : 'TextTranslate'
|
|
130
|
+
const headers = {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
Host: HOST,
|
|
133
|
+
'X-TC-Action': action,
|
|
134
|
+
'X-TC-Version': VERSION,
|
|
135
|
+
'X-TC-Region': REGION,
|
|
136
|
+
'X-TC-Timestamp': String(timestamp),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let payload
|
|
140
|
+
if (batch.length > 1) {
|
|
141
|
+
payload = {
|
|
142
|
+
SourceTextList: batch,
|
|
143
|
+
Source: from,
|
|
144
|
+
Target: to,
|
|
145
|
+
ProjectId: 0,
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
payload = {
|
|
149
|
+
SourceText: batch[0],
|
|
150
|
+
Source: from,
|
|
151
|
+
Target: to,
|
|
152
|
+
ProjectId: 0,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
headers.Authorization = buildAuthorization(secretId, secretKey, timestamp, 'application/json', HOST, payload)
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const response = await withRetry(() => axios.post(ENDPOINT, payload, { headers }))
|
|
160
|
+
const d = response.data
|
|
161
|
+
|
|
162
|
+
if (d && d.Response) {
|
|
163
|
+
if (d.Response.TargetTextList) {
|
|
164
|
+
allResults.push(...d.Response.TargetTextList)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (d.Response.TargetText) {
|
|
168
|
+
allResults.push(d.Response.TargetText)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (d.Response.Error) {
|
|
172
|
+
throw new Error(`Tencent API error: ${d.Response.Error.Message} (Code: ${d.Response.Error.Code})`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new Error('Translation failed: no result returned')
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error.response) {
|
|
178
|
+
throw new Error(`Tencent API request failed: ${error.response.status} ${JSON.stringify(error.response.data)}`)
|
|
179
|
+
}
|
|
180
|
+
throw error
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return allResults
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { tencentTranslate }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const CACHE_FILE = '.hias/.translation-cache.json'
|
|
5
|
+
|
|
6
|
+
let cache = null
|
|
7
|
+
let cacheDirty = false
|
|
8
|
+
|
|
9
|
+
function getCachePath(baseDir) {
|
|
10
|
+
return path.join(baseDir, CACHE_FILE)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadCache(baseDir) {
|
|
14
|
+
if (cache) return cache
|
|
15
|
+
const cp = getCachePath(baseDir)
|
|
16
|
+
try {
|
|
17
|
+
if (fs.existsSync(cp)) {
|
|
18
|
+
cache = JSON.parse(fs.readFileSync(cp, 'utf-8'))
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// ignore corrupt cache
|
|
22
|
+
}
|
|
23
|
+
if (!cache) cache = {}
|
|
24
|
+
cacheDirty = false
|
|
25
|
+
return cache
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveCache(baseDir) {
|
|
29
|
+
if (!cacheDirty) return
|
|
30
|
+
const cp = getCachePath(baseDir)
|
|
31
|
+
try {
|
|
32
|
+
const dir = path.dirname(cp)
|
|
33
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
34
|
+
fs.writeFileSync(cp, JSON.stringify(cache, null, 2), 'utf-8')
|
|
35
|
+
cacheDirty = false
|
|
36
|
+
} catch {
|
|
37
|
+
// silent fail on cache write
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getCachedTranslation(text, fromLang, toLang, baseDir) {
|
|
42
|
+
const c = loadCache(baseDir)
|
|
43
|
+
const pair = `${fromLang}|${toLang}`
|
|
44
|
+
return c[pair] && c[pair][text] ? c[pair][text] : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setCachedTranslation(text, fromLang, toLang, translation, baseDir) {
|
|
48
|
+
const c = loadCache(baseDir)
|
|
49
|
+
const pair = `${fromLang}|${toLang}`
|
|
50
|
+
if (!c[pair]) c[pair] = {}
|
|
51
|
+
c[pair][text] = translation
|
|
52
|
+
cacheDirty = true
|
|
53
|
+
setImmediate(() => saveCache(baseDir))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setBatchCache(results, texts, fromLang, toLang, baseDir) {
|
|
57
|
+
const c = loadCache(baseDir)
|
|
58
|
+
const pair = `${fromLang}|${toLang}`
|
|
59
|
+
if (!c[pair]) c[pair] = {}
|
|
60
|
+
for (let i = 0; i < texts.length; i++) {
|
|
61
|
+
c[pair][texts[i]] = results[i] || texts[i]
|
|
62
|
+
}
|
|
63
|
+
cacheDirty = true
|
|
64
|
+
setImmediate(() => saveCache(baseDir))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
getCachedTranslation,
|
|
69
|
+
setCachedTranslation,
|
|
70
|
+
setBatchCache,
|
|
71
|
+
saveCache,
|
|
72
|
+
loadCache,
|
|
73
|
+
}
|
package/lib/utils/write-file.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// 文件写入工具 - 处理文件和目录的创建及内容写入
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// 异步写入文件函数,自动创建必要的目录结构
|
|
7
|
+
async function writeFile(filePath, content) {
|
|
8
|
+
// 获取文件所在的目录
|
|
9
|
+
const dir = path.dirname(filePath)
|
|
10
|
+
// 递归创建目录结构(如果目录不存在)
|
|
11
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
12
|
+
// 将内容写入文件
|
|
13
|
+
return fs.promises.writeFile(filePath, content)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = writeFile
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ansstory/hias",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "个人自用脚手架内置个人常用功能",
|
|
6
6
|
"author": "AnsStory story <story0809@163.com>",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": {
|
|
@@ -12,15 +12,23 @@
|
|
|
12
12
|
"hias ansstory"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"test": "
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"addv": "npm version patch",
|
|
17
|
+
"asp": "npm publish --access public",
|
|
18
|
+
"prepublish": "npm run addv && npm run asp"
|
|
16
19
|
},
|
|
17
20
|
"license": "MIT",
|
|
18
21
|
"dependencies": {
|
|
22
|
+
"axios": "^1.17.0",
|
|
19
23
|
"chalk": "^4.1.2",
|
|
20
24
|
"commander": "^14.0.0",
|
|
21
25
|
"download-git-repo": "^3.0.2",
|
|
22
26
|
"ejs": "^3.1.10",
|
|
27
|
+
"i18next": "^26.3.0",
|
|
23
28
|
"inquirer": "^8.2.6",
|
|
24
29
|
"ora": "^5.4.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tencentcloud-sdk-nodejs-tmt": "^4.1.207"
|
|
25
33
|
}
|
|
26
34
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
|
|
4
|
+
const { closePort, closePorts, normalizePort, parsePortInputs, parsePidsFromWindowsNetstat } = require('../lib/core/close-port')
|
|
5
|
+
|
|
6
|
+
test('normalizePort accepts valid numeric port strings', () => {
|
|
7
|
+
assert.equal(normalizePort('3000'), 3000)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('normalizePort rejects invalid ports', () => {
|
|
11
|
+
assert.throws(() => normalizePort('abc'), /Invalid port/)
|
|
12
|
+
assert.throws(() => normalizePort('0'), /Invalid port/)
|
|
13
|
+
assert.throws(() => normalizePort('65536'), /Invalid port/)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('parsePortInputs supports space-separated and comma-separated ports', () => {
|
|
17
|
+
assert.deepEqual(parsePortInputs(['8076', '8077,8078']), [8076, 8077, 8078])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('parsePortInputs removes duplicate ports', () => {
|
|
21
|
+
assert.deepEqual(parsePortInputs(['8076,8077', '8076']), [8076, 8077])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('parsePidsFromWindowsNetstat returns unique PIDs listening on the requested port', () => {
|
|
25
|
+
const output = [
|
|
26
|
+
' TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 1234',
|
|
27
|
+
' TCP [::]:3000 [::]:0 LISTENING 1234',
|
|
28
|
+
' TCP 127.0.0.1:3001 0.0.0.0:0 LISTENING 7777',
|
|
29
|
+
' TCP 127.0.0.1:3000 127.0.0.1:54511 ESTABLISHED 2222',
|
|
30
|
+
' UDP 0.0.0.0:3000 *:* 5678',
|
|
31
|
+
].join('\n')
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(parsePidsFromWindowsNetstat(output, 3000), ['1234', '5678'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('closePort kills every process occupying the port', async () => {
|
|
37
|
+
const commands = []
|
|
38
|
+
const exec = async (command) => {
|
|
39
|
+
commands.push(command)
|
|
40
|
+
if (command.startsWith('netstat')) {
|
|
41
|
+
return {
|
|
42
|
+
stdout: [
|
|
43
|
+
' TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 1001',
|
|
44
|
+
' UDP 0.0.0.0:5173 *:* 1002',
|
|
45
|
+
].join('\n'),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { stdout: '' }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await closePort('5173', { exec, platform: 'win32' })
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(result, { port: 5173, pids: ['1001', '1002'], killed: ['1001', '1002'] })
|
|
54
|
+
assert.deepEqual(commands, ['netstat -ano', 'taskkill /F /PID 1001', 'taskkill /F /PID 1002'])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('closePorts closes each requested port', async () => {
|
|
58
|
+
const commands = []
|
|
59
|
+
const exec = async (command) => {
|
|
60
|
+
commands.push(command)
|
|
61
|
+
if (command === 'netstat -ano') {
|
|
62
|
+
return {
|
|
63
|
+
stdout: [
|
|
64
|
+
' TCP 0.0.0.0:8076 0.0.0.0:0 LISTENING 9001',
|
|
65
|
+
' TCP 0.0.0.0:8077 0.0.0.0:0 LISTENING 9002',
|
|
66
|
+
].join('\n'),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { stdout: '' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const results = await closePorts(['8076', '8077'], { exec, platform: 'win32' })
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(results, [
|
|
75
|
+
{ port: 8076, pids: ['9001'], killed: ['9001'] },
|
|
76
|
+
{ port: 8077, pids: ['9002'], killed: ['9002'] },
|
|
77
|
+
])
|
|
78
|
+
assert.deepEqual(commands, ['netstat -ano', 'taskkill /F /PID 9001', 'netstat -ano', 'taskkill /F /PID 9002'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('closePort returns an empty result when no process uses the port', async () => {
|
|
82
|
+
const result = await closePort('5173', {
|
|
83
|
+
exec: async () => ({ stdout: '' }),
|
|
84
|
+
platform: 'win32',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
assert.deepEqual(result, { port: 5173, pids: [], killed: [] })
|
|
88
|
+
})
|