@imweapp/openclaw-imwe 2026.4.12-alpha.4 → 2026.4.12-alpha.5
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/dist/index.js +14 -0
- package/dist/setup-entry.js +15 -0
- package/dist/src/accounts.js +95 -0
- package/dist/src/api-client.js +498 -0
- package/dist/src/bot-info-cache.js +43 -0
- package/dist/src/channel.js +384 -0
- package/dist/src/channel.runtime.js +15 -0
- package/dist/src/config-schema.js +20 -0
- package/dist/src/e2ee/api.js +137 -0
- package/dist/src/e2ee/canonical.js +56 -0
- package/dist/src/e2ee/errors.js +82 -0
- package/dist/src/e2ee/index.js +5 -0
- package/dist/src/e2ee/service.js +897 -0
- package/dist/src/e2ee/store.js +135 -0
- package/dist/src/e2ee/types.js +89 -0
- package/dist/src/e2ee/vodozemac.js +202 -0
- package/dist/src/emote/api.js +9 -0
- package/dist/src/emote/handle.js +16 -0
- package/dist/src/emote/index.js +2 -0
- package/dist/src/file-transfer/api.js +198 -0
- package/dist/src/file-transfer/concurrency.js +53 -0
- package/dist/src/file-transfer/download.js +226 -0
- package/dist/src/file-transfer/file-crypto.js +77 -0
- package/dist/src/file-transfer/index.js +8 -0
- package/dist/src/file-transfer/scheduler.js +157 -0
- package/dist/src/file-transfer/types.js +14 -0
- package/dist/src/file-transfer/upload.js +493 -0
- package/dist/src/markdown-detect.js +113 -0
- package/dist/src/media-upload.js +238 -0
- package/dist/src/media-utils.js +103 -0
- package/dist/src/monitor.js +661 -0
- package/dist/src/proto/codec.js +26 -0
- package/dist/src/proto/inbound.codec.js +552 -0
- package/dist/src/proto/proto-types.js +20 -0
- package/dist/src/proto/registry.js +142 -0
- package/dist/src/proto/send.codec.js +390 -0
- package/dist/src/recent-message-cache.js +308 -0
- package/dist/src/secret-input.js +1 -0
- package/dist/src/send.js +575 -0
- package/dist/src/setup-core.js +54 -0
- package/dist/src/types.js +14 -0
- package/dist/src/vodozemackit/index.js +197 -0
- package/dist/src/vodozemackit/pkg/vodozemackit_wasm.js +24 -0
- package/dist/src/vodozemackit/pkg/vodozemackit_wasm_bg.js +1172 -0
- package/dist/src/vodozemackit/pkg/vodozemackit_wasm_bg.wasm +0 -0
- package/openclaw.plugin.json +14 -1
- package/package.json +16 -5
- package/src/api-client.ts +2 -2
- package/src/channel.ts +4 -6
- package/src/e2ee/service.ts +3 -3
- package/src/e2ee/store.ts +1 -1
- package/src/emote/handle.ts +1 -1
- package/src/file-transfer/scheduler.ts +2 -2
- package/src/file-transfer/upload.ts +4 -4
- package/src/media-upload.ts +2 -3
- package/src/proto/codec.ts +1 -1
- package/src/proto/definitions/PbBoxPullProto.proto +43 -0
- package/src/proto/definitions/PbChatAudioContent.proto +23 -0
- package/src/proto/definitions/PbChatDeliverMsg.proto +38 -0
- package/src/proto/definitions/PbChatEmoteContent.proto +34 -0
- package/src/proto/definitions/PbChatFileMeta.proto +34 -0
- package/src/proto/definitions/PbChatLocationContent.proto +15 -0
- package/src/proto/definitions/PbChatMsg.proto +95 -0
- package/src/proto/definitions/PbChatRichMediaContent.proto +31 -0
- package/src/proto/definitions/PbChatTextContent.proto +38 -0
- package/src/proto/definitions/PbMarkdownContent.proto +18 -0
- package/src/proto/definitions/PbMsgReadStampContent.proto +11 -0
- package/src/proto/definitions/PbPacket.proto +61 -0
- package/src/proto/definitions/PbSingleChatMsg.proto +60 -0
- package/src/proto/inbound.codec.ts +1 -1
- package/src/proto/registry.ts +5 -3
- package/src/proto/send.codec.ts +1 -1
- package/src/send.ts +3 -3
- package/src/types.ts +1 -1
- package/src/vodozemackit/index.ts +1 -1
- /package/{proto → dist/src/proto/definitions}/PbBoxPullProto.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatAudioContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatDeliverMsg.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatEmoteContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatFileMeta.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatLocationContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatMsg.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatRichMediaContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbChatTextContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbMarkdownContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbMsgReadStampContent.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbPacket.proto +0 -0
- /package/{proto → dist/src/proto/definitions}/PbSingleChatMsg.proto +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — 插件主入口(full 模式)
|
|
3
|
+
*
|
|
4
|
+
* 使用 defineChannelPluginEntry 定义入口。
|
|
5
|
+
* AppKey/AppSecret 签名认证无需 runtime 注入,故省略 setRuntime。
|
|
6
|
+
*/
|
|
7
|
+
import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/channel-core';
|
|
8
|
+
import { imwePlugin } from './src/channel.js';
|
|
9
|
+
export default defineChannelPluginEntry({
|
|
10
|
+
id: 'imwe',
|
|
11
|
+
name: 'imwe',
|
|
12
|
+
description: 'imwe 即时通讯渠道插件,AppKey/AppSecret 签名认证,支持多账号',
|
|
13
|
+
plugin: imwePlugin,
|
|
14
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup-entry.ts — 轻量入口(setup-only 模式)
|
|
3
|
+
*
|
|
4
|
+
* 当渠道未配置或被禁用时,core 加载这个文件而不是 index.ts。
|
|
5
|
+
* defineSetupPluginEntry 只暴露 ChannelPlugin 对象(用于 setup wizard),
|
|
6
|
+
* 不注入 runtime,不注册 CLI,不加载任何运行时代码。
|
|
7
|
+
*
|
|
8
|
+
* 这样做的好处:
|
|
9
|
+
* - 节省内存:不加载 monitor.ts、api-client.ts 等重量级模块
|
|
10
|
+
* - 加快启动:setup 流程不需要建立网络连接
|
|
11
|
+
* - 安全隔离:未配置的渠道不会尝试连接平台
|
|
12
|
+
*/
|
|
13
|
+
import { defineSetupPluginEntry } from 'openclaw/plugin-sdk/channel-core';
|
|
14
|
+
import { imwePlugin } from './src/channel.js';
|
|
15
|
+
export default defineSetupPluginEntry(imwePlugin);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* accounts.ts — 账号解析逻辑
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 从 OpenClawConfig 中读取 imwe 渠道配置
|
|
6
|
+
* - 合并顶层默认值与账号级配置(多账号继承)
|
|
7
|
+
* - 解析 appKey / appSecret(优先级:账号级 config > 顶层 config > 环境变量)
|
|
8
|
+
* - 导出 listImweAccountIds / resolveImweAccount 供 ChannelConfigAdapter 使用
|
|
9
|
+
*/
|
|
10
|
+
import { createAccountListHelpers, resolveMergedAccountConfig, } from 'openclaw/plugin-sdk/account-helpers';
|
|
11
|
+
import { normalizeAccountId } from 'openclaw/plugin-sdk/account-id';
|
|
12
|
+
import { coerceSecretRef } from 'openclaw/plugin-sdk/provider-auth';
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// 账号 id 列表工具
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
const { listAccountIds: listImweAccountIds, resolveDefaultAccountId: resolveDefaultImweAccountId } = createAccountListHelpers('imwe');
|
|
17
|
+
export { listImweAccountIds, resolveDefaultImweAccountId };
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// 配置合并
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
function mergeImweAccountConfig(cfg, accountId) {
|
|
22
|
+
return resolveMergedAccountConfig({
|
|
23
|
+
channelConfig: cfg.channels?.imwe,
|
|
24
|
+
accounts: cfg.channels?.imwe?.accounts,
|
|
25
|
+
accountId,
|
|
26
|
+
omitKeys: ['defaultAccount'],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// SecretRef 解析 helper
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
/** 将纯字符串或 SecretRef 对象解析为字符串值。env 类型 ref 从 process.env 读取。 */
|
|
33
|
+
function resolveSecretLike(value) {
|
|
34
|
+
if (value != null && typeof value === 'string') {
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
if (trimmed)
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
const ref = coerceSecretRef(value);
|
|
40
|
+
if (!ref)
|
|
41
|
+
return undefined;
|
|
42
|
+
// env 类型 SecretRef 从 process.env 读取
|
|
43
|
+
if (ref.source === 'env') {
|
|
44
|
+
return process.env[ref.id]?.trim() || undefined;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// 凭证解析:优先级 账号级 config > 顶层 config > 环境变量
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
function resolveImweCredentials(merged, accountId) {
|
|
52
|
+
// 1. config 里的 appKey + appSecret(支持 SecretRef)
|
|
53
|
+
const configKey = resolveSecretLike(merged.appKey);
|
|
54
|
+
const configSecret = resolveSecretLike(merged.appSecret);
|
|
55
|
+
if (configKey && configSecret) {
|
|
56
|
+
return { appKey: configKey, appSecret: configSecret, source: 'config' };
|
|
57
|
+
}
|
|
58
|
+
// 2. 环境变量(仅 default 账号,避免多账号混用同一组 env)
|
|
59
|
+
if (normalizeAccountId(accountId) === 'default') {
|
|
60
|
+
const envKey = process.env.IMWE_APP_KEY?.trim();
|
|
61
|
+
const envSecret = process.env.IMWE_APP_SECRET?.trim();
|
|
62
|
+
if (envKey && envSecret) {
|
|
63
|
+
return { appKey: envKey, appSecret: envSecret, source: 'env' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { appKey: '', appSecret: '', source: 'none' };
|
|
67
|
+
}
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
// 主解析函数
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
export function resolveImweAccount(params) {
|
|
72
|
+
const accountId = normalizeAccountId(params.accountId) ??
|
|
73
|
+
normalizeAccountId(params.cfg.channels?.imwe?.defaultAccount) ??
|
|
74
|
+
'default';
|
|
75
|
+
const baseEnabled = params.cfg.channels?.imwe?.enabled !== false;
|
|
76
|
+
const merged = mergeImweAccountConfig(params.cfg, accountId);
|
|
77
|
+
const accountEnabled = merged.enabled !== false;
|
|
78
|
+
const { appKey, appSecret, source } = resolveImweCredentials(merged, accountId);
|
|
79
|
+
const apiBaseUrl = merged.apiBaseUrl?.trim() || process.env.IMWE_API_BASE_URL?.trim() || '';
|
|
80
|
+
return {
|
|
81
|
+
accountId,
|
|
82
|
+
name: merged.name,
|
|
83
|
+
enabled: baseEnabled && accountEnabled,
|
|
84
|
+
apiBaseUrl,
|
|
85
|
+
appKey,
|
|
86
|
+
appSecret,
|
|
87
|
+
credentialSource: source,
|
|
88
|
+
config: merged,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function listEnabledImweAccounts(cfg) {
|
|
92
|
+
return listImweAccountIds(cfg)
|
|
93
|
+
.map((accountId) => resolveImweAccount({ cfg, accountId }))
|
|
94
|
+
.filter((account) => account.enabled);
|
|
95
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client.ts — imwe Open API HTTP 客户端
|
|
3
|
+
*
|
|
4
|
+
* 对应接口文档:docs/02-OpenAPI接口设计.md
|
|
5
|
+
* Base URL:/api/im/open/bot
|
|
6
|
+
*
|
|
7
|
+
* ── 传输格式 ──────────────────────────────────────────────────────────────────
|
|
8
|
+
* sendMessage / pullMessages:application/x-protobuf
|
|
9
|
+
* getMe 等 JSON 接口: application/json
|
|
10
|
+
*
|
|
11
|
+
* ── 鉴权机制(文档 §1) ───────────────────────────────────────────────────────
|
|
12
|
+
* 每个请求携带三个 Header:
|
|
13
|
+
* X-Api-Key: API Key(ak_ 前缀)
|
|
14
|
+
* X-Timestamp: 毫秒级时间戳字符串
|
|
15
|
+
* X-Signature: Base64(HMAC-SHA256(apiSecret, signString))
|
|
16
|
+
*
|
|
17
|
+
* 签名原文(signString):
|
|
18
|
+
* {METHOD}&{PATH}&{TIMESTAMP}&{BODY_HASH}
|
|
19
|
+
* BODY_HASH = hex(SHA-256(body)),body 为空时对空字符串 "" 哈希
|
|
20
|
+
*
|
|
21
|
+
* ── 设计原则 ──────────────────────────────────────────────────────────────────
|
|
22
|
+
* - 纯函数,不持有状态
|
|
23
|
+
* - Protobuf encode/decode 集中在 src/proto/codec.ts,此文件只负责 HTTP 传输
|
|
24
|
+
* - 请求失败时抛出带 HTTP 状态码的 Error,由上层决定是否重试
|
|
25
|
+
*/
|
|
26
|
+
import { createHmac, createHash, randomUUID } from 'node:crypto';
|
|
27
|
+
import { decodeSendResponse, encodeSendRequest, encodeMediaSendRequest, encodeMarkdownSendRequest, encodeTypingSignalRequest, decodeInboundPacket, genClientMsgId, } from './proto/codec.js';
|
|
28
|
+
import { decodeInboundPacketWithE2ee } from './proto/inbound.codec.js';
|
|
29
|
+
import { getRegistry } from './proto/registry.js';
|
|
30
|
+
import { uploadMediaToStorage } from './media-upload.js';
|
|
31
|
+
import { inferMediaType, inferMediaTypeFromMime } from './media-utils.js';
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// 签名工具(文档 §1.2)
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* 计算请求签名。
|
|
37
|
+
*
|
|
38
|
+
* 签名原文:{METHOD}&{PATH}&{TIMESTAMP}&{BODY_HASH}
|
|
39
|
+
* - METHOD:HTTP 方法大写,固定为 "POST"
|
|
40
|
+
* - PATH:请求路径,如 "/api/im/open/bot/sendMessage"
|
|
41
|
+
* - TIMESTAMP:与 X-Timestamp Header 相同的毫秒时间戳字符串
|
|
42
|
+
* - BODY_HASH:hex(SHA-256(body)),body 为空时对空字符串 "" 哈希
|
|
43
|
+
*
|
|
44
|
+
* 签名算法:Base64(HMAC-SHA256(apiSecret, signString))
|
|
45
|
+
*
|
|
46
|
+
* @param params.apiSecret API Secret(创建机器人时返回,明文仅展示一次)
|
|
47
|
+
* @param params.method HTTP 方法(大写),通常为 "POST"
|
|
48
|
+
* @param params.path 请求路径,如 "/api/im/open/bot/sendMessage"
|
|
49
|
+
* @param params.timestamp 毫秒时间戳字符串,与 X-Timestamp Header 一致
|
|
50
|
+
* @param params.body 请求体字节(Protobuf 二进制或 JSON 字符串的 UTF-8 编码)
|
|
51
|
+
* @returns Base64 编码的 HMAC-SHA256 签名字符串
|
|
52
|
+
*/
|
|
53
|
+
export function buildSignature(params) {
|
|
54
|
+
// 1. 计算请求体的 SHA-256 哈希(小写 hex)
|
|
55
|
+
// body 为空时对空字符串 "" 哈希(SHA-256("") 是固定值)
|
|
56
|
+
const bodyHash = createHash('sha256').update(params.body).digest('hex');
|
|
57
|
+
// 2. 拼接签名原文
|
|
58
|
+
const signString = [params.method, params.path, params.timestamp, bodyHash].join('&');
|
|
59
|
+
// 3. HMAC-SHA256 签名,输出 Base64
|
|
60
|
+
return createHmac('sha256', params.apiSecret).update(signString, 'utf8').digest('base64');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 构造鉴权 Header 集合。
|
|
64
|
+
*
|
|
65
|
+
* @param apiKey API Key(ak_ 前缀)
|
|
66
|
+
* @param apiSecret API Secret
|
|
67
|
+
* @param method HTTP 方法(大写)
|
|
68
|
+
* @param path 请求路径(含前导 /)
|
|
69
|
+
* @param body 请求体字节
|
|
70
|
+
* @returns 包含 X-Api-Key / X-Timestamp / X-Signature 的 Header 对象
|
|
71
|
+
*/
|
|
72
|
+
function buildAuthHeaders(apiKey, apiSecret, method, path, body) {
|
|
73
|
+
// 毫秒级时间戳(文档要求毫秒,服务端校验 5 分钟窗口)
|
|
74
|
+
const timestamp = String(Date.now());
|
|
75
|
+
const signature = buildSignature({ apiSecret, method, path, timestamp, body });
|
|
76
|
+
return {
|
|
77
|
+
'X-Api-Key': apiKey,
|
|
78
|
+
'X-Timestamp': timestamp,
|
|
79
|
+
'X-Signature': signature,
|
|
80
|
+
reqid: randomUUID().replaceAll('-', ''),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
// 内部工具:统一 HTTP 请求
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* 发送 Protobuf 二进制请求,返回响应体字节。
|
|
88
|
+
*
|
|
89
|
+
* Content-Type: application/x-protobuf
|
|
90
|
+
* Accept: application/x-protobuf
|
|
91
|
+
*
|
|
92
|
+
* @param apiBaseUrl API 基础地址,如 https://api.imwe.example.com
|
|
93
|
+
* @param path 请求路径,如 /api/im/open/bot/sendMessage
|
|
94
|
+
* @param body Protobuf 编码的请求体
|
|
95
|
+
* @param auth 鉴权凭证(apiKey + apiSecret)
|
|
96
|
+
* @param opts.timeoutMs 请求超时(毫秒),默认不超时
|
|
97
|
+
* @throws 非 2xx 响应时抛出包含状态码的 Error
|
|
98
|
+
*/
|
|
99
|
+
export async function postProto(apiBaseUrl, path, body, auth, opts) {
|
|
100
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}${path}`;
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = opts?.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : null;
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/x-protobuf',
|
|
108
|
+
Accept: 'application/x-protobuf',
|
|
109
|
+
...buildAuthHeaders(auth.apiKey, auth.apiSecret, 'POST', path, body),
|
|
110
|
+
},
|
|
111
|
+
body: body,
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const errText = await res.text().catch(() => '');
|
|
116
|
+
throw new Error(`imwe API ${path} 返回 ${res.status}: ${errText}`);
|
|
117
|
+
}
|
|
118
|
+
const buf = await res.arrayBuffer();
|
|
119
|
+
return new Uint8Array(buf);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (timer) {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 发送 JSON 请求,返回解析后的 JSON 对象。
|
|
129
|
+
*
|
|
130
|
+
* Content-Type: application/json
|
|
131
|
+
*
|
|
132
|
+
* @param apiBaseUrl API 基础地址
|
|
133
|
+
* @param path 请求路径
|
|
134
|
+
* @param body 请求体对象(会被 JSON.stringify)
|
|
135
|
+
* @param auth 鉴权凭证
|
|
136
|
+
* @param opts.timeoutMs 请求超时(毫秒)
|
|
137
|
+
* @throws 非 2xx 响应时抛出包含状态码的 Error
|
|
138
|
+
*/
|
|
139
|
+
export async function postJson(apiBaseUrl, path, body, auth, opts) {
|
|
140
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}${path}`;
|
|
141
|
+
const bodyStr = JSON.stringify(body);
|
|
142
|
+
const bodyBytes = new TextEncoder().encode(bodyStr);
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timer = opts?.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : null;
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch(url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
Accept: 'application/json',
|
|
151
|
+
...buildAuthHeaders(auth.apiKey, auth.apiSecret, 'POST', path, bodyBytes),
|
|
152
|
+
},
|
|
153
|
+
body: bodyStr,
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const errText = await res.text().catch(() => '');
|
|
158
|
+
throw new Error(`imwe API ${path} 返回 ${res.status}: ${errText}`);
|
|
159
|
+
}
|
|
160
|
+
return res.json();
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (timer) {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// getJson — 签名 GET 请求(JSON 响应)
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* 发送带签名的 GET 请求,返回 JSON 响应。
|
|
173
|
+
*
|
|
174
|
+
* @param apiBaseUrl API 基础地址
|
|
175
|
+
* @param path 请求路径
|
|
176
|
+
* @param auth 鉴权凭证
|
|
177
|
+
* @param opts.timeoutMs 请求超时(毫秒)
|
|
178
|
+
* @throws 非 2xx 响应时抛出包含状态码的 Error
|
|
179
|
+
*/
|
|
180
|
+
export async function getJson(apiBaseUrl, path, auth, opts) {
|
|
181
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}${path}`;
|
|
182
|
+
const emptyBody = new Uint8Array(0);
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const timer = opts?.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : null;
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(url, {
|
|
187
|
+
method: 'GET',
|
|
188
|
+
headers: {
|
|
189
|
+
Accept: 'application/json',
|
|
190
|
+
...buildAuthHeaders(auth.apiKey, auth.apiSecret, 'GET', path, emptyBody),
|
|
191
|
+
},
|
|
192
|
+
signal: controller.signal,
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const errText = await res.text().catch(() => '');
|
|
196
|
+
throw new Error(`imwe API ${path} 返回 ${res.status}: ${errText}`);
|
|
197
|
+
}
|
|
198
|
+
return res.json();
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
if (timer) {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 获取当前机器人信息。
|
|
208
|
+
*
|
|
209
|
+
* 对应接口:POST /api/im/open/bot/getMe
|
|
210
|
+
* 返回的 botAcctId 是机器人的 mainAcctId,pullMessages 时作为 boxId 使用。
|
|
211
|
+
*
|
|
212
|
+
* @param apiBaseUrl API 基础地址
|
|
213
|
+
* @param auth 鉴权凭证
|
|
214
|
+
* @returns 机器人信息
|
|
215
|
+
* @throws 鉴权失败或网络错误时抛出 Error
|
|
216
|
+
*/
|
|
217
|
+
export async function getMe(apiBaseUrl, auth) {
|
|
218
|
+
return postJson(apiBaseUrl, '/api/im/open/bot/getMe', {}, auth);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 通过事件箱模式拉取机器人待处理消息。
|
|
222
|
+
*
|
|
223
|
+
* 对应接口:POST /api/im/open/bot/pullMessages
|
|
224
|
+
* 请求体:PbBoxPullReq(直接序列化,不包装在 PbPacket 中)
|
|
225
|
+
* 响应体:PbBoxPullRsp
|
|
226
|
+
*
|
|
227
|
+
* 增量拉取流程:
|
|
228
|
+
* 1. 首次调用:state.nextStartSeq=0,拉取全部历史消息
|
|
229
|
+
* 2. 响应 milestone=true:将 endSeq 落库,下次从 endSeq+1 开始
|
|
230
|
+
* 3. 响应 hasMore=true:立即继续拉取(调用方负责循环)
|
|
231
|
+
* 4. 响应 endSeq=0:无新消息
|
|
232
|
+
*
|
|
233
|
+
* @param apiBaseUrl API 基础地址
|
|
234
|
+
* @param auth 鉴权凭证
|
|
235
|
+
* @param boxId 事件箱 ID(机器人的 botAcctId/mainAcctId,来自 getMe 响应)
|
|
236
|
+
* @param state 拉取状态(含 nextStartSeq,调用方负责持久化)
|
|
237
|
+
* @returns 解包后的消息列表、更新后的状态、是否还有更多数据
|
|
238
|
+
*/
|
|
239
|
+
export async function pullMessages(apiBaseUrl, auth, boxId, state, opts) {
|
|
240
|
+
const reg = await getRegistry();
|
|
241
|
+
// 构造 PbBoxPullReq
|
|
242
|
+
const reqMsg = reg.PbBoxPullReq.create({
|
|
243
|
+
boxId,
|
|
244
|
+
startSeq: state.nextStartSeq,
|
|
245
|
+
});
|
|
246
|
+
const requestBody = reg.PbBoxPullReq.encode(reqMsg).finish();
|
|
247
|
+
const responseBytes = await postProto(apiBaseUrl, '/api/im/open/bot/pullMessages', requestBody, auth);
|
|
248
|
+
// 解码 PbBoxPullRsp
|
|
249
|
+
// protobufjs 将 uint64 字段解码为 Long 对象(来自 long 包),用 Number() 转换为 number
|
|
250
|
+
const rsp = reg.PbBoxPullRsp.decode(responseBytes);
|
|
251
|
+
// Long 对象用 toNumber(),普通 number 直接用,统一转为 number
|
|
252
|
+
const toNum = (v) => v == null ? 0 : typeof v === 'number' ? v : v.toNumber();
|
|
253
|
+
const endSeq = toNum(rsp.endSeq);
|
|
254
|
+
const hasMore = rsp.hasMore ?? false;
|
|
255
|
+
// milestone=true 且 endSeq>0 时更新游标,下次从 endSeq+1 开始
|
|
256
|
+
const nextStartSeq = rsp.milestone && endSeq > 0 ? endSeq + 1 : state.nextStartSeq;
|
|
257
|
+
// 解包每条消息:item.body → PbChatMsgDeliverBody → PbChatMsgEnvelope → text
|
|
258
|
+
const messages = [];
|
|
259
|
+
for (const item of rsp.items ?? []) {
|
|
260
|
+
if (!item.body || item.body.length === 0) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// item.body 是 PbChatMsgDeliverBody 的字节,需要包装成 PbPacket(SERVER_REQ) 再走四层解包
|
|
264
|
+
// 按文档:items[].request.body 为 PbChatMsgDeliverBody
|
|
265
|
+
// 构造一个虚拟的 PbPacket(SERVER_REQ) 供 decodeInboundPacket 使用
|
|
266
|
+
const requestMsg = reg.PbRequest.create({
|
|
267
|
+
path: '/chat/deliver',
|
|
268
|
+
reqId: item.reqId ?? '',
|
|
269
|
+
reqStamp: toNum(item.reqStamp),
|
|
270
|
+
body: item.body,
|
|
271
|
+
});
|
|
272
|
+
const requestBytes = reg.PbRequest.encode(requestMsg).finish();
|
|
273
|
+
const packetMsg = reg.PbPacket.create({
|
|
274
|
+
type: 2, // SERVER_REQ
|
|
275
|
+
request: reg.PbRequest.decode(requestBytes),
|
|
276
|
+
});
|
|
277
|
+
const packetBytes = reg.PbPacket.encode(packetMsg).finish();
|
|
278
|
+
const result = opts?.e2eeDecrypt
|
|
279
|
+
? await decodeInboundPacketWithE2ee(packetBytes, { e2eeDecrypt: opts.e2eeDecrypt })
|
|
280
|
+
: await decodeInboundPacket(packetBytes);
|
|
281
|
+
if (result.ok) {
|
|
282
|
+
messages.push(result.message);
|
|
283
|
+
}
|
|
284
|
+
// result.ok=false 时静默跳过(操作消息、非文本消息、decrypt-failed、operation-consumed 等)
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
messages,
|
|
288
|
+
state: { nextStartSeq },
|
|
289
|
+
hasMore,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
293
|
+
// sendMessage — 发送单聊消息(文档 §一)
|
|
294
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
295
|
+
/**
|
|
296
|
+
* 向用户发送单聊文字消息。
|
|
297
|
+
*
|
|
298
|
+
* 对应接口:POST /api/im/open/bot/sendMessage
|
|
299
|
+
* 请求体:PbPacket(CLIENT_REQ) 四层包装(由 encodeSendRequest 生成)
|
|
300
|
+
* 响应体:PbResponse 的 protobuf 二进制(statusCode + body),body 为 PbMsgRspBody
|
|
301
|
+
*
|
|
302
|
+
* 四层包装链路(由 send.codec.ts 负责):
|
|
303
|
+
* PbChatTextContent(text)
|
|
304
|
+
* → PbChatMsgEnvelope(textContent)
|
|
305
|
+
* → PbSingleChatMsgReqBody(fromId, toId, envelopeType=1, e2eeFlag=false, envelope)
|
|
306
|
+
* → PbRequest(path="/api/im/chat/bot", body)
|
|
307
|
+
* → PbPacket(type=CLIENT_REQ=0, request)
|
|
308
|
+
*
|
|
309
|
+
* @param apiBaseUrl API 基础地址
|
|
310
|
+
* @param auth 鉴权凭证
|
|
311
|
+
* @param fromId 发送方 ID(机器人的 botAcctId,来自 getMe 响应)
|
|
312
|
+
* @param to 接收方用户 ID
|
|
313
|
+
* @param text 消息文本内容
|
|
314
|
+
* @returns 发送结果
|
|
315
|
+
*/
|
|
316
|
+
export async function sendTextMessage(apiBaseUrl, auth, fromId, to, text, options) {
|
|
317
|
+
const clientMsgId = options?.clientMsgId ?? genClientMsgId();
|
|
318
|
+
// encodeSendRequest 负责四层包装,fromId 传入供 PbSingleChatMsgReqBody 使用
|
|
319
|
+
const requestBody = await encodeSendRequest(to, text, fromId, {
|
|
320
|
+
referenceClientMsgId: options?.replyToId,
|
|
321
|
+
clientMsgId,
|
|
322
|
+
});
|
|
323
|
+
const responseBytes = await postProto(apiBaseUrl, '/api/im/open/bot/sendMessage', requestBody, auth);
|
|
324
|
+
return {
|
|
325
|
+
...(await decodeSendResponse(responseBytes)),
|
|
326
|
+
clientMsgId,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
330
|
+
// sendMediaMessage — 发送多媒体消息(文档 §一)
|
|
331
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
332
|
+
/**
|
|
333
|
+
* 向用户发送多媒体消息(图片/视频/音频/文件)。
|
|
334
|
+
*
|
|
335
|
+
* 使用与 sendTextMessage 相同的 HTTP 接口和鉴权机制,
|
|
336
|
+
* 区别在于 PbChatMsgEnvelope 的 oneof 字段为 richMediaContent/fileContent/audioContent。
|
|
337
|
+
*
|
|
338
|
+
* @param apiBaseUrl API 基础地址
|
|
339
|
+
* @param auth 鉴权凭证
|
|
340
|
+
* @param media 多媒体消息参数(含 to、url、mediaType 等)
|
|
341
|
+
* @returns 发送结果
|
|
342
|
+
*/
|
|
343
|
+
export async function sendMediaMessage(apiBaseUrl, auth, media) {
|
|
344
|
+
const clientMsgId = media.clientMsgId ?? genClientMsgId();
|
|
345
|
+
const requestBody = await encodeMediaSendRequest({
|
|
346
|
+
...media,
|
|
347
|
+
clientMsgId,
|
|
348
|
+
});
|
|
349
|
+
const responseBytes = await postProto(apiBaseUrl, '/api/im/open/bot/sendMessage', requestBody, auth);
|
|
350
|
+
return {
|
|
351
|
+
...(await decodeSendResponse(responseBytes)),
|
|
352
|
+
clientMsgId,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
356
|
+
// sendMarkdownMessage — 发送 markdown 消息(文档 §一)
|
|
357
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
358
|
+
/**
|
|
359
|
+
* 向用户发送 markdown 格式消息。
|
|
360
|
+
*
|
|
361
|
+
* 使用与 sendTextMessage / sendMediaMessage 相同的 HTTP 接口和鉴权机制,
|
|
362
|
+
* 区别在于 PbChatMsgEnvelope 的 oneof 字段为 markdownContent(字段 16)。
|
|
363
|
+
*
|
|
364
|
+
* 四层包装链路(由 send.codec.ts::encodeMarkdownSendRequest 负责):
|
|
365
|
+
* PbMarkdownContent(digest, markdown)
|
|
366
|
+
* → PbChatMsgEnvelope(markdownContent=...)
|
|
367
|
+
* → PbSingleChatMsgReqBody(envelopeType=1, e2eeFlag=false)
|
|
368
|
+
* → PbRequest(path="/api/im/chat/bot")
|
|
369
|
+
* → PbPacket(type=CLIENT_REQ=0)
|
|
370
|
+
*
|
|
371
|
+
* @param apiBaseUrl API 基础地址
|
|
372
|
+
* @param auth 鉴权凭证
|
|
373
|
+
* @param params markdown 消息参数:fromId / to / markdown / digest? / clientMsgId?
|
|
374
|
+
* @returns 发送结果
|
|
375
|
+
*/
|
|
376
|
+
export async function sendMarkdownMessage(apiBaseUrl, auth, params) {
|
|
377
|
+
const clientMsgId = params.clientMsgId ?? genClientMsgId();
|
|
378
|
+
const requestBody = await encodeMarkdownSendRequest({
|
|
379
|
+
to: params.to,
|
|
380
|
+
fromId: params.fromId,
|
|
381
|
+
markdown: params.markdown,
|
|
382
|
+
digest: params.digest,
|
|
383
|
+
clientMsgId,
|
|
384
|
+
});
|
|
385
|
+
const responseBytes = await postProto(apiBaseUrl, '/api/im/open/bot/sendMessage', requestBody, auth);
|
|
386
|
+
return {
|
|
387
|
+
...(await decodeSendResponse(responseBytes)),
|
|
388
|
+
clientMsgId,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
392
|
+
// sendMediaMessageWithUpload — 上传并发送多媒体消息(deliver 回调专用)
|
|
393
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
394
|
+
/**
|
|
395
|
+
* 上传并发送多媒体消息。
|
|
396
|
+
*
|
|
397
|
+
* 与 sendMediaMessage 的区别:包含 uploadMediaToStorage 上传步骤。
|
|
398
|
+
* 与 sendImweMedia(send.ts)的区别:不走账号解析,直接接收已解析的连接参数。
|
|
399
|
+
* 适用于 monitor.ts 的 deliver 回调,此时账号信息已在上下文中。
|
|
400
|
+
*
|
|
401
|
+
* @param apiBaseUrl API 基础地址
|
|
402
|
+
* @param auth 鉴权凭证
|
|
403
|
+
* @param fromId 发送方 ID(机器人的 botAcctId)
|
|
404
|
+
* @param to 接收方用户 ID
|
|
405
|
+
* @param mediaUrl 媒体文件 URL(本地路径或远程 URL)
|
|
406
|
+
* @param options 可选参数(caption 等)
|
|
407
|
+
* @returns 发送结果
|
|
408
|
+
*/
|
|
409
|
+
export async function sendMediaMessageWithUpload(apiBaseUrl, auth, fromId, to, mediaUrl, options) {
|
|
410
|
+
const log = options?.log;
|
|
411
|
+
// 上传到平台存储
|
|
412
|
+
log?.info?.(`[sendMediaMessageWithUpload] 开始上传: mediaUrl=${mediaUrl.slice(0, 80)}, to=${to}`);
|
|
413
|
+
const uploadResult = await uploadMediaToStorage(mediaUrl, auth, apiBaseUrl, {
|
|
414
|
+
imMainAccId: to,
|
|
415
|
+
log,
|
|
416
|
+
});
|
|
417
|
+
log?.info?.(`[sendMediaMessageWithUpload] 上传完成: url=${uploadResult.url.slice(0, 80)}, contentType=${uploadResult.contentType}, fileSize=${uploadResult.fileSize}`);
|
|
418
|
+
// 优先从上传结果的 contentType 推断媒体类型,回退到原始 URL 后缀推断
|
|
419
|
+
// 注意:不能用 uploadResult.url(CDN URL 可能无扩展名,会永远回退为 'file')
|
|
420
|
+
const mediaType = uploadResult.contentType
|
|
421
|
+
? inferMediaTypeFromMime(uploadResult.contentType)
|
|
422
|
+
: inferMediaType(mediaUrl);
|
|
423
|
+
log?.info?.(`[sendMediaMessageWithUpload] 推断 mediaType=${mediaType}, 准备发送`);
|
|
424
|
+
return sendMediaMessage(apiBaseUrl, auth, {
|
|
425
|
+
to,
|
|
426
|
+
fromId,
|
|
427
|
+
url: uploadResult.url,
|
|
428
|
+
mediaType,
|
|
429
|
+
mimeType: uploadResult.contentType,
|
|
430
|
+
fileSize: uploadResult.fileSize,
|
|
431
|
+
caption: options?.caption,
|
|
432
|
+
blurHash: uploadResult.blurHash,
|
|
433
|
+
width: uploadResult.width,
|
|
434
|
+
height: uploadResult.height,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 获取文件上传的预签名 URL。
|
|
439
|
+
* 第三方使用该 URL 直传文件到对象存储(无需经过服务端中转)。
|
|
440
|
+
*
|
|
441
|
+
* @param apiBaseUrl API 基础地址
|
|
442
|
+
* @param auth 鉴权凭证
|
|
443
|
+
* @param params 上传参数
|
|
444
|
+
*/
|
|
445
|
+
export async function uploadPreSignedUrl(apiBaseUrl, auth, params) {
|
|
446
|
+
return postJson(apiBaseUrl, '/api/im/open/bot/uploadPreSignedUrl', {
|
|
447
|
+
imMainAccId: params.imMainAccId,
|
|
448
|
+
fileName: params.fileName,
|
|
449
|
+
fileSize: params.fileSize,
|
|
450
|
+
mimeType: params.mimeType,
|
|
451
|
+
scene: params.scene ?? 'AI_GC',
|
|
452
|
+
}, auth);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* 文件上传到对象存储后,确认上传完成,获取文件访问地址。
|
|
456
|
+
*
|
|
457
|
+
* @param apiBaseUrl API 基础地址
|
|
458
|
+
* @param auth 鉴权凭证
|
|
459
|
+
* @param params 确认参数
|
|
460
|
+
*/
|
|
461
|
+
export async function uploadConfirm(apiBaseUrl, auth, params) {
|
|
462
|
+
return postJson(apiBaseUrl, '/api/im/open/bot/uploadConfirm', {
|
|
463
|
+
imMainAccId: params.imMainAccId,
|
|
464
|
+
fileId: params.fileId,
|
|
465
|
+
needDownloadUrl: false,
|
|
466
|
+
scene: params.scene ?? 'AI_GC',
|
|
467
|
+
}, auth);
|
|
468
|
+
}
|
|
469
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
470
|
+
// sendTypingSignal — 发送 typing indicator 信号(文档 §一,envelopeType=2)
|
|
471
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
472
|
+
/**
|
|
473
|
+
* 向用户发送 typing signal(正在输入指示器)。
|
|
474
|
+
*
|
|
475
|
+
* 使用与 sendTextMessage 相同的 HTTP 接口和鉴权机制,
|
|
476
|
+
* 区别在于 envelopeType=2 和 PbChatOperationEnvelope 信封。
|
|
477
|
+
*
|
|
478
|
+
* 四层包装链路(由 send.codec.ts 负责):
|
|
479
|
+
* PbBotThinkingSignal(status, botImAcctId)
|
|
480
|
+
* → PbChatOperationEnvelope(botThinking=...)
|
|
481
|
+
* → PbSingleChatMsgReqBody(fromId, toId, envelopeType=2, envelope=...)
|
|
482
|
+
* → PbRequest(path="/api/im/chat/bot", body=...)
|
|
483
|
+
* → PbPacket(type=CLIENT_REQ=0, request=...)
|
|
484
|
+
*
|
|
485
|
+
* @param apiBaseUrl API 基础地址
|
|
486
|
+
* @param auth 鉴权凭证
|
|
487
|
+
* @param fromId 发送方 ID(机器人的 botAcctId,来自 getMe 响应)
|
|
488
|
+
* @param to 接收方用户 ID
|
|
489
|
+
* @param status 1=begin(开始输入),2=end(结束输入)
|
|
490
|
+
* @param botImAcctId Bot 的 imAcctId(通常等于 fromId / botAcctId)
|
|
491
|
+
* @returns 发送结果
|
|
492
|
+
*/
|
|
493
|
+
export async function sendTypingSignal(apiBaseUrl, auth, fromId, to, status, botImAcctId) {
|
|
494
|
+
// encodeTypingSignalRequest 负责四层包装,fromId 传入供 PbSingleChatMsgReqBody 使用
|
|
495
|
+
const requestBody = await encodeTypingSignalRequest(to, status, botImAcctId, fromId);
|
|
496
|
+
const responseBytes = await postProto(apiBaseUrl, '/api/im/open/bot/sendMessage', requestBody, auth);
|
|
497
|
+
return decodeSendResponse(responseBytes);
|
|
498
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot-info-cache.ts — 机器人信息的进程级缓存
|
|
3
|
+
*
|
|
4
|
+
* 生命周期:
|
|
5
|
+
* - monitor.ts 的 monitorImweAccount 在 getMe 成功后调用 setBotInfo 写入
|
|
6
|
+
* - send.ts 的 sendImweText 在发送消息时调用 getBotInfo 读取 botAcctId 作为 fromId
|
|
7
|
+
* - monitor.ts 的轮询循环退出时调用 clearBotInfo 清理
|
|
8
|
+
*
|
|
9
|
+
* 多账号安全:以 accountId 为 key,每个账号独立缓存,互不干扰。
|
|
10
|
+
*
|
|
11
|
+
* 为什么不用 channelRuntime.runtimeContexts:
|
|
12
|
+
* outbound 的 sendText 回调签名是 (ctx: ChannelOutboundContext) => ...,
|
|
13
|
+
* ChannelOutboundContext 只有 cfg/to/text/accountId,没有 channelRuntime,
|
|
14
|
+
* 无法调用 runtimeContexts.get()。模块级 Map 是最简单可靠的方案。
|
|
15
|
+
*/
|
|
16
|
+
/** accountId → BotInfo,进程级缓存 */
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* 缓存机器人信息(monitor 启动时写入)。
|
|
20
|
+
*
|
|
21
|
+
* @param accountId 账号 ID(多账号时区分不同机器人)
|
|
22
|
+
* @param info getMe 返回的机器人信息
|
|
23
|
+
*/
|
|
24
|
+
export function setBotInfo(accountId, info) {
|
|
25
|
+
cache.set(accountId, info);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 读取缓存的机器人信息(send / outbound / pairing 读取)。
|
|
29
|
+
*
|
|
30
|
+
* @param accountId 账号 ID
|
|
31
|
+
* @returns 缓存的 BotInfo,未缓存时返回 undefined
|
|
32
|
+
*/
|
|
33
|
+
export function getBotInfo(accountId) {
|
|
34
|
+
return cache.get(accountId);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 清除缓存的机器人信息(monitor 停止时调用)。
|
|
38
|
+
*
|
|
39
|
+
* @param accountId 账号 ID
|
|
40
|
+
*/
|
|
41
|
+
export function clearBotInfo(accountId) {
|
|
42
|
+
cache.delete(accountId);
|
|
43
|
+
}
|