openclaw-glance-plugin 0.1.2 → 0.1.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.
@@ -0,0 +1,56 @@
1
+ # 盯盘插件参考文档
2
+
3
+ ## 环境配置
4
+
5
+ ```bash
6
+ # 基础配置(桥接地址固定为 wss://glanceup-pre.100credit.cn)
7
+ export OPENCLAW_WS_TOKEN="<token>"
8
+
9
+ # Token 申请:在网页上申请 OPENCLAW_WS_TOKEN
10
+ ```
11
+
12
+ ## 市场类型与产品代码
13
+
14
+ | 市场 | productType | productCode 示例 | 行情频率 |
15
+ |------|-------------|-----------------|----------|
16
+ | A股个股 | stock | 000001 | 约每 3 秒 |
17
+ | A股指数 | index | 000300 | 约每 3 秒 |
18
+ | 港股 | hk_stock | 00700 | 延迟约 15 分钟 |
19
+ | 比特币 | crypto | BTCUSDT | 约每 10 秒 |
20
+
21
+ ## 条件表达式
22
+
23
+ ### 可用变量
24
+
25
+ - 通用: `price`, `volume`, `change_percent`
26
+ - A股/港股额外: `turnover_rate`
27
+ - 比特币: 不支持 `turnover_rate`
28
+
29
+ ### 示例条件
30
+
31
+ ```javascript
32
+ // 单一条件
33
+ 'price >= threshold'
34
+
35
+ // 多条件
36
+ 'price >= threshold and change_percent >= cp_threshold'
37
+
38
+ // A股专用
39
+ 'price >= threshold and turnover_rate >= tr_threshold'
40
+ ```
41
+
42
+ ## 产品代码速查
43
+
44
+ ### A股主要指数
45
+ - 沪深300: 000300
46
+ - 上证指数: 000001
47
+ - 创业板指: 399006
48
+
49
+ ### 热门港股
50
+ - 腾讯控股: 00700
51
+ - 阿里巴巴: 09988
52
+ - 美团: 03690
53
+ - 比亚迪股份: 01211
54
+
55
+ ### 主流加密货币
56
+ - 比特币: BTCUSDT
@@ -141,6 +141,10 @@ export class OpenClawBridgeClient extends EventEmitter {
141
141
  return this._request('ping', {});
142
142
  }
143
143
 
