@art_style666/hi-light 1.0.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/README.md +82 -0
- package/dist/index.js +732 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# HiLight 安装说明
|
|
2
|
+
|
|
3
|
+
## 安装方法
|
|
4
|
+
|
|
5
|
+
OpenClaw安装: https://github.com/openclaw/openclaw#install-recommended
|
|
6
|
+
|
|
7
|
+
插件安装:https://my.feishu.cn/wiki/CO5Vw6cG9iZUpckIvJoc279VnFc
|
|
8
|
+
|
|
9
|
+
## 安装前准备
|
|
10
|
+
|
|
11
|
+
先确认电脑里有这两个工具:
|
|
12
|
+
|
|
13
|
+
- `Node.js`(建议 18 或更高)
|
|
14
|
+
- `OpenClaw`
|
|
15
|
+
|
|
16
|
+
在终端里输入下面两行,能看到版本号就说明已经装好:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node -v
|
|
20
|
+
openclaw --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 安装步骤(源码安装)
|
|
24
|
+
|
|
25
|
+
### 1. 准备源码
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git@github.com:Gongcong/hi-light-plugin.git
|
|
29
|
+
cd hi-light-plugin
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
如果你已经在插件源码目录里了,可以跳过这一步。
|
|
33
|
+
|
|
34
|
+
### 2. 安装依赖并打包
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install
|
|
38
|
+
npm run build
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. 用本地源码安装到 OpenClaw
|
|
42
|
+
|
|
43
|
+
把下面命令里的路径改成你电脑上的插件目录绝对路径:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openclaw plugins install --link /绝对路径/hi-light-plugin
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. 打开配置文件
|
|
50
|
+
|
|
51
|
+
编辑文件:`~/.openclaw/openclaw.json`
|
|
52
|
+
|
|
53
|
+
把下面这段加到 `channels` 里(没有就新建):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
"channels": {
|
|
57
|
+
"hi-light": {
|
|
58
|
+
"enabled": true,
|
|
59
|
+
"wsUrl": "ws://你的服务地址:8080/ws",
|
|
60
|
+
"authToken": "你的API KEY"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
API KEY 获取方式:
|
|
66
|
+
|
|
67
|
+
各大应用商店,下载 HiLight APP,点击设置 -> 帐号管理 -> 获取 API KEY
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
<img src="https://github.com/user-attachments/assets/6b55651c-ac08-432f-948b-3f82902839c4" alt="API KEY 获取示意图" width="420" />
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### 5. 重启网关让配置生效
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
openclaw gateway restart
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 安装完成怎么检查
|
|
80
|
+
|
|
81
|
+
重启后如果没有报错,基本就安装成功了。
|
|
82
|
+
如果想更稳妥,可以看网关日志里是否出现 `hi-light` 连接成功的信息。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/accounts.ts
|
|
12
|
+
function resolveHiLightAccount(params) {
|
|
13
|
+
const { cfg, accountId } = params;
|
|
14
|
+
const id = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
15
|
+
const channels = cfg.channels;
|
|
16
|
+
const hlCfg = channels?.["hi-light"] ?? {};
|
|
17
|
+
const wsUrl = hlCfg.wsUrl;
|
|
18
|
+
const authToken = hlCfg.authToken;
|
|
19
|
+
const configured = !!wsUrl;
|
|
20
|
+
const enabled = hlCfg.enabled !== false;
|
|
21
|
+
return {
|
|
22
|
+
accountId: id,
|
|
23
|
+
enabled,
|
|
24
|
+
configured,
|
|
25
|
+
config: hlCfg,
|
|
26
|
+
wsUrl,
|
|
27
|
+
authToken
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function listHiLightAccountIds(cfg) {
|
|
31
|
+
const channels = cfg.channels;
|
|
32
|
+
if (!channels?.["hi-light"]) return [];
|
|
33
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
34
|
+
}
|
|
35
|
+
function resolveDefaultHiLightAccountId(_cfg) {
|
|
36
|
+
return DEFAULT_ACCOUNT_ID;
|
|
37
|
+
}
|
|
38
|
+
var DEFAULT_ACCOUNT_ID;
|
|
39
|
+
var init_accounts = __esm({
|
|
40
|
+
"src/accounts.ts"() {
|
|
41
|
+
DEFAULT_ACCOUNT_ID = "default";
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// src/send.ts
|
|
46
|
+
var send_exports = {};
|
|
47
|
+
__export(send_exports, {
|
|
48
|
+
sendHiLightText: () => sendHiLightText
|
|
49
|
+
});
|
|
50
|
+
async function sendHiLightText(ctx) {
|
|
51
|
+
console.warn(
|
|
52
|
+
`hi-light: outbound send to=${ctx.to} is not fully supported yet. Use inbound message flow instead.`
|
|
53
|
+
);
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: new Error("hi-light outbound send not yet implemented")
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var init_send = __esm({
|
|
60
|
+
"src/send.ts"() {
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/runtime.ts
|
|
65
|
+
function setHiLightRuntime(next) {
|
|
66
|
+
runtime = next;
|
|
67
|
+
}
|
|
68
|
+
function getHiLightRuntime() {
|
|
69
|
+
if (!runtime) {
|
|
70
|
+
throw new Error("HiLight runtime not initialized");
|
|
71
|
+
}
|
|
72
|
+
return runtime;
|
|
73
|
+
}
|
|
74
|
+
var runtime;
|
|
75
|
+
var init_runtime = __esm({
|
|
76
|
+
"src/runtime.ts"() {
|
|
77
|
+
runtime = null;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/ws-send.ts
|
|
82
|
+
import WebSocket from "ws";
|
|
83
|
+
function sendHiLightEnvelope(params) {
|
|
84
|
+
const { ws, envelope, log, tag } = params;
|
|
85
|
+
const label = tag ? `${tag}` : envelope.action;
|
|
86
|
+
const raw = JSON.stringify(envelope);
|
|
87
|
+
log?.debug?.(`hi-light: ws send start action=${envelope.action} tag=${label} payload=${raw}`);
|
|
88
|
+
if (typeof ws.readyState === "number" && ws.readyState !== WebSocket.OPEN) {
|
|
89
|
+
log?.warn(
|
|
90
|
+
`hi-light: ws send skipped (socket not open) action=${envelope.action} tag=${label} readyState=${ws.readyState} payload=${raw}`
|
|
91
|
+
);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
ws.send(raw, (err) => {
|
|
96
|
+
if (err) {
|
|
97
|
+
log?.error(
|
|
98
|
+
`hi-light: ws send failed action=${envelope.action} tag=${label} error=${err.message} payload=${raw}`
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
log?.debug?.(`hi-light: ws send success action=${envelope.action} tag=${label}`);
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
107
|
+
log?.error(
|
|
108
|
+
`hi-light: ws send failed action=${envelope.action} tag=${label} error=${errorText} payload=${raw}`
|
|
109
|
+
);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
var init_ws_send = __esm({
|
|
114
|
+
"src/ws-send.ts"() {
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// src/reply-dispatcher.ts
|
|
119
|
+
function createHiLightReplyDispatcher(params) {
|
|
120
|
+
const { ws, config, userId, context, log } = params;
|
|
121
|
+
const core = getHiLightRuntime();
|
|
122
|
+
const stringifyRaw = (value) => {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.stringify(value);
|
|
125
|
+
} catch {
|
|
126
|
+
return "[unserializable]";
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const textChunks = [];
|
|
130
|
+
let hasSentReply = false;
|
|
131
|
+
let sawFinalPayload = false;
|
|
132
|
+
let streamSeq = 0;
|
|
133
|
+
const flushBufferedReply = () => {
|
|
134
|
+
if (hasSentReply) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!sawFinalPayload && textChunks.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const fullText = textChunks.join("");
|
|
141
|
+
const replyEnvelope = {
|
|
142
|
+
context,
|
|
143
|
+
action: "reply",
|
|
144
|
+
payload: {
|
|
145
|
+
userId,
|
|
146
|
+
text: fullText,
|
|
147
|
+
done: true
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
if (sendHiLightEnvelope({ ws, envelope: replyEnvelope, log, tag: "buffered-reply" })) {
|
|
151
|
+
hasSentReply = true;
|
|
152
|
+
textChunks.length = 0;
|
|
153
|
+
log?.debug?.(`hi-light: sent buffered reply (len=${fullText.length})`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const {
|
|
157
|
+
dispatcher,
|
|
158
|
+
replyOptions,
|
|
159
|
+
markDispatchIdle: sdkMarkDispatchIdle
|
|
160
|
+
} = core.channel.reply.createReplyDispatcherWithTyping({
|
|
161
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(config),
|
|
162
|
+
deliver: async (payload, info) => {
|
|
163
|
+
const text = payload.text ?? "";
|
|
164
|
+
const kind = info?.kind ?? "unknown";
|
|
165
|
+
streamSeq += 1;
|
|
166
|
+
log?.debug?.(
|
|
167
|
+
`hi-light: openclaw stream chunk seq=${streamSeq} kind=${kind} textLen=${text.length} raw=${stringifyRaw({ payload, info })}`
|
|
168
|
+
);
|
|
169
|
+
if (text.length > 0) {
|
|
170
|
+
textChunks.push(text);
|
|
171
|
+
}
|
|
172
|
+
const isFinal = kind === "final";
|
|
173
|
+
if (isFinal) {
|
|
174
|
+
sawFinalPayload = true;
|
|
175
|
+
flushBufferedReply();
|
|
176
|
+
} else {
|
|
177
|
+
log?.debug?.(
|
|
178
|
+
`hi-light: buffering chunk (kind=${info?.kind ?? "unknown"}, len=${text.length})`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
onReplyStart: async () => {
|
|
183
|
+
const typingEnvelope = {
|
|
184
|
+
context,
|
|
185
|
+
action: "typing",
|
|
186
|
+
payload: { userId }
|
|
187
|
+
};
|
|
188
|
+
sendHiLightEnvelope({ ws, envelope: typingEnvelope, log, tag: "typing" });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const markDispatchIdle = () => {
|
|
192
|
+
flushBufferedReply();
|
|
193
|
+
sdkMarkDispatchIdle();
|
|
194
|
+
};
|
|
195
|
+
return { dispatcher, replyOptions, markDispatchIdle };
|
|
196
|
+
}
|
|
197
|
+
var init_reply_dispatcher = __esm({
|
|
198
|
+
"src/reply-dispatcher.ts"() {
|
|
199
|
+
init_runtime();
|
|
200
|
+
init_ws_send();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// src/bot.ts
|
|
205
|
+
async function handleHiLightMessage(params) {
|
|
206
|
+
const { ws, raw, config, accountId, log } = params;
|
|
207
|
+
const core = getHiLightRuntime();
|
|
208
|
+
const stringifyRaw = (value) => {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.stringify(value);
|
|
211
|
+
} catch {
|
|
212
|
+
return "[unserializable]";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
let envelope;
|
|
216
|
+
try {
|
|
217
|
+
envelope = JSON.parse(raw);
|
|
218
|
+
} catch {
|
|
219
|
+
log?.warn(`hi-light: failed to parse message: ${raw.slice(0, 200)}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (envelope.action !== "msg") {
|
|
223
|
+
log?.debug?.(`hi-light: ignoring action: ${envelope.action}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const payload = envelope.payload;
|
|
227
|
+
const userId = typeof payload.userId === "string" || typeof payload.userId === "number" ? String(payload.userId).trim() : "";
|
|
228
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
229
|
+
if (!userId || !text.trim()) {
|
|
230
|
+
log?.warn("hi-light: msg payload missing userId or text");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const context = typeof envelope.context === "string" && envelope.context.trim() ? envelope.context.trim() : "default";
|
|
234
|
+
const senderName = typeof payload.userName === "string" && payload.userName.trim() ? payload.userName.trim() : userId;
|
|
235
|
+
log?.info(`hi-light: msg from user=${userId} context=${context}`);
|
|
236
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
237
|
+
cfg: config,
|
|
238
|
+
channel: "hi-light",
|
|
239
|
+
accountId,
|
|
240
|
+
peer: {
|
|
241
|
+
kind: "direct",
|
|
242
|
+
id: userId
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
246
|
+
Body: text,
|
|
247
|
+
BodyForAgent: text,
|
|
248
|
+
From: userId,
|
|
249
|
+
To: "hi-light",
|
|
250
|
+
Provider: "hi-light",
|
|
251
|
+
AccountId: route.accountId,
|
|
252
|
+
ChatType: "direct",
|
|
253
|
+
SessionKey: route.sessionKey,
|
|
254
|
+
IsGroupchat: false,
|
|
255
|
+
SenderName: senderName
|
|
256
|
+
});
|
|
257
|
+
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
258
|
+
agentId: route.agentId
|
|
259
|
+
});
|
|
260
|
+
await core.channel.session.recordInboundSession({
|
|
261
|
+
storePath,
|
|
262
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
263
|
+
ctx: ctxPayload,
|
|
264
|
+
onRecordError: (err) => {
|
|
265
|
+
log?.error?.(`hi-light: failed to record inbound session: ${String(err)}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createHiLightReplyDispatcher({
|
|
269
|
+
ws,
|
|
270
|
+
config,
|
|
271
|
+
userId,
|
|
272
|
+
context,
|
|
273
|
+
log
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
log?.debug?.(`hi-light: openclaw inbound ctx raw=${stringifyRaw(ctxPayload)}`);
|
|
277
|
+
await core.channel.reply.dispatchReplyFromConfig({
|
|
278
|
+
ctx: ctxPayload,
|
|
279
|
+
cfg: config,
|
|
280
|
+
dispatcher,
|
|
281
|
+
replyOptions
|
|
282
|
+
});
|
|
283
|
+
} catch (err) {
|
|
284
|
+
log?.error(`hi-light: dispatch error: ${err}`);
|
|
285
|
+
const dispatchErrorRaw = err instanceof Error ? {
|
|
286
|
+
name: err.name,
|
|
287
|
+
message: err.message,
|
|
288
|
+
stack: err.stack
|
|
289
|
+
} : err;
|
|
290
|
+
log?.error(`hi-light: openclaw dispatch error raw=${JSON.stringify(dispatchErrorRaw)}`);
|
|
291
|
+
const errorEnvelope = {
|
|
292
|
+
context,
|
|
293
|
+
action: "error",
|
|
294
|
+
payload: {
|
|
295
|
+
userId,
|
|
296
|
+
code: "DISPATCH_FAILED",
|
|
297
|
+
message: err instanceof Error ? err.message : String(err)
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
sendHiLightEnvelope({ ws, envelope: errorEnvelope, log, tag: "dispatch-error" });
|
|
301
|
+
} finally {
|
|
302
|
+
markDispatchIdle?.();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
var init_bot = __esm({
|
|
306
|
+
"src/bot.ts"() {
|
|
307
|
+
init_reply_dispatcher();
|
|
308
|
+
init_runtime();
|
|
309
|
+
init_ws_send();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// src/monitor.ts
|
|
314
|
+
var monitor_exports = {};
|
|
315
|
+
__export(monitor_exports, {
|
|
316
|
+
startHiLightMonitor: () => startHiLightMonitor
|
|
317
|
+
});
|
|
318
|
+
import { randomUUID } from "node:crypto";
|
|
319
|
+
import WebSocket2 from "ws";
|
|
320
|
+
function resolveConnectWsUrl(wsUrl) {
|
|
321
|
+
const uuid = randomUUID();
|
|
322
|
+
if (wsUrl.includes(WS_UUID_PLACEHOLDER)) {
|
|
323
|
+
return wsUrl.replace(WS_UUID_PLACEHOLDER, uuid);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const parsed = new URL(wsUrl);
|
|
327
|
+
parsed.pathname = `${parsed.pathname.replace(/\/+$/, "")}/${uuid}`;
|
|
328
|
+
return parsed.toString();
|
|
329
|
+
} catch {
|
|
330
|
+
const trimmed = wsUrl.replace(/\/+$/, "");
|
|
331
|
+
return `${trimmed}/${uuid}`;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function startHiLightMonitor(params) {
|
|
335
|
+
const { config, abortSignal, accountId, log } = params;
|
|
336
|
+
const account = resolveHiLightAccount({ cfg: config, accountId });
|
|
337
|
+
if (!account.wsUrl) {
|
|
338
|
+
log?.error("hi-light: wsUrl is not configured, cannot start monitor");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const wsUrlTemplate = account.wsUrl;
|
|
342
|
+
const authToken = account.authToken;
|
|
343
|
+
const baseReconnectMs = account.config.reconnectIntervalMs ?? 3e3;
|
|
344
|
+
const maxReconnectMs = account.config.maxReconnectIntervalMs ?? 3e4;
|
|
345
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
346
|
+
let reconnectAttempts = 0;
|
|
347
|
+
let stopped = false;
|
|
348
|
+
let activeWs = null;
|
|
349
|
+
let heartbeatTimer = null;
|
|
350
|
+
let reconnectTimer = null;
|
|
351
|
+
let missedPongs = 0;
|
|
352
|
+
const MAX_MISSED_PONGS = 2;
|
|
353
|
+
let stopResolved = false;
|
|
354
|
+
let resolveStopped;
|
|
355
|
+
const stoppedPromise = new Promise((resolve) => {
|
|
356
|
+
resolveStopped = resolve;
|
|
357
|
+
});
|
|
358
|
+
function clearHeartbeat() {
|
|
359
|
+
if (heartbeatTimer) {
|
|
360
|
+
clearInterval(heartbeatTimer);
|
|
361
|
+
heartbeatTimer = null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function clearReconnect() {
|
|
365
|
+
if (reconnectTimer) {
|
|
366
|
+
clearTimeout(reconnectTimer);
|
|
367
|
+
reconnectTimer = null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function resolveStoppedOnce() {
|
|
371
|
+
if (stopResolved) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
stopResolved = true;
|
|
375
|
+
resolveStopped();
|
|
376
|
+
}
|
|
377
|
+
function stopAndDispose(reason) {
|
|
378
|
+
if (stopped) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
stopped = true;
|
|
382
|
+
clearHeartbeat();
|
|
383
|
+
clearReconnect();
|
|
384
|
+
if (activeWs) {
|
|
385
|
+
log?.info(`hi-light: stopping monitor (${reason}), closing WS connection`);
|
|
386
|
+
try {
|
|
387
|
+
activeWs.terminate();
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
activeWs = null;
|
|
391
|
+
}
|
|
392
|
+
resolveStoppedOnce();
|
|
393
|
+
}
|
|
394
|
+
const onAbort = () => {
|
|
395
|
+
stopAndDispose("gateway shutdown");
|
|
396
|
+
};
|
|
397
|
+
if (abortSignal.aborted) {
|
|
398
|
+
stopAndDispose("already aborted");
|
|
399
|
+
await stoppedPromise;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
403
|
+
function connect() {
|
|
404
|
+
if (stopped || abortSignal.aborted) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const headers = {};
|
|
408
|
+
if (authToken) {
|
|
409
|
+
headers["Authorization"] = `${authToken}`;
|
|
410
|
+
}
|
|
411
|
+
const connectWsUrl = resolveConnectWsUrl(wsUrlTemplate);
|
|
412
|
+
log?.info(`hi-light: connecting to ${connectWsUrl} (attempt ${reconnectAttempts + 1})`);
|
|
413
|
+
const ws = new WebSocket2(connectWsUrl, { headers });
|
|
414
|
+
activeWs = ws;
|
|
415
|
+
ws.on("open", () => {
|
|
416
|
+
if (stopped || abortSignal.aborted) {
|
|
417
|
+
try {
|
|
418
|
+
ws.terminate();
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
reconnectAttempts = 0;
|
|
424
|
+
log?.info(`hi-light: connected to ${connectWsUrl}`);
|
|
425
|
+
sendHiLightEnvelope({
|
|
426
|
+
ws,
|
|
427
|
+
log,
|
|
428
|
+
tag: "connected",
|
|
429
|
+
envelope: {
|
|
430
|
+
context: "",
|
|
431
|
+
action: "connected",
|
|
432
|
+
payload: { pluginId: "hi-light", accountId }
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
clearHeartbeat();
|
|
436
|
+
missedPongs = 0;
|
|
437
|
+
heartbeatTimer = setInterval(() => {
|
|
438
|
+
if (missedPongs >= MAX_MISSED_PONGS) {
|
|
439
|
+
log?.warn(
|
|
440
|
+
`hi-light: missed ${missedPongs} pongs, connection seems dead. Reconnecting...`
|
|
441
|
+
);
|
|
442
|
+
clearHeartbeat();
|
|
443
|
+
ws.close(4e3, "pong timeout");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
missedPongs++;
|
|
447
|
+
sendHiLightEnvelope({
|
|
448
|
+
ws,
|
|
449
|
+
log,
|
|
450
|
+
tag: `heartbeat-${missedPongs}`,
|
|
451
|
+
envelope: {
|
|
452
|
+
context: "",
|
|
453
|
+
action: "ping",
|
|
454
|
+
payload: { ts: Date.now() }
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
458
|
+
});
|
|
459
|
+
ws.on("message", (data) => {
|
|
460
|
+
if (stopped) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const raw = data.toString();
|
|
464
|
+
log?.debug?.(`hi-light: received raw msg len=${raw.length} raw=${raw}`);
|
|
465
|
+
try {
|
|
466
|
+
const envelope = JSON.parse(raw);
|
|
467
|
+
if (envelope.action === "pong") {
|
|
468
|
+
missedPongs = 0;
|
|
469
|
+
log?.debug?.("hi-light: pong received, connection healthy");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
handleHiLightMessage({
|
|
475
|
+
ws,
|
|
476
|
+
raw,
|
|
477
|
+
config,
|
|
478
|
+
accountId,
|
|
479
|
+
log
|
|
480
|
+
}).catch((err) => {
|
|
481
|
+
log?.error(`hi-light: error handling message: ${err}`);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
ws.on("close", (code, reason) => {
|
|
485
|
+
clearHeartbeat();
|
|
486
|
+
if (activeWs === ws) {
|
|
487
|
+
activeWs = null;
|
|
488
|
+
}
|
|
489
|
+
if (stopped || abortSignal.aborted) {
|
|
490
|
+
log?.info("hi-light: connection closed (gateway stopped)");
|
|
491
|
+
resolveStoppedOnce();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const reasonStr = reason?.toString() || "unknown";
|
|
495
|
+
log?.warn(`hi-light: closed (code=${code}, reason=${reasonStr}), reconnecting...`);
|
|
496
|
+
scheduleReconnect();
|
|
497
|
+
});
|
|
498
|
+
ws.on("error", (err) => {
|
|
499
|
+
log?.error(`hi-light: connection error: ${err.message}`);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function scheduleReconnect() {
|
|
503
|
+
if (stopped || abortSignal.aborted) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
reconnectAttempts++;
|
|
507
|
+
const delay = Math.min(baseReconnectMs * Math.pow(2, reconnectAttempts - 1), maxReconnectMs);
|
|
508
|
+
log?.info(`hi-light: reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
|
|
509
|
+
clearReconnect();
|
|
510
|
+
reconnectTimer = setTimeout(() => {
|
|
511
|
+
reconnectTimer = null;
|
|
512
|
+
connect();
|
|
513
|
+
}, delay);
|
|
514
|
+
}
|
|
515
|
+
connect();
|
|
516
|
+
try {
|
|
517
|
+
await stoppedPromise;
|
|
518
|
+
} finally {
|
|
519
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
520
|
+
clearHeartbeat();
|
|
521
|
+
clearReconnect();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
var WS_UUID_PLACEHOLDER;
|
|
525
|
+
var init_monitor = __esm({
|
|
526
|
+
"src/monitor.ts"() {
|
|
527
|
+
init_accounts();
|
|
528
|
+
init_bot();
|
|
529
|
+
init_ws_send();
|
|
530
|
+
WS_UUID_PLACEHOLDER = "{UUIDD}";
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// index.ts
|
|
535
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
536
|
+
|
|
537
|
+
// src/channel.ts
|
|
538
|
+
init_accounts();
|
|
539
|
+
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID2 } from "openclaw/plugin-sdk";
|
|
540
|
+
var meta = {
|
|
541
|
+
id: "hi-light",
|
|
542
|
+
label: "HiLight",
|
|
543
|
+
selectionLabel: "HiLight WebSocket Bridge",
|
|
544
|
+
docsPath: "/channels/hi-light",
|
|
545
|
+
docsLabel: "hi-light",
|
|
546
|
+
blurb: "HiLight \u2014 WebSocket bridge channel, connects to an external WS server.",
|
|
547
|
+
order: 80
|
|
548
|
+
};
|
|
549
|
+
var hiLightPlugin = {
|
|
550
|
+
id: "hi-light",
|
|
551
|
+
meta: { ...meta },
|
|
552
|
+
capabilities: {
|
|
553
|
+
chatTypes: ["direct"]
|
|
554
|
+
},
|
|
555
|
+
reload: { configPrefixes: ["channels.hi-light"] },
|
|
556
|
+
configSchema: {
|
|
557
|
+
schema: {
|
|
558
|
+
type: "object",
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
properties: {
|
|
561
|
+
enabled: { type: "boolean" },
|
|
562
|
+
wsUrl: { type: "string", format: "uri" },
|
|
563
|
+
authToken: { type: "string" },
|
|
564
|
+
reconnectIntervalMs: { type: "integer", minimum: 1e3 },
|
|
565
|
+
maxReconnectIntervalMs: { type: "integer", minimum: 1e3 },
|
|
566
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
567
|
+
allowFrom: {
|
|
568
|
+
type: "array",
|
|
569
|
+
items: { oneOf: [{ type: "string" }, { type: "number" }] }
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
uiHints: {
|
|
574
|
+
wsUrl: {
|
|
575
|
+
label: "WebSocket URL",
|
|
576
|
+
help: "Base WebSocket URL. Plugin appends a new UUID path segment on every connection.",
|
|
577
|
+
placeholder: "wss://host/path"
|
|
578
|
+
},
|
|
579
|
+
authToken: {
|
|
580
|
+
label: "Auth Token",
|
|
581
|
+
help: "Token sent as-is in the Authorization header during WS handshake",
|
|
582
|
+
sensitive: true
|
|
583
|
+
},
|
|
584
|
+
reconnectIntervalMs: {
|
|
585
|
+
label: "Reconnect Interval (ms)",
|
|
586
|
+
help: "Base interval for reconnection attempts (exponential backoff)",
|
|
587
|
+
advanced: true
|
|
588
|
+
},
|
|
589
|
+
maxReconnectIntervalMs: {
|
|
590
|
+
label: "Max Reconnect Interval (ms)",
|
|
591
|
+
help: "Maximum interval between reconnection attempts",
|
|
592
|
+
advanced: true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
// ── Config Adapter ──────────────────────────────────────────────────────
|
|
597
|
+
config: {
|
|
598
|
+
listAccountIds: (cfg) => listHiLightAccountIds(cfg),
|
|
599
|
+
resolveAccount: (cfg, accountId) => resolveHiLightAccount({ cfg, accountId }),
|
|
600
|
+
defaultAccountId: (_cfg) => resolveDefaultHiLightAccountId(_cfg),
|
|
601
|
+
isEnabled: (account) => account.enabled,
|
|
602
|
+
isConfigured: (account) => account.configured,
|
|
603
|
+
unconfiguredReason: () => "wsUrl is not set in channels.hi-light config",
|
|
604
|
+
resolveAllowFrom: ({ cfg }) => {
|
|
605
|
+
const hlCfg = resolveHiLightAccount({ cfg }).config;
|
|
606
|
+
return hlCfg.allowFrom;
|
|
607
|
+
},
|
|
608
|
+
describeAccount: (account) => ({
|
|
609
|
+
accountId: account.accountId,
|
|
610
|
+
enabled: account.enabled,
|
|
611
|
+
configured: account.configured,
|
|
612
|
+
wsUrl: account.wsUrl
|
|
613
|
+
}),
|
|
614
|
+
setAccountEnabled: ({ cfg, enabled }) => {
|
|
615
|
+
const channels = cfg.channels;
|
|
616
|
+
const hlCfg = channels?.["hi-light"] ?? {};
|
|
617
|
+
return {
|
|
618
|
+
...cfg,
|
|
619
|
+
channels: {
|
|
620
|
+
...channels,
|
|
621
|
+
"hi-light": {
|
|
622
|
+
...hlCfg,
|
|
623
|
+
enabled
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
applyAccountConfig: ({ cfg, input }) => {
|
|
629
|
+
const channels = cfg.channels;
|
|
630
|
+
const hlCfg = channels?.["hi-light"] ?? {};
|
|
631
|
+
return {
|
|
632
|
+
...cfg,
|
|
633
|
+
channels: {
|
|
634
|
+
...channels,
|
|
635
|
+
"hi-light": {
|
|
636
|
+
...hlCfg,
|
|
637
|
+
...input.url ? { wsUrl: input.url } : {},
|
|
638
|
+
...input.token ? { authToken: input.token } : {},
|
|
639
|
+
enabled: true
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
// ── Security ────────────────────────────────────────────────────────────
|
|
646
|
+
security: {
|
|
647
|
+
resolveDmPolicy: ({ account, cfg }) => {
|
|
648
|
+
const hlCfg = account.config;
|
|
649
|
+
const policy = hlCfg.dmPolicy ?? "open";
|
|
650
|
+
return {
|
|
651
|
+
policy,
|
|
652
|
+
allowFrom: hlCfg.allowFrom ?? null,
|
|
653
|
+
allowFromPath: 'channels["hi-light"].allowFrom',
|
|
654
|
+
policyPath: 'channels["hi-light"].dmPolicy',
|
|
655
|
+
approveHint: "openclaw allow hi-light <userId>"
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
// ── Status ──────────────────────────────────────────────────────────────
|
|
660
|
+
status: {
|
|
661
|
+
defaultRuntime: {
|
|
662
|
+
accountId: DEFAULT_ACCOUNT_ID2,
|
|
663
|
+
running: false,
|
|
664
|
+
lastStartAt: null,
|
|
665
|
+
lastStopAt: null,
|
|
666
|
+
lastError: null
|
|
667
|
+
},
|
|
668
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
669
|
+
configured: snapshot.configured ?? false,
|
|
670
|
+
running: snapshot.running ?? false,
|
|
671
|
+
wsUrl: snapshot.wsUrl ?? null,
|
|
672
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
673
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
674
|
+
lastError: snapshot.lastError ?? null
|
|
675
|
+
}),
|
|
676
|
+
buildAccountSnapshot: ({ account, runtime: runtime2 }) => ({
|
|
677
|
+
accountId: account.accountId,
|
|
678
|
+
enabled: account.enabled,
|
|
679
|
+
configured: account.configured,
|
|
680
|
+
wsUrl: account.wsUrl,
|
|
681
|
+
running: runtime2?.running ?? false,
|
|
682
|
+
lastStartAt: runtime2?.lastStartAt ?? null,
|
|
683
|
+
lastStopAt: runtime2?.lastStopAt ?? null,
|
|
684
|
+
lastError: runtime2?.lastError ?? null
|
|
685
|
+
})
|
|
686
|
+
},
|
|
687
|
+
// ── Outbound ────────────────────────────────────────────────────────────
|
|
688
|
+
outbound: {
|
|
689
|
+
deliveryMode: "direct",
|
|
690
|
+
sendText: async (ctx) => {
|
|
691
|
+
const { sendHiLightText: sendHiLightText2 } = await Promise.resolve().then(() => (init_send(), send_exports));
|
|
692
|
+
return sendHiLightText2(ctx);
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
// ── Gateway ─────────────────────────────────────────────────────────────
|
|
696
|
+
gateway: {
|
|
697
|
+
startAccount: async (ctx) => {
|
|
698
|
+
const { startHiLightMonitor: startHiLightMonitor2 } = await Promise.resolve().then(() => (init_monitor(), monitor_exports));
|
|
699
|
+
const account = resolveHiLightAccount({
|
|
700
|
+
cfg: ctx.cfg,
|
|
701
|
+
accountId: ctx.accountId
|
|
702
|
+
});
|
|
703
|
+
ctx.setStatus({ accountId: ctx.accountId });
|
|
704
|
+
ctx.log?.info(`hi-light: starting [${ctx.accountId}] \u2192 ${account.wsUrl ?? "(no url)"}`);
|
|
705
|
+
return startHiLightMonitor2({
|
|
706
|
+
config: ctx.cfg,
|
|
707
|
+
runtime: ctx.runtime,
|
|
708
|
+
abortSignal: ctx.abortSignal,
|
|
709
|
+
accountId: ctx.accountId,
|
|
710
|
+
log: ctx.log
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// index.ts
|
|
717
|
+
init_runtime();
|
|
718
|
+
var plugin = {
|
|
719
|
+
id: "hi-light",
|
|
720
|
+
name: "HiLight",
|
|
721
|
+
description: "HiLight WebSocket bridge channel plugin \u2014 connects to external WS server",
|
|
722
|
+
configSchema: emptyPluginConfigSchema(),
|
|
723
|
+
register(api) {
|
|
724
|
+
setHiLightRuntime(api.runtime);
|
|
725
|
+
api.registerChannel({ plugin: hiLightPlugin });
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
var index_default = plugin;
|
|
729
|
+
export {
|
|
730
|
+
index_default as default,
|
|
731
|
+
hiLightPlugin
|
|
732
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@art_style666/hi-light",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HiLight WebSocket bridge channel plugin for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"openclaw.plugin.json",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "esbuild index.ts --bundle --format=esm --platform=node --target=node18 --packages=external --outfile=dist/index.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ws": "^8.18.0",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"esbuild": "^0.25.0"
|
|
25
|
+
},
|
|
26
|
+
"openclaw": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"dist/index.js"
|
|
29
|
+
],
|
|
30
|
+
"channel": {
|
|
31
|
+
"id": "hi-light",
|
|
32
|
+
"label": "HiLight",
|
|
33
|
+
"selectionLabel": "HiLight WebSocket Bridge",
|
|
34
|
+
"docsPath": "/channels/hi-light",
|
|
35
|
+
"docsLabel": "hi-light",
|
|
36
|
+
"blurb": "HiLight — WebSocket bridge channel, connects to external WS server.",
|
|
37
|
+
"order": 80
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|