@cxyhhhhh/openclaw-qqbot 2.0.0-dev.202607081614 → 2.0.0-dev.202607081621
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/dist/index.cjs +61 -60
- package/dist/index.cjs.map +1 -1
- package/index.ts +2 -2
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -4
- package/skills/qqbot-channel/SKILL.md +2 -2
- package/skills/qqbot-group/SKILL.md +75 -0
- package/skills/qqbot-group/references/api_reference.md +94 -0
- package/src/tools/platform.ts +149 -0
- package/src/tools/channel.ts +0 -148
package/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
|
|
|
12
12
|
|
|
13
13
|
import { qqbotPlugin } from './src/channel.js';
|
|
14
14
|
import { setQQBotRuntime } from './src/runtime.js';
|
|
15
|
-
import {
|
|
15
|
+
import { registerPlatformTool } from './src/tools/platform.js';
|
|
16
16
|
import { registerRemindTool } from './src/tools/remind.js';
|
|
17
17
|
import { verifyRuntimeContract } from './src/runtime-adapter/contract-check.js';
|
|
18
18
|
|
|
@@ -39,7 +39,7 @@ const plugin = {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
api.registerChannel({ plugin: qqbotPlugin as any });
|
|
42
|
-
|
|
42
|
+
registerPlatformTool(api);
|
|
43
43
|
registerRemindTool(api);
|
|
44
44
|
},
|
|
45
45
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
|
|
5
5
|
"channels": ["qqbot"],
|
|
6
6
|
"extensions": ["./preload.cjs"],
|
|
7
|
-
"skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-upgrade"],
|
|
7
|
+
"skills": ["skills/qqbot-channel", "skills/qqbot-group", "skills/qqbot-remind", "skills/qqbot-upgrade"],
|
|
8
8
|
"contracts": {
|
|
9
|
-
"tools": ["
|
|
9
|
+
"tools": ["qqbot_platform_api", "qqbot_remind"]
|
|
10
10
|
},
|
|
11
11
|
"capabilities": {
|
|
12
12
|
"proactiveMessaging": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cxyhhhhh/openclaw-qqbot",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.202607081621",
|
|
4
4
|
"description": "QQ Bot channel plugin for OpenClaw",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -41,9 +41,8 @@
|
|
|
41
41
|
"lint:runtime": "npx tsx src/runtime-adapter/lint-runtime-access.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tencent-connect/qqbot-nodejs": "
|
|
45
|
-
"@tencent-connect/qqbot-connector": "1.2.0"
|
|
46
|
-
"ws": "^8.20.0"
|
|
44
|
+
"@tencent-connect/qqbot-nodejs": "npm:@tencent/qqbot-nodejs@0.0.0-dev.8a6be470",
|
|
45
|
+
"@tencent-connect/qqbot-connector": "1.2.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@types/node": "^20.0.0",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qqbot-channel
|
|
3
|
-
description: QQ 频道管理技能。查询频道列表、子频道、成员、发帖、公告、日程等操作。使用
|
|
3
|
+
description: QQ 频道管理技能。查询频道列表、子频道、成员、发帖、公告、日程等操作。使用 qqbot_platform_api 工具代理 QQ 开放平台 HTTP 接口,自动处理 Token 鉴权。当用户需要查看频道、管理子频道、查询成员、发布帖子/公告/日程时使用此技能。
|
|
4
4
|
metadata: {"openclaw":{"emoji":"📡","requires":{"config":["channels.qqbot"]}}}
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# QQ 频道 API 请求指导
|
|
8
8
|
|
|
9
|
-
`
|
|
9
|
+
`qqbot_platform_api` 是 QQ 开放平台统一 HTTP API 网关,**自动填充鉴权 Token**。你只需要指定 HTTP 方法、API 路径、请求体和查询参数即可调用频道相关接口。
|
|
10
10
|
|
|
11
11
|
## 📚 详细参考文档
|
|
12
12
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qqbot-group
|
|
3
|
+
description: QQ 群管理技能。查询群基础信息、群成员信息、机器人群内状态。使用 qqbot_platform_api 工具调用接口,自动鉴权。当用户需要查询群信息、查看群成员、检查机器人群内状态时使用此技能。
|
|
4
|
+
metadata: {"openclaw":{"emoji":"👥","requires":{"config":["channels.qqbot"]}}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# QQ 群 API 请求指导
|
|
8
|
+
|
|
9
|
+
通过 `qqbot_platform_api` 工具调用群相关接口,鉴权由工具自动处理。
|
|
10
|
+
|
|
11
|
+
详细字段说明见 `references/api_reference.md`。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ⭐ 接口速查
|
|
16
|
+
|
|
17
|
+
| 操作 | 方法 | 路径 |
|
|
18
|
+
|------|------|------|
|
|
19
|
+
| 获取机器人群内状态 | `GET` | `/v2/groups/{group_id}/bot_state` |
|
|
20
|
+
| 获取群成员信息 | `GET` | `/v2/groups/{group_id}/members/{member_id}` |
|
|
21
|
+
| 获取群基础信息 | `GET` | `/v2/groups/{group_id}/info` |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 💡 调用示例
|
|
26
|
+
|
|
27
|
+
### 获取机器人群内状态
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"method": "GET",
|
|
32
|
+
"path": "/v2/groups/G_123456789/bot_state"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
返回字段:`joined_at`(入群时间)、`allow_proactive_msg`(是否允许主动推送)、`recv_msg_setting`(`all`/`only_mention`/`mention_and_context`)、`member_role`(`member`/`owner`/`admin`)。
|
|
37
|
+
|
|
38
|
+
### 获取群成员信息
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"method": "GET",
|
|
43
|
+
"path": "/v2/groups/G_123456789/members/o_xxxxxxxx"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
返回字段:`username`(昵称)、`member_role`、`joined_at`、`bot` 等。昵称仅在单个查询时返回。
|
|
48
|
+
|
|
49
|
+
### 获取群基础信息
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"method": "GET",
|
|
54
|
+
"path": "/v2/groups/G_123456789/info"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
返回字段:`group_name`、`group_finger_memo`(简介)、`group_class_text`(分类)、`group_tags`(标签)、`group_member_num`(成员数)。
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 💬 @群成员
|
|
63
|
+
|
|
64
|
+
发送消息时使用 `<@member_id>` 语法 @指定群成员:
|
|
65
|
+
|
|
66
|
+
- 格式:`<@member_id>`,例如 `<@o_xxxxxxxx>`
|
|
67
|
+
- 文本消息中直接拼接即可:`你好 <@o_xxxxxxxx>,欢迎入群!`
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ⚠️ 注意事项
|
|
72
|
+
|
|
73
|
+
1. 路径占位符 `{group_id}`、`{member_id}` 需替换为实际值
|
|
74
|
+
2. 返回时间格式为 RFC3339(如 `2026-06-23T22:03:09+08:00`)
|
|
75
|
+
3. 群 openid 与频道 guild_id 是不同的概念,不要混淆
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# QQ 开放平台群接口参考
|
|
2
|
+
|
|
3
|
+
> 鉴权和请求头由 `qqbot_platform_api` 工具自动处理,无需手动管理。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 接口列表
|
|
8
|
+
|
|
9
|
+
### 1. 获取机器人群内状态
|
|
10
|
+
|
|
11
|
+
- 方法:`GET`
|
|
12
|
+
- 路径:`/v2/groups/{group_id}/bot_state`
|
|
13
|
+
|
|
14
|
+
响应参数:
|
|
15
|
+
|
|
16
|
+
| 字段 | 类型 | 说明 |
|
|
17
|
+
|------|------|------|
|
|
18
|
+
| joined_at | string | 入群时间(RFC3339 格式) |
|
|
19
|
+
| allow_proactive_msg | bool | 是否允许主动消息推送 |
|
|
20
|
+
| recv_msg_setting | string | 接收消息设置:`all`、`only_mention`、`mention_and_context` |
|
|
21
|
+
| member_role | string | 群成员角色:`member`、`owner`、`admin` |
|
|
22
|
+
|
|
23
|
+
响应示例:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"join_timestamp": "2026-06-23T22:03:09+08:00",
|
|
28
|
+
"allow_proactive_msg": true,
|
|
29
|
+
"recv_msg_setting": "all",
|
|
30
|
+
"member_role": "admin"
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### 2. 获取群成员信息
|
|
37
|
+
|
|
38
|
+
- 方法:`GET`
|
|
39
|
+
- 路径:`/v2/groups/{group_id}/members/{member_id}`
|
|
40
|
+
|
|
41
|
+
响应参数:
|
|
42
|
+
|
|
43
|
+
| 字段 | 类型 | 说明 |
|
|
44
|
+
|------|------|------|
|
|
45
|
+
| union_openid | string | 用户在应用/开放平台下的统一标识 |
|
|
46
|
+
| user_openid | string | 用户 openid |
|
|
47
|
+
| username | string | 用户昵称(仅单个查询时返回) |
|
|
48
|
+
| member_role | string | 群成员角色:`member`、`owner`、`admin` |
|
|
49
|
+
| joined_at | string | 入群时间(RFC3339 格式) |
|
|
50
|
+
| bot | bool | 是否为机器人账号 |
|
|
51
|
+
|
|
52
|
+
响应示例:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"union_openid": "u_xxxxxxxx",
|
|
57
|
+
"user_openid": "o_xxxxxxxx",
|
|
58
|
+
"username": "张三",
|
|
59
|
+
"member_role": "member",
|
|
60
|
+
"joined_at": "2026-06-23T22:03:09+08:00",
|
|
61
|
+
"bot": false
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### 3. 获取群基础信息
|
|
68
|
+
|
|
69
|
+
- 方法:`GET`
|
|
70
|
+
- 路径:`/v2/groups/{group_id}/info`
|
|
71
|
+
|
|
72
|
+
响应参数:
|
|
73
|
+
|
|
74
|
+
| 字段 | 类型 | 说明 |
|
|
75
|
+
|------|------|------|
|
|
76
|
+
| group_openid | string | 群 openid |
|
|
77
|
+
| group_name | string | 群名称 |
|
|
78
|
+
| group_finger_memo | string | 群简介/群公告 |
|
|
79
|
+
| group_class_text | string | 群分类文案 |
|
|
80
|
+
| group_tags | array[] | 群标签列表 |
|
|
81
|
+
| group_member_num | int | 群成员数量 |
|
|
82
|
+
|
|
83
|
+
响应示例:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"group_openid": "G_123456789",
|
|
88
|
+
"group_name": "技术交流群",
|
|
89
|
+
"group_finger_memo": "专注于后端技术开发交流",
|
|
90
|
+
"group_class_text": "技术-后端开发",
|
|
91
|
+
"group_tags": ["技术交流", "后端开发", "QQ机器人"],
|
|
92
|
+
"group_member_num": 1250
|
|
93
|
+
}
|
|
94
|
+
```
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
2
|
+
import { listQQBotAccountIds } from '../config.js';
|
|
3
|
+
import { getBotForAccount } from '../bot-instance.js';
|
|
4
|
+
import { getRequestAccountId } from '../request-context.js';
|
|
5
|
+
|
|
6
|
+
// ========== JSON Schema ==========
|
|
7
|
+
|
|
8
|
+
const PlatformApiSchema = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
method: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description:
|
|
14
|
+
'HTTP 请求方法。可选值:GET, POST, PUT, PATCH, DELETE',
|
|
15
|
+
enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
16
|
+
},
|
|
17
|
+
path: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description:
|
|
20
|
+
'API 路径(不含域名),占位符需替换为实际值。' +
|
|
21
|
+
'示例:/users/@me/guilds, /guilds/{guild_id}/channels, ' +
|
|
22
|
+
'/v2/groups/{group_id}/bot_state',
|
|
23
|
+
},
|
|
24
|
+
body: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
description:
|
|
27
|
+
'请求体(JSON),用于 POST/PUT/PATCH 请求。' +
|
|
28
|
+
'GET/DELETE 请求不需要此参数。',
|
|
29
|
+
},
|
|
30
|
+
query: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
description:
|
|
33
|
+
'URL 查询参数(键值对),会拼接到路径后面。' +
|
|
34
|
+
'如 { "limit": "100", "after": "0" } 会拼接为 ?limit=100&after=0',
|
|
35
|
+
additionalProperties: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['method', 'path'],
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
// ========== 工具函数 ==========
|
|
42
|
+
|
|
43
|
+
function json(data: unknown) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }],
|
|
46
|
+
details: data,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validatePath(path: string): string | null {
|
|
51
|
+
if (!path.startsWith('/')) return "path 必须以 / 开头";
|
|
52
|
+
if (path.includes('..') || path.includes('//')) return "path 不允许包含 .. 或 //";
|
|
53
|
+
if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== '/') {
|
|
54
|
+
return 'path 包含非法字符';
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ========== 注册入口 ==========
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 注册 QQBot 平台统一 API 网关工具。
|
|
63
|
+
* 通过 SDK 的 bot.api 网关代理所有 QQ 开放平台 HTTP 接口 — 自动鉴权、重试、结构化错误。
|
|
64
|
+
*
|
|
65
|
+
* 覆盖范围:
|
|
66
|
+
* - 频道(Guild/Channel/Member/Announce/Forum/Schedule)→ 见 qqbot-channel skill
|
|
67
|
+
* - 群(Group/Member)→ 见 qqbot-group skill
|
|
68
|
+
*/
|
|
69
|
+
export function registerPlatformTool(api: OpenClawPluginApi): void {
|
|
70
|
+
const cfg = api.config;
|
|
71
|
+
if (!cfg) return;
|
|
72
|
+
|
|
73
|
+
const accountIds = listQQBotAccountIds(cfg);
|
|
74
|
+
if (accountIds.length === 0) return;
|
|
75
|
+
|
|
76
|
+
api.registerTool(
|
|
77
|
+
{
|
|
78
|
+
name: 'qqbot_platform_api',
|
|
79
|
+
label: 'QQBot Platform API Gateway',
|
|
80
|
+
description:
|
|
81
|
+
'QQ 开放平台统一 HTTP API 网关,自动填充鉴权 Token。' +
|
|
82
|
+
'常用接口速查:' +
|
|
83
|
+
'【频道】GET /users/@me/guilds | /guilds/{guild_id}/channels | /channels/{channel_id} | ' +
|
|
84
|
+
'【群】GET /v2/groups/{group_id}/bot_state | /v2/groups/{group_id}/members/{member_id} | /v2/groups/{group_id}/info。' +
|
|
85
|
+
'更多接口和参数详情请阅读 qqbot-channel 和 qqbot-group skill。',
|
|
86
|
+
parameters: PlatformApiSchema,
|
|
87
|
+
async execute(_toolCallId, params) {
|
|
88
|
+
const p = params as {
|
|
89
|
+
method: string;
|
|
90
|
+
path: string;
|
|
91
|
+
body?: Record<string, unknown>;
|
|
92
|
+
query?: Record<string, string>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!p.method) return json({ error: 'method 为必填参数' });
|
|
96
|
+
if (!p.path) return json({ error: 'path 为必填参数' });
|
|
97
|
+
|
|
98
|
+
const method = p.method.toUpperCase();
|
|
99
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
100
|
+
return json({ error: `不支持的 HTTP 方法: ${method}` });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pathError = validatePath(p.path);
|
|
104
|
+
if (pathError) return json({ error: pathError });
|
|
105
|
+
|
|
106
|
+
const accountId = getRequestAccountId();
|
|
107
|
+
if (!accountId) {
|
|
108
|
+
return json({ error: '无法获取当前请求的账号信息,此工具仅支持在消息会话中使用' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const bot = getBotForAccount(accountId);
|
|
113
|
+
const apiGateway = bot.api;
|
|
114
|
+
|
|
115
|
+
let data: unknown;
|
|
116
|
+
switch (method) {
|
|
117
|
+
case 'GET':
|
|
118
|
+
data = await apiGateway.get(p.path, p.query);
|
|
119
|
+
break;
|
|
120
|
+
case 'POST':
|
|
121
|
+
data = await apiGateway.post(p.path, p.body);
|
|
122
|
+
break;
|
|
123
|
+
case 'PUT':
|
|
124
|
+
data = await apiGateway.put(p.path, p.body);
|
|
125
|
+
break;
|
|
126
|
+
case 'PATCH':
|
|
127
|
+
data = await apiGateway.patch(p.path, p.body);
|
|
128
|
+
break;
|
|
129
|
+
case 'DELETE':
|
|
130
|
+
data = await apiGateway.delete(p.path);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return json(data);
|
|
135
|
+
} catch (err: unknown) {
|
|
136
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
137
|
+
const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
|
|
138
|
+
return json({
|
|
139
|
+
error: errMsg,
|
|
140
|
+
status: apiErr.httpStatus,
|
|
141
|
+
code: apiErr.bizCode,
|
|
142
|
+
path: p.path,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{ name: 'qqbot_platform_api' },
|
|
148
|
+
);
|
|
149
|
+
}
|
package/src/tools/channel.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { resolveQQBotAccount, listQQBotAccountIds } from "../config.js";
|
|
3
|
-
import { getBotForAccount } from "../bot-instance.js";
|
|
4
|
-
|
|
5
|
-
// ========== JSON Schema ==========
|
|
6
|
-
|
|
7
|
-
const ChannelApiSchema = {
|
|
8
|
-
type: "object",
|
|
9
|
-
properties: {
|
|
10
|
-
method: {
|
|
11
|
-
type: "string",
|
|
12
|
-
description:
|
|
13
|
-
"HTTP 请求方法。可选值:GET, POST, PUT, PATCH, DELETE",
|
|
14
|
-
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
15
|
-
},
|
|
16
|
-
path: {
|
|
17
|
-
type: "string",
|
|
18
|
-
description:
|
|
19
|
-
"API 路径(不含域名),占位符需替换为实际值。" +
|
|
20
|
-
"示例:/users/@me/guilds, /guilds/{guild_id}/channels, /channels/{channel_id}",
|
|
21
|
-
},
|
|
22
|
-
body: {
|
|
23
|
-
type: "object",
|
|
24
|
-
description:
|
|
25
|
-
"请求体(JSON),用于 POST/PUT/PATCH 请求。" +
|
|
26
|
-
"GET/DELETE 请求不需要此参数。",
|
|
27
|
-
},
|
|
28
|
-
query: {
|
|
29
|
-
type: "object",
|
|
30
|
-
description:
|
|
31
|
-
"URL 查询参数(键值对),会拼接到路径后面。" +
|
|
32
|
-
"如 { \"limit\": \"100\", \"after\": \"0\" } 会拼接为 ?limit=100&after=0",
|
|
33
|
-
additionalProperties: { type: "string" },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
required: ["method", "path"],
|
|
37
|
-
} as const;
|
|
38
|
-
|
|
39
|
-
// ========== 工具函数 ==========
|
|
40
|
-
|
|
41
|
-
function json(data: unknown) {
|
|
42
|
-
return {
|
|
43
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
44
|
-
details: data,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function validatePath(path: string): string | null {
|
|
49
|
-
if (!path.startsWith("/")) return "path 必须以 / 开头";
|
|
50
|
-
if (path.includes("..") || path.includes("//")) return "path 不允许包含 .. 或 //";
|
|
51
|
-
if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== "/") {
|
|
52
|
-
return "path 包含非法字符";
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ========== 注册入口 ==========
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 注册 QQ 频道 API 代理工具。
|
|
61
|
-
* 使用 SDK 的 bot.api 网关替代自封装 HTTP — 自动鉴权、重试、结构化错误。
|
|
62
|
-
*/
|
|
63
|
-
export function registerChannelTool(api: OpenClawPluginApi): void {
|
|
64
|
-
const cfg = api.config;
|
|
65
|
-
if (!cfg) return;
|
|
66
|
-
|
|
67
|
-
const accountIds = listQQBotAccountIds(cfg);
|
|
68
|
-
if (accountIds.length === 0) return;
|
|
69
|
-
|
|
70
|
-
const firstAccountId = accountIds[0];
|
|
71
|
-
const account = resolveQQBotAccount(cfg, firstAccountId);
|
|
72
|
-
if (!account.appId || !account.clientSecret) return;
|
|
73
|
-
|
|
74
|
-
api.registerTool(
|
|
75
|
-
{
|
|
76
|
-
name: "qqbot_channel_api",
|
|
77
|
-
label: "QQBot Channel API",
|
|
78
|
-
description:
|
|
79
|
-
"QQ 开放平台频道 API HTTP 代理,自动填充鉴权 Token。" +
|
|
80
|
-
"常用接口速查:" +
|
|
81
|
-
"频道列表 GET /users/@me/guilds | " +
|
|
82
|
-
"子频道列表 GET /guilds/{guild_id}/channels | " +
|
|
83
|
-
"子频道详情 GET /channels/{channel_id} | " +
|
|
84
|
-
"创建子频道 POST /guilds/{guild_id}/channels | " +
|
|
85
|
-
"成员列表 GET /guilds/{guild_id}/members?after=0&limit=100 | " +
|
|
86
|
-
"成员详情 GET /guilds/{guild_id}/members/{user_id} | " +
|
|
87
|
-
"帖子列表 GET /channels/{channel_id}/threads | " +
|
|
88
|
-
"发帖 PUT /channels/{channel_id}/threads | " +
|
|
89
|
-
"创建公告 POST /guilds/{guild_id}/announces | " +
|
|
90
|
-
"创建日程 POST /channels/{channel_id}/schedules。" +
|
|
91
|
-
"更多接口和参数详情请阅读 qqbot-channel skill。",
|
|
92
|
-
parameters: ChannelApiSchema,
|
|
93
|
-
async execute(_toolCallId, params) {
|
|
94
|
-
const p = params as { method: string; path: string; body?: Record<string, unknown>; query?: Record<string, string> };
|
|
95
|
-
|
|
96
|
-
if (!p.method) return json({ error: "method 为必填参数" });
|
|
97
|
-
if (!p.path) return json({ error: "path 为必填参数" });
|
|
98
|
-
|
|
99
|
-
const method = p.method.toUpperCase();
|
|
100
|
-
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
|
101
|
-
return json({ error: `不支持的 HTTP 方法: ${method}` });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const pathError = validatePath(p.path);
|
|
105
|
-
if (pathError) return json({ error: pathError });
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
// 通过 SDK bot.api 网关发送请求(自动鉴权 + 超时 + 结构化错误)
|
|
109
|
-
const bot = getBotForAccount(account.accountId);
|
|
110
|
-
const apiGateway = bot.api;
|
|
111
|
-
|
|
112
|
-
let data: unknown;
|
|
113
|
-
switch (method) {
|
|
114
|
-
case "GET":
|
|
115
|
-
data = await apiGateway.get(p.path, p.query);
|
|
116
|
-
break;
|
|
117
|
-
case "POST":
|
|
118
|
-
data = await apiGateway.post(p.path, p.body);
|
|
119
|
-
break;
|
|
120
|
-
case "PUT":
|
|
121
|
-
data = await apiGateway.put(p.path, p.body);
|
|
122
|
-
break;
|
|
123
|
-
case "PATCH":
|
|
124
|
-
data = await apiGateway.patch(p.path, p.body);
|
|
125
|
-
break;
|
|
126
|
-
case "DELETE":
|
|
127
|
-
data = await apiGateway.delete(p.path);
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return json(data);
|
|
132
|
-
} catch (err: unknown) {
|
|
133
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
134
|
-
// 从 ApiError 提取结构化信息
|
|
135
|
-
const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
|
|
136
|
-
return json({
|
|
137
|
-
error: errMsg,
|
|
138
|
-
status: apiErr.httpStatus,
|
|
139
|
-
code: apiErr.bizCode,
|
|
140
|
-
path: p.path,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
{ name: "qqbot_channel_api" },
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
}
|