@ryantest/openclaw-qqbot 1.6.6-alpha.4 → 1.6.7-beta.2

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 (53) hide show
  1. package/README.md +24 -15
  2. package/README.zh.md +24 -15
  3. package/dist/src/api.d.ts +32 -5
  4. package/dist/src/api.js +111 -12
  5. package/dist/src/channel.d.ts +18 -0
  6. package/dist/src/channel.js +85 -2
  7. package/dist/src/config.d.ts +33 -2
  8. package/dist/src/config.js +125 -1
  9. package/dist/src/gateway.js +566 -24
  10. package/dist/src/group-history.d.ts +136 -0
  11. package/dist/src/group-history.js +226 -0
  12. package/dist/src/message-gating.d.ts +53 -0
  13. package/dist/src/message-gating.js +107 -0
  14. package/dist/src/message-queue.d.ts +36 -0
  15. package/dist/src/message-queue.js +164 -22
  16. package/dist/src/outbound.d.ts +4 -4
  17. package/dist/src/outbound.js +18 -6
  18. package/dist/src/ref-index-store.js +5 -28
  19. package/dist/src/request-context.d.ts +7 -0
  20. package/dist/src/request-context.js +7 -0
  21. package/dist/src/slash-commands.d.ts +6 -0
  22. package/dist/src/slash-commands.js +3 -3
  23. package/dist/src/tools/remind.js +17 -9
  24. package/dist/src/types.d.ts +90 -2
  25. package/dist/src/utils/audio-convert.d.ts +1 -1
  26. package/dist/src/utils/audio-convert.js +1 -1
  27. package/dist/src/utils/chunked-upload.d.ts +11 -2
  28. package/dist/src/utils/chunked-upload.js +63 -11
  29. package/dist/src/utils/media-send.js +1 -1
  30. package/dist/src/utils/text-parsing.js +7 -18
  31. package/package.json +1 -1
  32. package/scripts/postinstall-link-sdk.js +22 -9
  33. package/scripts/upgrade-via-npm.sh +11 -3
  34. package/scripts/upgrade-via-source.sh +63 -15
  35. package/skills/qqbot-remind/SKILL.md +21 -11
  36. package/src/api.ts +135 -7
  37. package/src/channel.ts +85 -2
  38. package/src/config.ts +170 -3
  39. package/src/gateway.ts +662 -29
  40. package/src/group-history.ts +328 -0
  41. package/src/message-gating.ts +190 -0
  42. package/src/message-queue.ts +201 -21
  43. package/src/openclaw-plugin-sdk.d.ts +65 -0
  44. package/src/outbound.ts +18 -6
  45. package/src/ref-index-store.ts +5 -27
  46. package/src/request-context.ts +10 -0
  47. package/src/slash-commands.ts +3 -3
  48. package/src/tools/remind.ts +17 -9
  49. package/src/types.ts +94 -2
  50. package/src/utils/audio-convert.ts +1 -1
  51. package/src/utils/chunked-upload.ts +76 -12
  52. package/src/utils/media-send.ts +1 -2
  53. package/src/utils/text-parsing.ts +7 -14
@@ -1,5 +1,5 @@
1
1
  import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/core";
2
- import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId } from "./config.js";
2
+ import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId, resolveRequireMention, resolveToolPolicy, resolveGroupConfig } from "./config.js";
3
3
  import { sendText, sendMedia } from "./outbound.js";
4
4
  import { startGateway } from "./gateway.js";
5
5
  import { qqbotOnboardingAdapter } from "./onboarding.js";
@@ -39,8 +39,50 @@ export const qqbotPlugin = {
39
39
  blockStreaming: true,
40
40
  },
41
41
  reload: { configPrefixes: ["channels.qqbot"] },
