@raolin2025/claude-code-node 2.5.2 → 2.6.2
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 +33 -2
- package/package.json +2 -2
- package/src/__tests__/llm-server.test.js +62 -0
- package/src/channel/notify-daemon.js +105 -18
- package/src/channel/qqbot-listener.js +3 -7
- package/src/channel/tg-listener.js +1 -1
- package/src/channel/tg-proxy.js +105 -32
- package/src/core/cli.js +220 -58
- package/src/core/index.js +2 -0
- package/src/core/query-engine.js +40 -9
- package/src/stdio/server.js +426 -0
- package/src/tools/index.js +3 -3
- package/src/tools/telegram-tools.js +319 -0
- package/src/types/index.js +6 -1
- package/src/utils/index.js +1 -0
- package/src/utils/llm-server.js +106 -0
- package/src/tools/qqbot-channel-api.js +0 -147
- package/src/tools/qqbot-media.js +0 -177
- package/src/tools/qqbot-remind.js +0 -42
- package/src/tools/qqbot-tools-wrapper.js +0 -185
package/src/tools/qqbot-media.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ Bot 富媒体工具 — 图片/语音/文件上传与发送
|
|
3
|
-
*
|
|
4
|
-
* 功能:
|
|
5
|
-
* - 验证文件路径(必须在 ~/.openclaw/media/qqbot 或 ~/.openclaw/media)
|
|
6
|
-
* - 自动检测文件类型
|
|
7
|
-
* - 上传并通过 QQ Bot 发送
|
|
8
|
-
*
|
|
9
|
-
* 使用方式:
|
|
10
|
-
* qqbot_media: { action: 'upload', path: '/home/.../image.png', scope: 'group', targetId: '群OPENID' }
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { QQBotEnhanced } from '../channel/qqbot-enhanced.js'
|
|
14
|
-
import { parseQQMediaTags } from '../channel/qqbot-enhanced.js'
|
|
15
|
-
|
|
16
|
-
const API_BASE = 'https://api.sgroup.qq.com'
|
|
17
|
-
|
|
18
|
-
async function getToken(appId, clientSecret) {
|
|
19
|
-
const TOKEN_URL = 'https://bots.qq.com/app/getAppAccessToken'
|
|
20
|
-
const res = await fetch(TOKEN_URL, {
|
|
21
|
-
method: 'POST',
|
|
22
|
-
headers: { 'Content-Type': 'application/json' },
|
|
23
|
-
body: JSON.stringify({ appId, clientSecret }),
|
|
24
|
-
})
|
|
25
|
-
if (!res.ok) throw new Error(`Token API ${res.status}`)
|
|
26
|
-
const data = await res.json()
|
|
27
|
-
if (!data.access_token) throw new Error('No access_token')
|
|
28
|
-
return data.access_token
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function uploadFileToQQ(token, scope, targetId, filePath, fileType) {
|
|
32
|
-
const { readFileSync, existsSync } = await import('fs')
|
|
33
|
-
const { resolve } = await import('path')
|
|
34
|
-
|
|
35
|
-
const absPath = resolve(filePath)
|
|
36
|
-
if (!existsSync(absPath)) {
|
|
37
|
-
throw new Error(`文件不存在: ${filePath}`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 安全检查:必须在 media/qqbot 或 media 目录下
|
|
41
|
-
const allowedDirs = [
|
|
42
|
-
process.env.HOME + '/.openclaw/media/qqbot',
|
|
43
|
-
process.env.HOME + '/.openclaw/media'
|
|
44
|
-
]
|
|
45
|
-
const isAllowed = allowedDirs.some(dir => absPath.startsWith(dir))
|
|
46
|
-
if (!isAllowed) {
|
|
47
|
-
throw new Error(`安全限制: 文件必须在 ~/.openclaw/media/qqbot 或 ~/.openclaw/media 目录下`)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const path = scope === 'group'
|
|
51
|
-
? `/v2/groups/${targetId}/files`
|
|
52
|
-
: `/v2/users/${targetId}/files`
|
|
53
|
-
|
|
54
|
-
const body = {
|
|
55
|
-
file_type: fileType,
|
|
56
|
-
srv_send_msg: false
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 读取文件并转为 base64
|
|
60
|
-
const buf = readFileSync(absPath)
|
|
61
|
-
body.file_data = buf.toString('base64')
|
|
62
|
-
|
|
63
|
-
const res = await fetch(API_BASE + path, {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: {
|
|
66
|
-
'Authorization': `QQBot ${token}`,
|
|
67
|
-
'Content-Type': 'application/json',
|
|
68
|
-
},
|
|
69
|
-
body: JSON.stringify(body),
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
if (!res.ok) {
|
|
73
|
-
const err = await res.text().catch(() => '')
|
|
74
|
-
throw new Error(`上传失败 ${res.status}: ${err.slice(0, 100)}`)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result = await res.json()
|
|
78
|
-
if (!result.file_info) {
|
|
79
|
-
throw new Error(`上传响应异常: ${JSON.stringify(result)}`)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return result.file_info
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function sendMediaMessage(token, scope, targetId, fileInfo) {
|
|
86
|
-
const path = scope === 'group'
|
|
87
|
-
? `/v2/groups/${targetId}/messages`
|
|
88
|
-
: `/v2/users/${targetId}/messages`
|
|
89
|
-
|
|
90
|
-
const body = {
|
|
91
|
-
msg_type: 7, // 媒体消息
|
|
92
|
-
media: { file_info: fileInfo }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const res = await fetch(API_BASE + path, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: {
|
|
98
|
-
'Authorization': `QQBot ${token}`,
|
|
99
|
-
'Content-Type': 'application/json',
|
|
100
|
-
},
|
|
101
|
-
body: JSON.stringify(body),
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
if (!res.ok) {
|
|
105
|
-
const err = await res.text().catch(() => '')
|
|
106
|
-
throw new Error(`发送失败 ${res.status}: ${err.slice(0, 100)}`)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return res.json()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** 工具函数 */
|
|
113
|
-
|
|
114
|
-
async function uploadAndSendMedia(params) {
|
|
115
|
-
const { path: filePath, scope, targetId, appId, clientSecret } = params
|
|
116
|
-
|
|
117
|
-
if (!filePath) throw new Error('path 必填')
|
|
118
|
-
if (!scope || !targetId) throw new Error('scope 和 targetId 必填')
|
|
119
|
-
|
|
120
|
-
// 获取凭证
|
|
121
|
-
const resolvedAppId = appId || process.env.CC_NODE_CHANNEL_QQBOT_APPID || ''
|
|
122
|
-
const resolvedSecret = clientSecret || process.env.CC_NODE_CHANNEL_QQBOT_SECRET || ''
|
|
123
|
-
const token = await getToken(resolvedAppId, resolvedSecret)
|
|
124
|
-
|
|
125
|
-
// 检测文件类型
|
|
126
|
-
const ext = filePath.split('.').pop().toLowerCase()
|
|
127
|
-
let fileType
|
|
128
|
-
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)) fileType = 1
|
|
129
|
-
else if (['mp4', 'mov', 'avi', 'mkv', 'webm'].includes(ext)) fileType = 2
|
|
130
|
-
else if (['silk', 'wav', 'mp3', 'ogg', 'aac', 'flac', 'm4a'].includes(ext)) fileType = 3
|
|
131
|
-
else fileType = 4
|
|
132
|
-
|
|
133
|
-
// 上传
|
|
134
|
-
const fileInfo = await uploadFileToQQ(token, scope, targetId, filePath, fileType)
|
|
135
|
-
|
|
136
|
-
// 发送
|
|
137
|
-
const result = await sendMediaMessage(token, scope, targetId, fileInfo)
|
|
138
|
-
|
|
139
|
-
return { ok: true, fileInfo, result }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** 解析文本中的 <qqmedia> 标签并批量处理 */
|
|
143
|
-
async function processQQMediaText(text, context) {
|
|
144
|
-
const { mediaFiles } = parseQQMediaTags(text)
|
|
145
|
-
const scope = context.scope || 'group'
|
|
146
|
-
const targetId = context.targetId
|
|
147
|
-
|
|
148
|
-
const results = []
|
|
149
|
-
for (const media of mediaFiles) {
|
|
150
|
-
try {
|
|
151
|
-
const result = await uploadAndSendMedia({
|
|
152
|
-
path: media.path,
|
|
153
|
-
scope,
|
|
154
|
-
targetId,
|
|
155
|
-
appId: context.appId,
|
|
156
|
-
clientSecret: context.clientSecret
|
|
157
|
-
})
|
|
158
|
-
results.push({ ok: true, path: media.path, result })
|
|
159
|
-
} catch (e) {
|
|
160
|
-
results.push({ ok: false, path: media.path, error: e.message })
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return results
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 导出
|
|
168
|
-
export const tools = {
|
|
169
|
-
qqbot_media_upload: async (args) => await uploadAndSendMedia(args),
|
|
170
|
-
qqbot_media_process_text: async (args) => await processQQMediaText(args.text, args.context)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export const metadata = {
|
|
174
|
-
name: 'qqbot-media',
|
|
175
|
-
description: 'QQ Bot 富媒体上传工具,支持图片、文件、语音',
|
|
176
|
-
tools: ['qqbot_media_upload', 'qqbot_media_process_text']
|
|
177
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ Bot 定时提醒工具 — 基于 cron 的定时任务(简化版)
|
|
3
|
-
*
|
|
4
|
-
* 注意:此工具依赖主项目的定时任务系统。当前实现为占位符,
|
|
5
|
-
* 实际功能待后续与 cc-node 的调度系统集成后完成。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// 时间解析器
|
|
9
|
-
function parseRelativeTime(timeStr) {
|
|
10
|
-
const match = timeStr.match(/^(\d+)(h|m|s)$/)
|
|
11
|
-
if (!match) return null
|
|
12
|
-
const [, value, unit] = match
|
|
13
|
-
const num = parseInt(value, 10)
|
|
14
|
-
const multipliers = { s: 1000, m: 60000, h: 3600000 }
|
|
15
|
-
return num * multipliers[unit]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 主工具函数 */
|
|
19
|
-
async function qqbotRemind(args) {
|
|
20
|
-
const { action, content, time, targetId, accountId, jobId } = args
|
|
21
|
-
|
|
22
|
-
if (!action || !['add', 'list', 'remove'].includes(action)) {
|
|
23
|
-
return { ok: false, error: 'action 必须为 add/list/remove' }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 占位:说明需要集成调度系统
|
|
27
|
-
return {
|
|
28
|
-
ok: false,
|
|
29
|
-
error: `qqbot_remind 尚未完整集成定时任务系统。当前收到: action=${action}, content=${content}, time=${time}, targetId=${targetId}`
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 导出
|
|
34
|
-
export const tools = {
|
|
35
|
-
qqbotRemind
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const metadata = {
|
|
39
|
-
name: 'qqbot-remind',
|
|
40
|
-
description: 'QQ Bot 定时提醒工具 —— 支持一次性/周期性提醒(待集成调度系统)',
|
|
41
|
-
tools: ['qqbot_remind']
|
|
42
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ Bot 工具包装器 — 将新工具适配到 claude-code-node 工具系统
|
|
3
|
-
*
|
|
4
|
-
* 将以下工具包装为标准 ToolDef 格式:
|
|
5
|
-
* - qqbot_channel_api
|
|
6
|
-
* - qqbot_remind
|
|
7
|
-
* - qqbot_media
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { ToolDef } from '../types/index.js'
|
|
11
|
-
import { tools as qqbotChannelApiTools } from './qqbot-channel-api.js'
|
|
12
|
-
import { tools as qqbotRemindTools } from './qqbot-remind.js'
|
|
13
|
-
import { tools as qqbotMediaTools } from './qqbot-media.js'
|
|
14
|
-
|
|
15
|
-
// 通用执行器:将原始函数包装为 ToolDef 的执行格式
|
|
16
|
-
function createExecutor(originalFunc) {
|
|
17
|
-
return async (input, ctx) => {
|
|
18
|
-
try {
|
|
19
|
-
// 合并上下文配置(如 targetId, scope, accountId)
|
|
20
|
-
const args = { ...input, ...ctx }
|
|
21
|
-
const result = await originalFunc(args)
|
|
22
|
-
// 保持与现有工具一致的返回格式
|
|
23
|
-
if (result.ok) {
|
|
24
|
-
return typeof result.result !== 'undefined' ? result.result : result
|
|
25
|
-
}
|
|
26
|
-
return `[ERROR] ${result.error || 'Unknown error'}`
|
|
27
|
-
} catch (e) {
|
|
28
|
-
return `[ERROR] ${e.message}`
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 导出所有工具(使用 ToolDef 格式)
|
|
34
|
-
export const qqbotTools = [
|
|
35
|
-
// qqbot_channel_api — 通用 API 调用
|
|
36
|
-
new ToolDef(
|
|
37
|
-
'qqbot_channel_api',
|
|
38
|
-
`调用 QQ Bot API v2。自动携带鉴权 Token,无需手动处理。
|
|
39
|
-
使用方法:
|
|
40
|
-
method: HTTP 方法 (GET/POST/PUT/PATCH/DELETE)
|
|
41
|
-
path: API 路径(不含域名),如 /guilds/{guild_id}/channels
|
|
42
|
-
body: 请求体 JSON(POST/PUT/PATCH 使用)
|
|
43
|
-
query: URL 查询参数对象(值必须是字符串)
|
|
44
|
-
|
|
45
|
-
示例:
|
|
46
|
-
- 获取频道列表: { "method": "GET", "path": "/users/@me/guilds", "query": { "limit": "100" } }
|
|
47
|
-
- 获取子频道: { "method": "GET", "path": "/guilds/123/channels" }`,
|
|
48
|
-
{
|
|
49
|
-
type: 'object',
|
|
50
|
-
properties: {
|
|
51
|
-
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], description: 'HTTP 方法' },
|
|
52
|
-
path: { type: 'string', description: 'API 路径(不含域名),如 /guilds/{guild_id}/channels' },
|
|
53
|
-
body: { type: 'object', description: '请求体 JSON(POST/PUT/PATCH 使用)' },
|
|
54
|
-
query: { type: 'object', additionalProperties: { type: 'string' }, description: 'URL 查询参数(值必须是字符串)' }
|
|
55
|
-
},
|
|
56
|
-
required: ['method', 'path']
|
|
57
|
-
},
|
|
58
|
-
createExecutor(qqbotChannelApiTools.qqbot_channel_api)
|
|
59
|
-
),
|
|
60
|
-
|
|
61
|
-
// qqbot_list_guilds — 获取频道列表
|
|
62
|
-
new ToolDef(
|
|
63
|
-
'qqbot_list_guilds',
|
|
64
|
-
'获取机器人所在的频道列表(GUILD 列表)',
|
|
65
|
-
{
|
|
66
|
-
type: 'object',
|
|
67
|
-
properties: {
|
|
68
|
-
limit: { type: 'number', description: '返回数量,最大100' },
|
|
69
|
-
before: { type: 'string', description: '分页游标(上一页最后一条的 id)' },
|
|
70
|
-
after: { type: 'string', description: '分页游标(上一页第一条的 id)' }
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
createExecutor(qqbotChannelApiTools.qqbot_list_guilds)
|
|
74
|
-
),
|
|
75
|
-
|
|
76
|
-
// qqbot_list_channels — 获取子频道列表
|
|
77
|
-
new ToolDef(
|
|
78
|
-
'qqbot_list_channels',
|
|
79
|
-
'获取指定频道的子频道列表',
|
|
80
|
-
{
|
|
81
|
-
type: 'object',
|
|
82
|
-
properties: {
|
|
83
|
-
guildId: { type: 'string', description: '频道 ID' }
|
|
84
|
-
},
|
|
85
|
-
required: ['guildId']
|
|
86
|
-
},
|
|
87
|
-
createExecutor(qqbotChannelApiTools.qqbot_list_channels)
|
|
88
|
-
),
|
|
89
|
-
|
|
90
|
-
// qqbot_get_member — 获取成员详情
|
|
91
|
-
new ToolDef(
|
|
92
|
-
'qqbot_get_member',
|
|
93
|
-
'获取指定成员详情',
|
|
94
|
-
{
|
|
95
|
-
type: 'object',
|
|
96
|
-
properties: {
|
|
97
|
-
guildId: { type: 'string', description: '频道 ID' },
|
|
98
|
-
userId: { type: 'string', description: '用户 ID' }
|
|
99
|
-
},
|
|
100
|
-
required: ['guildId', 'userId']
|
|
101
|
-
},
|
|
102
|
-
createExecutor(qqbotChannelApiTools.qqbot_get_member)
|
|
103
|
-
),
|
|
104
|
-
|
|
105
|
-
// qqbot_list_members — 获取成员列表(分页)
|
|
106
|
-
new ToolDef(
|
|
107
|
-
'qqbot_list_members',
|
|
108
|
-
'获取频道成员列表(分页),首次调用 after=0',
|
|
109
|
-
{
|
|
110
|
-
type: 'object',
|
|
111
|
-
properties: {
|
|
112
|
-
guildId: { type: 'string', description: '频道 ID' },
|
|
113
|
-
limit: { type: 'number', description: '每页数量(1-400)' },
|
|
114
|
-
after: { type: 'string', description: '上一页最后一条的 user.id,首次填 0' }
|
|
115
|
-
},
|
|
116
|
-
required: ['guildId']
|
|
117
|
-
},
|
|
118
|
-
createExecutor(qqbotChannelApiTools.qqbot_list_members)
|
|
119
|
-
),
|
|
120
|
-
|
|
121
|
-
// qqbot_get_channel_online — 获取在线人数
|
|
122
|
-
new ToolDef(
|
|
123
|
-
'qqbot_get_channel_online',
|
|
124
|
-
'获取子频道在线人数',
|
|
125
|
-
{
|
|
126
|
-
type: 'object',
|
|
127
|
-
properties: {
|
|
128
|
-
channelId: { type: 'string', description: '子频道 ID' }
|
|
129
|
-
},
|
|
130
|
-
required: ['channelId']
|
|
131
|
-
},
|
|
132
|
-
createExecutor(qqbotChannelApiTools.qqbot_get_channel_online)
|
|
133
|
-
),
|
|
134
|
-
|
|
135
|
-
// qqbot_remind — 定时提醒
|
|
136
|
-
new ToolDef(
|
|
137
|
-
'qqbot_remind',
|
|
138
|
-
`QQ Bot 定时提醒。支持:
|
|
139
|
-
- 一次性:time = "5m"(5分钟)、"1h30m"(1.5小时)
|
|
140
|
-
- 周期性:time = "0 8 * * *"(每天8点),需设置 tz = "Asia/Shanghai"
|
|
141
|
-
|
|
142
|
-
注意:必须提供 targetId(openid 或 group_openid)和 content。
|
|
143
|
-
|
|
144
|
-
示例:
|
|
145
|
-
{ "action": "add", "content": "喝水", "time": "5m", "targetId": "群OPENID" }`,
|
|
146
|
-
{
|
|
147
|
-
type: 'object',
|
|
148
|
-
properties: {
|
|
149
|
-
action: { type: 'string', enum: ['add', 'list', 'remove'], description: '操作类型' },
|
|
150
|
-
content: { type: 'string', description: '提醒内容' },
|
|
151
|
-
time: { type: 'string', description: '相对时间 (5m, 1h30m) 或 cron 表达式 ("0 8 * * *")' },
|
|
152
|
-
targetId: { type: 'string', description: '目标 openid 或 group_openid' },
|
|
153
|
-
accountId: { type: 'string', description: '使用的 QQ 账户 ID(可选)' },
|
|
154
|
-
jobId: { type: 'string', description: '任务 ID(仅 remove 使用)' }
|
|
155
|
-
},
|
|
156
|
-
required: ['action']
|
|
157
|
-
},
|
|
158
|
-
createExecutor(qqbotRemindTools.qqbotRemind)
|
|
159
|
-
),
|
|
160
|
-
|
|
161
|
-
// qqbot_media_upload — 富媒体上传
|
|
162
|
-
new ToolDef(
|
|
163
|
-
'qqbot_media_upload',
|
|
164
|
-
`上传并发送图片/文件/语音。
|
|
165
|
-
重要:文件必须位于 ~/.openclaw/media/qqbot/ 或 ~/.openclaw/media/ 目录下(安全限制)。
|
|
166
|
-
自动检测文件类型:图片(jpg/png/gif)、视频(mp4/mkv)、语音(mp3/silk)、文件(其他)。
|
|
167
|
-
|
|
168
|
-
示例:
|
|
169
|
-
{ "path": "/home/user/.openclaw/media/qqbot/result.png", "scope": "group", "targetId": "群OPENID" }`,
|
|
170
|
-
{
|
|
171
|
-
type: 'object',
|
|
172
|
-
properties: {
|
|
173
|
-
path: { type: 'string', description: '本地文件绝对路径' },
|
|
174
|
-
scope: { type: 'string', enum: ['c2c', 'group'], description: '发送范围' },
|
|
175
|
-
targetId: { type: 'string', description: '目标 ID' },
|
|
176
|
-
accountId: { type: 'string', description: '使用的 QQ 账户 ID(可选)' },
|
|
177
|
-
appId: { type: 'string', description: 'QQ Bot AppID(可选,默认使用环境变量)' },
|
|
178
|
-
clientSecret: { type: 'string', description: 'QQ Bot ClientSecret(可选)' }
|
|
179
|
-
},
|
|
180
|
-
required: ['path', 'scope', 'targetId']
|
|
181
|
-
},
|
|
182
|
-
createExecutor(qqbotMediaTools.qqbot_media_upload)
|
|
183
|
-
)
|
|
184
|
-
]
|
|
185
|
-
|