@zeyiy/openclaw-channel 0.3.4 → 0.3.6

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
@@ -14,6 +14,9 @@ Chinese documentation: [README.zh-CN.md](https://github.com/ZeyiY/openclaw-chann
14
14
  - Quote/reply message parsing for inbound context
15
15
  - Multi-account login via `channels.openim.accounts.<id>`
16
16
  - Group trigger policy with optional mention-only mode
17
+ - 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
17
20
  - Interactive setup command: `openclaw openim setup`
18
21
 
19
22
  ## Installation
@@ -58,7 +61,9 @@ openclaw openim setup
58
61
  "enabled": true,
59
62
  "token": "your_token",
60
63
  "wsAddr": "ws://127.0.0.1:10001",
61
- "apiAddr": "http://127.0.0.1:10002"
64
+ "apiAddr": "http://127.0.0.1:10002",
65
+ "botId": "my-bot-001",
66
+ "portalWsAddr": "wss://portal.example.com/ws"
62
67
  }
63
68
  }
64
69
  }
@@ -75,6 +80,8 @@ If set, only these users can trigger processing:
75
80
  - direct messages to the account
76
81
  - group messages where they `@` the account
77
82
 
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.
84
+
78
85
  Single-account fallback (without `accounts`) is supported.
79
86
 
80
87
  Environment fallback is supported for the `default` account:
@@ -113,6 +120,27 @@ Optional env overrides:
113
120
  - `name` (optional): override filename for URL input
114
121
  - `accountId` (optional): select sending account
115
122
 
123
+ ## Agent Portal Bridge
124
+
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:
126
+
127
+ | 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 |
136
+ | `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 |
141
+
142
+ The connection features automatic reconnect with exponential backoff and heartbeat keepalive.
143
+
116
144
  ## Development
117
145
 
118
146
  ```bash
package/README.zh-CN.md CHANGED
@@ -14,6 +14,9 @@ English documentation: [README.md](https://github.com/ZeyiY/openclaw-channel/blo
14
14
  - 支持引用消息解析(用于入站上下文)
15
15
  - 支持多账号并发(`channels.openim.accounts.<id>`)
16
16
  - 支持群聊仅 @ 触发
17
+ - 私聊消息自动标记已读(已读回执)
18
+ - 每用户独立会话(私聊)/ 同群共享会话(群聊)
19
+ - Agent Portal Bridge — 与 agent-portal 云服务保持 WebSocket 长连接,支持远程管理
17
20
  - 提供交互式配置命令:`openclaw openim setup`
18
21
 
19
22
  ## 安装
@@ -58,7 +61,9 @@ openclaw openim setup
58
61
  "enabled": true,
59
62
  "token": "your_token",
60
63
  "wsAddr": "ws://127.0.0.1:10001",
61
- "apiAddr": "http://127.0.0.1:10002"
64
+ "apiAddr": "http://127.0.0.1:10002",
65
+ "botId": "my-bot-001",
66
+ "portalWsAddr": "wss://portal.example.com/ws"
62
67
  }
63
68
  }
64
69
  }
@@ -74,6 +79,8 @@ openclaw openim setup
74
79
  - 给账号发单聊消息
75
80
  - 在群里 @ 账号的消息
76
81
 
82
+ `botId` 和 `portalWsAddr` 为可选项。同时配置后,插件会与 agent-portal 云服务建立 WebSocket 连接,支持远程管理 agent、文件和模型。
83
+
77
84
  支持单账号兜底写法(不使用 `accounts`)。
78
85
 
79
86
  `default` 账号支持环境变量兜底:
@@ -112,6 +119,27 @@ openclaw openim setup
112
119
  - `name`(可选):URL 输入时覆盖文件名
113
120
  - `accountId`(可选):指定发送账号
114
121
 
122
+ ## Agent Portal Bridge
123
+
124
+ 配置 `botId` 和 `portalWsAddr` 后,插件会通过 WebSocket 连接到 agent-portal 云服务。Portal 可远程调用以下方法:
125
+
126
+ | 方法 | 说明 |
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 工作空间 |
135
+ | `tools.catalog` | 列出可用工具 |
136
+ | `skills.status` | 列出已安装技能/插件状态 |
137
+ | `skills.search` | 搜索 ClawHub 技能(占位) |
138
+ | `skills.detail` | 获取特定技能详情 |
139
+ | `cron.list` | 列出已配置的定时任务 |
140
+
141
+ 连接支持指数退避自动重连和心跳保活。
142
+
115
143
  ## 开发
