@ryantest/openclaw-qqbot 0.0.1
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/LICENSE +22 -0
- package/README.md +483 -0
- package/README.zh.md +478 -0
- package/bin/qqbot-cli.js +243 -0
- package/clawdbot.plugin.json +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +26 -0
- package/dist/src/admin-resolver.d.ts +27 -0
- package/dist/src/admin-resolver.js +122 -0
- package/dist/src/api.d.ts +156 -0
- package/dist/src/api.js +599 -0
- package/dist/src/channel.d.ts +11 -0
- package/dist/src/channel.js +354 -0
- package/dist/src/config.d.ts +25 -0
- package/dist/src/config.js +161 -0
- package/dist/src/credential-backup.d.ts +31 -0
- package/dist/src/credential-backup.js +66 -0
- package/dist/src/gateway.d.ts +18 -0
- package/dist/src/gateway.js +1265 -0
- package/dist/src/image-server.d.ts +68 -0
- package/dist/src/image-server.js +462 -0
- package/dist/src/inbound-attachments.d.ts +58 -0
- package/dist/src/inbound-attachments.js +234 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +263 -0
- package/dist/src/message-queue.d.ts +50 -0
- package/dist/src/message-queue.js +115 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +203 -0
- package/dist/src/outbound-deliver.d.ts +48 -0
- package/dist/src/outbound-deliver.js +462 -0
- package/dist/src/outbound.d.ts +203 -0
- package/dist/src/outbound.js +1102 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +399 -0
- package/dist/src/ref-index-store.d.ts +70 -0
- package/dist/src/ref-index-store.js +273 -0
- package/dist/src/reply-dispatcher.d.ts +35 -0
- package/dist/src/reply-dispatcher.js +311 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +52 -0
- package/dist/src/session-store.js +254 -0
- package/dist/src/slash-commands.d.ts +71 -0
- package/dist/src/slash-commands.js +1179 -0
- package/dist/src/startup-greeting.d.ts +30 -0
- package/dist/src/startup-greeting.js +78 -0
- package/dist/src/stt.d.ts +21 -0
- package/dist/src/stt.js +70 -0
- package/dist/src/tools/channel.d.ts +16 -0
- package/dist/src/tools/channel.js +234 -0
- package/dist/src/tools/remind.d.ts +2 -0
- package/dist/src/tools/remind.js +247 -0
- package/dist/src/types.d.ts +175 -0
- package/dist/src/types.js +1 -0
- package/dist/src/typing-keepalive.d.ts +27 -0
- package/dist/src/typing-keepalive.js +64 -0
- package/dist/src/update-checker.d.ts +34 -0
- package/dist/src/update-checker.js +166 -0
- package/dist/src/user-messages.d.ts +8 -0
- package/dist/src/user-messages.js +8 -0
- package/dist/src/utils/audio-convert.d.ts +89 -0
- package/dist/src/utils/audio-convert.js +704 -0
- package/dist/src/utils/file-utils.d.ts +55 -0
- package/dist/src/utils/file-utils.js +150 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/media-tags.d.ts +14 -0
- package/dist/src/utils/media-tags.js +164 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/dist/src/utils/platform.d.ts +137 -0
- package/dist/src/utils/platform.js +390 -0
- package/dist/src/utils/text-parsing.d.ts +32 -0
- package/dist/src/utils/text-parsing.js +80 -0
- package/dist/src/utils/upload-cache.d.ts +34 -0
- package/dist/src/utils/upload-cache.js +93 -0
- package/index.ts +31 -0
- package/moltbot.plugin.json +16 -0
- package/node_modules/@eshaz/web-worker/LICENSE +201 -0
- package/node_modules/@eshaz/web-worker/README.md +134 -0
- package/node_modules/@eshaz/web-worker/browser.js +17 -0
- package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
- package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
- package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
- package/node_modules/@eshaz/web-worker/node.js +223 -0
- package/node_modules/@eshaz/web-worker/package.json +54 -0
- package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
- package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
- package/node_modules/mpg123-decoder/README.md +265 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
- package/node_modules/mpg123-decoder/index.js +8 -0
- package/node_modules/mpg123-decoder/package.json +58 -0
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
- package/node_modules/mpg123-decoder/types.d.ts +30 -0
- package/node_modules/silk-wasm/LICENSE +21 -0
- package/node_modules/silk-wasm/README.md +85 -0
- package/node_modules/silk-wasm/lib/index.cjs +16 -0
- package/node_modules/silk-wasm/lib/index.d.ts +70 -0
- package/node_modules/silk-wasm/lib/index.mjs +16 -0
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
- package/node_modules/silk-wasm/package.json +39 -0
- package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
- package/node_modules/simple-yenc/.prettierignore +1 -0
- package/node_modules/simple-yenc/LICENSE +7 -0
- package/node_modules/simple-yenc/README.md +163 -0
- package/node_modules/simple-yenc/dist/esm.js +1 -0
- package/node_modules/simple-yenc/dist/index.js +1 -0
- package/node_modules/simple-yenc/package.json +50 -0
- package/node_modules/simple-yenc/rollup.config.js +27 -0
- package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +13 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +69 -0
- package/node_modules/ws/wrapper.mjs +8 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +76 -0
- package/scripts/cleanup-legacy-plugins.sh +124 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-alt-pkg.sh +307 -0
- package/scripts/upgrade-via-npm.ps1 +296 -0
- package/scripts/upgrade-via-npm.sh +301 -0
- package/scripts/upgrade-via-source.sh +774 -0
- package/skills/qqbot-channel/SKILL.md +263 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +56 -0
- package/skills/qqbot-remind/SKILL.md +149 -0
- package/src/admin-resolver.ts +140 -0
- package/src/api.ts +819 -0
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +46 -0
- package/src/channel.ts +381 -0
- package/src/config.ts +187 -0
- package/src/credential-backup.ts +72 -0
- package/src/gateway.log +43 -0
- package/src/gateway.ts +1404 -0
- package/src/image-server.ts +539 -0
- package/src/inbound-attachments.ts +304 -0
- package/src/known-users.ts +353 -0
- package/src/message-queue.ts +169 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-2026-03-21.log +3729 -0
- package/src/openclaw-plugin-sdk.d.ts +522 -0
- package/src/outbound-deliver.ts +552 -0
- package/src/outbound.ts +1266 -0
- package/src/proactive.ts +530 -0
- package/src/ref-index-store.ts +357 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/slash-commands.ts +1305 -0
- package/src/startup-greeting.ts +98 -0
- package/src/stt.ts +86 -0
- package/src/tools/channel.ts +281 -0
- package/src/tools/remind.ts +296 -0
- package/src/types.ts +183 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/update-checker.ts +179 -0
- package/src/user-messages.ts +7 -0
- package/src/utils/audio-convert.ts +803 -0
- package/src/utils/file-utils.ts +167 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-tags.ts +182 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/platform.ts +435 -0
- package/src/utils/text-parsing.ts +82 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot 引用索引持久化存储
|
|
3
|
+
*
|
|
4
|
+
* QQ Bot 使用 REFIDX_xxx 索引体系做引用消息,
|
|
5
|
+
* 入站事件只有索引值,无 API 可回查内容。
|
|
6
|
+
* 采用 内存缓存 + JSONL 追加写持久化 方案,确保重启后历史引用仍可命中。
|
|
7
|
+
*
|
|
8
|
+
* 存储位置:~/.openclaw/qqbot/data/ref-index.jsonl
|
|
9
|
+
*
|
|
10
|
+
* 每行格式:{"k":"REFIDX_xxx","v":{...},"t":1709000000}
|
|
11
|
+
* - k = refIdx 键
|
|
12
|
+
* - v = 消息数据
|
|
13
|
+
* - t = 写入时间(用于 TTL 淘汰和 compact)
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { getQQBotDataDir } from "./utils/platform.js";
|
|
18
|
+
// ============ 配置 ============
|
|
19
|
+
const STORAGE_DIR = getQQBotDataDir("data");
|
|
20
|
+
const REF_INDEX_FILE = path.join(STORAGE_DIR, "ref-index.jsonl");
|
|
21
|
+
const MAX_ENTRIES = 50000; // 内存中最大缓存条目数
|
|
22
|
+
const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
|
|
23
|
+
const COMPACT_THRESHOLD_RATIO = 2; // 文件行数超过有效条目 N 倍时 compact
|
|
24
|
+
// ============ 内存缓存 ============
|
|
25
|
+
let cache = null;
|
|
26
|
+
let totalLinesOnDisk = 0; // 磁盘文件总行数(含过期 / 被覆盖的)
|
|
27
|
+
/**
|
|
28
|
+
* 从 JSONL 文件加载到内存(懒加载,首次访问时触发)
|
|
29
|
+
*/
|
|
30
|
+
function loadFromFile() {
|
|
31
|
+
if (cache !== null)
|
|
32
|
+
return cache;
|
|
33
|
+
cache = new Map();
|
|
34
|
+
totalLinesOnDisk = 0;
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(REF_INDEX_FILE)) {
|
|
37
|
+
return cache;
|
|
38
|
+
}
|
|
39
|
+
const raw = fs.readFileSync(REF_INDEX_FILE, "utf-8");
|
|
40
|
+
const lines = raw.split("\n");
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
let expired = 0;
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed)
|
|
46
|
+
continue;
|
|
47
|
+
totalLinesOnDisk++;
|
|
48
|
+
try {
|
|
49
|
+
const entry = JSON.parse(trimmed);
|
|
50
|
+
if (!entry.k || !entry.v || !entry.t)
|
|
51
|
+
continue;
|
|
52
|
+
// 跳过过期条目
|
|
53
|
+
if (now - entry.t > TTL_MS) {
|
|
54
|
+
expired++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
cache.set(entry.k, {
|
|
58
|
+
...entry.v,
|
|
59
|
+
_createdAt: entry.t,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// 跳过损坏的行
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
console.log(`[ref-index-store] Loaded ${cache.size} entries from ${totalLinesOnDisk} lines (${expired} expired)`);
|
|
67
|
+
// 启动时检查是否需要 compact
|
|
68
|
+
if (shouldCompact()) {
|
|
69
|
+
compactFile();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(`[ref-index-store] Failed to load: ${err}`);
|
|
74
|
+
cache = new Map();
|
|
75
|
+
}
|
|
76
|
+
return cache;
|
|
77
|
+
}
|
|
78
|
+
// ============ JSONL 追加写入 ============
|
|
79
|
+
/**
|
|
80
|
+
* 追加一行到 JSONL 文件
|
|
81
|
+
*/
|
|
82
|
+
function appendLine(line) {
|
|
83
|
+
try {
|
|
84
|
+
ensureDir();
|
|
85
|
+
fs.appendFileSync(REF_INDEX_FILE, JSON.stringify(line) + "\n", "utf-8");
|
|
86
|
+
totalLinesOnDisk++;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error(`[ref-index-store] Failed to append: ${err}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function ensureDir() {
|
|
93
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
94
|
+
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ============ Compact:重写文件,去除过期和被覆盖的条目 ============
|
|
98
|
+
function shouldCompact() {
|
|
99
|
+
if (!cache)
|
|
100
|
+
return false;
|
|
101
|
+
// 文件行数远超有效条目数时 compact
|
|
102
|
+
return totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1000;
|
|
103
|
+
}
|
|
104
|
+
function compactFile() {
|
|
105
|
+
if (!cache)
|
|
106
|
+
return;
|
|
107
|
+
const before = totalLinesOnDisk;
|
|
108
|
+
try {
|
|
109
|
+
ensureDir();
|
|
110
|
+
const tmpPath = REF_INDEX_FILE + ".tmp";
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (const [key, entry] of cache) {
|
|
113
|
+
const line = {
|
|
114
|
+
k: key,
|
|
115
|
+
v: {
|
|
116
|
+
content: entry.content,
|
|
117
|
+
senderId: entry.senderId,
|
|
118
|
+
senderName: entry.senderName,
|
|
119
|
+
timestamp: entry.timestamp,
|
|
120
|
+
isBot: entry.isBot,
|
|
121
|
+
attachments: entry.attachments,
|
|
122
|
+
},
|
|
123
|
+
t: entry._createdAt,
|
|
124
|
+
};
|
|
125
|
+
lines.push(JSON.stringify(line));
|
|
126
|
+
}
|
|
127
|
+
fs.writeFileSync(tmpPath, lines.join("\n") + "\n", "utf-8");
|
|
128
|
+
fs.renameSync(tmpPath, REF_INDEX_FILE);
|
|
129
|
+
totalLinesOnDisk = cache.size;
|
|
130
|
+
console.log(`[ref-index-store] Compacted: ${before} lines → ${totalLinesOnDisk} lines`);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.error(`[ref-index-store] Compact failed: ${err}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ============ 溢出淘汰 ============
|
|
137
|
+
function evictIfNeeded() {
|
|
138
|
+
if (!cache || cache.size < MAX_ENTRIES)
|
|
139
|
+
return;
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
// 第一轮:清理过期
|
|
142
|
+
for (const [key, entry] of cache) {
|
|
143
|
+
if (now - entry._createdAt > TTL_MS) {
|
|
144
|
+
cache.delete(key);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 第二轮:仍超限,按时间删最旧
|
|
148
|
+
if (cache.size >= MAX_ENTRIES) {
|
|
149
|
+
const sorted = [...cache.entries()].sort((a, b) => a[1]._createdAt - b[1]._createdAt);
|
|
150
|
+
const toRemove = sorted.slice(0, cache.size - MAX_ENTRIES + 1000);
|
|
151
|
+
for (const [key] of toRemove) {
|
|
152
|
+
cache.delete(key);
|
|
153
|
+
}
|
|
154
|
+
console.log(`[ref-index-store] Evicted ${toRemove.length} oldest entries`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ============ 公共 API ============
|
|
158
|
+
/**
|
|
159
|
+
* 存储一条消息的 refIdx 映射
|
|
160
|
+
*/
|
|
161
|
+
export function setRefIndex(refIdx, entry) {
|
|
162
|
+
const store = loadFromFile();
|
|
163
|
+
evictIfNeeded();
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
store.set(refIdx, {
|
|
166
|
+
content: entry.content,
|
|
167
|
+
senderId: entry.senderId,
|
|
168
|
+
senderName: entry.senderName,
|
|
169
|
+
timestamp: entry.timestamp,
|
|
170
|
+
isBot: entry.isBot,
|
|
171
|
+
attachments: entry.attachments,
|
|
172
|
+
_createdAt: now,
|
|
173
|
+
});
|
|
174
|
+
// 追加写入 JSONL
|
|
175
|
+
appendLine({
|
|
176
|
+
k: refIdx,
|
|
177
|
+
v: {
|
|
178
|
+
content: entry.content,
|
|
179
|
+
senderId: entry.senderId,
|
|
180
|
+
senderName: entry.senderName,
|
|
181
|
+
timestamp: entry.timestamp,
|
|
182
|
+
isBot: entry.isBot,
|
|
183
|
+
attachments: entry.attachments,
|
|
184
|
+
},
|
|
185
|
+
t: now,
|
|
186
|
+
});
|
|
187
|
+
// 检查是否需要 compact
|
|
188
|
+
if (shouldCompact()) {
|
|
189
|
+
compactFile();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 查找被引用消息
|
|
194
|
+
*/
|
|
195
|
+
export function getRefIndex(refIdx) {
|
|
196
|
+
const store = loadFromFile();
|
|
197
|
+
const entry = store.get(refIdx);
|
|
198
|
+
if (!entry)
|
|
199
|
+
return null;
|
|
200
|
+
// 检查过期
|
|
201
|
+
if (Date.now() - entry._createdAt > TTL_MS) {
|
|
202
|
+
store.delete(refIdx);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
content: entry.content,
|
|
207
|
+
senderId: entry.senderId,
|
|
208
|
+
senderName: entry.senderName,
|
|
209
|
+
timestamp: entry.timestamp,
|
|
210
|
+
isBot: entry.isBot,
|
|
211
|
+
attachments: entry.attachments,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 将引用消息内容格式化为人类可读的描述(供 AI 上下文注入)
|
|
216
|
+
*/
|
|
217
|
+
export function formatRefEntryForAgent(entry) {
|
|
218
|
+
const parts = [];
|
|
219
|
+
// 文本内容
|
|
220
|
+
if (entry.content.trim()) {
|
|
221
|
+
parts.push(entry.content);
|
|
222
|
+
}
|
|
223
|
+
// 附件描述
|
|
224
|
+
if (entry.attachments?.length) {
|
|
225
|
+
for (const att of entry.attachments) {
|
|
226
|
+
const sourceHint = att.localPath ? ` (${att.localPath})` : att.url ? ` (${att.url})` : "";
|
|
227
|
+
switch (att.type) {
|
|
228
|
+
case "image":
|
|
229
|
+
parts.push(`[图片${att.filename ? `: ${att.filename}` : ""}${sourceHint}]`);
|
|
230
|
+
break;
|
|
231
|
+
case "voice":
|
|
232
|
+
if (att.transcript) {
|
|
233
|
+
const sourceMap = { stt: "本地识别", asr: "官方识别", tts: "TTS原文", fallback: "兜底文案" };
|
|
234
|
+
const sourceTag = att.transcriptSource ? ` - ${sourceMap[att.transcriptSource] || att.transcriptSource}` : "";
|
|
235
|
+
parts.push(`[语音消息(内容: "${att.transcript}"${sourceTag})${sourceHint}]`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
parts.push(`[语音消息${sourceHint}]`);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case "video":
|
|
242
|
+
parts.push(`[视频${att.filename ? `: ${att.filename}` : ""}${sourceHint}]`);
|
|
243
|
+
break;
|
|
244
|
+
case "file":
|
|
245
|
+
parts.push(`[文件${att.filename ? `: ${att.filename}` : ""}${sourceHint}]`);
|
|
246
|
+
break;
|
|
247
|
+
default:
|
|
248
|
+
parts.push(`[附件${att.filename ? `: ${att.filename}` : ""}${sourceHint}]`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return parts.join(" ") || "[空消息]";
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 进程退出前强制 compact(确保数据一致性)
|
|
256
|
+
*/
|
|
257
|
+
export function flushRefIndex() {
|
|
258
|
+
if (cache && shouldCompact()) {
|
|
259
|
+
compactFile();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 缓存统计(调试用)
|
|
264
|
+
*/
|
|
265
|
+
export function getRefIndexStats() {
|
|
266
|
+
const store = loadFromFile();
|
|
267
|
+
return {
|
|
268
|
+
size: store.size,
|
|
269
|
+
maxEntries: MAX_ENTRIES,
|
|
270
|
+
totalLinesOnDisk,
|
|
271
|
+
filePath: REF_INDEX_FILE,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResolvedQQBotAccount } from "./types.js";
|
|
2
|
+
export interface MessageTarget {
|
|
3
|
+
type: "c2c" | "guild" | "dm" | "group";
|
|
4
|
+
senderId: string;
|
|
5
|
+
messageId: string;
|
|
6
|
+
channelId?: string;
|
|
7
|
+
groupOpenid?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ReplyContext {
|
|
10
|
+
target: MessageTarget;
|
|
11
|
+
account: ResolvedQQBotAccount;
|
|
12
|
+
cfg: unknown;
|
|
13
|
+
log?: {
|
|
14
|
+
info: (msg: string) => void;
|
|
15
|
+
error: (msg: string) => void;
|
|
16
|
+
debug?: (msg: string) => void;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 带 token 过期重试的消息发送
|
|
21
|
+
*/
|
|
22
|
+
export declare function sendWithTokenRetry<T>(appId: string, clientSecret: string, sendFn: (token: string) => Promise<T>, log?: ReplyContext["log"], accountId?: string): Promise<T>;
|
|
23
|
+
/**
|
|
24
|
+
* 根据消息类型路由发送文本
|
|
25
|
+
*/
|
|
26
|
+
export declare function sendTextToTarget(ctx: ReplyContext, text: string, refIdx?: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* 发送错误提示给用户
|
|
29
|
+
*/
|
|
30
|
+
export declare function sendErrorToTarget(ctx: ReplyContext, errorText: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* 处理结构化载荷(QQBOT_PAYLOAD: 前缀的 JSON)
|
|
33
|
+
* 返回 true 表示已处理,false 表示不是结构化载荷
|
|
34
|
+
*/
|
|
35
|
+
export declare function handleStructuredPayload(ctx: ReplyContext, replyText: string, recordActivity: () => void): Promise<boolean>;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getAccessToken, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, sendC2CImageMessage, sendGroupImageMessage, sendC2CVoiceMessage, sendGroupVoiceMessage, sendC2CVideoMessage, sendGroupVideoMessage, sendC2CFileMessage, sendGroupFileMessage } from "./api.js";
|
|
3
|
+
import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload } from "./utils/payload.js";
|
|
4
|
+
import { resolveTTSConfig, textToSilk, formatDuration } from "./utils/audio-convert.js";
|
|
5
|
+
import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize } from "./utils/file-utils.js";
|
|
6
|
+
import { getQQBotDataDir, normalizePath, sanitizeFileName } from "./utils/platform.js";
|
|
7
|
+
/**
|
|
8
|
+
* 带 token 过期重试的消息发送
|
|
9
|
+
*/
|
|
10
|
+
export async function sendWithTokenRetry(appId, clientSecret, sendFn, log, accountId) {
|
|
11
|
+
try {
|
|
12
|
+
const token = await getAccessToken(appId, clientSecret);
|
|
13
|
+
return await sendFn(token);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const errMsg = String(err);
|
|
17
|
+
if (errMsg.includes("401") || errMsg.includes("token") || errMsg.includes("access_token")) {
|
|
18
|
+
log?.info(`[qqbot:${accountId}] Token may be expired, refreshing...`);
|
|
19
|
+
clearTokenCache(appId);
|
|
20
|
+
const newToken = await getAccessToken(appId, clientSecret);
|
|
21
|
+
return await sendFn(newToken);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 根据消息类型路由发送文本
|
|
30
|
+
*/
|
|
31
|
+
export async function sendTextToTarget(ctx, text, refIdx) {
|
|
32
|
+
const { target, account } = ctx;
|
|
33
|
+
await sendWithTokenRetry(account.appId, account.clientSecret, async (token) => {
|
|
34
|
+
if (target.type === "c2c") {
|
|
35
|
+
await sendC2CMessage(token, target.senderId, text, target.messageId, refIdx);
|
|
36
|
+
}
|
|
37
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
38
|
+
await sendGroupMessage(token, target.groupOpenid, text, target.messageId);
|
|
39
|
+
}
|
|
40
|
+
else if (target.channelId) {
|
|
41
|
+
await sendChannelMessage(token, target.channelId, text, target.messageId);
|
|
42
|
+
}
|
|
43
|
+
else if (target.type === "dm") {
|
|
44
|
+
await sendC2CMessage(token, target.senderId, text, target.messageId, refIdx);
|
|
45
|
+
}
|
|
46
|
+
}, ctx.log, account.accountId);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 发送错误提示给用户
|
|
50
|
+
*/
|
|
51
|
+
export async function sendErrorToTarget(ctx, errorText) {
|
|
52
|
+
try {
|
|
53
|
+
await sendTextToTarget(ctx, errorText);
|
|
54
|
+
}
|
|
55
|
+
catch (sendErr) {
|
|
56
|
+
ctx.log?.error(`[qqbot:${ctx.account.accountId}] Failed to send error message: ${sendErr}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 处理结构化载荷(QQBOT_PAYLOAD: 前缀的 JSON)
|
|
61
|
+
* 返回 true 表示已处理,false 表示不是结构化载荷
|
|
62
|
+
*/
|
|
63
|
+
export async function handleStructuredPayload(ctx, replyText, recordActivity) {
|
|
64
|
+
const { target, account, cfg, log } = ctx;
|
|
65
|
+
const payloadResult = parseQQBotPayload(replyText);
|
|
66
|
+
if (!payloadResult.isPayload)
|
|
67
|
+
return false;
|
|
68
|
+
if (payloadResult.error) {
|
|
69
|
+
log?.error(`[qqbot:${account.accountId}] Payload parse error: ${payloadResult.error}`);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (!payloadResult.payload)
|
|
73
|
+
return true;
|
|
74
|
+
const parsedPayload = payloadResult.payload;
|
|
75
|
+
log?.info(`[qqbot:${account.accountId}] Detected structured payload, type: ${parsedPayload.type}`);
|
|
76
|
+
if (isCronReminderPayload(parsedPayload)) {
|
|
77
|
+
log?.info(`[qqbot:${account.accountId}] Processing cron_reminder payload`);
|
|
78
|
+
const cronMessage = encodePayloadForCron(parsedPayload);
|
|
79
|
+
const confirmText = `⏰ 提醒已设置,将在指定时间发送: "${parsedPayload.content}"`;
|
|
80
|
+
try {
|
|
81
|
+
await sendTextToTarget(ctx, confirmText);
|
|
82
|
+
log?.info(`[qqbot:${account.accountId}] Cron reminder confirmation sent, cronMessage: ${cronMessage}`);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send cron confirmation: ${err}`);
|
|
86
|
+
}
|
|
87
|
+
recordActivity();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (isMediaPayload(parsedPayload)) {
|
|
91
|
+
log?.info(`[qqbot:${account.accountId}] Processing media payload, mediaType: ${parsedPayload.mediaType}`);
|
|
92
|
+
if (parsedPayload.mediaType === "image") {
|
|
93
|
+
await handleImagePayload(ctx, parsedPayload);
|
|
94
|
+
}
|
|
95
|
+
else if (parsedPayload.mediaType === "audio") {
|
|
96
|
+
await handleAudioPayload(ctx, parsedPayload);
|
|
97
|
+
}
|
|
98
|
+
else if (parsedPayload.mediaType === "video") {
|
|
99
|
+
await handleVideoPayload(ctx, parsedPayload);
|
|
100
|
+
}
|
|
101
|
+
else if (parsedPayload.mediaType === "file") {
|
|
102
|
+
await handleFilePayload(ctx, parsedPayload);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
log?.error(`[qqbot:${account.accountId}] Unknown media type: ${parsedPayload.mediaType}`);
|
|
106
|
+
}
|
|
107
|
+
recordActivity();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${parsedPayload.type}`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// ============ 媒体载荷处理 ============
|
|
114
|
+
async function handleImagePayload(ctx, payload) {
|
|
115
|
+
const { target, account, log } = ctx;
|
|
116
|
+
let imageUrl = normalizePath(payload.path);
|
|
117
|
+
const originalImagePath = payload.source === "file" ? imageUrl : undefined;
|
|
118
|
+
if (payload.source === "file") {
|
|
119
|
+
try {
|
|
120
|
+
if (!(await fileExistsAsync(imageUrl))) {
|
|
121
|
+
log?.error(`[qqbot:${account.accountId}] Image not found: ${imageUrl}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const imgSzCheck = checkFileSize(imageUrl);
|
|
125
|
+
if (!imgSzCheck.ok) {
|
|
126
|
+
log?.error(`[qqbot:${account.accountId}] Image size check failed: ${imgSzCheck.error}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const fileBuffer = await readFileAsync(imageUrl);
|
|
130
|
+
const base64Data = fileBuffer.toString("base64");
|
|
131
|
+
const ext = path.extname(imageUrl).toLowerCase();
|
|
132
|
+
const mimeTypes = {
|
|
133
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
|
|
134
|
+
".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp",
|
|
135
|
+
};
|
|
136
|
+
const mimeType = mimeTypes[ext];
|
|
137
|
+
if (!mimeType) {
|
|
138
|
+
log?.error(`[qqbot:${account.accountId}] Unsupported image format: ${ext}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
imageUrl = `data:${mimeType};base64,${base64Data}`;
|
|
142
|
+
log?.info(`[qqbot:${account.accountId}] Converted local image to Base64 (size: ${formatFileSize(fileBuffer.length)})`);
|
|
143
|
+
}
|
|
144
|
+
catch (readErr) {
|
|
145
|
+
log?.error(`[qqbot:${account.accountId}] Failed to read local image: ${readErr}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await sendWithTokenRetry(account.appId, account.clientSecret, async (token) => {
|
|
151
|
+
if (target.type === "c2c") {
|
|
152
|
+
await sendC2CImageMessage(token, target.senderId, imageUrl, target.messageId, undefined, originalImagePath);
|
|
153
|
+
}
|
|
154
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
155
|
+
await sendGroupImageMessage(token, target.groupOpenid, imageUrl, target.messageId);
|
|
156
|
+
}
|
|
157
|
+
else if (target.channelId) {
|
|
158
|
+
await sendChannelMessage(token, target.channelId, ``, target.messageId);
|
|
159
|
+
}
|
|
160
|
+
}, log, account.accountId);
|
|
161
|
+
log?.info(`[qqbot:${account.accountId}] Sent image via media payload`);
|
|
162
|
+
if (payload.caption) {
|
|
163
|
+
await sendTextToTarget(ctx, payload.caption);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send image: ${err}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function handleAudioPayload(ctx, payload) {
|
|
171
|
+
const { target, account, cfg, log } = ctx;
|
|
172
|
+
try {
|
|
173
|
+
const ttsText = payload.caption || payload.path;
|
|
174
|
+
if (!ttsText?.trim()) {
|
|
175
|
+
log?.error(`[qqbot:${account.accountId}] Voice missing text`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const ttsCfg = resolveTTSConfig(cfg);
|
|
179
|
+
if (!ttsCfg) {
|
|
180
|
+
log?.error(`[qqbot:${account.accountId}] TTS not configured (channels.qqbot.tts in openclaw.json)`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
log?.info(`[qqbot:${account.accountId}] TTS: "${ttsText.slice(0, 50)}..." via ${ttsCfg.model}`);
|
|
184
|
+
const ttsDir = getQQBotDataDir("tts");
|
|
185
|
+
const { silkPath, silkBase64, duration } = await textToSilk(ttsText, ttsCfg, ttsDir);
|
|
186
|
+
log?.info(`[qqbot:${account.accountId}] TTS done: ${formatDuration(duration)}, file saved: ${silkPath}`);
|
|
187
|
+
await sendWithTokenRetry(account.appId, account.clientSecret, async (token) => {
|
|
188
|
+
if (target.type === "c2c") {
|
|
189
|
+
await sendC2CVoiceMessage(token, target.senderId, silkBase64, target.messageId, ttsText, silkPath);
|
|
190
|
+
}
|
|
191
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
192
|
+
await sendGroupVoiceMessage(token, target.groupOpenid, silkBase64, target.messageId);
|
|
193
|
+
}
|
|
194
|
+
else if (target.channelId) {
|
|
195
|
+
log?.error(`[qqbot:${account.accountId}] Voice not supported in channel, sending text fallback`);
|
|
196
|
+
await sendChannelMessage(token, target.channelId, ttsText, target.messageId);
|
|
197
|
+
}
|
|
198
|
+
}, log, account.accountId);
|
|
199
|
+
log?.info(`[qqbot:${account.accountId}] Voice message sent`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
log?.error(`[qqbot:${account.accountId}] TTS/voice send failed: ${err}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function handleVideoPayload(ctx, payload) {
|
|
208
|
+
const { target, account, log } = ctx;
|
|
209
|
+
try {
|
|
210
|
+
const videoPath = normalizePath(payload.path ?? "");
|
|
211
|
+
if (!videoPath?.trim()) {
|
|
212
|
+
log?.error(`[qqbot:${account.accountId}] Video missing path`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const isHttpUrl = videoPath.startsWith("http://") || videoPath.startsWith("https://");
|
|
216
|
+
log?.info(`[qqbot:${account.accountId}] Video send: "${videoPath.slice(0, 60)}..."`);
|
|
217
|
+
await sendWithTokenRetry(account.appId, account.clientSecret, async (token) => {
|
|
218
|
+
if (isHttpUrl) {
|
|
219
|
+
if (target.type === "c2c") {
|
|
220
|
+
await sendC2CVideoMessage(token, target.senderId, videoPath, undefined, target.messageId);
|
|
221
|
+
}
|
|
222
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
223
|
+
await sendGroupVideoMessage(token, target.groupOpenid, videoPath, undefined, target.messageId);
|
|
224
|
+
}
|
|
225
|
+
else if (target.channelId) {
|
|
226
|
+
log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
if (!(await fileExistsAsync(videoPath))) {
|
|
231
|
+
throw new Error(`视频文件不存在: ${videoPath}`);
|
|
232
|
+
}
|
|
233
|
+
const vPaySzCheck = checkFileSize(videoPath);
|
|
234
|
+
if (!vPaySzCheck.ok) {
|
|
235
|
+
throw new Error(vPaySzCheck.error);
|
|
236
|
+
}
|
|
237
|
+
const fileBuffer = await readFileAsync(videoPath);
|
|
238
|
+
const videoBase64 = fileBuffer.toString("base64");
|
|
239
|
+
log?.info(`[qqbot:${account.accountId}] Read local video (${formatFileSize(fileBuffer.length)}): ${videoPath}`);
|
|
240
|
+
if (target.type === "c2c") {
|
|
241
|
+
await sendC2CVideoMessage(token, target.senderId, undefined, videoBase64, target.messageId, undefined, videoPath);
|
|
242
|
+
}
|
|
243
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
244
|
+
await sendGroupVideoMessage(token, target.groupOpenid, undefined, videoBase64, target.messageId);
|
|
245
|
+
}
|
|
246
|
+
else if (target.channelId) {
|
|
247
|
+
log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}, log, account.accountId);
|
|
251
|
+
log?.info(`[qqbot:${account.accountId}] Video message sent`);
|
|
252
|
+
if (payload.caption) {
|
|
253
|
+
await sendTextToTarget(ctx, payload.caption);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
log?.error(`[qqbot:${account.accountId}] Video send failed: ${err}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function handleFilePayload(ctx, payload) {
|
|
262
|
+
const { target, account, log } = ctx;
|
|
263
|
+
try {
|
|
264
|
+
const filePath = normalizePath(payload.path ?? "");
|
|
265
|
+
if (!filePath?.trim()) {
|
|
266
|
+
log?.error(`[qqbot:${account.accountId}] File missing path`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const isHttpUrl = filePath.startsWith("http://") || filePath.startsWith("https://");
|
|
270
|
+
const fileName = sanitizeFileName(path.basename(filePath));
|
|
271
|
+
log?.info(`[qqbot:${account.accountId}] File send: "${filePath.slice(0, 60)}..." (${isHttpUrl ? "URL" : "local"})`);
|
|
272
|
+
await sendWithTokenRetry(account.appId, account.clientSecret, async (token) => {
|
|
273
|
+
if (isHttpUrl) {
|
|
274
|
+
if (target.type === "c2c") {
|
|
275
|
+
await sendC2CFileMessage(token, target.senderId, undefined, filePath, target.messageId, fileName);
|
|
276
|
+
}
|
|
277
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
278
|
+
await sendGroupFileMessage(token, target.groupOpenid, undefined, filePath, target.messageId, fileName);
|
|
279
|
+
}
|
|
280
|
+
else if (target.channelId) {
|
|
281
|
+
log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
if (!(await fileExistsAsync(filePath))) {
|
|
286
|
+
throw new Error(`文件不存在: ${filePath}`);
|
|
287
|
+
}
|
|
288
|
+
const fPaySzCheck = checkFileSize(filePath);
|
|
289
|
+
if (!fPaySzCheck.ok) {
|
|
290
|
+
throw new Error(fPaySzCheck.error);
|
|
291
|
+
}
|
|
292
|
+
const fileBuffer = await readFileAsync(filePath);
|
|
293
|
+
const fileBase64 = fileBuffer.toString("base64");
|
|
294
|
+
if (target.type === "c2c") {
|
|
295
|
+
await sendC2CFileMessage(token, target.senderId, fileBase64, undefined, target.messageId, fileName, filePath);
|
|
296
|
+
}
|
|
297
|
+
else if (target.type === "group" && target.groupOpenid) {
|
|
298
|
+
await sendGroupFileMessage(token, target.groupOpenid, fileBase64, undefined, target.messageId, fileName);
|
|
299
|
+
}
|
|
300
|
+
else if (target.channelId) {
|
|
301
|
+
log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}, log, account.accountId);
|
|
305
|
+
log?.info(`[qqbot:${account.accountId}] File message sent`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
log?.error(`[qqbot:${account.accountId}] File send failed: ${err}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 持久化存储
|
|
3
|
+
* 将 WebSocket 连接状态(sessionId、lastSeq)持久化到文件
|
|
4
|
+
* 支持进程重启后通过 Resume 机制快速恢复连接
|
|
5
|
+
*/
|
|
6
|
+
export interface SessionState {
|
|
7
|
+
/** WebSocket Session ID */
|
|
8
|
+
sessionId: string | null;
|
|
9
|
+
/** 最后收到的消息序号 */
|
|
10
|
+
lastSeq: number | null;
|
|
11
|
+
/** 上次连接成功的时间戳 */
|
|
12
|
+
lastConnectedAt: number;
|
|
13
|
+
/** 上次成功的权限级别索引 */
|
|
14
|
+
intentLevelIndex: number;
|
|
15
|
+
/** 关联的机器人账户 ID */
|
|
16
|
+
accountId: string;
|
|
17
|
+
/** 保存时间 */
|
|
18
|
+
savedAt: number;
|
|
19
|
+
/** 创建此 session 时使用的 appId(用于检测凭据变更) */
|
|
20
|
+
appId?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 加载 Session 状态
|
|
24
|
+
* @param accountId 账户 ID
|
|
25
|
+
* @param expectedAppId 当前使用的 appId,如果与保存时的 appId 不匹配则视为失效
|
|
26
|
+
* @returns Session 状态,如果不存在、已过期或 appId 不匹配返回 null
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadSession(accountId: string, expectedAppId?: string): SessionState | null;
|
|
29
|
+
/**
|
|
30
|
+
* 保存 Session 状态(带节流,避免频繁写入)
|
|
31
|
+
* @param state Session 状态
|
|
32
|
+
*/
|
|
33
|
+
export declare function saveSession(state: SessionState): void;
|
|
34
|
+
/**
|
|
35
|
+
* 清除 Session 状态
|
|
36
|
+
* @param accountId 账户 ID
|
|
37
|
+
*/
|
|
38
|
+
export declare function clearSession(accountId: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* 更新 lastSeq(轻量级更新)
|
|
41
|
+
* @param accountId 账户 ID
|
|
42
|
+
* @param lastSeq 最新的消息序号
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateLastSeq(accountId: string, lastSeq: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* 获取所有保存的 Session 状态
|
|
47
|
+
*/
|
|
48
|
+
export declare function getAllSessions(): SessionState[];
|
|
49
|
+
/**
|
|
50
|
+
* 清理过期的 Session 文件
|
|
51
|
+
*/
|
|
52
|
+
export declare function cleanupExpiredSessions(): number;
|