@karinjs/adapter-qqbot 2.2.1 → 3.0.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/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { AdapterBase, AdapterType, Contact, CreateGroupFolderResponse, DownloadFileOptions, DownloadFileResponse, ElementTypes, ForwardOptions, GetAiCharactersResponse, GetAtAllCountResponse, GetGroupFileListResponse, GetGroupFileSystemInfoResponse, GetGroupHighlightsResponse, GetGroupMuteListResponse, GetRkeyResponse, GroupInfo, GroupMemberInfo, LogMethodNames, Message, MessageResponse, NodeElement, QQGroupHonorInfo, SendMsgResults, Sender, UserInfo, karinToQQBot } from "node-karin";
2
2
  import FormData from "form-data";
3
-
4
3
  //#region src/core/api/types.d.ts
5
4
  /** 上传富媒体文件场景 */
6
5
  type Scene = 'user' | 'group';
@@ -8,8 +7,8 @@ type Scene = 'user' | 'group';
8
7
  type MediaType = 'image' | 'video' | 'record' | 'file';
9
8
  /** 发送QQ消息请求参数基类 */
10
9
  interface SendQQMessageRequest {
11
- /** 消息类型:0 是文本,2 是 markdown, 3 ark,4 embed,7 media 富媒体 */
12
- msg_type: 0 | 2 | 3 | 4 | 7;
10
+ /** 消息类型:0 是文本,2 是 markdown, 3 ark,4 embed,6 输入中状态(仅单聊),7 media 富媒体 */
11
+ msg_type: 0 | 2 | 3 | 4 | 6 | 7;
13
12
  }
14
13
  /** 消息引用对象 */
