@sliverp/qqbot 1.4.2 → 1.4.4

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.
Files changed (42) hide show
  1. package/README.md +32 -1
  2. package/node_modules/silk-wasm/LICENSE +21 -0
  3. package/node_modules/silk-wasm/README.md +85 -0
  4. package/node_modules/silk-wasm/lib/index.cjs +16 -0
  5. package/node_modules/silk-wasm/lib/index.d.ts +70 -0
  6. package/node_modules/silk-wasm/lib/index.mjs +16 -0
  7. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  8. package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
  9. package/node_modules/silk-wasm/package.json +39 -0
  10. package/package.json +17 -5
  11. package/src/gateway.ts +71 -4
  12. package/src/utils/audio-convert.ts +138 -0
  13. package/dist/index.d.ts +0 -17
  14. package/dist/index.js +0 -22
  15. package/dist/src/api.d.ts +0 -194
  16. package/dist/src/api.js +0 -555
  17. package/dist/src/channel.d.ts +0 -3
  18. package/dist/src/channel.js +0 -281
  19. package/dist/src/config.d.ts +0 -25
  20. package/dist/src/config.js +0 -156
  21. package/dist/src/gateway.d.ts +0 -18
  22. package/dist/src/gateway.js +0 -1475
  23. package/dist/src/image-server.d.ts +0 -62
  24. package/dist/src/image-server.js +0 -401
  25. package/dist/src/known-users.d.ts +0 -100
  26. package/dist/src/known-users.js +0 -264
  27. package/dist/src/onboarding.d.ts +0 -10
  28. package/dist/src/onboarding.js +0 -195
  29. package/dist/src/outbound.d.ts +0 -149
  30. package/dist/src/outbound.js +0 -476
  31. package/dist/src/proactive.d.ts +0 -170
  32. package/dist/src/proactive.js +0 -398
  33. package/dist/src/runtime.d.ts +0 -3
  34. package/dist/src/runtime.js +0 -10
  35. package/dist/src/session-store.d.ts +0 -49
  36. package/dist/src/session-store.js +0 -242
  37. package/dist/src/types.d.ts +0 -116
  38. package/dist/src/types.js +0 -1
  39. package/dist/src/utils/image-size.d.ts +0 -51
  40. package/dist/src/utils/image-size.js +0 -234
  41. package/dist/src/utils/payload.d.ts +0 -112
  42. package/dist/src/utils/payload.js +0 -186
