@cxyhhhhh/openclaw-qqbot 2.0.0-dev.202607081621 → 2.0.0-dev.202607082155
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 +606 -427
- package/dist/index.cjs.map +1 -1
- package/index.ts +1 -1
- package/package.json +2 -2
- package/skills/qqbot-channel/SKILL.md +33 -11
- package/src/{runtime-adapter/contract-check.ts → adapter/contract.ts} +4 -7
- package/src/{runtime-adapter/gateway-runtime.ts → adapter/gateway.ts} +2 -3
- package/src/{runtime-adapter → adapter}/index.ts +3 -3
- package/src/{runtime-adapter/lint-runtime-access.ts → adapter/lint.ts} +7 -7
- package/src/adapter/media.ts +112 -0
- package/src/adapter/setup.ts +55 -0
- package/src/adapter/workspace.ts +29 -0
- package/src/bot-instance.ts +1 -1
- package/src/channel.ts +1 -1
- package/src/commands/bot-approve.ts +1 -1
- package/src/commands/bot-logs.ts +1 -1
- package/src/commands/bot-pairing.ts +1 -1
- package/src/commands/config-util.ts +1 -1
- package/src/dispatch/body-assembler.ts +1 -1
- package/src/dispatch/dispatch.ts +75 -45
- package/src/features/approval-handler.ts +1 -1
- package/src/gateway/event-handlers.ts +1 -1
- package/src/gateway/lifecycle.ts +1 -1
- package/src/gateway/qqbot-gateway.ts +1 -1
- package/src/middleware/access-control.ts +1 -1
- package/src/middleware/attachment.ts +4 -4
- package/src/openclaw-plugin-sdk.d.ts +41 -0
- package/src/outbound/media-send.ts +4 -6
- package/src/runtime.ts +3 -1
- package/src/setup/finalize.ts +1 -2
- package/src/setup/surface.ts +5 -6
- package/src/utils/pkg-version.ts +100 -49
- package/tsup.config.ts +1 -1
- /package/src/{runtime-adapter/pairing-runtime.ts → adapter/pairing.ts} +0 -0
- /package/src/{runtime-adapter → adapter}/resolve.ts +0 -0
- /package/src/{runtime-adapter/webhook-adapter.ts → adapter/webhook.ts} +0 -0
package/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { qqbotPlugin } from './src/channel.js';
|
|
|
14
14
|
import { setQQBotRuntime } from './src/runtime.js';
|
|
15
15
|
import { registerPlatformTool } from './src/tools/platform.js';
|
|
16
16
|
import { registerRemindTool } from './src/tools/remind.js';
|
|
17
|
-
import { verifyRuntimeContract } from './src/
|
|
17
|
+
import { verifyRuntimeContract } from './src/adapter/contract.js';
|
|
18
18
|
|
|
19
19
|
let registered = false;
|
|
20
20
|
|
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.202607082155",
|
|
4
4
|
"description": "QQ Bot channel plugin for OpenClaw",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"lint:runtime": "npx tsx src/runtime-adapter/lint-runtime-access.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tencent-connect/qqbot-nodejs": "npm:@
|
|
44
|
+
"@tencent-connect/qqbot-nodejs": "npm:@cxyhhhhh/qqbot-nodejs@1.0.2-dev.202607081646",
|
|
45
45
|
"@tencent-connect/qqbot-connector": "1.2.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qqbot-channel
|
|
3
|
-
description: QQ
|
|
3
|
+
description: QQ 频道管理技能(非QQ群)。查询频道列表、子频道、成员、发帖、公告、日程等操作。使用 qqbot_platform_api 工具代理 QQ 开放平台 HTTP 接口,自动处理 Token 鉴权。仅用于 QQ 频道(Guild/Channel)。触发条件:用户提到「频道」「子频道」「guild」「channel」「帖子」「日程」「公告」。
|
|
4
4
|
metadata: {"openclaw":{"emoji":"📡","requires":{"config":["channels.qqbot"]}}}
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# QQ 频道 API 请求指导
|
|
8
8
|
|
|
9
|
+
> **本 Skill 仅处理 QQ 频道(Guild/Channel)操作。** 频道 ID 为纯数字,API 路径为 `/guilds/...` 和 `/channels/...`。
|
|
10
|
+
|
|
9
11
|
`qqbot_platform_api` 是 QQ 开放平台统一 HTTP API 网关,**自动填充鉴权 Token**。你只需要指定 HTTP 方法、API 路径、请求体和查询参数即可调用频道相关接口。
|
|
10
12
|
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🔀 适用范围(必读)
|
|
16
|
+
|
|
17
|
+
QQ 频道是类似 Discord 的社区产品,层级结构为 **频道(Guild) → 子频道(Channel)**:
|
|
18
|
+
|
|
19
|
+
| 层级 | 中文名 | 英文 | API 路径 | ID 格式 |
|
|
20
|
+
|------|--------|------|----------|---------|
|
|
21
|
+
| 顶层 | 频道 | Guild | `/guilds/{guild_id}` | 纯数字(如 `123456`) |
|
|
22
|
+
| 次级 | 子频道 | Channel | `/channels/{channel_id}` | 纯数字(如 `789012`) |
|
|
23
|
+
|
|
24
|
+
**核心特征**:
|
|
25
|
+
- 频道内可以创建多个子频道(文字、语音、论坛、直播等)
|
|
26
|
+
- 支持发帖、公告、日程等功能
|
|
27
|
+
- 频道成员通过 `/guilds/{guild_id}/members` 管理
|
|
28
|
+
- ID 为纯数字格式,API 路径以 `/guilds/` 或 `/channels/` 开头
|
|
29
|
+
|
|
30
|
+
**触发关键词**:频道、子频道、guild、channel、帖子、日程、公告、论坛、频道成员
|
|
31
|
+
|
|
11
32
|
## 📚 详细参考文档
|
|
12
33
|
|
|
13
34
|
每个接口的完整参数说明、返回值结构和枚举值定义:
|
|
@@ -251,13 +272,14 @@ metadata: {"openclaw":{"emoji":"📡","requires":{"config":["channels.qqbot"]}}}
|
|
|
251
272
|
|
|
252
273
|
## ⚠️ 注意事项
|
|
253
274
|
|
|
254
|
-
1.
|
|
255
|
-
2.
|
|
256
|
-
3.
|
|
257
|
-
4.
|
|
258
|
-
5.
|
|
259
|
-
6.
|
|
260
|
-
7.
|
|
261
|
-
8.
|
|
262
|
-
9.
|
|
263
|
-
10.
|
|
275
|
+
1. **适用范围**:本 Skill 仅处理 QQ 频道(Guild/Channel)操作,API 路径以 `/guilds/` 或 `/channels/` 开头,ID 为纯数字
|
|
276
|
+
2. **路径中的占位符**(如 `{guild_id}`、`{channel_id}`)必须替换为实际的纯数字 ID
|
|
277
|
+
3. **query 参数的值必须为字符串类型**,如 `{ "limit": "100" }` 而非 `{ "limit": 100 }`
|
|
278
|
+
4. **成员列表翻页**时可能返回重复成员,需按 `user.id` 去重
|
|
279
|
+
5. **公告**的两种类型(消息公告和推荐子频道公告)会互相顶替
|
|
280
|
+
6. **日程**的时间戳为毫秒级字符串
|
|
281
|
+
7. **删除操作不可逆**,请谨慎使用
|
|
282
|
+
8. **论坛操作**仅私域机器人可用
|
|
283
|
+
9. **子频道分组**(type=4)的 `position` 必须 >= 2
|
|
284
|
+
10. **日程操作**有频率限制:单个管理员每天 10 次,单个频道每天 100 次
|
|
285
|
+
11. **头像/图标展示**:成员 `user.avatar` 和频道 `icon` 等图片 URL 必须使用 Markdown 图片语法 `` 展示,禁止作为纯文本或超链接展示
|
|
@@ -16,13 +16,6 @@ interface ApiProbe {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const REQUIRED: ApiProbe[] = [
|
|
19
|
-
{
|
|
20
|
-
name: 'channel.inbound.run',
|
|
21
|
-
probe: (rt) => {
|
|
22
|
-
const c = (rt as any).channel;
|
|
23
|
-
return typeof c?.inbound?.run === 'function' || typeof c?.turn?.run === 'function';
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
19
|
{
|
|
27
20
|
name: 'channel.reply.dispatchReplyWithBufferedBlockDispatcher',
|
|
28
21
|
probe: (rt) => typeof (rt as any).channel?.reply?.dispatchReplyWithBufferedBlockDispatcher === 'function',
|
|
@@ -30,6 +23,10 @@ const REQUIRED: ApiProbe[] = [
|
|
|
30
23
|
];
|
|
31
24
|
|
|
32
25
|
const OPTIONAL: ApiProbe[] = [
|
|
26
|
+
{ name: 'channel.inbound.run (degraded)', probe: (rt) => {
|
|
27
|
+
const c = (rt as any).channel;
|
|
28
|
+
return typeof c?.inbound?.run === 'function' || typeof c?.turn?.run === 'function';
|
|
29
|
+
}},
|
|
33
30
|
{ name: 'channel.inbound.buildContext', probe: (rt) => typeof (rt as any).channel?.inbound?.buildContext === 'function' },
|
|
34
31
|
{ name: 'channel.reply.formatAgentEnvelope', probe: (rt) => typeof (rt as any).channel?.reply?.formatAgentEnvelope === 'function' },
|
|
35
32
|
{ name: 'channel.text.chunkMarkdownText', probe: (rt) => typeof (rt as any).channel?.text?.chunkMarkdownText === 'function' },
|
|
@@ -17,9 +17,8 @@ export interface ApprovalGatewayClient {
|
|
|
17
17
|
export function loadApprovalGatewayRuntime(): {
|
|
18
18
|
createOperatorApprovalsGatewayClient: (...args: any[]) => Promise<ApprovalGatewayClient>;
|
|
19
19
|
} | null {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const pluginRoot = path.resolve(path.dirname(currentFile), '..', '..');
|
|
20
|
+
const req = createRequire(__filename);
|
|
21
|
+
const pluginRoot = path.resolve(path.dirname(__filename), '..', '..');
|
|
23
22
|
const fs = req('node:fs') as typeof import('node:fs');
|
|
24
23
|
|
|
25
24
|
const tryLoadFromRoot = (root: string) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Runtime Adapter — 隔离 OpenClaw 框架 API 变更对插件核心逻辑的冲击。
|
|
3
3
|
*
|
|
4
4
|
* 所有 `runtime.channel.*` / `runtime.config.*` 访问 **必须** 经由本模块导出的函数。
|
|
5
|
-
* 禁止在
|
|
5
|
+
* 禁止在 adapter/ 外直接使用 `(runtime as any).channel.*`。
|
|
6
6
|
*
|
|
7
7
|
* 设计原则:
|
|
8
8
|
* 1. Capability Probe — 按候选 API 路径探测,兼容多版本框架
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
* 3. 单一 resolve 入口 — 一次 resolve,全程复用缓存
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export { verifyRuntimeContract, type ContractResult } from './contract
|
|
13
|
+
export { verifyRuntimeContract, type ContractResult } from './contract.js';
|
|
14
14
|
export { resolveRuntimeAdapters, getAdapters, type RuntimeAdapters } from './resolve.js';
|
|
15
|
-
export { loadApprovalGatewayRuntime, type ApprovalGatewayClient } from './gateway
|
|
15
|
+
export { loadApprovalGatewayRuntime, type ApprovalGatewayClient } from './gateway.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lint: 检测
|
|
2
|
+
* Lint: 检测 adapter/ 外部直接访问 runtime.channel.* 的代码
|
|
3
3
|
*
|
|
4
4
|
* 运行方式:
|
|
5
|
-
* npx tsx src/
|
|
5
|
+
* npx tsx src/adapter/lint-runtime-access.ts
|
|
6
6
|
*
|
|
7
7
|
* 退出码:
|
|
8
8
|
* 0 = 无违规
|
|
@@ -21,7 +21,7 @@ import * as path from 'node:path';
|
|
|
21
21
|
// ── 配置 ──
|
|
22
22
|
|
|
23
23
|
const SRC_ROOT = path.resolve(import.meta.dirname, '..');
|
|
24
|
-
const ADAPTER_DIR = path.resolve(SRC_ROOT, '
|
|
24
|
+
const ADAPTER_DIR = path.resolve(SRC_ROOT, 'adapter');
|
|
25
25
|
|
|
26
26
|
/** 匹配直接使用 runtime.channel / runtime.config / (runtime as any) 的模式 */
|
|
27
27
|
const VIOLATION_PATTERNS: RegExp[] = [
|
|
@@ -36,7 +36,7 @@ const VIOLATION_PATTERNS: RegExp[] = [
|
|
|
36
36
|
const BYPASS_MARKER = '@adapter-bypass';
|
|
37
37
|
|
|
38
38
|
/** 排除的目录/文件 */
|
|
39
|
-
const EXCLUDE_DIRS = ['
|
|
39
|
+
const EXCLUDE_DIRS = ['adapter', 'node_modules', 'dist', 'tests'];
|
|
40
40
|
const EXCLUDE_FILES = ['types.ts', 'types-augment.d.ts', 'openclaw-plugin-sdk.d.ts'];
|
|
41
41
|
|
|
42
42
|
// ── 扫描逻辑 ──
|
|
@@ -50,7 +50,7 @@ interface Violation {
|
|
|
50
50
|
|
|
51
51
|
function shouldExclude(filePath: string): boolean {
|
|
52
52
|
const relative = path.relative(SRC_ROOT, filePath);
|
|
53
|
-
// 排除
|
|
53
|
+
// 排除 adapter 自身
|
|
54
54
|
if (filePath.startsWith(ADAPTER_DIR)) return true;
|
|
55
55
|
// 排除配置的目录
|
|
56
56
|
for (const dir of EXCLUDE_DIRS) {
|
|
@@ -123,7 +123,7 @@ function main(): void {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
if (allViolations.length === 0) {
|
|
126
|
-
console.log('✅ No direct runtime access found outside
|
|
126
|
+
console.log('✅ No direct runtime access found outside adapter/');
|
|
127
127
|
process.exit(0);
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -132,7 +132,7 @@ function main(): void {
|
|
|
132
132
|
console.error(` ${v.file}:${v.line}`);
|
|
133
133
|
console.error(` ${v.content}`);
|
|
134
134
|
console.error(` → matched: ${v.pattern}`);
|
|
135
|
-
console.error(` → fix: use
|
|
135
|
+
console.error(` → fix: use adapter or add "// @adapter-bypass: <reason>"\n`);
|
|
136
136
|
}
|
|
137
137
|
console.error(
|
|
138
138
|
`\nTo bypass a specific line, add an inline comment:\n` +
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远程媒体下载(动态加载 plugin-sdk/media-runtime,降级到 fetch)
|
|
3
|
+
*
|
|
4
|
+
* 新版本 openclaw 有 openclaw/plugin-sdk/media-runtime(含 SSRF 防护/重试/大小限制),
|
|
5
|
+
* 旧版本不可用时降级到原生 fetch 直连。
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import * as dns from 'node:dns';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as crypto from 'node:crypto';
|
|
12
|
+
import { getQQBotMediaDir } from '../utils/platform.js';
|
|
13
|
+
|
|
14
|
+
const req = createRequire(__filename);
|
|
15
|
+
|
|
16
|
+
type SaveRemoteMedia = (opts: {
|
|
17
|
+
url: string;
|
|
18
|
+
subdir?: string;
|
|
19
|
+
originalFilename?: string;
|
|
20
|
+
maxBytes?: number;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}) => Promise<{ path: string }>;
|
|
23
|
+
|
|
24
|
+
let _save: SaveRemoteMedia | null | undefined;
|
|
25
|
+
|
|
26
|
+
export function downloadRemoteMedia(opts: {
|
|
27
|
+
url: string;
|
|
28
|
+
subdir?: string;
|
|
29
|
+
originalFilename?: string;
|
|
30
|
+
maxBytes?: number;
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}): Promise<{ path: string }> {
|
|
33
|
+
if (_save === undefined) {
|
|
34
|
+
try {
|
|
35
|
+
const mod = req('openclaw/plugin-sdk/media-runtime') as { saveRemoteMedia: SaveRemoteMedia };
|
|
36
|
+
_save = mod.saveRemoteMedia;
|
|
37
|
+
} catch {
|
|
38
|
+
_save = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return _save ? _save(opts) : downloadViaFetch(opts);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── SSRF 防护 ──
|
|
45
|
+
|
|
46
|
+
const PRIVATE_RANGES: Array<[netmask: bigint, prefix: number]> = [
|
|
47
|
+
[0x0A000000n, 8], // 10.0.0.0/8
|
|
48
|
+
[0xAC100000n, 12], // 172.16.0.0/12
|
|
49
|
+
[0xC0A80000n, 16], // 192.168.0.0/16
|
|
50
|
+
[0x7F000000n, 8], // 127.0.0.0/8
|
|
51
|
+
[0xA9FE0000n, 16], // 169.254.0.0/16
|
|
52
|
+
[0xE0000000n, 4], // 224.0.0.0/4 (multicast)
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function ipToBigInt(ip: string): bigint {
|
|
56
|
+
return ip.split('.').reduce((acc, octet) => (acc << 8n) | BigInt(Number(octet)), 0n);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPrivateIP(ip: string): boolean {
|
|
60
|
+
const val = ipToBigInt(ip);
|
|
61
|
+
return PRIVATE_RANGES.some(([mask, prefix]) => (val >> (32n - BigInt(prefix))) === (mask >> (32n - BigInt(prefix))));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function assertSafeHostname(hostname: string): Promise<void> {
|
|
65
|
+
const addresses = await dns.promises.resolve4(hostname).catch(() => []);
|
|
66
|
+
if (addresses.length === 0) throw new Error(`DNS resolution failed: ${hostname}`);
|
|
67
|
+
for (const addr of addresses) {
|
|
68
|
+
if (isPrivateIP(addr)) {
|
|
69
|
+
throw new Error(`SSRF blocked: ${hostname} resolves to private IP ${addr}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── 降级 fetch ──
|
|
75
|
+
|
|
76
|
+
/** 降级:原生 fetch 直连(含 SSRF 防护、大小限制) */
|
|
77
|
+
async function downloadViaFetch(opts: {
|
|
78
|
+
url: string;
|
|
79
|
+
subdir?: string;
|
|
80
|
+
originalFilename?: string;
|
|
81
|
+
maxBytes?: number;
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
}): Promise<{ path: string }> {
|
|
84
|
+
const parsed = new URL(opts.url);
|
|
85
|
+
if (parsed.protocol !== 'https:') {
|
|
86
|
+
throw new Error(`Only HTTPS allowed: ${parsed.protocol}`);
|
|
87
|
+
}
|
|
88
|
+
await assertSafeHostname(parsed.hostname);
|
|
89
|
+
|
|
90
|
+
const dir = getQQBotMediaDir(opts.subdir ?? 'downloads');
|
|
91
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const resp = await fetch(opts.url, {
|
|
94
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 120_000),
|
|
95
|
+
});
|
|
96
|
+
if (!resp.ok) throw new Error(`Download HTTP ${resp.status}`);
|
|
97
|
+
|
|
98
|
+
const maxBytes = opts.maxBytes ?? 500 * 1024 * 1024;
|
|
99
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
100
|
+
if (buf.length > maxBytes) throw new Error(`Download exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB`);
|
|
101
|
+
|
|
102
|
+
const ext = opts.originalFilename
|
|
103
|
+
? path.extname(opts.originalFilename) || '.bin'
|
|
104
|
+
: '.bin';
|
|
105
|
+
const name = opts.originalFilename
|
|
106
|
+
? path.basename(opts.originalFilename, path.extname(opts.originalFilename))
|
|
107
|
+
: 'download';
|
|
108
|
+
const rand = crypto.randomBytes(4).toString('hex');
|
|
109
|
+
const filePath = path.join(dir, `${name}_${Date.now()}_${rand}${ext}`);
|
|
110
|
+
fs.writeFileSync(filePath, buf);
|
|
111
|
+
return { path: filePath };
|
|
112
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup 工具动态加载(plugin-sdk/setup + plugin-sdk/setup-tools)
|
|
3
|
+
*
|
|
4
|
+
* 新版本 openclaw 有这些导出,旧版本可能缺失。
|
|
5
|
+
* 不可用时提供最小降级实现。
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
|
|
9
|
+
const req = createRequire(__filename);
|
|
10
|
+
|
|
11
|
+
let _setup: typeof import('openclaw/plugin-sdk/setup') | null | undefined;
|
|
12
|
+
let _tools: typeof import('openclaw/plugin-sdk/setup-tools') | null | undefined;
|
|
13
|
+
|
|
14
|
+
function loadSetup() {
|
|
15
|
+
if (_setup !== undefined) return _setup;
|
|
16
|
+
try { _setup = req('openclaw/plugin-sdk/setup'); } catch { _setup = null; }
|
|
17
|
+
return _setup;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadTools() {
|
|
21
|
+
if (_tools !== undefined) return _tools;
|
|
22
|
+
try { _tools = req('openclaw/plugin-sdk/setup-tools'); } catch { _tools = null; }
|
|
23
|
+
return _tools;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type { ChannelSetupWizard } from 'openclaw/plugin-sdk/setup';
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_ACCOUNT_ID = 'default';
|
|
29
|
+
|
|
30
|
+
export function createStandardChannelSetupStatus(
|
|
31
|
+
...args: Parameters<NonNullable<typeof _setup>['createStandardChannelSetupStatus']>
|
|
32
|
+
): ReturnType<NonNullable<typeof _setup>['createStandardChannelSetupStatus']> {
|
|
33
|
+
const mod = loadSetup();
|
|
34
|
+
if (mod) return mod.createStandardChannelSetupStatus(...args);
|
|
35
|
+
return {
|
|
36
|
+
channelLabel: args[0]?.channelLabel ?? 'QQ Bot',
|
|
37
|
+
configuredLabel: 'Configured',
|
|
38
|
+
unconfiguredLabel: 'Not configured',
|
|
39
|
+
resolveConfigured: () => false,
|
|
40
|
+
} as unknown as ReturnType<NonNullable<typeof _setup>['createStandardChannelSetupStatus']>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setSetupChannelEnabled(
|
|
44
|
+
...args: Parameters<NonNullable<typeof _setup>['setSetupChannelEnabled']>
|
|
45
|
+
): void {
|
|
46
|
+
loadSetup()?.setSetupChannelEnabled?.(...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatDocsLink(
|
|
50
|
+
...args: Parameters<NonNullable<typeof _tools>['formatDocsLink']>
|
|
51
|
+
): ReturnType<NonNullable<typeof _tools>['formatDocsLink']> {
|
|
52
|
+
const mod = loadTools();
|
|
53
|
+
if (mod) return mod.formatDocsLink(...args);
|
|
54
|
+
return (args[1] ? `${args[1]}: ${args[0]}` : args[0]) as ReturnType<NonNullable<typeof _tools>['formatDocsLink']>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent workspace 目录解析(动态加载 plugin-sdk/health)
|
|
3
|
+
*
|
|
4
|
+
* 新版本 openclaw 有 openclaw/plugin-sdk/health 导出,
|
|
5
|
+
* 旧版本不可用时 fallback 到 QQ Bot media 目录。
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { getQQBotMediaDir } from '../utils/platform.js';
|
|
9
|
+
|
|
10
|
+
const req = createRequire(__filename);
|
|
11
|
+
|
|
12
|
+
let health: {
|
|
13
|
+
resolveAgentWorkspaceDir: (cfg: any, agentId: string) => string;
|
|
14
|
+
resolveDefaultAgentId: (cfg: any) => string;
|
|
15
|
+
} | null = null;
|
|
16
|
+
|
|
17
|
+
export function resolveAgentWorkspace(cfg: any, agentId?: string): string {
|
|
18
|
+
if (!health) {
|
|
19
|
+
try {
|
|
20
|
+
health = req('openclaw/plugin-sdk/health') as typeof health;
|
|
21
|
+
} catch {
|
|
22
|
+
health = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (health) {
|
|
26
|
+
return health.resolveAgentWorkspaceDir(cfg, agentId ?? health.resolveDefaultAgentId(cfg));
|
|
27
|
+
}
|
|
28
|
+
return getQQBotMediaDir();
|
|
29
|
+
}
|
package/src/bot-instance.ts
CHANGED
|
@@ -15,7 +15,7 @@ const PLUGIN_VERSION = getPackageVersion();
|
|
|
15
15
|
let _openclawVersion = 'unknown';
|
|
16
16
|
|
|
17
17
|
export function setOpenClawVersion(version: string): void {
|
|
18
|
-
_openclawVersion = version;
|
|
18
|
+
if (version) _openclawVersion = version;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function getOpenClawVersion(): string {
|
package/src/channel.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
resolveGroupConfig,
|
|
25
25
|
} from './config.js';
|
|
26
26
|
import { getQQBotRuntime, tryGetQQBotRuntime } from './runtime.js';
|
|
27
|
-
import { getAdapters } from './
|
|
27
|
+
import { getAdapters } from './adapter/resolve.js';
|
|
28
28
|
import { sendText, getGateway } from './outbound/outbound-service.js';
|
|
29
29
|
import { sendMedia } from './outbound/media-send.js';
|
|
30
30
|
import type { PluginLogger } from './utils/plugin-logger.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
|
|
2
2
|
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
3
|
-
import { getAdapters } from '../
|
|
3
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
4
4
|
import { updateGlobalConfig, checkCommandAuth } from './config-util.js';
|
|
5
5
|
|
|
6
6
|
/** 审批预设配置 */
|
package/src/commands/bot-logs.ts
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import { getQQBotMediaDir } from '../utils/platform.js';
|
|
6
6
|
import { checkCommandAuth } from './config-util.js';
|
|
7
7
|
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
8
|
-
import { getAdapters } from '../
|
|
8
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
const MAX_LINES_PER_FILE = 1000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
|
|
2
2
|
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
3
|
-
import { getPairingApi } from '../
|
|
3
|
+
import { getPairingApi } from '../adapter/pairing.js';
|
|
4
4
|
import { checkCommandAuth } from './config-util.js';
|
|
5
5
|
|
|
6
6
|
/** /bot-pairing — DM 配对审批管理 */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ResolvedQQBotAccount } from '../types.js';
|
|
2
2
|
import type { SlashCommandHandlerContext } from '@tencent-connect/qqbot-nodejs';
|
|
3
3
|
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
4
|
-
import { getAdapters } from '../
|
|
4
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 命令授权检查(用于 SlashCommand.authorized 回调)。
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
} from '@tencent-connect/qqbot-nodejs';
|
|
25
25
|
import type { ResolvedQQBotAccount } from '../types.js';
|
|
26
26
|
import type { ProcessedAttachments } from '../middleware/attachment.js';
|
|
27
|
-
import { getAdapters } from '../
|
|
27
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
28
28
|
|
|
29
29
|
// ── 协议常量 ─────────────────────────────
|
|
30
30
|
const QUOTE_BEGIN = '[Quoted message begins]';
|
package/src/dispatch/dispatch.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { deliverReply, type DeliverPayload, type DeliverInfo, type DeliverContex
|
|
|
21
21
|
|
|
22
22
|
import { DeliverDebouncer } from '../outbound/debounce.js';
|
|
23
23
|
import { StreamingController, shouldUseStreaming } from '../outbound/streaming-controller.js';
|
|
24
|
-
import { getAdapters } from '../
|
|
24
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
25
25
|
import { clearGroupHistory } from '../features/history-store.js';
|
|
26
26
|
|
|
27
27
|
|
|
@@ -41,11 +41,6 @@ export async function dispatchToOpenClaw(
|
|
|
41
41
|
|
|
42
42
|
dlog?.debug(`received sender=${envelope.senderId} scope=${envelope.chatScope} msgId=${envelope.messageId}`);
|
|
43
43
|
|
|
44
|
-
if (!adapters.inboundRun) {
|
|
45
|
-
dlog?.error(`runtime adapter inboundRun not available (openclaw=${adapters.version})`);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
44
|
if (!adapters.dispatchReply) {
|
|
50
45
|
dlog?.error(`runtime adapter dispatchReply not available (openclaw=${adapters.version})`);
|
|
51
46
|
return;
|
|
@@ -162,46 +157,80 @@ export async function dispatchToOpenClaw(
|
|
|
162
157
|
dlog?.debug(`streaming enabled for ${envelope.senderId}`);
|
|
163
158
|
}
|
|
164
159
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ctxPayload,
|
|
183
|
-
recordInboundSession: adapters.recordInboundSession,
|
|
184
|
-
record: {
|
|
185
|
-
onRecordError: (err: unknown) => {
|
|
186
|
-
dlog?.error(`Session record error: ${err}`);
|
|
187
|
-
},
|
|
160
|
+
if (!adapters.inboundRun) {
|
|
161
|
+
// 低版本:手动 session + dispatchReply 直调
|
|
162
|
+
if (adapters.recordInboundSession) {
|
|
163
|
+
try {
|
|
164
|
+
await adapters.recordInboundSession({
|
|
165
|
+
storePath,
|
|
166
|
+
sessionKey: route.sessionKey,
|
|
167
|
+
ctx: ctxPayload,
|
|
168
|
+
});
|
|
169
|
+
} catch { /* best-effort */ }
|
|
170
|
+
}
|
|
171
|
+
await adapters.dispatchReply!({
|
|
172
|
+
ctx: ctxPayload,
|
|
173
|
+
cfg,
|
|
174
|
+
dispatcherOptions: {
|
|
175
|
+
deliver: async (payload: DeliverPayload, info?: DeliverInfo) => {
|
|
176
|
+
await deliverReply(payload, info, deliverCtx);
|
|
188
177
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
178
|
+
},
|
|
179
|
+
...(streamingController
|
|
180
|
+
? {
|
|
181
|
+
replyOptions: {
|
|
182
|
+
onPartialReply: async (p: { text?: string }) => {
|
|
183
|
+
if (p.text) await streamingController.onPartialReply(p.text);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
: {}),
|
|
188
|
+
});
|
|
189
|
+
if (streamingController && !streamingController.isTerminal) {
|
|
190
|
+
await streamingController.finalize();
|
|
191
|
+
}
|
|
192
|
+
if (debouncer) await debouncer.flushAll();
|
|
193
|
+
} else {
|
|
194
|
+
await adapters.inboundRun!({
|
|
195
|
+
channel: 'qqbot',
|
|
196
|
+
accountId: route.accountId,
|
|
197
|
+
raw: envelope,
|
|
198
|
+
adapter: {
|
|
199
|
+
ingest: (raw: any) => ({
|
|
200
|
+
id: envelope.messageId,
|
|
201
|
+
rawText: assembled.rawBody,
|
|
202
|
+
textForAgent: assembled.agentBody,
|
|
203
|
+
textForCommands: assembled.rawBody,
|
|
204
|
+
raw,
|
|
205
|
+
}),
|
|
206
|
+
resolveTurn: (_input: unknown, _eventClass: unknown, _preflight: unknown) => ({
|
|
207
|
+
channel: 'qqbot',
|
|
208
|
+
accountId: route.accountId,
|
|
209
|
+
routeSessionKey: route.sessionKey,
|
|
210
|
+
storePath,
|
|
211
|
+
ctxPayload,
|
|
212
|
+
recordInboundSession: adapters.recordInboundSession,
|
|
213
|
+
record: {
|
|
214
|
+
onRecordError: (err: unknown) => {
|
|
215
|
+
dlog?.error(`Session record error: ${err}`);
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
runDispatch: () => {
|
|
219
|
+
let blockDelivered = false;
|
|
220
|
+
const deliveredMediaUrls = new Set<string>();
|
|
221
|
+
return adapters.dispatchReply!({
|
|
222
|
+
ctx: ctxPayload,
|
|
223
|
+
cfg,
|
|
224
|
+
dispatcherOptions: {
|
|
225
|
+
deliver: async (payload: DeliverPayload, info?: DeliverInfo) => {
|
|
226
|
+
try {
|
|
227
|
+
const kind = (info as any)?.kind as string | undefined;
|
|
228
|
+
dlog?.debug(`deliver kind=${kind ?? 'none'} textLen=${payload.text?.length ?? 0} audioAsVoice=${payload.audioAsVoice ?? false} mediaUrl=${payload.mediaUrl?.slice(0, 60) ?? 'none'} mediaUrls=${payload.mediaUrls?.length ?? 0}`);
|
|
200
229
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
// ── block 带音频/媒体时优先发送(不发送文本,留流式处理) ──
|
|
231
|
+
if (kind === 'block') {
|
|
232
|
+
if (payload.audioAsVoice) {
|
|
233
|
+
// TTS 语音:走完整 deliverReply(voice intent 消费文本)
|
|
205
234
|
await deliverReply(payload, info, deliverCtx);
|
|
206
235
|
} else {
|
|
207
236
|
// 普通媒体(图片等):只发媒体,文本留给流式
|
|
@@ -296,7 +325,8 @@ export async function dispatchToOpenClaw(
|
|
|
296
325
|
}),
|
|
297
326
|
},
|
|
298
327
|
});
|
|
299
|
-
|
|
328
|
+
}
|
|
329
|
+
|
|
300
330
|
dlog?.debug(`inboundRun completed sessionKey=${route.sessionKey}`);
|
|
301
331
|
|
|
302
332
|
// 群消息回复后清空历史缓存(避免下次 @ 时重复组包)
|
|
@@ -14,7 +14,7 @@ import { getBotForAccount } from "../bot-instance.js";
|
|
|
14
14
|
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
15
15
|
import type { InlineKeyboard, KeyboardButton } from "../types.js";
|
|
16
16
|
|
|
17
|
-
import { loadApprovalGatewayRuntime, type ApprovalGatewayClient } from '../
|
|
17
|
+
import { loadApprovalGatewayRuntime, type ApprovalGatewayClient } from '../adapter/gateway.js';
|
|
18
18
|
type EventFrame = {
|
|
19
19
|
event: string;
|
|
20
20
|
payload: unknown;
|
|
@@ -15,7 +15,7 @@ import { runWithRequestContext } from '../request-context.js';
|
|
|
15
15
|
import { getApprovalHandler } from '../features/approval-handler.js';
|
|
16
16
|
import { recordKnownUser } from '../features/proactive.js';
|
|
17
17
|
import { cacheMsgId } from '../features/msgid-cache.js';
|
|
18
|
-
import { getAdapters } from '../
|
|
18
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
19
19
|
import { resolveGroupConfigFromAccount, resolveGroupPolicy, resolveMentionPatterns } from '../config.js';
|
|
20
20
|
import { getPackageVersion } from '../utils/pkg-version.js';
|
|
21
21
|
import { getOpenClawVersion } from '../bot-instance.js';
|
package/src/gateway/lifecycle.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { OpenClawConfig } from 'openclaw/plugin-sdk/core';
|
|
|
11
11
|
import type { ResolvedQQBotAccount } from '../types.js';
|
|
12
12
|
import { DEFAULT_ACCOUNT_ID, resolveQQBotAccount, applyQQBotAccountConfig } from '../config.js';
|
|
13
13
|
import { getQQBotRuntime } from '../runtime.js';
|
|
14
|
-
import { getAdapters } from '../
|
|
14
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
15
15
|
import { QQBotGateway } from './qqbot-gateway.js';
|
|
16
16
|
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
17
17
|
import type { PluginLogger } from '../utils/plugin-logger.js';
|