15
14
  interface MessageReference {
@@ -74,7 +73,9 @@ interface SendQQArkMessageRequest extends SendQQMessageRequest, QQMessageID {
74
73
  msg_type: 3;
75
74
  /** ark 消息内容 */
76
75
  ark: {
77
- /** ark 模版id */template_id: number; /** 模版内变量与填充值的kv映射 tips: 未经测试 先any */
76
+ /** ark 模版id */
77
+ template_id: number;
78
+ /** 模版内变量与填充值的kv映射 tips: 未经测试 先any */
78
79
  kv: any[];
79
80
  };
80
81
  }
@@ -84,7 +85,49 @@ interface SendQQMediaMessageRequest extends SendQQMessageRequest, QQMessageID {
84
85
  msg_type: 7;
85
86
  /** media 消息内容 */
86
87
  media: {
87
- /** 文件信息,用于发消息接口的 media 字段使用 */file_info: string;
88
+ /** 文件信息,用于发消息接口的 media 字段使用 */
89
+ file_info: string;
90
+ };
91
+ }
92
+ /** 发送QQ流式消息请求参数(POST /v2/users/{openid}/stream_messages,仅单聊) */
93
+ interface SendQQStreamMessageRequest {
94
+ /**
95
+ * 输入模式:
96
+ * - append 增量追加(默认),content_raw 为本次新增内容
97
+ * - replace 全量替换,content_raw 为全量内容,且必须保持已下发内容前缀不变
98
+ */
99
+ input_mode?: 'append' | 'replace';
100
+ /** 输入状态:1 生成中 / 10 正文结束 */
101
+ input_state: 1 | 10;
102
+ /** 流式序号,从 0 开始逐帧递增,同一次流的 msg_seq 保持一致 */
103
+ index: number;
104
+ /** 内容类型,默认 text */
105
+ content_type?: 'text' | 'markdown';
106
+ /** 内容原文 */
107
+ content_raw: string;
108
+ /** 首帧由服务端生成并返回,后续帧需携带 */
109
+ stream_msg_id?: string;
110
+ /** 前置事件 ID(被动消息,与 msg_id 二选一) */
111
+ event_id?: string;
112
+ /** 前置消息 ID(被动消息,与 event_id 二选一) */
113
+ msg_id?: string;
114
+ /** 回复序号,与 msg_id 联合使用 */
115
+ msg_seq?: number;
116
+ /** 互动召回消息,与 msg_id、event_id 互斥 */
117
+ is_wakeup?: boolean;
118
+ }
119
+ /** 发送QQ流式消息响应 */
120
+ interface SendQQStreamMessageResponse {
121
+ /** 流式消息 ID,后续帧作为 stream_msg_id 携带 */
122
+ id: string;
123
+ /** 发送时间 */
124
+ timestamp: number | string;
125
+ /** 剩余可发送字数 */
126
+ remain_msg_len?: number;
127
+ /** 扩展信息 */
128
+ ext_info?: {
129
+ /** 当前发送消息可被引用时使用的索引 */
130
+ ref_idx?: string;
88
131
  };
89
132
  }
90
133
  /** 发送QQ消息请求参数 */
@@ -138,7 +181,8 @@ interface Embeds {
138
181
  prompt: string;
139
182
  /** 缩略图 */
140
183
  thumbnail: {
141
- /** 图片地址 */url: string;
184
+ /** 图片地址 */
185
+ url: string;
142
186
  };
143
187
  /** embed 字段数据 */
144
188
  fields: Record<string, any>[];
@@ -156,7 +200,8 @@ interface Ark {
156
200
  /** 模版内变量与填充值的kv映射 */
157
201
  kv: {
158
202
  key: string;
159
- value: string; /** 这个嵌套是认真的嘛... */
203
+ value: string;
204
+ /** 这个嵌套是认真的嘛... */
160
205
  obj: {
161
206
  obj_kv: {
162
207
  key: string;
@@ -215,9 +260,57 @@ interface SendQQMsgResponse {
215
260
  timestamp: number | string;
216
261
  /** 扩展信息;部分 QQ 消息发送响应会返回可用于 message_reference 的 REFIDX。 */
217
262
  ext_info?: {
218
- /** 当前发送消息可被引用时使用的索引。 */ref_idx?: string;
263
+ /** 当前发送消息可被引用时使用的索引。 */
264
+ ref_idx?: string;
219
265
  };
220
266
  }
267
+ /** 获取群基础信息接口响应(GET /v2/groups/{group_openid}/info,白名单接口) */
268
+ interface QQGroupInfoResponse {
269
+ /** 群 openid */
270
+ group_openid: string;
271
+ /** 群名称 */
272
+ group_name: string;
273
+ /** 群简介 */
274
+ group_finger_memo?: string;
275
+ /** 群分类文本 */
276
+ group_class_text?: string;
277
+ /** 群标签 */
278
+ group_tags?: string[];
279
+ /** 群成员人数 */
280
+ group_member_num?: number;
281
+ }
282
+ /** 获取机器人群内状态接口响应(GET /v2/groups/{group_openid}/bot_state,白名单接口) */
283
+ interface GroupBotStateResponse {
284
+ /** 机器人在群内的 member_openid */
285
+ member_openid: string;
286
+ /** 入群时间,RFC3339 格式 */
287
+ joined_at?: string;
288
+ /** 是否允许主动发消息 */
289
+ allow_proactive_msg?: boolean;
290
+ /** 接收消息设置:all / only_mention / mention_and_context */
291
+ recv_msg_setting?: string;
292
+ /** 机器人在群内的角色:owner / admin / member */
293
+ member_role?: 'owner' | 'admin' | 'member';
294
+ }
295
+ /**
296
+ * 获取群成员详情接口响应(GET /v2/groups/{group_openid}/members/{member_openid})
297
+ *
298
+ * 该接口未出现在公开文档中,仅在腾讯官方 openclaw 插件
299
+ * (@tencent-connect/openclaw-qqbot v2.0.0)中被作为一等接口使用,返回结构未公开,
300
+ * 字段全部按可选处理,实际以平台返回为准。
301
+ */
302
+ interface QQGroupMemberResponse {
303
+ /** 群成员 openid */
304
+ member_openid?: string;
305
+ /** 群成员昵称 */
306
+ nick?: string;
307
+ /** 入群时间,RFC3339 格式 */
308
+ joined_at?: string;
309
+ /** 群内角色:owner / admin / member */
310
+ member_role?: 'owner' | 'admin' | 'member';
311
+ /** 其余未公开字段原样保留 */
312
+ [key: string]: unknown;
313
+ }
221
314
  /** 发送频道消息后响应 */
222
315
  interface SendGuildResponse {
223
316
  /** 消息唯一 ID */
@@ -266,6 +359,22 @@ declare class MessagesApi extends Http {
266
359
  sendFriendMsg(openid: string, body: SendQQMsg): Promise<SendQQMsgResponse>;
267
360
  /** 群聊消息 */
268
361
  sendGroupMsg(groupOpenid: string, body: SendQQMsg): Promise<SendQQMsgResponse>;
362
+ /**
363
+ * 单聊输入中状态(msg_type=6,仅单聊)
364
+ * @param openid 用户 openid
365
+ * @param inputSecond 展示时长(秒),默认 60
366
+ * @param passive 被动消息参数(msg_id / event_id / msg_seq),可选
367
+ */
368
+ sendFriendInputNotify(openid: string, inputSecond?: number, passive?: QQMessageID): Promise<SendQQMsgResponse>;
369
+ /**
370
+ * 单聊流式消息(仅单聊)
371
+ *
372
+ * 首帧不带 stream_msg_id,响应的 id 作为后续帧的 stream_msg_id 回传;
373
+ * 同一次流 index 从 0 递增、msg_seq 保持一致;建议帧间隔 300~500ms。
374
+ * @param openid 用户 openid
375
+ * @param body 流式消息帧
376
+ */
377
+ sendFriendStreamMsg(openid: string, body: SendQQStreamMessageRequest): Promise<SendQQStreamMessageResponse>;
269
378
  /** 文字子频道消息 */
270
379
  sendChannelMsg(channelId: string, body: SendGuildMsg | FormData): Promise<SendGuildResponse>;
271
380
  /**
@@ -387,6 +496,34 @@ declare class MetaApi extends Http {
387
496
  }>;
388
497
  }
389
498
  //#endregion
499
+ //#region src/core/api/groups.d.ts
500
+ /**
501
+ * QQ 群信息接口
502
+ *
503
+ * 注意:以下接口均为白名单接口,未开通权限时平台返回错误码 11253,
504
+ * 需要向平台运营申请后可用。
505
+ */
506
+ declare class GroupsApi extends Http {
507
+ /**
508
+ * 获取群基础信息
509
+ * @param groupOpenid 群 openid
510
+ */
511
+ getGroupInfo(groupOpenid: string): Promise<QQGroupInfoResponse>;
512
+ /**
513
+ * 获取机器人在群内的状态(角色、入群时间、主动消息开关等)
514
+ * @param groupOpenid 群 openid
515
+ */
516
+ getBotState(groupOpenid: string): Promise<GroupBotStateResponse>;
517
+ /**
518
+ * 获取群成员详情
519
+ *
520
+ * 未公开文档,来自腾讯官方 openclaw 插件(v2.0.0)的接口速查。
521
+ * @param groupOpenid 群 openid
522
+ * @param memberOpenid 群成员 openid
523
+ */
524
+ getGroupMember(groupOpenid: string, memberOpenid: string): Promise<QQGroupMemberResponse>;
525
+ }
526
+ //#endregion
390
527
  //#region src/core/api/index.d.ts
391
528
  /**
392
529
  * QQBot 官方 API 门面
@@ -396,6 +533,7 @@ declare class MetaApi extends Http {
396
533
  * - media:富媒体上传
397
534
  * - interaction:按钮回调 ack
398
535
  * - meta:@me / dms / gateway
536
+ * - groups:群基础信息 / 机器人群内状态(白名单接口)
399
537
  *
400
538
  * 请求体构造器 {@link buildQQMsg} / {@link buildGuildMsg} 独立导出,避免与 HTTP 调用耦合
401
539
  */
@@ -405,6 +543,7 @@ declare class QQBotApi {
405
543
  readonly media: MediaApi;
406
544
  readonly interaction: InteractionApi;
407
545
  readonly meta: MetaApi;
546
+ readonly groups: GroupsApi;
408
547
  /** 请求体构造器(静态便利) */
409
548
  readonly qq: {
410
549
  text(content: string): SendQQTextMessageRequest;
@@ -422,6 +561,85 @@ declare class QQBotApi {
422
561
  constructor(axios: AxiosInstance);
423
562
  }
424
563
  //#endregion
564
+ //#region src/types/config.d.ts
565
+ /**
566
+ * 自定义连接代理配置
567
+ */
568
+ interface QQBotProxyConfig {
569
+ /** 正式环境 API */
570
+ prodApi: string;
571
+ /** 测试环境 API */
572
+ sandboxApi: string;
573
+ /** 调用凭证 API */
574
+ tokenApi: string;
575
+ /** 正式环境 WebSocket 网关 */
576
+ prodWs: string;
577
+ /** 测试环境 WebSocket 网关 */
578
+ sandboxWs: string;
579
+ }
580
+ /**
581
+ * 消息缓存存储分级。
582
+ *
583
+ * - minimal: 只存 ID 映射与引用关系(reply 段),撤回 / 引用解析可用,正文不落库;
584
+ * - standard: 额外保存文本、表情与媒体本地路径,markdown 原文不落库;
585
+ * - full: 全部可缓存消息段原样保存。
586
+ */
587
+ type MessageCacheLevel = 'minimal' | 'standard' | 'full';
588
+ /**
589
+ * 单个 QQBot 配置
590
+ */
591
+ interface QQBotConfig {
592
+ /** 机器人名称 */
593
+ name: string;
594
+ /** 机器人 ID */
595
+ appId: string;
596
+ /** 机器人密钥 */
597
+ secret: string;
598
+ /** 自定义连接代理 */
599
+ proxy: QQBotProxyConfig;
600
+ /** 开启沙盒环境 */
601
+ sandbox: boolean;
602
+ /** 开启 QQ 场景能力(单聊 / 群聊) */
603
+ qqEnable: boolean;
604
+ /** 开启频道场景能力 */
605
+ guildEnable: boolean;
606
+ /** 频道场景模式 0 公域 / 1 私域 */
607
+ guildMode: 0 | 1;
608
+ /** 收到消息后对文本进行表达式处理 */
609
+ regex: {
610
+ reg: string | RegExp;
611
+ rep: string;
612
+ }[];
613
+ /** 按钮自动化 */
614
+ keyboard: {
615
+ /** 是否将文本中的 URL 自动转换为 keyboard 按钮 */
616
+ enable: boolean;
617
+ };
618
+ /** Markdown 通道 */
619
+ markdown: {
620
+ /** 是否启用 Markdown 通道发送消息 */
621
+ enable: boolean;
622
+ };
623
+ /** 消息缓存配置 */
624
+ messageCache: {
625
+ /** 是否启用数据库消息缓存,用于 bot.getMsg */
626
+ enable: boolean;
627
+ /** 是否缓存机器人自己发送的消息 */
628
+ self: boolean;
629
+ /** 存储分级,控制单条消息的落库体积 */
630
+ level: MessageCacheLevel;
631
+ /** 缓存保留小时数;多个 bot 同时开启时取最大值 */
632
+ ttlHours: number;
633
+ /** 消息缓存最大行数,超出后删除最旧;多个 bot 同时开启时取最大值 */
634
+ maxRows: number;
635
+ };
636
+ /** 事件接收配置 */
637
+ event: {
638
+ /** 接收方式 0 关闭 / 1 webhook / 2 ws */
639
+ type: 0 | 1 | 2;
640
+ };
641
+ }
642
+ //#endregion
425
643
  //#region src/core/storage/message-store/types.d.ts
426
644
  interface CachedMessage {
427
645
  messageId: string;
@@ -431,63 +649,75 @@ interface CachedMessage {
431
649
  sender: Sender;
432
650
  elements: ElementTypes[];
433
651
  }
652
+ /** 保存消息时的附加标记。 */
653
+ interface SaveOptions {
654
+ /** QQ 客户端引用使用的 `msg_idx`(REFIDX),作为消息的第二查询索引。 */
655
+ refIdx?: string;
656
+ /** 是否是机器人自己发送的消息,用于单聊撤回判定。 */
657
+ isSelf?: boolean;
658
+ /** 存储分级;缺省按 standard 处理。 */
659
+ level?: MessageCacheLevel;
660
+ }
434
661
  //#endregion
435
662
  //#region src/core/storage/message-store/store.d.ts
436
663
  /**
437
664
  * QQBot 收到的消息缓存。
438
665
  *
439
- * 热路径只写内存并入队,不等待 SQLite:
440
- * - `getMsg` 优先查内存,插件在同一事件回调内可立即命中;
441
- * - SQLite 在后台按批次事务落库,避免一条消息一个事务拖慢事件分发;
442
- * - 元素表只保存 `type + value`,避免宽表 NULL 和多详情表多次 INSERT。
666
+ * SQLite 是唯一数据层,没有内存镜像:
667
+ * - 写入热路径只入队,后台按批次事务落库,每条消息稳定 2~3 次语句
668
+ * 往返(prepared statement 复用);
669
+ * - 所有读取直接查 SQLite(WAL + 整数 hash 索引,点查亚毫秒级);
670
+ * 读取前先排空待写队列,保证"读到自己刚写的数据"。
443
671
  */
444
672
  declare class MessageStore {
445
673
  private db?;
446
674
  private readonly ready;
447
- private lastCleanup;
448
675
  private shouldVacuum;
449
676
  private flushTimer?;
450
- private flushing;
677
+ private flushActive?;
451
678
  private mediaTimer?;
452
679
  private mediaActive;
453
- private readonly pending;
454
- private readonly pendingMedia;
680
+ private pending;
681
+ private pendingMedia;
455
682
  private readonly pendingMediaKeys;
456
- private readonly memory;
457
- private readonly directIndex;
458
- private readonly aliasIndex;
459
- private readonly scopedAliasIndex;
460
- private readonly botRefCache;
683
+ private readonly statements;
461
684
  private readonly contactRefCache;
462
685
  private readonly senderRefCache;
686
+ /** 当前生效的缓存保留时长;由配置解析,多 bot 取最大值。 */
687
+ private ttlMs;
688
+ /** 当前生效的消息行数硬上限;由配置解析,多 bot 取最大值。 */
689
+ private maxRows;
463
690
  constructor();
464
691
  /**
465
692
  * 立即执行一次过期消息和本地媒体缓存清理。
466
693
  *
467
- * 该方法交给 Karin `task` 调度调用;类内部只保留清理能力,不自行维护常驻定时器。
694
+ * 该方法交给 Karin `task` 调度调用;读写路径不再内嵌清理,避免尾延迟。
468
695
  *
469
696
  * @returns 清理完成 Promise。
470
697
  */
471
698
  cleanupExpired(): Promise<void>;
472
699
  /**
473
- * 保存实际消息,并为 QQ 的 `msg_idx` 等引用索引建立轻量映射。
700
+ * 保存实际消息。
474
701
  *
475
- * 该方法只同步写入内存并投递后台队列,不等待 SQLite。这样消息事件创建不会被
702
+ * 只克隆快照并投递后台队列,不等待 SQLite,消息事件创建不会被
476
703
  * 数据库写锁或小事务阻塞。
477
704
  *
478
705
  * @param botId 当前 QQBot appId。
479
706
  * @param message 已转换为 Karin elements 的消息缓存。
480
- * @param aliases 可查询到该消息的额外消息索引,例如 QQ `msg_idx`。
707
+ * @param options 引用索引与自己消息标记。
481
708
  */
482
- save(botId: string, message: CachedMessage, aliases?: string[]): Promise<void>;
709
+ save(botId: string, message: CachedMessage, options?: SaveOptions): Promise<void>;
483
710
  /**
484
- * 仅当内存中没有该引用索引时保存 QQ 下发的引用上下文。
485
- * 数据库是否已存在由后台 flush 再判断,避免阻塞事件热路径。
711
+ * 保存 QQ 下发的引用上下文。
712
+ *
713
+ * 是否已有同 ID 缓存(原消息或引用索引)由后台 flush 在事务内判断并去重,
714
+ * 避免阻塞事件热路径。
486
715
  *
487
716
  * @param botId 当前 QQBot appId。
488
717
  * @param message 以 `ref_msg_idx` 作为 messageId 的引用上下文。
718
+ * @param level 存储分级;缺省按 standard 处理。
489
719
  */
490
- saveReferenceIfAbsent(botId: string, message: CachedMessage): Promise<void>;
720
+ saveReferenceIfAbsent(botId: string, message: CachedMessage, level?: SaveOptions['level']): Promise<void>;
491
721
  /**
492
722
  * 读取缓存消息。
493
723
  *
@@ -513,11 +743,68 @@ declare class MessageStore {
513
743
  */
514
744
  getHistory(botId: string, contact: Contact, startMsgId: string, count: number): Promise<MessageResponse[]>;
515
745
  /**
516
- * 初始化 SQLite 连接、表结构,并执行启动清理和媒体本地化补救。
746
+ * 将 QQ `REFIDX` 引用索引解析为 API 消息 ID。
747
+ *
748
+ * 撤回等平台接口的唯一映射来源;ID 映射行不受缓存开关影响,始终落库。
749
+ *
750
+ * @param botId 当前 QQBot appId。
751
+ * @param contact 消息所在会话。
752
+ * @param refIdx QQ 引用索引,通常为 REFIDX_...。
753
+ * @returns 对应的 API 消息 ID;未命中时返回 null。
754
+ */
755
+ resolveApiMessageId(botId: string, contact: Contact, refIdx: string): Promise<string | null>;
756
+ /**
757
+ * 将 API 消息 ID 反向解析为 QQ `REFIDX` 引用索引。
758
+ *
759
+ * 发送侧显式引用回复的唯一映射来源。
760
+ *
761
+ * @param botId 当前 QQBot appId。
762
+ * @param contact 消息所在会话。
763
+ * @param messageId QQ API 消息 ID,通常为 ROBOT1.0_...。
764
+ * @returns 对应的引用索引;未命中时返回 null。
765
+ */
766
+ resolveRefIdx(botId: string, contact: Contact, messageId: string): Promise<string | null>;
767
+ /**
768
+ * 读取持久化的 file_info 上传缓存。
769
+ *
770
+ * @param key 由 api 层构造的缓存键。
771
+ * @returns 序列化的上传响应 JSON;未命中或已过期时返回 null。
772
+ */
773
+ getUploadCache(key: string): Promise<string | null>;
774
+ /**
775
+ * 写入持久化的 file_info 上传缓存。
776
+ *
777
+ * @param key 由 api 层构造的缓存键。
778
+ * @param response 序列化的上传响应 JSON。
779
+ * @param expiresAt 失效时间戳;0 表示长期有效。
780
+ */
781
+ setUploadCache(key: string, response: string, expiresAt: number): Promise<void>;
782
+ /**
783
+ * 判断消息是否由机器人自己发送。
784
+ *
785
+ * 单聊撤回判定的唯一来源,读取消息行的 `is_self` 标记。
786
+ *
787
+ * @param botId 当前 QQBot appId。
788
+ * @param contact 消息所在会话。
789
+ * @param messageId QQ API 消息 ID。
790
+ * @returns true 表示是机器人自己发送的消息。
791
+ */
792
+ isSelfMessage(botId: string, contact: Contact, messageId: string): Promise<boolean>;
793
+ /**
794
+ * 初始化 SQLite 连接、表结构,并执行一次启动清理。
795
+ *
796
+ * 远程媒体补救不在这里执行,避免阻塞 `ready` 拖慢首批消息落库。
517
797
  *
518
798
  * @returns 初始化完成 Promise。
519
799
  */
520
800
  private initialize;
801
+ /**
802
+ * 从配置解析当前生效的 TTL 与行数上限。
803
+ *
804
+ * 数据库为全 bot 共享,多个 bot 同时开启缓存时取最大值;配置读取失败时
805
+ * 保持当前值。每次清理前刷新,配置修改最迟 10 分钟内生效。
806
+ */
807
+ private refreshSettings;
521
808
  /**
522
809
  * 投递待落库消息。
523
810
  *
@@ -530,6 +817,18 @@ declare class MessageStore {
530
817
  * 已有 timer 或正在 flush 时不会重复安排。
531
818
  */
532
819
  private scheduleFlush;
820
+ /**
821
+ * 触发一次批量落库;已有进行中的 flush 时复用其 Promise。
822
+ *
823
+ * @returns 当前批次落库完成的 Promise。
824
+ */
825
+ private flushQueue;
826
+ /**
827
+ * 等待写入队列完全清空。
828
+ *
829
+ * 所有读取入口先经过它,保证"读到自己刚写的数据";队列为空时零开销。
830
+ */
831
+ private flushAll;
533
832
  /**
534
833
  * 批量落库队列中的消息。
535
834
  *
@@ -537,11 +836,14 @@ declare class MessageStore {
537
836
  *
538
837
  * @returns flush 完成 Promise。
539
838
  */
540
- private flushQueue;
839
+ private doFlushBatch;
541
840
  /**
542
841
  * 写入一条待落库消息。
543
842
  *
544
- * @param item 待写入的消息、alias 和引用上下文标记。
843
+ * 稳定状态下(contact/sender 已缓存)每条消息 2 次语句往返:
844
+ * 定位既有行 + INSERT 或 UPDATE。
845
+ *
846
+ * @param item 待写入的消息任务。
545
847
  */
546
848
  private writeItem;
547
849
  /**
@@ -556,7 +858,8 @@ declare class MessageStore {
556
858
  *
557
859
  * @param botId 当前 QQBot appId。
558
860
  * @param message 可能包含远程媒体 URL 的消息快照。
559
- * @param aliases 该消息可被查询到的额外 alias。
861
+ * @param refIdx 该消息的引用索引,成功本地化后随消息一并回写。
862
+ * @param isSelf 是否机器人自己发送。
560
863
  */
561
864
  private enqueueMediaLocalize;
562
865
  /**
@@ -580,47 +883,25 @@ declare class MessageStore {
580
883
  private localizeMediaItem;
581
884
  /**
582
885
  * 清理已经过期的媒体本地化待处理任务。
583
- *
584
- * @returns 无返回值;会同步维护 pendingMediaKeys。
585
886
  */
586
887
  private prunePendingMedia;
587
888
  /**
588
- * 启动时扫描 SQLite 中尚未本地化的远程媒体 URL,并重新投递队列补救。
889
+ * 启动时扫描 SQLite 中尚未本地化的远程媒体消息,并重新投递队列补救。
890
+ *
891
+ * `has_remote_media` 是部分索引列,扫描不再需要 LIKE 全表匹配。
589
892
  *
590
893
  * @returns 扫描和投递完成 Promise。
591
894
  */
592
895
  private recoverRemoteMedia;
593
896
  /**
594
- * 写入内存热缓存。
595
- *
596
- * @param botId 当前 QQBot appId。
597
- * @param message 消息缓存。
598
- * @param aliases 可查询到该消息的额外消息索引。
599
- */
600
- private putMemory;
601
- /**
602
- * 从内存热缓存读取消息。
897
+ * 按消息 ID 或引用索引查询消息行。
603
898
  *
604
899
  * @param botId 当前 QQBot appId。
605
- * @param messageId 真实消息 ID 或 alias。
900
+ * @param messageId 真实消息 ID 或 `REFIDX` 引用索引。
606
901
  * @param contact 可选会话范围。
607
- * @returns 命中的缓存消息;未命中或过期时返回 null。
608
- */
609
- private getFromMemory;
610
- /**
611
- * 到达清理间隔时清理内存缓存。
612
- */
613
- private cleanupMemoryIfDue;
614
- /**
615
- * 清理一天前的内存消息,并同步清理失效索引。
616
- */
617
- private cleanupMemory;
618
- /**
619
- * 清理指向已删除内存消息的索引。
620
- *
621
- * @param index 需要清理的索引 Map。
902
+ * @returns 命中的消息行;未命中时返回 null。
622
903
  */
623
- private cleanIndex;
904
+ private queryMessageRow;
624
905
  /**
625
906
  * 浅拷贝消息缓存,避免后续事件对象被插件侧修改后污染缓存。
626
907
  *
@@ -629,54 +910,12 @@ declare class MessageStore {
629
910
  */
630
911
  private cloneMessage;
631
912
  /**
632
- * 将内存缓存转换为 Karin `MessageResponse`。
913
+ * 将数据库行还原为 Karin `MessageResponse`。
633
914
  *
634
- * @param message 内存缓存消息。
915
+ * @param row 消息主查询结果。
635
916
  * @returns Karin 标准消息查询结果。
636
917
  */
637
- private toResponse;
638
- /**
639
- * 执行数据库版本迁移。
640
- *
641
- * 使用 SQLite `PRAGMA user_version` 记录已应用的 schema 版本。
642
- * 版本 0 只表示“未初始化或开发期旧库”,首个 alpha 发布基线为 v1。
643
- *
644
- * @returns 迁移完成 Promise。
645
- */
646
- private migrate;
647
- /**
648
- * 获取或创建 bot 映射 ID。
649
- *
650
- * @param botId 当前 QQBot appId。
651
- * @returns `qqbot_bots.id`。
652
- */
653
- private ensureBot;
654
- /**
655
- * 获取或创建会话映射 ID。
656
- *
657
- * @param botRef `qqbot_bots.id`。
658
- * @param contact Karin 会话对象。
659
- * @returns `qqbot_contacts.id`。
660
- */
661
- private ensureContact;
662
- /**
663
- * 获取或创建发送者映射 ID。
664
- *
665
- * @param botRef `qqbot_bots.id`。
666
- * @param sender Karin sender 对象。
667
- * @returns `qqbot_senders.id`。
668
- */
669
- private ensureSender;
670
- /**
671
- * 获取或创建消息主记录。
672
- *
673
- * @param botRef `qqbot_bots.id`。
674
- * @param contactRef `qqbot_contacts.id`。
675
- * @param senderRef `qqbot_senders.id`。
676
- * @param message 消息缓存。
677
- * @returns `qqbot_messages.id`。
678
- */
679
- private ensureMessage;
918
+ private rowToResponse;
680
919
  /**
681
920
  * 从查询行还原 Karin Contact。
682
921
  *
@@ -685,63 +924,24 @@ declare class MessageStore {
685
924
  */
686
925
  private contactFromRow;
687
926
  /**
688
- * 将数据库行还原为 Karin `MessageResponse`。
689
- *
690
- * @param row 消息主查询结果。
691
- * @returns Karin 标准消息查询结果。
692
- */
693
- private rowToResponse;
694
- /**
695
- * 在指定会话中解析消息 ID 或 alias 到消息主表行。
696
- *
697
- * @param botId 当前 QQBot appId。
698
- * @param contact Karin 会话对象。
699
- * @param messageId 真实消息 ID 或 alias。
700
- * @returns 命中的消息行;未命中时返回 null。
701
- */
702
- private resolveMessageRow;
703
- /**
704
- * 构造会话级查询键。
705
- *
706
- * @param botId 当前 QQBot appId。
707
- * @param contact Karin 会话对象。
708
- * @param messageId 消息 ID 或 alias。
709
- * @returns 用于 SQL 参数或内存 key 的字段数组。
710
- */
711
- private contactKey;
712
- /**
713
- * 构造内存会话级 key。
927
+ * 获取或创建会话映射 ID。
714
928
  *
715
929
  * @param botId 当前 QQBot appId。
716
930
  * @param contact Karin 会话对象。
717
- * @param messageId 消息 ID 或 alias。
718
- * @returns 内存 Map 使用的稳定 key。
931
+ * @returns `qqbot_contacts.id`。
719
932
  */
720
- private scopedKey;
933
+ private ensureContact;
721
934
  /**
722
- * 构造内存全局 key。
935
+ * 获取或创建发送者映射 ID。
723
936
  *
724
- * @param botId 当前 QQBot appId。
725
- * @param messageId 消息 ID 或 alias。
726
- * @returns 不限定会话时使用的内存 key。
727
- */
728
- private globalKey;
729
- /**
730
- * 删除指定会话下的 alias。
937
+ * sender 按 (userId, nick, name, role) 去重;不再被引用的行由清理任务 GC,
938
+ * 不会随昵称变化永久膨胀。
731
939
  *
732
940
  * @param botId 当前 QQBot appId。
733
- * @param contact Karin 会话对象。
734
- * @param messageId 需要删除的 alias ID。
735
- */
736
- private deleteAlias;
737
- /**
738
- * 判断某条消息是否包含指向指定 ID 的 reply 段。
739
- *
740
- * @param messageRef `qqbot_messages.id`。
741
- * @param replyId 被引用的消息 ID。
742
- * @returns true 表示该消息包含对应 reply 段。
941
+ * @param sender Karin sender 对象。
942
+ * @returns `qqbot_senders.id`。
743
943
  */
744
- private hasReplyTo;
944
+ private ensureSender;
745
945
  /**
746
946
  * 判断消息是否已经超过本地缓存 TTL。
747
947
  *
@@ -750,15 +950,21 @@ declare class MessageStore {
750
950
  */
751
951
  private isExpired;
752
952
  /**
753
- * 到达清理间隔时清理内存和 SQLite 过期消息。
953
+ * 清理过期缓存消息,并执行体积治理。
954
+ *
955
+ * 依次执行:配置刷新、TTL 删除、硬上限删除、上传缓存清理、
956
+ * sender GC(写空闲时)、增量 vacuum 归还空间、WAL checkpoint 截断、
957
+ * 媒体文件清理。
754
958
  */
755
- private cleanupIfDue;
959
+ private cleanup;
756
960
  /**
757
- * 清理一天前的缓存消息。
961
+ * 执行数据库版本迁移。
758
962
  *
759
- * SQLite 只删除主消息表,消息段和 alias 通过外键级联删除。
963
+ * 使用 SQLite `PRAGMA user_version` 记录已应用的 schema 版本。
964
+ *
965
+ * @returns 迁移完成 Promise。
760
966
  */
761
- private cleanup;
967
+ private migrate;
762
968
  /**
763
969
  * 判断 SQLite 表是否存在。
764
970
  *
@@ -779,12 +985,6 @@ declare class MessageStore {
779
985
  * @returns 当前 `PRAGMA user_version`;新库默认为 0。
780
986
  */
781
987
  private getUserVersion;
782
- /**
783
- * 写入 SQLite schema 版本。
784
- *
785
- * @param version 已成功应用的数据库版本号。
786
- */
787
- private setUserVersion;
788
988
  /**
789
989
  * 在 `BEGIN IMMEDIATE` 事务中执行写操作。
790
990
  *
@@ -792,88 +992,66 @@ declare class MessageStore {
792
992
  */
793
993
  private transaction;
794
994
  /**
795
- * 执行不返回行的 SQL。
995
+ * 获取当前数据库连接。
996
+ *
997
+ * @returns 已初始化的 sqlite3 Database。
998
+ */
999
+ private database;
1000
+ /**
1001
+ * 获取(并缓存)prepared statement。
1002
+ *
1003
+ * 热路径 SQL 只 parse 一次,重复执行只做参数绑定。
1004
+ *
1005
+ * @param sql SQL 字符串。
1006
+ * @returns 复用的 Statement。
1007
+ */
1008
+ private prepared;
1009
+ /**
1010
+ * 以 prepared statement 执行不返回行的 SQL。
796
1011
  *
797
1012
  * @param sql SQL 字符串。
798
1013
  * @param params SQL 参数。
799
1014
  */
800
- private run;
1015
+ private runS;
801
1016
  /**
802
- * 查询单行数据。
1017
+ * 以 prepared statement 查询单行数据。
803
1018
  *
804
1019
  * @param sql SQL 字符串。
805
1020
  * @param params SQL 参数。
806
1021
  * @returns 查询结果;未命中时返回 undefined。
807
1022
  */
808
- private getRow;
1023
+ private getS;
809
1024
  /**
810
- * 查询多行数据。
1025
+ * 以 prepared statement 查询多行数据。
811
1026
  *
812
1027
  * @param sql SQL 字符串。
813
1028
  * @param params SQL 参数。
814
1029
  * @returns 查询结果数组。
815
1030
  */
816
- private getRows;
817
- }
818
- //#endregion
819
- //#region src/types/config.d.ts
820
- /**
821
- * 自定义连接代理配置
822
- */
823
- interface QQBotProxyConfig {
824
- /** 正式环境 API */
825
- prodApi: string;
826
- /** 测试环境 API */
827
- sandboxApi: string;
828
- /** 调用凭证 API */
829
- tokenApi: string;
830
- /** 正式环境 WebSocket 网关 */
831
- prodWs: string;
832
- /** 测试环境 WebSocket 网关 */
833
- sandboxWs: string;
834
- }
835
- /**
836
- * 单个 QQBot 配置
837
- */
838
- interface QQBotConfig {
839
- /** 机器人名称 */
840
- name: string;
841
- /** 机器人 ID */
842
- appId: string;
843
- /** 机器人密钥 */
844
- secret: string;
845
- /** 自定义连接代理 */
846
- proxy: QQBotProxyConfig;
847
- /** 开启沙盒环境 */
848
- sandbox: boolean;
849
- /** 开启 QQ 场景能力(单聊 / 群聊) */
850
- qqEnable: boolean;
851
- /** 开启频道场景能力 */
852
- guildEnable: boolean;
853
- /** 频道场景模式 0 公域 / 1 私域 */
854
- guildMode: 0 | 1;
855
- /** 收到消息后对文本进行表达式处理 */
856
- regex: {
857
- reg: string | RegExp;
858
- rep: string;
859
- }[];
860
- /** 按钮自动化 */
861
- keyboard: {
862
- /** 是否将文本中的 URL 自动转换为 keyboard 按钮 */enable: boolean;
863
- };
864
- /** Markdown 通道 */
865
- markdown: {
866
- /** 是否启用 Markdown 通道发送消息 */enable: boolean;
867
- };
868
- /** 消息缓存配置 */
869
- messageCache: {
870
- /** 是否启用数据库消息缓存,用于 bot.getMsg */enable: boolean; /** 是否缓存机器人自己发送的消息 */
871
- self: boolean;
872
- };
873
- /** 事件接收配置 */
874
- event: {
875
- /** 接收方式 0 关闭 / 1 webhook / 2 ws */type: 0 | 1 | 2;
876
- };
1031
+ private allS;
1032
+ /**
1033
+ * 执行一次性 SQL(DDL、PRAGMA、事务控制),不走 prepared statement 缓存。
1034
+ *
1035
+ * @param sql SQL 字符串。
1036
+ * @param params SQL 参数。
1037
+ */
1038
+ private exec;
1039
+ /**
1040
+ * 一次性查询单行(PRAGMA / 系统表)。
1041
+ *
1042
+ * @param sql SQL 字符串。
1043
+ * @param params SQL 参数。
1044
+ * @returns 查询结果;未命中时返回 undefined。
1045
+ */
1046
+ private rawGet;
1047
+ /**
1048
+ * 一次性查询多行(PRAGMA / 系统表)。
1049
+ *
1050
+ * @param sql SQL 字符串。
1051
+ * @param params SQL 参数。
1052
+ * @returns 查询结果数组。
1053
+ */
1054
+ private rawAll;
877
1055
  }
878
1056
  //#endregion
879
1057
  //#region src/core/adapter/base.d.ts
@@ -906,6 +1084,8 @@ declare class AdapterQQBot extends AdapterBase implements AdapterType {
906
1084
  * 撤回消息
907
1085
  *
908
1086
  * 群聊中机器人被群主设为管理员后,也可撤回成员消息;平台仍限制消息发送后两分钟内。
1087
+ * REFIDX 与 API 消息 ID 的映射、单聊"是否自己发送"的判定都直接查询
1088
+ * SQLite 消息缓存(ID 映射行不受缓存开关影响,始终落库)。
909
1089
  */
910
1090
  recallMsg(contact: Contact, messageId: string): Promise<void>;
911
1091
  /**
@@ -1020,6 +1200,9 @@ declare class AdapterQQBot extends AdapterBase implements AdapterType {
1020
1200
  getFriendList(_refresh?: boolean): Promise<Array<UserInfo>>;
1021
1201
  /**
1022
1202
  * 获取群信息
1203
+ *
1204
+ * 走官方 `GET /v2/groups/{group_openid}/info`(白名单接口)。
1205
+ * 未开通权限(11253)或查询失败时降级为空结构并告警。
1023
1206
  */
1024
1207
  getGroupInfo(_groupId: string, _noCache?: boolean): Promise<GroupInfo>;
1025
1208
  /**
@@ -1028,6 +1211,11 @@ declare class AdapterQQBot extends AdapterBase implements AdapterType {
1028
1211
  getGroupList(_refresh?: boolean): Promise<Array<GroupInfo>>;
1029
1212
  /**
1030
1213
  * 获取群成员信息
1214
+ *
1215
+ * - 查询机器人自身(targetId 为 selfId)走官方 `GET /v2/groups/{group_openid}/bot_state`(白名单)。
1216
+ * - 查询其他成员走 `GET /v2/groups/{group_openid}/members/{member_openid}`,
1217
+ * 该接口未公开文档,来自腾讯官方 openclaw 插件 v2.0.0,返回结构以平台实际为准。
1218
+ * 接口不可用(如白名单未开通返回 11253)时降级为占位结构并告警。
1031
1219
  */
1032
1220
  getGroupMemberInfo(_groupId: string, _targetId: string, _refresh?: boolean): Promise<GroupMemberInfo>;
1033
1221
  /**
@@ -1056,6 +1244,10 @@ declare class AdapterQQBot extends AdapterBase implements AdapterType {
1056
1244
  setMsgReaction(_contact: Contact, _messageId: string, _faceId: number | string, _isSet: boolean): Promise<void>;
1057
1245
  /**
1058
1246
  * 上传群文件、私聊文件
1247
+ *
1248
+ * QQ 官方 Bot API 没有群文件系统,上传走 `POST /v2/{users|groups}/{openid}/files`
1249
+ * (file_type=4 + srv_send_msg=true),由服务端直接发送文件消息。
1250
+ * 频道 / 频道私信场景官方无文件接口,暂不支持。
1059
1251
  */
1060
1252
  uploadFile(_contact: Contact, _file: string, _name: string, _folder?: string): Promise<void>;
1061
1253
  /**
@@ -1076,6 +1268,8 @@ declare class AdapterQQBot extends AdapterBase implements AdapterType {
1076
1268
  delGroupFolder(_groupId: string, _folderId: string): Promise<boolean>;
1077
1269
  /**
1078
1270
  * 上传群文件
1271
+ *
1272
+ * 实现同 {@link uploadFile}:官方无群文件系统,走 files 接口由服务端直发文件消息。
1079
1273
  */
1080
1274
  uploadGroupFile(_groupId: string, _file: string, _name?: string): Promise<boolean>;
1081
1275
  /**