@@ -0,0 +1,138 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { decode, isSilk } from "silk-wasm";
4
+
5
+ /**
6
+ * 检查文件是否为 SILK 格式(QQ/微信语音常用格式)
7
+ * QQ 语音文件通常以 .amr 扩展名保存,但实际编码可能是 SILK v3
8
+ * SILK 文件头部标识: 0x02 "#!SILK_V3"
9
+ */
10
+ function isSilkFile(filePath: string): boolean {
11
+ try {
12
+ const buf = fs.readFileSync(filePath);
13
+ return isSilk(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * 将 PCM (s16le) 数据封装为 WAV 文件格式
21
+ * WAV = 44 字节 RIFF 头 + PCM 原始数据
22
+ */
23
+ function pcmToWav(pcmData: Uint8Array, sampleRate: number, channels: number = 1, bitsPerSample: number = 16): Buffer {
24
+ const byteRate = sampleRate * channels * (bitsPerSample / 8);
25
+ const blockAlign = channels * (bitsPerSample / 8);
26
+ const dataSize = pcmData.length;
27
+ const headerSize = 44;
28
+ const fileSize = headerSize + dataSize;
29
+
30
+ const buffer = Buffer.alloc(fileSize);
31
+
32
+ // RIFF header
33
+ buffer.write("RIFF", 0);
34
+ buffer.writeUInt32LE(fileSize - 8, 4);
35
+ buffer.write("WAVE", 8);
36
+
37
+ // fmt sub-chunk
38
+ buffer.write("fmt ", 12);
39
+ buffer.writeUInt32LE(16, 16); // sub-chunk size
40
+ buffer.writeUInt16LE(1, 20); // PCM format
41
+ buffer.writeUInt16LE(channels, 22);
42
+ buffer.writeUInt32LE(sampleRate, 24);
43
+ buffer.writeUInt32LE(byteRate, 28);
44
+ buffer.writeUInt16LE(blockAlign, 32);
45
+ buffer.writeUInt16LE(bitsPerSample, 34);
46
+
47
+ // data sub-chunk
48
+ buffer.write("data", 36);
49
+ buffer.writeUInt32LE(dataSize, 40);
50
+ Buffer.from(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength).copy(buffer, headerSize);
51
+
52
+ return buffer;
53
+ }
54
+
55
+ /**
56
+ * 去除 QQ 语音文件的 AMR 头(如果存在)
57
+ * QQ 的 .amr 文件可能在 SILK 数据前有 "#!AMR\n" 头(6 字节)
58
+ * 需要去除后才能被 silk-wasm 正确解码
59
+ */
60
+ function stripAmrHeader(buf: Buffer): Buffer {
61
+ const AMR_HEADER = Buffer.from("#!AMR\n");
62
+ if (buf.length > 6 && buf.subarray(0, 6).equals(AMR_HEADER)) {
63
+ return buf.subarray(6);
64
+ }
65
+ return buf;
66
+ }
67
+
68
+ /**
69
+ * 将 SILK/AMR 语音文件转换为 WAV 格式
70
+ *
71
+ * @param inputPath 输入文件路径(.amr / .silk / .slk)
72
+ * @param outputDir 输出目录(默认与输入文件同目录)
73
+ * @returns 转换后的 WAV 文件路径,失败返回 null
74
+ */
75
+ export async function convertSilkToWav(
76
+ inputPath: string,
77
+ outputDir?: string,
78
+ ): Promise<{ wavPath: string; duration: number } | null> {
79
+ if (!fs.existsSync(inputPath)) {
80
+ return null;
81
+ }
82
+
83
+ const fileBuf = fs.readFileSync(inputPath);
84
+
85
+ // 去除可能的 AMR 头
86
+ const strippedBuf = stripAmrHeader(fileBuf);
87
+
88
+ // 转为 Uint8Array 以兼容 silk-wasm 类型要求
89
+ const rawData = new Uint8Array(strippedBuf.buffer, strippedBuf.byteOffset, strippedBuf.byteLength);
90
+
91
+ // 验证是否为 SILK 格式
92
+ if (!isSilk(rawData)) {
93
+ return null;
94
+ }
95
+
96
+ // SILK 解码为 PCM (s16le)
97
+ // QQ 语音通常采样率为 24000Hz
98
+ const sampleRate = 24000;
99
+ const result = await decode(rawData, sampleRate);
100
+
101
+ // PCM → WAV
102
+ const wavBuffer = pcmToWav(result.data, sampleRate);
103
+
104
+ // 写入 WAV 文件
105
+ const dir = outputDir || path.dirname(inputPath);
106
+ if (!fs.existsSync(dir)) {
107
+ fs.mkdirSync(dir, { recursive: true });
108
+ }
109
+ const baseName = path.basename(inputPath, path.extname(inputPath));
110
+ const wavPath = path.join(dir, `${baseName}.wav`);
111
+ fs.writeFileSync(wavPath, wavBuffer);
112
+
113
+ return { wavPath, duration: result.duration };
114
+ }
115
+
116
+ /**
117
+ * 判断是否为语音附件(根据 content_type 或文件扩展名)
118
+ */
119
+ export function isVoiceAttachment(att: { content_type?: string; filename?: string }): boolean {
120
+ if (att.content_type === "voice" || att.content_type?.startsWith("audio/")) {
121
+ return true;
122
+ }
123
+ const ext = att.filename ? path.extname(att.filename).toLowerCase() : "";
124
+ return [".amr", ".silk", ".slk"].includes(ext);
125
+ }
126
+
127
+ /**
128
+ * 格式化语音时长为可读字符串
129
+ */
130
+ export function formatDuration(durationMs: number): string {
131
+ const seconds = Math.round(durationMs / 1000);
132
+ if (seconds < 60) {
133
+ return `${seconds}秒`;
134
+ }
135
+ const minutes = Math.floor(seconds / 60);
136
+ const remainSeconds = seconds % 60;
137
+ return remainSeconds > 0 ? `${minutes}分${remainSeconds}秒` : `${minutes}分钟`;
138
+ }
package/dist/index.d.ts DELETED
@@ -1,17 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- declare const plugin: {
3
- id: string;
4
- name: string;
5
- description: string;
6
- configSchema: unknown;
7
- register(api: OpenClawPluginApi): void;
8
- };
9
- export default plugin;
10
- export { qqbotPlugin } from "./src/channel.js";
11
- export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js";
12
- export { qqbotOnboardingAdapter } from "./src/onboarding.js";
13
- export * from "./src/types.js";
14
- export * from "./src/api.js";
15
- export * from "./src/config.js";
16
- export * from "./src/gateway.js";
17
- export * from "./src/outbound.js";
package/dist/index.js DELETED
@@ -1,22 +0,0 @@
1
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
- import { qqbotPlugin } from "./src/channel.js";
3
- import { setQQBotRuntime } from "./src/runtime.js";
4
- const plugin = {
5
- id: "qqbot",
6
- name: "QQ Bot",
7
- description: "QQ Bot channel plugin",
8
- configSchema: emptyPluginConfigSchema(),
9
- register(api) {
10
- setQQBotRuntime(api.runtime);
11
- api.registerChannel({ plugin: qqbotPlugin });
12
- },
13
- };
14
- export default plugin;
15
- export { qqbotPlugin } from "./src/channel.js";
16
- export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js";
17
- export { qqbotOnboardingAdapter } from "./src/onboarding.js";
18
- export * from "./src/types.js";
19
- export * from "./src/api.js";
20
- export * from "./src/config.js";
21
- export * from "./src/gateway.js";
22
- export * from "./src/outbound.js";
package/dist/src/api.d.ts DELETED
@@ -1,194 +0,0 @@
1
- /**
2
- * QQ Bot API 鉴权和请求封装
3
- */
4
- /**
5
- * 初始化 API 配置
6
- * @param options.markdownSupport - 是否支持 markdown 消息(默认 false,需要机器人具备该权限才能启用)
7
- */
8
- export declare function initApiConfig(options: {
9
- markdownSupport?: boolean;
10
- }): void;
11
- /**
12
- * 获取当前是否支持 markdown
13
- */
14
- export declare function isMarkdownSupport(): boolean;
15
- /**
16
- * 获取 AccessToken(带缓存 + singleflight 并发安全)
17
- *
18
- * 使用 singleflight 模式:当多个请求同时发现 Token 过期时,
19
- * 只有第一个请求会真正去获取新 Token,其他请求复用同一个 Promise。
20
- */
21
- export declare function getAccessToken(appId: string, clientSecret: string): Promise<string>;
22
- /**
23
- * 清除 Token 缓存
24
- */
25
- export declare function clearTokenCache(): void;
26
- /**
27
- * 获取 Token 缓存状态(用于监控)
28
- */
29
- export declare function getTokenStatus(): {
30
- status: "valid" | "expired" | "refreshing" | "none";
31
- expiresAt: number | null;
32
- };
33
- /**
34
- * 获取并递增消息序号
35
- * 返回的 seq 会基于时间戳,避免进程重启后重复
36
- */
37
- export declare function getNextMsgSeq(msgId: string): number;
38
- /**
39
- * API 请求封装
40
- */
41
- export declare function apiRequest<T = unknown>(accessToken: string, method: string, path: string, body?: unknown): Promise<T>;
42
- /**
43
- * 获取 WebSocket Gateway URL
44
- */
45
- export declare function getGatewayUrl(accessToken: string): Promise<string>;
46
- /**
47
- * 消息响应
48
- */
49
- export interface MessageResponse {
50
- id: string;
51
- timestamp: number | string;
52
- }
53
- /**
54
- * 发送 C2C 单聊消息
55
- */
56
- export declare function sendC2CMessage(accessToken: string, openid: string, content: string, msgId?: string): Promise<MessageResponse>;
57
- /**
58
- * 发送 C2C 输入状态提示(告知用户机器人正在输入)
59
- */
60
- export declare function sendC2CInputNotify(accessToken: string, openid: string, msgId?: string, inputSecond?: number): Promise<void>;
61
- /**
62
- * 发送频道消息(不支持流式)
63
- */
64
- export declare function sendChannelMessage(accessToken: string, channelId: string, content: string, msgId?: string): Promise<{
65
- id: string;
66
- timestamp: string;
67
- }>;
68
- /**
69
- * 发送群聊消息
70
- */
71
- export declare function sendGroupMessage(accessToken: string, groupOpenid: string, content: string, msgId?: string): Promise<MessageResponse>;
72
- /**
73
- * 主动发送 C2C 单聊消息(不需要 msg_id,每月限 4 条/用户)
74
- *
75
- * 注意:
76
- * 1. 内容不能为空(对应 markdown.content 字段)
77
- * 2. 不支持流式发送
78
- */
79
- export declare function sendProactiveC2CMessage(accessToken: string, openid: string, content: string): Promise<{
80
- id: string;
81
- timestamp: number;
82
- }>;
83
- /**
84
- * 主动发送群聊消息(不需要 msg_id,每月限 4 条/群)
85
- *
86
- * 注意:
87
- * 1. 内容不能为空(对应 markdown.content 字段)
88
- * 2. 不支持流式发送
89
- */
90
- export declare function sendProactiveGroupMessage(accessToken: string, groupOpenid: string, content: string): Promise<{
91
- id: string;
92
- timestamp: string;
93
- }>;
94
- /**
95
- * 媒体文件类型
96
- */
97
- export declare enum MediaFileType {
98
- IMAGE = 1,
99
- VIDEO = 2,
100
- VOICE = 3,
101
- FILE = 4
102
- }
103
- /**
104
- * 上传富媒体文件的响应
105
- */
106
- export interface UploadMediaResponse {
107
- file_uuid: string;
108
- file_info: string;
109
- ttl: number;
110
- id?: string;
111
- }
112
- /**
113
- * 上传富媒体文件到 C2C 单聊
114
- * @param url - 公网可访问的图片 URL(与 fileData 二选一)
115
- * @param fileData - Base64 编码的文件内容(与 url 二选一)
116
- */
117
- export declare function uploadC2CMedia(accessToken: string, openid: string, fileType: MediaFileType, url?: string, fileData?: string, srvSendMsg?: boolean): Promise<UploadMediaResponse>;
118
- /**
119
- * 上传富媒体文件到群聊
120
- * @param url - 公网可访问的图片 URL(与 fileData 二选一)
121
- * @param fileData - Base64 编码的文件内容(与 url 二选一)
122
- */
123
- export declare function uploadGroupMedia(accessToken: string, groupOpenid: string, fileType: MediaFileType, url?: string, fileData?: string, srvSendMsg?: boolean): Promise<UploadMediaResponse>;
124
- /**
125
- * 发送 C2C 单聊富媒体消息
126
- */
127
- export declare function sendC2CMediaMessage(accessToken: string, openid: string, fileInfo: string, msgId?: string, content?: string): Promise<{
128
- id: string;
129
- timestamp: number;
130
- }>;
131
- /**
132
- * 发送群聊富媒体消息
133
- */
134
- export declare function sendGroupMediaMessage(accessToken: string, groupOpenid: string, fileInfo: string, msgId?: string, content?: string): Promise<{
135
- id: string;
136
- timestamp: string;
137
- }>;
138
- /**
139
- * 发送带图片的 C2C 单聊消息(封装上传+发送)
140
- * @param imageUrl - 图片来源,支持:
141
- * - 公网 URL: https://example.com/image.png
142
- * - Base64 Data URL: data:image/png;base64,xxxxx
143
- */
144
- export declare function sendC2CImageMessage(accessToken: string, openid: string, imageUrl: string, msgId?: string, content?: string): Promise<{
145
- id: string;
146
- timestamp: number;
147
- }>;
148
- /**
149
- * 发送带图片的群聊消息(封装上传+发送)
150
- * @param imageUrl - 图片来源,支持:
151
- * - 公网 URL: https://example.com/image.png
152
- * - Base64 Data URL: data:image/png;base64,xxxxx
153
- */
154
- export declare function sendGroupImageMessage(accessToken: string, groupOpenid: string, imageUrl: string, msgId?: string, content?: string): Promise<{
155
- id: string;
156
- timestamp: string;
157
- }>;
158
- /**
159
- * 后台 Token 刷新配置
160
- */
161
- interface BackgroundTokenRefreshOptions {
162
- /** 提前刷新时间(毫秒,默认 5 分钟) */
163
- refreshAheadMs?: number;
164
- /** 随机偏移范围(毫秒,默认 0-30 秒) */
165
- randomOffsetMs?: number;
166
- /** 最小刷新间隔(毫秒,默认 1 分钟) */
167
- minRefreshIntervalMs?: number;
168
- /** 失败后重试间隔(毫秒,默认 5 秒) */
169
- retryDelayMs?: number;
170
- /** 日志函数 */
171
- log?: {
172
- info: (msg: string) => void;
173
- error: (msg: string) => void;
174
- debug?: (msg: string) => void;
175
- };
176
- }
177
- /**
178
- * 启动后台 Token 刷新
179
- * 在后台定时刷新 Token,避免请求时才发现过期
180
- *
181
- * @param appId 应用 ID
182
- * @param clientSecret 应用密钥
183
- * @param options 配置选项
184
- */
185
- export declare function startBackgroundTokenRefresh(appId: string, clientSecret: string, options?: BackgroundTokenRefreshOptions): void;
186
- /**
187
- * 停止后台 Token 刷新
188
- */
189
- export declare function stopBackgroundTokenRefresh(): void;
190
- /**
191
- * 检查后台 Token 刷新是否正在运行
192
- */
193
- export declare function isBackgroundTokenRefreshRunning(): boolean;
194
- export {};