@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 +18 -0
- package/README.md +129 -0
- package/README.zh-CN.md +128 -0
- package/dist/channel.d.ts +51 -0
- package/dist/channel.js +74 -0
- package/dist/clients.d.ts +6 -0
- package/dist/clients.js +103 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +168 -0
- package/dist/inbound.d.ts +3 -0
- package/dist/inbound.js +461 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +52 -0
- package/dist/media.d.ts +12 -0
- package/dist/media.js +206 -0
- package/dist/polyfills.d.ts +9 -0
- package/dist/polyfills.js +22 -0
- package/dist/portal.d.ts +11 -0
- package/dist/portal.js +531 -0
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +67 -0
- package/dist/targets.d.ts +6 -0
- package/dist/targets.js +21 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +131 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +31 -0
- package/openclaw.plugin.json +116 -0
- package/package.json +74 -0
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).
|
package/README.zh-CN.md
ADDED
|
@@ -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
|
+
};
|
package/dist/channel.js
ADDED
|
@@ -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>;
|
package/dist/clients.js
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|