@raolin2025/claude-code-node 2.3.6 → 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/README.md +71 -4
- package/package.json +2 -2
- package/src/channel/index.js +287 -33
- package/src/channel/notify-daemon.js +616 -384
- package/src/channel/qqbot-listener.js +403 -0
- package/src/channel/tg-listener.js +614 -0
- package/src/channel/tg-proxy.js +236 -0
|
@@ -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
|
+
}
|