@ryantest/openclaw-qqbot 0.0.3 → 1.6.6-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.
Files changed (89) hide show
  1. package/README.md +2 -15
  2. package/README.zh.md +3 -16
  3. package/dist/src/admin-resolver.d.ts +12 -6
  4. package/dist/src/admin-resolver.js +69 -34
  5. package/dist/src/api.d.ts +105 -1
  6. package/dist/src/api.js +164 -15
  7. package/dist/src/channel.js +13 -0
  8. package/dist/src/config.js +3 -10
  9. package/dist/src/deliver-debounce.d.ts +74 -0
  10. package/dist/src/deliver-debounce.js +174 -0
  11. package/dist/src/gateway.js +450 -248
  12. package/dist/src/image-server.d.ts +27 -8
  13. package/dist/src/image-server.js +179 -71
  14. package/dist/src/inbound-attachments.d.ts +3 -1
  15. package/dist/src/inbound-attachments.js +28 -14
  16. package/dist/src/outbound-deliver.js +77 -148
  17. package/dist/src/outbound.d.ts +6 -4
  18. package/dist/src/outbound.js +266 -442
  19. package/dist/src/reply-dispatcher.js +4 -4
  20. package/dist/src/request-context.d.ts +18 -0
  21. package/dist/src/request-context.js +30 -0
  22. package/dist/src/slash-commands.js +277 -32
  23. package/dist/src/startup-greeting.d.ts +5 -5
  24. package/dist/src/startup-greeting.js +32 -13
  25. package/dist/src/streaming.d.ts +244 -0
  26. package/dist/src/streaming.js +907 -0
  27. package/dist/src/tools/remind.js +11 -10
  28. package/dist/src/types.d.ts +101 -0
  29. package/dist/src/types.js +17 -1
  30. package/dist/src/update-checker.js +2 -8
  31. package/dist/src/utils/audio-convert.d.ts +9 -0
  32. package/dist/src/utils/audio-convert.js +51 -0
  33. package/dist/src/utils/chunked-upload.d.ts +59 -0
  34. package/dist/src/utils/chunked-upload.js +289 -0
  35. package/dist/src/utils/file-utils.d.ts +7 -1
  36. package/dist/src/utils/file-utils.js +24 -2
  37. package/dist/src/utils/media-send.d.ts +147 -0
  38. package/dist/src/utils/media-send.js +434 -0
  39. package/dist/src/utils/pkg-version.d.ts +5 -0
  40. package/dist/src/utils/pkg-version.js +51 -0
  41. package/dist/src/utils/ssrf-guard.d.ts +25 -0
  42. package/dist/src/utils/ssrf-guard.js +91 -0
  43. package/node_modules/ws/index.js +15 -6
  44. package/node_modules/ws/lib/permessage-deflate.js +6 -6
  45. package/node_modules/ws/lib/websocket-server.js +5 -5
  46. package/node_modules/ws/lib/websocket.js +6 -6
  47. package/node_modules/ws/package.json +4 -3
  48. package/node_modules/ws/wrapper.mjs +14 -1
  49. package/openclaw.plugin.json +1 -0
  50. package/package.json +11 -22
  51. package/scripts/postinstall-link-sdk.js +113 -0
  52. package/scripts/upgrade-via-npm.ps1 +161 -6
  53. package/scripts/upgrade-via-npm.sh +311 -104
  54. package/scripts/upgrade-via-source.sh +117 -0
  55. package/skills/qqbot-media/SKILL.md +9 -5
  56. package/skills/qqbot-remind/SKILL.md +3 -3
  57. package/src/admin-resolver.ts +76 -35
  58. package/src/api.ts +284 -12
  59. package/src/channel.ts +12 -0
  60. package/src/config.ts +3 -10
  61. package/src/deliver-debounce.ts +229 -0
  62. package/src/gateway.ts +277 -67
  63. package/src/image-server.ts +213 -77
  64. package/src/inbound-attachments.ts +32 -15
  65. package/src/outbound-deliver.ts +77 -157
  66. package/src/outbound.ts +304 -451
  67. package/src/reply-dispatcher.ts +4 -4
  68. package/src/request-context.ts +39 -0
  69. package/src/slash-commands.ts +303 -33
  70. package/src/startup-greeting.ts +35 -13
  71. package/src/streaming.ts +1096 -0
  72. package/src/tools/remind.ts +15 -11
  73. package/src/types.ts +111 -0
  74. package/src/update-checker.ts +2 -7
  75. package/src/utils/audio-convert.ts +56 -0
  76. package/src/utils/chunked-upload.ts +419 -0
  77. package/src/utils/file-utils.ts +28 -2
  78. package/src/utils/media-send.ts +563 -0
  79. package/src/utils/pkg-version.ts +54 -0
  80. package/src/utils/ssrf-guard.ts +102 -0
  81. package/clawdbot.plugin.json +0 -16
  82. package/dist/src/user-messages.d.ts +0 -8
  83. package/dist/src/user-messages.js +0 -8
  84. package/moltbot.plugin.json +0 -16
  85. package/scripts/upgrade-via-alt-pkg.sh +0 -307
  86. package/src/bot-logs-2026-03-21T11-21-47(2).txt +0 -46
  87. package/src/gateway.log +0 -43
  88. package/src/openclaw-2026-03-21.log +0 -3729
  89. package/src/user-messages.ts +0 -7
