@imweapp/openclaw-imwe 2026.4.12-alpha.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 +21 -0
- package/README.md +172 -0
- package/index.ts +16 -0
- package/openclaw.plugin.json +58 -0
- package/package.json +73 -0
- package/proto/PbBoxPullProto.proto +43 -0
- package/proto/PbChatAudioContent.proto +23 -0
- package/proto/PbChatDeliverMsg.proto +38 -0
- package/proto/PbChatFileMeta.proto +34 -0
- package/proto/PbChatMsg.proto +93 -0
- package/proto/PbChatRichMediaContent.proto +31 -0
- package/proto/PbChatTextContent.proto +38 -0
- package/proto/PbMarkdownContent.proto +18 -0
- package/proto/PbMsgReadStampContent.proto +11 -0
- package/proto/PbPacket.proto +61 -0
- package/proto/PbSingleChatMsg.proto +60 -0
- package/setup-entry.ts +17 -0
- package/src/accounts.ts +109 -0
- package/src/api-client.ts +740 -0
- package/src/bot-info-cache.ts +49 -0
- package/src/channel.runtime.ts +29 -0
- package/src/channel.ts +456 -0
- package/src/config-schema.ts +26 -0
- package/src/e2ee/api.ts +261 -0
- package/src/e2ee/canonical.ts +59 -0
- package/src/e2ee/errors.ts +103 -0
- package/src/e2ee/index.ts +8 -0
- package/src/e2ee/proper-lockfile.d.ts +61 -0
- package/src/e2ee/service.ts +1273 -0
- package/src/e2ee/store.ts +174 -0
- package/src/e2ee/types.ts +113 -0
- package/src/e2ee/vodozemac.ts +373 -0
- package/src/file-transfer/api.ts +364 -0
- package/src/file-transfer/concurrency.ts +77 -0
- package/src/file-transfer/download.ts +261 -0
- package/src/file-transfer/file-crypto.ts +93 -0
- package/src/file-transfer/index.ts +18 -0
- package/src/file-transfer/scheduler.ts +185 -0
- package/src/file-transfer/types.ts +195 -0
- package/src/file-transfer/upload.ts +656 -0
- package/src/markdown-detect.ts +119 -0
- package/src/media-upload.ts +338 -0
- package/src/media-utils.ts +110 -0
- package/src/monitor.ts +838 -0
- package/src/proto/codec.ts +54 -0
- package/src/proto/inbound.codec.ts +624 -0
- package/src/proto/proto-types.ts +291 -0
- package/src/proto/registry.ts +226 -0
- package/src/proto/send.codec.ts +535 -0
- package/src/recent-message-cache.ts +350 -0
- package/src/send.ts +792 -0
- package/src/setup-core.ts +62 -0
- package/src/types.ts +153 -0
- package/src/vodozemackit/index.ts +297 -0
- package/src/vodozemackit/pkg/vodozemackit_wasm.d.ts +138 -0
- package/src/vodozemackit/pkg/vodozemackit_wasm.js +24 -0
- package/src/vodozemackit/pkg/vodozemackit_wasm_bg.js +1172 -0
- package/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm +0 -0
- package/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm.d.ts +109 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-crypto.ts — AES-256-CTR 加解密模块
|
|
3
|
+
*
|
|
4
|
+
* 与 iOS FileCryptography 完全对齐的实现:
|
|
5
|
+
* - CTR 计数器规则:IV 前 12 字节固定,后 4 字节 big-endian 32-bit counter
|
|
6
|
+
* - counter = initialCounter + Math.floor(plaintextOffset / 16)
|
|
7
|
+
* - CTR 模式:输出长度 === 输入长度(无 padding)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { FileEncryptionKey } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 生成文件加密密钥对。
|
|
15
|
+
* - key: 32 字节随机(AES-256)
|
|
16
|
+
* - iv: 16 字节随机
|
|
17
|
+
*/
|
|
18
|
+
export function generateFileKey(): FileEncryptionKey {
|
|
19
|
+
return {
|
|
20
|
+
key: new Uint8Array(crypto.randomBytes(32)),
|
|
21
|
+
iv: new Uint8Array(crypto.randomBytes(16)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* AES-256-CTR XOR 操作(加密/解密共用,CTR 模式对称)。
|
|
27
|
+
* 支持从任意 offset 开始(分片 + 断点续传场景)。
|
|
28
|
+
*
|
|
29
|
+
* CTR 计数器规则(与 iOS 对齐):
|
|
30
|
+
* - IV 前 12 字节固定不变
|
|
31
|
+
* - IV 后 4 字节作为 big-endian 32-bit counter
|
|
32
|
+
* - blockIndex = Math.floor(plaintextOffset / 16)
|
|
33
|
+
* - counter = initialCounter + blockIndex
|
|
34
|
+
*
|
|
35
|
+
* @param data 输入数据(明文或密文)
|
|
36
|
+
* @param key AES-256 密钥(32 字节)
|
|
37
|
+
* @param iv 初始向量(16 字节)
|
|
38
|
+
* @param plaintextOffset 数据在完整文件中的字节偏移量
|
|
39
|
+
* @returns 输出数据(密文或明文)
|
|
40
|
+
*/
|
|
41
|
+
export function aesCtrXor(
|
|
42
|
+
data: Uint8Array,
|
|
43
|
+
key: Uint8Array,
|
|
44
|
+
iv: Uint8Array,
|
|
45
|
+
plaintextOffset: number,
|
|
46
|
+
): Uint8Array {
|
|
47
|
+
if (key.length !== 32) {
|
|
48
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
49
|
+
}
|
|
50
|
+
if (iv.length !== 16) {
|
|
51
|
+
throw new Error(`Invalid iv length: expected 16 bytes, got ${iv.length}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (data.length === 0) {
|
|
55
|
+
return new Uint8Array(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 计算起始 block 的 counter 值
|
|
59
|
+
const initialCounter = iv[12]! * 0x1000000 + iv[13]! * 0x10000 + iv[14]! * 0x100 + iv[15]!;
|
|
60
|
+
const blockIndex = Math.floor(plaintextOffset / 16);
|
|
61
|
+
const currentCounter = initialCounter + blockIndex;
|
|
62
|
+
|
|
63
|
+
// 构造调整后的 IV:前 12 字节不变,后 4 字节写入 currentCounter
|
|
64
|
+
const adjustedIv = Buffer.alloc(16);
|
|
65
|
+
iv.slice(0, 12).forEach((b, i) => (adjustedIv[i] = b));
|
|
66
|
+
adjustedIv.writeUInt32BE(currentCounter >>> 0, 12);
|
|
67
|
+
|
|
68
|
+
// 处理 offset 不在 block 边界的情况:需要跳过 block 内的前几个字节
|
|
69
|
+
const blockOffset = plaintextOffset % 16;
|
|
70
|
+
|
|
71
|
+
if (blockOffset === 0) {
|
|
72
|
+
// 对齐到 block 边界,直接加密
|
|
73
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', key, adjustedIv);
|
|
74
|
+
const result = cipher.update(data);
|
|
75
|
+
cipher.final(); // CTR 模式 final 不产出额外数据
|
|
76
|
+
return new Uint8Array(result);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 不在 block 边界:先填充 blockOffset 个零字节消耗 keystream,再处理实际数据
|
|
80
|
+
const padding = Buffer.alloc(blockOffset);
|
|
81
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', key, adjustedIv);
|
|
82
|
+
cipher.update(padding); // 消耗前 blockOffset 字节的 keystream
|
|
83
|
+
const result = cipher.update(data);
|
|
84
|
+
cipher.final();
|
|
85
|
+
return new Uint8Array(result);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 计算数据的 SHA256 摘要(小写十六进制)。
|
|
90
|
+
*/
|
|
91
|
+
export function sha256Hex(data: Uint8Array): string {
|
|
92
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
93
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-transfer/index.ts — 统一导出文件传输模块的公共接口
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { generateFileKey, aesCtrXor, sha256Hex } from './file-crypto.js';
|
|
6
|
+
export { uploadEncryptedFile } from './upload.js';
|
|
7
|
+
export { downloadAndDecryptFile } from './download.js';
|
|
8
|
+
export { runConcurrent } from './concurrency.js';
|
|
9
|
+
export { resumePendingUploads, resumePendingDownloads } from './scheduler.js';
|
|
10
|
+
export type {
|
|
11
|
+
FileEncryptionKey,
|
|
12
|
+
UploadResult,
|
|
13
|
+
UploadSendContext,
|
|
14
|
+
DownloadAndDecryptParams,
|
|
15
|
+
DownloadResult,
|
|
16
|
+
DownloadMessageContext,
|
|
17
|
+
FileTransferConfig,
|
|
18
|
+
} from './types.js';
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler.ts — 断点续传恢复调度器
|
|
3
|
+
*
|
|
4
|
+
* 在 gateway 启动时(startAccount)扫描 stateDir,
|
|
5
|
+
* 对未完成的上传/下载任务启动恢复,过期任务清理。
|
|
6
|
+
*
|
|
7
|
+
* 对齐 iOS 的 LargeAttachmentUploadScheduler:
|
|
8
|
+
* - 启动时扫描未完成任务并恢复
|
|
9
|
+
* - 异步执行(fire-and-forget),不阻塞轮询启动
|
|
10
|
+
* - 单个任务失败不阻断其他任务
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { UploadState, UploadResult, DownloadState } from './types.js';
|
|
16
|
+
import { uploadEncryptedFile } from './upload.js';
|
|
17
|
+
import { downloadAndDecryptFile, clearDownloadState } from './download.js';
|
|
18
|
+
|
|
19
|
+
// 过期阈值
|
|
20
|
+
const UPLOAD_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 小时
|
|
21
|
+
const DOWNLOAD_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 扫描并恢复未完成的上传任务。
|
|
25
|
+
* 异步执行不阻塞调用方。
|
|
26
|
+
*
|
|
27
|
+
* - 扫描 stateDir 中所有 upload-*.json
|
|
28
|
+
* - 超过 24 小时未更新的视为过期并清理
|
|
29
|
+
* - 未过期的启动恢复上传,完成后调用 onUploadComplete
|
|
30
|
+
* - 单个任务失败记录 WARN 日志,不阻断其他任务
|
|
31
|
+
*/
|
|
32
|
+
export async function resumePendingUploads(params: {
|
|
33
|
+
stateDir: string;
|
|
34
|
+
auth: { apiKey: string; apiSecret: string };
|
|
35
|
+
apiBaseUrl: string;
|
|
36
|
+
onUploadComplete: (state: UploadState, result: UploadResult) => Promise<void>;
|
|
37
|
+
log?: { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };
|
|
38
|
+
}): Promise<void> {
|
|
39
|
+
const { stateDir, auth, apiBaseUrl, onUploadComplete, log } = params;
|
|
40
|
+
|
|
41
|
+
if (!stateDir) return;
|
|
42
|
+
|
|
43
|
+
let files: string[];
|
|
44
|
+
try {
|
|
45
|
+
files = fs.readdirSync(stateDir).filter((f) => f.startsWith('upload-') && f.endsWith('.json'));
|
|
46
|
+
} catch {
|
|
47
|
+
// stateDir 不存在,无需恢复
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (files.length === 0) return;
|
|
52
|
+
|
|
53
|
+
log?.info?.(`[scheduler] 发现 ${files.length} 个待恢复上传任务`);
|
|
54
|
+
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const tasks: Promise<void>[] = [];
|
|
57
|
+
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const filePath = path.join(stateDir, file);
|
|
60
|
+
let state: UploadState;
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
63
|
+
state = JSON.parse(raw) as UploadState;
|
|
64
|
+
} catch {
|
|
65
|
+
log?.warn?.(`[scheduler] 读取上传状态文件失败: ${file}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 过期检查:超过 24 小时未更新则清理
|
|
70
|
+
if (now - state.updatedAt > UPLOAD_EXPIRY_MS) {
|
|
71
|
+
log?.info?.(`[scheduler] 上传状态已过期,清理: ${file}`);
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(filePath);
|
|
74
|
+
} catch {
|
|
75
|
+
// 清理失败忽略
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 启动恢复任务
|
|
81
|
+
const task = (async () => {
|
|
82
|
+
try {
|
|
83
|
+
const result = await uploadEncryptedFile(state.mediaUrl, auth, apiBaseUrl, {
|
|
84
|
+
imMainAccId: state.imMainAccId,
|
|
85
|
+
config: { stateDir },
|
|
86
|
+
sendContext: state.sendContext,
|
|
87
|
+
log,
|
|
88
|
+
});
|
|
89
|
+
await onUploadComplete(state, result);
|
|
90
|
+
log?.info?.(`[scheduler] 上传恢复完成: ${state.mediaUrl}`);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log?.warn?.(`[scheduler] 上传恢复失败: ${state.mediaUrl}, ${String(err)}`);
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
tasks.push(task);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 并行执行所有恢复任务
|
|
100
|
+
await Promise.allSettled(tasks);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 扫描并恢复未完成的下载任务。
|
|
105
|
+
* 异步执行不阻塞调用方。
|
|
106
|
+
*
|
|
107
|
+
* - 扫描 stateDir 中所有 *.partial.meta 文件
|
|
108
|
+
* - 超过 7 天未更新的视为过期并清理(含 .partial 文件)
|
|
109
|
+
* - 未过期的启动恢复下载,完成后调用 onDownloadComplete
|
|
110
|
+
* - 单个任务失败记录 WARN 日志,不阻断其他任务
|
|
111
|
+
*/
|
|
112
|
+
export async function resumePendingDownloads(params: {
|
|
113
|
+
stateDir: string;
|
|
114
|
+
auth: { apiKey: string; apiSecret: string };
|
|
115
|
+
apiBaseUrl: string;
|
|
116
|
+
onDownloadComplete: (state: DownloadState, localPath: string) => Promise<void>;
|
|
117
|
+
log?: { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };
|
|
118
|
+
}): Promise<void> {
|
|
119
|
+
const { stateDir, auth, apiBaseUrl, onDownloadComplete, log } = params;
|
|
120
|
+
|
|
121
|
+
if (!stateDir) return;
|
|
122
|
+
|
|
123
|
+
let files: string[];
|
|
124
|
+
try {
|
|
125
|
+
files = fs.readdirSync(stateDir).filter((f) => f.endsWith('.partial.meta'));
|
|
126
|
+
} catch {
|
|
127
|
+
// stateDir 不存在,无需恢复
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (files.length === 0) return;
|
|
132
|
+
|
|
133
|
+
log?.info?.(`[scheduler] 发现 ${files.length} 个待恢复下载任务`);
|
|
134
|
+
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const tasks: Promise<void>[] = [];
|
|
137
|
+
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const metaPath = path.join(stateDir, file);
|
|
140
|
+
let state: DownloadState;
|
|
141
|
+
try {
|
|
142
|
+
const raw = fs.readFileSync(metaPath, 'utf-8');
|
|
143
|
+
state = JSON.parse(raw) as DownloadState;
|
|
144
|
+
} catch {
|
|
145
|
+
log?.warn?.(`[scheduler] 读取下载状态文件失败: ${file}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 过期检查:超过 7 天未更新则清理
|
|
150
|
+
if (now - state.updatedAt > DOWNLOAD_EXPIRY_MS) {
|
|
151
|
+
log?.info?.(`[scheduler] 下载状态已过期,清理: ${file}`);
|
|
152
|
+
// savePath = metaPath 去掉 .partial.meta 后缀
|
|
153
|
+
const savePath = metaPath.replace(/\.partial\.meta$/, '');
|
|
154
|
+
clearDownloadState(savePath);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 需要 messageContext 才能恢复(否则无法分发)
|
|
159
|
+
if (!state.messageContext) {
|
|
160
|
+
log?.warn?.(`[scheduler] 下载状态缺少 messageContext,跳过: ${file}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 启动恢复任务
|
|
165
|
+
const task = (async () => {
|
|
166
|
+
try {
|
|
167
|
+
// savePath = metaPath 去掉 .partial.meta 后缀
|
|
168
|
+
const savePath = metaPath.replace(/\.partial\.meta$/, '');
|
|
169
|
+
|
|
170
|
+
// 恢复下载需要原始参数,从 state 中无法完全恢复(缺少 key/iv/digest 等)
|
|
171
|
+
// 下载恢复依赖调用方再次调用 downloadAndDecryptFile 时自动检测 .partial.meta
|
|
172
|
+
// scheduler 层面仅通知调用方有待恢复的任务
|
|
173
|
+
await onDownloadComplete(state, savePath);
|
|
174
|
+
log?.info?.(`[scheduler] 下载恢复通知完成: ${file}`);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
log?.warn?.(`[scheduler] 下载恢复失败: ${file}, ${String(err)}`);
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
|
|
180
|
+
tasks.push(task);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 并行执行所有恢复任务
|
|
184
|
+
await Promise.allSettled(tasks);
|
|
185
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — 文件传输模块的共享类型定义
|
|
3
|
+
*
|
|
4
|
+
* 定义加密密钥、分片计划、断点续传状态、上传/下载结果和配置等接口。
|
|
5
|
+
* 与 iOS 客户端的双层加密 + 分片传输架构对齐。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 文件加密密钥对 */
|
|
9
|
+
export interface FileEncryptionKey {
|
|
10
|
+
/** AES-256 密钥,32 字节 */
|
|
11
|
+
key: Uint8Array;
|
|
12
|
+
/** 初始向量,16 字节 */
|
|
13
|
+
iv: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 分片计划 */
|
|
17
|
+
export interface ChunkPlan {
|
|
18
|
+
/** 分片大小(字节),默认 2MB */
|
|
19
|
+
chunkSize: number;
|
|
20
|
+
/** 总分片数 */
|
|
21
|
+
totalChunks: number;
|
|
22
|
+
/** 文件总大小(字节) */
|
|
23
|
+
fileSize: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 单个分片的上传状态 */
|
|
27
|
+
export interface ChunkStatus {
|
|
28
|
+
/** 分片索引(0-based) */
|
|
29
|
+
index: number;
|
|
30
|
+
/** 是否已上传完成 */
|
|
31
|
+
completed: boolean;
|
|
32
|
+
/** 上传成功后的 ETag */
|
|
33
|
+
etag?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 消息发送上下文(断点续传恢复后需要) */
|
|
37
|
+
export interface UploadSendContext {
|
|
38
|
+
/** 接收方 userId */
|
|
39
|
+
to: string;
|
|
40
|
+
/** 发送方 botAcctId */
|
|
41
|
+
fromId: string;
|
|
42
|
+
/** 媒体类型 */
|
|
43
|
+
mediaType: 'image' | 'video' | 'audio' | 'file';
|
|
44
|
+
/** 文字说明 */
|
|
45
|
+
caption?: string;
|
|
46
|
+
/** 文件名 */
|
|
47
|
+
fileName?: string;
|
|
48
|
+
/** MIME 类型 */
|
|
49
|
+
mimeType?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 上传断点续传状态(持久化到 JSON 文件) */
|
|
53
|
+
export interface UploadState {
|
|
54
|
+
/** 服务端返回的 fileId */
|
|
55
|
+
fileId: string;
|
|
56
|
+
/** 原始文件名(用于 initMultipartUpload 的 fileName 参数) */
|
|
57
|
+
fileName: string;
|
|
58
|
+
/** 原始媒体 URL 或路径(断点续传恢复时用于重新定位文件) */
|
|
59
|
+
mediaUrl: string;
|
|
60
|
+
/** IM 主账号 ID(API 接口必填参数) */
|
|
61
|
+
imMainAccId: string;
|
|
62
|
+
/** 明文文件大小 */
|
|
63
|
+
plaintextLength: number;
|
|
64
|
+
/** 分片大小 */
|
|
65
|
+
chunkSize: number;
|
|
66
|
+
/** 总分片数 */
|
|
67
|
+
totalChunks: number;
|
|
68
|
+
/** AES-256 密钥(hex 编码,持久化用) */
|
|
69
|
+
keyHex: string;
|
|
70
|
+
/** AES-CTR IV(hex 编码,持久化用) */
|
|
71
|
+
ivHex: string;
|
|
72
|
+
/** 各分片状态 */
|
|
73
|
+
chunks: ChunkStatus[];
|
|
74
|
+
/** 消息发送上下文(断点续传恢复后需要,用于上传完成后自动发送消息) */
|
|
75
|
+
sendContext: UploadSendContext;
|
|
76
|
+
/** 上传会话过期时间(毫秒时间戳,initMultipartUpload 返回,断点续传恢复后仍需) */
|
|
77
|
+
expireTime?: number;
|
|
78
|
+
/** 创建时间戳 */
|
|
79
|
+
createdAt: number;
|
|
80
|
+
/** 最后更新时间戳 */
|
|
81
|
+
updatedAt: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 消息分发上下文(断点续传恢复后需要) */
|
|
85
|
+
export interface DownloadMessageContext {
|
|
86
|
+
/** 消息 ID */
|
|
87
|
+
msgId: string;
|
|
88
|
+
/** 发送者 ID */
|
|
89
|
+
senderId: string;
|
|
90
|
+
/** 消息内容(文本部分) */
|
|
91
|
+
content: string;
|
|
92
|
+
/** 消息时间戳 */
|
|
93
|
+
timestampMs: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 下载断点续传状态(持久化到 .partial.meta 文件) */
|
|
97
|
+
export interface DownloadState {
|
|
98
|
+
/** 远程文件 ETag(用于检测文件变更) */
|
|
99
|
+
etag: string;
|
|
100
|
+
/** 分片大小 */
|
|
101
|
+
chunkSize: number;
|
|
102
|
+
/** 总分片数 */
|
|
103
|
+
totalChunks: number;
|
|
104
|
+
/** 文件总大小(密文) */
|
|
105
|
+
fileSize: number;
|
|
106
|
+
/** 已下载完成的分片索引集合 */
|
|
107
|
+
downloadedChunks: number[];
|
|
108
|
+
/** 消息分发上下文(断点续传恢复后需要,用于下载完成后分发给 AI agent) */
|
|
109
|
+
messageContext?: DownloadMessageContext;
|
|
110
|
+
/** 创建时间戳 */
|
|
111
|
+
createdAt: number;
|
|
112
|
+
/** 最后更新时间戳 */
|
|
113
|
+
updatedAt: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 上传结果 */
|
|
117
|
+
export interface UploadResult {
|
|
118
|
+
/** 平台可访问的文件 URL(密文) */
|
|
119
|
+
url: string;
|
|
120
|
+
/** AES-256 加密密钥(32 字节) */
|
|
121
|
+
fileKey: Uint8Array;
|
|
122
|
+
/** AES-CTR 初始向量(16 字节) */
|
|
123
|
+
fileIv: Uint8Array;
|
|
124
|
+
/** SHA256(plaintext) 小写十六进制 */
|
|
125
|
+
fileDigest: string;
|
|
126
|
+
/** 明文长度(字节) */
|
|
127
|
+
plaintextLength: number;
|
|
128
|
+
/** 操作凭证(下载回调用) */
|
|
129
|
+
opCreds?: string;
|
|
130
|
+
/** 文件 MIME 类型 */
|
|
131
|
+
contentType?: string;
|
|
132
|
+
/** 图片 blurhash */
|
|
133
|
+
blurHash?: string;
|
|
134
|
+
/** 图片宽度 */
|
|
135
|
+
width?: number;
|
|
136
|
+
/** 图片高度 */
|
|
137
|
+
height?: number;
|
|
138
|
+
/** 上传会话过期时间(毫秒时间戳),用于填充 PbChatFileMeta.expireTime */
|
|
139
|
+
expireTime?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 下载并解密的参数 */
|
|
143
|
+
export interface DownloadAndDecryptParams {
|
|
144
|
+
/** 文件下载短链接 URL,格式:https://xxx/api/im/media/shortLink?shortCode=xxx */
|
|
145
|
+
url: string;
|
|
146
|
+
/** AES-256 密钥(32 字节) */
|
|
147
|
+
key: Uint8Array;
|
|
148
|
+
/** AES-CTR IV(16 字节) */
|
|
149
|
+
iv: Uint8Array;
|
|
150
|
+
/** 期望的 SHA256(plaintext) 摘要 */
|
|
151
|
+
digest: string;
|
|
152
|
+
/** 明文长度(字节) */
|
|
153
|
+
plaintextLength: number;
|
|
154
|
+
/** 操作凭证(下载完成回调用) */
|
|
155
|
+
opCreds?: string;
|
|
156
|
+
/** IM 主账号 ID(downloadCallback 必填) */
|
|
157
|
+
imMainAccId?: string;
|
|
158
|
+
/** 本地保存路径(不含扩展名,由调用方决定) */
|
|
159
|
+
savePath: string;
|
|
160
|
+
/** 并发数(默认 3) */
|
|
161
|
+
concurrency?: number;
|
|
162
|
+
/** 消息分发上下文(用于断点续传恢复后分发) */
|
|
163
|
+
messageContext?: DownloadMessageContext;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 下载结果 */
|
|
167
|
+
export interface DownloadResult {
|
|
168
|
+
/** 解密后的本地文件路径 */
|
|
169
|
+
localPath: string;
|
|
170
|
+
/** 验证通过的 digest */
|
|
171
|
+
digest: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 文件传输配置 */
|
|
175
|
+
export interface FileTransferConfig {
|
|
176
|
+
/** 分片大小(字节),默认 2MB */
|
|
177
|
+
chunkSize?: number;
|
|
178
|
+
/** 上传并发数,默认 3 */
|
|
179
|
+
uploadConcurrency?: number;
|
|
180
|
+
/** 下载并发数,默认 3 */
|
|
181
|
+
downloadConcurrency?: number;
|
|
182
|
+
/** 上传状态持久化目录(使用 botAcctId 作为路径标识,与 pullState 一致) */
|
|
183
|
+
stateDir?: string;
|
|
184
|
+
/** Stream 模式阈值(字节),默认 10MB,超过此大小使用 Stream 模式 */
|
|
185
|
+
streamThreshold?: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 默认配置 */
|
|
189
|
+
export const DEFAULT_CONFIG: Required<FileTransferConfig> = {
|
|
190
|
+
chunkSize: 2 * 1024 * 1024, // 2MB
|
|
191
|
+
uploadConcurrency: 3,
|
|
192
|
+
downloadConcurrency: 3,
|
|
193
|
+
stateDir: '',
|
|
194
|
+
streamThreshold: 10 * 1024 * 1024, // 10MB
|
|
195
|
+
};
|