@zeyiy/openclaw-channel 0.3.4

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,18 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only
5
+
6
+ This project is licensed under the GNU Affero General Public License v3.0 only.
7
+
8
+ You should have received a copy of the GNU Affero General Public License
9
+ along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
10
+
11
+ ---
12
+
13
+ This project is a fork of https://github.com/openimsdk/openclaw-channel
14
+ Original authors: blooming and contributors
15
+ Copyright (c) openimsdk contributors
16
+
17
+ Modifications copyright (c) 2026 ZeyiY
18
+ All modifications are also licensed under AGPL-3.0-only.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @zeyiy/openclaw-channel
2
+
3
+ OpenIM channel plugin for OpenClaw Gateway.
4
+
5
+ > Forked from [@openim/openclaw-channel](https://github.com/openimsdk/openclaw-channel). Licensed under AGPL-3.0-only.
6
+
7
+ Chinese documentation: [README.zh-CN.md](https://github.com/ZeyiY/openclaw-channel/blob/main/README.zh-CN.md)
8
+
9
+ ## Features
10
+
11
+ - Direct chat and group chat support
12
+ - Inbound and outbound text/image/file messages
13
+ - `openim_send_video` is intentionally sent as a file message
14
+ - Quote/reply message parsing for inbound context
15
+ - Multi-account login via `channels.openim.accounts.<id>`
16
+ - Group trigger policy with optional mention-only mode
17
+ - Interactive setup command: `openclaw openim setup`
18
+
19
+ ## Installation
20
+
21
+ Install from npm:
22
+
23
+ ```bash
24
+ openclaw plugins install @zeyiy/openclaw-channel
25
+ ```
26
+
27
+ Or install from local path:
28
+
29
+ ```bash
30
+ openclaw plugins install /path/to/openclaw-channel
31
+ ```
32
+
33
+ Repository: https://github.com/ZeyiY/openclaw-channel
34
+
35
+ ## Identity Mapping
36
+
37
+ - npm package name: `@zeyiy/openclaw-channel`
38
+ - plugin id: `openclaw-channel` (used in `plugins.entries` and `plugins.allow`)
39
+ - channel id: `openim` (used in `channels.openim`)
40
+ - setup command: `openclaw openim setup`
41
+
42
+ ## Configuration
43
+
44
+ ### Option 1: Interactive setup (recommended)
45
+
46
+ ```bash
47
+ openclaw openim setup
48
+ ```
49
+
50
+ ### Option 2: Edit `~/.openclaw/openclaw.json`
51
+
52
+ ```json
53
+ {
54
+ "channels": {
55
+ "openim": {
56
+ "accounts": {
57
+ "default": {
58
+ "enabled": true,
59
+ "token": "your_token",
60
+ "wsAddr": "ws://127.0.0.1:10001",
61
+ "apiAddr": "http://127.0.0.1:10002"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ `userID` and `platformID` are optional. If omitted, they are auto-derived from JWT token claims (`UserID` and `PlatformID`).
70
+
71
+ `requireMention` is optional and defaults to `true`.
72
+
73
+ `inboundWhitelist` is optional. If omitted or empty, inbound handling keeps existing behavior.
74
+ If set, only these users can trigger processing:
75
+ - direct messages to the account
76
+ - group messages where they `@` the account
77
+
78
+ Single-account fallback (without `accounts`) is supported.
79
+
80
+ Environment fallback is supported for the `default` account:
81
+
82
+ - `OPENIM_TOKEN`
83
+ - `OPENIM_WS_ADDR`
84
+ - `OPENIM_API_ADDR`
85
+
86
+ Optional env overrides:
87
+
88
+ - `OPENIM_USER_ID`
89
+ - `OPENIM_PLATFORM_ID`
90
+
91
+ ## Agent Tools
92
+
93
+ - `openim_send_text`
94
+ - `target`: `user:<id>` or `group:<id>`
95
+ - `text`: message text
96
+ - `accountId` (optional): select sending account
97
+
98
+ - `openim_send_image`
99
+ - `target`: `user:<id>` or `group:<id>`
100
+ - `image`: local path (`file://` supported) or `http(s)` URL
101
+ - `accountId` (optional): select sending account
102
+
103
+ - `openim_send_video`
104
+ - `target`: `user:<id>` or `group:<id>`
105
+ - `video`: local path (`file://` supported) or `http(s)` URL
106
+ - behavior: sent as a file message (not OpenIM video message)
107
+ - `name` (optional): override filename for URL input
108
+ - `accountId` (optional): select sending account
109
+
110
+ - `openim_send_file`
111
+ - `target`: `user:<id>` or `group:<id>`
112
+ - `file`: local path (`file://` supported) or `http(s)` URL
113
+ - `name` (optional): override filename for URL input
114
+ - `accountId` (optional): select sending account
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ pnpm run build
120
+ pnpm run test:connect
121
+ ```
122
+
123
+ For `test:connect`, configure `.env` first (see `.env.example`).
124
+
125
+ ## License
126
+
127
+ AGPL-3.0-only. See [LICENSE](https://github.com/ZeyiY/openclaw-channel/blob/main/LICENSE).
128
+
129
+ Originally developed by [openimsdk](https://github.com/openimsdk/openclaw-channel).
@@ -0,0 +1,128 @@
1
+ # @zeyiy/openclaw-channel
2
+
3
+ OpenClaw Gateway 的 OpenIM 渠道插件。
4
+
5
+ > 从 [@openim/openclaw-channel](https://github.com/openimsdk/openclaw-channel) fork 而来,采用 AGPL-3.0-only 许可证。
6
+
7
+ English documentation: [README.md](https://github.com/ZeyiY/openclaw-channel/blob/main/README.md)
8
+
9
+ ## 功能
10
+
11
+ - 支持私聊与群聊
12
+ - 支持文本/图片/文件消息的收发
13
+ - `openim_send_video` 按文件消息发送(不使用 OpenIM 视频消息)
14
+ - 支持引用消息解析(用于入站上下文)
15
+ - 支持多账号并发(`channels.openim.accounts.<id>`)
16
+ - 支持群聊仅 @ 触发
17
+ - 提供交互式配置命令:`openclaw openim setup`
18
+
19
+ ## 安装
20
+
21
+ 从 npm 安装:
22
+
23
+ ```bash
24
+ openclaw plugins install @zeyiy/openclaw-channel
25
+ ```
26
+
27
+ 本地路径安装:
28
+
29
+ ```bash
30
+ openclaw plugins install /path/to/openclaw-channel
31
+ ```
32
+
33
+ 仓库地址:https://github.com/ZeyiY/openclaw-channel
34
+
35
+ ## 标识说明
36
+
37
+ - npm 包名:`@zeyiy/openclaw-channel`
38
+ - 插件 id:`openclaw-channel`(用于 `plugins.entries` / `plugins.allow`)
39
+ - 渠道 id:`openim`(用于 `channels.openim`)
40
+ - 配置命令:`openclaw openim setup`
41
+
42
+ ## 配置
43
+
44
+ ### 方式一:交互式配置(推荐)
45
+
46
+ ```bash
47
+ openclaw openim setup
48
+ ```
49
+
50
+ ### 方式二:手动编辑 `~/.openclaw/openclaw.json`
51
+
52
+ ```json
53
+ {
54
+ "channels": {
55
+ "openim": {
56
+ "accounts": {
57
+ "default": {
58
+ "enabled": true,
59
+ "token": "your_token",
60
+ "wsAddr": "ws://127.0.0.1:10001",
61
+ "apiAddr": "http://127.0.0.1:10002"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ `userID` 和 `platformID` 为可选项,未填写时会自动从 JWT token 的 `UserID` / `PlatformID` 声明解析。
70
+
71
+ `requireMention` 为可选项,默认 `true`。
72
+
73
+ `inboundWhitelist` 为可选项,不填或为空时保持当前逻辑;填了后仅处理白名单用户触发的消息:
74
+ - 给账号发单聊消息
75
+ - 在群里 @ 账号的消息
76
+
77
+ 支持单账号兜底写法(不使用 `accounts`)。
78
+
79
+ `default` 账号支持环境变量兜底:
80
+
81
+ - `OPENIM_TOKEN`
82
+ - `OPENIM_WS_ADDR`
83
+ - `OPENIM_API_ADDR`
84
+
85
+ 可选环境变量覆盖项:
86
+
87
+ - `OPENIM_USER_ID`
88
+ - `OPENIM_PLATFORM_ID`
89
+
90
+ ## Agent 工具
91
+
92
+ - `openim_send_text`
93
+ - `target`: `user:<id>` 或 `group:<id>`
94
+ - `text`: 文本内容
95
+ - `accountId`(可选):指定发送账号
96
+
97
+ - `openim_send_image`
98
+ - `target`: `user:<id>` 或 `group:<id>`
99
+ - `image`: 本地路径(支持 `file://`)或 `http(s)` URL
100
+ - `accountId`(可选):指定发送账号
101
+
102
+ - `openim_send_video`
103
+ - `target`: `user:<id>` 或 `group:<id>`
104
+ - `video`: 本地路径(支持 `file://`)或 `http(s)` URL
105
+ - 行为:按文件消息发送(不是视频消息)
106
+ - `name`(可选):URL 输入时覆盖文件名
107
+ - `accountId`(可选):指定发送账号
108
+
109
+ - `openim_send_file`
110
+ - `target`: `user:<id>` 或 `group:<id>`
111
+ - `file`: 本地路径(支持 `file://`)或 `http(s)` URL
112
+ - `name`(可选):URL 输入时覆盖文件名
113
+ - `accountId`(可选):指定发送账号
114
+
115
+ ## 开发
116
+
117
+ ```bash
118
+ pnpm run build
119
+ pnpm run test:connect
120
+ ```
121
+
122
+ 运行 `test:connect` 前请先配置 `.env`(参考 `.env.example`)。
123
+
124
+ ## 许可证
125
+
126
+ 本项目采用 `AGPL-3.0-only` 许可证。详见 [LICENSE](https://github.com/ZeyiY/openclaw-channel/blob/main/LICENSE)。
127
+
128
+ 原始项目由 [openimsdk](https://github.com/openimsdk/openclaw-channel) 开发。
@@ -0,0 +1,51 @@
1
+ export declare const OpenIMChannelPlugin: {
2
+ id: string;
3
+ meta: {
4
+ id: string;
5
+ label: string;
6
+ selectionLabel: string;
7
+ docsPath: string;
8
+ blurb: string;
9
+ aliases: string[];
10
+ };
11
+ capabilities: {
12
+ chatTypes: string[];
13
+ };
14
+ config: {
15
+ listAccountIds: (cfg: any) => string[];
16
+ resolveAccount: (cfg: any, accountId?: string) => {
17
+ [k: string]: unknown;
18
+ accountId: string;
19
+ };
20
+ };
21
+ gateway: {
22
+ startAccount: (ctx: any) => Promise<void>;
23
+ };
24
+ outbound: {
25
+ deliveryMode: "direct";
26
+ resolveTarget: ({ to }: {
27
+ to?: string;
28
+ }) => {
29
+ ok: boolean;
30
+ error: Error;
31
+ to?: undefined;
32
+ } | {
33
+ ok: boolean;
34
+ to: string;
35
+ error?: undefined;
36
+ };
37
+ sendText: ({ to, text, accountId }: {
38
+ to: string;
39
+ text: string;
40
+ accountId?: string;
41
+ }) => Promise<{
42
+ ok: boolean;
43
+ error: Error;
44
+ provider?: undefined;
45
+ } | {
46
+ ok: boolean;
47
+ provider: string;
48
+ error?: undefined;
49
+ }>;
50
+ };
51
+ };
@@ -0,0 +1,74 @@
1
+ import { getConnectedClient, startAccountClient, stopAccountClient } from "./clients";
2
+ import { listAccountIds, resolveAccountConfig, getOpenIMAccountConfig } from "./config";
3
+ import { sendTextToTarget } from "./media";
4
+ import { parseTarget } from "./targets";
5
+ import { formatSdkError } from "./utils";
6
+ export const OpenIMChannelPlugin = {
7
+ id: "openim",
8
+ meta: {
9
+ id: "openim",
10
+ label: "OpenIM",
11
+ selectionLabel: "OpenIM",
12
+ docsPath: "/channels/openim",
13
+ blurb: "OpenIM protocol channel via @openim/client-sdk",
14
+ aliases: ["openim", "im"],
15
+ },
16
+ capabilities: {
17
+ chatTypes: ["direct", "group"],
18
+ },
19
+ config: {
20
+ listAccountIds: (cfg) => listAccountIds(cfg),
21
+ resolveAccount: (cfg, accountId) => resolveAccountConfig(cfg, accountId),
22
+ },
23
+ gateway: {
24
+ startAccount: async (ctx) => {
25
+ const api = globalThis.__openimApi;
26
+ if (!api) {
27
+ ctx.log?.error?.("[openim] api not initialized");
28
+ return;
29
+ }
30
+ const config = getOpenIMAccountConfig(ctx.cfg ?? api.config, ctx.accountId);
31
+ if (!config || !config.enabled)
32
+ return;
33
+ if (!getConnectedClient(ctx.accountId)) {
34
+ await startAccountClient(api, config);
35
+ }
36
+ await new Promise((resolve) => {
37
+ if (ctx.abortSignal?.aborted) {
38
+ resolve();
39
+ }
40
+ else {
41
+ ctx.abortSignal?.addEventListener("abort", () => resolve(), { once: true });
42
+ }
43
+ });
44
+ await stopAccountClient(api, ctx.accountId);
45
+ },
46
+ },
47
+ outbound: {
48
+ deliveryMode: "direct",
49
+ resolveTarget: ({ to }) => {
50
+ const target = parseTarget(to);
51
+ if (!target) {
52
+ return { ok: false, error: new Error("OpenIM requires --to <user:ID|group:ID>") };
53
+ }
54
+ return { ok: true, to: `${target.kind}:${target.id}` };
55
+ },
56
+ sendText: async ({ to, text, accountId }) => {
57
+ const target = parseTarget(to);
58
+ if (!target) {
59
+ return { ok: false, error: new Error("invalid target, expected user:<id> or group:<id>") };
60
+ }
61
+ const client = getConnectedClient(accountId);
62
+ if (!client) {
63
+ return { ok: false, error: new Error("OpenIM not connected") };
64
+ }
65
+ try {
66
+ await sendTextToTarget(client, target, text);
67
+ return { ok: true, provider: "openim" };
68
+ }
69
+ catch (e) {
70
+ return { ok: false, error: new Error(formatSdkError(e)) };
71
+ }
72
+ },
73
+ },
74
+ };
@@ -0,0 +1,6 @@
1
+ import type { OpenIMAccountConfig, OpenIMClientState } from "./types";
2
+ export declare function getConnectedClient(accountId?: string): OpenIMClientState | null;
3
+ export declare function connectedClientCount(): number;
4
+ export declare function startAccountClient(api: any, config: OpenIMAccountConfig): Promise<void>;
5
+ export declare function stopAccountClient(api: any, accountId: string): Promise<void>;
6
+ export declare function stopAllClients(api: any): Promise<void>;
@@ -0,0 +1,103 @@
1
+ import { CbEvents, getSDK } from "@openim/client-sdk";
2
+ import { processInboundMessage } from "./inbound";
3
+ import { startPortalBridge, stopPortalBridge, stopAllPortalBridges } from "./portal";
4
+ import { formatSdkError } from "./utils";
5
+ const clients = new Map();
6
+ function detachHandlers(state) {
7
+ state.sdk.off(CbEvents.OnRecvNewMessage, state.handlers.onRecvNewMessage);
8
+ state.sdk.off(CbEvents.OnRecvNewMessages, state.handlers.onRecvNewMessages);
9
+ state.sdk.off(CbEvents.OnRecvOfflineNewMessages, state.handlers.onRecvOfflineNewMessages);
10
+ }
11
+ export function getConnectedClient(accountId) {
12
+ if (accountId && clients.has(accountId)) {
13
+ return clients.get(accountId) ?? null;
14
+ }
15
+ if (clients.has("default"))
16
+ return clients.get("default") ?? null;
17
+ const first = clients.values().next();
18
+ return first.done ? null : first.value;
19
+ }
20
+ export function connectedClientCount() {
21
+ return clients.size;
22
+ }
23
+ export async function startAccountClient(api, config) {
24
+ const sdk = getSDK();
25
+ const state = {
26
+ sdk,
27
+ config,
28
+ handlers: {
29
+ onRecvNewMessage: () => undefined,
30
+ onRecvNewMessages: () => undefined,
31
+ onRecvOfflineNewMessages: () => undefined,
32
+ },
33
+ };
34
+ const consumeMessage = (msg) => {
35
+ processInboundMessage(api, state, msg).catch((e) => {
36
+ api.logger?.error?.(`[openim] processInboundMessage failed: ${formatSdkError(e)}`);
37
+ });
38
+ };
39
+ state.handlers.onRecvNewMessage = (event) => {
40
+ if (event?.data)
41
+ consumeMessage(event.data);
42
+ };
43
+ state.handlers.onRecvNewMessages = (event) => {
44
+ const list = Array.isArray(event?.data) ? event.data : [];
45
+ for (const msg of list)
46
+ consumeMessage(msg);
47
+ };
48
+ state.handlers.onRecvOfflineNewMessages = (event) => {
49
+ const list = Array.isArray(event?.data) ? event.data : [];
50
+ for (const msg of list)
51
+ consumeMessage(msg);
52
+ };
53
+ sdk.on(CbEvents.OnRecvNewMessage, state.handlers.onRecvNewMessage);
54
+ sdk.on(CbEvents.OnRecvNewMessages, state.handlers.onRecvNewMessages);
55
+ sdk.on(CbEvents.OnRecvOfflineNewMessages, state.handlers.onRecvOfflineNewMessages);
56
+ try {
57
+ await sdk.login({
58
+ userID: config.userID,
59
+ token: config.token,
60
+ wsAddr: config.wsAddr,
61
+ apiAddr: config.apiAddr,
62
+ platformID: config.platformID,
63
+ });
64
+ clients.set(config.accountId, state);
65
+ api.logger?.info?.(`[openim] account ${config.accountId} connected`);
66
+ // Start portal bridge after successful OpenIM login
67
+ startPortalBridge(api, config);
68
+ }
69
+ catch (e) {
70
+ detachHandlers(state);
71
+ api.logger?.error?.(`[openim] account ${config.accountId} login failed: ${formatSdkError(e)}`);
72
+ }
73
+ }
74
+ export async function stopAccountClient(api, accountId) {
75
+ // Stop portal bridge before disconnecting OpenIM
76
+ stopPortalBridge(api, accountId);
77
+ const state = clients.get(accountId);
78
+ if (!state)
79
+ return;
80
+ clients.delete(accountId);
81
+ detachHandlers(state);
82
+ try {
83
+ await state.sdk.logout();
84
+ }
85
+ catch (e) {
86
+ api.logger?.warn?.(`[openim] account ${accountId} logout failed: ${formatSdkError(e)}`);
87
+ }
88
+ }
89
+ export async function stopAllClients(api) {
90
+ // Stop all portal bridges first
91
+ stopAllPortalBridges(api);
92
+ const items = Array.from(clients.values());
93
+ clients.clear();
94
+ for (const state of items) {
95
+ detachHandlers(state);
96
+ try {
97
+ await state.sdk.logout();
98
+ }
99
+ catch (e) {
100
+ api.logger?.warn?.(`[openim] account ${state.config.accountId} logout failed: ${formatSdkError(e)}`);
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,9 @@
1
+ import type { OpenIMAccountConfig } from "./types";
2
+ export declare function getOpenIMChannelConfig(apiOrCfg: any): any;
3
+ export declare function listAccountIds(apiOrCfg: any): string[];
4
+ export declare function getOpenIMAccountConfig(apiOrCfg: any, accountId?: string): OpenIMAccountConfig | null;
5
+ export declare function listEnabledAccountConfigs(apiOrCfg: any): OpenIMAccountConfig[];
6
+ export declare function resolveAccountConfig(apiOrCfg: any, accountId?: string): {
7
+ accountId: string;
8
+ [k: string]: unknown;
9
+ };
package/dist/config.js ADDED
@@ -0,0 +1,168 @@
1
+ import { toFiniteNumber } from "./utils";
2
+ function getConfigRoot(apiOrCfg) {
3
+ if (apiOrCfg?.config)
4
+ return apiOrCfg.config;
5
+ if (apiOrCfg?.channels)
6
+ return apiOrCfg;
7
+ return globalThis.__openimGatewayConfig ?? {};
8
+ }
9
+ export function getOpenIMChannelConfig(apiOrCfg) {
10
+ const root = getConfigRoot(apiOrCfg);
11
+ return root?.channels?.openim ?? {};
12
+ }
13
+ function decodeJwtPayload(token) {
14
+ const parts = token.split(".");
15
+ if (parts.length < 2)
16
+ return null;
17
+ const payload = parts[1];
18
+ if (!payload)
19
+ return null;
20
+ try {
21
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
22
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
23
+ const json = Buffer.from(padded, "base64").toString("utf8");
24
+ const parsed = JSON.parse(json);
25
+ return parsed && typeof parsed === "object" ? parsed : null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function extractAccountHintsFromToken(token) {
32
+ const payload = decodeJwtPayload(token);
33
+ if (!payload)
34
+ return {};
35
+ const userIDRaw = payload.UserID ?? payload.userID;
36
+ const userID = String(userIDRaw ?? "").trim();
37
+ const platformRaw = payload.PlatformID ?? payload.platformID;
38
+ const platformID = toFiniteNumber(platformRaw, NaN);
39
+ return {
40
+ ...(userID ? { userID } : {}),
41
+ ...(Number.isFinite(platformID) ? { platformID } : {}),
42
+ };
43
+ }
44
+ function envDefaultAccount() {
45
+ const token = String(process.env.OPENIM_TOKEN ?? "").trim();
46
+ const wsAddr = String(process.env.OPENIM_WS_ADDR ?? "").trim();
47
+ const apiAddr = String(process.env.OPENIM_API_ADDR ?? "").trim();
48
+ if (!token || !wsAddr || !apiAddr)
49
+ return null;
50
+ const hints = extractAccountHintsFromToken(token);
51
+ const userID = String(process.env.OPENIM_USER_ID ?? hints.userID ?? "").trim();
52
+ const platformID = toFiniteNumber(process.env.OPENIM_PLATFORM_ID ?? hints.platformID, 5);
53
+ if (!userID)
54
+ return null;
55
+ const botId = String(process.env.OPENIM_BOT_ID ?? "").trim() || undefined;
56
+ const portalWsAddr = String(process.env.OPENIM_PORTAL_WS_ADDR ?? "").trim() || undefined;
57
+ return {
58
+ userID,
59
+ token,
60
+ wsAddr,
61
+ apiAddr,
62
+ platformID,
63
+ enabled: true,
64
+ requireMention: true,
65
+ botId,
66
+ portalWsAddr,
67
+ };
68
+ }
69
+ function normalizeInboundWhitelist(raw) {
70
+ const values = Array.isArray(raw)
71
+ ? raw
72
+ : typeof raw === "string"
73
+ ? raw.split(",")
74
+ : [];
75
+ const normalized = values.map((item) => String(item ?? "").trim()).filter(Boolean);
76
+ return Array.from(new Set(normalized));
77
+ }
78
+ function normalizeAccount(accountId, raw) {
79
+ if (!raw || typeof raw !== "object")
80
+ return null;
81
+ const token = String(raw.token ?? "").trim();
82
+ const wsAddr = String(raw.wsAddr ?? "").trim();
83
+ const apiAddr = String(raw.apiAddr ?? "").trim();
84
+ if (!token || !wsAddr || !apiAddr)
85
+ return null;
86
+ const hints = extractAccountHintsFromToken(token);
87
+ const userID = String(raw.userID ?? hints.userID ?? "").trim();
88
+ const platformID = toFiniteNumber(raw.platformID ?? hints.platformID, 5);
89
+ const enabled = raw.enabled !== false;
90
+ const requireMention = raw.requireMention !== false;
91
+ const inboundWhitelist = normalizeInboundWhitelist(raw.inboundWhitelist);
92
+ const botId = String(raw.botId ?? "").trim() || undefined;
93
+ const portalWsAddr = String(raw.portalWsAddr ?? "").trim() || undefined;
94
+ if (!userID)
95
+ return null;
96
+ return {
97
+ accountId,
98
+ enabled,
99
+ userID,
100
+ token,
101
+ wsAddr,
102
+ apiAddr,
103
+ platformID,
104
+ requireMention,
105
+ inboundWhitelist,
106
+ botId,
107
+ portalWsAddr,
108
+ };
109
+ }
110
+ export function listAccountIds(apiOrCfg) {
111
+ const ch = getOpenIMChannelConfig(apiOrCfg);
112
+ const accounts = ch?.accounts;
113
+ if (accounts && typeof accounts === "object") {
114
+ const ids = Object.keys(accounts);
115
+ if (ids.length > 0)
116
+ return ids;
117
+ }
118
+ if (ch?.userID || ch?.token || ch?.wsAddr || ch?.apiAddr)
119
+ return ["default"];
120
+ if (envDefaultAccount())
121
+ return ["default"];
122
+ return [];
123
+ }
124
+ export function getOpenIMAccountConfig(apiOrCfg, accountId = "default") {
125
+ const ch = getOpenIMChannelConfig(apiOrCfg);
126
+ const accountRaw = ch?.accounts?.[accountId];
127
+ if (accountRaw) {
128
+ return normalizeAccount(accountId, accountRaw);
129
+ }
130
+ if (accountId === "default") {
131
+ if (ch?.userID || ch?.token || ch?.wsAddr || ch?.apiAddr) {
132
+ const normalized = normalizeAccount("default", ch);
133
+ if (normalized)
134
+ return normalized;
135
+ }
136
+ const env = envDefaultAccount();
137
+ if (env) {
138
+ return normalizeAccount("default", env);
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ export function listEnabledAccountConfigs(apiOrCfg) {
144
+ const ids = listAccountIds(apiOrCfg);
145
+ const out = [];
146
+ for (const id of ids) {
147
+ const cfg = getOpenIMAccountConfig(apiOrCfg, id);
148
+ if (cfg && cfg.enabled)
149
+ out.push(cfg);
150
+ }
151
+ return out;
152
+ }
153
+ export function resolveAccountConfig(apiOrCfg, accountId) {
154
+ const id = accountId ?? "default";
155
+ const ch = getOpenIMChannelConfig(apiOrCfg);
156
+ if (ch?.accounts?.[id]) {
157
+ return { accountId: id, ...ch.accounts[id] };
158
+ }
159
+ if (id === "default" && (ch?.userID || ch?.token || ch?.wsAddr || ch?.apiAddr)) {
160
+ return { accountId: id, ...ch };
161
+ }
162
+ if (id === "default") {
163
+ const env = envDefaultAccount();
164
+ if (env)
165
+ return { accountId: id, ...env };
166
+ }
167
+ return { accountId: id };
168
+ }
@@ -0,0 +1,3 @@
1
+ import { type MessageItem } from "@openim/client-sdk";
2
+ import type { OpenIMClientState } from "./types";
3
+ export declare function processInboundMessage(api: any, client: OpenIMClientState, msg: MessageItem): Promise<void>;