@openclaw/qqbot 2026.5.2 → 2026.5.3-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 (203) hide show
  1. package/dist/api.js +646 -0
  2. package/dist/approval-cg0SVahb.js +94 -0
  3. package/dist/channel-N6Y_Rcjp.js +551 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.setup-ByCRAe5H.js +25 -0
  6. package/dist/config-D6545NkC.js +94 -0
  7. package/dist/config-schema-DFcjQw73.js +309 -0
  8. package/dist/exec-approvals-COUsM6wZ.js +138 -0
  9. package/dist/gateway-CmSUJKSt.js +6726 -0
  10. package/dist/handler-runtime-Bqm6N0WG.js +120 -0
  11. package/dist/index.js +29 -0
  12. package/dist/narrowing-BoieBTIU.js +25 -0
  13. package/dist/outbound-BJfhwrPg.js +1357 -0
  14. package/dist/request-context-DXtpwWui.js +921 -0
  15. package/dist/resolve-D_06fV6-.js +284 -0
  16. package/dist/runtime-BJAS3eXW.js +18 -0
  17. package/dist/runtime-api.js +2 -0
  18. package/dist/secret-contract-api.js +56 -0
  19. package/dist/sender-p-B14eLG.js +1987 -0
  20. package/dist/setup-entry.js +15 -0
  21. package/dist/setup-plugin-api.js +2 -0
  22. package/dist/string-normalize-Ci6NM5DE.js +98 -0
  23. package/dist/target-parser-Y0prnrXD.js +245 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -56
  26. package/channel-plugin-api.ts +0 -1
  27. package/index.ts +0 -29
  28. package/runtime-api.ts +0 -9
  29. package/setup-entry.ts +0 -9
  30. package/setup-plugin-api.ts +0 -3
  31. package/src/bridge/approval/capability.ts +0 -237
  32. package/src/bridge/approval/handler-runtime.ts +0 -204
  33. package/src/bridge/bootstrap.ts +0 -135
  34. package/src/bridge/channel-entry.ts +0 -18
  35. package/src/bridge/commands/framework-context-adapter.ts +0 -60
  36. package/src/bridge/commands/framework-registration.ts +0 -47
  37. package/src/bridge/commands/from-parser.test.ts +0 -86
  38. package/src/bridge/commands/from-parser.ts +0 -60
  39. package/src/bridge/commands/result-dispatcher.ts +0 -76
  40. package/src/bridge/config-shared.ts +0 -132
  41. package/src/bridge/config.ts +0 -111
  42. package/src/bridge/gateway.ts +0 -174
  43. package/src/bridge/logger.ts +0 -31
  44. package/src/bridge/narrowing.ts +0 -31
  45. package/src/bridge/plugin-version.test.ts +0 -146
  46. package/src/bridge/plugin-version.ts +0 -102
  47. package/src/bridge/runtime.ts +0 -25
  48. package/src/bridge/sdk-adapter.ts +0 -131
  49. package/src/bridge/setup/finalize.ts +0 -144
  50. package/src/bridge/setup/surface.ts +0 -34
  51. package/src/bridge/tools/channel.ts +0 -58
  52. package/src/bridge/tools/index.ts +0 -15
  53. package/src/bridge/tools/remind.test.ts +0 -124
  54. package/src/bridge/tools/remind.ts +0 -91
  55. package/src/channel.setup.ts +0 -33
  56. package/src/channel.ts +0 -288
  57. package/src/command-auth.test.ts +0 -62
  58. package/src/config-schema.ts +0 -84
  59. package/src/config.test.ts +0 -364
  60. package/src/engine/access/access-control.test.ts +0 -198
  61. package/src/engine/access/access-control.ts +0 -226
  62. package/src/engine/access/index.ts +0 -16
  63. package/src/engine/access/resolve-policy.test.ts +0 -59
  64. package/src/engine/access/resolve-policy.ts +0 -57
  65. package/src/engine/access/sender-match.test.ts +0 -60
  66. package/src/engine/access/sender-match.ts +0 -55
  67. package/src/engine/access/types.ts +0 -53
  68. package/src/engine/adapter/audio.port.ts +0 -27
  69. package/src/engine/adapter/commands.port.ts +0 -22
  70. package/src/engine/adapter/history.port.ts +0 -52
  71. package/src/engine/adapter/index.ts +0 -139
  72. package/src/engine/adapter/mention-gate.port.ts +0 -50
  73. package/src/engine/adapter/types.ts +0 -38
  74. package/src/engine/api/api-client.ts +0 -212
  75. package/src/engine/api/media-chunked.test.ts +0 -336
  76. package/src/engine/api/media-chunked.ts +0 -622
  77. package/src/engine/api/media.ts +0 -218
  78. package/src/engine/api/messages.ts +0 -293
  79. package/src/engine/api/retry.ts +0 -217
  80. package/src/engine/api/routes.ts +0 -95
  81. package/src/engine/api/token.ts +0 -271
  82. package/src/engine/approval/index.test.ts +0 -22
  83. package/src/engine/approval/index.ts +0 -224
  84. package/src/engine/commands/builtin/log-helpers.ts +0 -319
  85. package/src/engine/commands/builtin/register-all.ts +0 -17
  86. package/src/engine/commands/builtin/register-approve.ts +0 -201
  87. package/src/engine/commands/builtin/register-basic.ts +0 -95
  88. package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
  89. package/src/engine/commands/builtin/register-logs.ts +0 -20
  90. package/src/engine/commands/builtin/register-streaming.ts +0 -137
  91. package/src/engine/commands/builtin/state.ts +0 -31
  92. package/src/engine/commands/slash-command-auth.ts +0 -48
  93. package/src/engine/commands/slash-command-handler.ts +0 -146
  94. package/src/engine/commands/slash-commands-impl.test.ts +0 -8
  95. package/src/engine/commands/slash-commands-impl.ts +0 -61
  96. package/src/engine/commands/slash-commands.ts +0 -199
  97. package/src/engine/config/credential-backup.test.ts +0 -88
  98. package/src/engine/config/credential-backup.ts +0 -107
  99. package/src/engine/config/credentials.ts +0 -76
  100. package/src/engine/config/group.test.ts +0 -234
  101. package/src/engine/config/group.ts +0 -299
  102. package/src/engine/config/resolve.test.ts +0 -152
  103. package/src/engine/config/resolve.ts +0 -283
  104. package/src/engine/config/setup-logic.ts +0 -84
  105. package/src/engine/engine-import-boundary.test.ts +0 -73
  106. package/src/engine/gateway/codec.ts +0 -47
  107. package/src/engine/gateway/constants.ts +0 -117
  108. package/src/engine/gateway/event-dispatcher.ts +0 -177
  109. package/src/engine/gateway/gateway-connection.ts +0 -371
  110. package/src/engine/gateway/gateway.ts +0 -291
  111. package/src/engine/gateway/inbound-attachments.test.ts +0 -126
  112. package/src/engine/gateway/inbound-attachments.ts +0 -360
  113. package/src/engine/gateway/inbound-context.ts +0 -195
  114. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
  115. package/src/engine/gateway/inbound-pipeline.ts +0 -235
  116. package/src/engine/gateway/interaction-handler.ts +0 -220
  117. package/src/engine/gateway/message-queue.test.ts +0 -282
  118. package/src/engine/gateway/message-queue.ts +0 -499
  119. package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
  120. package/src/engine/gateway/outbound-dispatch.ts +0 -575
  121. package/src/engine/gateway/reconnect.ts +0 -199
  122. package/src/engine/gateway/stages/access-stage.ts +0 -132
  123. package/src/engine/gateway/stages/assembly-stage.ts +0 -156
  124. package/src/engine/gateway/stages/content-stage.test.ts +0 -77
  125. package/src/engine/gateway/stages/content-stage.ts +0 -77
  126. package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
  127. package/src/engine/gateway/stages/envelope-stage.ts +0 -144
  128. package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
  129. package/src/engine/gateway/stages/index.ts +0 -18
  130. package/src/engine/gateway/stages/quote-stage.ts +0 -113
  131. package/src/engine/gateway/stages/refidx-stage.ts +0 -62
  132. package/src/engine/gateway/stages/stub-contexts.ts +0 -116
  133. package/src/engine/gateway/types.ts +0 -264
  134. package/src/engine/gateway/typing-keepalive.ts +0 -79
  135. package/src/engine/group/activation.test.ts +0 -114
  136. package/src/engine/group/activation.ts +0 -147
  137. package/src/engine/group/history.test.ts +0 -314
  138. package/src/engine/group/history.ts +0 -321
  139. package/src/engine/group/mention.test.ts +0 -141
  140. package/src/engine/group/mention.ts +0 -197
  141. package/src/engine/group/message-gating.test.ts +0 -188
  142. package/src/engine/group/message-gating.ts +0 -216
  143. package/src/engine/messaging/decode-media-path.ts +0 -82
  144. package/src/engine/messaging/media-source.ts +0 -215
  145. package/src/engine/messaging/media-type-detect.ts +0 -37
  146. package/src/engine/messaging/outbound-audio-port.ts +0 -38
  147. package/src/engine/messaging/outbound-deliver.ts +0 -810
  148. package/src/engine/messaging/outbound-media-send.ts +0 -702
  149. package/src/engine/messaging/outbound-reply.ts +0 -27
  150. package/src/engine/messaging/outbound-result-helpers.ts +0 -54
  151. package/src/engine/messaging/outbound-types.ts +0 -45
  152. package/src/engine/messaging/outbound.ts +0 -485
  153. package/src/engine/messaging/reply-dispatcher.ts +0 -597
  154. package/src/engine/messaging/reply-limiter.ts +0 -164
  155. package/src/engine/messaging/sender.ts +0 -729
  156. package/src/engine/messaging/streaming-c2c.ts +0 -1192
  157. package/src/engine/messaging/streaming-media-send.ts +0 -544
  158. package/src/engine/messaging/target-parser.ts +0 -104
  159. package/src/engine/ref/format-message-ref.ts +0 -142
  160. package/src/engine/ref/format-ref-entry.test.ts +0 -60
  161. package/src/engine/ref/format-ref-entry.ts +0 -27
  162. package/src/engine/ref/store.ts +0 -208
  163. package/src/engine/ref/types.ts +0 -27
  164. package/src/engine/session/known-users.ts +0 -137
  165. package/src/engine/session/session-store.ts +0 -204
  166. package/src/engine/tools/channel-api.ts +0 -244
  167. package/src/engine/tools/remind-logic.test.ts +0 -280
  168. package/src/engine/tools/remind-logic.ts +0 -377
  169. package/src/engine/types.ts +0 -313
  170. package/src/engine/utils/attachment-tags.test.ts +0 -186
  171. package/src/engine/utils/attachment-tags.ts +0 -174
  172. package/src/engine/utils/audio.test.ts +0 -250
  173. package/src/engine/utils/audio.ts +0 -585
  174. package/src/engine/utils/data-paths.ts +0 -38
  175. package/src/engine/utils/diagnostics.ts +0 -109
  176. package/src/engine/utils/file-utils.test.ts +0 -72
  177. package/src/engine/utils/file-utils.ts +0 -225
  178. package/src/engine/utils/format.test.ts +0 -68
  179. package/src/engine/utils/format.ts +0 -70
  180. package/src/engine/utils/image-size.test.ts +0 -158
  181. package/src/engine/utils/image-size.ts +0 -249
  182. package/src/engine/utils/log.test.ts +0 -28
  183. package/src/engine/utils/log.ts +0 -61
  184. package/src/engine/utils/media-tags.test.ts +0 -32
  185. package/src/engine/utils/media-tags.ts +0 -177
  186. package/src/engine/utils/payload.test.ts +0 -68
  187. package/src/engine/utils/payload.ts +0 -145
  188. package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
  189. package/src/engine/utils/platform.test.ts +0 -148
  190. package/src/engine/utils/platform.ts +0 -343
  191. package/src/engine/utils/request-context.ts +0 -60
  192. package/src/engine/utils/string-normalize.ts +0 -91
  193. package/src/engine/utils/stt.test.ts +0 -104
  194. package/src/engine/utils/stt.ts +0 -100
  195. package/src/engine/utils/text-parsing.test.ts +0 -29
  196. package/src/engine/utils/text-parsing.ts +0 -155
  197. package/src/engine/utils/upload-cache.ts +0 -96
  198. package/src/engine/utils/voice-text.ts +0 -15
  199. package/src/exec-approvals.ts +0 -218
  200. package/src/manifest-schema.test.ts +0 -56
  201. package/src/qqbot-test-support.ts +0 -29
  202. package/src/types.ts +0 -210
  203. package/tsconfig.json +0 -16
