@rlynicrisis/link 0.0.9 → 0.1.1
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 +85 -0
- package/index.ts +0 -0
- package/openclaw.plugin.json +23 -3
- package/package.json +1 -1
- package/src/bot.ts +27 -20
- package/src/channel.ts +13 -8
- package/src/client-manager.ts +2 -2
- package/src/link/client.ts +16 -1
- package/src/link/constants.ts +1 -0
- package/src/link/message.d.ts +0 -0
- package/src/link/message.js +2 -1
- package/src/link/protocol.ts +0 -0
- package/src/link/types.ts +0 -0
- package/src/onboarding.ts +0 -0
- package/src/reply-dispatcher.ts +0 -0
- package/src/runtime.ts +0 -0
- package/src/send.ts +26 -10
- package/src/types.ts +47 -2
package/README.md
CHANGED
|
@@ -103,6 +103,91 @@ curl --location --request POST 'https://www.bingolink.biz/sso/oauth2/token' \
|
|
|
103
103
|
|
|
104
104
|
> **提示**: 请将 `username` 和 `password` 替换为您的实际账号信息。响应结果将包含 `access_token` 和 `refresh_token`。
|
|
105
105
|
|
|
106
|
+
## 多账号支持 (Multi-Account Support)
|
|
107
|
+
|
|
108
|
+
插件支持在同一个 OpenClaw 实例中同时连接多个 Link 账号,每个账号独立维护连接和消息路由。
|
|
109
|
+
|
|
110
|
+
### 配置格式
|
|
111
|
+
|
|
112
|
+
在 `channels.link` 下添加 `accounts` 字段,以账号 ID 为键,每个账号独立配置:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
channels:
|
|
116
|
+
link:
|
|
117
|
+
accounts:
|
|
118
|
+
default:
|
|
119
|
+
host: "embtcp.bingolink.biz:20081"
|
|
120
|
+
accessToken: "token_for_default_account"
|
|
121
|
+
refreshToken: "refresh_token_for_default"
|
|
122
|
+
ssoUrl: "https://www.bingolink.biz/sso"
|
|
123
|
+
work:
|
|
124
|
+
host: "embtcp.bingolink.biz:20081"
|
|
125
|
+
accessToken: "token_for_work_account"
|
|
126
|
+
refreshToken: "refresh_token_for_work"
|
|
127
|
+
ssoUrl: "https://www.bingolink.biz/sso"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 绑定配置
|
|
131
|
+
|
|
132
|
+
使用 `bindings` 将不同账号路由到不同的 Agent:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
bindings:
|
|
136
|
+
- agentId: "main"
|
|
137
|
+
match:
|
|
138
|
+
channel: "link"
|
|
139
|
+
accountId: "default"
|
|
140
|
+
- agentId: "work-agent"
|
|
141
|
+
match:
|
|
142
|
+
channel: "link"
|
|
143
|
+
accountId: "work"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### JSON 格式示例
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"channels": {
|
|
151
|
+
"link": {
|
|
152
|
+
"accounts": {
|
|
153
|
+
"default": {
|
|
154
|
+
"host": "embtcp.bingolink.biz:20081",
|
|
155
|
+
"accessToken": "xxx",
|
|
156
|
+
"refreshToken": "xxx",
|
|
157
|
+
"ssoUrl": "https://www.bingolink.biz/sso"
|
|
158
|
+
},
|
|
159
|
+
"work": {
|
|
160
|
+
"host": "embtcp.bingolink.biz:20081",
|
|
161
|
+
"accessToken": "yyy",
|
|
162
|
+
"refreshToken": "yyy",
|
|
163
|
+
"ssoUrl": "https://www.bingolink.biz/sso"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"bindings": [
|
|
169
|
+
{ "agentId": "main", "match": { "channel": "link", "accountId": "default" } },
|
|
170
|
+
{ "agentId": "work-agent", "match": { "channel": "link", "accountId": "work" } }
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 向后兼容 (Backward Compatibility)
|
|
176
|
+
|
|
177
|
+
**无需修改现有配置**。如果没有 `accounts` 字段,插件会将根级别的 `host`、`accessToken` 等字段视为 `accountId = "default"` 的账号,行为与之前完全一致:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
# 旧配置格式,继续有效
|
|
181
|
+
channels:
|
|
182
|
+
link:
|
|
183
|
+
host: "embtcp.bingolink.biz:20081"
|
|
184
|
+
accessToken: "your_token"
|
|
185
|
+
refreshToken: "your_refresh_token"
|
|
186
|
+
ssoUrl: "https://www.bingolink.biz/sso"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> **注意**: `accounts` 字段与根级别的 `host`/`accessToken` 互斥。当 `accounts` 存在时,根级别字段会被忽略。
|
|
190
|
+
|
|
106
191
|
## 开发调试
|
|
107
192
|
|
|
108
193
|
### 单元测试
|
package/index.ts
CHANGED
|
File without changes
|
package/openclaw.plugin.json
CHANGED
|
@@ -8,11 +8,31 @@
|
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
10
10
|
"properties": {
|
|
11
|
-
"host": { "type": "string", "description": "Link Server Host
|
|
12
|
-
"ssoUrl": { "type": "string", "description": "SSO URL for refreshing token
|
|
11
|
+
"host": { "type": "string", "description": "Link Server Host", "default": "embtcp.bingolink.biz:20081" },
|
|
12
|
+
"ssoUrl": { "type": "string", "description": "SSO URL for refreshing token", "default": "https://www.bingolink.biz:443/sso" },
|
|
13
13
|
"accessToken": { "type": "string", "description": "User Access Token" },
|
|
14
14
|
"refreshToken": { "type": "string", "description": "User Refresh Token (Optional)" },
|
|
15
|
-
"
|
|
15
|
+
"groupId": { "type": "string", "description": "Group ID to monitor (enables group bot mode)" },
|
|
16
|
+
"botToken": { "type": "string", "description": "Group bot webhook token" },
|
|
17
|
+
"notificationApiUrl": { "type": "string", "description": "Webhook base URL", "default": "https://notificationapi.bingolink.biz:443/notificationapi" },
|
|
18
|
+
"accounts": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "Multi-account configuration. Keys are accountIds.",
|
|
21
|
+
"additionalProperties": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["accessToken"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"host": { "type": "string", "description": "Link Server Host", "default": "embtcp.bingolink.biz:20081" },
|
|
27
|
+
"accessToken": { "type": "string", "description": "User Access Token" },
|
|
28
|
+
"refreshToken": { "type": "string", "description": "User Refresh Token (Optional)" },
|
|
29
|
+
"ssoUrl": { "type": "string", "description": "SSO URL for refreshing token", "default": "https://www.bingolink.biz:443/sso" },
|
|
30
|
+
"groupId": { "type": "string", "description": "Group ID to monitor (enables group bot mode)" },
|
|
31
|
+
"botToken": { "type": "string", "description": "Group bot webhook token" },
|
|
32
|
+
"notificationApiUrl": { "type": "string", "description": "Webhook base URL", "default": "https://notificationapi.bingolink.biz:443/notificationapi" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
16
36
|
}
|
|
17
37
|
}
|
|
18
38
|
}
|
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -2,29 +2,34 @@ import {
|
|
|
2
2
|
type ClawdbotConfig,
|
|
3
3
|
type RuntimeEnv,
|
|
4
4
|
} from "openclaw/plugin-sdk";
|
|
5
|
-
import { LinkConfig } from "./types.js";
|
|
6
|
-
import { createLinkClient, getLinkClient } from "./client-manager.js";
|
|
5
|
+
import { LinkConfig, LinkChannelConfig, resolveAccountConfig, applyLinkDefaults } from "./types.js";
|
|
6
|
+
import { createLinkClient, getLinkClient, removeLinkClient } from "./client-manager.js";
|
|
7
7
|
import { getLinkRuntime } from "./runtime.js";
|
|
8
8
|
import { createLinkReplyDispatcher } from "./reply-dispatcher.js";
|
|
9
9
|
import { EmbMessage } from "./link/types.js";
|
|
10
10
|
import { MsgType, ParticipantType } from "./link/constants.js";
|
|
11
11
|
|
|
12
|
-
export async function startLinkBot(cfg: ClawdbotConfig, runtime: RuntimeEnv) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!linkCfg) {
|
|
17
|
-
// Channel not enabled or configured
|
|
12
|
+
export async function startLinkBot(cfg: ClawdbotConfig, runtime: RuntimeEnv, accountId: string = "default") {
|
|
13
|
+
const channelCfg = cfg.channels?.link as LinkChannelConfig | undefined;
|
|
14
|
+
|
|
15
|
+
if (!channelCfg) {
|
|
18
16
|
return;
|
|
19
17
|
}
|
|
20
|
-
|
|
18
|
+
|
|
19
|
+
const rawCfg = resolveAccountConfig(channelCfg, accountId);
|
|
20
|
+
|
|
21
|
+
if (!rawCfg) {
|
|
22
|
+
runtime.log?.(`Link: no config found for accountId=${accountId}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const linkCfg = applyLinkDefaults(rawCfg);
|
|
27
|
+
|
|
21
28
|
if (!linkCfg.accessToken) {
|
|
22
|
-
runtime.log?.(
|
|
29
|
+
runtime.log?.(`Link channel configured but missing accessToken for accountId=${accountId}`);
|
|
23
30
|
return;
|
|
24
31
|
}
|
|
25
|
-
|
|
26
|
-
const accountId = "default"; // For now single account support
|
|
27
|
-
|
|
32
|
+
|
|
28
33
|
let client = getLinkClient(accountId);
|
|
29
34
|
if (!client) {
|
|
30
35
|
client = createLinkClient(accountId, linkCfg);
|
|
@@ -51,8 +56,8 @@ export async function startLinkBot(cfg: ClawdbotConfig, runtime: RuntimeEnv) {
|
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
export async function stopLinkBot() {
|
|
55
|
-
|
|
59
|
+
export async function stopLinkBot(accountId: string = "default") {
|
|
60
|
+
removeLinkClient(accountId);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
async function handleLinkMessage(params: {
|
|
@@ -75,15 +80,17 @@ async function handleLinkMessage(params: {
|
|
|
75
80
|
|
|
76
81
|
const senderId = msg.fromId || "unknown";
|
|
77
82
|
|
|
78
|
-
// SECURITY: Only process messages sent by the bot user itself AND sent to itself (FileHelper/Self)
|
|
79
|
-
// Note: userId is populated in client.ts into verifyInfo during connection
|
|
80
83
|
const allowedUserId = linkCfg.verifyInfo?.userId;
|
|
81
84
|
const receiverId = msg.toId || "unknown";
|
|
82
85
|
|
|
83
|
-
console.log(`[LinkBot] allowedUserId=${allowedUserId}, senderId=${senderId}, receiverId=${receiverId}`);
|
|
86
|
+
console.log(`[LinkBot] allowedUserId=${allowedUserId}, senderId=${senderId}, receiverId=${receiverId}, toType=${msg.toType}`);
|
|
84
87
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
88
|
+
if (linkCfg.groupId) {
|
|
89
|
+
// Group mode: only accept messages sent to the configured group
|
|
90
|
+
if (msg.toId !== linkCfg.groupId || msg.toType !== ParticipantType.GROUP) return;
|
|
91
|
+
} else {
|
|
92
|
+
// FileHelper mode: only accept self-messages (sender == receiver == self)
|
|
93
|
+
if (!allowedUserId || senderId !== allowedUserId || receiverId !== allowedUserId) return;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
// Convert content to string if it's a buffer
|
package/src/channel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
2
2
|
import { startLinkBot, stopLinkBot } from "./bot.js";
|
|
3
3
|
import { linkOnboardingAdapter } from "./onboarding.js";
|
|
4
|
+
import { LinkChannelConfig } from "./types.js";
|
|
4
5
|
|
|
5
6
|
export const linkPlugin: ChannelPlugin<any> = {
|
|
6
7
|
id: "link",
|
|
@@ -51,17 +52,21 @@ export const linkPlugin: ChannelPlugin<any> = {
|
|
|
51
52
|
},
|
|
52
53
|
config: {
|
|
53
54
|
listAccountIds: (cfg) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const linkCfg = cfg?.channels?.link as LinkChannelConfig | undefined;
|
|
56
|
+
if (!linkCfg) return [];
|
|
57
|
+
if (linkCfg.accounts) {
|
|
58
|
+
return Object.keys(linkCfg.accounts);
|
|
59
|
+
}
|
|
60
|
+
// Backward compat: single-account root config
|
|
61
|
+
if (linkCfg.accessToken) return ["default"];
|
|
62
|
+
return [];
|
|
57
63
|
},
|
|
58
64
|
resolveAccount: (cfg, accountId) => {
|
|
59
|
-
// Return dummy account for now
|
|
60
65
|
return {
|
|
61
66
|
accountId,
|
|
62
67
|
enabled: true,
|
|
63
68
|
configured: true,
|
|
64
|
-
name:
|
|
69
|
+
name: `Link Bot (${accountId})`,
|
|
65
70
|
} as any;
|
|
66
71
|
},
|
|
67
72
|
defaultAccountId: (cfg) => "default",
|
|
@@ -77,11 +82,11 @@ export const linkPlugin: ChannelPlugin<any> = {
|
|
|
77
82
|
},
|
|
78
83
|
gateway: {
|
|
79
84
|
startAccount: async (ctx) => {
|
|
80
|
-
await startLinkBot(ctx.cfg, ctx.runtime);
|
|
81
|
-
|
|
85
|
+
await startLinkBot(ctx.cfg, ctx.runtime, ctx.accountId);
|
|
86
|
+
|
|
82
87
|
return new Promise((resolve) => {
|
|
83
88
|
ctx.abortSignal.addEventListener("abort", () => {
|
|
84
|
-
stopLinkBot();
|
|
89
|
+
stopLinkBot(ctx.accountId);
|
|
85
90
|
resolve(undefined);
|
|
86
91
|
});
|
|
87
92
|
});
|
package/src/client-manager.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LinkClient } from "./link/client.js";
|
|
2
|
-
import { LinkConfig } from "./types.js";
|
|
2
|
+
import { LinkConfig, LINK_DEFAULTS } from "./types.js";
|
|
3
3
|
|
|
4
4
|
const clients = new Map<string, LinkClient>();
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ export function createLinkClient(accountId: string, config: LinkConfig): LinkCli
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Parse host:port
|
|
17
|
-
const parts = config.host.split(':');
|
|
17
|
+
const parts = (config.host || LINK_DEFAULTS.host).split(':');
|
|
18
18
|
let host = parts[0];
|
|
19
19
|
let port = 20081; // Default port
|
|
20
20
|
|
package/src/link/client.ts
CHANGED
|
@@ -13,6 +13,7 @@ export class LinkClient extends EventEmitter {
|
|
|
13
13
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
14
14
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
15
15
|
private isConnected = false;
|
|
16
|
+
private immediateReconnect = false;
|
|
16
17
|
|
|
17
18
|
constructor(
|
|
18
19
|
public readonly config: {
|
|
@@ -181,11 +182,13 @@ export class LinkClient extends EventEmitter {
|
|
|
181
182
|
|
|
182
183
|
private scheduleReconnect() {
|
|
183
184
|
if (this.reconnectTimeout) return;
|
|
185
|
+
const delay = this.immediateReconnect ? 0 : 5000;
|
|
186
|
+
this.immediateReconnect = false;
|
|
184
187
|
this.reconnectTimeout = setTimeout(() => {
|
|
185
188
|
this.reconnectTimeout = null;
|
|
186
189
|
console.log('Reconnecting...');
|
|
187
190
|
this.connect();
|
|
188
|
-
},
|
|
191
|
+
}, delay);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
private processBuffer() {
|
|
@@ -250,6 +253,7 @@ export class LinkClient extends EventEmitter {
|
|
|
250
253
|
if (body) {
|
|
251
254
|
try {
|
|
252
255
|
const msg = decodeMessage(body);
|
|
256
|
+
this.sendReceipt(msg.id);
|
|
253
257
|
this.emit('message', msg);
|
|
254
258
|
} catch (e) {
|
|
255
259
|
console.error('Failed to decode message:', e);
|
|
@@ -278,11 +282,22 @@ export class LinkClient extends EventEmitter {
|
|
|
278
282
|
// Ignore for now or log
|
|
279
283
|
// console.log(`Received cmd: 0x${cmdCode.toString(16)}, body: ${body}`);
|
|
280
284
|
break;
|
|
285
|
+
case CmdCodes.FORCE_RECONNECT_DOWN:
|
|
286
|
+
console.log('Server signaled session reset (0x6b), will reconnect immediately on disconnect');
|
|
287
|
+
this.immediateReconnect = true;
|
|
288
|
+
break;
|
|
281
289
|
default:
|
|
282
290
|
console.log(`Received unknown cmd: 0x${cmdCode.toString(16)}, body: ${body}`);
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
293
|
|
|
294
|
+
private sendReceipt(msgId: unknown) {
|
|
295
|
+
if (!this.socket || !this.isConnected) return;
|
|
296
|
+
console.log('Sending receipt for message:', msgId);
|
|
297
|
+
const packet = encodePacket(CmdCodes.SEND_MSG_RECEIPT, String(msgId));
|
|
298
|
+
this.socket.write(packet);
|
|
299
|
+
}
|
|
300
|
+
|
|
286
301
|
private async refreshAccessToken() {
|
|
287
302
|
if (!this.config.refreshToken || !this.config.ssoUrl) {
|
|
288
303
|
console.error('Cannot refresh token: Missing refreshToken or ssoUrl in config');
|
package/src/link/constants.ts
CHANGED
package/src/link/message.d.ts
CHANGED
|
File without changes
|
package/src/link/message.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/
|
|
2
|
-
import * as $
|
|
2
|
+
import * as $protobufNs from "protobufjs/minimal.js";
|
|
3
|
+
const $protobuf = $protobufNs.default || $protobufNs;
|
|
3
4
|
|
|
4
5
|
// Common aliases
|
|
5
6
|
const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
|
package/src/link/protocol.ts
CHANGED
|
File without changes
|
package/src/link/types.ts
CHANGED
|
File without changes
|
package/src/onboarding.ts
CHANGED
|
File without changes
|
package/src/reply-dispatcher.ts
CHANGED
|
File without changes
|
package/src/runtime.ts
CHANGED
|
File without changes
|
package/src/send.ts
CHANGED
|
@@ -8,32 +8,48 @@ export async function sendMessageLink(params: {
|
|
|
8
8
|
to: string;
|
|
9
9
|
text: string;
|
|
10
10
|
accountId?: string;
|
|
11
|
-
cfg: LinkConfig;
|
|
11
|
+
cfg: LinkConfig;
|
|
12
12
|
}): Promise<void> {
|
|
13
|
+
const { cfg } = params;
|
|
14
|
+
|
|
15
|
+
// Group bot mode: send via HTTP webhook
|
|
16
|
+
if (cfg.groupId && cfg.botToken && cfg.notificationApiUrl) {
|
|
17
|
+
const url = `${cfg.notificationApiUrl}/hook/send?token=${cfg.botToken}`;
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
msgType: 'text',
|
|
23
|
+
content: { text: params.text }
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(`Group bot webhook failed: ${res.status} ${res.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// FileHelper mode: send via TCP self-message
|
|
13
33
|
const accountId = params.accountId || "default";
|
|
14
34
|
const client = getLinkClient(accountId);
|
|
15
|
-
|
|
35
|
+
|
|
16
36
|
if (!client) {
|
|
17
37
|
throw new Error(`Link client not found for account ${accountId}`);
|
|
18
38
|
}
|
|
19
39
|
|
|
20
|
-
// SECURITY: Only allow sending messages to self
|
|
21
|
-
// Note: verifyInfo.userId is populated in client.ts
|
|
40
|
+
// SECURITY: Only allow sending messages to self in non-group mode
|
|
22
41
|
const allowedUserId = client.config.verifyInfo?.userId;
|
|
23
42
|
if (!allowedUserId || params.to !== allowedUserId) {
|
|
24
|
-
|
|
25
|
-
// Silently fail or throw? Throwing might cause retries or errors in logs.
|
|
26
|
-
// Let's throw for now to make it explicit.
|
|
27
|
-
throw new Error(`Link: Sending messages to others (${params.to}) is restricted. Only self-messages allowed.`);
|
|
43
|
+
throw new Error(`Link: Sending messages to others (${params.to}) is restricted. Only self-messages allowed in non-group mode.`);
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
const msg: EmbMessage = {
|
|
31
47
|
id: randomUUID(),
|
|
32
48
|
type: MsgType.TEXT,
|
|
33
49
|
content: params.text,
|
|
34
|
-
fromType: ParticipantType.USER,
|
|
50
|
+
fromType: ParticipantType.USER,
|
|
35
51
|
fromId: allowedUserId,
|
|
36
|
-
toType: ParticipantType.USER,
|
|
52
|
+
toType: ParticipantType.USER,
|
|
37
53
|
toId: params.to,
|
|
38
54
|
sendTime: Date.now(),
|
|
39
55
|
read: false
|
package/src/types.ts
CHANGED
|
@@ -1,18 +1,63 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { ClientVerifyInfo } from "./link/types.js";
|
|
3
3
|
|
|
4
|
+
export const LINK_DEFAULTS = {
|
|
5
|
+
host: 'embtcp.bingolink.biz:20081',
|
|
6
|
+
ssoUrl: 'https://www.bingolink.biz:443/sso',
|
|
7
|
+
notificationApiUrl: 'https://notificationapi.bingolink.biz:443/notificationapi',
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
4
10
|
export interface LinkConfig {
|
|
5
|
-
host
|
|
11
|
+
host?: string; // Default: embtcp.bingolink.biz:20081
|
|
6
12
|
// port: number; // merged into host
|
|
7
13
|
accessToken: string;
|
|
8
14
|
refreshToken?: string;
|
|
9
|
-
ssoUrl?: string;
|
|
15
|
+
ssoUrl?: string; // Default: https://www.bingolink.biz:443/sso
|
|
10
16
|
verifyInfo?: Partial<ClientVerifyInfo>;
|
|
11
17
|
heartbeatIntervalMs?: number;
|
|
12
18
|
protocol?: "tcp" | "ws"; // Default tcp
|
|
19
|
+
// Group bot config
|
|
20
|
+
groupId?: string; // Group ID to monitor
|
|
21
|
+
botToken?: string; // Group bot webhook token
|
|
22
|
+
notificationApiUrl?: string; // Default: https://notificationapi.bingolink.biz:443/notificationapi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fill in default values for optional fields that have known defaults. */
|
|
26
|
+
export function applyLinkDefaults(cfg: LinkConfig): LinkConfig {
|
|
27
|
+
return {
|
|
28
|
+
...cfg,
|
|
29
|
+
host: cfg.host || LINK_DEFAULTS.host,
|
|
30
|
+
ssoUrl: cfg.ssoUrl || LINK_DEFAULTS.ssoUrl,
|
|
31
|
+
notificationApiUrl: cfg.notificationApiUrl || LINK_DEFAULTS.notificationApiUrl,
|
|
32
|
+
};
|
|
13
33
|
}
|
|
14
34
|
|
|
15
35
|
export interface LinkAccountConfig {
|
|
16
36
|
accountId?: string;
|
|
17
37
|
config: LinkConfig;
|
|
18
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Top-level channels.link config — supports both single-account and multi-account */
|
|
41
|
+
export interface LinkChannelConfig extends Partial<LinkConfig> {
|
|
42
|
+
accounts?: Record<string, LinkConfig>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the LinkConfig for a given accountId.
|
|
47
|
+
* When `accounts` is present, looks up the named entry.
|
|
48
|
+
* Otherwise falls back to the root config (backward-compat single-account "default").
|
|
49
|
+
*/
|
|
50
|
+
export function resolveAccountConfig(
|
|
51
|
+
channelCfg: LinkChannelConfig,
|
|
52
|
+
accountId: string
|
|
53
|
+
): LinkConfig | undefined {
|
|
54
|
+
if (channelCfg.accounts) {
|
|
55
|
+
return channelCfg.accounts[accountId];
|
|
56
|
+
}
|
|
57
|
+
// Backward compat: treat root-level fields as the "default" account
|
|
58
|
+
if (accountId === "default" && channelCfg.host && channelCfg.accessToken) {
|
|
59
|
+
const { accounts, ...rest } = channelCfg as any;
|
|
60
|
+
return rest as LinkConfig;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|