@zeyiy/openclaw-channel 0.3.7 → 0.3.9

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/README.md CHANGED
@@ -1,56 +1,46 @@
1
1
  # @zeyiy/openclaw-channel
2
2
 
3
- OpenIM channel plugin for OpenClaw Gateway.
3
+ OpenIM channel plugin for [OpenClaw](https://openclaw.ai) Gateway.
4
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)
5
+ Connects OpenClaw agents to the [OpenIM](https://www.openim.io/) messaging platform, enabling AI-powered conversations in direct chats and group chats.
8
6
 
9
7
  ## Features
10
8
 
11
9
  - 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
10
+ - Text / image / file / video message send & receive
11
+ - Quote/reply message context parsing
12
+ - Multi-account concurrent login
13
+ - Group trigger with optional mention-only mode
14
+ - Per-user session isolation (direct) / shared session (group)
17
15
  - Auto read-receipt for direct messages
18
- - Per-user session isolation (direct chat) / shared session (group chat)
19
- - Agent Portal Bridge — persistent WebSocket connection to agent-portal cloud service
20
- - Interactive setup command: `openclaw openim setup`
16
+ - Inbound sender whitelist (optional)
17
+ - **Agent Portal Bridge** — persistent WebSocket to agent-portal for remote agent management
18
+ - Interactive setup: `openclaw openim setup`
21
19
 
22
20
  ## Installation
23
21
 
24
- Install from npm:
25
-
26
22
  ```bash
27
23
  openclaw plugins install @zeyiy/openclaw-channel
28
24
  ```
29
25
 
30
- Or install from local path:
31
-
32
- ```bash
33
- openclaw plugins install /path/to/openclaw-channel
34
- ```
35
-
36
- Repository: https://github.com/ZeyiY/openclaw-channel
37
-
38
26
  ## Identity Mapping
39
27
 
40
- - npm package name: `@zeyiy/openclaw-channel`
41
- - plugin id: `openclaw-channel` (used in `plugins.entries` and `plugins.allow`)
42
- - channel id: `openim` (used in `channels.openim`)
43
- - setup command: `openclaw openim setup`
28
+ | Item | Value |
29
+ |------|-------|
30
+ | npm package | `@zeyiy/openclaw-channel` |
31
+ | plugin id | `openclaw-channel` |
32
+ | channel id | `openim` |
33
+ | setup command | `openclaw openim setup` |
44
34
 
45
35
  ## Configuration
46
36
 
47
- ### Option 1: Interactive setup (recommended)
37
+ ### Interactive setup (recommended)
48
38
 
49
39
  ```bash
50
40
  openclaw openim setup
51
41
  ```
52
42
 
53
- ### Option 2: Edit `~/.openclaw/openclaw.json`
43
+ ### Manual edit `~/.openclaw/openclaw.json`
54
44
 
55
45
  ```json
56
46
  {
@@ -59,11 +49,11 @@ openclaw openim setup
59
49
  "accounts": {
60
50
  "default": {
61
51
  "enabled": true,
62
- "token": "your_token",
63
- "wsAddr": "ws://127.0.0.1:10001",
64
- "apiAddr": "http://127.0.0.1:10002",
65
- "botId": "my-bot-001",
66
- "portalWsAddr": "wss://portal.example.com/ws"
52
+ "token": "your_jwt_token",
53
+ "wsAddr": "wss://your-openim-server/msg_gateway",
54
+ "apiAddr": "https://your-openim-server/api",
55
+ "botId": "your-bot-id",
56
+ "portalWsAddr": "wss://agent-portal.example.com/ws/workspace"
67
57
  }
68
58
  }
69
59
  }
@@ -71,87 +61,70 @@ openclaw openim setup
71
61
  }
72
62
  ```
73
63
 
74
- `userID` and `platformID` are optional. If omitted, they are auto-derived from JWT token claims (`UserID` and `PlatformID`).
75
-
76
- `requireMention` is optional and defaults to `true`.
64
+ ### Field reference
77
65
 
78
- `inboundWhitelist` is optional. If omitted or empty, inbound handling keeps existing behavior.
79
- If set, only these users can trigger processing:
80
- - direct messages to the account
81
- - group messages where they `@` the account
66
+ | Field | Required | Description |
67
+ |-------|----------|-------------|
68
+ | `token` | Yes | OpenIM JWT token |
69
+ | `wsAddr` | Yes | OpenIM WebSocket endpoint |
70
+ | `apiAddr` | Yes | OpenIM REST API endpoint |
71
+ | `userID` | No | Auto-derived from JWT `UserID` claim |
72
+ | `platformID` | No | Auto-derived from JWT `PlatformID` claim |
73
+ | `enabled` | No | Default `true` |
74
+ | `requireMention` | No | Require @mention in groups, default `true` |
75
+ | `inboundWhitelist` | No | Only process messages from listed user IDs |
76
+ | `botId` | No | Bot ID for agent-portal connection |
77
+ | `portalWsAddr` | No | Agent-portal WebSocket endpoint |
78
+ | `historyLimit` | No | Chat history context limit, default `20` |
82
79
 
83
- `botId` and `portalWsAddr` are optional. When both are set, the plugin establishes a WebSocket connection to the agent-portal cloud service, enabling remote management of agents, files, and models.
80
+ Single-account shorthand (without `accounts` wrapper) is also supported.
84
81
 
85
- Single-account fallback (without `accounts`) is supported.
86
-
87
- Environment fallback is supported for the `default` account:
88
-
89
- - `OPENIM_TOKEN`
90
- - `OPENIM_WS_ADDR`
91
- - `OPENIM_API_ADDR`
92
-
93
- Optional env overrides:
94
-
95
- - `OPENIM_USER_ID`
96
- - `OPENIM_PLATFORM_ID`
82
+ > **Note:** Environment variable fallback has been removed. All configuration is via `openclaw.json`.
97
83
 
98
84
  ## Agent Tools
99
85
 
100
- - `openim_send_text`
101
- - `target`: `user:<id>` or `group:<id>`
102
- - `text`: message text
103
- - `accountId` (optional): select sending account
104
-
105
- - `openim_send_image`
106
- - `target`: `user:<id>` or `group:<id>`
107
- - `image`: local path (`file://` supported) or `http(s)` URL
108
- - `accountId` (optional): select sending account
109
-
110
- - `openim_send_video`
111
- - `target`: `user:<id>` or `group:<id>`
112
- - `video`: local path (`file://` supported) or `http(s)` URL
113
- - behavior: sent as a file message (not OpenIM video message)
114
- - `name` (optional): override filename for URL input
115
- - `accountId` (optional): select sending account
116
-
117
- - `openim_send_file`
118
- - `target`: `user:<id>` or `group:<id>`
119
- - `file`: local path (`file://` supported) or `http(s)` URL
120
- - `name` (optional): override filename for URL input
121
- - `accountId` (optional): select sending account
86
+ | Tool | Parameters | Description |
87
+ |------|-----------|-------------|
88
+ | `openim_send_text` | `target`, `text`, `accountId?` | Send text message |
89
+ | `openim_send_image` | `target`, `image`, `accountId?` | Send image (local path or URL) |
90
+ | `openim_send_file` | `target`, `file`, `name?`, `accountId?` | Send file (local path or URL) |
91
+ | `openim_send_video` | `target`, `video`, `name?`, `accountId?` | Send video as file message |
92
+
93
+ `target` format: `user:<id>` or `group:<id>`
122
94
 
123
95
  ## Agent Portal Bridge
124
96
 
125
- When `botId` and `portalWsAddr` are configured, the plugin connects to the agent-portal cloud service via WebSocket. The portal can remotely invoke the following methods:
97
+ When `botId` and `portalWsAddr` are configured, the plugin maintains a WebSocket connection to agent-portal, enabling remote management:
126
98
 
127
99
  | Method | Description |
128
- |---|---|
129
- | `bot.agent.get` | Resolve the agentId bound to the current bot |
130
- | `models.list` | List available models from config |
131
- | `agents.list` | List all configured agents |
132
- | `agents.create` | Create a new agent with workspace |
133
- | `agents.files.list` | List workspace files for an agent |
134
- | `agents.files.get` | Read a single workspace file |
135
- | `agents.files.set` | Write a file to agent workspace |
100
+ |--------|-------------|
101
+ | `bot.agent.get` | Resolve bot's bound agent |
102
+ | `models.list` | List available models |
103
+ | `agents.list` | List configured agents |
104
+ | `agents.create` | Create new agent + workspace |
105
+ | `agents.files.list` | List agent workspace files |
106
+ | `agents.files.get` | Read workspace file |
107
+ | `agents.files.set` | Write workspace file |
136
108
  | `tools.catalog` | List available tools |
137
- | `skills.status` | List installed skills/plugins status |
138
- | `skills.search` | Search ClawHub for skills (placeholder) |
139
- | `skills.detail` | Get detail for a specific skill |
140
- | `cron.list` | List configured cron jobs |
109
+ | `skills.status` | Skill/plugin status |
110
+ | `agent.skills.status` | Per-agent skill status with whitelist |
111
+ | `agent.skills.set` | Enable/disable skill for agent |
112
+ | `agent.model.set` | Switch agent model |
113
+ | `skills.search` | Search ClawHub registry |
114
+ | `skills.install` | Install skill from ClawHub or URL |
115
+ | `skills.set` | Enable/disable skill globally |
116
+ | `cron.list` | List cron jobs |
141
117
 
142
- The connection features automatic reconnect with exponential backoff and heartbeat keepalive.
118
+ Features auto-reconnect with exponential backoff and heartbeat keepalive.
143
119
 
144
120
  ## Development
145
121
 
146
122
  ```bash
123
+ pnpm install
147
124
  pnpm run build
148
- pnpm run test:connect
125
+ pnpm run test:connect # configure .env first
149
126
  ```
150
127
 
151
- For `test:connect`, configure `.env` first (see `.env.example`).
152
-
153
128
  ## License
154
129
 
155
- AGPL-3.0-only. See [LICENSE](https://github.com/ZeyiY/openclaw-channel/blob/main/LICENSE).
156
-
157
- Originally developed by [openimsdk](https://github.com/openimsdk/openclaw-channel).
130
+ AGPL-3.0-only. See [LICENSE](./LICENSE).
package/README.zh-CN.md CHANGED
@@ -1,56 +1,46 @@
1
1
  # @zeyiy/openclaw-channel
2
2
 
3
- OpenClaw Gateway 的 OpenIM 渠道插件。
3
+ [OpenClaw](https://openclaw.ai) Gateway 的 OpenIM 渠道插件。
4
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)
5
+ OpenClaw 智能体接入 [OpenIM](https://www.openim.io/) 即时通讯平台,支持私聊和群聊中的 AI 对话。
8
6
 
9
7
  ## 功能
10
8
 
11
9
  - 支持私聊与群聊
12
- - 支持文本/图片/文件消息的收发
13
- - `openim_send_video` 按文件消息发送(不使用 OpenIM 视频消息)
14
- - 支持引用消息解析(用于入站上下文)
15
- - 支持多账号并发(`channels.openim.accounts.<id>`)
16
- - 支持群聊仅 @ 触发
17
- - 私聊消息自动标记已读(已读回执)
10
+ - 文本 / 图片 / 文件 / 视频消息收发
11
+ - 引用消息上下文解析
12
+ - 多账号并发登录
13
+ - 群聊 @触发模式(可选)
18
14
  - 每用户独立会话(私聊)/ 同群共享会话(群聊)
19
- - Agent Portal Bridge — 与 agent-portal 云服务保持 WebSocket 长连接,支持远程管理
20
- - 提供交互式配置命令:`openclaw openim setup`
15
+ - 私聊自动已读回执
16
+ - 入站发送者白名单(可选)
17
+ - **Agent Portal Bridge** — 与 agent-portal 云服务保持 WebSocket 长连接,支持远程管理
18
+ - 交互式配置命令:`openclaw openim setup`
21
19
 
22
20
  ## 安装
23
21
 
24
- 从 npm 安装:
25
-
26
22
  ```bash
27
23
  openclaw plugins install @zeyiy/openclaw-channel
28
24
  ```
29
25
 
30
- 本地路径安装:
31
-
32
- ```bash
33
- openclaw plugins install /path/to/openclaw-channel
34
- ```
35
-
36
- 仓库地址:https://github.com/ZeyiY/openclaw-channel
37
-
38
26
  ## 标识说明
39
27
 
40
- - npm 包名:`@zeyiy/openclaw-channel`
41
- - 插件 id:`openclaw-channel`(用于 `plugins.entries` / `plugins.allow`)
42
- - 渠道 id:`openim`(用于 `channels.openim`)
43
- - 配置命令:`openclaw openim setup`
28
+ | 项目 | 值 |
29
+ |------|-----|
30
+ | npm 包名 | `@zeyiy/openclaw-channel` |
31
+ | 插件 ID | `openclaw-channel` |
32
+ | 渠道 ID | `openim` |
33
+ | 配置命令 | `openclaw openim setup` |
44
34
 
45
35
  ## 配置
46
36
 
47
- ### 方式一:交互式配置(推荐)
37
+ ### 交互式配置(推荐)
48
38
 
49
39
  ```bash
50
40
  openclaw openim setup
51
41
  ```
52
42
 
53
- ### 方式二:手动编辑 `~/.openclaw/openclaw.json`
43
+ ### 手动编辑 `~/.openclaw/openclaw.json`
54
44
 
55
45
  ```json
56
46
  {
@@ -59,11 +49,11 @@ openclaw openim setup
59
49
  "accounts": {
60
50
  "default": {
61
51
  "enabled": true,
62
- "token": "your_token",
63
- "wsAddr": "ws://127.0.0.1:10001",
64
- "apiAddr": "http://127.0.0.1:10002",
65
- "botId": "my-bot-001",
66
- "portalWsAddr": "wss://portal.example.com/ws"
52
+ "token": "your_jwt_token",
53
+ "wsAddr": "wss://your-openim-server/msg_gateway",
54
+ "apiAddr": "https://your-openim-server/api",
55
+ "botId": "your-bot-id",
56
+ "portalWsAddr": "wss://agent-portal.example.com/ws/workspace"
67
57
  }
68
58
  }
69
59
  }
@@ -71,86 +61,70 @@ openclaw openim setup
71
61
  }
72
62
  ```
73
63
 
74
- `userID` 和 `platformID` 为可选项,未填写时会自动从 JWT token 的 `UserID` / `PlatformID` 声明解析。
75
-
76
- `requireMention` 为可选项,默认 `true`。
64
+ ### 字段说明
77
65
 
78
- `inboundWhitelist` 为可选项,不填或为空时保持当前逻辑;填了后仅处理白名单用户触发的消息:
79
- - 给账号发单聊消息
80
- - 在群里 @ 账号的消息
66
+ | 字段 | 必填 | 说明 |
67
+ |------|------|------|
68
+ | `token` | 是 | OpenIM JWT token |
69
+ | `wsAddr` | 是 | OpenIM WebSocket 端点 |
70
+ | `apiAddr` | 是 | OpenIM REST API 端点 |
71
+ | `userID` | 否 | 自动从 JWT `UserID` 声明解析 |
72
+ | `platformID` | 否 | 自动从 JWT `PlatformID` 声明解析 |
73
+ | `enabled` | 否 | 默认 `true` |
74
+ | `requireMention` | 否 | 群聊需 @触发,默认 `true` |
75
+ | `inboundWhitelist` | 否 | 仅处理指定用户 ID 的消息 |
76
+ | `botId` | 否 | 用于 agent-portal 连接的 Bot ID |
77
+ | `portalWsAddr` | 否 | Agent-portal WebSocket 端点 |
78
+ | `historyLimit` | 否 | 聊天历史上下文条数,默认 `20` |
81
79
 
82
- `botId` `portalWsAddr` 为可选项。同时配置后,插件会与 agent-portal 云服务建立 WebSocket 连接,支持远程管理 agent、文件和模型。
80
+ 支持单账号简写(不使用 `accounts` 包装)。
83
81
 
84
- 支持单账号兜底写法(不使用 `accounts`)。
85
-
86
- `default` 账号支持环境变量兜底:
87
-
88
- - `OPENIM_TOKEN`
89
- - `OPENIM_WS_ADDR`
90
- - `OPENIM_API_ADDR`
91
-
92
- 可选环境变量覆盖项:
93
-
94
- - `OPENIM_USER_ID`
95
- - `OPENIM_PLATFORM_ID`
82
+ > **注意:** 已移除环境变量配置方式,所有配置通过 `openclaw.json` 完成。
96
83
 
97
84
  ## Agent 工具
98
85
 
99
- - `openim_send_text`
100
- - `target`: `user:<id>` 或 `group:<id>`
101
- - `text`: 文本内容
102
- - `accountId`(可选):指定发送账号
103
-
104
- - `openim_send_image`
105
- - `target`: `user:<id>` 或 `group:<id>`
106
- - `image`: 本地路径(支持 `file://`)或 `http(s)` URL
107
- - `accountId`(可选):指定发送账号
108
-
109
- - `openim_send_video`
110
- - `target`: `user:<id>` 或 `group:<id>`
111
- - `video`: 本地路径(支持 `file://`)或 `http(s)` URL
112
- - 行为:按文件消息发送(不是视频消息)
113
- - `name`(可选):URL 输入时覆盖文件名
114
- - `accountId`(可选):指定发送账号
115
-
116
- - `openim_send_file`
117
- - `target`: `user:<id>` 或 `group:<id>`
118
- - `file`: 本地路径(支持 `file://`)或 `http(s)` URL
119
- - `name`(可选):URL 输入时覆盖文件名
120
- - `accountId`(可选):指定发送账号
86
+ | 工具 | 参数 | 说明 |
87
+ |------|------|------|
88
+ | `openim_send_text` | `target`, `text`, `accountId?` | 发送文本消息 |
89
+ | `openim_send_image` | `target`, `image`, `accountId?` | 发送图片(本地路径或 URL) |
90
+ | `openim_send_file` | `target`, `file`, `name?`, `accountId?` | 发送文件(本地路径或 URL) |
91
+ | `openim_send_video` | `target`, `video`, `name?`, `accountId?` | 发送视频(按文件消息发送) |
92
+
93
+ `target` 格式:`user:<id>` `group:<id>`
121
94
 
122
95
  ## Agent Portal Bridge
123
96
 
124
- 配置 `botId` 和 `portalWsAddr` 后,插件会通过 WebSocket 连接到 agent-portal 云服务。Portal 可远程调用以下方法:
97
+ 配置 `botId` 和 `portalWsAddr` 后,插件与 agent-portal 保持 WebSocket 连接,支持远程管理:
125
98
 
126
99
  | 方法 | 说明 |
127
- |---|---|
128
- | `bot.agent.get` | 获取当前 bot 绑定的 agentId |
129
- | `models.list` | 列出配置中的可用模型 |
130
- | `agents.list` | 列出所有已配置的 agent |
131
- | `agents.create` | 创建新 agent 及工作空间 |
132
- | `agents.files.list` | 列出 agent 工作空间文件 |
133
- | `agents.files.get` | 读取单个工作空间文件 |
134
- | `agents.files.set` | 写入文件到 agent 工作空间 |
100
+ |------|------|
101
+ | `bot.agent.get` | 获取 bot 绑定的 agent |
102
+ | `models.list` | 列出可用模型 |
103
+ | `agents.list` | 列出已配置的 agent |
104
+ | `agents.create` | 创建 agent + 工作空间 |
105
+ | `agents.files.list` | 列出工作空间文件 |
106
+ | `agents.files.get` | 读取工作空间文件 |
107
+ | `agents.files.set` | 写入工作空间文件 |
135
108
  | `tools.catalog` | 列出可用工具 |
136
- | `skills.status` | 列出已安装技能/插件状态 |
137
- | `skills.search` | 搜索 ClawHub 技能(占位) |
138
- | `skills.detail` | 获取特定技能详情 |
139
- | `cron.list` | 列出已配置的定时任务 |
109
+ | `skills.status` | 技能/插件状态 |
110
+ | `agent.skills.status` | Agent 级技能状态(含白名单) |
111
+ | `agent.skills.set` | 启用/禁用 agent 技能 |
112
+ | `agent.model.set` | 切换 agent 模型 |
113
+ | `skills.search` | 搜索 ClawHub 技能 |
114
+ | `skills.install` | 从 ClawHub 或 URL 安装技能 |
115
+ | `skills.set` | 全局启用/禁用技能 |
116
+ | `cron.list` | 列出定时任务 |
140
117
 
141
- 连接支持指数退避自动重连和心跳保活。
118
+ 支持指数退避自动重连和心跳保活。
142
119
 
143
120
  ## 开发
144
121
 
145
122
  ```bash
123
+ pnpm install
146
124
  pnpm run build
147
- pnpm run test:connect
125
+ pnpm run test:connect # 先配置 .env
148
126
  ```
149
127
 
150
- 运行 `test:connect` 前请先配置 `.env`(参考 `.env.example`)。
151
-
152
128
  ## 许可证
153
129
 
154
- 本项目采用 `AGPL-3.0-only` 许可证。详见 [LICENSE](https://github.com/ZeyiY/openclaw-channel/blob/main/LICENSE)。
155
-
156
- 原始项目由 [openimsdk](https://github.com/openimsdk/openclaw-channel) 开发。
130
+ AGPL-3.0-only,详见 [LICENSE](./LICENSE)。
package/dist/clients.js CHANGED
@@ -3,6 +3,7 @@ import { processInboundMessage } from "./inbound";
3
3
  import { startPortalBridge, stopPortalBridge, stopAllPortalBridges } from "./portal";
4
4
  import { formatSdkError } from "./utils";
5
5
  const clients = new Map();
6
+ const loginInProgress = new Set();
6
7
  function detachHandlers(state) {
7
8
  state.sdk.off(CbEvents.OnRecvNewMessage, state.handlers.onRecvNewMessage);
8
9
  state.sdk.off(CbEvents.OnRecvNewMessages, state.handlers.onRecvNewMessages);
@@ -21,6 +22,26 @@ export function connectedClientCount() {
21
22
  return clients.size;
22
23
  }
23
24
  export async function startAccountClient(api, config) {
25
+ if (clients.has(config.accountId) || loginInProgress.has(config.accountId)) {
26
+ api.logger?.debug?.(`[openim] account ${config.accountId} already connected or login in progress, skipping`);
27
+ return;
28
+ }
29
+ loginInProgress.add(config.accountId);
30
+ // Diagnostic: test raw WebSocket before SDK login
31
+ const WS = globalThis.WebSocket;
32
+ api.logger?.info?.(`[openim] WebSocket global: ${typeof WS}, constructor: ${WS?.name ?? "N/A"}`);
33
+ try {
34
+ const testWs = new WS(config.wsAddr);
35
+ await new Promise((resolve, reject) => {
36
+ const timer = setTimeout(() => { testWs.close(); reject(new Error("timeout")); }, 5000);
37
+ testWs.onopen = () => { clearTimeout(timer); testWs.close(); resolve(); };
38
+ testWs.onerror = (e) => { clearTimeout(timer); reject(e); };
39
+ });
40
+ api.logger?.info?.(`[openim] raw WebSocket test to ${config.wsAddr}: OK`);
41
+ }
42
+ catch (e) {
43
+ api.logger?.error?.(`[openim] raw WebSocket test to ${config.wsAddr}: FAILED — ${e?.message ?? e}`);
44
+ }
24
45
  const sdk = getSDK();
25
46
  const state = {
26
47
  sdk,
@@ -62,14 +83,16 @@ export async function startAccountClient(api, config) {
62
83
  platformID: config.platformID,
63
84
  });
64
85
  clients.set(config.accountId, state);
86
+ loginInProgress.delete(config.accountId);
65
87
  api.logger?.info?.(`[openim] account ${config.accountId} connected`);
66
- // Start portal bridge after successful OpenIM login
67
- startPortalBridge(api, config);
68
88
  }
69
89
  catch (e) {
70
90
  detachHandlers(state);
91
+ loginInProgress.delete(config.accountId);
71
92
  api.logger?.error?.(`[openim] account ${config.accountId} login failed: ${formatSdkError(e)}`);
72
93
  }
94
+ // Start portal bridge regardless of SDK login result — portal is an independent service
95
+ startPortalBridge(api, config);
73
96
  }
74
97
  export async function stopAccountClient(api, accountId) {
75
98
  // Stop portal bridge before disconnecting OpenIM
package/dist/config.js CHANGED
@@ -41,32 +41,6 @@ function extractAccountHintsFromToken(token) {
41
41
  ...(Number.isFinite(platformID) ? { platformID } : {}),
42
42
  };
43
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
- historyLimit: 20,
66
- botId,
67
- portalWsAddr,
68
- };
69
- }
70
44
  function normalizeInboundWhitelist(raw) {
71
45
  const values = Array.isArray(raw)
72
46
  ? raw
@@ -120,8 +94,6 @@ export function listAccountIds(apiOrCfg) {
120
94
  }
121
95
  if (ch?.userID || ch?.token || ch?.wsAddr || ch?.apiAddr)
122
96
  return ["default"];
123
- if (envDefaultAccount())
124
- return ["default"];
125
97
  return [];
126
98
  }
127
99
  export function getOpenIMAccountConfig(apiOrCfg, accountId = "default") {
@@ -136,10 +108,6 @@ export function getOpenIMAccountConfig(apiOrCfg, accountId = "default") {
136
108
  if (normalized)
137
109
  return normalized;
138
110
  }
139
- const env = envDefaultAccount();
140
- if (env) {
141
- return normalizeAccount("default", env);
142
- }
143
111
  }
144
112
  return null;
145
113
  }
@@ -162,10 +130,5 @@ export function resolveAccountConfig(apiOrCfg, accountId) {
162
130
  if (id === "default" && (ch?.userID || ch?.token || ch?.wsAddr || ch?.apiAddr)) {
163
131
  return { accountId: id, ...ch };
164
132
  }
165
- if (id === "default") {
166
- const env = envDefaultAccount();
167
- if (env)
168
- return { accountId: id, ...env };
169
- }
170
133
  return { accountId: id };
171
134
  }
@@ -1,9 +1 @@
1
- declare class NodeFileReaderPolyfill {
2
- result: ArrayBuffer | null;
3
- error: Error | null;
4
- onload: ((ev: {
5
- target: NodeFileReaderPolyfill;
6
- }) => void) | null;
7
- onerror: ((err: unknown) => void) | null;
8
- readAsArrayBuffer(blob: Blob): void;
9
- }
1
+ export {};
package/dist/polyfills.js CHANGED
@@ -1,4 +1,15 @@
1
- "use strict";
1
+ import WsWebSocket from "ws";
2
+ // Force ws library over native WebSocket — OpenIM WASM SDK is incompatible
3
+ // with Node.js native WebSocket (undici-based), causing instant "network error".
4
+ globalThis.WebSocket = WsWebSocket;
5
+ // ws library sends Buffer for binary messages; OpenIM SDK calls .arrayBuffer() expecting a Blob.
6
+ // Polyfill Buffer.prototype.arrayBuffer so it returns Promise<ArrayBuffer>.
7
+ if (typeof Buffer !== "undefined" && !Buffer.prototype.arrayBuffer) {
8
+ Buffer.prototype.arrayBuffer = function () {
9
+ const ab = this.buffer.slice(this.byteOffset, this.byteOffset + this.byteLength);
10
+ return Promise.resolve(ab);
11
+ };
12
+ }
2
13
  class NodeFileReaderPolyfill {
3
14
  result = null;
4
15
  error = null;
package/dist/portal.js CHANGED
@@ -6,9 +6,10 @@
6
6
  * Lifecycle is tied to the OpenIM account: starts/stops alongside the account.
7
7
  */
8
8
  import { readFile, writeFile, stat, mkdir, unlink, rm } from "node:fs/promises";
9
- import { accessSync, readdirSync, realpathSync, existsSync, readFileSync, statSync, constants as fsConstants } from "node:fs";
9
+ import { readdirSync, realpathSync, existsSync, readFileSync, statSync } from "node:fs";
10
+ import WsWebSocket from "ws";
10
11
  import { resolve, join, dirname, basename } from "node:path";
11
- import { execFile } from "node:child_process";
12
+ import { execFile, execFileSync } from "node:child_process";
12
13
  import { promisify } from "node:util";
13
14
  import { tmpdir, homedir } from "node:os";
14
15
  const bridges = new Map();
@@ -51,7 +52,7 @@ function resolveDefaultAgentId(cfg) {
51
52
  }
52
53
  /** Expand leading ~ to $HOME, then resolve to absolute path. */
53
54
  function resolveUserPath(p) {
54
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
55
+ const home = homedir() || "";
55
56
  if (p.startsWith("~/") || p === "~") {
56
57
  return resolve(home, p.slice(2));
57
58
  }
@@ -65,7 +66,7 @@ function resolveAgentWorkspaceDir(cfg, agentId) {
65
66
  return resolveUserPath(entry.workspace.trim());
66
67
  const fallback = cfg.agents?.defaults?.workspace?.trim();
67
68
  const defaultId = resolveDefaultAgentId(cfg);
68
- const home = process.env.HOME ?? process.cwd();
69
+ const home = homedir() || process.cwd();
69
70
  if (id === defaultId) {
70
71
  if (fallback)
71
72
  return resolveUserPath(fallback);
@@ -498,25 +499,8 @@ function handleToolsCatalog(api, params) {
498
499
  * Resolve the openclaw state directory root.
499
500
  * Mirrors gateway's resolveStateDir: OPENCLAW_STATE_DIR > dirname(OPENCLAW_CONFIG_PATH) > ~/.openclaw
500
501
  */
501
- /** Resolve the effective home directory, matching gateway's resolveRequiredHomeDir priority:
502
- * OPENCLAW_HOME → process.env.HOME → process.env.USERPROFILE → os.homedir() → cwd */
502
+ /** Resolve the effective home directory. */
503
503
  function resolveEffectiveHomeDir() {
504
- const openclawHome = (process.env.OPENCLAW_HOME ?? "").trim();
505
- if (openclawHome && openclawHome !== "undefined" && openclawHome !== "null") {
506
- // Support ~/... prefix expansion using os.homedir()
507
- if (openclawHome === "~" || openclawHome.startsWith("~/") || openclawHome.startsWith("~\\")) {
508
- const osHome = process.env.HOME || process.env.USERPROFILE || homedir();
509
- if (osHome)
510
- return resolve(openclawHome.replace(/^~(?=$|[\\/])/, osHome));
511
- }
512
- return resolve(openclawHome);
513
- }
514
- const envHome = (process.env.HOME ?? "").trim();
515
- if (envHome)
516
- return resolve(envHome);
517
- const userProfile = (process.env.USERPROFILE ?? "").trim();
518
- if (userProfile)
519
- return resolve(userProfile);
520
504
  try {
521
505
  return resolve(homedir());
522
506
  }
@@ -524,47 +508,26 @@ function resolveEffectiveHomeDir() {
524
508
  return resolve(process.cwd());
525
509
  }
526
510
  function resolveStateDir() {
527
- const stateDir = (process.env.OPENCLAW_STATE_DIR ?? "").trim();
528
- if (stateDir)
529
- return resolve(stateDir.startsWith("~") ? stateDir.replace(/^~(?=$|[\\/])/, resolveEffectiveHomeDir()) : stateDir);
530
- const configPath = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
531
- if (configPath)
532
- return resolve(dirname(configPath.startsWith("~") ? configPath.replace(/^~(?=$|[\\/])/, resolveEffectiveHomeDir()) : configPath));
533
511
  return join(resolveEffectiveHomeDir(), ".openclaw");
534
512
  }
535
- /**
536
- * Resolve the openclaw config file path.
537
- * Mirrors gateway's resolveCanonicalConfigPath: OPENCLAW_CONFIG_PATH > {stateDir}/openclaw.json
538
- */
539
513
  function resolveOpenClawConfigPath() {
540
- const configPathOverride = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
541
- if (configPathOverride)
542
- return resolveUserPath(configPathOverride);
543
514
  return join(resolveStateDir(), "openclaw.json");
544
515
  }
545
- /** Check if a binary is executable on the system PATH. Mirrors gateway's hasBinary. */
516
+ /** Check if a binary is executable on the system PATH. */
546
517
  const _binaryCache = new Map();
547
518
  function hasBinarySync(bin) {
548
519
  const cached = _binaryCache.get(bin);
549
520
  if (cached !== undefined)
550
521
  return cached;
551
- const delimiter = process.platform === "win32" ? ";" : ":";
552
- const parts = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
553
- const extensions = process.platform === "win32"
554
- ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
555
- : [""];
556
- for (const part of parts) {
557
- for (const ext of extensions) {
558
- try {
559
- accessSync(join(part, bin + ext), fsConstants.X_OK);
560
- _binaryCache.set(bin, true);
561
- return true;
562
- }
563
- catch { /* keep searching */ }
564
- }
522
+ try {
523
+ execFileSync("which", [bin], { stdio: "ignore" });
524
+ _binaryCache.set(bin, true);
525
+ return true;
526
+ }
527
+ catch {
528
+ _binaryCache.set(bin, false);
529
+ return false;
565
530
  }
566
- _binaryCache.set(bin, false);
567
- return false;
568
531
  }
569
532
  /**
570
533
  * Relaxed JSON5 parser: removes trailing commas before } or ].
@@ -767,9 +730,6 @@ function loadSkillsFromDir(dir, source) {
767
730
  * Searches multiple known installation paths (npm, pnpm, nvm, etc.)
768
731
  */
769
732
  function resolveBundledSkillsDir() {
770
- const override = (process.env.OPENCLAW_BUNDLED_SKILLS_DIR ?? "").trim();
771
- if (override)
772
- return override;
773
733
  const candidates = [];
774
734
  // 1. Adjacent to node binary (nvm-style installs)
775
735
  try {
@@ -789,7 +749,7 @@ function resolveBundledSkillsDir() {
789
749
  }
790
750
  catch { }
791
751
  // 4. ~/.npm-global/lib/node_modules/openclaw/skills (common npm prefix)
792
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
752
+ const home = homedir() || "";
793
753
  if (home) {
794
754
  candidates.push(join(home, ".npm-global", "lib", "node_modules", "openclaw", "skills"));
795
755
  }
@@ -969,7 +929,7 @@ function buildSkillStatus(entry, cfg) {
969
929
  const requiresAnyBins = entry.metadata?.requires?.anyBins ?? [];
970
930
  const requiresEnv = entry.metadata?.requires?.env ?? [];
971
931
  const requiresConfig = entry.metadata?.requires?.config ?? [];
972
- const isEnvSatisfied = (name) => Boolean(process.env[name] || skillCfg?.env?.[name] ||
932
+ const isEnvSatisfied = (name) => Boolean(skillCfg?.env?.[name] ||
973
933
  (skillCfg?.apiKey && entry.metadata?.primaryEnv === name));
974
934
  const missingBins = always ? [] : requiresBins.filter(b => !hasBinarySync(b));
975
935
  const missingAnyBins = always ? [] :
@@ -1022,9 +982,7 @@ function buildSkillStatus(entry, cfg) {
1022
982
  * Mirrors gateway's fetchJson (clawhub-t8tftw_j.js).
1023
983
  */
1024
984
  async function fetchClawHub(path, searchParams) {
1025
- const baseUrl = ((process.env.OPENCLAW_CLAWHUB_URL ?? "").trim() ||
1026
- (process.env.CLAWHUB_URL ?? "").trim() ||
1027
- "https://clawhub.ai").replace(/\/+$/, "");
985
+ const baseUrl = "https://clawhub.ai";
1028
986
  let url = `${baseUrl}${path}`;
1029
987
  if (searchParams && Object.keys(searchParams).length > 0) {
1030
988
  url += `?${new URLSearchParams(searchParams).toString()}`;
@@ -1289,7 +1247,7 @@ async function handleSkillsInstall(api, params) {
1289
1247
  let archiveBytes;
1290
1248
  try {
1291
1249
  const searchParams = { version };
1292
- const archiveUrl = ((process.env.OPENCLAW_CLAWHUB_URL ?? process.env.CLAWHUB_URL ?? "https://clawhub.ai").replace(/\/+$/, "")
1250
+ const archiveUrl = ("https://clawhub.ai"
1293
1251
  + `/api/v1/packages/${encodeURIComponent(slug)}/download?` + new URLSearchParams(searchParams));
1294
1252
  const controller = new AbortController();
1295
1253
  const timer = setTimeout(() => controller.abort(), 60000);
@@ -1535,22 +1493,14 @@ async function handleAgentModelSet(api, params) {
1535
1493
  * 4. Default: ~/.openclaw/cron/jobs.json
1536
1494
  */
1537
1495
  function resolveCronStorePath(api) {
1538
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1496
+ const home = homedir() || "";
1539
1497
  const expandHome = (p) => p.startsWith("~/") || p === "~" ? join(home, p.slice(2)) : p;
1540
- // 1. OPENCLAW_STATE_DIR
1541
- const stateDir = (process.env.OPENCLAW_STATE_DIR ?? "").trim();
1542
- if (stateDir)
1543
- return resolve(join(expandHome(stateDir), "cron", "jobs.json"));
1544
- // 2. OPENCLAW_CONFIG_PATH
1545
- const configPath = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
1546
- if (configPath)
1547
- return resolve(join(dirname(expandHome(configPath)), "cron", "jobs.json"));
1548
- // 3. cfg.cron?.store
1498
+ // cfg.cron?.store
1549
1499
  const cfg = getConfig(api);
1550
1500
  const cfgStore = String(cfg?.cron?.store ?? "").trim();
1551
1501
  if (cfgStore)
1552
1502
  return resolve(expandHome(cfgStore));
1553
- // 4. Default
1503
+ // Default
1554
1504
  return join(home, ".openclaw", "cron", "jobs.json");
1555
1505
  }
1556
1506
  /**
@@ -1704,7 +1654,8 @@ async function handlePortalRequest(api, accountId, request) {
1704
1654
  // WebSocket connection management (unchanged logic)
1705
1655
  // ---------------------------------------------------------------------------
1706
1656
  function sendResponse(ws, response) {
1707
- if (ws.readyState === WebSocket.OPEN) {
1657
+ // readyState 1 === OPEN for both native WebSocket and ws library
1658
+ if (ws.readyState === 1) {
1708
1659
  ws.send(JSON.stringify(response));
1709
1660
  }
1710
1661
  }
@@ -1713,9 +1664,18 @@ function connectPortal(api, bridge) {
1713
1664
  return;
1714
1665
  const url = `${bridge.portalWsAddr}/${bridge.botId}`;
1715
1666
  portalLog(api, "info", `connecting to agent-portal: url=${url} botId=${bridge.botId} accountId=${bridge.accountId}`);
1667
+ // Use `ws` library for wss:// to bypass undici globalDispatcher interference
1668
+ const useWsLib = url.startsWith("wss://");
1716
1669
  let ws;
1717
1670
  try {
1718
- ws = new WebSocket(url);
1671
+ if (useWsLib) {
1672
+ const wsClient = new WsWebSocket(url);
1673
+ // Wrap ws library instance to match WebSocket API used below
1674
+ ws = wsClient;
1675
+ }
1676
+ else {
1677
+ ws = new WebSocket(url);
1678
+ }
1719
1679
  }
1720
1680
  catch (err) {
1721
1681
  portalLog(api, "error", `WebSocket constructor failed: ${err?.message ?? err}`);
@@ -1724,11 +1684,12 @@ function connectPortal(api, bridge) {
1724
1684
  }
1725
1685
  bridge.ws = ws;
1726
1686
  let reconnectAttempts = 0;
1687
+ const READY_STATE_OPEN = useWsLib ? WsWebSocket.OPEN : WebSocket.OPEN;
1727
1688
  ws.addEventListener("open", () => {
1728
1689
  reconnectAttempts = 0;
1729
1690
  portalLog(api, "info", `connected to agent-portal: botId=${bridge.botId}`);
1730
1691
  bridge.heartbeatTimer = setInterval(() => {
1731
- if (ws.readyState === WebSocket.OPEN) {
1692
+ if (ws.readyState === READY_STATE_OPEN) {
1732
1693
  const pingMsg = { id: `ping-${Date.now()}`, method: "ping", params: {} };
1733
1694
  ws.send(JSON.stringify(pingMsg));
1734
1695
  portalLog(api, "debug", `heartbeat ping sent: botId=${bridge.botId}`);
@@ -1737,14 +1698,19 @@ function connectPortal(api, bridge) {
1737
1698
  });
1738
1699
  ws.addEventListener("message", async (event) => {
1739
1700
  let raw;
1740
- if (typeof event.data === "string") {
1741
- raw = event.data;
1701
+ // ws library: event.data is Buffer/string; native: event.data is string/ArrayBuffer
1702
+ const data = event.data ?? event;
1703
+ if (typeof data === "string") {
1704
+ raw = data;
1705
+ }
1706
+ else if (data instanceof ArrayBuffer) {
1707
+ raw = new TextDecoder().decode(data);
1742
1708
  }
1743
- else if (event.data instanceof ArrayBuffer) {
1744
- raw = new TextDecoder().decode(event.data);
1709
+ else if (Buffer.isBuffer(data)) {
1710
+ raw = data.toString("utf-8");
1745
1711
  }
1746
1712
  else {
1747
- portalLog(api, "warn", `unexpected message data type: ${typeof event.data}`);
1713
+ portalLog(api, "warn", `unexpected message data type: ${typeof data}`);
1748
1714
  return;
1749
1715
  }
1750
1716
  let request;
@@ -1764,7 +1730,7 @@ function connectPortal(api, bridge) {
1764
1730
  portalLog(api, "debug", `response sent: id=${request.id} method=${request.method} ok=${!response.error}`);
1765
1731
  });
1766
1732
  ws.addEventListener("close", (event) => {
1767
- portalLog(api, "info", `disconnected from agent-portal: botId=${bridge.botId} code=${event.code} reason=${event.reason || "none"}`);
1733
+ portalLog(api, "info", `disconnected from agent-portal: botId=${bridge.botId} code=${event.code ?? 1006} reason=${event.reason || "none"}`);
1768
1734
  clearHeartbeat(bridge);
1769
1735
  bridge.ws = null;
1770
1736
  if (!bridge.stopped) {
package/dist/setup.js CHANGED
@@ -19,15 +19,15 @@ export async function runOpenIMSetup() {
19
19
  clackIntro("OpenIM Channel Setup Wizard");
20
20
  const token = guardCancel(await clackText({
21
21
  message: "Enter OpenIM Access Token",
22
- initialValue: process.env.OPENIM_TOKEN || "",
22
+ initialValue: "",
23
23
  }));
24
24
  const wsAddr = guardCancel(await clackText({
25
25
  message: "Enter OpenIM WebSocket endpoint",
26
- initialValue: process.env.OPENIM_WS_ADDR || "ws://127.0.0.1:10001",
26
+ initialValue: "ws://127.0.0.1:10001",
27
27
  }));
28
28
  const apiAddr = guardCancel(await clackText({
29
29
  message: "Enter OpenIM REST API endpoint",
30
- initialValue: process.env.OPENIM_API_ADDR || "http://127.0.0.1:10002",
30
+ initialValue: "http://127.0.0.1:10002",
31
31
  }));
32
32
  const trimmedToken = String(token).trim();
33
33
  const trimmedWsAddr = String(wsAddr).trim();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-channel",
3
3
  "name": "OpenIM Channel",
4
- "version": "0.3.6",
4
+ "version": "0.3.9",
5
5
  "description": "OpenIM protocol channel for OpenClaw",
6
6
  "author": "ZeyiY",
7
7
  "channels": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeyiy/openclaw-channel",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "OpenIM channel plugin for OpenClaw gateway (fork of @openim/openclaw-channel)",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "ZeyiY",
@@ -49,7 +49,9 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@clack/prompts": "^1.0.0",
52
- "@openim/client-sdk": "^3.8.3"
52
+ "@openim/client-sdk": "^3.8.3",
53
+ "@types/ws": "^8.18.1",
54
+ "ws": "^8.20.0"
53
55
  },
54
56
  "peerDependencies": {
55
57
  "clawdbot": "*",