@ryantest/openclaw-qqbot 0.0.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/LICENSE +22 -0
- package/README.md +483 -0
- package/README.zh.md +478 -0
- package/bin/qqbot-cli.js +243 -0
- package/clawdbot.plugin.json +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +26 -0
- package/dist/src/admin-resolver.d.ts +27 -0
- package/dist/src/admin-resolver.js +122 -0
- package/dist/src/api.d.ts +156 -0
- package/dist/src/api.js +599 -0
- package/dist/src/channel.d.ts +11 -0
- package/dist/src/channel.js +354 -0
- package/dist/src/config.d.ts +25 -0
- package/dist/src/config.js +161 -0
- package/dist/src/credential-backup.d.ts +31 -0
- package/dist/src/credential-backup.js +66 -0
- package/dist/src/gateway.d.ts +18 -0
- package/dist/src/gateway.js +1265 -0
- package/dist/src/image-server.d.ts +68 -0
- package/dist/src/image-server.js +462 -0
- package/dist/src/inbound-attachments.d.ts +58 -0
- package/dist/src/inbound-attachments.js +234 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +263 -0
- package/dist/src/message-queue.d.ts +50 -0
- package/dist/src/message-queue.js +115 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +203 -0
- package/dist/src/outbound-deliver.d.ts +48 -0
- package/dist/src/outbound-deliver.js +462 -0
- package/dist/src/outbound.d.ts +203 -0
- package/dist/src/outbound.js +1102 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +399 -0
- package/dist/src/ref-index-store.d.ts +70 -0
- package/dist/src/ref-index-store.js +273 -0
- package/dist/src/reply-dispatcher.d.ts +35 -0
- package/dist/src/reply-dispatcher.js +311 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +52 -0
- package/dist/src/session-store.js +254 -0
- package/dist/src/slash-commands.d.ts +71 -0
- package/dist/src/slash-commands.js +1179 -0
- package/dist/src/startup-greeting.d.ts +30 -0
- package/dist/src/startup-greeting.js +78 -0
- package/dist/src/stt.d.ts +21 -0
- package/dist/src/stt.js +70 -0
- package/dist/src/tools/channel.d.ts +16 -0
- package/dist/src/tools/channel.js +234 -0
- package/dist/src/tools/remind.d.ts +2 -0
- package/dist/src/tools/remind.js +247 -0
- package/dist/src/types.d.ts +175 -0
- package/dist/src/types.js +1 -0
- package/dist/src/typing-keepalive.d.ts +27 -0
- package/dist/src/typing-keepalive.js +64 -0
- package/dist/src/update-checker.d.ts +34 -0
- package/dist/src/update-checker.js +166 -0
- package/dist/src/user-messages.d.ts +8 -0
- package/dist/src/user-messages.js +8 -0
- package/dist/src/utils/audio-convert.d.ts +89 -0
- package/dist/src/utils/audio-convert.js +704 -0
- package/dist/src/utils/file-utils.d.ts +55 -0
- package/dist/src/utils/file-utils.js +150 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/media-tags.d.ts +14 -0
- package/dist/src/utils/media-tags.js +164 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/dist/src/utils/platform.d.ts +137 -0
- package/dist/src/utils/platform.js +390 -0
- package/dist/src/utils/text-parsing.d.ts +32 -0
- package/dist/src/utils/text-parsing.js +80 -0
- package/dist/src/utils/upload-cache.d.ts +34 -0
- package/dist/src/utils/upload-cache.js +93 -0
- package/index.ts +31 -0
- package/moltbot.plugin.json +16 -0
- package/node_modules/@eshaz/web-worker/LICENSE +201 -0
- package/node_modules/@eshaz/web-worker/README.md +134 -0
- package/node_modules/@eshaz/web-worker/browser.js +17 -0
- package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
- package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
- package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
- package/node_modules/@eshaz/web-worker/node.js +223 -0
- package/node_modules/@eshaz/web-worker/package.json +54 -0
- package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
- package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
- package/node_modules/mpg123-decoder/README.md +265 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
- package/node_modules/mpg123-decoder/index.js +8 -0
- package/node_modules/mpg123-decoder/package.json +58 -0
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
- package/node_modules/mpg123-decoder/types.d.ts +30 -0
- package/node_modules/silk-wasm/LICENSE +21 -0
- package/node_modules/silk-wasm/README.md +85 -0
- package/node_modules/silk-wasm/lib/index.cjs +16 -0
- package/node_modules/silk-wasm/lib/index.d.ts +70 -0
- package/node_modules/silk-wasm/lib/index.mjs +16 -0
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
- package/node_modules/silk-wasm/package.json +39 -0
- package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
- package/node_modules/simple-yenc/.prettierignore +1 -0
- package/node_modules/simple-yenc/LICENSE +7 -0
- package/node_modules/simple-yenc/README.md +163 -0
- package/node_modules/simple-yenc/dist/esm.js +1 -0
- package/node_modules/simple-yenc/dist/index.js +1 -0
- package/node_modules/simple-yenc/package.json +50 -0
- package/node_modules/simple-yenc/rollup.config.js +27 -0
- package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +13 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +69 -0
- package/node_modules/ws/wrapper.mjs +8 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +76 -0
- package/scripts/cleanup-legacy-plugins.sh +124 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-alt-pkg.sh +307 -0
- package/scripts/upgrade-via-npm.ps1 +296 -0
- package/scripts/upgrade-via-npm.sh +301 -0
- package/scripts/upgrade-via-source.sh +774 -0
- package/skills/qqbot-channel/SKILL.md +263 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +56 -0
- package/skills/qqbot-remind/SKILL.md +149 -0
- package/src/admin-resolver.ts +140 -0
- package/src/api.ts +819 -0
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +46 -0
- package/src/channel.ts +381 -0
- package/src/config.ts +187 -0
- package/src/credential-backup.ts +72 -0
- package/src/gateway.log +43 -0
- package/src/gateway.ts +1404 -0
- package/src/image-server.ts +539 -0
- package/src/inbound-attachments.ts +304 -0
- package/src/known-users.ts +353 -0
- package/src/message-queue.ts +169 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-2026-03-21.log +3729 -0
- package/src/openclaw-plugin-sdk.d.ts +522 -0
- package/src/outbound-deliver.ts +552 -0
- package/src/outbound.ts +1266 -0
- package/src/proactive.ts +530 -0
- package/src/ref-index-store.ts +357 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/slash-commands.ts +1305 -0
- package/src/startup-greeting.ts +98 -0
- package/src/stt.ts +86 -0
- package/src/tools/channel.ts +281 -0
- package/src/tools/remind.ts +296 -0
- package/src/types.ts +183 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/update-checker.ts +179 -0
- package/src/user-messages.ts +7 -0
- package/src/utils/audio-convert.ts +803 -0
- package/src/utils/file-utils.ts +167 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-tags.ts +182 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/platform.ts +435 -0
- package/src/utils/text-parsing.ts +82 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台兼容工具
|
|
3
|
+
*
|
|
4
|
+
* 统一 Mac / Linux / Windows 三大系统的:
|
|
5
|
+
* - 用户主目录获取
|
|
6
|
+
* - 临时目录获取
|
|
7
|
+
* - 本地路径判断
|
|
8
|
+
* - ffmpeg / ffprobe 可执行文件路径
|
|
9
|
+
* - silk-wasm 原生模块兼容性检测
|
|
10
|
+
* - 启动诊断报告
|
|
11
|
+
*/
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
export function getPlatform() {
|
|
17
|
+
const p = process.platform;
|
|
18
|
+
if (p === "darwin" || p === "linux" || p === "win32")
|
|
19
|
+
return p;
|
|
20
|
+
return "other";
|
|
21
|
+
}
|
|
22
|
+
export function isWindows() {
|
|
23
|
+
return process.platform === "win32";
|
|
24
|
+
}
|
|
25
|
+
// ============ 用户主目录 ============
|
|
26
|
+
/**
|
|
27
|
+
* 安全获取用户主目录
|
|
28
|
+
*
|
|
29
|
+
* 优先级:
|
|
30
|
+
* 1. os.homedir()(Node 原生,所有平台)
|
|
31
|
+
* 2. $HOME(Mac/Linux)或 %USERPROFILE%(Windows)
|
|
32
|
+
* 3. 降级到 /tmp(Linux/Mac)或 os.tmpdir()(Windows)
|
|
33
|
+
*
|
|
34
|
+
* 与之前 `process.env.HOME || "/home/ubuntu"` 的硬编码相比,
|
|
35
|
+
* 现在能正确处理 Windows 和非 ubuntu 用户。
|
|
36
|
+
*/
|
|
37
|
+
export function getHomeDir() {
|
|
38
|
+
try {
|
|
39
|
+
const home = os.homedir();
|
|
40
|
+
if (home && fs.existsSync(home))
|
|
41
|
+
return home;
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
// fallback 环境变量
|
|
45
|
+
const envHome = process.env.HOME || process.env.USERPROFILE;
|
|
46
|
+
if (envHome && fs.existsSync(envHome))
|
|
47
|
+
return envHome;
|
|
48
|
+
// 最后降级
|
|
49
|
+
return os.tmpdir();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 获取 .openclaw/qqbot 下的子目录路径,并自动创建
|
|
53
|
+
* 替代各文件中分散的 path.join(HOME, ".openclaw", "qqbot", ...)
|
|
54
|
+
*/
|
|
55
|
+
export function getQQBotDataDir(...subPaths) {
|
|
56
|
+
const dir = path.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 获取 .openclaw/media/qqbot 下的子目录路径,并自动创建
|
|
64
|
+
*
|
|
65
|
+
* 与 getQQBotDataDir 不同,此目录位于 OpenClaw 核心的媒体安全白名单
|
|
66
|
+
* (~/.openclaw/media) 之下,下载到这里的文件可以被框架的 image/media
|
|
67
|
+
* 工具直接访问,不会触发 "Local media path is not under an allowed directory" 错误。
|
|
68
|
+
*
|
|
69
|
+
* 用于存放从 QQ 下载的图片、语音等需要被框架处理的媒体文件。
|
|
70
|
+
*/
|
|
71
|
+
export function getQQBotMediaDir(...subPaths) {
|
|
72
|
+
const dir = path.join(getHomeDir(), ".openclaw", "media", "qqbot", ...subPaths);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
return dir;
|
|
77
|
+
}
|
|
78
|
+
// ============ 临时目录 ============
|
|
79
|
+
/**
|
|
80
|
+
* 获取系统临时目录(跨平台安全)
|
|
81
|
+
* Mac: /var/folders/... 或 /tmp
|
|
82
|
+
* Linux: /tmp
|
|
83
|
+
* Windows: %TEMP% 或 C:\Users\xxx\AppData\Local\Temp
|
|
84
|
+
*/
|
|
85
|
+
export function getTempDir() {
|
|
86
|
+
return os.tmpdir();
|
|
87
|
+
}
|
|
88
|
+
// ============ 波浪线路径展开 ============
|
|
89
|
+
/**
|
|
90
|
+
* 展开路径中的波浪线(~)为用户主目录
|
|
91
|
+
*
|
|
92
|
+
* Mac/Linux 用户经常使用 `~/Desktop/file.png` 这样的路径,
|
|
93
|
+
* 但 Node.js 的 fs 模块不会像 shell 一样自动展开 `~`。
|
|
94
|
+
*
|
|
95
|
+
* 支持:
|
|
96
|
+
* - `~/xxx` → `/Users/you/xxx`(Mac)或 `/home/you/xxx`(Linux)
|
|
97
|
+
* - `~` → `/Users/you`
|
|
98
|
+
* - 非 `~` 开头的路径原样返回
|
|
99
|
+
*
|
|
100
|
+
* 注意: 不支持 `~otheruser/xxx` 语法(极少使用,且需要系统调用获取其他用户信息)
|
|
101
|
+
*/
|
|
102
|
+
export function expandTilde(p) {
|
|
103
|
+
if (!p)
|
|
104
|
+
return p;
|
|
105
|
+
if (p === "~")
|
|
106
|
+
return getHomeDir();
|
|
107
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) {
|
|
108
|
+
return path.join(getHomeDir(), p.slice(2));
|
|
109
|
+
}
|
|
110
|
+
return p;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 对路径进行完整的规范化处理:剥离 file:// 前缀 + 展开波浪线 + 去除首尾空白
|
|
114
|
+
* 所有文件操作前应通过此函数处理用户输入的路径
|
|
115
|
+
*/
|
|
116
|
+
export function normalizePath(p) {
|
|
117
|
+
let result = p.trim();
|
|
118
|
+
// 剥离 file:// 协议前缀: file:///Users/... → /Users/...
|
|
119
|
+
if (result.startsWith("file://")) {
|
|
120
|
+
result = result.slice("file://".length);
|
|
121
|
+
// 处理 URL 编码(file:// 路径中空格等字符可能被编码)
|
|
122
|
+
try {
|
|
123
|
+
result = decodeURIComponent(result);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// decodeURIComponent 失败时保留原样
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return expandTilde(result);
|
|
130
|
+
}
|
|
131
|
+
// ============ 文件名 UTF-8 规范化 ============
|
|
132
|
+
/**
|
|
133
|
+
* 规范化文件名为 QQ Bot API 要求的 UTF-8 编码格式
|
|
134
|
+
*
|
|
135
|
+
* 问题场景:
|
|
136
|
+
* - macOS HFS+/APFS 文件系统使用 NFD(Unicode 分解形式)存储文件名,
|
|
137
|
+
* 例如「中文.txt」被分解为多个码点,QQ Bot API 可能拒绝
|
|
138
|
+
* - 文件名可能包含 API 不接受的特殊控制字符
|
|
139
|
+
* - URL 路径中可能包含 percent-encoded 的文件名需要解码
|
|
140
|
+
*
|
|
141
|
+
* 处理:
|
|
142
|
+
* 1. Unicode NFC 规范化(将 NFD 分解形式合并为 NFC 组合形式)
|
|
143
|
+
* 2. 去除 ASCII 控制字符(0x00-0x1F, 0x7F)
|
|
144
|
+
* 3. 去除首尾空白
|
|
145
|
+
* 4. 对 percent-encoded 的文件名尝试 URI 解码
|
|
146
|
+
*/
|
|
147
|
+
export function sanitizeFileName(name) {
|
|
148
|
+
if (!name)
|
|
149
|
+
return name;
|
|
150
|
+
let result = name.trim();
|
|
151
|
+
// 尝试 URI 解码(处理 URL 中 percent-encoded 的中文文件名)
|
|
152
|
+
// 例如 %E4%B8%AD%E6%96%87.txt → 中文.txt
|
|
153
|
+
if (result.includes("%")) {
|
|
154
|
+
try {
|
|
155
|
+
result = decodeURIComponent(result);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// 解码失败(非合法 percent-encoding),保留原始值
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Unicode NFC 规范化:将 macOS NFD 分解形式合并为标准 NFC 组合形式
|
|
162
|
+
result = result.normalize("NFC");
|
|
163
|
+
// 去除 ASCII 控制字符(保留所有可打印字符和非 ASCII Unicode 字符)
|
|
164
|
+
result = result.replace(/[\x00-\x1F\x7F]/g, "");
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
// ============ 本地路径判断 ============
|
|
168
|
+
/**
|
|
169
|
+
* 判断字符串是否为本地文件路径(非 URL)
|
|
170
|
+
*
|
|
171
|
+
* 覆盖:
|
|
172
|
+
* - Unix 绝对路径: /Users/..., /home/..., /tmp/...
|
|
173
|
+
* - Windows 绝对路径: C:\..., D:/..., \\server\share
|
|
174
|
+
* - 相对路径: ./file, ../file
|
|
175
|
+
* - 波浪线路径: ~/Desktop/file.png
|
|
176
|
+
* - file:// 协议: file:///Users/..., file:///home/...
|
|
177
|
+
*
|
|
178
|
+
* 不匹配:
|
|
179
|
+
* - http:// / https:// URL
|
|
180
|
+
* - data: URL
|
|
181
|
+
*/
|
|
182
|
+
export function isLocalPath(p) {
|
|
183
|
+
if (!p)
|
|
184
|
+
return false;
|
|
185
|
+
// file:// 协议(本地文件 URI)
|
|
186
|
+
if (p.startsWith("file://"))
|
|
187
|
+
return true;
|
|
188
|
+
// 波浪线路径(Mac/Linux 用户常用)
|
|
189
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\"))
|
|
190
|
+
return true;
|
|
191
|
+
// Unix 绝对路径
|
|
192
|
+
if (p.startsWith("/"))
|
|
193
|
+
return true;
|
|
194
|
+
// Windows 盘符: C:\ 或 C:/
|
|
195
|
+
if (/^[a-zA-Z]:[\\/]/.test(p))
|
|
196
|
+
return true;
|
|
197
|
+
// Windows UNC: \\server\share
|
|
198
|
+
if (p.startsWith("\\\\"))
|
|
199
|
+
return true;
|
|
200
|
+
// 相对路径
|
|
201
|
+
if (p.startsWith("./") || p.startsWith("../"))
|
|
202
|
+
return true;
|
|
203
|
+
// Windows 相对路径
|
|
204
|
+
if (p.startsWith(".\\") || p.startsWith("..\\"))
|
|
205
|
+
return true;
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 判断 markdown 中提取的路径是否像本地路径
|
|
210
|
+
* 比 isLocalPath 更宽松,用于从 markdown  中检测误用
|
|
211
|
+
*/
|
|
212
|
+
export function looksLikeLocalPath(p) {
|
|
213
|
+
if (isLocalPath(p))
|
|
214
|
+
return true;
|
|
215
|
+
// 常见系统目录前缀(不以 / 开头时也匹配)
|
|
216
|
+
return /^(?:Users|home|tmp|var|private|[A-Z]:)/i.test(p);
|
|
217
|
+
}
|
|
218
|
+
// ============ ffmpeg 跨平台检测 ============
|
|
219
|
+
let _ffmpegPath; // undefined = 未检测, null = 不可用
|
|
220
|
+
let _ffmpegCheckPromise = null;
|
|
221
|
+
/**
|
|
222
|
+
* 检测 ffmpeg 是否可用,返回可执行路径
|
|
223
|
+
*
|
|
224
|
+
* Windows 上检测 ffmpeg.exe,Mac/Linux 检测 ffmpeg
|
|
225
|
+
* 支持通过环境变量 FFMPEG_PATH 指定自定义路径
|
|
226
|
+
*
|
|
227
|
+
* @returns ffmpeg 可执行文件路径,不可用返回 null
|
|
228
|
+
*/
|
|
229
|
+
export function detectFfmpeg() {
|
|
230
|
+
if (_ffmpegPath !== undefined)
|
|
231
|
+
return Promise.resolve(_ffmpegPath);
|
|
232
|
+
if (_ffmpegCheckPromise)
|
|
233
|
+
return _ffmpegCheckPromise;
|
|
234
|
+
_ffmpegCheckPromise = (async () => {
|
|
235
|
+
// 1. 环境变量自定义路径
|
|
236
|
+
const envPath = process.env.FFMPEG_PATH;
|
|
237
|
+
if (envPath) {
|
|
238
|
+
const ok = await testExecutable(envPath, ["-version"]);
|
|
239
|
+
if (ok) {
|
|
240
|
+
_ffmpegPath = envPath;
|
|
241
|
+
console.log(`[platform] ffmpeg found via FFMPEG_PATH: ${envPath}`);
|
|
242
|
+
return _ffmpegPath;
|
|
243
|
+
}
|
|
244
|
+
console.warn(`[platform] FFMPEG_PATH set but not working: ${envPath}`);
|
|
245
|
+
}
|
|
246
|
+
// 2. 系统 PATH 中检测
|
|
247
|
+
const cmd = isWindows() ? "ffmpeg.exe" : "ffmpeg";
|
|
248
|
+
const ok = await testExecutable(cmd, ["-version"]);
|
|
249
|
+
if (ok) {
|
|
250
|
+
_ffmpegPath = cmd;
|
|
251
|
+
console.log(`[platform] ffmpeg detected in PATH`);
|
|
252
|
+
return _ffmpegPath;
|
|
253
|
+
}
|
|
254
|
+
// 3. 常见安装位置(Mac brew、Windows choco/scoop)
|
|
255
|
+
const commonPaths = isWindows()
|
|
256
|
+
? [
|
|
257
|
+
"C:\\ffmpeg\\bin\\ffmpeg.exe",
|
|
258
|
+
path.join(process.env.LOCALAPPDATA || "", "Programs", "ffmpeg", "bin", "ffmpeg.exe"),
|
|
259
|
+
path.join(process.env.ProgramFiles || "", "ffmpeg", "bin", "ffmpeg.exe"),
|
|
260
|
+
]
|
|
261
|
+
: [
|
|
262
|
+
"/usr/local/bin/ffmpeg", // Mac brew
|
|
263
|
+
"/opt/homebrew/bin/ffmpeg", // Mac ARM brew
|
|
264
|
+
"/usr/bin/ffmpeg", // Linux apt
|
|
265
|
+
"/snap/bin/ffmpeg", // Linux snap
|
|
266
|
+
];
|
|
267
|
+
for (const p of commonPaths) {
|
|
268
|
+
if (p && fs.existsSync(p)) {
|
|
269
|
+
const works = await testExecutable(p, ["-version"]);
|
|
270
|
+
if (works) {
|
|
271
|
+
_ffmpegPath = p;
|
|
272
|
+
console.log(`[platform] ffmpeg found at: ${p}`);
|
|
273
|
+
return _ffmpegPath;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
_ffmpegPath = null;
|
|
278
|
+
return null;
|
|
279
|
+
})().finally(() => {
|
|
280
|
+
_ffmpegCheckPromise = null;
|
|
281
|
+
});
|
|
282
|
+
return _ffmpegCheckPromise;
|
|
283
|
+
}
|
|
284
|
+
/** 测试可执行文件是否能正常运行 */
|
|
285
|
+
function testExecutable(cmd, args) {
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
execFile(cmd, args, { timeout: 5000 }, (err) => {
|
|
288
|
+
resolve(!err);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/** 重置 ffmpeg 缓存(用于测试) */
|
|
293
|
+
export function resetFfmpegCache() {
|
|
294
|
+
_ffmpegPath = undefined;
|
|
295
|
+
_ffmpegCheckPromise = null;
|
|
296
|
+
}
|
|
297
|
+
// ============ silk-wasm 兼容性 ============
|
|
298
|
+
let _silkWasmAvailable = null;
|
|
299
|
+
/**
|
|
300
|
+
* 检测 silk-wasm 是否可用
|
|
301
|
+
*
|
|
302
|
+
* silk-wasm 依赖 WASM 运行时,在某些环境(如老版本 Node、某些容器)可能不可用。
|
|
303
|
+
* 提前检测避免运行时崩溃。
|
|
304
|
+
*/
|
|
305
|
+
export async function checkSilkWasmAvailable() {
|
|
306
|
+
if (_silkWasmAvailable !== null)
|
|
307
|
+
return _silkWasmAvailable;
|
|
308
|
+
try {
|
|
309
|
+
const { isSilk } = await import("silk-wasm");
|
|
310
|
+
// 用一个空 buffer 快速测试 WASM 是否能加载
|
|
311
|
+
isSilk(new Uint8Array(0));
|
|
312
|
+
_silkWasmAvailable = true;
|
|
313
|
+
console.log("[platform] silk-wasm: available");
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
_silkWasmAvailable = false;
|
|
317
|
+
console.warn(`[platform] silk-wasm: NOT available (${err instanceof Error ? err.message : String(err)})`);
|
|
318
|
+
}
|
|
319
|
+
return _silkWasmAvailable;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 运行启动诊断,返回环境报告
|
|
323
|
+
* 在 gateway 启动时调用,打印环境信息并给出警告
|
|
324
|
+
*/
|
|
325
|
+
export async function runDiagnostics() {
|
|
326
|
+
const warnings = [];
|
|
327
|
+
const platform = `${process.platform} (${os.release()})`;
|
|
328
|
+
const arch = process.arch;
|
|
329
|
+
const nodeVersion = process.version;
|
|
330
|
+
const homeDir = getHomeDir();
|
|
331
|
+
const tempDir = getTempDir();
|
|
332
|
+
const dataDir = getQQBotDataDir();
|
|
333
|
+
// 检测 ffmpeg
|
|
334
|
+
const ffmpegPath = await detectFfmpeg();
|
|
335
|
+
if (!ffmpegPath) {
|
|
336
|
+
warnings.push(isWindows()
|
|
337
|
+
? "⚠️ ffmpeg 未安装。语音/视频格式转换将受限。安装方式: choco install ffmpeg 或 scoop install ffmpeg 或从 https://ffmpeg.org 下载"
|
|
338
|
+
: getPlatform() === "darwin"
|
|
339
|
+
? "⚠️ ffmpeg 未安装。语音/视频格式转换将受限。安装方式: brew install ffmpeg"
|
|
340
|
+
: "⚠️ ffmpeg 未安装。语音/视频格式转换将受限。安装方式: sudo apt install ffmpeg 或 sudo yum install ffmpeg");
|
|
341
|
+
}
|
|
342
|
+
// 检测 silk-wasm
|
|
343
|
+
const silkWasm = await checkSilkWasmAvailable();
|
|
344
|
+
if (!silkWasm) {
|
|
345
|
+
warnings.push("⚠️ silk-wasm 不可用。QQ 语音消息的收发将无法工作。请确认 Node.js 版本 >= 16 且 WASM 支持正常");
|
|
346
|
+
}
|
|
347
|
+
// 检查数据目录可写性
|
|
348
|
+
try {
|
|
349
|
+
const testFile = path.join(dataDir, ".write-test");
|
|
350
|
+
fs.writeFileSync(testFile, "test");
|
|
351
|
+
fs.unlinkSync(testFile);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
warnings.push(`⚠️ 数据目录不可写: ${dataDir}。请检查权限`);
|
|
355
|
+
}
|
|
356
|
+
// Windows 特殊提醒
|
|
357
|
+
if (isWindows()) {
|
|
358
|
+
// 检查路径中是否有中文或空格(可能导致某些工具异常)
|
|
359
|
+
if (/[\u4e00-\u9fa5]/.test(homeDir) || homeDir.includes(" ")) {
|
|
360
|
+
warnings.push(`⚠️ 用户目录包含中文或空格: ${homeDir}。某些工具可能无法正常工作,建议设置 QQBOT_DATA_DIR 环境变量指定纯英文路径`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const report = {
|
|
364
|
+
platform,
|
|
365
|
+
arch,
|
|
366
|
+
nodeVersion,
|
|
367
|
+
homeDir,
|
|
368
|
+
tempDir,
|
|
369
|
+
dataDir,
|
|
370
|
+
ffmpeg: ffmpegPath,
|
|
371
|
+
silkWasm,
|
|
372
|
+
warnings,
|
|
373
|
+
};
|
|
374
|
+
// 打印诊断报告
|
|
375
|
+
console.log("=== QQBot 环境诊断 ===");
|
|
376
|
+
console.log(` 平台: ${platform} (${arch})`);
|
|
377
|
+
console.log(` Node: ${nodeVersion}`);
|
|
378
|
+
console.log(` 主目录: ${homeDir}`);
|
|
379
|
+
console.log(` 数据目录: ${dataDir}`);
|
|
380
|
+
console.log(` ffmpeg: ${ffmpegPath ?? "未安装"}`);
|
|
381
|
+
console.log(` silk-wasm: ${silkWasm ? "可用" : "不可用"}`);
|
|
382
|
+
if (warnings.length > 0) {
|
|
383
|
+
console.log(" --- 警告 ---");
|
|
384
|
+
for (const w of warnings) {
|
|
385
|
+
console.log(` ${w}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
console.log("======================");
|
|
389
|
+
return report;
|
|
390
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot 文本解析工具函数
|
|
3
|
+
*/
|
|
4
|
+
import type { RefAttachmentSummary } from "../ref-index-store.js";
|
|
5
|
+
/**
|
|
6
|
+
* 解析 QQ 表情标签,将 <faceType=1,faceId="13",ext="base64..."> 格式
|
|
7
|
+
* 替换为 【表情: 中文名】 格式
|
|
8
|
+
* ext 字段为 Base64 编码的 JSON,格式如 {"text":"呲牙"}
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseFaceTags(text: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* 过滤内部标记(如 [[reply_to: xxx]])
|
|
13
|
+
* 这些标记可能被 AI 错误地学习并输出,需要在发送前移除
|
|
14
|
+
*/
|
|
15
|
+
export declare function filterInternalMarkers(text: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* 从 message_scene.ext 数组中解析引用索引
|
|
18
|
+
* ext 格式示例: ["", "ref_msg_idx=REFIDX_xxx", "msg_idx=REFIDX_yyy"]
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseRefIndices(ext?: string[]): {
|
|
21
|
+
refMsgIdx?: string;
|
|
22
|
+
msgIdx?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 从附件列表中构建附件摘要(用于引用索引缓存)
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildAttachmentSummaries(attachments?: Array<{
|
|
28
|
+
content_type: string;
|
|
29
|
+
url: string;
|
|
30
|
+
filename?: string;
|
|
31
|
+
voice_wav_url?: string;
|
|
32
|
+
}>, localPaths?: Array<string | null>): RefAttachmentSummary[] | undefined;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot 文本解析工具函数
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 解析 QQ 表情标签,将 <faceType=1,faceId="13",ext="base64..."> 格式
|
|
6
|
+
* 替换为 【表情: 中文名】 格式
|
|
7
|
+
* ext 字段为 Base64 编码的 JSON,格式如 {"text":"呲牙"}
|
|
8
|
+
*/
|
|
9
|
+
export function parseFaceTags(text) {
|
|
10
|
+
if (!text)
|
|
11
|
+
return text;
|
|
12
|
+
return text.replace(/<faceType=\d+,faceId="[^"]*",ext="([^"]*)">/g, (_match, ext) => {
|
|
13
|
+
try {
|
|
14
|
+
const decoded = Buffer.from(ext, "base64").toString("utf-8");
|
|
15
|
+
const parsed = JSON.parse(decoded);
|
|
16
|
+
const faceName = parsed.text || "未知表情";
|
|
17
|
+
return `【表情: ${faceName}】`;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return _match;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 过滤内部标记(如 [[reply_to: xxx]])
|
|
26
|
+
* 这些标记可能被 AI 错误地学习并输出,需要在发送前移除
|
|
27
|
+
*/
|
|
28
|
+
export function filterInternalMarkers(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return text;
|
|
31
|
+
let result = text.replace(/\[\[[a-z_]+:\s*[^\]]*\]\]/gi, "");
|
|
32
|
+
// 过滤框架内部图片引用标记:@image:image_xxx.png、@voice:voice_xxx.silk 等
|
|
33
|
+
result = result.replace(/@(?:image|voice|video|file):[a-zA-Z0-9_.-]+/g, "");
|
|
34
|
+
result = result.replace(/\n{3,}/g, "\n\n").trim();
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 从 message_scene.ext 数组中解析引用索引
|
|
39
|
+
* ext 格式示例: ["", "ref_msg_idx=REFIDX_xxx", "msg_idx=REFIDX_yyy"]
|
|
40
|
+
*/
|
|
41
|
+
export function parseRefIndices(ext) {
|
|
42
|
+
if (!ext || ext.length === 0)
|
|
43
|
+
return {};
|
|
44
|
+
let refMsgIdx;
|
|
45
|
+
let msgIdx;
|
|
46
|
+
for (const item of ext) {
|
|
47
|
+
if (item.startsWith("ref_msg_idx=")) {
|
|
48
|
+
refMsgIdx = item.slice("ref_msg_idx=".length);
|
|
49
|
+
}
|
|
50
|
+
else if (item.startsWith("msg_idx=")) {
|
|
51
|
+
msgIdx = item.slice("msg_idx=".length);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { refMsgIdx, msgIdx };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 从附件列表中构建附件摘要(用于引用索引缓存)
|
|
58
|
+
*/
|
|
59
|
+
export function buildAttachmentSummaries(attachments, localPaths) {
|
|
60
|
+
if (!attachments || attachments.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return attachments.map((att, idx) => {
|
|
63
|
+
const ct = att.content_type?.toLowerCase() ?? "";
|
|
64
|
+
let type = "unknown";
|
|
65
|
+
if (ct.startsWith("image/"))
|
|
66
|
+
type = "image";
|
|
67
|
+
else if (ct === "voice" || ct.startsWith("audio/") || ct.includes("silk") || ct.includes("amr"))
|
|
68
|
+
type = "voice";
|
|
69
|
+
else if (ct.startsWith("video/"))
|
|
70
|
+
type = "video";
|
|
71
|
+
else if (ct.startsWith("application/") || ct.startsWith("text/"))
|
|
72
|
+
type = "file";
|
|
73
|
+
return {
|
|
74
|
+
type,
|
|
75
|
+
filename: att.filename,
|
|
76
|
+
contentType: att.content_type,
|
|
77
|
+
localPath: localPaths?.[idx] ?? undefined,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file_info 缓存 — 借鉴 Telegram file_id 机制
|
|
3
|
+
*
|
|
4
|
+
* QQ Bot API 上传文件后返回 file_info + ttl,在 TTL 内相同文件可直接复用 file_info
|
|
5
|
+
* 避免重复上传同一文件,节省带宽和时间。
|
|
6
|
+
*
|
|
7
|
+
* 缓存 key = md5(fileContent) + targetType(c2c/group) + targetId + fileType
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 计算文件内容的 MD5 hash(用于缓存 key)
|
|
11
|
+
* 对于 Base64 数据直接 hash,对于文件路径读取后 hash
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeFileHash(data: string | Buffer): string;
|
|
14
|
+
/**
|
|
15
|
+
* 从缓存获取 file_info
|
|
16
|
+
* @returns file_info 字符串,未命中或已过期返回 null
|
|
17
|
+
*/
|
|
18
|
+
export declare function getCachedFileInfo(contentHash: string, scope: "c2c" | "group", targetId: string, fileType: number): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* 将上传结果写入缓存
|
|
21
|
+
* @param ttl - API 返回的 TTL(秒),缓存会提前 60 秒失效
|
|
22
|
+
*/
|
|
23
|
+
export declare function setCachedFileInfo(contentHash: string, scope: "c2c" | "group", targetId: string, fileType: number, fileInfo: string, fileUuid: string, ttl: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* 获取缓存统计
|
|
26
|
+
*/
|
|
27
|
+
export declare function getUploadCacheStats(): {
|
|
28
|
+
size: number;
|
|
29
|
+
maxSize: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 清除所有缓存
|
|
33
|
+
*/
|
|
34
|
+
export declare function clearUploadCache(): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file_info 缓存 — 借鉴 Telegram file_id 机制
|
|
3
|
+
*
|
|
4
|
+
* QQ Bot API 上传文件后返回 file_info + ttl,在 TTL 内相同文件可直接复用 file_info
|
|
5
|
+
* 避免重复上传同一文件,节省带宽和时间。
|
|
6
|
+
*
|
|
7
|
+
* 缓存 key = md5(fileContent) + targetType(c2c/group) + targetId + fileType
|
|
8
|
+
*/
|
|
9
|
+
import * as crypto from "node:crypto";
|
|
10
|
+
// 内存缓存,key 格式:`${contentHash}:${scope}:${targetId}:${fileType}`
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
// 最大缓存条目数,防止内存泄漏
|
|
13
|
+
const MAX_CACHE_SIZE = 500;
|
|
14
|
+
/**
|
|
15
|
+
* 计算文件内容的 MD5 hash(用于缓存 key)
|
|
16
|
+
* 对于 Base64 数据直接 hash,对于文件路径读取后 hash
|
|
17
|
+
*/
|
|
18
|
+
export function computeFileHash(data) {
|
|
19
|
+
const content = typeof data === "string" ? data : data;
|
|
20
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 构建缓存 key
|
|
24
|
+
* @param contentHash - 文件内容 hash
|
|
25
|
+
* @param scope - "c2c" | "group"
|
|
26
|
+
* @param targetId - 用户 openid 或群 openid
|
|
27
|
+
* @param fileType - 1=IMAGE, 2=VIDEO, 3=VOICE, 4=FILE
|
|
28
|
+
*/
|
|
29
|
+
function buildCacheKey(contentHash, scope, targetId, fileType) {
|
|
30
|
+
return `${contentHash}:${scope}:${targetId}:${fileType}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 从缓存获取 file_info
|
|
34
|
+
* @returns file_info 字符串,未命中或已过期返回 null
|
|
35
|
+
*/
|
|
36
|
+
export function getCachedFileInfo(contentHash, scope, targetId, fileType) {
|
|
37
|
+
const key = buildCacheKey(contentHash, scope, targetId, fileType);
|
|
38
|
+
const entry = cache.get(key);
|
|
39
|
+
if (!entry)
|
|
40
|
+
return null;
|
|
41
|
+
// 检查是否过期
|
|
42
|
+
if (Date.now() >= entry.expiresAt) {
|
|
43
|
+
cache.delete(key);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
console.log(`[upload-cache] Cache HIT: key=${key.slice(0, 40)}..., fileUuid=${entry.fileUuid}`);
|
|
47
|
+
return entry.fileInfo;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 将上传结果写入缓存
|
|
51
|
+
* @param ttl - API 返回的 TTL(秒),缓存会提前 60 秒失效
|
|
52
|
+
*/
|
|
53
|
+
export function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fileUuid, ttl) {
|
|
54
|
+
// 清理过期条目(惰性清理)
|
|
55
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
for (const [k, v] of cache) {
|
|
58
|
+
if (now >= v.expiresAt) {
|
|
59
|
+
cache.delete(k);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 如果清理后仍然超限,删除最早的一半
|
|
63
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
64
|
+
const keys = Array.from(cache.keys());
|
|
65
|
+
for (let i = 0; i < keys.length / 2; i++) {
|
|
66
|
+
cache.delete(keys[i]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const key = buildCacheKey(contentHash, scope, targetId, fileType);
|
|
71
|
+
// 提前 60 秒失效,避免临界点过期
|
|
72
|
+
const safetyMargin = 60;
|
|
73
|
+
const effectiveTtl = Math.max(ttl - safetyMargin, 10);
|
|
74
|
+
cache.set(key, {
|
|
75
|
+
fileInfo,
|
|
76
|
+
fileUuid,
|
|
77
|
+
expiresAt: Date.now() + effectiveTtl * 1000,
|
|
78
|
+
});
|
|
79
|
+
console.log(`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 获取缓存统计
|
|
83
|
+
*/
|
|
84
|
+
export function getUploadCacheStats() {
|
|
85
|
+
return { size: cache.size, maxSize: MAX_CACHE_SIZE };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 清除所有缓存
|
|
89
|
+
*/
|
|
90
|
+
export function clearUploadCache() {
|
|
91
|
+
cache.clear();
|
|
92
|
+
console.log(`[upload-cache] Cache cleared`);
|
|
93
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
|
|
4
|
+
import { qqbotPlugin } from "./src/channel.js";
|
|
5
|
+
import { setQQBotRuntime } from "./src/runtime.js";
|
|
6
|
+
import { registerChannelTool } from "./src/tools/channel.js";
|
|
7
|
+
import { registerRemindTool } from "./src/tools/remind.js";
|
|
8
|
+
|
|
9
|
+
const plugin = {
|
|
10
|
+
id: "openclaw-qqbot",
|
|
11
|
+
name: "QQ Bot",
|
|
12
|
+
description: "QQ Bot channel plugin",
|
|
13
|
+
configSchema: emptyPluginConfigSchema(),
|
|
14
|
+
register(api: OpenClawPluginApi) {
|
|
15
|
+
setQQBotRuntime(api.runtime);
|
|
16
|
+
api.registerChannel({ plugin: qqbotPlugin });
|
|
17
|
+
registerChannelTool(api);
|
|
18
|
+
registerRemindTool(api);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default plugin;
|
|
23
|
+
|
|
24
|
+
export { qqbotPlugin } from "./src/channel.js";
|
|
25
|
+
export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js";
|
|
26
|
+
export { qqbotOnboardingAdapter } from "./src/onboarding.js";
|
|
27
|
+
export * from "./src/types.js";
|
|
28
|
+
export * from "./src/api.js";
|
|
29
|
+
export * from "./src/config.js";
|
|
30
|
+
export * from "./src/gateway.js";
|
|
31
|
+
export * from "./src/outbound.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-qqbot",
|
|
3
|
+
"name": "OpenClaw QQ Bot",
|
|
4
|
+
"description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
|
|
5
|
+
"channels": ["qqbot"],
|
|
6
|
+
"skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media"],
|
|
7
|
+
"capabilities": {
|
|
8
|
+
"proactiveMessaging": true,
|
|
9
|
+
"cronJobs": true
|
|
10
|
+
},
|
|
11
|
+
"configSchema": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {}
|
|
15
|
+
}
|
|
16
|
+
}
|