@@ -1,142 +0,0 @@
1
- /**
2
- * Format a message_reference (from msg_elements[0]) into text for model context.
3
- *
4
- * This handles the cache-miss path: when a user quotes a message we haven't
5
- * cached in the ref-index store, we fall back to the msg_elements[0] data
6
- * pushed by the QQ platform.
7
- *
8
- * The heavy lifting (attachment download, STT, etc.) is delegated to an
9
- * injected `AttachmentProcessor` so this module stays framework-agnostic.
10
- */
11
-
12
- import type { EngineLogger } from "../types.js";
13
- import { parseFaceTags, buildAttachmentSummaries } from "../utils/text-parsing.js";
14
- import { formatRefEntryForAgent } from "./format-ref-entry.js";
15
- import type { RefAttachmentSummary } from "./types.js";
16
-
17
- // ============ Injected dependency ============
18
-
19
- /** Attachment download & voice transcription — injected from the outer layer. */
20
- export interface AttachmentProcessor {
21
- processAttachments(
22
- attachments:
23
- | Array<{
24
- content_type: string;
25
- url: string;
26
- filename?: string;
27
- height?: number;
28
- width?: number;
29
- size?: number;
30
- voice_wav_url?: string;
31
- asr_refer_text?: string;
32
- }>
33
- | undefined,
34
- ctx: { appId: string; peerId?: string; cfg: unknown; log?: EngineLogger },
35
- ): Promise<{
36
- attachmentInfo: string;
37
- voiceTranscripts: string[];
38
- voiceTranscriptSources: string[];
39
- attachmentLocalPaths: Array<string | null>;
40
- }>;
41
-
42
- formatVoiceText(voiceTranscripts: string[]): string;
43
- }
44
-
45
- // ============ Public API ============
46
-
47
- /**
48
- * Format a quoted message reference into human-readable text for model context.
49
- *
50
- * This mirrors the independent version's `formatMessageReferenceForAgent` —
51
- * processing attachments (download + STT) and combining them with parsed text.
52
- *
53
- * @param ref - The msg_elements[0] data from the QQ push event.
54
- * @param ctx - Context containing appId, peerId, config, and logger.
55
- * @param processor - Injected attachment processor (download + voice transcription).
56
- */
57
- export async function formatMessageReferenceForAgent(
58
- ref:
59
- | {
60
- content?: string;
61
- attachments?: Array<{
62
- content_type: string;
63
- url: string;
64
- filename?: string;
65
- height?: number;
66
- width?: number;
67
- size?: number;
68
- voice_wav_url?: string;
69
- asr_refer_text?: string;
70
- }>;
71
- }
72
- | undefined,
73
- ctx: {
74
- appId: string;
75
- peerId?: string;
76
- cfg: unknown;
77
- log?: EngineLogger;
78
- },
79
- processor: AttachmentProcessor,
80
- ): Promise<string> {
81
- if (!ref) {
82
- return "";
83
- }
84
-
85
- // Process attachments (download images, transcribe voice, etc.)
86
- const processed = await processor.processAttachments(ref.attachments, ctx);
87
- const { attachmentInfo, voiceTranscripts, voiceTranscriptSources, attachmentLocalPaths } =
88
- processed;
89
-
90
- // Format voice transcript text
91
- const voiceText = processor.formatVoiceText(voiceTranscripts);
92
-
93
- // Parse QQ face tags into readable text
94
- const parsedContent = parseFaceTags(ref.content ?? "");
95
-
96
- // Combine text content with voice transcript and attachment info
97
- const userContent = voiceText
98
- ? (parsedContent.trim() ? `${parsedContent}\n${voiceText}` : voiceText) + attachmentInfo
99
- : parsedContent + attachmentInfo;
100
-
101
- // Build attachment summaries and inject voice transcripts
102
- const attSummaries = buildAttachmentSummaries(
103
- ref.attachments as Array<{
104
- content_type: string;
105
- url: string;
106
- filename?: string;
107
- voice_wav_url?: string;
108
- }>,
109
- attachmentLocalPaths,
110
- );
111
- if (attSummaries && voiceTranscripts.length > 0) {
112
- let voiceIdx = 0;
113
- for (const att of attSummaries) {
114
- if (att.type === "voice" && voiceIdx < voiceTranscripts.length) {
115
- att.transcript = voiceTranscripts[voiceIdx];
116
- if (voiceIdx < voiceTranscriptSources.length) {
117
- att.transcriptSource = voiceTranscriptSources[
118
- voiceIdx
119
- ] as RefAttachmentSummary["transcriptSource"];
120
- }
121
- voiceIdx++;
122
- }
123
- }
124
- }
125
-
126
- // Format using the same function as the cache-hit path
127
- const refEntry = {
128
- content: userContent.trim(),
129
- senderId: "",
130
- timestamp: Date.now(),
131
- attachments: attSummaries,
132
- };
133
-
134
- const formattedAttachments = formatRefEntryForAgent(refEntry);
135
- // If formatRefEntryForAgent already includes the content, use it directly.
136
- // Otherwise combine manually.
137
- if (formattedAttachments !== "[empty message]") {
138
- return formattedAttachments;
139
- }
140
-
141
- return userContent.trim() || "";
142
- }
@@ -1,60 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { formatRefEntryForAgent } from "./format-ref-entry.js";
3
- import type { RefIndexEntry } from "./types.js";
4
-
5
- function makeEntry(overrides: Partial<RefIndexEntry> = {}): RefIndexEntry {
6
- return {
7
- content: "hello",
8
- senderId: "user-1",
9
- timestamp: 1,
10
- ...overrides,
11
- };
12
- }
13
-
14
- describe("engine/ref/format-ref-entry", () => {
15
- it("formats text and attachment hints for model context", () => {
16
- const formatted = formatRefEntryForAgent(
17
- makeEntry({
18
- content: "see these",
19
- attachments: [
20
- {
21
- type: "image",
22
- filename: "photo.png",
23
- localPath: "/tmp/photo.png",
24
- },
25
- {
26
- type: "voice",
27
- transcript: "spoken words",
28
- transcriptSource: "asr",
29
- url: "https://example.test/voice.amr",
30
- },
31
- {
32
- type: "file",
33
- filename: "notes.txt",
34
- },
35
- ],
36
- }),
37
- );
38
-
39
- expect(formatted).toBe(
40
- 'see these MEDIA:/tmp/photo.png MEDIA:https://example.test/voice.amr (transcript: "spoken words") [source: platform ASR] [file: notes.txt]',
41
- );
42
- });
43
-
44
- it("keeps voice attachments visible when no transcript exists", () => {
45
- expect(
46
- formatRefEntryForAgent(
47
- makeEntry({
48
- content: "",
49
- attachments: [{ type: "voice", localPath: "/tmp/voice.wav" }],
50
- }),
51
- ),
52
- ).toBe("MEDIA:/tmp/voice.wav");
53
- });
54
-
55
- it("returns an explicit empty marker for blank entries", () => {
56
- expect(formatRefEntryForAgent(makeEntry({ content: " ", attachments: [] }))).toBe(
57
- "[empty message]",
58
- );
59
- });
60
- });
@@ -1,27 +0,0 @@
1
- /**
2
- * Format a ref-index entry into text suitable for model context.
3
- *
4
- * Delegates all attachment rendering to the shared
5
- * `utils/attachment-tags.ts::renderAttachmentTags` (with `mode: "ref"`)
6
- * so the quoted-message preview and the current-message history use
7
- * identical wording for identical attachment types.
8
- */
9
-
10
- import { renderAttachmentTags } from "../utils/attachment-tags.js";
11
- import type { RefIndexEntry } from "./types.js";
12
-
13
- /** Format a ref-index entry into text suitable for model context. */
14
- export function formatRefEntryForAgent(entry: RefIndexEntry): string {
15
- const parts: string[] = [];
16
-
17
- if (entry.content.trim()) {
18
- parts.push(entry.content);
19
- }
20
-
21
- const attachmentTags = renderAttachmentTags(entry.attachments, { mode: "ref" });
22
- if (attachmentTags) {
23
- parts.push(attachmentTags);
24
- }
25
-
26
- return parts.join(" ") || "[empty message]";
27
- }
@@ -1,208 +0,0 @@
1
- /**
2
- * Ref-index store — JSONL file-based store for message reference index.
3
- *
4
- * Migrated from src/ref-index-store.ts. Dependencies are only Node.js
5
- * built-ins + log + platform (both zero plugin-sdk).
6
- */
7
-
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { formatErrorMessage } from "../utils/format.js";
11
- import { debugLog, debugError } from "../utils/log.js";
12
- import { getQQBotDataDir, getQQBotDataPath } from "../utils/platform.js";
13
- import type { RefIndexEntry } from "./types.js";
14
-
15
- // Re-export types and format function for convenience.
16
- export type { RefIndexEntry, RefAttachmentSummary } from "./types.js";
17
- export { formatRefEntryForAgent } from "./format-ref-entry.js";
18
-
19
- const MAX_ENTRIES = 50000;
20
- const TTL_MS = 7 * 24 * 60 * 60 * 1000;
21
- const COMPACT_THRESHOLD_RATIO = 2;
22
-
23
- interface RefIndexLine {
24
- k: string;
25
- v: RefIndexEntry;
26
- t: number;
27
- }
28
-
29
- let cache: Map<string, RefIndexEntry & { _createdAt: number }> | null = null;
30
- let totalLinesOnDisk = 0;
31
-
32
- function getRefIndexFile(): string {
33
- return path.join(getQQBotDataPath("data"), "ref-index.jsonl");
34
- }
35
-
36
- function loadFromFile(): Map<string, RefIndexEntry & { _createdAt: number }> {
37
- if (cache !== null) {
38
- return cache;
39
- }
40
- cache = new Map();
41
- totalLinesOnDisk = 0;
42
-
43
- try {
44
- const refIndexFile = getRefIndexFile();
45
- if (!fs.existsSync(refIndexFile)) {
46
- return cache;
47
- }
48
- const raw = fs.readFileSync(refIndexFile, "utf-8");
49
- const lines = raw.split("\n");
50
- const now = Date.now();
51
- let expired = 0;
52
-
53
- for (const line of lines) {
54
- const trimmed = line.trim();
55
- if (!trimmed) {
56
- continue;
57
- }
58
- totalLinesOnDisk++;
59
- try {
60
- const entry = JSON.parse(trimmed) as RefIndexLine;
61
- if (!entry.k || !entry.v || !entry.t) {
62
- continue;
63
- }
64
- if (now - entry.t > TTL_MS) {
65
- expired++;
66
- continue;
67
- }
68
- cache.set(entry.k, { ...entry.v, _createdAt: entry.t });
69
- } catch {}
70
- }
71
- debugLog(
72
- `[ref-index-store] Loaded ${cache.size} entries from ${totalLinesOnDisk} lines (${expired} expired)`,
73
- );
74
- if (shouldCompact()) {
75
- compactFile();
76
- }
77
- } catch (err) {
78
- debugError(`[ref-index-store] Failed to load: ${formatErrorMessage(err)}`);
79
- cache = new Map();
80
- }
81
- return cache;
82
- }
83
-
84
- function ensureDir(): void {
85
- getQQBotDataDir("data");
86
- }
87
-
88
- function appendLine(line: RefIndexLine): void {
89
- try {
90
- ensureDir();
91
- fs.appendFileSync(getRefIndexFile(), JSON.stringify(line) + "\n", "utf-8");
92
- totalLinesOnDisk++;
93
- } catch (err) {
94
- debugError(`[ref-index-store] Failed to append: ${formatErrorMessage(err)}`);
95
- }
96
- }
97
-
98
- function shouldCompact(): boolean {
99
- return (
100
- !!cache && totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1000
101
- );
102
- }
103
-
104
- function compactFile(): void {
105
- if (!cache) {
106
- return;
107
- }
108
- const before = totalLinesOnDisk;
109
- try {
110
- ensureDir();
111
- const refIndexFile = getRefIndexFile();
112
- const tmpPath = refIndexFile + ".tmp";
113
- const lines: string[] = [];
114
- for (const [key, entry] of cache) {
115
- lines.push(
116
- JSON.stringify({
117
- k: key,
118
- v: {
119
- content: entry.content,
120
- senderId: entry.senderId,
121
- senderName: entry.senderName,
122
- timestamp: entry.timestamp,
123
- isBot: entry.isBot,
124
- attachments: entry.attachments,
125
- },
126
- t: entry._createdAt,
127
- }),
128
- );
129
- }
130
- fs.writeFileSync(tmpPath, lines.join("\n") + "\n", "utf-8");
131
- fs.renameSync(tmpPath, refIndexFile);
132
- totalLinesOnDisk = cache.size;
133
- debugLog(`[ref-index-store] Compacted: ${before} lines → ${totalLinesOnDisk} lines`);
134
- } catch (err) {
135
- debugError(`[ref-index-store] Compact failed: ${formatErrorMessage(err)}`);
136
- }
137
- }
138
-
139
- function evictIfNeeded(): void {
140
- if (!cache || cache.size < MAX_ENTRIES) {
141
- return;
142
- }
143
- const now = Date.now();
144
- for (const [key, entry] of cache) {
145
- if (now - entry._createdAt > TTL_MS) {
146
- cache.delete(key);
147
- }
148
- }
149
- if (cache.size >= MAX_ENTRIES) {
150
- const sorted = [...cache.entries()].toSorted((a, b) => a[1]._createdAt - b[1]._createdAt);
151
- const toRemove = sorted.slice(0, cache.size - MAX_ENTRIES + 1000);
152
- for (const [key] of toRemove) {
153
- cache.delete(key);
154
- }
155
- debugLog(`[ref-index-store] Evicted ${toRemove.length} oldest entries`);
156
- }
157
- }
158
-
159
- /** Persist a refIdx mapping for one message. */
160
- export function setRefIndex(refIdx: string, entry: RefIndexEntry): void {
161
- const store = loadFromFile();
162
- evictIfNeeded();
163
- const now = Date.now();
164
- store.set(refIdx, { ...entry, _createdAt: now });
165
- appendLine({
166
- k: refIdx,
167
- v: {
168
- content: entry.content,
169
- senderId: entry.senderId,
170
- senderName: entry.senderName,
171
- timestamp: entry.timestamp,
172
- isBot: entry.isBot,
173
- attachments: entry.attachments,
174
- },
175
- t: now,
176
- });
177
- if (shouldCompact()) {
178
- compactFile();
179
- }
180
- }
181
-
182
- /** Look up one quoted message by refIdx. */
183
- export function getRefIndex(refIdx: string): RefIndexEntry | null {
184
- const store = loadFromFile();
185
- const entry = store.get(refIdx);
186
- if (!entry) {
187
- return null;
188
- }
189
- if (Date.now() - entry._createdAt > TTL_MS) {
190
- store.delete(refIdx);
191
- return null;
192
- }
193
- return {
194
- content: entry.content,
195
- senderId: entry.senderId,
196
- senderName: entry.senderName,
197
- timestamp: entry.timestamp,
198
- isBot: entry.isBot,
199
- attachments: entry.attachments,
200
- };
201
- }
202
-
203
- /** Compact the store before process exit when needed. */
204
- export function flushRefIndex(): void {
205
- if (cache && shouldCompact()) {
206
- compactFile();
207
- }
208
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Ref-index types shared between both plugin versions.
3
- *
4
- * These types define the structure of quoted-message metadata
5
- * persisted by the ref-index store.
6
- */
7
-
8
- /** Summary stored for one quoted message. */
9
- export interface RefIndexEntry {
10
- content: string;
11
- senderId: string;
12
- senderName?: string;
13
- timestamp: number;
14
- isBot?: boolean;
15
- attachments?: RefAttachmentSummary[];
16
- }
17
-
18
- /** Attachment summary persisted alongside a ref index entry. */
19
- export interface RefAttachmentSummary {
20
- type: "image" | "voice" | "video" | "file" | "unknown";
21
- filename?: string;
22
- contentType?: string;
23
- transcript?: string;
24
- transcriptSource?: "stt" | "asr" | "tts" | "fallback";
25
- localPath?: string;
26
- url?: string;
27
- }
@@ -1,137 +0,0 @@
1
- /**
2
- * Known user tracking — JSON file-based store.
3
- *
4
- * Migrated from src/known-users.ts. Dependencies are only Node.js
5
- * built-ins + log + platform (both zero plugin-sdk).
6
- */
7
-
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import type { ChatScope } from "../types.js";
11
- import { formatErrorMessage } from "../utils/format.js";
12
- import { debugLog, debugError } from "../utils/log.js";
13
- import { getQQBotDataDir, getQQBotDataPath } from "../utils/platform.js";
14
-
15
- /** Persisted record for a user who has interacted with the bot. */
16
- interface KnownUser {
17
- openid: string;
18
- type: ChatScope;
19
- nickname?: string;
20
- groupOpenid?: string;
21
- accountId: string;
22
- firstSeenAt: number;
23
- lastSeenAt: number;
24
- interactionCount: number;
25
- }
26
-
27
- let usersCache: Map<string, KnownUser> | null = null;
28
- const SAVE_THROTTLE_MS = 5000;
29
- let saveTimer: ReturnType<typeof setTimeout> | null = null;
30
- let isDirty = false;
31
-
32
- function ensureDir(): void {
33
- getQQBotDataDir("data");
34
- }
35
-
36
- function getKnownUsersFile(): string {
37
- return path.join(getQQBotDataPath("data"), "known-users.json");
38
- }
39
-
40
- function makeUserKey(user: Partial<KnownUser>): string {
41
- const base = `${user.accountId}:${user.type}:${user.openid}`;
42
- return user.type === "group" && user.groupOpenid ? `${base}:${user.groupOpenid}` : base;
43
- }
44
-
45
- function loadUsersFromFile(): Map<string, KnownUser> {
46
- if (usersCache !== null) {
47
- return usersCache;
48
- }
49
- usersCache = new Map();
50
- try {
51
- const knownUsersFile = getKnownUsersFile();
52
- if (fs.existsSync(knownUsersFile)) {
53
- const data = fs.readFileSync(knownUsersFile, "utf-8");
54
- const users = JSON.parse(data) as KnownUser[];
55
- for (const user of users) {
56
- usersCache.set(makeUserKey(user), user);
57
- }
58
- debugLog(`[known-users] Loaded ${usersCache.size} users`);
59
- }
60
- } catch (err) {
61
- debugError(`[known-users] Failed to load users: ${formatErrorMessage(err)}`);
62
- usersCache = new Map();
63
- }
64
- return usersCache;
65
- }
66
-
67
- function saveUsersToFile(): void {
68
- if (!isDirty || saveTimer) {
69
- return;
70
- }
71
- saveTimer = setTimeout(() => {
72
- saveTimer = null;
73
- doSaveUsersToFile();
74
- }, SAVE_THROTTLE_MS);
75
- }
76
-
77
- function doSaveUsersToFile(): void {
78
- if (!usersCache || !isDirty) {
79
- return;
80
- }
81
- try {
82
- ensureDir();
83
- fs.writeFileSync(
84
- getKnownUsersFile(),
85
- JSON.stringify(Array.from(usersCache.values()), null, 2),
86
- "utf-8",
87
- );
88
- isDirty = false;
89
- } catch (err) {
90
- debugError(`[known-users] Failed to save users: ${formatErrorMessage(err)}`);
91
- }
92
- }
93
-
94
- /** Flush pending writes immediately, typically during shutdown. */
95
- export function flushKnownUsers(): void {
96
- if (saveTimer) {
97
- clearTimeout(saveTimer);
98
- saveTimer = null;
99
- }
100
- doSaveUsersToFile();
101
- }
102
-
103
- /** Record a known user whenever a message is received. */
104
- export function recordKnownUser(user: {
105
- openid: string;
106
- type: ChatScope;
107
- nickname?: string;
108
- groupOpenid?: string;
109
- accountId: string;
110
- }): void {
111
- const cache = loadUsersFromFile();
112
- const key = makeUserKey(user);
113
- const now = Date.now();
114
- const existing = cache.get(key);
115
-
116
- if (existing) {
117
- existing.lastSeenAt = now;
118
- existing.interactionCount++;
119
- if (user.nickname && user.nickname !== existing.nickname) {
120
- existing.nickname = user.nickname;
121
- }
122
- } else {
123
- cache.set(key, {
124
- openid: user.openid,
125
- type: user.type,
126
- nickname: user.nickname,
127
- groupOpenid: user.groupOpenid,
128
- accountId: user.accountId,
129
- firstSeenAt: now,
130
- lastSeenAt: now,
131
- interactionCount: 1,
132
- });
133
- debugLog(`[known-users] New user: ${user.openid} (${user.type})`);
134
- }
135
- isDirty = true;
136
- saveUsersToFile();
137
- }