openclaw-glance-plugin 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/README.md +122 -0
- package/package.json +25 -0
- package/src/OpenClawBridgeClient.js +360 -0
- package/src/OpenClawPluginAdapter.js +104 -0
- package/src/index.js +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# openclaw-glance
|
|
2
|
+
|
|
3
|
+
智能盯盘:OpenClaw 插件客户端(连接 `openclaw-bridge`)。
|
|
4
|
+
|
|
5
|
+
OpenClaw 集成引导请看:[docs/openclaw-install-guide.md](./docs/openclaw-install-guide.md)
|
|
6
|
+
|
|
7
|
+
## 插件定位
|
|
8
|
+
|
|
9
|
+
这个插件用于让 OpenClaw 在“用户提出盯盘需求”时,快速完成以下动作:
|
|
10
|
+
|
|
11
|
+
- 建立与 `openclaw-bridge` 的长连接
|
|
12
|
+
- 把用户自然语言需求转成结构化策略并提交(`watch.create`)
|
|
13
|
+
- 在触发后实时接收 `watch.triggered` 回调给 OpenClaw 业务层
|
|
14
|
+
- 支持策略控制(激活、暂停、删除)
|
|
15
|
+
|
|
16
|
+
## OpenClaw 调用时机
|
|
17
|
+
|
|
18
|
+
- 用户新建盯盘:调用 `submitWatchDemand` 或 `createWatch`
|
|
19
|
+
- 用户修改状态:调用 `activate` / `pause`
|
|
20
|
+
- 用户删除盯盘:调用 `remove`
|
|
21
|
+
- 需要接收触发消息:注册 `onTriggered` 并保持连接在线
|
|
22
|
+
|
|
23
|
+
## 功能
|
|
24
|
+
|
|
25
|
+
- 与 `openclaw-bridge` 建立 WebSocket 长连接
|
|
26
|
+
- 支持请求:`watch.create` / `watch.activate` / `watch.pause` / `watch.delete` / `ping`
|
|
27
|
+
- 订阅推送:`watch.triggered`
|
|
28
|
+
- 自动重连 + 心跳
|
|
29
|
+
- 断线请求排队(可配置),重连后自动冲刷
|
|
30
|
+
- 提供业务适配层 `OpenClawPluginAdapter`
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 测试
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm test
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 运行示例
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
OPENCLAW_BRIDGE_WS_BASE=ws://glanceup-pre.100credit.cn \
|
|
48
|
+
OPENCLAW_WS_TOKEN=your_ws_token \
|
|
49
|
+
npm start
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
运行适配层示例:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm run start:adapter
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## SDK 使用
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { OpenClawBridgeClient } from './src/index.js';
|
|
62
|
+
|
|
63
|
+
const client = new OpenClawBridgeClient({
|
|
64
|
+
baseWsUrl: 'ws://glanceup-pre.100credit.cn',
|
|
65
|
+
token: '<JWT_TOKEN>',
|
|
66
|
+
enqueueIfDisconnected: true
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
client.on('triggered', (msg) => {
|
|
70
|
+
console.log(msg);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await client.connect();
|
|
74
|
+
const res = await client.createWatch({
|
|
75
|
+
product_code: '00700',
|
|
76
|
+
product_type: 'hk_stock',
|
|
77
|
+
operator_type: 'rule',
|
|
78
|
+
operator_parameters: {
|
|
79
|
+
condition: 'price < threshold',
|
|
80
|
+
variables: { threshold: 420 }
|
|
81
|
+
},
|
|
82
|
+
channels: ['openclaw'],
|
|
83
|
+
channel_configs: { openclaw: {} }
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 适配层用法
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { OpenClawPluginAdapter } from './src/index.js';
|
|
91
|
+
|
|
92
|
+
const adapter = new OpenClawPluginAdapter({
|
|
93
|
+
baseWsUrl: 'ws://glanceup-pre.100credit.cn',
|
|
94
|
+
token: '<JWT_TOKEN>'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await adapter.start();
|
|
98
|
+
await adapter.submitWatchDemand({
|
|
99
|
+
productCode: '06608',
|
|
100
|
+
productType: 'hk_stock',
|
|
101
|
+
condition: 'price >= threshold',
|
|
102
|
+
variables: { threshold: 8.97 },
|
|
103
|
+
channels: ['openclaw'],
|
|
104
|
+
channelConfigs: { openclaw: {} }
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 客户端事件
|
|
109
|
+
|
|
110
|
+
- `connected`
|
|
111
|
+
- `disconnected`
|
|
112
|
+
- `reconnecting`
|
|
113
|
+
- `warning`
|
|
114
|
+
- `error`
|
|
115
|
+
- `queued`(请求入队)
|
|
116
|
+
- `queueFlushed`(重连后队列发送)
|
|
117
|
+
- `triggered`
|
|
118
|
+
|
|
119
|
+
## 说明
|
|
120
|
+
|
|
121
|
+
- 先获取 ws `token`,然后连接 `ws://<host>:8005/openclaw/ws`,并在握手 Header 传 `Authorization: Bearer <TOKEN>`。
|
|
122
|
+
- 发布时将导出 `src/` 与 `README.md`(见 `package.json` 的 `files/exports`)。
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-glance-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw plugin client for ticker-monitor openclaw-bridge",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node examples/basic.js",
|
|
16
|
+
"start:adapter": "node examples/adapter.js",
|
|
17
|
+
"test": "node --test tests/**/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"ws": "^8.18.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BASE_WS_URL = 'ws://glanceup-pre.100credit.cn';
|
|
5
|
+
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makeRequestId() {
|
|
11
|
+
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 全局单例实例
|
|
16
|
+
* @type {OpenClawBridgeClient|null}
|
|
17
|
+
*/
|
|
18
|
+
let globalInstance = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取全局单例实例
|
|
22
|
+
* @param {Object} options - 创建实例的选项(仅首次创建时有效)
|
|
23
|
+
* @returns {OpenClawBridgeClient} 全局单例实例
|
|
24
|
+
*/
|
|
25
|
+
export function getGlobalClient(options) {
|
|
26
|
+
if (!globalInstance) {
|
|
27
|
+
globalInstance = new OpenClawBridgeClient(options);
|
|
28
|
+
}
|
|
29
|
+
return globalInstance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 获取全局单例实例(别名,推荐使用)
|
|
34
|
+
* @param {Object} options - 创建实例的选项(仅首次创建时有效)
|
|
35
|
+
* @returns {OpenClawBridgeClient} 全局单例实例
|
|
36
|
+
*/
|
|
37
|
+
export function getInstance(options) {
|
|
38
|
+
return getGlobalClient(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 重置全局单例实例(主要用于测试)
|
|
43
|
+
*/
|
|
44
|
+
export async function resetGlobalClient() {
|
|
45
|
+
if (globalInstance) {
|
|
46
|
+
await globalInstance.close(true);
|
|
47
|
+
globalInstance = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class OpenClawBridgeClient extends EventEmitter {
|
|
52
|
+
constructor(options) {
|
|
53
|
+
super();
|
|
54
|
+
const {
|
|
55
|
+
baseWsUrl = DEFAULT_BASE_WS_URL,
|
|
56
|
+
token = '',
|
|
57
|
+
heartbeatMs = 15000,
|
|
58
|
+
requestTimeoutMs = 10000,
|
|
59
|
+
waitConnectTimeoutMs = 20000,
|
|
60
|
+
reconnect = true,
|
|
61
|
+
reconnectBaseMs = 1000,
|
|
62
|
+
reconnectMaxMs = 15000,
|
|
63
|
+
enqueueIfDisconnected = true,
|
|
64
|
+
maxQueueSize = 200
|
|
65
|
+
} = options || {};
|
|
66
|
+
|
|
67
|
+
if (!token) throw new Error('token is required');
|
|
68
|
+
|
|
69
|
+
this.baseWsUrl = baseWsUrl.replace(/\/$/, '');
|
|
70
|
+
this.userId = '';
|
|
71
|
+
this.token = token;
|
|
72
|
+
|
|
73
|
+
this.heartbeatMs = heartbeatMs;
|
|
74
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
75
|
+
this.waitConnectTimeoutMs = waitConnectTimeoutMs;
|
|
76
|
+
this.reconnect = reconnect;
|
|
77
|
+
this.reconnectBaseMs = reconnectBaseMs;
|
|
78
|
+
this.reconnectMaxMs = reconnectMaxMs;
|
|
79
|
+
this.enqueueIfDisconnected = enqueueIfDisconnected;
|
|
80
|
+
this.maxQueueSize = maxQueueSize;
|
|
81
|
+
|
|
82
|
+
this.ws = null;
|
|
83
|
+
this.connected = false;
|
|
84
|
+
this.stopped = false;
|
|
85
|
+
this.heartbeatTimer = null;
|
|
86
|
+
this.reconnectAttempt = 0;
|
|
87
|
+
this.pending = new Map();
|
|
88
|
+
this.requestQueue = [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get wsUrl() {
|
|
92
|
+
const url = new URL('/openclaw/ws', this.baseWsUrl);
|
|
93
|
+
return url.toString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async connect() {
|
|
97
|
+
this.stopped = false;
|
|
98
|
+
await this._connectOnce();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async close(force = true) {
|
|
102
|
+
this.stopped = Boolean(force);
|
|
103
|
+
this._clearHeartbeat();
|
|
104
|
+
|
|
105
|
+
for (const [requestId, waiter] of this.pending.entries()) {
|
|
106
|
+
clearTimeout(waiter.timer);
|
|
107
|
+
waiter.reject(new Error(`connection closed before response: ${requestId}`));
|
|
108
|
+
}
|
|
109
|
+
this.pending.clear();
|
|
110
|
+
for (const queued of this.requestQueue) {
|
|
111
|
+
queued.reject(new Error(`connection closed before request sent: ${queued.requestId}`));
|
|
112
|
+
}
|
|
113
|
+
this.requestQueue = [];
|
|
114
|
+
|
|
115
|
+
if (this.ws) {
|
|
116
|
+
try {
|
|
117
|
+
this.ws.close();
|
|
118
|
+
} catch (_err) {
|
|
119
|
+
// ignore
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async createWatch(payload) {
|
|
125
|
+
return this._request('watch.create', payload);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async activateWatch(strategyId) {
|
|
129
|
+
return this._request('watch.activate', { strategy_id: strategyId });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async pauseWatch(strategyId) {
|
|
133
|
+
return this._request('watch.pause', { strategy_id: strategyId });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async deleteWatch(strategyId) {
|
|
137
|
+
return this._request('watch.delete', { strategy_id: strategyId });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async ping() {
|
|
141
|
+
return this._request('ping', {});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async waitUntilConnected(timeoutMs = this.waitConnectTimeoutMs) {
|
|
145
|
+
if (this.connected && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
cleanup();
|
|
152
|
+
reject(new Error(`waitUntilConnected timeout (${timeoutMs}ms)`));
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
|
|
155
|
+
const onConnected = () => {
|
|
156
|
+
cleanup();
|
|
157
|
+
resolve(true);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
this.off('connected', onConnected);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
this.on('connected', onConnected);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async _request(type, payload) {
|
|
170
|
+
const requestId = makeRequestId();
|
|
171
|
+
const msg = { type, request_id: requestId, payload: payload || {} };
|
|
172
|
+
const { promise, resolve, reject } = this._buildWaiter(type, requestId);
|
|
173
|
+
|
|
174
|
+
if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
175
|
+
if (!this.enqueueIfDisconnected) {
|
|
176
|
+
reject(new Error('websocket not connected'));
|
|
177
|
+
return promise;
|
|
178
|
+
}
|
|
179
|
+
if (this.requestQueue.length >= this.maxQueueSize) {
|
|
180
|
+
reject(new Error(`request queue overflow (max=${this.maxQueueSize})`));
|
|
181
|
+
return promise;
|
|
182
|
+
}
|
|
183
|
+
this.requestQueue.push({ msg, requestId, type, resolve, reject });
|
|
184
|
+
this.emit('queued', { type, requestId, queueSize: this.requestQueue.length });
|
|
185
|
+
return promise;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this._sendWithTimeout({ msg, requestId, type, resolve, reject });
|
|
189
|
+
return promise;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_buildWaiter(type, requestId) {
|
|
193
|
+
let resolve;
|
|
194
|
+
let reject;
|
|
195
|
+
const promise = new Promise((res, rej) => {
|
|
196
|
+
resolve = res;
|
|
197
|
+
reject = rej;
|
|
198
|
+
});
|
|
199
|
+
return { promise, resolve, reject, type, requestId };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_sendWithTimeout({ msg, requestId, type, resolve, reject }) {
|
|
203
|
+
const timer = setTimeout(() => {
|
|
204
|
+
this.pending.delete(requestId);
|
|
205
|
+
reject(new Error(`request timeout: ${type} (${requestId})`));
|
|
206
|
+
}, this.requestTimeoutMs);
|
|
207
|
+
|
|
208
|
+
this.pending.set(requestId, { resolve, reject, timer, type });
|
|
209
|
+
this.ws.send(JSON.stringify(msg));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async _connectOnce() {
|
|
213
|
+
const ws = new WebSocket(this.wsUrl, {
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Bearer ${this.token}`
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await new Promise((resolve, reject) => {
|
|
220
|
+
const onOpen = () => {
|
|
221
|
+
cleanup();
|
|
222
|
+
resolve();
|
|
223
|
+
};
|
|
224
|
+
const onError = (err) => {
|
|
225
|
+
cleanup();
|
|
226
|
+
reject(err);
|
|
227
|
+
};
|
|
228
|
+
const cleanup = () => {
|
|
229
|
+
ws.off('open', onOpen);
|
|
230
|
+
ws.off('error', onError);
|
|
231
|
+
};
|
|
232
|
+
ws.on('open', onOpen);
|
|
233
|
+
ws.on('error', onError);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this.ws = ws;
|
|
237
|
+
this.connected = true;
|
|
238
|
+
this.reconnectAttempt = 0;
|
|
239
|
+
this.emit('connected');
|
|
240
|
+
this._flushQueue();
|
|
241
|
+
|
|
242
|
+
ws.on('message', (raw) => {
|
|
243
|
+
this._onMessage(raw.toString());
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
ws.on('close', (code, reason) => {
|
|
247
|
+
this._onClose(code, reason?.toString());
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
ws.on('error', (err) => {
|
|
251
|
+
this.emit('error', err);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this._startHeartbeat();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_onMessage(raw) {
|
|
258
|
+
let msg;
|
|
259
|
+
try {
|
|
260
|
+
msg = JSON.parse(raw);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
this.emit('warning', new Error(`invalid json from bridge: ${raw}`));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const requestId = msg.request_id;
|
|
267
|
+
if (requestId && this.pending.has(requestId)) {
|
|
268
|
+
const waiter = this.pending.get(requestId);
|
|
269
|
+
this.pending.delete(requestId);
|
|
270
|
+
clearTimeout(waiter.timer);
|
|
271
|
+
waiter.resolve(msg);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (msg.type === 'watch.triggered') {
|
|
276
|
+
this.emit('triggered', msg);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (msg.type === 'system.connected') {
|
|
281
|
+
this.userId = msg.user_id || this.userId;
|
|
282
|
+
this.emit('systemConnected', msg);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.emit('message', msg);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async _onClose(code, reason) {
|
|
290
|
+
this.connected = false;
|
|
291
|
+
this._clearHeartbeat();
|
|
292
|
+
this.emit('disconnected', { code, reason, reconnect: this.reconnect, stopped: this.stopped });
|
|
293
|
+
|
|
294
|
+
if (this.stopped) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 如果不是主动停止,则自动重连
|
|
299
|
+
if (!this.reconnect) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
while (!this.stopped) {
|
|
304
|
+
this.reconnectAttempt += 1;
|
|
305
|
+
const backoff = Math.min(
|
|
306
|
+
this.reconnectBaseMs * Math.min(this.reconnectAttempt, 10),
|
|
307
|
+
this.reconnectMaxMs
|
|
308
|
+
);
|
|
309
|
+
this.emit('reconnecting', { attempt: this.reconnectAttempt, backoffMs: backoff, reason: 'auto' });
|
|
310
|
+
await sleep(backoff);
|
|
311
|
+
|
|
312
|
+
if (this.stopped) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await this._connectOnce();
|
|
318
|
+
this.emit('reconnected', { attempt: this.reconnectAttempt });
|
|
319
|
+
return;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const reconnectErr = new Error(`重连失败: ${err.message}`);
|
|
322
|
+
reconnectErr.attempt = this.reconnectAttempt;
|
|
323
|
+
this.emit('error', reconnectErr);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_startHeartbeat() {
|
|
329
|
+
this._clearHeartbeat();
|
|
330
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
331
|
+
if (!this.connected) return;
|
|
332
|
+
try {
|
|
333
|
+
await this.ping();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
this.emit('warning', new Error(`heartbeat failed: ${err.message}`));
|
|
336
|
+
}
|
|
337
|
+
}, this.heartbeatMs);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_flushQueue() {
|
|
341
|
+
if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!this.requestQueue.length) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const queued = this.requestQueue.splice(0, this.requestQueue.length);
|
|
348
|
+
for (const item of queued) {
|
|
349
|
+
this._sendWithTimeout(item);
|
|
350
|
+
}
|
|
351
|
+
this.emit('queueFlushed', { count: queued.length });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_clearHeartbeat() {
|
|
355
|
+
if (this.heartbeatTimer) {
|
|
356
|
+
clearInterval(this.heartbeatTimer);
|
|
357
|
+
this.heartbeatTimer = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { OpenClawBridgeClient } from './OpenClawBridgeClient.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 全局单例 Adapter 实例
|
|
5
|
+
* @type {OpenClawPluginAdapter|null}
|
|
6
|
+
*/
|
|
7
|
+
let globalAdapterInstance = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取全局单例 Adapter 实例
|
|
11
|
+
* @param {Object} clientOrOptions - 客户端实例或配置选项
|
|
12
|
+
* @returns {OpenClawPluginAdapter} 全局单例 Adapter 实例
|
|
13
|
+
*/
|
|
14
|
+
export function getGlobalAdapter(clientOrOptions) {
|
|
15
|
+
if (!globalAdapterInstance) {
|
|
16
|
+
globalAdapterInstance = new OpenClawPluginAdapter(clientOrOptions);
|
|
17
|
+
}
|
|
18
|
+
return globalAdapterInstance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 获取全局单例 Adapter 实例(别名,推荐使用)
|
|
23
|
+
* @param {Object} clientOrOptions - 客户端实例或配置选项
|
|
24
|
+
* @returns {OpenClawPluginAdapter} 全局单例 Adapter 实例
|
|
25
|
+
*/
|
|
26
|
+
export function getAdapter(clientOrOptions) {
|
|
27
|
+
return getGlobalAdapter(clientOrOptions);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 重置全局单例 Adapter(主要用于测试)
|
|
32
|
+
*/
|
|
33
|
+
export async function resetGlobalAdapter() {
|
|
34
|
+
if (globalAdapterInstance) {
|
|
35
|
+
await globalAdapterInstance.stop();
|
|
36
|
+
globalAdapterInstance = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 面向 OpenClaw 业务层的薄适配器。
|
|
42
|
+
* 负责把业务输入转换为 bridge 协议中的 watch.* 请求。
|
|
43
|
+
* 支持单例模式:通过 getAdapter() 或 getGlobalAdapter() 获取全局实例
|
|
44
|
+
*/
|
|
45
|
+
export class OpenClawPluginAdapter {
|
|
46
|
+
constructor(clientOrOptions) {
|
|
47
|
+
const looksLikeClient =
|
|
48
|
+
clientOrOptions &&
|
|
49
|
+
typeof clientOrOptions === 'object' &&
|
|
50
|
+
typeof clientOrOptions.createWatch === 'function' &&
|
|
51
|
+
typeof clientOrOptions.pauseWatch === 'function' &&
|
|
52
|
+
typeof clientOrOptions.activateWatch === 'function' &&
|
|
53
|
+
typeof clientOrOptions.deleteWatch === 'function';
|
|
54
|
+
|
|
55
|
+
if (clientOrOptions instanceof OpenClawBridgeClient || looksLikeClient) {
|
|
56
|
+
this.client = clientOrOptions;
|
|
57
|
+
} else {
|
|
58
|
+
this.client = new OpenClawBridgeClient(clientOrOptions || {});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start() {
|
|
63
|
+
await this.client.connect();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async stop() {
|
|
67
|
+
await this.client.close(true);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onTriggered(handler) {
|
|
71
|
+
this.client.on('triggered', handler);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 统一创建盯盘需求接口(适配 OpenClaw 侧参数)。
|
|
76
|
+
*/
|
|
77
|
+
async submitWatchDemand(demand) {
|
|
78
|
+
const payload = {
|
|
79
|
+
product_code: demand.productCode,
|
|
80
|
+
product_type: demand.productType || 'stock',
|
|
81
|
+
operator_type: 'rule',
|
|
82
|
+
operator_parameters: {
|
|
83
|
+
condition: demand.condition,
|
|
84
|
+
variables: demand.variables || {},
|
|
85
|
+
message_template: demand.messageTemplate
|
|
86
|
+
},
|
|
87
|
+
channels: demand.channels || ['openclaw'],
|
|
88
|
+
channel_configs: demand.channelConfigs || { openclaw: {} }
|
|
89
|
+
};
|
|
90
|
+
return this.client.createWatch(payload);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async pause(strategyId) {
|
|
94
|
+
return this.client.pauseWatch(strategyId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async activate(strategyId) {
|
|
98
|
+
return this.client.activateWatch(strategyId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async remove(strategyId) {
|
|
102
|
+
return this.client.deleteWatch(strategyId);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/index.js
ADDED