@gakr-gakr/qqbot 0.1.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 (149) hide show
  1. package/api.ts +56 -0
  2. package/autobot.plugin.json +167 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +33 -0
  5. package/package.json +64 -0
  6. package/runtime-api.ts +9 -0
  7. package/secret-contract-api.ts +5 -0
  8. package/setup-entry.ts +13 -0
  9. package/setup-plugin-api.ts +3 -0
  10. package/skills/qqbot-channel/SKILL.md +262 -0
  11. package/skills/qqbot-channel/references/api_references.md +521 -0
  12. package/skills/qqbot-media/SKILL.md +37 -0
  13. package/skills/qqbot-remind/SKILL.md +153 -0
  14. package/src/bridge/approval/capability.ts +225 -0
  15. package/src/bridge/approval/handler-runtime.ts +204 -0
  16. package/src/bridge/bootstrap.ts +135 -0
  17. package/src/bridge/channel-entry.ts +18 -0
  18. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  19. package/src/bridge/commands/framework-registration.ts +66 -0
  20. package/src/bridge/commands/from-parser.ts +60 -0
  21. package/src/bridge/commands/result-dispatcher.ts +76 -0
  22. package/src/bridge/config-shared.ts +132 -0
  23. package/src/bridge/config.ts +176 -0
  24. package/src/bridge/gateway.ts +178 -0
  25. package/src/bridge/logger.ts +31 -0
  26. package/src/bridge/narrowing.ts +31 -0
  27. package/src/bridge/plugin-version.ts +102 -0
  28. package/src/bridge/runtime.ts +25 -0
  29. package/src/bridge/sdk-adapter.ts +164 -0
  30. package/src/bridge/setup/finalize.ts +144 -0
  31. package/src/bridge/setup/surface.ts +34 -0
  32. package/src/bridge/tools/channel.ts +58 -0
  33. package/src/bridge/tools/index.ts +15 -0
  34. package/src/bridge/tools/remind.ts +91 -0
  35. package/src/channel.setup.ts +33 -0
  36. package/src/channel.ts +399 -0
  37. package/src/config-schema.ts +84 -0
  38. package/src/engine/access/index.ts +2 -0
  39. package/src/engine/access/resolve-policy.ts +30 -0
  40. package/src/engine/access/sender-match.ts +55 -0
  41. package/src/engine/access/types.ts +2 -0
  42. package/src/engine/adapter/audio.port.ts +27 -0
  43. package/src/engine/adapter/commands.port.ts +22 -0
  44. package/src/engine/adapter/history.port.ts +52 -0
  45. package/src/engine/adapter/index.ts +76 -0
  46. package/src/engine/adapter/mention-gate.port.ts +50 -0
  47. package/src/engine/adapter/types.ts +38 -0
  48. package/src/engine/api/api-client.ts +212 -0
  49. package/src/engine/api/media-chunked.ts +644 -0
  50. package/src/engine/api/media.ts +218 -0
  51. package/src/engine/api/messages.ts +293 -0
  52. package/src/engine/api/retry.ts +217 -0
  53. package/src/engine/api/routes.ts +95 -0
  54. package/src/engine/api/token.ts +277 -0
  55. package/src/engine/approval/index.ts +224 -0
  56. package/src/engine/commands/builtin/log-helpers.ts +341 -0
  57. package/src/engine/commands/builtin/register-all.ts +17 -0
  58. package/src/engine/commands/builtin/register-approve.ts +201 -0
  59. package/src/engine/commands/builtin/register-basic.ts +95 -0
  60. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  61. package/src/engine/commands/builtin/register-logs.ts +20 -0
  62. package/src/engine/commands/builtin/register-streaming.ts +138 -0
  63. package/src/engine/commands/builtin/state.ts +31 -0
  64. package/src/engine/commands/slash-command-auth.ts +88 -0
  65. package/src/engine/commands/slash-command-handler.ts +168 -0
  66. package/src/engine/commands/slash-command-test-support.ts +39 -0
  67. package/src/engine/commands/slash-commands-impl.ts +61 -0
  68. package/src/engine/commands/slash-commands.ts +202 -0
  69. package/src/engine/config/credential-backup.ts +108 -0
  70. package/src/engine/config/credentials.ts +76 -0
  71. package/src/engine/config/group.ts +227 -0
  72. package/src/engine/config/resolve.ts +283 -0
  73. package/src/engine/config/setup-logic.ts +84 -0
  74. package/src/engine/gateway/active-cfg.ts +52 -0
  75. package/src/engine/gateway/codec.ts +47 -0
  76. package/src/engine/gateway/constants.ts +117 -0
  77. package/src/engine/gateway/event-dispatcher.ts +177 -0
  78. package/src/engine/gateway/gateway-connection.ts +356 -0
  79. package/src/engine/gateway/gateway.ts +267 -0
  80. package/src/engine/gateway/inbound-attachments.ts +360 -0
  81. package/src/engine/gateway/inbound-context.ts +82 -0
  82. package/src/engine/gateway/inbound-pipeline.ts +171 -0
  83. package/src/engine/gateway/interaction-handler.ts +345 -0
  84. package/src/engine/gateway/message-queue.ts +404 -0
  85. package/src/engine/gateway/outbound-dispatch.ts +590 -0
  86. package/src/engine/gateway/reconnect.ts +199 -0
  87. package/src/engine/gateway/stages/access-stage.ts +99 -0
  88. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  89. package/src/engine/gateway/stages/content-stage.ts +77 -0
  90. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  91. package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
  92. package/src/engine/gateway/stages/index.ts +18 -0
  93. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  94. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  95. package/src/engine/gateway/stages/stub-contexts.ts +77 -0
  96. package/src/engine/gateway/types.ts +230 -0
  97. package/src/engine/gateway/typing-keepalive.ts +102 -0
  98. package/src/engine/gateway/ws-client.ts +16 -0
  99. package/src/engine/group/activation.ts +88 -0
  100. package/src/engine/group/history.ts +321 -0
  101. package/src/engine/group/mention.ts +114 -0
  102. package/src/engine/group/message-gating.ts +108 -0
  103. package/src/engine/messaging/decode-media-path.ts +82 -0
  104. package/src/engine/messaging/media-source.ts +210 -0
  105. package/src/engine/messaging/media-type-detect.ts +27 -0
  106. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  107. package/src/engine/messaging/outbound-deliver.ts +810 -0
  108. package/src/engine/messaging/outbound-media-send.ts +658 -0
  109. package/src/engine/messaging/outbound-reply.ts +27 -0
  110. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  111. package/src/engine/messaging/outbound-types.ts +47 -0
  112. package/src/engine/messaging/outbound.ts +485 -0
  113. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  114. package/src/engine/messaging/reply-limiter.ts +164 -0
  115. package/src/engine/messaging/sender.ts +741 -0
  116. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  117. package/src/engine/messaging/streaming-media-send.ts +544 -0
  118. package/src/engine/messaging/target-parser.ts +104 -0
  119. package/src/engine/ref/format-message-ref.ts +142 -0
  120. package/src/engine/ref/format-ref-entry.ts +27 -0
  121. package/src/engine/ref/store.ts +211 -0
  122. package/src/engine/ref/types.ts +27 -0
  123. package/src/engine/session/known-users.ts +138 -0
  124. package/src/engine/session/session-store.ts +207 -0
  125. package/src/engine/tools/channel-api.ts +244 -0
  126. package/src/engine/tools/remind-logic.ts +377 -0
  127. package/src/engine/types.ts +313 -0
  128. package/src/engine/utils/attachment-tags.ts +174 -0
  129. package/src/engine/utils/audio.ts +525 -0
  130. package/src/engine/utils/data-paths.ts +38 -0
  131. package/src/engine/utils/diagnostics.ts +93 -0
  132. package/src/engine/utils/file-utils.ts +215 -0
  133. package/src/engine/utils/format.ts +70 -0
  134. package/src/engine/utils/image-size.ts +249 -0
  135. package/src/engine/utils/log.ts +77 -0
  136. package/src/engine/utils/media-tags.ts +177 -0
  137. package/src/engine/utils/payload.ts +157 -0
  138. package/src/engine/utils/platform.ts +265 -0
  139. package/src/engine/utils/request-context.ts +60 -0
  140. package/src/engine/utils/string-normalize.ts +91 -0
  141. package/src/engine/utils/stt.ts +103 -0
  142. package/src/engine/utils/text-parsing.ts +155 -0
  143. package/src/engine/utils/upload-cache.ts +96 -0
  144. package/src/engine/utils/voice-text.ts +15 -0
  145. package/src/exec-approvals.ts +237 -0
  146. package/src/qqbot-test-support.ts +29 -0
  147. package/src/secret-contract.ts +82 -0
  148. package/src/types.ts +210 -0
  149. package/tsconfig.json +16 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * INTERACTION_CREATE event handler.
