@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "2.4.0",
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",
@@ -35,11 +35,20 @@ class TelegramChannel {
35
35
  constructor({ token, chatId }) {
36
36
  this.token = token
37
37
  this.chatId = chatId
38
- this.apiBase = `https://api.telegram.org/bot${token}`
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 fetch(`${this.apiBase}/sendMessage`, {
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 fetch(`${this.apiBase}/editMessageText`, {
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 fetch(`${this.apiBase}/deleteMessage`, {
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
- await fetch(`https://api.telegram.org/bot${config.channels.telegram.token}/sendChatAction`, {
417
- method: 'POST',
418
- headers: { 'Content-Type': 'application/json' },
419
- body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
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 fetch(`${this.apiBase}/sendMessage`, {
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 fetch(`${this.apiBase}/editMessageText`, {
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 fetch(`${this.apiBase}/deleteMessage`, {
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 fetch(`${this.apiBase}/sendChatAction`, {
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 fetch(`${this.apiBase}/getFile`, {
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 fetch(`${this.apiBase}/setMyCommands`, {
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.bot = this.token ? new TelegramBotClient(this.token) : null
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 fetch(url, {
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 fetch(`${this.bot.apiBase}/answerCallbackQuery`, {
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
+ }