@raolin2025/claude-code-node 2.4.3 → 2.5.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "2.4.3",
4
- "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming",
3
+ "version": "2.5.1",
4
+ "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming, rich media upload, multi-account management",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -195,82 +195,140 @@ class TelegramChannel {
195
195
  }
196
196
 
197
197
  // ============================================================
198
- // QQ Bot 通道适配器 v2.0
198
+ // QQ Bot 通道适配器 v3.0 — 多账户、权限、富媒体
199
199
  // ============================================================
200
200
 
201
+ import { QQBotEnhanced } from './qqbot-enhanced.js'
202
+ import { QQBotAccountManager } from './qqbot-account-manager.js'
203
+
201
204
  /**
202
- * QQ Bot 通道适配器 — 独立、零依赖
205
+ * QQ Bot 增强版通道适配器
203
206
  *
204
- * 使用 QQ Bot API v2,仅需 appId + clientSecret
207
+ * 功能:
208
+ * - 多账户管理(从配置文件加载)
209
+ * - 权限控制(dmPolicy/groupPolicy + allowFrom 白名单)
210
+ * - 自动解析 <qqmedia> 标签并上传富媒体
211
+ * - 支持图片、文件、语音上传(限于 ~/.openclaw/media/qqbot)
212
+ * - WebSocket 监听(可选)
205
213
  */
