@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/index.ts +16 -0
  4. package/openclaw.plugin.json +58 -0
  5. package/package.json +73 -0
  6. package/proto/PbBoxPullProto.proto +43 -0
  7. package/proto/PbChatAudioContent.proto +23 -0
  8. package/proto/PbChatDeliverMsg.proto +38 -0
  9. package/proto/PbChatFileMeta.proto +34 -0
  10. package/proto/PbChatMsg.proto +93 -0
  11. package/proto/PbChatRichMediaContent.proto +31 -0
  12. package/proto/PbChatTextContent.proto +38 -0
  13. package/proto/PbMarkdownContent.proto +18 -0
  14. package/proto/PbMsgReadStampContent.proto +11 -0
  15. package/proto/PbPacket.proto +61 -0
  16. package/proto/PbSingleChatMsg.proto +60 -0
  17. package/setup-entry.ts +17 -0
  18. package/src/accounts.ts +109 -0
  19. package/src/api-client.ts +740 -0
  20. package/src/bot-info-cache.ts +49 -0
  21. package/src/channel.runtime.ts +29 -0
  22. package/src/channel.ts +456 -0
  23. package/src/config-schema.ts +26 -0
  24. package/src/e2ee/api.ts +261 -0
  25. package/src/e2ee/canonical.ts +59 -0
  26. package/src/e2ee/errors.ts +103 -0
  27. package/src/e2ee/index.ts +8 -0
  28. package/src/e2ee/proper-lockfile.d.ts +61 -0
  29. package/src/e2ee/service.ts +1273 -0
  30. package/src/e2ee/store.ts +174 -0
  31. package/src/e2ee/types.ts +113 -0
  32. package/src/e2ee/vodozemac.ts +373 -0
  33. package/src/file-transfer/api.ts +364 -0
  34. package/src/file-transfer/concurrency.ts +77 -0
  35. package/src/file-transfer/download.ts +261 -0
  36. package/src/file-transfer/file-crypto.ts +93 -0
  37. package/src/file-transfer/index.ts +18 -0
  38. package/src/file-transfer/scheduler.ts +185 -0
  39. package/src/file-transfer/types.ts +195 -0
  40. package/src/file-transfer/upload.ts +656 -0
  41. package/src/markdown-detect.ts +119 -0
  42. package/src/media-upload.ts +338 -0
  43. package/src/media-utils.ts +110 -0
  44. package/src/monitor.ts +838 -0
  45. package/src/proto/codec.ts +54 -0
  46. package/src/proto/inbound.codec.ts +624 -0
  47. package/src/proto/proto-types.ts +291 -0
  48. package/src/proto/registry.ts +226 -0
  49. package/src/proto/send.codec.ts +535 -0
  50. package/src/recent-message-cache.ts +350 -0
  51. package/src/send.ts +792 -0
  52. package/src/setup-core.ts +62 -0
  53. package/src/types.ts +153 -0
  54. package/src/vodozemackit/index.ts +297 -0
  55. package/src/vodozemackit/pkg/vodozemackit_wasm.d.ts +138 -0
  56. package/src/vodozemackit/pkg/vodozemackit_wasm.js +24 -0
  57. package/src/vodozemackit/pkg/vodozemackit_wasm_bg.js +1172 -0
  58. package/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm +0 -0
  59. package/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm.d.ts +109 -0
