@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.
@@ -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
+ }
@@ -1,10 +1,16 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
-
4
- async function writeFile(filePath, content) {
5
- const dir = path.dirname(filePath)
6
- await fs.promises.mkdir(dir, { recursive: true })
7
- return fs.promises.writeFile(filePath, content)
8
- }
9
-
10
- module.exports = writeFile
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.4",
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": "echo \"Error: no test specified\" && exit 1"
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
+ })