@tencent-connect/openclaw-qqbot 1.0.0-alpha.0
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 +393 -0
- package/README.zh.md +390 -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 +22 -0
- package/dist/src/api.d.ts +138 -0
- package/dist/src/api.js +523 -0
- package/dist/src/channel.d.ts +3 -0
- package/dist/src/channel.js +337 -0
- package/dist/src/config.d.ts +25 -0
- package/dist/src/config.js +156 -0
- package/dist/src/gateway.d.ts +18 -0
- package/dist/src/gateway.js +2315 -0
- package/dist/src/image-server.d.ts +62 -0
- package/dist/src/image-server.js +401 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +263 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +203 -0
- package/dist/src/outbound.d.ts +150 -0
- package/dist/src/outbound.js +1175 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +399 -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/types.d.ts +145 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils/audio-convert.d.ts +73 -0
- package/dist/src/utils/audio-convert.js +645 -0
- package/dist/src/utils/file-utils.d.ts +46 -0
- package/dist/src/utils/file-utils.js +107 -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 +120 -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 +126 -0
- package/dist/src/utils/platform.js +358 -0
- package/dist/src/utils/upload-cache.d.ts +34 -0
- package/dist/src/utils/upload-cache.js +93 -0
- package/index.ts +27 -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/proactive-api-server.ts +356 -0
- package/scripts/pull-latest.sh +316 -0
- package/scripts/send-proactive.ts +273 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/upgrade-and-run.sh +525 -0
- package/scripts/upgrade.sh +127 -0
- package/skills/qqbot-cron/SKILL.md +513 -0
- package/skills/qqbot-media/SKILL.md +194 -0
- package/src/api.ts +704 -0
- package/src/channel.ts +368 -0
- package/src/config.ts +182 -0
- package/src/gateway.ts +2459 -0
- package/src/image-server.ts +474 -0
- package/src/known-users.ts +353 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-plugin-sdk.d.ts +483 -0
- package/src/outbound.ts +1301 -0
- package/src/proactive.ts +530 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/types.ts +153 -0
- package/src/utils/audio-convert.ts +738 -0
- package/src/utils/file-utils.ts +122 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-tags.ts +134 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/platform.ts +404 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 持久化存储
|
|
3
|
+
* 将 WebSocket 连接状态(sessionId、lastSeq)持久化到文件
|
|
4
|
+
* 支持进程重启后通过 Resume 机制快速恢复连接
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { getQQBotDataDir } from "./utils/platform.js";
|
|
9
|
+
// Session 文件目录
|
|
10
|
+
const SESSION_DIR = getQQBotDataDir("sessions");
|
|
11
|
+
// Session 过期时间(5分钟)- Resume 要求在断开后一定时间内恢复
|
|
12
|
+
const SESSION_EXPIRE_TIME = 5 * 60 * 1000;
|
|
13
|
+
// 写入节流时间(避免频繁写入)
|
|
14
|
+
const SAVE_THROTTLE_MS = 1000;
|
|
15
|
+
// 每个账户的节流状态
|
|
16
|
+
const throttleState = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* 确保目录存在
|
|
19
|
+
*/
|
|
20
|
+
function ensureDir() {
|
|
21
|
+
if (!fs.existsSync(SESSION_DIR)) {
|
|
22
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 获取 Session 文件路径
|
|
27
|
+
*/
|
|
28
|
+
function getSessionPath(accountId) {
|
|
29
|
+
// 清理 accountId 中的特殊字符
|
|
30
|
+
const safeId = accountId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
31
|
+
return path.join(SESSION_DIR, `session-${safeId}.json`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 加载 Session 状态
|
|
35
|
+
* @param accountId 账户 ID
|
|
36
|
+
* @param expectedAppId 当前使用的 appId,如果与保存时的 appId 不匹配则视为失效
|
|
37
|
+
* @returns Session 状态,如果不存在、已过期或 appId 不匹配返回 null
|
|
38
|
+
*/
|
|
39
|
+
export function loadSession(accountId, expectedAppId) {
|
|
40
|
+
const filePath = getSessionPath(accountId);
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(filePath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
46
|
+
const state = JSON.parse(data);
|
|
47
|
+
// 检查是否过期
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (now - state.savedAt > SESSION_EXPIRE_TIME) {
|
|
50
|
+
console.log(`[session-store] Session expired for ${accountId}, age: ${Math.round((now - state.savedAt) / 1000)}s`);
|
|
51
|
+
try {
|
|
52
|
+
fs.unlinkSync(filePath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// 忽略删除错误
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// 检查 appId 是否匹配(凭据变更检测)
|
|
60
|
+
if (expectedAppId && state.appId && state.appId !== expectedAppId) {
|
|
61
|
+
console.log(`[session-store] appId mismatch for ${accountId}: saved=${state.appId}, current=${expectedAppId}. Discarding stale session.`);
|
|
62
|
+
try {
|
|
63
|
+
fs.unlinkSync(filePath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// 忽略删除错误
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// 验证必要字段
|
|
71
|
+
if (!state.sessionId || state.lastSeq === null || state.lastSeq === undefined) {
|
|
72
|
+
console.log(`[session-store] Invalid session data for ${accountId}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
console.log(`[session-store] Loaded session for ${accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}, appId=${state.appId ?? "unknown"}, age=${Math.round((now - state.savedAt) / 1000)}s`);
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(`[session-store] Failed to load session for ${accountId}: ${err}`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 保存 Session 状态(带节流,避免频繁写入)
|
|
85
|
+
* @param state Session 状态
|
|
86
|
+
*/
|
|
87
|
+
export function saveSession(state) {
|
|
88
|
+
const { accountId } = state;
|
|
89
|
+
// 获取或初始化节流状态
|
|
90
|
+
let throttle = throttleState.get(accountId);
|
|
91
|
+
if (!throttle) {
|
|
92
|
+
throttle = {
|
|
93
|
+
pendingState: null,
|
|
94
|
+
lastSaveTime: 0,
|
|
95
|
+
throttleTimer: null,
|
|
96
|
+
};
|
|
97
|
+
throttleState.set(accountId, throttle);
|
|
98
|
+
}
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const timeSinceLastSave = now - throttle.lastSaveTime;
|
|
101
|
+
// 如果距离上次保存时间足够长,立即保存
|
|
102
|
+
if (timeSinceLastSave >= SAVE_THROTTLE_MS) {
|
|
103
|
+
doSaveSession(state);
|
|
104
|
+
throttle.lastSaveTime = now;
|
|
105
|
+
throttle.pendingState = null;
|
|
106
|
+
// 清除待定的节流定时器
|
|
107
|
+
if (throttle.throttleTimer) {
|
|
108
|
+
clearTimeout(throttle.throttleTimer);
|
|
109
|
+
throttle.throttleTimer = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// 记录待保存的状态
|
|
114
|
+
throttle.pendingState = state;
|
|
115
|
+
// 如果没有设置定时器,设置一个
|
|
116
|
+
if (!throttle.throttleTimer) {
|
|
117
|
+
const delay = SAVE_THROTTLE_MS - timeSinceLastSave;
|
|
118
|
+
throttle.throttleTimer = setTimeout(() => {
|
|
119
|
+
const t = throttleState.get(accountId);
|
|
120
|
+
if (t && t.pendingState) {
|
|
121
|
+
doSaveSession(t.pendingState);
|
|
122
|
+
t.lastSaveTime = Date.now();
|
|
123
|
+
t.pendingState = null;
|
|
124
|
+
}
|
|
125
|
+
if (t) {
|
|
126
|
+
t.throttleTimer = null;
|
|
127
|
+
}
|
|
128
|
+
}, delay);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 实际执行保存操作
|
|
134
|
+
*/
|
|
135
|
+
function doSaveSession(state) {
|
|
136
|
+
const filePath = getSessionPath(state.accountId);
|
|
137
|
+
try {
|
|
138
|
+
ensureDir();
|
|
139
|
+
// 更新保存时间
|
|
140
|
+
const stateToSave = {
|
|
141
|
+
...state,
|
|
142
|
+
savedAt: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
fs.writeFileSync(filePath, JSON.stringify(stateToSave, null, 2), "utf-8");
|
|
145
|
+
console.log(`[session-store] Saved session for ${state.accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(`[session-store] Failed to save session for ${state.accountId}: ${err}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 清除 Session 状态
|
|
153
|
+
* @param accountId 账户 ID
|
|
154
|
+
*/
|
|
155
|
+
export function clearSession(accountId) {
|
|
156
|
+
const filePath = getSessionPath(accountId);
|
|
157
|
+
// 清除节流状态
|
|
158
|
+
const throttle = throttleState.get(accountId);
|
|
159
|
+
if (throttle) {
|
|
160
|
+
if (throttle.throttleTimer) {
|
|
161
|
+
clearTimeout(throttle.throttleTimer);
|
|
162
|
+
}
|
|
163
|
+
throttleState.delete(accountId);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(filePath)) {
|
|
167
|
+
fs.unlinkSync(filePath);
|
|
168
|
+
console.log(`[session-store] Cleared session for ${accountId}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.error(`[session-store] Failed to clear session for ${accountId}: ${err}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 更新 lastSeq(轻量级更新)
|
|
177
|
+
* @param accountId 账户 ID
|
|
178
|
+
* @param lastSeq 最新的消息序号
|
|
179
|
+
*/
|
|
180
|
+
export function updateLastSeq(accountId, lastSeq) {
|
|
181
|
+
const existing = loadSession(accountId);
|
|
182
|
+
if (existing && existing.sessionId) {
|
|
183
|
+
saveSession({
|
|
184
|
+
...existing,
|
|
185
|
+
lastSeq,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 获取所有保存的 Session 状态
|
|
191
|
+
*/
|
|
192
|
+
export function getAllSessions() {
|
|
193
|
+
const sessions = [];
|
|
194
|
+
try {
|
|
195
|
+
ensureDir();
|
|
196
|
+
const files = fs.readdirSync(SESSION_DIR);
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
if (file.startsWith("session-") && file.endsWith(".json")) {
|
|
199
|
+
const filePath = path.join(SESSION_DIR, file);
|
|
200
|
+
try {
|
|
201
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
202
|
+
const state = JSON.parse(data);
|
|
203
|
+
sessions.push(state);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// 忽略解析错误
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// 目录不存在等错误
|
|
213
|
+
}
|
|
214
|
+
return sessions;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 清理过期的 Session 文件
|
|
218
|
+
*/
|
|
219
|
+
export function cleanupExpiredSessions() {
|
|
220
|
+
let cleaned = 0;
|
|
221
|
+
try {
|
|
222
|
+
ensureDir();
|
|
223
|
+
const files = fs.readdirSync(SESSION_DIR);
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
if (file.startsWith("session-") && file.endsWith(".json")) {
|
|
227
|
+
const filePath = path.join(SESSION_DIR, file);
|
|
228
|
+
try {
|
|
229
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
230
|
+
const state = JSON.parse(data);
|
|
231
|
+
if (now - state.savedAt > SESSION_EXPIRE_TIME) {
|
|
232
|
+
fs.unlinkSync(filePath);
|
|
233
|
+
cleaned++;
|
|
234
|
+
console.log(`[session-store] Cleaned expired session: ${file}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// 忽略解析错误,但也删除损坏的文件
|
|
239
|
+
try {
|
|
240
|
+
fs.unlinkSync(filePath);
|
|
241
|
+
cleaned++;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// 忽略
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// 目录不存在等错误
|
|
252
|
+
}
|
|
253
|
+
return cleaned;
|
|
254
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot 配置类型
|
|
3
|
+
*/
|
|
4
|
+
export interface QQBotConfig {
|
|
5
|
+
appId: string;
|
|
6
|
+
clientSecret?: string;
|
|
7
|
+
clientSecretFile?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 解析后的 QQ Bot 账户
|
|
11
|
+
*/
|
|
12
|
+
export interface ResolvedQQBotAccount {
|
|
13
|
+
accountId: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
appId: string;
|
|
17
|
+
clientSecret: string;
|
|
18
|
+
secretSource: "config" | "file" | "env" | "none";
|
|
19
|
+
/** 系统提示词 */
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
/** 图床服务器公网地址 */
|
|
22
|
+
imageServerBaseUrl?: string;
|
|
23
|
+
/** 是否支持 markdown 消息(默认 true) */
|
|
24
|
+
markdownSupport: boolean;
|
|
25
|
+
config: QQBotAccountConfig;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* QQ Bot 账户配置
|
|
29
|
+
*/
|
|
30
|
+
export interface QQBotAccountConfig {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
name?: string;
|
|
33
|
+
appId?: string;
|
|
34
|
+
clientSecret?: string;
|
|
35
|
+
clientSecretFile?: string;
|
|
36
|
+
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
37
|
+
allowFrom?: string[];
|
|
38
|
+
/** 系统提示词,会添加在用户消息前面 */
|
|
39
|
+
systemPrompt?: string;
|
|
40
|
+
/** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */
|
|
41
|
+
imageServerBaseUrl?: string;
|
|
42
|
+
/** 是否支持 markdown 消息(默认 true,设为 false 可禁用) */
|
|
43
|
+
markdownSupport?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated 请使用 audioFormatPolicy.uploadDirectFormats
|
|
46
|
+
* 可直接上传的音频格式(不转换为 SILK),向后兼容
|
|
47
|
+
*/
|
|
48
|
+
voiceDirectUploadFormats?: string[];
|
|
49
|
+
/**
|
|
50
|
+
* 音频格式策略配置
|
|
51
|
+
* 统一管理入站(STT)和出站(上传)的音频格式转换行为
|
|
52
|
+
*/
|
|
53
|
+
audioFormatPolicy?: AudioFormatPolicy;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 音频格式策略:控制哪些格式可跳过转换
|
|
57
|
+
*/
|
|
58
|
+
export interface AudioFormatPolicy {
|
|
59
|
+
/**
|
|
60
|
+
* STT 模型直接支持的音频格式(入站:跳过 SILK→WAV 转换)
|
|
61
|
+
* 如果 STT 服务支持直接处理某些格式(如 silk/amr),可将其加入此列表
|
|
62
|
+
* 例如: [".silk", ".amr", ".wav", ".mp3", ".ogg"]
|
|
63
|
+
* 默认为空(所有语音都先转换为 WAV 再送 STT)
|
|
64
|
+
*/
|
|
65
|
+
sttDirectFormats?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* QQ 平台支持直传的音频格式(出站:跳过→SILK 转换)
|
|
68
|
+
* 默认为 [".wav", ".mp3", ".silk"](QQ Bot API 原生支持的三种格式)
|
|
69
|
+
* 仅当需要覆盖默认值时才配置此项
|
|
70
|
+
*/
|
|
71
|
+
uploadDirectFormats?: string[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 富媒体附件
|
|
75
|
+
*/
|
|
76
|
+
export interface MessageAttachment {
|
|
77
|
+
content_type: string;
|
|
78
|
+
filename?: string;
|
|
79
|
+
height?: number;
|
|
80
|
+
width?: number;
|
|
81
|
+
size?: number;
|
|
82
|
+
url: string;
|
|
83
|
+
voice_wav_url?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* C2C 消息事件
|
|
87
|
+
*/
|
|
88
|
+
export interface C2CMessageEvent {
|
|
89
|
+
author: {
|
|
90
|
+
id: string;
|
|
91
|
+
union_openid: string;
|
|
92
|
+
user_openid: string;
|
|
93
|
+
};
|
|
94
|
+
content: string;
|
|
95
|
+
id: string;
|
|
96
|
+
timestamp: string;
|
|
97
|
+
message_scene?: {
|
|
98
|
+
source: string;
|
|
99
|
+
};
|
|
100
|
+
attachments?: MessageAttachment[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 频道 AT 消息事件
|
|
104
|
+
*/
|
|
105
|
+
export interface GuildMessageEvent {
|
|
106
|
+
id: string;
|
|
107
|
+
channel_id: string;
|
|
108
|
+
guild_id: string;
|
|
109
|
+
content: string;
|
|
110
|
+
timestamp: string;
|
|
111
|
+
author: {
|
|
112
|
+
id: string;
|
|
113
|
+
username?: string;
|
|
114
|
+
bot?: boolean;
|
|
115
|
+
};
|
|
116
|
+
member?: {
|
|
117
|
+
nick?: string;
|
|
118
|
+
joined_at?: string;
|
|
119
|
+
};
|
|
120
|
+
attachments?: MessageAttachment[];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 群聊 AT 消息事件
|
|
124
|
+
*/
|
|
125
|
+
export interface GroupMessageEvent {
|
|
126
|
+
author: {
|
|
127
|
+
id: string;
|
|
128
|
+
member_openid: string;
|
|
129
|
+
};
|
|
130
|
+
content: string;
|
|
131
|
+
id: string;
|
|
132
|
+
timestamp: string;
|
|
133
|
+
group_id: string;
|
|
134
|
+
group_openid: string;
|
|
135
|
+
attachments?: MessageAttachment[];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* WebSocket 事件负载
|
|
139
|
+
*/
|
|
140
|
+
export interface WSPayload {
|
|
141
|
+
op: number;
|
|
142
|
+
d?: unknown;
|
|
143
|
+
s?: number;
|
|
144
|
+
t?: string;
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将 SILK/AMR 语音文件转换为 WAV 格式
|
|
3
|
+
*
|
|
4
|
+
* @param inputPath 输入文件路径(.amr / .silk / .slk)
|
|
5
|
+
* @param outputDir 输出目录(默认与输入文件同目录)
|
|
6
|
+
* @returns 转换后的 WAV 文件路径,失败返回 null
|
|
7
|
+
*/
|
|
8
|
+
export declare function convertSilkToWav(inputPath: string, outputDir?: string): Promise<{
|
|
9
|
+
wavPath: string;
|
|
10
|
+
duration: number;
|
|
11
|
+
} | null>;
|
|
12
|
+
/**
|
|
13
|
+
* 判断是否为语音附件(根据 content_type 或文件扩展名)
|
|
14
|
+
*/
|
|
15
|
+
export declare function isVoiceAttachment(att: {
|
|
16
|
+
content_type?: string;
|
|
17
|
+
filename?: string;
|
|
18
|
+
}): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 格式化语音时长为可读字符串
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatDuration(durationMs: number): string;
|
|
23
|
+
export declare function isAudioFile(filePath: string): boolean;
|
|
24
|
+
export interface TTSConfig {
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
apiKey: string;
|
|
27
|
+
model: string;
|
|
28
|
+
voice: string;
|
|
29
|
+
/** Azure OpenAI 风格:使用 api-key header 而非 Bearer token */
|
|
30
|
+
authStyle?: "bearer" | "api-key";
|
|
31
|
+
/** 附加在 URL 后的查询参数,如 Azure 的 api-version */
|
|
32
|
+
queryParams?: Record<string, string>;
|
|
33
|
+
/** 自定义速度(默认不传) */
|
|
34
|
+
speed?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function resolveTTSConfig(cfg: Record<string, unknown>): TTSConfig | null;
|
|
37
|
+
export declare function textToSpeechPCM(text: string, ttsCfg: TTSConfig): Promise<{
|
|
38
|
+
pcmBuffer: Buffer;
|
|
39
|
+
sampleRate: number;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function pcmToSilk(pcmBuffer: Buffer, sampleRate: number): Promise<{
|
|
42
|
+
silkBuffer: Buffer;
|
|
43
|
+
duration: number;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function textToSilk(text: string, ttsCfg: TTSConfig, outputDir: string): Promise<{
|
|
46
|
+
silkPath: string;
|
|
47
|
+
silkBase64: string;
|
|
48
|
+
duration: number;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* 将本地音频文件转换为 QQ Bot 可上传的 Base64
|
|
52
|
+
*
|
|
53
|
+
* QQ Bot API 支持直传 WAV、MP3、SILK 三种格式,其他格式仍需转换。
|
|
54
|
+
* 转换策略(参考 NapCat/go-cqhttp/Discord/Telegram 的做法):
|
|
55
|
+
*
|
|
56
|
+
* 1. WAV / MP3 / SILK → 直传(跳过转换)
|
|
57
|
+
* 2. 有 ffmpeg → ffmpeg 万能解码为 PCM → silk-wasm 编码
|
|
58
|
+
* 支持: ogg, opus, aac, flac, wma, m4a, pcm 等所有 ffmpeg 支持的格式
|
|
59
|
+
* 3. 无 ffmpeg → WASM fallback(仅支持 pcm, wav)
|
|
60
|
+
*
|
|
61
|
+
* @param directUploadFormats - 自定义直传格式列表,覆盖默认值。传 undefined 使用 QQ_NATIVE_UPLOAD_FORMATS
|
|
62
|
+
*/
|
|
63
|
+
export declare function audioFileToSilkBase64(filePath: string, directUploadFormats?: string[]): Promise<string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* 等待文件就绪(轮询直到文件出现且大小稳定)
|
|
66
|
+
* 用于 TTS 生成后等待文件写入完成
|
|
67
|
+
*
|
|
68
|
+
* @param filePath 文件路径
|
|
69
|
+
* @param timeoutMs 最大等待时间(默认 2 分钟)
|
|
70
|
+
* @param pollMs 轮询间隔(默认 500ms)
|
|
71
|
+
* @returns 文件大小(字节),超时或文件始终为空返回 0
|
|
72
|
+
*/
|
|
73
|
+
export declare function waitForFile(filePath: string, timeoutMs?: number, pollMs?: number): Promise<number>;
|