@aalis/plugin-adapter-onebot 0.4.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ace Nyan <ace@acenyan.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @aalis/plugin-adapter-onebot
2
+
3
+ OneBot v11/v12 协议适配器(QQ 等平台接入,多连接 + 自动重连)
4
+ ## 角色
5
+
6
+ 运行时插件
7
+
8
+ ## 安装
9
+
10
+ ```bash
11
+ pnpm add @aalis/plugin-adapter-onebot
12
+ ```
13
+
14
+ ## 文档
15
+
16
+ 详见 [docs/plugins/plugin-adapter-onebot.md](../../docs/plugins/plugin-adapter-onebot.md)。
17
+
18
+ ## 许可
19
+
20
+ 见仓库根目录 LICENSE。
@@ -0,0 +1,24 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type { ProcessService } from '@aalis/plugin-process-api';
3
+ import type { StorageService } from '@aalis/plugin-storage-api';
4
+ type AttachmentKind = 'image' | 'audio' | 'video' | 'file';
5
+ /**
6
+ * 把 Buffer 落盘到 `data:/{kind}s/{session}/{hash}.{ext}`。
7
+ * 超过 maxBytes 时返回 null,由调用方决定降级策略。
8
+ * 返回值是 `data/{kind}s/{session}/{filename}` 这一历史相对路径,
9
+ * 用于写入 attachment ref;storage 落盘走 `data:/...` URI。
10
+ */
11
+ export declare function cacheAttachmentBuffer(storage: StorageService, buf: Buffer, kind: AttachmentKind, sessionId: string, ext: string, maxBytes: number): Promise<string | null>;
12
+ /**
13
+ * 从 URL / data: URI / file:// / storage URI 取到 Buffer。失败返回 null。
14
+ */
15
+ export declare function loadAttachmentBuffer(storage: StorageService, proc: ProcessService, source: string): Promise<Buffer | null>;
16
+ /** 简易 magic-header 探测,仅用于落盘扩展名选择与日志。 */
17
+ export declare function detectExtensionFromBuffer(buf: Buffer, fallback?: string): string;
18
+ /**
19
+ * 用 ffmpeg 把任意可解码音频 Buffer 转为 16kHz mono PCM WAV。
20
+ * 失败(典型:SILK,ffmpeg 不含 silk 解码器)返回 null。
21
+ */
22
+ export declare function transcodeAudioBufferToWav(proc: ProcessService, storage: StorageService, input: Buffer, inputExt: string): Promise<Buffer | null>;
23
+ export {};
24
+ //# sourceMappingURL=attachment-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-cache.d.ts","sourceRoot":"","sources":["../src/attachment-cache.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,KAAK,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAqB3D;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsBxB;AAED,wCAAwC;AACxC,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,MAAM,CAqB/E;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsBxB"}
@@ -0,0 +1,144 @@
1
+ // ============================================================
2
+ // attachment-cache.ts — 统一附件落盘(storage URI 版)
3
+ //
4
+ // 入站/出站的 image / audio / video / file 统一缓存到
5
+ // data:/{kind}s/{safeSessionId}/{sha256-16}.{ext}
6
+ //
7
+ // 设计要点:
8
+ // - 只接受已下载好的 Buffer,下载/get_record/ffmpeg 由调用方负责
9
+ // - 单文件 size cap 由调用方传入,超限直接返回 null
10
+ // - 文件名用 sha256(buffer) 前 16 字符做内容寻址,自然去重
11
+ // - 返回的 ref 仍是相对路径(如 `data/audios/.../xxx.wav`)以兼容历史 ref:
12
+ // 解析;存盘走 storage.writeFile('data:/...')
13
+ //
14
+ // loadAttachmentBuffer 遇到 file:// / OS 绝对路径时改走 ProcessService.readExternalFile,
15
+ // 避免 adapter 直接依赖 node:fs。原因:OneBot daemon 推来的路径可能不在任何 storage
16
+ // root 下(典型:NapCat 容器挂载的 /tmp)。“读外部任意路径”是 process 能力、
17
+ // 不应污染 storage 沙箱语义。
18
+ // ============================================================
19
+ import { Buffer } from 'node:buffer';
20
+ const KIND_DIR = {
21
+ image: 'images',
22
+ audio: 'audios',
23
+ video: 'videos',
24
+ file: 'files',
25
+ };
26
+ /** 把 sessionId 转为文件系统安全的名字(替换 `:`/`/`/`\\`)。 */
27
+ function safeSessionDir(sessionId) {
28
+ return sessionId.replace(/[:/\\]/g, '_');
29
+ }
30
+ /** 简易判定:形如 <root>:/<path>,且 root 非 http/https/data/file。 */
31
+ function isStorageUri(s) {
32
+ if (!/^[a-z][a-z0-9_-]*:\//.test(s))
33
+ return false;
34
+ const scheme = s.slice(0, s.indexOf(':')).toLowerCase();
35
+ return scheme !== 'http' && scheme !== 'https' && scheme !== 'data' && scheme !== 'file';
36
+ }
37
+ /**
38
+ * 把 Buffer 落盘到 `data:/{kind}s/{session}/{hash}.{ext}`。
39
+ * 超过 maxBytes 时返回 null,由调用方决定降级策略。
40
+ * 返回值是 `data/{kind}s/{session}/{filename}` 这一历史相对路径,
41
+ * 用于写入 attachment ref;storage 落盘走 `data:/...` URI。
42
+ */
43
+ export async function cacheAttachmentBuffer(storage, buf, kind, sessionId, ext, maxBytes) {
44
+ if (buf.byteLength > maxBytes)
45
+ return null;
46
+ const digest = await crypto.subtle.digest('SHA-256', buf);
47
+ const hash = Buffer.from(digest).toString('hex').slice(0, 16);
48
+ const dirRel = `${KIND_DIR[kind]}/${safeSessionDir(sessionId)}`;
49
+ const filename = `${hash}.${ext.replace(/^\.+/, '') || 'bin'}`;
50
+ const uri = `data:/${dirRel}/${filename}`;
51
+ await storage.writeFile(uri, buf);
52
+ return `data/${dirRel}/${filename}`;
53
+ }
54
+ /**
55
+ * 从 URL / data: URI / file:// / storage URI 取到 Buffer。失败返回 null。
56
+ */
57
+ export async function loadAttachmentBuffer(storage, proc, source) {
58
+ try {
59
+ if (source.startsWith('data:')) {
60
+ const m = source.match(/^data:[^;]+;base64,(.+)$/);
61
+ if (!m)
62
+ return null;
63
+ return Buffer.from(m[1], 'base64');
64
+ }
65
+ if (source.startsWith('http://') || source.startsWith('https://')) {
66
+ const res = await fetch(source, { signal: AbortSignal.timeout(30000) });
67
+ if (!res.ok)
68
+ return null;
69
+ return Buffer.from(await res.arrayBuffer());
70
+ }
71
+ if (isStorageUri(source)) {
72
+ const raw = (await storage.readFile(source));
73
+ return Buffer.from(raw);
74
+ }
75
+ // file:// 或绝对路径:OneBot daemon 推来的任意 OS 路径,走 process.readExternalFile
76
+ const raw = await proc.readExternalFile(source);
77
+ return Buffer.from(raw);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /** 简易 magic-header 探测,仅用于落盘扩展名选择与日志。 */
84
+ export function detectExtensionFromBuffer(buf, fallback = 'bin') {
85
+ if (buf.length < 12)
86
+ return fallback;
87
+ // image
88
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
89
+ return 'png';
90
+ if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
91
+ return 'jpg';
92
+ if (buf.subarray(0, 6).toString('ascii').startsWith('GIF8'))
93
+ return 'gif';
94
+ if (buf.subarray(0, 4).toString('ascii') === 'RIFF' && buf.subarray(8, 12).toString('ascii') === 'WEBP')
95
+ return 'webp';
96
+ // audio
97
+ if (buf.subarray(0, 4).toString('ascii') === 'RIFF' && buf.subarray(8, 12).toString('ascii') === 'WAVE')
98
+ return 'wav';
99
+ if (buf.subarray(0, 3).toString('ascii') === 'ID3')
100
+ return 'mp3';
101
+ if (buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0)
102
+ return 'mp3';
103
+ if (buf.subarray(0, 4).toString('ascii') === 'OggS')
104
+ return 'ogg';
105
+ if (buf.subarray(0, 4).toString('ascii') === 'fLaC')
106
+ return 'flac';
107
+ if (buf.subarray(0, 5).toString('ascii') === '#!AMR')
108
+ return 'amr';
109
+ const silkHead = buf[0] === 0x02 ? buf.subarray(1, 10) : buf.subarray(0, 9);
110
+ if (silkHead.toString('ascii') === '#!SILK_V3')
111
+ return 'silk';
112
+ // video / container
113
+ if (buf.subarray(4, 8).toString('ascii') === 'ftyp')
114
+ return 'mp4';
115
+ if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3)
116
+ return 'webm';
117
+ return fallback;
118
+ }
119
+ /**
120
+ * 用 ffmpeg 把任意可解码音频 Buffer 转为 16kHz mono PCM WAV。
121
+ * 失败(典型:SILK,ffmpeg 不含 silk 解码器)返回 null。
122
+ */
123
+ export async function transcodeAudioBufferToWav(proc, storage, input, inputExt) {
124
+ const tmp = await proc.makeTempDir('onebot-audio');
125
+ try {
126
+ const ext = inputExt.replace(/^\.+/, '') || 'bin';
127
+ const inUri = `${tmp.uri}/in.${ext}`;
128
+ const outUri = `${tmp.uri}/out.wav`;
129
+ const inPath = `${tmp.path}/in.${ext}`;
130
+ const outPath = `${tmp.path}/out.wav`;
131
+ await storage.writeFile(inUri, input);
132
+ await proc.execFile('ffmpeg', ['-i', inPath, '-vn', '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1', '-y', outPath], { timeout: 60000 });
133
+ const raw = (await storage.readFile(outUri));
134
+ const out = Buffer.from(raw);
135
+ return out.byteLength >= 256 ? out : null;
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ finally {
141
+ await tmp.cleanup();
142
+ }
143
+ }
144
+ //# sourceMappingURL=attachment-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-cache.js","sourceRoot":"","sources":["../src/attachment-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Logger } from '@aalis/core';
2
+ import type { MessageAttachment } from '@aalis/plugin-message-api';
3
+ import type { StorageService } from '@aalis/plugin-storage-api';
4
+ /**
5
+ * 把 attachments[] 渲染为可拼接到 content 的标记串。
6
+ * - image / audio:base64 内联(走 WS 隧道,Docker 部署最稳)→ `<image>` / `<record>`
7
+ * - video:通常过大,不内联,http/file URL 透传 → `<video>`
8
+ * - 其余 kind(file 等)暂未支持,debug 后跳过
9
+ * - 失败的附件 warn 后跳过
10
+ */
11
+ export declare function renderAttachmentsAsContentMarkers(attachments: MessageAttachment[] | undefined, storage: StorageService, logger?: Logger): Promise<string>;
12
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAqFhE;;;;;;GAMG;AACH,wBAAsB,iCAAiC,CACrD,WAAW,EAAE,iBAAiB,EAAE,GAAG,SAAS,EAC5C,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
@@ -0,0 +1,163 @@
1
+ // ============================================================
2
+ // attachments.ts — 把 OutgoingMessage.attachments 转为 OneBot 可发的字符串
3
+ //
4
+ // OneBot v11 image.file 字段支持三种 scheme:
5
+ // - http(s)://... OneBot 守护进程自行拉取
6
+ // - file:///abs/path OneBot 守护进程从本地文件系统读取
7
+ // - base64://<b64> 数据内嵌在消息里随 WS 隧道发送(适合 Docker 部署)
8
+ //
9
+ // 由于 NapCat / go-cqhttp 经常跑在 Docker / 远端机器,file:// 不一定可达。
10
+ // 默认策略:把 storage URI / data:/http 都转成 base64:// 让数据走 WS 隧道,
11
+ // 最稳。超过 MAX_INLINE_BYTES 的附件回退到原始 URL/file:// + warn。
12
+ //
13
+ // file:// 与本地绝对路径不再由本插件直接读取(避免依赖 node:fs),原样透传
14
+ // 给 daemon。生产侧 attachments 几乎都来自 plugin-media / plugin-image-sender
15
+ // 产出的 storage URI / data URI,故此回归仅在裸 file:// 用例下生效。
16
+ // ============================================================
17
+ import { Buffer } from 'node:buffer';
18
+ /** base64 内联上限(10 MiB)。超过则降级为 URL/file:// 并记 warn。 */
19
+ const MAX_INLINE_BYTES = 10 * 1024 * 1024;
20
+ /** 把 Buffer 包成 base64:// 字符串。 */
21
+ function toBase64Uri(buf) {
22
+ return `base64://${buf.toString('base64')}`;
23
+ }
24
+ /** 简易判定:形如 <root>:/<path>,且 root 非 http/https/file。
25
+ * 注意:"data:/path" 是 storage 根路径(storage URI),
26
+ * "data:image/png;base64,..." 是标准 data URI(非 storage)——
27
+ * 区分依据:storage URI 的冒号后紧跟 '/',标准 data URI 后跟 MIME type。 */
28
+ function isStorageUri(s) {
29
+ if (!/^[a-z][a-z0-9_-]*:\//.test(s))
30
+ return false;
31
+ const colonIdx = s.indexOf(':');
32
+ const scheme = s.slice(0, colonIdx).toLowerCase();
33
+ if (scheme === 'http' || scheme === 'https' || scheme === 'file')
34
+ return false;
35
+ // data:/ → storage root;data:mime/type;base64,... → 标准 data URI → 非 storage
36
+ if (scheme === 'data')
37
+ return s[colonIdx + 1] === '/';
38
+ return true;
39
+ }
40
+ /**
41
+ * 把附件物化为 OneBot `image.file` 可接受的字符串。
42
+ */
43
+ async function attachmentToOneBotFile(att, storage, logger) {
44
+ const data = att.data;
45
+ if (!data)
46
+ throw new Error('attachment.data is empty');
47
+ // data:image/...;base64,xxx → base64://xxx
48
+ // 注意:data[5] === '/' 时是 storage URI(data:/images/...),不是 data URI
49
+ if (data.startsWith('data:') && data[5] !== '/') {
50
+ const m = data.match(/^data:[^;]+;base64,(.+)$/);
51
+ if (!m)
52
+ throw new Error('invalid data URI');
53
+ const buf = Buffer.from(m[1], 'base64');
54
+ if (buf.byteLength > MAX_INLINE_BYTES) {
55
+ logger?.warn?.(`OneBot 附件超过 ${MAX_INLINE_BYTES} bytes,无法 base64 内联,已跳过`);
56
+ throw new Error('attachment too large for base64 inline');
57
+ }
58
+ return toBase64Uri(buf);
59
+ }
60
+ // http(s):// → 下载后 base64 内联
61
+ if (data.startsWith('http://') || data.startsWith('https://')) {
62
+ logger?.debug?.(`OneBot 下载远程附件: ${data.slice(0, 120)}`);
63
+ const res = await fetch(data);
64
+ if (!res.ok)
65
+ throw new Error(`download failed (${res.status}): ${data}`);
66
+ const ab = await res.arrayBuffer();
67
+ if (ab.byteLength > MAX_INLINE_BYTES) {
68
+ logger?.warn?.(`OneBot 远程附件 ${ab.byteLength}B 超过内联上限,回退到 URL(依赖 daemon 直拉)`);
69
+ return data;
70
+ }
71
+ return toBase64Uri(Buffer.from(ab));
72
+ }
73
+ // storage URI(如 data:/images/xxx)→ storage.readFile → base64
74
+ if (isStorageUri(data)) {
75
+ const raw = (await storage.readFile(data));
76
+ if (raw.byteLength > MAX_INLINE_BYTES) {
77
+ logger?.warn?.(`OneBot storage 附件 ${raw.byteLength}B 超过内联上限,尝试转 file:// 由 daemon 处理`);
78
+ try {
79
+ const local = await storage.resolveLocalPath?.(data, 'read');
80
+ if (local)
81
+ return `file://${local}`;
82
+ }
83
+ catch {
84
+ /* fall through */
85
+ }
86
+ throw new Error('attachment too large and not resolvable to local path');
87
+ }
88
+ return toBase64Uri(Buffer.from(raw));
89
+ }
90
+ // file:// 或裸路径:直接交给 daemon 处理(依赖 daemon 与文件系统共享)
91
+ if (data.startsWith('file://')) {
92
+ return data;
93
+ }
94
+ // 兜底:当作本地绝对路径,包成 file://
95
+ return `file://${data}`;
96
+ }
97
+ /**
98
+ * 把 attachments[] 渲染为可拼接到 content 的标记串。
99
+ * - image / audio:base64 内联(走 WS 隧道,Docker 部署最稳)→ `<image>` / `<record>`
100
+ * - video:通常过大,不内联,http/file URL 透传 → `<video>`
101
+ * - 其余 kind(file 等)暂未支持,debug 后跳过
102
+ * - 失败的附件 warn 后跳过
103
+ */
104
+ export async function renderAttachmentsAsContentMarkers(attachments, storage, logger) {
105
+ if (!attachments?.length)
106
+ return '';
107
+ const parts = [];
108
+ for (const att of attachments) {
109
+ if (att.kind !== 'image' && att.kind !== 'audio' && att.kind !== 'video') {
110
+ logger?.debug?.(`OneBot 跳过 ${att.kind} 附件(暂未支持该结构化发送)`);
111
+ continue;
112
+ }
113
+ if (att.kind === 'video') {
114
+ // 视频不做 base64 内联(文件通常过大)。
115
+ // http(s) URL 直接透传让 NapCat daemon 自行下载;file:// 原样传出(需 daemon 可访问)。
116
+ const data = att.data;
117
+ if (!data) {
118
+ logger?.warn?.('OneBot 跳过空 video 附件');
119
+ continue;
120
+ }
121
+ let videoUri;
122
+ if (data.startsWith('http://') || data.startsWith('https://') || data.startsWith('file://')) {
123
+ videoUri = data;
124
+ }
125
+ else if (isStorageUri(data)) {
126
+ try {
127
+ const local = await storage.resolveLocalPath?.(data, 'read');
128
+ videoUri = local ? `file://${local}` : data;
129
+ }
130
+ catch {
131
+ logger?.warn?.(`OneBot video storage URI 无法解析为本地路径: ${data.slice(0, 80)}`);
132
+ continue;
133
+ }
134
+ }
135
+ else {
136
+ videoUri = `file://${data}`;
137
+ }
138
+ parts.push(`<video url="${videoUri}"/>`);
139
+ continue;
140
+ }
141
+ if (att.kind === 'audio') {
142
+ // 语音文件通常较小,与 image 一致走 base64 内联。
143
+ try {
144
+ const uri = await attachmentToOneBotFile(att, storage, logger);
145
+ parts.push(`<record url="${uri}"/>`);
146
+ }
147
+ catch (err) {
148
+ logger?.warn?.(`OneBot 语音附件物化失败: ${err instanceof Error ? err.message : err}`);
149
+ }
150
+ continue;
151
+ }
152
+ // kind === 'image'
153
+ try {
154
+ const uri = await attachmentToOneBotFile(att, storage, logger);
155
+ parts.push(`<image url="${uri}"/>`);
156
+ }
157
+ catch (err) {
158
+ logger?.warn?.(`OneBot 附件物化失败: ${err instanceof Error ? err.message : err}`);
159
+ }
160
+ }
161
+ return parts.join('');
162
+ }
163
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { Context } from '@aalis/core';
2
+ import type { OneBotMessageSegment } from './types.js';
3
+ /**
4
+ * 合并转发摘要默认 system prompt。
5
+ *
6
+ * 设计重点:明确要求保留多人互动结构(谁说了什么 / 态度 / 回应关系),
7
+ * 避免出现 “Alice 表达了对…” 这种压成一句、丢掉互动的退化摘要。
8
+ *
9
+ * 用户可通过配置 `forward.summaryPrompt` 完全覆盖本默认值。
10
+ */
11
+ export declare const DEFAULT_FORWARD_SUMMARY_PROMPT: string;
12
+ /**
13
+ * 合并转发原文缓存条目:完整原文 + 摘要 + 元信息。
14
+ */
15
+ export interface ForwardEntry {
16
+ fullText: string;
17
+ summary: string | null;
18
+ count: number;
19
+ participants: string[];
20
+ expandedAt: number;
21
+ }
22
+ export interface ForwardConfig {
23
+ enabled: boolean;
24
+ maxDepth: number;
25
+ maxNodesPerLevel: number;
26
+ imageRecognition: boolean;
27
+ /** 同一条合并转发内图片识别并发上限。默认 8。 */
28
+ imageRecognitionConcurrency: number;
29
+ summarize: boolean;
30
+ summaryLLM?: {
31
+ provider: string;
32
+ model: string;
33
+ };
34
+ summaryMaxChars: number;
35
+ /** 喂给摘要 LLM 的原文最大字符数。原文超过则前段截断;<=0 表示不截断 */
36
+ summaryInputLimit: number /**
37
+ * 摘要使用的 system prompt。为空字符串 / undefined 时使用 `DEFAULT_FORWARD_SUMMARY_PROMPT`。
38
+ * 传入非空字符串则完全覆盖默认 prompt。
39
+ */;
40
+ summaryPrompt?: string;
41
+ }
42
+ export interface ForwardExpanderDeps<TState> {
43
+ ctx: Context;
44
+ forwardCfg: ForwardConfig;
45
+ /** 调用 OneBot action 的回调(已绑定到具体的连接状态) */
46
+ sendAction: (state: TState, action: string, params: Record<string, unknown>) => Promise<unknown>;
47
+ }
48
+ export interface ForwardExpander<TState> {
49
+ getCachedForward(id: string): ForwardEntry | undefined;
50
+ setCachedForward(id: string, entry: ForwardEntry): void;
51
+ loadPersistedForward(id: string): Promise<ForwardEntry | undefined>;
52
+ fetchForwardOnce(state: TState, id: string): Promise<unknown | null>;
53
+ expandForwardsInText(state: TState, text: string, rawSegments: OneBotMessageSegment[] | undefined): Promise<string>;
54
+ }
55
+ /**
56
+ * 合并转发展开器工厂。
57
+ *
58
+ * 收到一条带 forward 段的消息时,立即递归拉取原文、做图像识别、生成摘要,
59
+ * 并把完整原文写入此缓存(也会同步到 MemoryService.saveMetadata 做持久化),
60
+ * 这样:
61
+ * 1) LLM 在对话上下文里看到的是"信封 + 摘要",不被超长原文淹没;
62
+ * 2) 想看细节时调 onebot_get_forward_msg 工具直接命中缓存/持久化层;
63
+ * 3) 摘要会随 inbound:message 进入历史归档与向量库,被语义召回。
64
+ *
65
+ * 内存缓存 1h TTL;持久化由 memory metadata 兜底(如果实现支持)。
66
+ */
67
+ export declare function createForwardExpander<TState>(deps: ForwardExpanderDeps<TState>): ForwardExpander<TState>;
68
+ //# sourceMappingURL=forward-expand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forward-expand.d.ts","sourceRoot":"","sources":["../src/forward-expand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAK3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,QAOF,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,6BAA6B;IAC7B,2BAA2B,EAAE,MAAM,CAAC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,iBAAiB,EAAE,MAAM,CAAC;;;OAGvB,CAAC;IACJ,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB,CAAC,MAAM;IACzC,GAAG,EAAE,OAAO,CAAC;IACb,UAAU,EAAE,aAAa,CAAC;IAC1B,wCAAwC;IACxC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAClG;AAED,MAAM,WAAW,eAAe,CAAC,MAAM;IACrC,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACvD,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACxD,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IACpE,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACrE,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,oBAAoB,EAAE,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrH;AA0CD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,CAgPxG"}