openclaw-channel-openilink 0.1.0
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 +21 -0
- package/README.md +148 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/src/channel.d.ts +34 -0
- package/dist/src/channel.js +31 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +30 -0
- package/dist/src/gateway.d.ts +2 -0
- package/dist/src/gateway.js +71 -0
- package/dist/src/inbound.d.ts +2 -0
- package/dist/src/inbound.js +97 -0
- package/dist/src/outbound.d.ts +6 -0
- package/dist/src/outbound.js +43 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +7 -0
- package/dist/src/types.d.ts +54 -0
- package/dist/src/types.js +1 -0
- package/openclaw.plugin.json +18 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 openilink
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# openclaw-channel-openilink
|
|
2
|
+
|
|
3
|
+
[OpenClaw](https://github.com/openclaw/openclaw) 的 Channel 插件,通过 [OpeniLink Hub](https://github.com/openilink/openilink-hub) 接入微信 Bot。
|
|
4
|
+
|
|
5
|
+
安装后,OpenClaw AI 可以通过 Hub 管理的微信 Bot 收发消息。
|
|
6
|
+
|
|
7
|
+
## 工作原理
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
OpenClaw AI 助手
|
|
11
|
+
↕ (插件 SDK)
|
|
12
|
+
[openclaw-channel-openilink]
|
|
13
|
+
↕ (WebSocket + HTTP)
|
|
14
|
+
OpeniLink Hub Bot API
|
|
15
|
+
↕ (iLink 协议)
|
|
16
|
+
微信 ClawBot
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **收消息**:微信用户发消息 → Hub 通过 WebSocket 推送给插件 → 插件转发给 OpenClaw AI 处理
|
|
20
|
+
- **发消息**:OpenClaw AI 生成回复 → 插件调 Hub Bot API 发送 → Hub 通过 Bot 发到微信
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm config set @openilink:registry https://npm.pkg.github.com && openclaw plugins install @openilink/openclaw-channel-openilink
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 配置
|
|
29
|
+
|
|
30
|
+
### 前置步骤
|
|
31
|
+
|
|
32
|
+
1. 部署或访问一个 [OpeniLink Hub](https://github.com/openilink/openilink-hub) 实例
|
|
33
|
+
2. 在 Hub 上安装 **OpenClaw** App 到你的 Bot(应用市场 → OpenClaw → 安装)
|
|
34
|
+
3. 在安装详情页复制 **Token**
|
|
35
|
+
|
|
36
|
+
### 单账户
|
|
37
|
+
|
|
38
|
+
编辑 OpenClaw 配置文件(`openclaw.yaml`):
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
channels:
|
|
42
|
+
openilink:
|
|
43
|
+
hub_url: "https://hub.openilink.com"
|
|
44
|
+
app_token: "app_你的token"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 多账户
|
|
48
|
+
|
|
49
|
+
连接多个 Hub Bot(不同 Bot 或不同 Hub 实例):
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
channels:
|
|
53
|
+
openilink:
|
|
54
|
+
accounts:
|
|
55
|
+
sales-bot:
|
|
56
|
+
hub_url: "https://hub.openilink.com"
|
|
57
|
+
app_token: "app_销售bot的token"
|
|
58
|
+
support-bot:
|
|
59
|
+
hub_url: "https://hub.openilink.com"
|
|
60
|
+
app_token: "app_客服bot的token"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
每个账户维护独立的 WebSocket 连接,互不干扰。
|
|
64
|
+
|
|
65
|
+
### 重启 OpenClaw
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
openclaw restart
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
配置完成后,微信消息会自动转发到 OpenClaw AI 处理。
|
|
72
|
+
|
|
73
|
+
## 配置参数
|
|
74
|
+
|
|
75
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
76
|
+
|------|------|------|------|
|
|
77
|
+
| `hub_url` | string | 是 | OpeniLink Hub 地址(如 `https://hub.openilink.com`) |
|
|
78
|
+
| `app_token` | string | 是 | 从 Hub 安装 OpenClaw App 后获取的 Token |
|
|
79
|
+
|
|
80
|
+
## 功能
|
|
81
|
+
|
|
82
|
+
- **实时消息** — 通过 WebSocket 持久连接接收 Hub 消息
|
|
83
|
+
- **自动重连** — 断线后 5 秒自动重连
|
|
84
|
+
- **私聊/群聊** — 支持直接对话和群组消息
|
|
85
|
+
- **媒体消息** — 支持通过 Hub Bot API 发送图片
|
|
86
|
+
- **多账户** — 同时连接多个 Bot
|
|
87
|
+
- **链路追踪** — Trace ID 端到端传递,方便调试
|
|
88
|
+
|
|
89
|
+
## 开发
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/openilink/openclaw-channel-openilink.git
|
|
93
|
+
cd openclaw-channel-openilink
|
|
94
|
+
npm install
|
|
95
|
+
npm run build
|
|
96
|
+
|
|
97
|
+
# 本地开发链接
|
|
98
|
+
openclaw plugins install --link .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 相关链接
|
|
102
|
+
|
|
103
|
+
- [OpeniLink Hub](https://github.com/openilink/openilink-hub) — 微信 Bot 消息管理平台
|
|
104
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) — 开源 AI 助手
|
|
105
|
+
- [OpenClaw 插件文档](https://docs.openclaw.ai/tools/plugin) — 插件开发指南
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## English
|
|
114
|
+
|
|
115
|
+
OpenClaw channel plugin for [OpeniLink Hub](https://github.com/openilink/openilink-hub) — bridge OpenClaw AI to WeChat bots.
|
|
116
|
+
|
|
117
|
+
### Quick Start
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm config set @openilink:registry https://npm.pkg.github.com && openclaw plugins install @openilink/openclaw-channel-openilink
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Configure `openclaw.yaml`:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
channels:
|
|
127
|
+
openilink:
|
|
128
|
+
hub_url: "https://hub.openilink.com"
|
|
129
|
+
app_token: "your_app_token"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then `openclaw restart`.
|
|
133
|
+
|
|
134
|
+
### How to Get Your Token
|
|
135
|
+
|
|
136
|
+
1. Log into your OpeniLink Hub
|
|
137
|
+
2. Go to your Bot → App Marketplace → Install **OpenClaw** app
|
|
138
|
+
3. Copy the **Token** from the installation detail page
|
|
139
|
+
4. Paste into your OpenClaw config
|
|
140
|
+
|
|
141
|
+
### Features
|
|
142
|
+
|
|
143
|
+
- Real-time messaging via WebSocket
|
|
144
|
+
- Auto-reconnect (5s backoff)
|
|
145
|
+
- Direct and group chat support
|
|
146
|
+
- Media messages (images)
|
|
147
|
+
- Multi-account support
|
|
148
|
+
- Trace ID propagation
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
|
|
6
|
+
register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
|
|
7
|
+
} & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "kind">;
|
|
8
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { openiLinkChannel } from "./src/channel.js";
|
|
3
|
+
import { setPluginRuntime } from "./src/runtime.js";
|
|
4
|
+
export default defineChannelPluginEntry({
|
|
5
|
+
id: "openilink",
|
|
6
|
+
name: "OpeniLink Hub",
|
|
7
|
+
description: "Bridge WeChat bots via OpeniLink Hub",
|
|
8
|
+
plugin: openiLinkChannel,
|
|
9
|
+
setRuntime: setPluginRuntime,
|
|
10
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { OpeniLinkConfig } from "./types.js";
|
|
2
|
+
import { startAccount, stopAccount } from "./gateway.js";
|
|
3
|
+
export declare const openiLinkChannel: {
|
|
4
|
+
id: any;
|
|
5
|
+
meta: {
|
|
6
|
+
id: any;
|
|
7
|
+
label: string;
|
|
8
|
+
selectionLabel: string;
|
|
9
|
+
docsPath: string;
|
|
10
|
+
blurb: string;
|
|
11
|
+
};
|
|
12
|
+
capabilities: {
|
|
13
|
+
chatTypes: Array<"direct" | "group">;
|
|
14
|
+
media: boolean;
|
|
15
|
+
};
|
|
16
|
+
config: {
|
|
17
|
+
listAccountIds: (cfg: any) => string[];
|
|
18
|
+
resolveAccount: (cfg: any, accountId?: string | null) => OpeniLinkConfig;
|
|
19
|
+
isConfigured: (account: OpeniLinkConfig) => boolean;
|
|
20
|
+
describeAccount: (account: OpeniLinkConfig) => {
|
|
21
|
+
status: string;
|
|
22
|
+
label: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
gateway: {
|
|
26
|
+
startAccount: typeof startAccount;
|
|
27
|
+
stopAccount: typeof stopAccount;
|
|
28
|
+
};
|
|
29
|
+
outbound: {
|
|
30
|
+
deliveryMode: "direct";
|
|
31
|
+
sendText(ctx: any): Promise<any>;
|
|
32
|
+
sendMedia(ctx: any): Promise<any>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolveConfig, listAccountIds } from "./config.js";
|
|
2
|
+
import { startAccount, stopAccount } from "./gateway.js";
|
|
3
|
+
import { createOutboundAdapter } from "./outbound.js";
|
|
4
|
+
export const openiLinkChannel = {
|
|
5
|
+
id: "openilink",
|
|
6
|
+
meta: {
|
|
7
|
+
id: "openilink",
|
|
8
|
+
label: "OpeniLink Hub",
|
|
9
|
+
selectionLabel: "OpeniLink Hub (WeChat Bridge)",
|
|
10
|
+
docsPath: "channels/openilink",
|
|
11
|
+
blurb: "Send and receive WeChat messages via OpeniLink Hub",
|
|
12
|
+
},
|
|
13
|
+
capabilities: {
|
|
14
|
+
chatTypes: ["direct", "group"],
|
|
15
|
+
media: true,
|
|
16
|
+
},
|
|
17
|
+
config: {
|
|
18
|
+
listAccountIds: (cfg) => listAccountIds(cfg),
|
|
19
|
+
resolveAccount: (cfg, accountId) => resolveConfig(cfg, accountId),
|
|
20
|
+
isConfigured: (account) => !!(account.hubUrl && account.appToken),
|
|
21
|
+
describeAccount: (account) => ({
|
|
22
|
+
status: account.hubUrl ? "configured" : "unconfigured",
|
|
23
|
+
label: account.hubUrl || "Not configured",
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
gateway: {
|
|
27
|
+
startAccount,
|
|
28
|
+
stopAccount,
|
|
29
|
+
},
|
|
30
|
+
outbound: createOutboundAdapter(resolveConfig),
|
|
31
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OpeniLinkConfig } from "./types.js";
|
|
2
|
+
export declare function resolveConfig(cfg: any, accountId?: string | null): OpeniLinkConfig;
|
|
3
|
+
export declare function listAccountIds(cfg: any): string[];
|
|
4
|
+
export declare function isConfigured(cfg: any, accountId?: string | null): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function resolveConfig(cfg, accountId) {
|
|
2
|
+
const channelCfg = cfg?.channels?.openilink || {};
|
|
3
|
+
// Support multi-account
|
|
4
|
+
if (accountId && channelCfg.accounts?.[accountId]) {
|
|
5
|
+
const account = channelCfg.accounts[accountId];
|
|
6
|
+
return {
|
|
7
|
+
hubUrl: (account.hub_url || channelCfg.hub_url || "").replace(/\/$/, ""),
|
|
8
|
+
appToken: account.app_token || channelCfg.app_token || "",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
hubUrl: (channelCfg.hub_url || "").replace(/\/$/, ""),
|
|
13
|
+
appToken: channelCfg.app_token || "",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function listAccountIds(cfg) {
|
|
17
|
+
const channelCfg = cfg?.channels?.openilink || {};
|
|
18
|
+
if (channelCfg.accounts) {
|
|
19
|
+
return Object.keys(channelCfg.accounts);
|
|
20
|
+
}
|
|
21
|
+
// Single account mode — return default ID
|
|
22
|
+
if (channelCfg.hub_url && channelCfg.app_token) {
|
|
23
|
+
return ["default"];
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
export function isConfigured(cfg, accountId) {
|
|
28
|
+
const resolved = resolveConfig(cfg, accountId);
|
|
29
|
+
return !!(resolved.hubUrl && resolved.appToken);
|
|
30
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { handleInboundEvent } from "./inbound.js";
|
|
3
|
+
const connections = new Map();
|
|
4
|
+
export async function startAccount(ctx) {
|
|
5
|
+
const { accountId, account, cfg, abortSignal, setStatus } = ctx;
|
|
6
|
+
const config = account;
|
|
7
|
+
if (!config.hubUrl || !config.appToken) {
|
|
8
|
+
setStatus({ status: "error", error: "Missing hub_url or app_token" });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const wsUrl = `${config.hubUrl.replace(/^http/, "ws")}/bot/v1/ws?token=${config.appToken}`;
|
|
12
|
+
function connect() {
|
|
13
|
+
if (abortSignal.aborted)
|
|
14
|
+
return;
|
|
15
|
+
const ws = new WebSocket(wsUrl);
|
|
16
|
+
connections.set(accountId, ws);
|
|
17
|
+
ws.on("open", () => {
|
|
18
|
+
setStatus({ status: "connected" });
|
|
19
|
+
console.log(`[openilink] Connected to Hub: ${config.hubUrl}`);
|
|
20
|
+
});
|
|
21
|
+
ws.on("message", (data) => {
|
|
22
|
+
try {
|
|
23
|
+
const msg = JSON.parse(data.toString());
|
|
24
|
+
if (msg.type === "init") {
|
|
25
|
+
console.log(`[openilink] Init: bot=${msg.data.bot_id}, app=${msg.data.app_slug}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (msg.type === "event") {
|
|
29
|
+
handleInboundEvent(msg, config, cfg, accountId);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (msg.type === "pong")
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error("[openilink] Parse error:", err);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
ws.on("close", () => {
|
|
40
|
+
connections.delete(accountId);
|
|
41
|
+
setStatus({ status: "disconnected" });
|
|
42
|
+
// Reconnect after 5s
|
|
43
|
+
if (!abortSignal.aborted) {
|
|
44
|
+
setTimeout(connect, 5000);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
ws.on("error", (err) => {
|
|
48
|
+
console.error("[openilink] WS error:", err.message);
|
|
49
|
+
setStatus({ status: "error", error: err.message });
|
|
50
|
+
});
|
|
51
|
+
// Ping every 30s
|
|
52
|
+
const pingInterval = setInterval(() => {
|
|
53
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
54
|
+
ws.send(JSON.stringify({ type: "ping" }));
|
|
55
|
+
}
|
|
56
|
+
}, 30000);
|
|
57
|
+
ws.on("close", () => clearInterval(pingInterval));
|
|
58
|
+
abortSignal.addEventListener("abort", () => {
|
|
59
|
+
clearInterval(pingInterval);
|
|
60
|
+
ws.close();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
connect();
|
|
64
|
+
}
|
|
65
|
+
export async function stopAccount(ctx) {
|
|
66
|
+
const ws = connections.get(ctx.accountId);
|
|
67
|
+
if (ws) {
|
|
68
|
+
ws.close();
|
|
69
|
+
connections.delete(ctx.accountId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getPluginRuntime } from "./runtime.js";
|
|
2
|
+
export async function handleInboundEvent(event, config, cfg, accountId) {
|
|
3
|
+
const rt = getPluginRuntime();
|
|
4
|
+
if (!rt?.channel)
|
|
5
|
+
return;
|
|
6
|
+
const eventData = event.event.data;
|
|
7
|
+
const sender = eventData.sender;
|
|
8
|
+
const group = eventData.group;
|
|
9
|
+
const content = eventData.content || "";
|
|
10
|
+
const senderId = sender?.id || "unknown";
|
|
11
|
+
const senderName = sender?.name || senderId;
|
|
12
|
+
const isDirect = !group;
|
|
13
|
+
const peerId = isDirect ? senderId : group.id;
|
|
14
|
+
// Resolve agent route
|
|
15
|
+
const route = rt.channel.routing.resolveAgentRoute({
|
|
16
|
+
cfg,
|
|
17
|
+
channel: "openilink",
|
|
18
|
+
accountId,
|
|
19
|
+
peer: { kind: isDirect ? "direct" : "group", id: peerId },
|
|
20
|
+
});
|
|
21
|
+
// Format inbound envelope
|
|
22
|
+
const envelopeOptions = rt.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
23
|
+
const body = rt.channel.reply.formatInboundEnvelope({
|
|
24
|
+
channel: "OpeniLink",
|
|
25
|
+
from: senderName,
|
|
26
|
+
timestamp: event.event.timestamp * 1000,
|
|
27
|
+
body: content,
|
|
28
|
+
chatType: isDirect ? "direct" : "group",
|
|
29
|
+
sender: { name: senderName, id: senderId },
|
|
30
|
+
envelope: envelopeOptions,
|
|
31
|
+
});
|
|
32
|
+
// Build inbound context
|
|
33
|
+
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
34
|
+
Body: body,
|
|
35
|
+
RawBody: content,
|
|
36
|
+
CommandBody: content,
|
|
37
|
+
From: peerId,
|
|
38
|
+
To: peerId,
|
|
39
|
+
SessionKey: route.sessionKey,
|
|
40
|
+
AccountId: accountId,
|
|
41
|
+
ChatType: isDirect ? "direct" : "group",
|
|
42
|
+
SenderName: senderName,
|
|
43
|
+
SenderId: senderId,
|
|
44
|
+
Provider: "openilink",
|
|
45
|
+
Surface: "openilink",
|
|
46
|
+
MessageSid: event.event.id || `${event.trace_id}-${Date.now()}`,
|
|
47
|
+
Timestamp: event.event.timestamp * 1000,
|
|
48
|
+
CommandAuthorized: true,
|
|
49
|
+
OriginatingChannel: "openilink",
|
|
50
|
+
OriginatingTo: peerId,
|
|
51
|
+
});
|
|
52
|
+
// Record inbound session
|
|
53
|
+
const storePath = rt.channel.session.resolveStorePath(`openilink/${accountId}`);
|
|
54
|
+
await rt.channel.session.recordInboundSession({
|
|
55
|
+
storePath,
|
|
56
|
+
sessionKey: route.sessionKey,
|
|
57
|
+
ctx,
|
|
58
|
+
onRecordError: (err) => {
|
|
59
|
+
console.error("[openilink] Session record error:", err);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
// Dispatch to AI and deliver response
|
|
63
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
64
|
+
ctx,
|
|
65
|
+
cfg,
|
|
66
|
+
dispatcherOptions: {
|
|
67
|
+
responsePrefix: "",
|
|
68
|
+
deliver: async (payload) => {
|
|
69
|
+
// Send AI response back to Hub
|
|
70
|
+
if (payload.text) {
|
|
71
|
+
await sendToHub(config, payload.text, event.trace_id);
|
|
72
|
+
}
|
|
73
|
+
if (payload.mediaUrl) {
|
|
74
|
+
await sendToHub(config, payload.mediaUrl, event.trace_id);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
replyOptions: {},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function sendToHub(config, content, traceId) {
|
|
82
|
+
const body = { content };
|
|
83
|
+
if (traceId)
|
|
84
|
+
body.trace_id = traceId;
|
|
85
|
+
const resp = await fetch(`${config.hubUrl}/bot/v1/message/send`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Authorization: `Bearer ${config.appToken}`,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
});
|
|
93
|
+
if (!resp.ok) {
|
|
94
|
+
const text = await resp.text();
|
|
95
|
+
console.error(`[openilink] Send failed: ${resp.status} ${text}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OpeniLinkConfig } from "./types.js";
|
|
2
|
+
export declare function createOutboundAdapter(resolveConfig: (cfg: any, accountId?: string | null) => OpeniLinkConfig): {
|
|
3
|
+
deliveryMode: "direct";
|
|
4
|
+
sendText(ctx: any): Promise<any>;
|
|
5
|
+
sendMedia(ctx: any): Promise<any>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function createOutboundAdapter(resolveConfig) {
|
|
2
|
+
return {
|
|
3
|
+
deliveryMode: "direct",
|
|
4
|
+
async sendText(ctx) {
|
|
5
|
+
const config = resolveConfig(ctx.cfg, ctx.accountId);
|
|
6
|
+
const body = { content: ctx.text };
|
|
7
|
+
const resp = await fetch(`${config.hubUrl}/bot/v1/message/send`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
Authorization: `Bearer ${config.appToken}`,
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(body),
|
|
14
|
+
});
|
|
15
|
+
if (!resp.ok) {
|
|
16
|
+
return { ok: false, error: `Hub returned ${resp.status}` };
|
|
17
|
+
}
|
|
18
|
+
const result = await resp.json();
|
|
19
|
+
return { ok: true, messageId: result.client_id };
|
|
20
|
+
},
|
|
21
|
+
async sendMedia(ctx) {
|
|
22
|
+
const config = resolveConfig(ctx.cfg, ctx.accountId);
|
|
23
|
+
const body = {
|
|
24
|
+
content: ctx.text || "",
|
|
25
|
+
type: "image",
|
|
26
|
+
url: ctx.mediaUrl || "",
|
|
27
|
+
};
|
|
28
|
+
const resp = await fetch(`${config.hubUrl}/bot/v1/message/send`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
Authorization: `Bearer ${config.appToken}`,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
});
|
|
36
|
+
if (!resp.ok) {
|
|
37
|
+
return { ok: false, error: `Hub returned ${resp.status}` };
|
|
38
|
+
}
|
|
39
|
+
const result = await resp.json();
|
|
40
|
+
return { ok: true, messageId: result.client_id };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface OpeniLinkConfig {
|
|
2
|
+
hubUrl: string;
|
|
3
|
+
appToken: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HubWSInit {
|
|
6
|
+
type: "init";
|
|
7
|
+
data: {
|
|
8
|
+
installation_id: string;
|
|
9
|
+
bot_id: string;
|
|
10
|
+
app_name: string;
|
|
11
|
+
app_slug: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface HubWSEvent {
|
|
15
|
+
type: "event";
|
|
16
|
+
v: number;
|
|
17
|
+
trace_id: string;
|
|
18
|
+
installation_id: string;
|
|
19
|
+
bot: {
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
event: {
|
|
23
|
+
type: string;
|
|
24
|
+
id: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
data: {
|
|
27
|
+
content?: string;
|
|
28
|
+
sender?: {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
group?: {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
} | null;
|
|
36
|
+
msg_type?: string;
|
|
37
|
+
message_id?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface HubWSAck {
|
|
43
|
+
type: "ack";
|
|
44
|
+
req_id: string;
|
|
45
|
+
ok: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface HubWSError {
|
|
48
|
+
type: "error";
|
|
49
|
+
req_id?: string;
|
|
50
|
+
error: string;
|
|
51
|
+
}
|
|
52
|
+
export type HubWSMessage = HubWSInit | HubWSEvent | HubWSAck | HubWSError | {
|
|
53
|
+
type: "pong";
|
|
54
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openilink",
|
|
3
|
+
"channels": ["openilink"],
|
|
4
|
+
"configSchema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"hub_url": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "OpeniLink Hub URL (e.g., https://hub.openilink.com)"
|
|
10
|
+
},
|
|
11
|
+
"app_token": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "App installation token from Hub"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"required": ["hub_url", "app_token"]
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-channel-openilink",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw channel plugin for OpeniLink Hub — bridge WeChat bots via Hub",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"openclaw.plugin.json",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["openclaw", "openclaw-plugin", "openilink", "wechat"],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/openilink/openclaw-channel-openilink"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"openclaw": ">=2025.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"openclaw": "*",
|
|
28
|
+
"typescript": "^5.0.0",
|
|
29
|
+
"ws": "^8.0.0",
|
|
30
|
+
"@types/ws": "^8.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"ws": "^8.18.0"
|
|
34
|
+
}
|
|
35
|
+
}
|