144
+ async queryTickerData(payload) {
145
+ return this._request('ticker.query', payload || {});
146
+ }
147
+
144
148
  async waitUntilConnected(timeoutMs = this.waitConnectTimeoutMs) {
145
149
  if (this.connected && this.ws && this.ws.readyState === WebSocket.OPEN) {
146
150
  return true;
@@ -94,6 +94,10 @@ export class OpenClawPluginAdapter {
94
94
  channelConfigs.call = demand.callConfig;
95
95
  if (!channels.includes('call')) channels.push('call');
96
96
  }
97
+ if (demand.smsConfig) {
98
+ channelConfigs.sms = demand.smsConfig;
99
+ if (!channels.includes('sms')) channels.push('sms');
100
+ }
97
101
  if (!channels.includes('openclaw')) channels.unshift('openclaw');
98
102
  if (!channelConfigs.openclaw) channelConfigs.openclaw = {};
99
103
 
@@ -112,6 +116,25 @@ export class OpenClawPluginAdapter {
112
116
  return this.client.createWatch(payload);
113
117
  }
114
118
 
119
+ /**
120
+ * 查询标的实时行情(透传到 bridge 的 ticker.query)。
121
+ */
122
+ async queryTickerData(query) {
123
+ const stockCode = query?.stockCode || query?.productCode || query?.stock_code || '';
124
+ const productType = query?.productType || query?.product_type || '';
125
+ let market = query?.market;
126
+
127
+ if (market == null && String(productType).toLowerCase() === 'crypto') {
128
+ market = '';
129
+ }
130
+
131
+ return this.client.queryTickerData({
132
+ stock_code: stockCode,
133
+ market: market == null ? '' : String(market),
134
+ product_type: productType
135
+ });
136
+ }
137
+
115
138
  async pause(strategyId) {
116
139
  return this.client.pauseWatch(strategyId);
117
140
  }
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+
4
+ import { ProcessLock } from '../runtime/lock/ProcessLock.js';
5
+
6
+ const DEFAULT_BASE_WS_URL = 'wss://glanceup-pre.100credit.cn';
7
+
8
+ function pick(source, keys, fallback = undefined) {
9
+ for (const key of keys) {
10
+ const value = source?.[key];
11
+ if (value !== undefined && value !== null && String(value).trim() !== '') {
12
+ return value;
13
+ }
14
+ }
15
+ return fallback;
16
+ }
17
+
18
+ export function resolveRuntimeConfig({ env = process.env, pluginConfig = {} } = {}) {
19
+ const baseWsUrl = String(
20
+ pick(pluginConfig, ['baseWsUrl', 'base_ws_url'], pick(env, ['OPENCLAW_BASE_WS_URL'], DEFAULT_BASE_WS_URL))
21
+ );
22
+ const token = String(pick(pluginConfig, ['token'], pick(env, ['OPENCLAW_WS_TOKEN'], '')));
23
+ const lockDir = String(
24
+ pick(pluginConfig, ['lockDir', 'lock_dir'], pick(env, ['OPENCLAW_LOCK_DIR'], path.join(process.cwd(), '.openclaw-locks')))
25
+ );
26
+ const lockKey = ProcessLock.buildLockKey(baseWsUrl, token);
27
+ if (!token) {
28
+ throw new Error('token is required');
29
+ }
30
+ try {
31
+ const parsed = new URL(baseWsUrl);
32
+ if (!parsed.protocol || !parsed.host) {
33
+ throw new Error('invalid baseWsUrl');
34
+ }
35
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
36
+ throw new Error(`invalid baseWsUrl protocol: ${parsed.protocol}`);
37
+ }
38
+ } catch (_err) {
39
+ if (String(_err?.message || '').toLowerCase().includes('protocol')) {
40
+ throw _err;
41
+ }
42
+ throw new Error(`invalid baseWsUrl: ${baseWsUrl}`);
43
+ }
44
+ return {
45
+ baseWsUrl,
46
+ token,
47
+ lockDir,
48
+ lockKey
49
+ };
50
+ }
package/src/index.js CHANGED
@@ -1,2 +1,11 @@
1
1
  export { OpenClawBridgeClient, getGlobalClient, getInstance, resetGlobalClient } from './OpenClawBridgeClient.js';
2
- export { OpenClawPluginAdapter, getGlobalAdapter, getAdapter, resetGlobalAdapter } from './OpenClawPluginAdapter.js';
2
+ export { OpenClawPluginAdapter, getGlobalAdapter, getAdapter, resetGlobalAdapter } from './OpenClawPluginAdapter.js';
3
+ export { BridgeRuntime } from './runtime/BridgeRuntime.js';
4
+ export { ProcessLock, SingleActiveConflictError } from './runtime/lock/ProcessLock.js';
5
+ export { PluginDispatcher } from './runtime/dispatchers/PluginDispatcher.js';
6
+ export { resolveRuntimeConfig } from './config/runtime-config.js';
7
+ export {
8
+ startPluginRuntime,
9
+ stopPluginRuntime,
10
+ getActivePluginRuntime
11
+ } from './plugin/index.js';
@@ -0,0 +1,228 @@
1
+ import { resolveRuntimeConfig } from '../config/runtime-config.js';
2
+ import { BridgeRuntime } from '../runtime/BridgeRuntime.js';
3
+ import { PluginDispatcher } from '../runtime/dispatchers/PluginDispatcher.js';
4
+ import { ProcessLock } from '../runtime/lock/ProcessLock.js';
5
+
6
+ let activeRuntime = null;
7
+
8
+ function installProcessShutdown(runtime) {
9
+ const stop = async () => {
10
+ if (!runtime) return;
11
+ await runtime.stop().catch(() => {});
12
+ process.exit(0);
13
+ };
14
+ process.once('SIGINT', stop);
15
+ process.once('SIGTERM', stop);
16
+ }
17
+
18
+ export function getActivePluginRuntime() {
19
+ return activeRuntime;
20
+ }
21
+
22
+ export async function startPluginRuntime({ runtime, pluginConfig } = {}) {
23
+ if (activeRuntime) {
24
+ return activeRuntime;
25
+ }
26
+ const config = resolveRuntimeConfig({ pluginConfig });
27
+ const lock = new ProcessLock({
28
+ lockDir: config.lockDir,
29
+ key: config.lockKey
30
+ });
31
+ const dispatcher = new PluginDispatcher({ runtime });
32
+ activeRuntime = new BridgeRuntime({
33
+ baseWsUrl: config.baseWsUrl,
34
+ token: config.token,
35
+ dispatcher,
36
+ lock
37
+ });
38
+ await activeRuntime.start();
39
+ return activeRuntime;
40
+ }
41
+
42
+ export async function stopPluginRuntime() {
43
+ if (!activeRuntime) return;
44
+ await activeRuntime.stop();
45
+ activeRuntime = null;
46
+ }
47
+
48
+ async function getReadyRuntime(startupPromise) {
49
+ await startupPromise;
50
+ if (!activeRuntime) {
51
+ throw new Error('plugin runtime is not active');
52
+ }
53
+ return activeRuntime;
54
+ }
55
+
56
+ function mapDemandToCreatePayload(demand = {}) {
57
+ const channels = Array.isArray(demand.channels)
58
+ ? demand.channels
59
+ .filter((x) => typeof x === 'string' && x.trim())
60
+ .map((x) => x.trim().toLowerCase())
61
+ : [];
62
+ const channelConfigs = { ...(demand.channelConfigs || {}) };
63
+
64
+ if (demand.openclawConfig) {
65
+ channelConfigs.openclaw = demand.openclawConfig;
66
+ if (!channels.includes('openclaw')) channels.push('openclaw');
67
+ }
68
+ if (demand.emailConfig) {
69
+ channelConfigs.email = demand.emailConfig;
70
+ if (!channels.includes('email')) channels.push('email');
71
+ }
72
+ if (demand.callConfig) {
73
+ channelConfigs.call = demand.callConfig;
74
+ if (!channels.includes('call')) channels.push('call');
75
+ }
76
+ if (demand.smsConfig) {
77
+ channelConfigs.sms = demand.smsConfig;
78
+ if (!channels.includes('sms')) channels.push('sms');
79
+ }
80
+ if (!channels.includes('openclaw')) channels.unshift('openclaw');
81
+ if (!channelConfigs.openclaw) channelConfigs.openclaw = {};
82
+
83
+ return {
84
+ product_code: demand.productCode || demand.product_code,
85
+ product_type: demand.productType || demand.product_type || 'stock',
86
+ operator_type: 'rule',
87
+ operator_parameters: {
88
+ condition: demand.condition,
89
+ variables: demand.variables || {},
90
+ message_template: demand.messageTemplate || demand.message_template
91
+ },
92
+ channels,
93
+ channel_configs: channelConfigs
94
+ };
95
+ }
96
+
97
+ function buildControlApi(startupPromise) {
98
+ return {
99
+ async queryTickerData(query = {}) {
100
+ const runtime = await getReadyRuntime(startupPromise);
101
+ const stockCode = query.stockCode || query.productCode || query.stock_code || '';
102
+ const productType = query.productType || query.product_type || '';
103
+ let market = query.market;
104
+ if (market == null && String(productType).toLowerCase() === 'crypto') {
105
+ market = '';
106
+ }
107
+ return runtime.request('ticker.query', {
108
+ stock_code: stockCode,
109
+ market: market == null ? '' : String(market),
110
+ product_type: productType
111
+ });
112
+ },
113
+ async createWatch(payload = {}) {
114
+ const runtime = await getReadyRuntime(startupPromise);
115
+ return runtime.request('watch.create', payload);
116
+ },
117
+ async submitWatchDemand(demand = {}) {
118
+ const runtime = await getReadyRuntime(startupPromise);
119
+ return runtime.request('watch.create', mapDemandToCreatePayload(demand));
120
+ },
121
+ async pauseWatch(strategyId) {
122
+ const runtime = await getReadyRuntime(startupPromise);
123
+ return runtime.request('watch.pause', { strategy_id: strategyId });
124
+ },
125
+ async activateWatch(strategyId) {
126
+ const runtime = await getReadyRuntime(startupPromise);
127
+ return runtime.request('watch.activate', { strategy_id: strategyId });
128
+ },
129
+ async deleteWatch(strategyId) {
130
+ const runtime = await getReadyRuntime(startupPromise);
131
+ return runtime.request('watch.delete', { strategy_id: strategyId });
132
+ }
133
+ };
134
+ }
135
+
136
+ function tryRegisterTool(registerTool, name, description, handler) {
137
+ if (typeof registerTool !== 'function') return;
138
+ const def = {
139
+ name,
140
+ description,
141
+ handler,
142
+ execute: async (_toolCallId, params) => handler(params || {})
143
+ };
144
+ const meta = {
145
+ name,
146
+ description
147
+ };
148
+
149
+ // OpenClaw-style: registerTool(def, meta)
150
+ try {
151
+ registerTool(def, meta);
152
+ return;
153
+ } catch (_err) {
154
+ // try one-arg object signature
155
+ }
156
+
157
+ try {
158
+ registerTool(def);
159
+ return;
160
+ } catch (_err) {
161
+ // try alternate host signature: (name, handler)
162
+ }
163
+
164
+ try {
165
+ registerTool(name, handler);
166
+ } catch (_err) {
167
+ // ignore host differences
168
+ }
169
+ }
170
+
171
+ function registerControlTools(api, controlApi) {
172
+ const registerTool = api?.registerTool || api?.runtime?.registerTool;
173
+ tryRegisterTool(registerTool, 'watch.query_ticker', 'Query ticker data', (args) =>
174
+ controlApi.queryTickerData(args || {})
175
+ );
176
+ tryRegisterTool(registerTool, 'watch.create', 'Create watch strategy', (args) =>
177
+ controlApi.createWatch(args || {})
178
+ );
179
+ tryRegisterTool(registerTool, 'watch.pause', 'Pause watch strategy', (args) =>
180
+ controlApi.pauseWatch(args?.strategyId || args?.strategy_id)
181
+ );
182
+ tryRegisterTool(registerTool, 'watch.activate', 'Activate watch strategy', (args) =>
183
+ controlApi.activateWatch(args?.strategyId || args?.strategy_id)
184
+ );
185
+ tryRegisterTool(registerTool, 'watch.remove', 'Delete watch strategy', (args) =>
186
+ controlApi.deleteWatch(args?.strategyId || args?.strategy_id)
187
+ );
188
+ }
189
+
190
+ const plugin = {
191
+ id: 'glance-bridge',
192
+ name: 'Glance Bridge Channel',
193
+ description: 'OpenClaw bridge long connection plugin',
194
+ register(api) {
195
+ const pluginConfig =
196
+ api?.config?.channels?.['glance-bridge'] ||
197
+ api?.config?.channels?.glanceBridge ||
198
+ api?.config?.glanceBridge ||
199
+ {};
200
+
201
+ const startupPromise = startPluginRuntime({
202
+ runtime: api?.runtime,
203
+ pluginConfig
204
+ });
205
+ startupPromise.catch((err) => {
206
+ api?.runtime?.logger?.error?.(`[glance-bridge] runtime start failed: ${err.message}`);
207
+ });
208
+
209
+ const controlApi = buildControlApi(startupPromise);
210
+ api.glanceBridge = controlApi;
211
+ registerControlTools(api, controlApi);
212
+
213
+ if (typeof api?.onShutdown === 'function') {
214
+ api.onShutdown(async () => {
215
+ await startupPromise.catch(() => {});
216
+ await stopPluginRuntime();
217
+ });
218
+ } else {
219
+ startupPromise
220
+ .then((runtime) => {
221
+ installProcessShutdown(runtime);
222
+ })
223
+ .catch(() => {});
224
+ }
225
+ }
226
+ };
227
+
228
+ export default plugin;
@@ -0,0 +1,254 @@
1
+ import EventEmitter from 'node:events';
2
+ import WebSocket from 'ws';
3
+
4
+ function sleep(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+
8
+ function makeRequestId() {
9
+ return `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
10
+ }
11
+
12
+ export class BridgeRuntime extends EventEmitter {
13
+ constructor({
14
+ baseWsUrl,
15
+ token,
16
+ dispatcher,
17
+ lock,
18
+ heartbeatMs = 15000,
19
+ requestTimeoutMs = 10000,
20
+ reconnect = true,
21
+ reconnectBaseMs = 1000,
22
+ reconnectMaxMs = 15000,
23
+ enqueueIfDisconnected = true,
24
+ maxQueueSize = 200
25
+ }) {
26
+ super();
27
+ if (!baseWsUrl) throw new Error('baseWsUrl is required');
28
+ if (!token) throw new Error('token is required');
29
+ this.baseWsUrl = baseWsUrl.replace(/\/$/, '');
30
+ this.token = token;
31
+ this.dispatcher = dispatcher;
32
+ this.lock = lock;
33
+
34
+ this.heartbeatMs = heartbeatMs;
35
+ this.requestTimeoutMs = requestTimeoutMs;
36
+ this.reconnect = reconnect;
37
+ this.reconnectBaseMs = reconnectBaseMs;
38
+ this.reconnectMaxMs = reconnectMaxMs;
39
+ this.enqueueIfDisconnected = enqueueIfDisconnected;
40
+ this.maxQueueSize = maxQueueSize;
41
+
42
+ this.ws = null;
43
+ this.connected = false;
44
+ this.stopped = false;
45
+ this.reconnectAttempt = 0;
46
+ this.heartbeatTimer = null;
47
+ this.pending = new Map();
48
+ this.requestQueue = [];
49
+ }
50
+
51
+ get wsUrl() {
52
+ const url = new URL('/openclaw/ws', this.baseWsUrl);
53
+ return url.toString();
54
+ }
55
+
56
+ async start() {
57
+ this.stopped = false;
58
+ await this.lock?.acquire();
59
+ try {
60
+ await this._connectOnce();
61
+ } catch (err) {
62
+ await this.lock?.release().catch(() => {});
63
+ throw err;
64
+ }
65
+ }
66
+
67
+ async stop() {
68
+ this.stopped = true;
69
+ this.connected = false;
70
+ this._clearHeartbeat();
71
+ for (const [requestId, waiter] of this.pending.entries()) {
72
+ clearTimeout(waiter.timer);
73
+ waiter.reject(new Error(`connection closed before response: ${requestId}`));
74
+ }
75
+ this.pending.clear();
76
+ for (const queued of this.requestQueue) {
77
+ queued.reject(new Error(`connection closed before request sent: ${queued.requestId}`));
78
+ }
79
+ this.requestQueue = [];
80
+
81
+ if (this.ws) {
82
+ try {
83
+ this.ws.close();
84
+ } catch (_err) {
85
+ // ignore
86
+ }
87
+ }
88
+ await this.lock?.release().catch(() => {});
89
+ }
90
+
91
+ async request(type, payload = {}) {
92
+ const requestId = makeRequestId();
93
+ const msg = { type, request_id: requestId, payload };
94
+ const { promise, resolve, reject } = this._buildWaiter();
95
+
96
+ if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
97
+ if (!this.enqueueIfDisconnected) {
98
+ reject(new Error('websocket not connected'));
99
+ return promise;
100
+ }
101
+ if (this.requestQueue.length >= this.maxQueueSize) {
102
+ reject(new Error(`request queue overflow (max=${this.maxQueueSize})`));
103
+ return promise;
104
+ }
105
+ this.requestQueue.push({ msg, requestId, resolve, reject });
106
+ return promise;
107
+ }
108
+ this._sendWithTimeout({ msg, requestId, resolve, reject });
109
+ return promise;
110
+ }
111
+
112
+ _buildWaiter() {
113
+ let resolve;
114
+ let reject;
115
+ const promise = new Promise((res, rej) => {
116
+ resolve = res;
117
+ reject = rej;
118
+ });
119
+ return { promise, resolve, reject };
120
+ }
121
+
122
+ _sendWithTimeout({ msg, requestId, resolve, reject }) {
123
+ const timer = setTimeout(() => {
124
+ this.pending.delete(requestId);
125
+ reject(new Error(`request timeout: ${msg.type} (${requestId})`));
126
+ }, this.requestTimeoutMs);
127
+
128
+ this.pending.set(requestId, { resolve, reject, timer });
129
+ try {
130
+ this.ws.send(JSON.stringify(msg));
131
+ } catch (err) {
132
+ clearTimeout(timer);
133
+ this.pending.delete(requestId);
134
+ reject(err);
135
+ }
136
+ }
137
+
138
+ async _connectOnce() {
139
+ const ws = new WebSocket(this.wsUrl, {
140
+ headers: {
141
+ Authorization: `Bearer ${this.token}`
142
+ }
143
+ });
144
+
145
+ await new Promise((resolve, reject) => {
146
+ const onOpen = () => {
147
+ cleanup();
148
+ resolve();
149
+ };
150
+ const onError = (err) => {
151
+ cleanup();
152
+ reject(err);
153
+ };
154
+ const cleanup = () => {
155
+ ws.off('open', onOpen);
156
+ ws.off('error', onError);
157
+ };
158
+ ws.on('open', onOpen);
159
+ ws.on('error', onError);
160
+ });
161
+
162
+ this.ws = ws;
163
+ this.connected = true;
164
+ this.reconnectAttempt = 0;
165
+ this.emit('connected');
166
+ this._startHeartbeat();
167
+ this._flushQueue();
168
+
169
+ ws.on('message', (raw) => {
170
+ this._onMessage(raw.toString());
171
+ });
172
+ ws.on('close', (code, reason) => {
173
+ void this._onClose(code, reason?.toString());
174
+ });
175
+ ws.on('error', (err) => {
176
+ this.emit('error', err);
177
+ });
178
+ }
179
+
180
+ _onMessage(raw) {
181
+ let msg;
182
+ try {
183
+ msg = JSON.parse(raw);
184
+ } catch (_err) {
185
+ this.emit('warning', new Error(`invalid json from bridge: ${raw}`));
186
+ return;
187
+ }
188
+ const requestId = msg.request_id;
189
+ if (requestId && this.pending.has(requestId)) {
190
+ const waiter = this.pending.get(requestId);
191
+ this.pending.delete(requestId);
192
+ clearTimeout(waiter.timer);
193
+ waiter.resolve(msg);
194
+ return;
195
+ }
196
+ if (msg.type === 'watch.triggered') {
197
+ this.dispatcher?.onTriggered?.(msg).catch((err) => this.emit('error', err));
198
+ this.emit('triggered', msg);
199
+ return;
200
+ }
201
+ this.emit('message', msg);
202
+ }
203
+
204
+ async _onClose(code, reason) {
205
+ this.connected = false;
206
+ this._clearHeartbeat();
207
+ this.emit('disconnected', { code, reason });
208
+ if (this.stopped || !this.reconnect) {
209
+ return;
210
+ }
211
+ while (!this.stopped) {
212
+ this.reconnectAttempt += 1;
213
+ const backoff = Math.min(
214
+ this.reconnectBaseMs * Math.min(this.reconnectAttempt, 10),
215
+ this.reconnectMaxMs
216
+ );
217
+ this.emit('reconnecting', { attempt: this.reconnectAttempt, backoffMs: backoff });
218
+ await sleep(backoff);
219
+ if (this.stopped) return;
220
+ try {
221
+ await this._connectOnce();
222
+ this.emit('reconnected', { attempt: this.reconnectAttempt });
223
+ return;
224
+ } catch (err) {
225
+ this.emit('warning', err);
226
+ }
227
+ }
228
+ }
229
+
230
+ _startHeartbeat() {
231
+ this._clearHeartbeat();
232
+ this.heartbeatTimer = setInterval(() => {
233
+ if (!this.connected) return;
234
+ this.request('ping', {}).catch(() => {});
235
+ }, this.heartbeatMs);
236
+ }
237
+
238
+ _clearHeartbeat() {
239
+ if (this.heartbeatTimer) {
240
+ clearInterval(this.heartbeatTimer);
241
+ this.heartbeatTimer = null;
242
+ }
243
+ }
244
+
245
+ _flushQueue() {
246
+ if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
247
+ return;
248
+ }
249
+ const queued = this.requestQueue.splice(0, this.requestQueue.length);
250
+ for (const item of queued) {
251
+ this._sendWithTimeout(item);
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,18 @@
1
+ export class PluginDispatcher {
2
+ constructor({ runtime }) {
3
+ this.runtime = runtime;
4
+ }
5
+
6
+ async onTriggered(event) {
7
+ if (!this.runtime?.dispatchReply) {
8
+ return;
9
+ }
10
+ await this.runtime.dispatchReply({
11
+ text: event?.payload?.message || '',
12
+ metadata: {
13
+ source: 'watch.triggered',
14
+ event
15
+ }
16
+ });
17
+ }
18
+ }