42
+ // ============ 群消息策略适配器 ============
43
+ groups: {
44
+ /** 是否需要 @机器人才响应 */
45
+ resolveRequireMention: ({ cfg, accountId, groupId }) => {
46
+ if (!groupId)
47
+ return undefined;
48
+ return resolveRequireMention(cfg, groupId, accountId ?? undefined);
49
+ },
50
+ /** 群聊工具范围 */
51
+ resolveToolPolicy: ({ cfg, accountId, groupId }) => {
52
+ if (!groupId)
53
+ return undefined;
54
+ const policy = resolveToolPolicy(cfg, groupId, accountId ?? undefined);
55
+ // 将简单字符串策略映射为 GroupToolPolicyConfig 对象
56
+ if (policy === "full")
57
+ return undefined; // full = 默认不限制
58
+ if (policy === "none")
59
+ return { allow: [], deny: ["*"] };
60
+ // restricted: 默认空 allow(框架会使用内置 restricted 列表)
61
+ return { allow: [] };
62
+ },
63
+ /** QQ Bot 平台特有的群聊行为提示 */
64
+ resolveGroupIntroHint: ({ cfg, accountId, groupId }) => {
65
+ if (!groupId)
66
+ return undefined;
67
+ const groupCfg = resolveGroupConfig(cfg, groupId, accountId ?? undefined);
68
+ const hints = [];
69
+ if (groupCfg.name) {
70
+ hints.push(`当前群: ${groupCfg.name}`);
71
+ }
72
+ // bot 互聊防护、@状态行为指引在 gateway.ts 动态注入
73
+ return hints.join(" ") || undefined;
74
+ },
75
+ },
76
+ // ============ @mention 检测与清理 ============
77
+ mentions: {
78
+ /** 清理 @mention 文本(SDK ChannelMentionAdapter 接口) */
79
+ stripMentions: ({ text, ctx }) => {
80
+ const mentions = ctx?.mentions;
81
+ return stripMentionText(text, mentions);
82
+ },
83
+ },
42
84
  // CLI onboarding wizard
43
- // @ts-expect-error onboarding removed from ChannelPlugin type in 2026.3.23 but still supported at runtime
85
+ // @ts-ignore onboarding removed from ChannelPlugin type in 2026.3.23 but still supported at runtime
44
86
  onboarding: qqbotOnboardingAdapter,
45
87
  config: {
46
88
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
@@ -367,3 +409,44 @@ export const qqbotPlugin = {
367
409
  }),
368
410
  },
369
411
  };
412
+ // ============ 独立的 mention 工具函数(供 gateway.ts 等直接调用) ============
413
+ /** 清理 @mention:替换 <@openid> 为 @用户名,去除 @机器人自身 */
414
+ export function stripMentionText(text, mentions) {
415
+ if (!text || !mentions?.length)
416
+ return text;
417
+ let cleaned = text;
418
+ for (const m of mentions) {
419
+ const openid = m.member_openid ?? m.id ?? m.user_openid;
420
+ if (!openid)
421
+ continue;
422
+ if (m.is_you) {
423
+ cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, "g"), "").trim();
424
+ }
425
+ else {
426
+ const displayName = m.nickname ?? m.username;
427
+ if (displayName) {
428
+ cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, "g"), `@${displayName}`);
429
+ }
430
+ }
431
+ }
432
+ return cleaned;
433
+ }
434
+ /** 检测消息是否 @了机器人(mentions > eventType > mentionPatterns) */
435
+ export function detectWasMentioned({ eventType, mentions, content, mentionPatterns }) {
436
+ if (mentions?.some((m) => m.is_you))
437
+ return true;
438
+ if (eventType === "GROUP_AT_MESSAGE_CREATE")
439
+ return true;
440
+ if (mentionPatterns?.length && content) {
441
+ for (const pattern of mentionPatterns) {
442
+ try {
443
+ if (new RegExp(pattern, "i").test(content))
444
+ return true;
445
+ }
446
+ catch {
447
+ // 无效正则,跳过
448
+ }
449
+ }
450
+ }
451
+ return false;
452
+ }
@@ -1,6 +1,36 @@
1
- import type { ResolvedQQBotAccount } from "./types.js";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { ResolvedQQBotAccount, ToolPolicy, GroupConfig } from "./types.js";
2
+ import type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk";
3
+ /**
4
+ * 解析 mentionPatterns(agent → global → 空数组)
5
+ *
6
+ * 优先级:
7
+ * 1. agents.list[agentId].groupChat.mentionPatterns
8
+ * 2. messages.groupChat.mentionPatterns
9
+ * 3. []
10
+ */
11
+ export declare function resolveMentionPatterns(cfg: OpenClawConfig, agentId?: string): string[];
3
12
  export declare const DEFAULT_ACCOUNT_ID = "default";