116
144
 
117
145
  ```bash
package/dist/portal.js CHANGED
@@ -207,26 +207,24 @@ async function handleAgentsFilesList(api, params) {
207
207
  for (const name of AGENT_FILE_NAMES) {
208
208
  const filePath = join(workspaceDir, name);
209
209
  const meta = await statFileSafely(filePath);
210
- if (meta) {
211
- let content;
212
- try {
213
- content = await readFile(filePath, "utf-8");
214
- }
215
- catch {
216
- // skip unreadable files
217
- }
218
- files.push({
219
- name,
220
- path: filePath,
221
- missing: false,
222
- size: meta.size,
223
- updatedAtMs: meta.updatedAtMs,
224
- content,
225
- });
210
+ if (!meta)
211
+ continue;
212
+ let content;
213
+ try {
214
+ content = await readFile(filePath, "utf-8");
226
215
  }
227
- else {
228
- files.push({ name, path: filePath, missing: true });
216
+ catch {
217
+ // skip unreadable files
218
+ continue;
229
219
  }
220
+ files.push({
221
+ name,
222
+ path: filePath,
223
+ missing: false,
224
+ size: meta.size,
225
+ updatedAtMs: meta.updatedAtMs,
226
+ content,
227
+ });
230
228
  }
231
229
  portalLog(api, "info", `agents.files.list: agentId=${agentId} workspace=${workspaceDir} found=${files.filter(f => !f.missing).length}`);
232
230
  return { agentId, workspace: workspaceDir, files };
@@ -350,15 +348,191 @@ async function handleAgentsCreate(api, params) {
350
348
  portalLog(api, "info", `agents.create: agentId=${agentId} name=${rawName} workspace=${workspaceDir}`);
351
349
  return { ok: true, agentId, name: rawName, workspace: workspaceDir };
352
350
  }
351
+ /**
352
+ * bot.agent.get — resolve the agentId bound to the current bot connection.
353
+ *
354
+ * Lookup order:
355
+ * 1. Config bindings: match channel=openim + accountId
356
+ * 2. Fallback to default agent
357
+ */
358
+ function handleBotAgentGet(api, accountId) {
359
+ const cfg = getConfig(api);
360
+ const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
361
+ // 1. Check bindings
362
+ const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
363
+ for (const b of bindings) {
364
+ if (b?.match?.channel === "openim" &&
365
+ b?.match?.accountId === accountId &&
366
+ b?.agentId) {
367
+ const agentId = normalizeAgentId(b.agentId);
368
+ const entry = agents.find((a) => a?.id && normalizeAgentId(a.id) === agentId);
369
+ return { agentId, ...(entry?.name ? { name: entry.name } : {}) };
370
+ }
371
+ }
372
+ // 2. Fallback to default agent
373
+ const defaultId = resolveDefaultAgentId(cfg);
374
+ const defaultEntry = agents.find((a) => a?.id && normalizeAgentId(a.id) === defaultId);
375
+ return { agentId: defaultId, ...(defaultEntry?.name ? { name: defaultEntry.name } : {}) };
376
+ }
377
+ /**
378
+ * tools.catalog — return the runtime tool catalog from config.
379
+ * Reads tools from agents' TOOLS.md references and registered plugin tools.
380
+ */
381
+ function handleToolsCatalog(api) {
382
+ const cfg = getConfig(api);
383
+ const tools = [];
384
+ // Core tools from config.tools section
385
+ const configTools = cfg.tools;
386
+ if (configTools && typeof configTools === "object") {
387
+ for (const [name, toolCfg] of Object.entries(configTools)) {
388
+ if (!name || name.startsWith("_"))
389
+ continue;
390
+ tools.push({
391
+ name,
392
+ description: toolCfg?.description ?? "",
393
+ source: "core",
394
+ optional: Boolean(toolCfg?.optional),
395
+ });
396
+ }
397
+ }
398
+ // Plugin-registered tools (from channel registrations)
399
+ const plugins = cfg.plugins;
400
+ if (plugins && typeof plugins === "object") {
401
+ for (const [pluginId, pluginCfg] of Object.entries(plugins)) {
402
+ if (!pluginId)
403
+ continue;
404
+ const pluginTools = pluginCfg?.tools;
405
+ if (Array.isArray(pluginTools)) {
406
+ for (const t of pluginTools) {
407
+ const tName = typeof t === "string" ? t : t?.name;
408
+ if (!tName)
409
+ continue;
410
+ tools.push({
411
+ name: String(tName),
412
+ description: typeof t === "object" ? t?.description ?? "" : "",
413
+ source: "plugin",
414
+ pluginId,
415
+ optional: typeof t === "object" ? Boolean(t?.optional) : false,
416
+ });
417
+ }
418
+ }
419
+ }
420
+ }
421
+ // Also include OpenIM channel tools registered by this plugin
422
+ const openimTools = ["openim_send_text", "openim_send_image", "openim_send_file", "openim_send_video"];
423
+ for (const name of openimTools) {
424
+ tools.push({ name, description: `OpenIM: ${name.replace("openim_", "")}`, source: "plugin", pluginId: "openim" });
425
+ }
426
+ return { tools };
427
+ }
428
+ /**
429
+ * skills.status — return the visible skill list for an agent.
430
+ * Reads installed plugins/skills from config and reports their status.
431
+ */
432
+ function handleSkillsStatus(api, params) {
433
+ const cfg = getConfig(api);
434
+ const skills = [];
435
+ // Plugins as skills
436
+ const plugins = cfg.plugins;
437
+ if (plugins && typeof plugins === "object") {
438
+ for (const [pluginId, pluginCfg] of Object.entries(plugins)) {
439
+ if (!pluginId)
440
+ continue;
441
+ const enabled = pluginCfg?.enabled !== false;
442
+ skills.push({
443
+ name: pluginId,
444
+ description: pluginCfg?.description ?? "",
445
+ installed: true,
446
+ enabled,
447
+ configCheck: enabled ? "ok" : "missing",
448
+ });
449
+ }
450
+ }
451
+ // Channels as skills
452
+ const channels = cfg.channels;
453
+ if (channels && typeof channels === "object") {
454
+ for (const [chanId, chanCfg] of Object.entries(channels)) {
455
+ if (!chanId)
456
+ continue;
457
+ skills.push({
458
+ name: `channel:${chanId}`,
459
+ description: `Channel: ${chanId}`,
460
+ installed: true,
461
+ enabled: chanCfg?.enabled !== false,
462
+ configCheck: "ok",
463
+ });
464
+ }
465
+ }
466
+ return { skills };
467
+ }
468
+ /**
469
+ * skills.search — search ClawHub for discoverable skills.
470
+ * Currently returns an empty list (ClawHub integration not yet implemented locally).
471
+ */
472
+ function handleSkillsSearch(_api, params) {
473
+ // ClawHub discovery requires network access to the registry — placeholder for now
474
+ return { skills: [] };
475
+ }
476
+ /**
477
+ * skills.detail — get detail for a specific skill from ClawHub.
478
+ * Placeholder: returns basic info from local config if the skill is installed.
479
+ */
480
+ function handleSkillsDetail(api, params) {
481
+ const name = String(params.name ?? "").trim();
482
+ if (!name)
483
+ throw { code: 400, message: "name is required" };
484
+ const cfg = getConfig(api);
485
+ const plugins = cfg.plugins;
486
+ if (plugins && typeof plugins === "object" && name in plugins) {
487
+ const p = plugins[name];
488
+ return {
489
+ skill: {
490
+ name,
491
+ description: p?.description ?? "",
492
+ author: p?.author ?? undefined,
493
+ version: p?.version ?? undefined,
494
+ source: "local",
495
+ },
496
+ };
497
+ }
498
+ return { skill: null };
499
+ }
500
+ /**
501
+ * cron.list — return configured cron jobs.
502
+ */
503
+ function handleCronList(api) {
504
+ const cfg = getConfig(api);
505
+ const jobs = [];
506
+ const cronConfig = cfg.cron ?? cfg.automation?.cron;
507
+ if (Array.isArray(cronConfig)) {
508
+ for (const entry of cronConfig) {
509
+ if (!entry || typeof entry !== "object")
510
+ continue;
511
+ jobs.push({
512
+ id: String(entry.id ?? entry.name ?? `cron-${jobs.length}`),
513
+ name: entry.name ?? entry.id ?? undefined,
514
+ schedule: String(entry.schedule ?? entry.cron ?? ""),
515
+ command: entry.command ?? entry.text ?? entry.message ?? undefined,
516
+ enabled: entry.enabled !== false,
517
+ lastRun: entry.lastRun ?? undefined,
518
+ nextRun: entry.nextRun ?? undefined,
519
+ });
520
+ }
521
+ }
522
+ return { jobs };
523
+ }
353
524
  // ---------------------------------------------------------------------------
354
525
  // Request dispatch
355
526
  // ---------------------------------------------------------------------------
356
- async function handlePortalRequest(api, request) {
527
+ async function handlePortalRequest(api, accountId, request) {
357
528
  const { id, method, params } = request;
358
- portalLog(api, "info", `request received: id=${id} method=${method} params=${JSON.stringify(params)}`);
529
+ portalLog(api, "info", `request received: id=${id} method=${method} params=${JSON.stringify(params)} accountId=${accountId}`);
359
530
  try {
360
531
  let result;
361
532
  switch (method) {
533
+ case "bot.agent.get":
534
+ result = handleBotAgentGet(api, accountId);
535
+ break;
362
536
  case "models.list":
363
537
  result = handleModelsList(api, params ?? {});
364
538
  break;
@@ -377,6 +551,21 @@ async function handlePortalRequest(api, request) {
377
551
  case "agents.create":
378
552
  result = await handleAgentsCreate(api, params ?? {});
379
553
  break;
554
+ case "tools.catalog":
555
+ result = handleToolsCatalog(api);
556
+ break;
557
+ case "skills.status":
558
+ result = handleSkillsStatus(api, params ?? {});
559
+ break;
560
+ case "skills.search":
561
+ result = handleSkillsSearch(api, params ?? {});
562
+ break;
563
+ case "skills.detail":
564
+ result = handleSkillsDetail(api, params ?? {});
565
+ break;
566
+ case "cron.list":
567
+ result = handleCronList(api);
568
+ break;
380
569
  case "ping":
381
570
  result = { pong: true };
382
571
  break;
@@ -451,7 +640,7 @@ function connectPortal(api, bridge) {
451
640
  portalLog(api, "warn", `malformed request from portal: missing id or method`);
452
641
  return;
453
642
  }
454
- const response = await handlePortalRequest(api, request);
643
+ const response = await handlePortalRequest(api, bridge.accountId, request);
455
644
  sendResponse(ws, response);
456
645
  portalLog(api, "debug", `response sent: id=${request.id} method=${request.method} ok=${!response.error}`);
457
646
  });
package/dist/types.d.ts CHANGED
@@ -39,7 +39,38 @@ export interface InboundBodyResult {
39
39
  kind: "text" | "image" | "video" | "file" | "mixed" | "unknown";
40
40
  media?: InboundMediaItem[];
41
41
  }
42
- export type PortalMethod = "models.list" | "agents.list" | "agents.files.list" | "agents.files.get" | "agents.files.set" | "agents.create" | "ping";
42
+ export type PortalMethod = "bot.agent.get" | "models.list" | "agents.list" | "agents.files.list" | "agents.files.get" | "agents.files.set" | "agents.create" | "tools.catalog" | "skills.status" | "skills.search" | "skills.detail" | "cron.list" | "ping";
43
+ export interface ToolCatalogEntry {
44
+ name: string;
45
+ description?: string;
46
+ source: "core" | "plugin";
47
+ pluginId?: string;
48
+ optional?: boolean;
49
+ }
50
+ export interface SkillStatusEntry {
51
+ name: string;
52
+ description?: string;
53
+ installed: boolean;
54
+ enabled: boolean;
55
+ configCheck?: "ok" | "missing" | "invalid";
56
+ missingRequirements?: string[];
57
+ }
58
+ export interface SkillSearchEntry {
59
+ name: string;
60
+ description?: string;
61
+ author?: string;
62
+ version?: string;
63
+ source: string;
64
+ }
65
+ export interface CronJobEntry {
66
+ id: string;
67
+ name?: string;
68
+ schedule: string;
69
+ command?: string;
70
+ enabled: boolean;
71
+ lastRun?: number;
72
+ nextRun?: number;
73
+ }
43
74
  export interface PortalRequest {
44
75
  id: string;
45
76
  method: PortalMethod;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-channel",
3
3
  "name": "OpenIM Channel",
4
- "version": "0.3.4",
4
+ "version": "0.3.6",
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.4",
3
+ "version": "0.3.6",
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",