@@ -1,4 +1,5 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { getRequestTarget } from "../request-context.js";
2
3
 
3
4
  // ========== 类型定义 ==========
4
5
 
@@ -6,7 +7,10 @@ interface RemindParams {
6
7
  action: "add" | "list" | "remove";
7
8
  /** 提醒内容(action=add 时必填) */
8
9
  content?: string;
9
- /** 目标地址,格式为上下文中的 to= 值(action=add 时必填) */
10
+ /**
11
+ * 投递目标地址(可选,系统会自动从当前会话上下文获取)。
12
+ * 仅在需要手动指定时填写。
13
+ */
10
14
  to?: string;
11
15
  /**
12
16
  * 时间描述(action=add 时必填)
@@ -40,8 +44,8 @@ const RemindSchema = {
40
44
  to: {
41
45
  type: "string",
42
46
  description:
43
- "投递目标地址,取自上下文中 [QQBot] to= 的值。" +
44
- "私聊格式:user_openid,群聊格式:group:group_openid。action=add 时必填。",
47
+ "投递目标地址(可选)。系统会自动从当前会话获取,通常无需手动填写。" +
48
+ "私聊格式:qqbot:c2c:user_openid,群聊格式:qqbot:group:group_openid。",
45
49
  },
46
50
  time: {
47
51
  type: "string",
@@ -130,9 +134,8 @@ function generateJobName(content: string): string {
130
134
  /**
131
135
  * 构建一次性提醒的 cron 工具参数
132
136
  */
133
- function buildOnceJob(params: RemindParams, delayMs: number) {
137
+ function buildOnceJob(params: RemindParams, delayMs: number, to: string) {
134
138
  const atMs = Date.now() + delayMs;
135
- const to = params.to!;
136
139
  const content = params.content!;
137
140
  const name = params.name || generateJobName(content);
138
141
 
@@ -158,8 +161,7 @@ function buildOnceJob(params: RemindParams, delayMs: number) {
158
161
  /**
159
162
  * 构建周期提醒的 cron 工具参数
160
163
  */
161
- function buildCronJob(params: RemindParams) {
162
- const to = params.to!;
164
+ function buildCronJob(params: RemindParams, to: string) {
163
165
  const content = params.content!;
164
166
  const name = params.name || generateJobName(content);
165
167
  const tz = params.timezone || "Asia/Shanghai";
@@ -249,8 +251,10 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
249
251
  if (!p.content) {
250
252
  return json({ error: "action=add 时 content(提醒内容)为必填参数" });
251
253
  }
252
- if (!p.to) {
253
- return json({ error: "action=add to(目标地址)为必填参数,取自上下文 [QQBot] to= 的值" });
254
+ // 优先使用 AI 传入的 to,否则自动从请求级上下文获取(AsyncLocalStorage)
255
+ const resolvedTo = p.to || getRequestTarget();
256
+ if (!resolvedTo) {
257
+ return json({ error: "action=add 时无法确定投递目标地址。请稍后重试。" });
254
258
  }
255
259
  if (!p.time) {
256
260
  return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
@@ -259,7 +263,7 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
259
263
  // 判断是 cron 表达式还是相对时间
260
264
  if (isCronExpression(p.time)) {
261
265
  // 周期提醒
262
- const cronJob = buildCronJob(p);
266
+ const cronJob = buildCronJob(p, resolvedTo);
263
267
  return json({
264
268
  _instruction: "请立即使用 cron 工具执行以下参数,然后回复用户提醒已设置",
265
269
  cronParams: cronJob,
@@ -281,7 +285,7 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
281
285
  return json({ error: "提醒时间不能少于 30 秒" });
282
286
  }
283
287
 
284
- const onceJob = buildOnceJob(p, delayMs);
288
+ const onceJob = buildOnceJob(p, delayMs, resolvedTo);
285
289
  return json({
286
290
  _instruction: "请立即使用 cron 工具执行以下参数,然后回复用户提醒已设置",
287
291
  cronParams: onceJob,
package/src/types.ts CHANGED
@@ -70,6 +70,46 @@ export interface QQBotAccountConfig {
70
70
  * - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新
71
71
  */
72
72
  upgradeMode?: "doc" | "hot-reload";
73
+ /**
74
+ * 出站消息合并回复(debounce)配置
75
+ * 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
76
+ */
77
+ deliverDebounce?: DeliverDebounceConfig;
78
+ /**
79
+ * 是否启用流式消息(默认 false)
80
+ * 启用后,AI 的回复会以流式形式逐步显示在 QQ 聊天中,
81
+ * 用户可以看到文字逐字出现的打字机效果。
82
+ * 设置为 true 可开启流式消息。
83
+ *
84
+ * 注意:仅 C2C(私聊)支持流式消息 API。
85
+ */
86
+ streaming?: boolean;
87
+ }
88
+
89
+ /**
90
+ * 出站消息合并回复配置
91
+ */
92
+ export interface DeliverDebounceConfig {
93
+ /**
94
+ * 是否启用合并回复(默认 true)
95
+ */
96
+ enabled?: boolean;
97
+ /**
98
+ * 合并窗口时长(毫秒),在此时间内的连续 deliver 会被合并
99
+ * 默认 1500ms
100
+ */
101
+ windowMs?: number;
102
+ /**
103
+ * 最大等待时长(毫秒),从第一条 deliver 开始计算,超过此时间强制发送
104
+ * 防止持续有新 deliver 导致一直不发送
105
+ * 默认 8000ms
106
+ */
107
+ maxWaitMs?: number;
108
+ /**
109
+ * 合并文本之间的分隔符
110
+ * 默认 "\n\n---\n\n"
111
+ */
112
+ separator?: string;
73
113
  }
74
114
 
75
115
  /**
@@ -181,3 +221,74 @@ export interface WSPayload {
181
221
  s?: number;
182
222
  t?: string;
183
223
  }
224
+
225
+
226
+
227
+ // ---- 流式消息常量 ----
228
+
229
+ /** 流式消息输入模式 */
230
+ export const StreamInputMode = {
231
+ /** 每次发送的 content_raw 替换整条消息内容 */
232
+ REPLACE: "replace",
233
+ } as const;
234
+ export type StreamInputMode = (typeof StreamInputMode)[keyof typeof StreamInputMode];
235
+
236
+ /** 流式消息输入状态 */
237
+ export const StreamInputState = {
238
+ /** 正文生成中 */
239
+ GENERATING: 1,
240
+ /** 正文生成结束(终结状态) */
241
+ DONE: 10,
242
+ } as const;
243
+ export type StreamInputState = (typeof StreamInputState)[keyof typeof StreamInputState];
244
+
245
+ /** 流式消息内容类型 */
246
+ export const StreamContentType = {
247
+ MARKDOWN: "markdown",
248
+ } as const;
249
+ export type StreamContentType = (typeof StreamContentType)[keyof typeof StreamContentType];
250
+
251
+ /**
252
+ * 流式消息请求体
253
+ * 对应 StreamReq proto
254
+ */
255
+ export interface StreamMessageRequest {
256
+ /** 输入模式 */
257
+ input_mode: StreamInputMode;
258
+ /** 输入状态 */
259
+ input_state: StreamInputState;
260
+ /** 内容类型 */
261
+ content_type: StreamContentType;
262
+ /** markdown 内容 */
263
+ content_raw: string;
264
+ /** 事件 ID */
265
+ event_id: string;
266
+ /** 原始消息 ID */
267
+ msg_id: string;
268
+ /** 流式消息 ID,首次发送后返回,后续分片需携带 */
269
+ stream_msg_id?: string;
270
+ /** 递增序号 */
271
+ msg_seq: number;
272
+ /** 同一条流式会话内的发送索引,从 0 开始,每次发送前递增;新流式会话重新从 0 开始 */
273
+ index: number;
274
+ }
275
+
276
+ /**
277
+ * 流式消息响应体
278
+ * 对应 StreamRsp proto
279
+ *
280
+ * 成功时返回:{ id, timestamp, extInfo }(无 code/message)
281
+ * 失败时返回:{ code, message }(code > 0)
282
+ */
283
+ export interface StreamMessageResponse {
284
+ /** 错误码,仅失败时存在(> 0 表示失败);成功时不存在 */
285
+ code?: number;
286
+ /** 错误信息,仅失败时存在 */
287
+ message?: string;
288
+ /** 流式消息 ID */
289
+ id?: string;
290
+ /** 时间戳 */
291
+ timestamp?: string;
292
+ /** 扩展信息 */
293
+ extInfo?: Record<string, unknown>;
294
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { createRequire } from "node:module";
12
12
  import https from "node:https";
13
+ import { getPackageVersion } from "./utils/pkg-version.js";
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
 
@@ -21,13 +22,7 @@ const REGISTRIES = [
21
22
  `https://registry.npmmirror.com/${ENCODED_PKG}`,
22
23
  ];
23
24
 
24
- let CURRENT_VERSION = "unknown";
25
- try {
26
- const pkg = require("../package.json");
27
- CURRENT_VERSION = pkg.version ?? "unknown";
28
- } catch {
29
- // fallback
30
- }
25
+ let CURRENT_VERSION = getPackageVersion(import.meta.url);
31
26
 
32
27
  export interface UpdateInfo {
33
28
  current: string;
@@ -517,6 +517,62 @@ export async function audioFileToSilkBase64(filePath: string, directUploadFormat
517
517
  return null;
518
518
  }
519
519
 
520
+ /**
521
+ * 将音频文件转码为 SILK,**输出到临时文件**(供分片上传使用)。
522
+ *
523
+ * 如果文件已经是 QQ 原生格式(WAV/MP3/SILK)或已经是 SILK 编码,
524
+ * 则直接返回原文件路径(不需要转码)。
525
+ *
526
+ * @returns 转码后的文件路径,或 null 表示转码失败
527
+ */
528
+ export async function audioFileToSilkFile(filePath: string, directUploadFormats?: string[]): Promise<string | null> {
529
+ if (!fs.existsSync(filePath)) return null;
530
+
531
+ const buf = fs.readFileSync(filePath);
532
+ if (buf.length === 0) {
533
+ console.error(`[audio-convert] file is empty: ${filePath}`);
534
+ return null;
535
+ }
536
+
537
+ const ext = path.extname(filePath).toLowerCase();
538
+
539
+ // 0. 直传格式 → 直接返回原文件
540
+ const uploadFormats = directUploadFormats ? normalizeFormats(directUploadFormats) : QQ_NATIVE_UPLOAD_FORMATS;
541
+ if (uploadFormats.includes(ext)) {
542
+ console.log(`[audio-convert] direct upload (QQ native format): ${ext} (${buf.length} bytes)`);
543
+ return filePath;
544
+ }
545
+
546
+ // 1. 已经是 SILK 编码 → 直接返回原文件
547
+ if ([".slk", ".slac"].includes(ext)) {
548
+ const stripped = stripAmrHeader(buf);
549
+ const raw = new Uint8Array(stripped.buffer, stripped.byteOffset, stripped.byteLength);
550
+ if (isSilk(raw)) {
551
+ console.log(`[audio-convert] SILK file, direct use: ${filePath} (${buf.length} bytes)`);
552
+ return filePath;
553
+ }
554
+ }
555
+ const rawCheck = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
556
+ const strippedCheck = stripAmrHeader(buf);
557
+ const strippedRaw = new Uint8Array(strippedCheck.buffer, strippedCheck.byteOffset, strippedCheck.byteLength);
558
+ if (isSilk(rawCheck) || isSilk(strippedRaw)) {
559
+ console.log(`[audio-convert] SILK detected by header: ${filePath} (${buf.length} bytes)`);
560
+ return filePath;
561
+ }
562
+
563
+ // 需要转码 → 调用 audioFileToSilkBase64 获取结果,写入临时文件
564
+ const silkBase64 = await audioFileToSilkBase64(filePath, directUploadFormats);
565
+ if (!silkBase64) return null;
566
+
567
+ const silkBuffer = Buffer.from(silkBase64, "base64");
568
+ const os = await import("node:os");
569
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "voice-silk-"));
570
+ const tmpFile = path.join(tmpDir, `voice${Date.now()}.silk`);
571
+ fs.writeFileSync(tmpFile, silkBuffer);
572
+ console.log(`[audio-convert] SILK written to temp file: ${tmpFile} (${silkBuffer.length} bytes)`);
573
+ return tmpFile;
574
+ }
575
+
520
576
  /**
521
577
  * 等待文件就绪(轮询直到文件出现且大小稳定)
522
578
  * 用于 TTS 生成后等待文件写入完成