206
214
  class QQBotChannel {
207
- constructor(config = {}) {
208
- this.appId = config.appId || process.env.CC_NODE_CHANNEL_QQBOT_APPID || ''
209
- this.secret = config.secret || config.clientSecret || process.env.CC_NODE_CHANNEL_QQBOT_SECRET || ''
210
- this._token = null
211
- this._tokenCache = { token: null, expireAt: 0 }
212
- this.channelId = config.channelId || ''
213
- this.groupOpenId = config.groupOpenId || ''
214
- }
215
+ constructor(config = {}, globalConfig = {}) {
216
+ this.name = 'qqbot'
217
+ this.enabled = config.enabled !== false
218
+
219
+ // 合并全局配置
220
+ const qqbotConfig = {
221
+ ...globalConfig.qqbot,
222
+ ...config
223
+ }
215
224
 
216
- get name() { return 'qqbot' }
225
+ // 创建增强版 QQBot 实例
226
+ this.bot = new QQBotEnhanced(qqbotConfig)
217
227
 
218
- async _getToken() {
219
- if (this._tokenCache.token && Date.now() < this._tokenCache.expireAt - 300000) {
220
- return this._tokenCache.token
221
- }
222
- const res = await fetch('https://bots.qq.com/app/getAppAccessToken', {
223
- method: 'POST',
224
- headers: { 'Content-Type': 'application/json' },
225
- body: JSON.stringify({ appId: this.appId, clientSecret: this.secret }),
226
- })
227
- if (!res.ok) throw new Error('QQ Token API ' + res.status)
228
- const data = JSON.parse(await res.text())
229
- if (!data.access_token) throw new Error('Token API no access_token')
230
- this._tokenCache.token = data.access_token
231
- this._tokenCache.expireAt = Date.now() + (data.expires_in || 7200) * 1000
232
- return data.access_token
228
+ // 通道特定配置
229
+ this.scope = config.scope || 'group' // 默认发送到群
230
+ this.defaultTargetId = config.targetId || config.groupOpenId || ''
231
+
232
+ // WebSocket 监听状态
233
+ this._listener = null
234
+ this._onMessageCallback = null
233
235
  }
234
236
 
235
- async send(text, options = {}) {
236
- if (!this.appId || !this.secret) {
237
- return [{ channel: 'qqbot', ok: false, error: '需要 appId 和 clientSecret' }]
237
+ /** 启动消息监听 */
238
+ async listen(onMessage) {
239
+ if (!this.enabled) {
240
+ console.warn('[QQBotChannel] 通道未启用')
241
+ return
238
242
  }
239
243
  try {
240
- const token = await this._getToken()
241
- const results = []
242
-
243
- const scope = options.scope || (this.groupOpenId ? 'group' : '')
244
- const targetId = options.targetId || this.groupOpenId || this.channelId
244
+ this._onMessageCallback = onMessage
245
+ await this.bot.listen(this._handleIncomingMessage.bind(this))
246
+ console.log('[QQBotChannel] 监听已启动')
247
+ } catch (e) {
248
+ console.error('[QQBotChannel] 监听启动失败:', e.message)
249
+ throw e
250
+ }
251
+ }
245
252
 
246
- if (!scope || !targetId) {
247
- return [{ channel: 'qqbot', ok: false, error: '未配置目标 (groupOpenId)' }]
248
- }
253
+ _handleIncomingMessage(msg) {
254
+ // 包装为统一消息格式,转发给回调
255
+ if (this._onMessageCallback) {
256
+ this._onMessageCallback({
257
+ channel: 'qqbot',
258
+ text: msg.text,
259
+ scope: msg.scope,
260
+ chatId: msg.chatId,
261
+ from: msg.from,
262
+ messageId: msg.messageId,
263
+ raw: msg.raw,
264
+ accountId: msg.accountId
265
+ })
266
+ }
267
+ }
249
268
 
250
- const path = scope === 'group'
251
- ? '/v2/groups/' + targetId + '/messages'
252
- : '/v2/users/' + targetId + '/messages'
269
+ /** 停止监听 */
270
+ stop() {
271
+ if (this.bot) {
272
+ this.bot.stop()
273
+ }
274
+ }
253
275
 
254
- const body = { content: text.slice(0, 2000), msg_type: 0 }
255
- if (options.replyMsgId) body.msg_id = options.replyMsgId
276
+ /** 发送消息(支持富媒体标签) */
277
+ async send(text, options = {}) {
278
+ if (!this.enabled) {
279
+ return [{ channel: 'qqbot', ok: false, error: '通道未启用' }]
280
+ }
256
281
 
257
- const r = await fetch('https://api.sgroup.qq.com' + path, {
258
- method: 'POST',
259
- headers: { 'Content-Type': 'application/json', Authorization: 'QQBot ' + token },
260
- body: JSON.stringify(body),
282
+ try {
283
+ const scope = options.scope || this.scope
284
+ const targetId = options.targetId || this.defaultTargetId
285
+ const accountId = options.accountId || null
286
+
287
+ const result = await this.bot.send({
288
+ text,
289
+ scope,
290
+ targetId,
291
+ accountId,
292
+ opts: { replyMsgId: options.replyMsgId }
261
293
  })
262
- if (!r.ok) {
263
- const errText = await r.text().catch(() => '')
264
- throw new Error('HTTP ' + r.status + ': ' + errText.slice(0, 100))
265
- }
266
- results.push({ channel: 'qqbot', ok: true })
267
- return results
294
+
295
+ return result.ok
296
+ ? [{ channel: 'qqbot', ok: true }]
297
+ : [{ channel: 'qqbot', ok: false, error: result.error }]
268
298
  } catch (e) {
269
- return [{ channel: 'qqbot', ok: false, error: e.message.slice(0, 200) }]
299
+ console.error('[QQBotChannel] 发送失败:', e)
300
+ return [{ channel: 'qqbot', ok: false, error: e.message }]
301
+ }
302
+ }
303
+
304
+ /** 支持的工具调用(提供给 Agent) */
305
+ get tools() {
306
+ return {
307
+ /** 发送 QQ 消息 */
308
+ qqbot_send: async (args) => {
309
+ const { text, scope = this.scope, targetId = this.defaultTargetId } = args
310
+ const result = await this.send(text, { scope, targetId })
311
+ return result[0]
312
+ },
313
+
314
+ /** 获取账户列表 */
315
+ qqbot_list_accounts: async () => {
316
+ const accounts = this.bot.accountManager.getAllAccounts()
317
+ return accounts.map(a => ({ id: a.id, name: a.name, enabled: a.enabled }))
318
+ },
319
+
320
+ /** 发送图片(直接文件路径) */
321
+ qqbot_send_image: async (args) => {
322
+ const { path, scope = this.scope, targetId = this.defaultTargetId } = args
323
+ // 这里需要支持直接的图片发送,不通过文本标签
324
+ // 临时方案:调用 bot 的底层方法
325
+ return { ok: false, error: '暂未实现' }
326
+ }
270
327
  }
271
328
  }
272
329
  }
273
330
 
331
+
274
332
  // ============================================================
275
333
  // 已有适配器(保持兼容)
276
334
  // ============================================================
@@ -0,0 +1,117 @@
1
+ /**
2
+ * QQBot 多账户管理器
3
+ *
4
+ * 功能:
5
+ * - 管理多个 QQ 机器人账户
6
+ * - 自动选择账户发送消息
7
+ * - 支持账户级别的权限控制
8
+ */
9
+
10
+ export class QQBotAccount {
11
+ constructor(config, globalConfig = {}) {
12
+ this.id = config.id || 'default'
13
+ this.name = config.name || this.id
14
+ this.enabled = config.enabled !== false
15
+
16
+ // 认证信息(支持环境变量或直接配置)
17
+ this.appId = config.appId || process.env[`CC_NODE_CHANNEL_QQBOT_${this.id.toUpperCase()}_APPID`] || ''
18
+ this.clientSecret = config.clientSecret || process.env[`CC_NODE_CHANNEL_QQBOT_${this.id.toUpperCase()}_SECRET`] || ''
19
+
20
+ // 权限策略
21
+ this.dmPolicy = config.dmPolicy || globalConfig.dmPolicy || 'open' // 'open'|'allowlist'|'disabled'
22
+ this.groupPolicy = config.groupPolicy || globalConfig.groupPolicy || 'open'
23
+
24
+ // 白名单(格式: "qqbot:openid" 或 "qqbot:group:group_openid")
25
+ this.allowFrom = config.allowFrom || globalConfig.allowFrom || ['*']
26
+
27
+ // 目标群/用户(可选默认值)
28
+ this.defaultTargets = config.defaultTargets || {}
29
+
30
+ // 高级配置
31
+ this.markdownSupport = config.markdownSupport !== false
32
+ this.streaming = config.streaming || { mode: 'partial' }
33
+
34
+ // 验证
35
+ if (!this.appId || !this.clientSecret) {
36
+ console.warn(`[QQBot] 账户 ${this.id} 未配置 appId/clientSecret`)
37
+ }
38
+ }
39
+
40
+ /** 检查是否允许来自指定源的消息 */
41
+ isAllowed(scope, openid, groupOpenid = null) {
42
+ // 如果允许所有人
43
+ if (this.allowFrom.includes('*')) return true
44
+
45
+ // 检查具体白名单
46
+ const checks = []
47
+ if (scope === 'c2c') {
48
+ checks.push(`qqbot:${openid}`)
49
+ } else if (scope === 'group' && groupOpenid) {
50
+ checks.push(`qqbot:${groupOpenid}`) // 群级别
51
+ checks.push(`qqbot:group:${groupOpenid}:${openid}`) // 用户级别
52
+ }
53
+
54
+ return checks.some(c => this.allowFrom.includes(c))
55
+ }
56
+
57
+ /** 获取目标 ID(优先级:传入 > 默认配置) */
58
+ getTargetId(scope, overrideTargetId = null) {
59
+ if (overrideTargetId) return overrideTargetId
60
+ return this.defaultTargets[scope] || null
61
+ }
62
+ }
63
+
64
+ export class QQBotAccountManager {
65
+ constructor(config = {}) {
66
+ this.accounts = new Map()
67
+ this.defaultAccountId = config.defaultAccount || 'default'
68
+ this.globalConfig = {
69
+ dmPolicy: config.dmPolicy || 'open',
70
+ groupPolicy: config.groupPolicy || 'open',
71
+ allowFrom: config.allowFrom || ['*']
72
+ }
73
+
74
+ // 初始化所有账户
75
+ if (config.accounts) {
76
+ for (const [id, accountConfig] of Object.entries(config.accounts)) {
77
+ this.addAccount(new QQBotAccount(accountConfig, this.globalConfig))
78
+ }
79
+ }
80
+
81
+ // 确保有默认账户
82
+ if (!this.accounts.has(this.defaultAccountId)) {
83
+ this.addAccount(new QQBotAccount({ id: this.defaultAccountId }, this.globalConfig))
84
+ }
85
+ }
86
+
87
+ addAccount(account) {
88
+ this.accounts.set(account.id, account)
89
+ return this
90
+ }
91
+
92
+ getAccount(id = null) {
93
+ const targetId = id || this.defaultAccountId
94
+ return this.accounts.get(targetId)
95
+ }
96
+
97
+ getAllAccounts() {
98
+ return Array.from(this.accounts.values()).filter(a => a.enabled)
99
+ }
100
+
101
+ /** 根据消息上下文选择账户(基于 routing 规则) */
102
+ selectAccount(scope, openid, groupOpenid = null) {
103
+ // 遍历所有账户,找到第一个匹配权限的
104
+ for (const account of this.getAllAccounts()) {
105
+ const policy = scope === 'c2c' ? account.dmPolicy : account.groupPolicy
106
+ if (policy === 'disabled') continue
107
+
108
+ if (account.isAllowed(scope, openid, groupOpenid)) {
109
+ return account
110
+ }
111
+ }
112
+
113
+ // 没匹配到,返回默认账户(如果默认账户开启了的话)
114
+ const defaultAcc = this.getAccount()
115
+ return defaultAcc && defaultAcc.enabled ? defaultAcc : null
116
+ }
117
+ }