3
+ *
4
+ * Handles three interaction branches:
5
+ *
6
+ * 1. **Config query** (type=2001) — reads config, ACKs with `claw_cfg`.
7
+ * 2. **Config update** (type=2002) — writes config, ACKs with updated snapshot.
8
+ * 3. **Approval button** (other) — ACKs, resolves authorized approval actions.
9
+ *
10
+ * Config query/update require `runtime.config`. When unavailable, those
11
+ * branches fall through to a bare ACK (backward-compatible).
12
+ */
13
+
14
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
15
+ import { authorizeQQBotApprovalAction } from "../../exec-approvals.js";
16
+ import { resolveQQBotEffectivePolicies } from "../access/resolve-policy.js";
17
+ import { getPlatformAdapter } from "../adapter/index.js";
18
+ import { parseApprovalButtonData } from "../approval/index.js";
19
+ import { getPluginVersion, getFrameworkVersion } from "../commands/slash-commands-impl.js";
20
+ import { resolveGroupConfig, resolveMentionPatterns } from "../config/group.js";
21
+ import { resolveAccountBase } from "../config/resolve.js";
22
+ import type { GroupActivationMode } from "../group/activation.js";
23
+ import { accountToCreds, acknowledgeInteraction } from "../messaging/sender.js";
24
+ import type { InteractionEvent, QQBotAccountConfigView } from "../types.js";
25
+ import { InteractionType } from "./constants.js";
26
+ import type { GatewayAccount, GatewayPluginRuntime, EngineLogger } from "./types.js";
27
+
28
+ // ============ claw_cfg snapshot ============
29
+
30
+ /**
31
+ * Build the canonical `claw_cfg` snapshot returned in interaction ACKs.
32
+ *
33
+ * Pure function — all resolution helpers live in engine/config/.
34
+ */
35
+ function buildClawCfgSnapshot(
36
+ cfg: Record<string, unknown>,
37
+ accountId: string,
38
+ groupOpenid: string,
39
+ runtime: GatewayPluginRuntime,
40
+ ): Record<string, unknown> {
41
+ const groupCfg = groupOpenid ? resolveGroupConfig(cfg, groupOpenid, accountId) : null;
42
+ const accountBase = resolveAccountBase(cfg, accountId);
43
+ const acctCfg = accountBase.config as QQBotAccountConfigView;
44
+ const policies = resolveQQBotEffectivePolicies({
45
+ allowFrom: acctCfg.allowFrom,
46
+ groupAllowFrom: acctCfg.groupAllowFrom,
47
+ dmPolicy: acctCfg.dmPolicy,
48
+ groupPolicy: acctCfg.groupPolicy,
49
+ });
50
+
51
+ const requireMentionMode: GroupActivationMode =
52
+ (groupCfg?.requireMention ?? true) ? "mention" : "always";
53
+
54
+ const interactionAgentId = groupOpenid
55
+ ? (
56
+ runtime.channel.routing.resolveAgentRoute({
57
+ cfg,
58
+ channel: "qqbot",
59
+ accountId,
60
+ peer: { kind: "group", id: groupOpenid },
61
+ }) as { agentId?: string } | undefined
62
+ )?.agentId
63
+ : undefined;
64
+
65
+ return {
66
+ channel_type: "qqbot",
67
+ channel_ver: getPluginVersion(),
68
+ claw_type: "autobot",
69
+ claw_ver: getFrameworkVersion(),
70
+ require_mention: requireMentionMode,
71
+ group_policy: policies.groupPolicy,
72
+ mention_patterns: resolveMentionPatterns(cfg, interactionAgentId).join(","),
73
+ online_state: "online",
74
+ };
75
+ }
76
+
77
+ // ============ Config update ============
78
+
79
+ /** Apply a config-update interaction and return the updated claw_cfg. */
80
+ async function applyConfigUpdate(
81
+ event: InteractionEvent,
82
+ accountId: string,
83
+ runtime: GatewayPluginRuntime,
84
+ log?: EngineLogger,
85
+ ): Promise<Record<string, unknown>> {
86
+ const configApi = runtime.config;
87
+ if (!configApi) {
88
+ throw new Error("runtime.config not available");
89
+ }
90
+
91
+ const resolved = event.data?.resolved as Record<string, unknown> | undefined;
92
+ const clawCfgUpdate = resolved?.claw_cfg as Record<string, unknown> | undefined;
93
+ const groupOpenid = event.group_openid ?? "";
94
+
95
+ const currentCfg = structuredClone(configApi.current());
96
+ let changed = false;
97
+
98
+ if (clawCfgUpdate?.require_mention !== undefined && groupOpenid) {
99
+ applyRequireMentionUpdate(currentCfg, accountId, groupOpenid, clawCfgUpdate);
100
+ changed = true;
101
+ }
102
+
103
+ if (changed) {
104
+ await configApi.replaceConfigFile({ nextConfig: currentCfg, afterWrite: { mode: "auto" } });
105
+ log?.info(
106
+ `Config updated via interaction ${event.id}: require_mention=${String(clawCfgUpdate?.require_mention)}, group=${groupOpenid}`,
107
+ );
108
+ }
109
+
110
+ const latestCfg = changed ? configApi.current() : currentCfg;
111
+ return buildClawCfgSnapshot(latestCfg, accountId, groupOpenid, runtime);
112
+ }
113
+
114
+ /** Mutate `cfg` in place to apply a require_mention update for a group. */
115
+ function applyRequireMentionUpdate(
116
+ cfg: Record<string, unknown>,
117
+ accountId: string,
118
+ groupOpenid: string,
119
+ update: Record<string, unknown>,
120
+ ): void {
121
+ const requireMentionBool = update.require_mention === "mention";
122
+ const channels = (cfg.channels ?? {}) as Record<string, unknown>;
123
+ const qqbot = (channels.qqbot ?? {}) as Record<string, unknown>;
124
+
125
+ const isNamedAccount =
126
+ accountId !== "default" &&
127
+ Boolean((qqbot.accounts as Record<string, Record<string, unknown>> | undefined)?.[accountId]);
128
+
129
+ if (isNamedAccount) {
130
+ const accounts = (qqbot.accounts ?? {}) as Record<string, Record<string, unknown>>;
131
+ const acct = accounts[accountId] ?? {};
132
+ const groups = (acct.groups ?? {}) as Record<string, Record<string, unknown>>;
133
+ groups[groupOpenid] = { ...groups[groupOpenid], requireMention: requireMentionBool };
134
+ acct.groups = groups;
135
+ accounts[accountId] = acct;
136
+ qqbot.accounts = accounts;
137
+ } else {
138
+ const groups = (qqbot.groups ?? {}) as Record<string, Record<string, unknown>>;
139
+ groups[groupOpenid] = { ...groups[groupOpenid], requireMention: requireMentionBool };
140
+ qqbot.groups = groups;
141
+ }
142
+ }
143
+
144
+ // ============ Public factory ============
145
+
146
+ /**
147
+ * Create the INTERACTION_CREATE event handler.
148
+ *
149
+ * Returns a fire-and-forget callback that `GatewayConnection` calls
150
+ * on every `action: "interaction"` dispatch result.
151
+ */
152
+ export function createInteractionHandler(
153
+ account: GatewayAccount,
154
+ runtime: GatewayPluginRuntime,
155
+ log?: EngineLogger,
156
+ options?: { getActiveCfg?: () => AutoBotConfig },
157
+ ): (event: InteractionEvent) => void {
158
+ return (event) => {
159
+ const creds = accountToCreds(account);
160
+ const type = event.data?.type;
161
+
162
+ // ---- Config query (type=2001) ----
163
+ if (type === InteractionType.CONFIG_QUERY && runtime.config) {
164
+ void handleWithAck(creds, event, log, "CONFIG_QUERY", () => {
165
+ const cfg = runtime.config!.current();
166
+ return buildClawCfgSnapshot(cfg, account.accountId, event.group_openid ?? "", runtime);
167
+ });
168
+ return;
169
+ }
170
+
171
+ // ---- Config update (type=2002) ----
172
+ if (type === InteractionType.CONFIG_UPDATE && runtime.config) {
173
+ void handleWithAck(creds, event, log, "CONFIG_UPDATE", () =>
174
+ applyConfigUpdate(event, account.accountId, runtime, log),
175
+ );
176
+ return;
177
+ }
178
+
179
+ // ---- Approval button / other ----
180
+ const parsed = parseApprovalButtonData(event.data?.resolved?.button_data ?? "");
181
+ if (!parsed) {
182
+ void acknowledgeInteraction(creds, event.id).catch((err) => {
183
+ log?.error(`Interaction ACK failed: ${err instanceof Error ? err.message : String(err)}`);
184
+ });
185
+ return;
186
+ }
187
+
188
+ void handleApprovalButtonInteraction({
189
+ accountId: account.accountId,
190
+ creds,
191
+ event,
192
+ getActiveCfg: options?.getActiveCfg ?? runtime.config?.current,
193
+ log,
194
+ parsed,
195
+ });
196
+ };
197
+ }
198
+
199
+ // ============ Helpers ============
200
+
201
+ async function handleApprovalButtonInteraction(params: {
202
+ accountId: string;
203
+ creds: { appId: string; clientSecret: string };
204
+ event: InteractionEvent;
205
+ getActiveCfg?: () => AutoBotConfig | Record<string, unknown>;
206
+ log?: EngineLogger;
207
+ parsed: { approvalId: string; decision: "allow-once" | "allow-always" | "deny" };
208
+ }): Promise<void> {
209
+ if (!params.getActiveCfg) {
210
+ await acknowledgeApprovalInteraction(params.creds, params.event, params.log, {
211
+ content: "Approval is unavailable.",
212
+ });
213
+ params.log?.error("Approval button rejected: active config is unavailable");
214
+ return;
215
+ }
216
+
217
+ let cfg: AutoBotConfig;
218
+ try {
219
+ cfg = params.getActiveCfg() as AutoBotConfig;
220
+ } catch (err) {
221
+ await acknowledgeApprovalInteraction(params.creds, params.event, params.log, {
222
+ content: "Approval is unavailable.",
223
+ });
224
+ params.log?.error(
225
+ `Approval button rejected: active config failed to load: ${
226
+ err instanceof Error ? err.message : String(err)
227
+ }`,
228
+ );
229
+ return;
230
+ }
231
+
232
+ const authorization = authorizeApprovalButtonActor({
233
+ cfg,
234
+ accountId: params.accountId,
235
+ event: params.event,
236
+ approvalKind: resolveApprovalKind(params.parsed.approvalId),
237
+ });
238
+ if (!authorization.authorized) {
239
+ await acknowledgeApprovalInteraction(params.creds, params.event, params.log, {
240
+ content: authorization.reason ?? "You are not authorized to approve this request.",
241
+ });
242
+ params.log?.info(`Approval button rejected: id=${params.parsed.approvalId}`);
243
+ return;
244
+ }
245
+
246
+ await acknowledgeApprovalInteraction(params.creds, params.event, params.log);
247
+
248
+ const adapter = getPlatformAdapter();
249
+ if (!adapter.resolveApproval) {
250
+ params.log?.error("resolveApproval not available on PlatformAdapter");
251
+ return;
252
+ }
253
+
254
+ try {
255
+ const ok = await adapter.resolveApproval(params.parsed.approvalId, params.parsed.decision);
256
+ if (ok) {
257
+ params.log?.info(
258
+ `Approval resolved: id=${params.parsed.approvalId}, decision=${params.parsed.decision}`,
259
+ );
260
+ } else {
261
+ params.log?.error(`Approval resolve failed: id=${params.parsed.approvalId}`);
262
+ }
263
+ } catch (err) {
264
+ params.log?.error(
265
+ `Approval resolve failed: id=${params.parsed.approvalId}: ${
266
+ err instanceof Error ? err.message : String(err)
267
+ }`,
268
+ );
269
+ }
270
+ }
271
+
272
+ async function acknowledgeApprovalInteraction(
273
+ creds: { appId: string; clientSecret: string },
274
+ event: InteractionEvent,
275
+ log: EngineLogger | undefined,
276
+ data?: Record<string, unknown>,
277
+ ): Promise<void> {
278
+ try {
279
+ await acknowledgeInteraction(creds, event.id, 0, data);
280
+ } catch (err) {
281
+ log?.error(`Interaction ACK failed: ${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ }
284
+
285
+ function authorizeApprovalButtonActor(params: {
286
+ cfg: AutoBotConfig;
287
+ accountId: string;
288
+ event: InteractionEvent;
289
+ approvalKind: "exec" | "plugin";
290
+ }): { authorized: boolean; reason?: string } {
291
+ const senderIds = resolveApprovalActorSenderIds(params.event);
292
+ if (senderIds.length === 0) {
293
+ return authorizeQQBotApprovalAction({
294
+ cfg: params.cfg,
295
+ accountId: params.accountId,
296
+ senderId: null,
297
+ approvalKind: params.approvalKind,
298
+ });
299
+ }
300
+
301
+ let denial: { authorized: boolean; reason?: string } | undefined;
302
+ for (const senderId of senderIds) {
303
+ const result = authorizeQQBotApprovalAction({
304
+ cfg: params.cfg,
305
+ accountId: params.accountId,
306
+ senderId,
307
+ approvalKind: params.approvalKind,
308
+ });
309
+ if (result.authorized) {
310
+ return result;
311
+ }
312
+ denial ??= result;
313
+ }
314
+ return denial ?? { authorized: false, reason: "You are not authorized to approve this request." };
315
+ }
316
+
317
+ function resolveApprovalActorSenderIds(event: InteractionEvent): string[] {
318
+ const ids = [event.group_member_openid, event.user_openid].flatMap((value) => {
319
+ const normalized = typeof value === "string" ? value.trim() : "";
320
+ return normalized ? [normalized] : [];
321
+ });
322
+ return Array.from(new Set(ids));
323
+ }
324
+
325
+ function resolveApprovalKind(approvalId: string): "exec" | "plugin" {
326
+ return approvalId.toLowerCase().startsWith("plugin:") ? "plugin" : "exec";
327
+ }
328
+
329
+ /** Execute an async handler, ACK with the result, and handle errors. */
330
+ async function handleWithAck(
331
+ creds: { appId: string; clientSecret: string },
332
+ event: InteractionEvent,
333
+ log: EngineLogger | undefined,
334
+ label: string,
335
+ handler: () => Record<string, unknown> | Promise<Record<string, unknown>>,
336
+ ): Promise<void> {
337
+ try {
338
+ const clawCfg = await handler();
339
+ await acknowledgeInteraction(creds, event.id, 0, { claw_cfg: clawCfg });
340
+ log?.info(`Interaction ACK (${label}) sent: ${event.id}`);
341
+ } catch (err) {
342
+ log?.error(`${label} interaction failed: ${err instanceof Error ? err.message : String(err)}`);
343
+ void acknowledgeInteraction(creds, event.id).catch(() => {});
344
+ }
345
+ }