@rlynicrisis/link 0.0.3 → 0.0.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/README.md CHANGED
@@ -1,12 +1,19 @@
1
1
  # OpenClaw Link Channel Plugin
2
2
 
3
- 这是一个 OpenClaw 的 Link 消息服务接入插件。
3
+ 这是一个 OpenClaw 的 Link 消息服务接入插件,支持通过 TCP 协议连接到 Link/EMB 服务器,实现消息的接收与发送。
4
4
 
5
- ## 功能
6
- - 接入 Link 消息服务 (TCP 协议)
7
- - 支持接收文本消息
8
- - 支持回复文本消息
9
- - **安全限制**: 仅允许接收和回复自己账号的消息(Bot 自言自语模式)
5
+ ## 功能特性
6
+
7
+ - **协议支持**: 基于 TCP 的私有协议 (EMB Protocol V3),支持 Protobuf 消息序列化。
8
+ - **消息类型**:
9
+ - 文本消息 (Text)
10
+ - ✅ 文件消息 (File) - *需配合文件上传服务使用*
11
+ - **安全机制**:
12
+ - 🔒 **仅限己方消息**: 插件默认仅处理当前登录用户发送给自己的消息(即“文件传输助手”模式),忽略群聊和其他用户的私聊,确保数据安全。
13
+ - **连接管理**:
14
+ - 💓 自动心跳保活
15
+ - 🔄 断线自动重连
16
+ - 🎫 **Token 自动刷新**: 支持配置 `refreshToken` 和 `ssoUrl`,在 Token 过期时自动通过 SSO 接口刷新并重连。
10
17
 
11
18
  ## 安装与配置
12
19
 
@@ -19,29 +26,32 @@ npm install
19
26
  npm run build
20
27
  ```
21
28
 
22
- 这将生成 `dist` 目录。
29
+ 这将生成 `dist` 目录,包含编译后的插件代码。
23
30
 
24
31
  ### 2. 配置 OpenClaw
25
32
 
26
33
  在您的 OpenClaw 主配置文件(通常是 `config.yaml` 或 `config.json`)中,添加以下内容:
27
34
 
28
- #### 注册插件
29
- 如果 OpenClaw 支持本地插件加载,请确保插件路径被包含在加载列表中,或者将本插件发布/链接到 `node_modules`。
35
+ #### 频道配置 (Channel Config)
30
36
 
31
- #### 频道配置
32
37
  在 `channels` 部分添加 `link` 配置:
33
38
 
34
39
  ```yaml
35
40
  channels:
36
41
  link:
37
42
  enabled: true
38
- # Link 服务地址
39
- host: "embtcpbeta.bingolink.biz"
40
- port: 20081
43
+ # Link 服务地址 (格式: host:port)
44
+ host: "embtcpbeta.bingolink.biz:20081"
45
+
41
46
  # 鉴权信息 (必填)
42
- accessToken: "your_access_token"
43
- # 可选配置
44
- # heartbeatIntervalMs: 30000
47
+ accessToken: "your_access_token_here"
48
+
49
+ # Token 自动刷新配置 (可选,推荐配置)
50
+ refreshToken: "your_refresh_token_here"
51
+ ssoUrl: "https://sso.example.com" # SSO 服务地址
52
+
53
+ # 高级配置 (可选)
54
+ # heartbeatIntervalMs: 30000 # 心跳间隔,默认 30秒
45
55
  # verifyInfo: # 如果需要覆盖默认生成的设备信息,可在此配置
46
56
  # deviceName: "CustomBotName"
47
57
  ```
@@ -49,16 +59,36 @@ channels:
49
59
  > **注意**:
50
60
  > - `userId` 将自动从 `accessToken` (JWT) 中解析。
51
61
  > - `deviceUID` 将根据机器特征自动生成,确保同一设备上的稳定性。
52
- > - 其他参数如 `version`, `deviceToken` 等已内置默认值。
62
+ > - `host` 参数现已支持 `host:port` 格式,无需单独配置 `port`。
63
+
64
+ ## 开发调试
53
65
 
54
- ### 3. 启动 OpenClaw
66
+ ### 单元测试
55
67
 
56
- 启动 OpenClaw 主程序,插件将自动连接 Link 服务。
68
+ 运行所有单元测试:
57
69
 
58
- ## 开发调试
70
+ ```bash
71
+ npm test
72
+ ```
73
+
74
+ ### 手动连接测试
59
75
 
60
- 可以使用 `test/manual-connect.ts` 脚本单独测试连接:
76
+ 可以使用 `test/manual-connect.ts` 脚本单独测试连接和基本消息收发:
61
77
 
62
78
  ```bash
