@raolin2025/claude-code-node 2.4.0 → 2.4.1
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/package.json +1 -1
- package/src/channel/index.js +13 -4
- package/src/channel/notify-daemon.js +17 -5
- package/src/channel/tg-listener.js +30 -11
- package/src/channel/tg-proxy.js +236 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/channel/index.js
CHANGED
|
@@ -35,11 +35,20 @@ class TelegramChannel {
|
|
|
35
35
|
constructor({ token, chatId }) {
|
|
36
36
|
this.token = token
|
|
37
37
|
this.chatId = chatId
|
|
38
|
-
this.
|
|
38
|
+
this.proxyAddr = process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || ''
|
|
39
|
+
const customBase = process.env.CC_NODE_CHANNEL_TELEGRAM_API_BASE || ''
|
|
40
|
+
this.apiBase = customBase || `https://api.telegram.org/bot${token}`
|
|
39
41
|
this.lastCall = 0
|
|
40
42
|
this.callInterval = 50 // 20 calls/sec max
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/** 带代理支持的 fetch */
|
|
46
|
+
async _fetch(url, options = {}) {
|
|
47
|
+
if (!this.proxyAddr) return fetch(url, options)
|
|
48
|
+
const { fetchViaSocks5 } = await import('./tg-proxy.js')
|
|
49
|
+
return fetchViaSocks5(url, options, this.proxyAddr)
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
get name() { return 'telegram' }
|
|
44
53
|
|
|
45
54
|
/** 速率限制等待 */
|
|
@@ -92,7 +101,7 @@ class TelegramChannel {
|
|
|
92
101
|
// Telegram HTML 安全编码(只保留基本标签)
|
|
93
102
|
body.text = this._safeHTML(body.text)
|
|
94
103
|
|
|
95
|
-
const r = await
|
|
104
|
+
const r = await this._fetch(`${this.apiBase}/sendMessage`, {
|
|
96
105
|
method: 'POST',
|
|
97
106
|
headers: { 'Content-Type': 'application/json' },
|
|
98
107
|
body: JSON.stringify(body),
|
|
@@ -160,7 +169,7 @@ class TelegramChannel {
|
|
|
160
169
|
/** 编辑消息 */
|
|
161
170
|
async edit(messageId, text, options = {}) {
|
|
162
171
|
await this._rateLimit()
|
|
163
|
-
const r = await
|
|
172
|
+
const r = await this._fetch(`${this.apiBase}/editMessageText`, {
|
|
164
173
|
method: 'POST',
|
|
165
174
|
headers: { 'Content-Type': 'application/json' },
|
|
166
175
|
body: JSON.stringify({
|
|
@@ -176,7 +185,7 @@ class TelegramChannel {
|
|
|
176
185
|
/** 删除消息 */
|
|
177
186
|
async delete(messageId) {
|
|
178
187
|
await this._rateLimit()
|
|
179
|
-
const r = await
|
|
188
|
+
const r = await this._fetch(`${this.apiBase}/deleteMessage`, {
|
|
180
189
|
method: 'POST',
|
|
181
190
|
headers: { 'Content-Type': 'application/json' },
|
|
182
191
|
body: JSON.stringify({ chat_id: this.chatId, message_id: messageId }),
|
|
@@ -413,11 +413,23 @@ function createMessageHandler(config) {
|
|
|
413
413
|
// 发送"处理中"提示
|
|
414
414
|
if (isTelegram && config.channels.telegram?.token) {
|
|
415
415
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
416
|
+
const proxyAddr = config.channels.telegram.proxy || process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || ''
|
|
417
|
+
const apiBase = config.channels.telegram.apiBase || `https://api.telegram.org`
|
|
418
|
+
const url = `${apiBase}/bot${config.channels.telegram.token}/sendChatAction`
|
|
419
|
+
if (proxyAddr) {
|
|
420
|
+
const { fetchViaSocks5 } = await import('./tg-proxy.js')
|
|
421
|
+
await fetchViaSocks5(url, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: { 'Content-Type': 'application/json' },
|
|
424
|
+
body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
|
|
425
|
+
}, proxyAddr)
|
|
426
|
+
} else {
|
|
427
|
+
await fetch(url, {
|
|
428
|
+
method: 'POST',
|
|
429
|
+
headers: { 'Content-Type': 'application/json' },
|
|
430
|
+
body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
|
|
431
|
+
})
|
|
432
|
+
}
|
|
421
433
|
} catch {}
|
|
422
434
|
}
|
|
423
435
|
if (isQQBot) {
|
|
@@ -92,12 +92,22 @@ class RateLimiter {
|
|
|
92
92
|
// ============================================================
|
|
93
93
|
|
|
94
94
|
class TelegramBotClient {
|
|
95
|
-
constructor(token) {
|
|
95
|
+
constructor(token, opts = {}) {
|
|
96
96
|
this.token = token
|
|
97
|
-
this.apiBase = API_BASE(token)
|
|
97
|
+
this.apiBase = opts.apiBase || API_BASE(token)
|
|
98
|
+
this.proxyAddr = opts.proxy || '' // SOCKS5 代理地址, 如 "127.0.0.1:1080" 或 "socks5://user:pass@host:port"
|
|
98
99
|
this.rateLimiter = new RateLimiter()
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/** 带代理支持的 fetch */
|
|
103
|
+
async _fetch(url, options = {}) {
|
|
104
|
+
if (!this.proxyAddr) {
|
|
105
|
+
return fetch(url, options)
|
|
106
|
+
}
|
|
107
|
+
const { fetchViaSocks5 } = await import('./tg-proxy.js')
|
|
108
|
+
return fetchViaSocks5(url, options, this.proxyAddr)
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
/** 发送消息(带自动重试和速率限制) */
|
|
102
112
|
async sendMessage(chatId, text, options = {}) {
|
|
103
113
|
const { parseMode, replyTo, silent, disableWebPreview, keyboard } = options
|
|
@@ -115,7 +125,7 @@ class TelegramBotClient {
|
|
|
115
125
|
if (replyTo) body.reply_parameters = { message_id: replyTo }
|
|
116
126
|
if (keyboard) body.reply_markup = JSON.stringify(keyboard)
|
|
117
127
|
|
|
118
|
-
const res = await
|
|
128
|
+
const res = await this._fetch(`${this.apiBase}/sendMessage`, {
|
|
119
129
|
method: 'POST',
|
|
120
130
|
headers: { 'Content-Type': 'application/json' },
|
|
121
131
|
body: JSON.stringify(body),
|
|
@@ -147,7 +157,7 @@ class TelegramBotClient {
|
|
|
147
157
|
text: text.slice(0, 4096),
|
|
148
158
|
parse_mode: parseMode || 'HTML',
|
|
149
159
|
}
|
|
150
|
-
const res = await
|
|
160
|
+
const res = await this._fetch(`${this.apiBase}/editMessageText`, {
|
|
151
161
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
152
162
|
})
|
|
153
163
|
const data = await res.json()
|
|
@@ -157,7 +167,7 @@ class TelegramBotClient {
|
|
|
157
167
|
|
|
158
168
|
/** 删除消息 */
|
|
159
169
|
async deleteMessage(chatId, messageId) {
|
|
160
|
-
const res = await
|
|
170
|
+
const res = await this._fetch(`${this.apiBase}/deleteMessage`, {
|
|
161
171
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
162
172
|
body: JSON.stringify({ chat_id: chatId, message_id: messageId }),
|
|
163
173
|
})
|
|
@@ -167,7 +177,7 @@ class TelegramBotClient {
|
|
|
167
177
|
/** 发送聊天动作(typing/upload_photo 等) */
|
|
168
178
|
async sendChatAction(chatId, action = 'typing') {
|
|
169
179
|
try {
|
|
170
|
-
await
|
|
180
|
+
await this._fetch(`${this.apiBase}/sendChatAction`, {
|
|
171
181
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
172
182
|
body: JSON.stringify({ chat_id: chatId, action }),
|
|
173
183
|
})
|
|
@@ -176,7 +186,7 @@ class TelegramBotClient {
|
|
|
176
186
|
|
|
177
187
|
/** 获取文件下载链接 */
|
|
178
188
|
async getFile(fileId) {
|
|
179
|
-
const res = await
|
|
189
|
+
const res = await this._fetch(`${this.apiBase}/getFile`, {
|
|
180
190
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
181
191
|
body: JSON.stringify({ file_id: fileId }),
|
|
182
192
|
})
|
|
@@ -187,7 +197,7 @@ class TelegramBotClient {
|
|
|
187
197
|
|
|
188
198
|
/** 设置机器人命令菜单 */
|
|
189
199
|
async setMyCommands(commands) {
|
|
190
|
-
await
|
|
200
|
+
await this._fetch(`${this.apiBase}/setMyCommands`, {
|
|
191
201
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
192
202
|
body: JSON.stringify({ commands }),
|
|
193
203
|
})
|
|
@@ -255,7 +265,9 @@ export class TelegramListener {
|
|
|
255
265
|
this.config = config
|
|
256
266
|
const ch = config.channels?.telegram || {}
|
|
257
267
|
this.token = ch.token
|
|
258
|
-
this.
|
|
268
|
+
this.proxyAddr = ch.proxy || process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || ''
|
|
269
|
+
this.apiBase = ch.apiBase || ''
|
|
270
|
+
this.bot = this.token ? new TelegramBotClient(this.token, { proxy: this.proxyAddr, apiBase: this.apiBase }) : null
|
|
259
271
|
this.lastUpdateId = 0
|
|
260
272
|
this.running = false
|
|
261
273
|
this._pollTimer = null
|
|
@@ -271,6 +283,13 @@ export class TelegramListener {
|
|
|
271
283
|
this._handlers[event] = handler
|
|
272
284
|
}
|
|
273
285
|
|
|
286
|
+
/** 带代理的 fetch(供类内部使用) */
|
|
287
|
+
async _fetch(url, options = {}) {
|
|
288
|
+
if (!this.proxyAddr) return fetch(url, options)
|
|
289
|
+
const { fetchViaSocks5 } = await import('./tg-proxy.js')
|
|
290
|
+
return fetchViaSocks5(url, options, this.proxyAddr)
|
|
291
|
+
}
|
|
292
|
+
|
|
274
293
|
/** 启动监听 */
|
|
275
294
|
async start(onMessage) {
|
|
276
295
|
if (!this.bot) {
|
|
@@ -301,7 +320,7 @@ export class TelegramListener {
|
|
|
301
320
|
while (this.running) {
|
|
302
321
|
try {
|
|
303
322
|
const url = `${this.bot.apiBase}/getUpdates`
|
|
304
|
-
const res = await
|
|
323
|
+
const res = await this._fetch(url, {
|
|
305
324
|
method: 'POST',
|
|
306
325
|
headers: { 'Content-Type': 'application/json' },
|
|
307
326
|
body: JSON.stringify({
|
|
@@ -421,7 +440,7 @@ export class TelegramListener {
|
|
|
421
440
|
|
|
422
441
|
// 确认收到回调(去除loading状态)
|
|
423
442
|
try {
|
|
424
|
-
await
|
|
443
|
+
await this._fetch(`${this.bot.apiBase}/answerCallbackQuery`, {
|
|
425
444
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
426
445
|
body: JSON.stringify({ callback_query_id: cb.id }),
|
|
427
446
|
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 零依赖 SOCKS5 代理连接器
|
|
3
|
+
*
|
|
4
|
+
* 用于 Telegram Bot API 通过 SOCKS5 代理访问(突破网络限制)
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* const tunnel = socks5Connect('127.0.0.1:1080', 'api.telegram.org', 443)
|
|
8
|
+
* const tlsSocket = tls.connect({ socket: tunnel, host: 'api.telegram.org', servername: 'api.telegram.org' })
|
|
9
|
+
*
|
|
10
|
+
* SOCKS5 协议参考: RFC 1928
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { connect as tcpConnect } from 'node:net'
|
|
14
|
+
import { connect as tlsConnect } from 'node:tls'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 建立 SOCKS5 隧道连接
|
|
18
|
+
*
|
|
19
|
+
* @param {string} proxyHost - 代理主机
|
|
20
|
+
* @param {number} proxyPort - 代理端口
|
|
21
|
+
* @param {string} targetHost - 目标主机
|
|
22
|
+
* @param {number} targetPort - 目标端口
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {string} [opts.username] - SOCKS5 用户名(可选)
|
|
25
|
+
* @param {string} [opts.password] - SOCKS5 密码(可选)
|
|
26
|
+
* @returns {Promise<import('node:net').Socket>}
|
|
27
|
+
*/
|
|
28
|
+
export function socks5Connect(proxyHost, proxyPort, targetHost, targetPort, opts = {}) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const socket = tcpConnect({ host: proxyHost, port: proxyPort })
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
socket.destroy()
|
|
33
|
+
reject(new Error('SOCKS5 proxy timeout'))
|
|
34
|
+
}, 10000)
|
|
35
|
+
|
|
36
|
+
socket.once('connect', async () => {
|
|
37
|
+
try {
|
|
38
|
+
// Step 1: 握手 — 协商认证方式
|
|
39
|
+
const authMethods = opts.username ? [0x00, 0x02] : [0x00] // 无认证 + 用户名密码
|
|
40
|
+
socket.write(Buffer.from([0x05, authMethods.length, ...authMethods]))
|
|
41
|
+
|
|
42
|
+
const handshake = await readBytes(socket, 2)
|
|
43
|
+
if (handshake[0] !== 0x05) {
|
|
44
|
+
throw new Error('SOCKS5: 版本不匹配')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 2: 认证(如果需要)
|
|
48
|
+
if (handshake[1] === 0x02) {
|
|
49
|
+
if (!opts.username) throw new Error('SOCKS5: 代理需要用户名密码')
|
|
50
|
+
const u = Buffer.from(opts.username, 'utf8')
|
|
51
|
+
const p = Buffer.from(opts.password, 'utf8')
|
|
52
|
+
const authReq = Buffer.from([0x01, u.length, ...u, p.length, ...p])
|
|
53
|
+
socket.write(authReq)
|
|
54
|
+
const authResp = await readBytes(socket, 2)
|
|
55
|
+
if (authResp[1] !== 0x00) throw new Error('SOCKS5: 认证失败')
|
|
56
|
+
} else if (handshake[1] !== 0x00) {
|
|
57
|
+
throw new Error('SOCKS5: 代理不支持不需要的认证方式')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 3: 发送连接请求
|
|
61
|
+
const hostType = /^\d+\.\d+\.\d+\.\d+$/.test(targetHost) ? 0x01 : 0x03
|
|
62
|
+
let addr
|
|
63
|
+
if (hostType === 0x01) {
|
|
64
|
+
addr = Buffer.from(targetHost.split('.').map(Number))
|
|
65
|
+
} else {
|
|
66
|
+
const hostBuf = Buffer.from(targetHost, 'utf8')
|
|
67
|
+
addr = Buffer.from([hostBuf.length, ...hostBuf])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const portBuf = Buffer.alloc(2)
|
|
71
|
+
portBuf.writeUInt16BE(targetPort)
|
|
72
|
+
const connectReq = Buffer.from([0x05, 0x01, 0x00, hostType, ...addr, ...portBuf])
|
|
73
|
+
socket.write(connectReq)
|
|
74
|
+
|
|
75
|
+
const connectResp = await readBytes(socket, 4)
|
|
76
|
+
if (connectResp[0] !== 0x05 || connectResp[1] !== 0x00) {
|
|
77
|
+
const errors = { 0x01: '通用错误', 0x02: '不允许', 0x03: '网络不可达', 0x04: '主机不可达', 0x05: '连接被拒', 0x06: 'TTL超时', 0x07: '命令不支持', 0x08: '地址类型不支持' }
|
|
78
|
+
throw new Error(`SOCKS5: 连接失败 — ${errors[connectResp[1]] || `错误码 ${connectResp[1]}`}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 读取剩余响应包头(根据地址类型)
|
|
82
|
+
const addrType = connectResp[3]
|
|
83
|
+
if (addrType === 0x01) await readBytes(socket, 6) // IPv4 + port
|
|
84
|
+
else if (addrType === 0x03) {
|
|
85
|
+
const len = (await readBytes(socket, 1))[0]
|
|
86
|
+
await readBytes(socket, len + 2) // hostname + port
|
|
87
|
+
} else if (addrType === 0x04) await readBytes(socket, 18) // IPv6 + port
|
|
88
|
+
|
|
89
|
+
clearTimeout(timeout)
|
|
90
|
+
resolve(socket)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
socket.destroy()
|
|
93
|
+
clearTimeout(timeout)
|
|
94
|
+
reject(e)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
socket.once('error', (err) => {
|
|
99
|
+
clearTimeout(timeout)
|
|
100
|
+
reject(err)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 创建通过 SOCKS5 代理的 TLS 连接
|
|
107
|
+
*
|
|
108
|
+
* @param {string} proxyAddr - 代理地址, 如 "127.0.0.1:1080" 或 "socks5://user:pass@host:port"
|
|
109
|
+
* @param {string} targetHost - 目标主机名 (如 "api.telegram.org")
|
|
110
|
+
* @param {number} targetPort - 目标端口 (如 443)
|
|
111
|
+
* @returns {Promise<import('node:tls').TLSSocket>}
|
|
112
|
+
*/
|
|
113
|
+
export async function createTlsTunnel(proxyAddr, targetHost, targetPort = 443) {
|
|
114
|
+
// 解析代理地址格式
|
|
115
|
+
let p = proxyAddr
|
|
116
|
+
let username, password
|
|
117
|
+
|
|
118
|
+
if (p.startsWith('socks5://')) {
|
|
119
|
+
p = p.slice(9)
|
|
120
|
+
const atIdx = p.lastIndexOf('@')
|
|
121
|
+
if (atIdx >= 0) {
|
|
122
|
+
const auth = p.slice(0, atIdx)
|
|
123
|
+
const colon = auth.indexOf(':')
|
|
124
|
+
username = colon >= 0 ? decodeURIComponent(auth.slice(0, colon)) : decodeURIComponent(auth)
|
|
125
|
+
password = colon >= 0 ? decodeURIComponent(auth.slice(colon + 1)) : ''
|
|
126
|
+
p = p.slice(atIdx + 1)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const colon = p.lastIndexOf(':')
|
|
131
|
+
if (colon < 0) throw new Error(`SOCKS5: 无效代理地址 "${proxyAddr}"`)
|
|
132
|
+
const proxyHost = p.slice(0, colon)
|
|
133
|
+
const proxyPort = parseInt(p.slice(colon + 1), 10)
|
|
134
|
+
|
|
135
|
+
const socket = await socks5Connect(proxyHost, proxyPort, targetHost, targetPort, { username, password })
|
|
136
|
+
const tlsSocket = tlsConnect({
|
|
137
|
+
socket,
|
|
138
|
+
host: targetHost,
|
|
139
|
+
servername: targetHost,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
tlsSocket.once('secureConnect', () => resolve(tlsSocket))
|
|
144
|
+
tlsSocket.once('error', reject)
|
|
145
|
+
setTimeout(() => reject(new Error('TLS handshake timeout')), 15000)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 发起 HTTPS 请求通过 SOCKS5 代理
|
|
151
|
+
*
|
|
152
|
+
* @param {string} url - 请求 URL
|
|
153
|
+
* @param {object} options - fetch 选项
|
|
154
|
+
* @param {string} proxyAddr - SOCKS5 代理地址
|
|
155
|
+
* @returns {Promise<Response>}
|
|
156
|
+
*/
|
|
157
|
+
export async function fetchViaSocks5(url, options = {}, proxyAddr) {
|
|
158
|
+
const parsedUrl = new URL(url)
|
|
159
|
+
const isHttps = parsedUrl.protocol === 'https:'
|
|
160
|
+
const port = parseInt(parsedUrl.port, 10) || (isHttps ? 443 : 80)
|
|
161
|
+
const host = parsedUrl.hostname
|
|
162
|
+
|
|
163
|
+
let socket
|
|
164
|
+
if (isHttps) {
|
|
165
|
+
socket = await createTlsTunnel(proxyAddr, host, port)
|
|
166
|
+
} else {
|
|
167
|
+
const [proxyHost, proxyPort] = proxyAddr.replace(/^socks5:\/\//, '').split(':')
|
|
168
|
+
socket = await socks5Connect(proxyHost, parseInt(proxyPort, 10), host, port)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 构建 HTTP 请求
|
|
172
|
+
const path = parsedUrl.pathname + parsedUrl.search
|
|
173
|
+
const headers = Object.entries(options.headers || {}).map(([k, v]) => `${k}: ${v}`).join('\r\n')
|
|
174
|
+
const body = options.body || ''
|
|
175
|
+
const req = `${options.method || 'GET'} ${path} HTTP/1.1\r\nHost: ${host}\r\n${headers ? headers + '\r\n' : ''}Content-Length: ${body.length}\r\nConnection: close\r\n\r\n${body}`
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
let responseData = ''
|
|
179
|
+
const timeout = setTimeout(() => {
|
|
180
|
+
socket.destroy()
|
|
181
|
+
reject(new Error('HTTP request timeout'))
|
|
182
|
+
}, 30000)
|
|
183
|
+
|
|
184
|
+
socket.write(req)
|
|
185
|
+
socket.on('data', (chunk) => {
|
|
186
|
+
responseData += chunk.toString()
|
|
187
|
+
})
|
|
188
|
+
socket.on('end', () => {
|
|
189
|
+
clearTimeout(timeout)
|
|
190
|
+
// 解析 HTTP 响应
|
|
191
|
+
const headerEnd = responseData.indexOf('\r\n\r\n')
|
|
192
|
+
if (headerEnd < 0) {
|
|
193
|
+
reject(new Error('Invalid HTTP response'))
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
const statusLine = responseData.split('\r\n')[0]
|
|
197
|
+
const statusCode = parseInt(statusLine.split(' ')[1], 10)
|
|
198
|
+
const bodyData = responseData.slice(headerEnd + 4)
|
|
199
|
+
|
|
200
|
+
resolve({
|
|
201
|
+
ok: statusCode >= 200 && statusCode < 300,
|
|
202
|
+
status: statusCode,
|
|
203
|
+
statusText: statusLine,
|
|
204
|
+
headers: {},
|
|
205
|
+
text: async () => bodyData,
|
|
206
|
+
json: async () => JSON.parse(bodyData),
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
socket.on('error', (err) => {
|
|
210
|
+
clearTimeout(timeout)
|
|
211
|
+
reject(err)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 从 socket 读取指定字节数 */
|
|
217
|
+
function readBytes(socket, n) {
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
if (n === 0) return resolve(Buffer.alloc(0))
|
|
220
|
+
let buf = Buffer.alloc(0)
|
|
221
|
+
const onData = (chunk) => {
|
|
222
|
+
buf = Buffer.concat([buf, chunk])
|
|
223
|
+
if (buf.length >= n) {
|
|
224
|
+
socket.removeListener('data', onData)
|
|
225
|
+
resolve(buf.slice(0, n))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
socket.on('data', onData)
|
|
229
|
+
socket.once('error', reject)
|
|
230
|
+
// 处理已经缓冲的数据
|
|
231
|
+
if (buf.length >= n) {
|
|
232
|
+
socket.removeListener('data', onData)
|
|
233
|
+
resolve(buf.slice(0, n))
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
}
|