13
+ /** 解析群消息策略 */
14
+ export declare function resolveGroupPolicy(cfg: OpenClawConfig, accountId?: string): GroupPolicy;
15
+ /** 解析群白名单(统一转大写) */
16
+ export declare function resolveGroupAllowFrom(cfg: OpenClawConfig, accountId?: string): string[];
17
+ /** 检查指定群是否被允许(使用标准策略引擎) */
18
+ export declare function isGroupAllowed(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
19
+ type ResolvedGroupConfig = Omit<Required<GroupConfig>, "prompt"> & Pick<GroupConfig, "prompt">;
20
+ /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
21
+ export declare function resolveGroupConfig(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ResolvedGroupConfig;
22
+ /** 解析群历史消息缓存条数 */
23
+ export declare function resolveHistoryLimit(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): number;
24
+ /** 解析群行为 PE(具体群 > "*" > 默认值) */
25
+ export declare function resolveGroupPrompt(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
26
+ /** 解析群是否需要 @机器人才响应 */
27
+ export declare function resolveRequireMention(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
28
+ /** 解析群是否忽略 @了其他人(非 bot)的消息 */
29
+ export declare function resolveIgnoreOtherMentions(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
30
+ /** 解析群工具策略 */
31
+ export declare function resolveToolPolicy(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ToolPolicy;
32
+ /** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
33
+ export declare function resolveGroupName(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
4
34
  /**
5
35
  * 列出所有 QQBot 账户 ID
6
36
  */
@@ -23,3 +53,4 @@ export declare function applyQQBotAccountConfig(cfg: OpenClawConfig, accountId:
23
53
  name?: string;
24
54
  imageServerBaseUrl?: string;
25
55
  }): OpenClawConfig;
56
+ export {};
@@ -1,4 +1,128 @@
1
+ /**
2
+ * 解析 mentionPatterns(agent → global → 空数组)
3
+ *
4
+ * 优先级:
5
+ * 1. agents.list[agentId].groupChat.mentionPatterns
6
+ * 2. messages.groupChat.mentionPatterns
7
+ * 3. []
8
+ */
9
+ export function resolveMentionPatterns(cfg, agentId) {
10
+ // 1. agent 级别
11
+ if (agentId) {
12
+ const agents = cfg.agents;
13
+ const entry = agents?.list?.find((a) => a.id?.trim().toLowerCase() === agentId.trim().toLowerCase());
14
+ const agentGroupChat = entry?.groupChat;
15
+ if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
16
+ return agentGroupChat.mentionPatterns ?? [];
17
+ }
18
+ }
19
+ // 2. 全局级别
20
+ const globalGroupChat = cfg?.messages?.groupChat;
21
+ if (globalGroupChat && typeof globalGroupChat === "object" && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
22
+ return globalGroupChat.mentionPatterns ?? [];
23
+ }
24
+ // 3. 空数组
25
+ return [];
26
+ }
1
27
  export const DEFAULT_ACCOUNT_ID = "default";
28
+ function evaluateMatchedGroupAccessForPolicy(params) {
29
+ if (params.groupPolicy === "disabled") {
30
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
31
+ }
32
+ if (params.groupPolicy === "allowlist") {
33
+ if (params.requireMatchInput && !params.hasMatchInput) {
34
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "missing_match_input" };
35
+ }
36
+ if (!params.allowlistConfigured) {
37
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
38
+ }
39
+ if (!params.allowlistMatched) {
40
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "not_allowlisted" };
41
+ }
42
+ }
43
+ return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
44
+ }
45
+ // ============ 群消息策略 ============
46
+ const DEFAULT_GROUP_POLICY = "open";
47
+ /** 群历史缓存条数默认值 */
48
+ const DEFAULT_GROUP_HISTORY_LIMIT = 50;
49
+ const DEFAULT_GROUP_CONFIG = {
50
+ requireMention: true,
51
+ ignoreOtherMentions: false,
52
+ toolPolicy: "restricted",
53
+ name: "",
54
+ historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
55
+ };
56
+ /** 默认群消息行为 PE(可通过配置覆盖) */
57
+ const DEFAULT_GROUP_PROMPT = [
58
+ "若发送者为机器人,仅在对方明确@你提问或请求协助具体任务时,以简洁明了的内容回复,",
59
+ "避免与其他机器人产生抢答或多轮无意义对话。",
60
+ "在群聊中优先让人类用户的消息得到响应,机器人之间保持协作而非竞争,确保对话有序不刷屏。",
61
+ ].join("");
62
+ /** 解析群消息策略 */
63
+ export function resolveGroupPolicy(cfg, accountId) {
64
+ const account = resolveQQBotAccount(cfg, accountId);
65
+ return account.config?.groupPolicy ?? DEFAULT_GROUP_POLICY;
66
+ }
67
+ /** 解析群白名单(统一转大写) */
68
+ export function resolveGroupAllowFrom(cfg, accountId) {
69
+ const account = resolveQQBotAccount(cfg, accountId);
70
+ return (account.config?.groupAllowFrom ?? []).map((id) => String(id).trim().toUpperCase());
71
+ }
72
+ /** 检查指定群是否被允许(使用标准策略引擎) */
73
+ export function isGroupAllowed(cfg, groupOpenid, accountId) {
74
+ const policy = resolveGroupPolicy(cfg, accountId);
75
+ const allowList = resolveGroupAllowFrom(cfg, accountId);
76
+ const allowlistConfigured = allowList.length > 0;
77
+ const allowlistMatched = allowList.some((id) => id === "*" || id === groupOpenid.toUpperCase());
78
+ return evaluateMatchedGroupAccessForPolicy({
79
+ groupPolicy: policy,
80
+ allowlistConfigured,
81
+ allowlistMatched,
82
+ }).allowed;
83
+ }
84
+ /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
85
+ export function resolveGroupConfig(cfg, groupOpenid, accountId) {
86
+ const account = resolveQQBotAccount(cfg, accountId);
87
+ const groups = account.config?.groups ?? {};
88
+ const wildcardCfg = groups["*"] ?? {};
89
+ const specificCfg = groups[groupOpenid] ?? {};
90
+ return {
91
+ requireMention: specificCfg.requireMention ?? wildcardCfg.requireMention ?? DEFAULT_GROUP_CONFIG.requireMention,
92
+ ignoreOtherMentions: specificCfg.ignoreOtherMentions ?? wildcardCfg.ignoreOtherMentions ?? DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
93
+ toolPolicy: specificCfg.toolPolicy ?? wildcardCfg.toolPolicy ?? DEFAULT_GROUP_CONFIG.toolPolicy,
94
+ name: specificCfg.name ?? wildcardCfg.name ?? DEFAULT_GROUP_CONFIG.name,
95
+ prompt: specificCfg.prompt ?? wildcardCfg.prompt,
96
+ historyLimit: specificCfg.historyLimit ?? wildcardCfg.historyLimit ?? DEFAULT_GROUP_CONFIG.historyLimit,
97
+ };
98
+ }
99
+ /** 解析群历史消息缓存条数 */
100
+ export function resolveHistoryLimit(cfg, groupOpenid, accountId) {
101
+ return Math.max(0, resolveGroupConfig(cfg, groupOpenid, accountId).historyLimit);
102
+ }
103
+ /** 解析群行为 PE(具体群 > "*" > 默认值) */
104
+ export function resolveGroupPrompt(cfg, groupOpenid, accountId) {
105
+ const account = resolveQQBotAccount(cfg, accountId);
106
+ const groups = account.config?.groups ?? {};
107
+ return groups[groupOpenid]?.prompt ?? groups["*"]?.prompt ?? DEFAULT_GROUP_PROMPT;
108
+ }
109
+ /** 解析群是否需要 @机器人才响应 */
110
+ export function resolveRequireMention(cfg, groupOpenid, accountId) {
111
+ return resolveGroupConfig(cfg, groupOpenid, accountId).requireMention;
112
+ }
113
+ /** 解析群是否忽略 @了其他人(非 bot)的消息 */
114
+ export function resolveIgnoreOtherMentions(cfg, groupOpenid, accountId) {
115
+ return resolveGroupConfig(cfg, groupOpenid, accountId).ignoreOtherMentions;
116
+ }
117
+ /** 解析群工具策略 */
118
+ export function resolveToolPolicy(cfg, groupOpenid, accountId) {
119
+ return resolveGroupConfig(cfg, groupOpenid, accountId).toolPolicy;
120
+ }
121
+ /** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
122
+ export function resolveGroupName(cfg, groupOpenid, accountId) {
123
+ const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
124
+ return name || groupOpenid.slice(0, 8);
125
+ }
2
126
  function normalizeAppId(raw) {
3
127
  if (raw === null || raw === undefined)
4
128
  return "";
@@ -44,7 +168,7 @@ export function resolveDefaultQQBotAccountId(cfg) {
44
168
  * 解析 QQBot 账户配置
45
169
  */
46
170
  export function resolveQQBotAccount(cfg, accountId) {
47
- const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
171
+ const resolvedAccountId = accountId ?? resolveDefaultQQBotAccountId(cfg);
48
172
  const qqbot = cfg.channels?.qqbot;
49
173
  // 基础配置
50
174
  let accountConfig = {};