63
79
  npx tsx test/manual-connect.ts
64
80
  ```
81
+
82
+ ### 发送文件消息测试
83
+
84
+ 测试发送文件类型消息(构造虚拟文件信息):
85
+
86
+ ```bash
87
+ npx tsx test/send-file.ts
88
+ ```
89
+
90
+ ## 常见问题
91
+
92
+ - **消息发送失败 (Protobuf Error)**: 确保服务端支持 V3 协议,且 `MsgType` 枚举值与服务端定义一致(如 File=3)。
93
+ - **Token 过期**: 如果配置了 `refreshToken` 和 `ssoUrl`,插件会自动尝试刷新。否则需手动更新配置文件中的 `accessToken`。
94
+ - **无法收到消息**: 检查 `bot.ts` 中的安全过滤逻辑,确保发送者和接收者均为当前登录用户。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlynicrisis/link",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Link channel plugin",
6
6
  "files": [
@@ -41,6 +41,9 @@
41
41
  "label": "Link",
42
42
  "selectionLabel": "Link Messaging Service",
43
43
  "blurb": "Integration with Link messaging service.",
44
+ "onboarding": {
45
+ "kind": "manual"
46
+ },
44
47
  "order": 100
45
48
  },
46
49
  "install": {
package/src/onboarding.ts CHANGED
@@ -16,12 +16,12 @@ export const linkOnboardingAdapter: ChannelOnboardingAdapter = {
16
16
  getStatus: async ({ cfg }) => {
17
17
  const linkCfg = getLinkConfig(cfg);
18
18
  const configured = Boolean(
19
- linkCfg?.host && linkCfg?.port && linkCfg?.accessToken
19
+ linkCfg?.host && linkCfg?.accessToken
20
20
  );
21
21
 
22
22
  const statusLines: string[] = [];
23
23
  if (!configured) {
24
- statusLines.push("Link: needs host, port, and accessToken");
24
+ statusLines.push("Link: needs host and accessToken");
25
25
  } else {
26
26
  statusLines.push(`Link: configured (host: ${linkCfg?.host})`);
27
27
  }
@@ -40,24 +40,14 @@ export const linkOnboardingAdapter: ChannelOnboardingAdapter = {
40
40
  const linkCfg = getLinkConfig(next) || {} as LinkConfig;
41
41
 
42
42
  const host = await prompter.text({
43
- message: "Link Server Host",
43
+ message: "Link Server Host (host:port)",
44
44
  initialValue: linkCfg.host,
45
- placeholder: "e.g. embtcpbeta.bingolink.biz",
45
+ placeholder: "e.g. embtcpbeta.bingolink.biz:20081",
46
46
  validate: (value) => (value?.trim() ? undefined : "Required"),
47
47
  });
48
48
 
49
- const portStr = await prompter.text({
50
- message: "Link Server Port",
51
- initialValue: linkCfg.port ? String(linkCfg.port) : undefined,
52
- placeholder: "e.g. 20081",
53
- validate: (value) => {
54
- if (!value?.trim()) return "Required";
55
- const n = Number(value);
56
- if (isNaN(n) || n <= 0) return "Must be a positive number";
57
- return undefined;
58
- },
59
- });
60
- const port = Number(portStr);
49
+ // Remove separate port prompt, as it is merged into host
50
+ // const portStr = ...
61
51
 
62
52
  const accessToken = await prompter.text({
63
53
  message: "User Access Token",
@@ -65,11 +55,24 @@ export const linkOnboardingAdapter: ChannelOnboardingAdapter = {
65
55
  validate: (value) => (value?.trim() ? undefined : "Required"),
66
56
  });
67
57
 
58
+ const refreshToken = await prompter.text({
59
+ message: "Refresh Token (Optional)",
60
+ initialValue: linkCfg.refreshToken,
61
+ });
62
+
63
+ const ssoUrl = await prompter.text({
64
+ message: "SSO URL (Optional, for refresh)",
65
+ initialValue: linkCfg.ssoUrl,
66
+ placeholder: "e.g. https://sso.example.com",
67
+ });
68
+
68
69
  const newLinkConfig: LinkConfig = {
69
70
  ...linkCfg,
70
71
  host: String(host).trim(),
71
- port,
72
+ // port is now part of host string or handled internally
72
73
  accessToken: String(accessToken).trim(),
74
+ refreshToken: refreshToken ? String(refreshToken).trim() : undefined,
75
+ ssoUrl: ssoUrl ? String(ssoUrl).trim() : undefined,
73
76
  };
74
77
 
75
78
  next = {