@@ -0,0 +1,535 @@
1
+ /**
2
+ * send.codec.ts — 出站消息编码 + 发送响应解码
3
+ *
4
+ * 使用 protobufjs 运行时动态加载 .proto 文件进行编解码,
5
+ * .proto 文件是唯一的结构定义来源(proto/ 目录)。
6
+ *
7
+ * 发送消息四层包装(CLIENT_REQ 方向,对应文档 §一):
8
+ *
9
+ * 第1层 PbPacket(type=CLIENT_REQ=0, request=PbRequest)
10
+ * ↑ PbPacket.proto
11
+ * 第2层 PbRequest(path="/api/im/chat/bot", reqId, reqStamp, body)
12
+ * ↑ PbPacket.proto
13
+ * 第3层 PbSingleChatMsgReqBody(fromId, toId, clientMsgId, envelopeType, e2eeFlag, envelope)
14
+ * ↑ PbSingleChatMsg.proto
15
+ * 第4层 PbChatMsgEnvelope / PbChatOperationEnvelope(由 builder 纯函数产出)
16
+ * ↑ PbChatMsg.proto + PbChatTextContent.proto 等
17
+ *
18
+ * 模块拆分:
19
+ * - buildChatEnvelopeBytes / buildMediaEnvelopeBytes / buildMarkdownEnvelopeBytes
20
+ * 为纯函数,只产出"第 4 层"envelope 的 bytes,可供 E2EE 加密前独立使用。
21
+ * - encodeSingleChatReqPacket 把任意 envelope bytes 包装为完整的
22
+ * PbSingleChatMsgReqBody → PbRequest → PbPacket 三层外壳。
23
+ * - encodeSendRequest / encodeMediaSendRequest / encodeMarkdownSendRequest /
24
+ * encodeTypingSignalRequest 保留原签名,实现内部委托给上述纯函数,字节输出保持不变。
25
+ *
26
+ * 发送响应解码:
27
+ * PbResponse → statusCode(200=成功) + body → PbMsgRspBody(serverMsgId)
28
+ */
29
+
30
+ import { randomBytes } from 'node:crypto';
31
+ import { getRegistry } from './registry.js';
32
+ import type { ImweSendResponse } from '../types.js';
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // 出站多媒体消息类型
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ /** 出站多媒体消息参数 */
39
+ export type OutboundMediaParams = {
40
+ /** 接收方 userId */
41
+ to: string;
42
+ /** 发送方 ID(机器人 botAcctId) */
43
+ fromId?: string;
44
+ /** 调用方显式指定的客户端消息 ID(可选) */
45
+ clientMsgId?: string;
46
+ /** 媒体文件 URL */
47
+ url: string;
48
+ /** 媒体类型 */
49
+ mediaType: 'image' | 'video' | 'audio' | 'file';
50
+ /** 文件名(可选) */
51
+ fileName?: string;
52
+ /** 文件大小(字节,可选) */
53
+ fileSize?: number;
54
+ /** MIME 类型(可选,如 image/jpeg) */
55
+ mimeType?: string;
56
+ /** 图片/视频宽度(可选) */
57
+ width?: number;
58
+ /** 图片/视频高度(可选) */
59
+ height?: number;
60
+ /** 文字描述/caption(可选) */
61
+ caption?: string;
62
+ /** 图片 blurhash(可选) */
63
+ blurHash?: string;
64
+ /** AES-256 文件加密密钥(32 字节,E2EE 文件传输时提供) */
65
+ fileKey?: Uint8Array;
66
+ /** AES-CTR 初始向量(16 字节,E2EE 文件传输时提供) */
67
+ fileIv?: Uint8Array;
68
+ /** SHA256(plaintext) 小写十六进制摘要(E2EE 文件传输时提供) */
69
+ fileDigest?: string;
70
+ /** 明文文件长度(字节,E2EE 文件传输时提供) */
71
+ plaintextLength?: number;
72
+ /** 上传会话过期时间(毫秒时间戳,E2EE 文件传输时提供) */
73
+ expireTime?: number;
74
+ /** 操作凭证(E2EE 文件传输时提供,batchCompleteChunks 返回) */
75
+ opCreds?: string;
76
+ /** 密文长度(字节,E2EE 文件传输时提供,AES-CTR 下等于 plaintextLength) */
77
+ length?: number;
78
+ };
79
+
80
+ /** mediaType → PbChatFileMetaType 枚举值映射 */
81
+ const MEDIA_TYPE_MAP: Record<OutboundMediaParams['mediaType'], number> = {
82
+ image: 1, // MEDIA_TYPE_IMAGE
83
+ video: 2, // MEDIA_TYPE_VIDEO
84
+ audio: 3, // MEDIA_TYPE_AUDIO
85
+ file: 4, // MEDIA_TYPE_FILE
86
+ };
87
+
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ // 出站 markdown 消息类型
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+
92
+ /** 出站 markdown 消息参数(与 OutboundMediaParams 风格对齐) */
93
+ export type OutboundMarkdownParams = {
94
+ /** 接收方 userId */
95
+ to: string;
96
+ /** 发送方 ID(机器人 botAcctId),为空时平台从 ApiKey 解析 */
97
+ fromId?: string;
98
+ /** 调用方显式指定的 clientMsgId(测试/重发时使用) */
99
+ clientMsgId?: string;
100
+ /** markdown 正文,非空 */
101
+ markdown: string;
102
+ /** 摘要标题(可选) */
103
+ digest?: string;
104
+ };
105
+
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+ // 内部工具
108
+ // ─────────────────────────────────────────────────────────────────────────────
109
+
110
+ /**
111
+ * 生成客户端消息 ID(UUID v4,32 字节十六进制,不带破折号)。
112
+ * 返回 32 个十六进制字符的字符串(不含 '-'),用于 clientMsgId 或 attachmentId。
113
+ * 使用随机字节并设置 UUID v4 的固定位以保证符合 UUID v4 的规范。
114
+ */
115
+ export function genClientMsgId(): string {
116
+ const b = randomBytes(16);
117
+ // 设置 UUID v4 固定位:版本号 4 和变体位
118
+ b[6] = (b[6] & 0x0f) | 0x40;
119
+ b[8] = (b[8] & 0x3f) | 0x80;
120
+ return b.toString('hex'); // 32 个十六进制字符,不含 '-'
121
+ }
122
+
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+ // 第 4 层:envelope bytes 构造器(纯函数,E2EE 加密前可独立使用)
125
+ // ─────────────────────────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * 构造文本聊天信封的 PbChatMsgEnvelope bytes(textContent 分支,oneof 字段 3)。
129
+ *
130
+ * 只产出"第 4 层" envelope bytes,不涉及 clientMsgId / 外层包装,
131
+ * 可供 E2EE `encryptSingle` 的明文输入或 `encodeSingleChatReqPacket` 进一步包装。
132
+ *
133
+ * @param text 文本正文
134
+ * @param options 可选项:referenceClientMsgId(引用消息 ID)
135
+ * @returns PbChatMsgEnvelope 编码后的 bytes
136
+ */
137
+ export async function buildChatEnvelopeBytes(
138
+ text: string,
139
+ options?: { referenceClientMsgId?: string },
140
+ ): Promise<Uint8Array> {
141
+ const reg = await getRegistry();
142
+
143
+ // 第 4 层内层:PbChatTextContent → bytes(含可选的引用消息 ID)
144
+ const textContentMsg = reg.PbChatTextContent.create({
145
+ text,
146
+ ...(options?.referenceClientMsgId
147
+ ? { referenceClientMsgId: options.referenceClientMsgId }
148
+ : {}),
149
+ });
150
+ const textContentBytes = reg.PbChatTextContent.encode(textContentMsg).finish();
151
+
152
+ // 第 4 层外层:PbChatMsgEnvelope(textContent 是 oneof 字段 3)
153
+ // 通过 decode 再传入保证 oneof 语义正确
154
+ const envelopeMsg = reg.PbChatMsgEnvelope.create({
155
+ textContent: reg.PbChatTextContent.decode(textContentBytes),
156
+ });
157
+ return reg.PbChatMsgEnvelope.encode(envelopeMsg).finish();
158
+ }
159
+
160
+ /**
161
+ * 构造多媒体聊天信封的 PbChatMsgEnvelope bytes(richMediaContent / fileContent / audioContent 分支)。
162
+ *
163
+ * 只产出"第 4 层" envelope bytes,不涉及 clientMsgId / 外层包装。
164
+ * 调用方(或重发场景)只需关心媒体元信息,不需要提前确定 clientMsgId。
165
+ *
166
+ * 注意:内部会独立生成 attachmentId(与 clientMsgId 无关),保持与历史行为一致。
167
+ */
168
+ export async function buildMediaEnvelopeBytes(params: OutboundMediaParams): Promise<Uint8Array> {
169
+ const reg = await getRegistry();
170
+ const attachmentId = genClientMsgId(); // 独立生成 attachmentId,保持历史行为
171
+
172
+ // 构造 PbChatFileMeta(含可选的 E2EE 加密字段)
173
+ const fileMeta = reg.PbChatFileMeta.create({
174
+ url: params.url,
175
+ fileType: params.mimeType ?? '',
176
+ mediaType: MEDIA_TYPE_MAP[params.mediaType],
177
+ attachmentId,
178
+ ...(params.fileKey ? { key: params.fileKey } : {}),
179
+ ...(params.fileIv ? { iv: params.fileIv } : {}),
180
+ ...(params.fileDigest ? { digest: params.fileDigest } : {}),
181
+ ...(params.plaintextLength != null ? { plaintextLength: params.plaintextLength } : {}),
182
+ ...(params.expireTime != null ? { expireTime: params.expireTime } : {}),
183
+ ...(params.opCreds ? { opCreds: params.opCreds } : {}),
184
+ ...(params.length != null ? { length: params.length } : {}),
185
+ });
186
+
187
+ let envelopeFields: Record<string, unknown>;
188
+
189
+ if (params.mediaType === 'audio') {
190
+ // 音频 → PbChatAudioContent → audioContent(oneof 字段 4)
191
+ const audioContent = reg.PbChatAudioContent.create({
192
+ meta: { attachmentId, duration: 0, fileMeta },
193
+ });
194
+ envelopeFields = { audioContent };
195
+ } else {
196
+ // 图片 / 视频 / 文件 → PbChatRichMediaContent
197
+ const richMedia = reg.PbChatRichMediaContent.create({
198
+ meta: [
199
+ {
200
+ attachmentId,
201
+ mediaType: MEDIA_TYPE_MAP[params.mediaType],
202
+ fileMeta,
203
+ ...(params.width != null ? { width: params.width } : {}),
204
+ ...(params.height != null ? { height: params.height } : {}),
205
+ ...(params.blurHash ? { blurHash: params.blurHash } : {}),
206
+ ...(params.fileName ? { fileName: params.fileName } : {}),
207
+ ...(params.fileSize != null ? { fileSize: params.fileSize } : {}),
208
+ ...(params.caption ? { caption: params.caption } : {}),
209
+ },
210
+ ],
211
+ ...(params.caption ? { caption: params.caption } : {}),
212
+ });
213
+
214
+ if (params.mediaType === 'file') {
215
+ envelopeFields = { fileContent: richMedia }; // oneof 字段 13
216
+ } else {
217
+ envelopeFields = { richMediaContent: richMedia }; // oneof 字段 9
218
+ }
219
+ }
220
+
221
+ const envelopeMsg = reg.PbChatMsgEnvelope.create(envelopeFields);
222
+ return reg.PbChatMsgEnvelope.encode(envelopeMsg).finish();
223
+ }
224
+
225
+ /**
226
+ * 构造 markdown 聊天信封的 PbChatMsgEnvelope bytes(markdownContent 分支,oneof 字段 16)。
227
+ *
228
+ * 只产出"第 4 层" envelope bytes。digest 缺省时写入 "" 以保证 proto3 默认值行为。
229
+ */
230
+ export async function buildMarkdownEnvelopeBytes(
231
+ params: Pick<OutboundMarkdownParams, 'markdown' | 'digest'>,
232
+ ): Promise<Uint8Array> {
233
+ const reg = await getRegistry();
234
+
235
+ // 第 4 层内层:PbMarkdownContent → bytes
236
+ const markdownMsg = reg.PbMarkdownContent.create({
237
+ digest: params.digest ?? '',
238
+ markdown: params.markdown,
239
+ });
240
+ const markdownBytes = reg.PbMarkdownContent.encode(markdownMsg).finish();
241
+
242
+ // 第 4 层外层:PbChatMsgEnvelope(markdownContent 是 oneof 字段 16)
243
+ const envelopeMsg = reg.PbChatMsgEnvelope.create({
244
+ markdownContent: reg.PbMarkdownContent.decode(markdownBytes),
245
+ });
246
+ return reg.PbChatMsgEnvelope.encode(envelopeMsg).finish();
247
+ }
248
+
249
+ /**
250
+ * 构造 decryptError 操作信令的 PbChatOperationEnvelope bytes(decryptError 分支,oneof 字段 5)。
251
+ *
252
+ * 由"解密失败方"发回给"原发送方"的反向信令,走 `envelopeType=2` 操作消息通道。
253
+ * 只产出"第 4 层" envelope bytes;后续需要通过 `encodeSingleChatReqPacket`
254
+ * (配合 E2EE 加密)包装成完整 PbPacket 再发送。
255
+ *
256
+ * 字段方向语义:
257
+ * - e2eeId = 信令发送方自己的 e2eeId(= 解密失败消息的接收者设备)
258
+ * - otherE2eeId = 失败消息的原发送方 e2eeId(= 信令接收方)
259
+ *
260
+ * @param content decryptError 信令三元组
261
+ * @returns PbChatOperationEnvelope 编码后的 bytes
262
+ */
263
+ export async function buildDecryptErrorEnvelopeBytes(content: {
264
+ clientMsgId: string;
265
+ e2eeId: string;
266
+ otherE2eeId: string;
267
+ }): Promise<Uint8Array> {
268
+ const reg = await getRegistry();
269
+
270
+ // 第 4 层内层:PbMessageDecryptErrorSendContent → bytes
271
+ const decryptErrorMsg = reg.PbMessageDecryptErrorSendContent.create({
272
+ clientMsgId: content.clientMsgId,
273
+ e2eeId: content.e2eeId,
274
+ otherE2eeId: content.otherE2eeId,
275
+ });
276
+ const decryptErrorBytes = reg.PbMessageDecryptErrorSendContent.encode(decryptErrorMsg).finish();
277
+
278
+ // 第 4 层外层:PbChatOperationEnvelope(decryptError) → bytes
279
+ // decryptError 是 oneof content 的字段 5,通过 decode 再传入保证 oneof 语义正确
280
+ const opEnvelopeMsg = reg.PbChatOperationEnvelope.create({
281
+ decryptError: reg.PbMessageDecryptErrorSendContent.decode(decryptErrorBytes),
282
+ });
283
+ return reg.PbChatOperationEnvelope.encode(opEnvelopeMsg).finish();
284
+ }
285
+
286
+ /**
287
+ * 构造已读操作信令的 PbChatOperationEnvelope bytes(msgRead 分支,oneof 字段 7)。
288
+ *
289
+ * 由插件在回复用户消息成功后发送,标记用户原始消息为已读。
290
+ * 走 `envelopeType=2` 操作消息通道,明文传输(`e2eeFlag=false`)。
291
+ */
292
+ export async function buildMsgReadEnvelopeBytes(params: {
293
+ targetClientMsgIds: string[];
294
+ readStamp: number;
295
+ }): Promise<Uint8Array> {
296
+ const reg = await getRegistry();
297
+
298
+ // 第 4 层内层:PbMsgReadStampSendContent → bytes
299
+ const msgReadMsg = reg.PbMsgReadStampSendContent.create({
300
+ clientMsgIds: params.targetClientMsgIds,
301
+ readStamp: params.readStamp,
302
+ });
303
+ const msgReadBytes = reg.PbMsgReadStampSendContent.encode(msgReadMsg).finish();
304
+
305
+ // 第 4 层外层:PbChatOperationEnvelope(msgRead) → bytes
306
+ // msgRead 是 oneof content 的字段 7,通过 decode 再传入保证 oneof 语义正确
307
+ const opEnvelopeMsg = reg.PbChatOperationEnvelope.create({
308
+ msgRead: reg.PbMsgReadStampSendContent.decode(msgReadBytes),
309
+ });
310
+ return reg.PbChatOperationEnvelope.encode(opEnvelopeMsg).finish();
311
+ }
312
+
313
+ // ─────────────────────────────────────────────────────────────────────────────
314
+ // 第 1~3 层:单聊请求包的通用装箱(E2EE / 明文共用入口)
315
+ // ─────────────────────────────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * 将任意 envelope bytes 包装为完整的 PbPacket(CLIENT_REQ) 二进制。
319
+ *
320
+ * 包装链路:
321
+ * envelopeBytes
322
+ * → PbSingleChatMsgReqBody(fromId, toId, clientMsgId, envelopeType, e2eeFlag, envelope)
323
+ * → PbRequest(path="/api/im/chat/bot", reqId=clientMsgId, body)
324
+ * → PbPacket(type=CLIENT_REQ=0, request)
325
+ *
326
+ * 适用场景:
327
+ * - 明文单聊消息:传入 `buildChatEnvelopeBytes` 产出,`e2eeFlag=false`。
328
+ * - E2EE 单聊消息:传入 `PbSingleChatMsgRecipients` bytes,`e2eeFlag=true` + `fromE2eeId`。
329
+ * - 操作信令:传入 `PbChatOperationEnvelope` bytes,`envelopeType=2`。
330
+ *
331
+ * @param args.envelopeBytes 第 4 层信封的 bytes(明文 envelope 或 E2EE recipients bytes)
332
+ * @param args.envelopeType 信封类型,1=聊天消息(默认),2=操作信令
333
+ * @param args.fromE2eeId 发送方 e2eeId;仅在 `e2eeFlag=true` 时需要
334
+ */
335
+ export async function encodeSingleChatReqPacket(args: {
336
+ fromId: string;
337
+ to: string;
338
+ clientMsgId: string;
339
+ envelopeBytes: Uint8Array;
340
+ e2eeFlag: boolean;
341
+ fromE2eeId?: string;
342
+ envelopeType?: 1 | 2;
343
+ }): Promise<Uint8Array> {
344
+ const reg = await getRegistry();
345
+ const clientStamp = Date.now();
346
+ const envelopeType = args.envelopeType ?? 1;
347
+
348
+ // 第 3 层:PbSingleChatMsgReqBody → bytes
349
+ const reqBodyMsg = reg.PbSingleChatMsgReqBody.create({
350
+ fromId: args.fromId,
351
+ toId: args.to,
352
+ clientMsgId: args.clientMsgId,
353
+ clientStamp,
354
+ envelopeType,
355
+ e2eeFlag: args.e2eeFlag,
356
+ ...(args.fromE2eeId ? { fromE2eeId: args.fromE2eeId } : {}),
357
+ envelope: args.envelopeBytes,
358
+ });
359
+ const reqBodyBytes = reg.PbSingleChatMsgReqBody.encode(reqBodyMsg).finish();
360
+
361
+ // 第 2 层:PbRequest → bytes(path 对应文档 §一 请求体结构)
362
+ const requestMsg = reg.PbRequest.create({
363
+ path: '/api/im/chat/bot',
364
+ reqId: args.clientMsgId,
365
+ reqStamp: clientStamp,
366
+ body: reqBodyBytes,
367
+ });
368
+ const requestBytes = reg.PbRequest.encode(requestMsg).finish();
369
+
370
+ // 第 1 层:PbPacket(type=CLIENT_REQ=0) → bytes
371
+ const packetMsg = reg.PbPacket.create({
372
+ type: 0, // CLIENT_REQ
373
+ request: reg.PbRequest.decode(requestBytes),
374
+ });
375
+ return reg.PbPacket.encode(packetMsg).finish();
376
+ }
377
+
378
+ // ─────────────────────────────────────────────────────────────────────────────
379
+ // 公开 API(保留既有签名,实现委托给纯函数)
380
+ // ─────────────────────────────────────────────────────────────────────────────
381
+
382
+ /**
383
+ * 将发送参数编码为 imwe 协议四层包装的 protobuf 二进制(明文路径)。
384
+ *
385
+ * 首次调用时会触发 .proto 文件加载(约几毫秒),后续调用使用缓存。
386
+ *
387
+ * @param to 接收方 userId
388
+ * @param text 消息文本内容
389
+ * @param fromId 发送方 ID(机器人的 botAcctId,来自 getMe 响应);为空时平台从 ApiKey 解析
390
+ * @returns 编码后的 protobuf 二进制,直接作为 HTTP 请求体发送
391
+ */
392
+ export async function encodeSendRequest(
393
+ to: string,
394
+ text: string,
395
+ fromId = '',
396
+ options?: { referenceClientMsgId?: string; clientMsgId?: string },
397
+ ): Promise<Uint8Array> {
398
+ const clientMsgId = options?.clientMsgId ?? genClientMsgId();
399
+ const envelopeBytes = await buildChatEnvelopeBytes(
400
+ text,
401
+ options?.referenceClientMsgId
402
+ ? { referenceClientMsgId: options.referenceClientMsgId }
403
+ : undefined,
404
+ );
405
+ return encodeSingleChatReqPacket({
406
+ fromId,
407
+ to,
408
+ clientMsgId,
409
+ envelopeBytes,
410
+ e2eeFlag: false,
411
+ });
412
+ }
413
+
414
+ /**
415
+ * 将多媒体消息参数编码为 imwe 协议四层包装的 protobuf 二进制(明文路径)。
416
+ */
417
+ export async function encodeMediaSendRequest(params: OutboundMediaParams): Promise<Uint8Array> {
418
+ const clientMsgId = params.clientMsgId ?? genClientMsgId();
419
+ const envelopeBytes = await buildMediaEnvelopeBytes(params);
420
+ return encodeSingleChatReqPacket({
421
+ fromId: params.fromId ?? '',
422
+ to: params.to,
423
+ clientMsgId,
424
+ envelopeBytes,
425
+ e2eeFlag: false,
426
+ });
427
+ }
428
+
429
+ /**
430
+ * 将 markdown 消息参数编码为 imwe 协议四层包装的 protobuf 二进制(明文路径)。
431
+ */
432
+ export async function encodeMarkdownSendRequest(
433
+ params: OutboundMarkdownParams,
434
+ ): Promise<Uint8Array> {
435
+ const clientMsgId = params.clientMsgId ?? genClientMsgId();
436
+ const envelopeBytes = await buildMarkdownEnvelopeBytes({
437
+ markdown: params.markdown,
438
+ ...(params.digest !== undefined ? { digest: params.digest } : {}),
439
+ });
440
+ return encodeSingleChatReqPacket({
441
+ fromId: params.fromId ?? '',
442
+ to: params.to,
443
+ clientMsgId,
444
+ envelopeBytes,
445
+ e2eeFlag: false,
446
+ });
447
+ }
448
+
449
+ /**
450
+ * 将 typing signal 参数编码为 imwe 协议四层包装的 protobuf 二进制。
451
+ *
452
+ * 包装链路(从内到外):
453
+ * PbBotThinkingSignal(status, botImAcctId)
454
+ * → PbChatOperationEnvelope(botThinking=...)
455
+ * → PbSingleChatMsgReqBody(envelopeType=2, envelope=...)
456
+ * → PbRequest(path="/api/im/chat/bot", body=...)
457
+ * → PbPacket(type=CLIENT_REQ=0, request=...)
458
+ *
459
+ * typing signal 走操作消息通道(envelopeType=2),按 R14 保持明文。
460
+ *
461
+ * @param to 接收方 userId
462
+ * @param status 1=begin(开始输入),2=end(结束输入)
463
+ * @param botImAcctId Bot 的 imAcctId,标识哪个 bot 正在输入
464
+ * @param fromId 发送方 ID(机器人的 botAcctId);为空时平台从 ApiKey 解析
465
+ * @returns 编码后的 protobuf 二进制,直接作为 HTTP 请求体发送
466
+ */
467
+ export async function encodeTypingSignalRequest(
468
+ to: string,
469
+ status: 1 | 2,
470
+ botImAcctId: string,
471
+ fromId = '',
472
+ ): Promise<Uint8Array> {
473
+ const reg = await getRegistry();
474
+ const clientMsgId = genClientMsgId();
475
+
476
+ // 第 4 层内层:PbBotThinkingSignal → bytes
477
+ const signalMsg = reg.PbBotThinkingSignal.create({ status, botImAcctId });
478
+ const signalBytes = reg.PbBotThinkingSignal.encode(signalMsg).finish();
479
+
480
+ // 第 4 层外层:PbChatOperationEnvelope(botThinking) → bytes
481
+ // botThinking 是 oneof content 的字段 9,通过 decode 再传入保证 oneof 语义正确
482
+ const opEnvelopeMsg = reg.PbChatOperationEnvelope.create({
483
+ botThinking: reg.PbBotThinkingSignal.decode(signalBytes),
484
+ });
485
+ const opEnvelopeBytes = reg.PbChatOperationEnvelope.encode(opEnvelopeMsg).finish();
486
+
487
+ return encodeSingleChatReqPacket({
488
+ fromId,
489
+ to,
490
+ clientMsgId,
491
+ envelopeBytes: opEnvelopeBytes,
492
+ e2eeFlag: false,
493
+ envelopeType: 2,
494
+ });
495
+ }
496
+
497
+ /**
498
+ * 将平台返回的发送响应解码为 ImweSendResponse。
499
+ *
500
+ * 响应体结构(对应文档 §一):
501
+ * 平台返回的 HTTP 响应体直接是 PbResponse 的 protobuf 二进制:
502
+ * PbResponse.statusCode 200=成功,其他=失败
503
+ * PbResponse.body PbMsgRspBody 的 protobuf 二进制(含 serverMsgId)
504
+ *
505
+ * 注意:响应体不包含外层 PbPacket 包装,直接从 PbResponse 开始解码。
506
+ *
507
+ * @param bytes POST /api/im/open/bot/sendMessage 响应体的原始字节(PbResponse 编码)
508
+ * @returns 解码后的发送结果
509
+ */
510
+ export async function decodeSendResponse(bytes: Uint8Array): Promise<ImweSendResponse> {
511
+ const reg = await getRegistry();
512
+
513
+ // 直接解码 PbResponse(响应体不含外层 PbPacket 包装)
514
+ const response = reg.PbResponse.decode(bytes) as unknown as {
515
+ statusCode: number | bigint;
516
+ body?: Uint8Array;
517
+ };
518
+
519
+ const statusCode = Number(response?.statusCode ?? 0);
520
+ const ok = statusCode === 200;
521
+
522
+ if (!ok || !response?.body?.length) {
523
+ return { ok };
524
+ }
525
+
526
+ // PbResponse.body → PbMsgRspBody(含 serverMsgId / serverStamp)
527
+ const rspBody = reg.PbMsgRspBody.decode(response.body) as unknown as {
528
+ serverMsgId?: string;
529
+ };
530
+
531
+ return {
532
+ ok,
533
+ ...(rspBody.serverMsgId ? { msgId: rspBody.serverMsgId } : {}),
534
+ };
535
+ }