openclaw-glance-plugin 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,6 +23,7 @@
23
23
  - 与 `openclaw-bridge` 建立 WebSocket 长连接
24
24
  - 支持请求:`watch.create` / `watch.activate` / `watch.pause` / `watch.delete` / `ticker.query` / `notify.send` / `ping`
25
25
  - 支持渠道:`openclaw` / `email` / `call` / `sms` / `dingtalk`
26
+ - 建议在 `channel_configs.openclaw` 中携带路由字段(如 `channel`、`session_key`、`account_id`、`conversation_id`),便于触发后回推到正确会话
26
27
  - 订阅推送:`watch.triggered`
27
28
  - 主动发起通知结果推送:`notify.sent`
28
29
  - 自动重连 + 心跳
@@ -127,6 +128,7 @@ await adapter.submitWatchDemand({
127
128
 
128
129
  - `watch_query_ticker`
129
130
  - `watch_create`
131
+ - `watch_list`
130
132
  - `watch_pause`
131
133
  - `watch_activate`
132
134
  - `watch_remove`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-glance-plugin",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "OpenClaw plugin client for ticker-monitor openclaw-bridge",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -30,6 +30,7 @@ description: 智能盯盘插件,用于监控A股、港股、比特币等金融
30
30
 
31
31
  - `watch.query_ticker`
32
32
  - `watch.create`
33
+ - `watch.list`
33
34
  - `watch.pause`
34
35
  - `watch.activate`
35
36
  - `watch.remove`
@@ -43,7 +44,8 @@ description: 智能盯盘插件,用于监控A股、港股、比特币等金融
43
44
  1. 用户是“查行情”意图:先调用 `watch.query_ticker`
44
45
  2. 用户是“盯盘创建”意图:先补齐参数后调用 `watch.create`
45
46
  3. 用户是“暂停/恢复/删除”意图:分别调用 `watch.pause` / `watch.activate` / `watch.remove`
46
- 4. 用户是“立即发短信/打电话/发邮件/发钉钉”意图:调用 `notify.sms` / `notify.call` / `notify.email` / `notify.dingtalk`
47
+ 4. 用户是“查看策略/查看我的策略/看 active paused completed 策略”意图:调用 `watch.list`
48
+ 5. 用户是“立即发短信/打电话/发邮件/发钉钉”意图:调用 `notify.sms` / `notify.call` / `notify.email` / `notify.dingtalk`
47
49
 
48
50
  禁止跳步:创建盯盘前若缺关键字段必须先追问。
49
51
 
@@ -146,6 +148,24 @@ description: 智能盯盘插件,用于监控A股、港股、比特币等金融
146
148
  失败处理:
147
149
  - 返回失败原因并提示用户确认策略 ID
148
150
 
151
+ #### `watch.list`
152
+
153
+ 参数(可选):
154
+ - `status`:策略状态过滤。可传 `active` / `paused` / `completed` / `failed` / `expired`;不传表示查询该用户全部策略
155
+ - `product_code`(或 `productCode`):按标的代码过滤
156
+
157
+ 成功判定:
158
+ - 返回 `success = true`
159
+ - `data.total` 为命中策略数,`data.strategies` 为策略列表
160
+
161
+ 失败处理:
162
+ - 返回失败原因,不要静默重试
163
+ - 若筛选条件为空结果,明确告知“当前条件下没有策略”
164
+
165
+ 安全约束(必须):
166
+ - `watch.list` 只能查询当前连接用户自己的策略
167
+ - 不要尝试通过参数传 `user_id` / `use_id` 越权查询
168
+
149
169
  #### `notify.sms` / `notify.call` / `notify.email` / `notify.dingtalk`
150
170
 
151
171
  参数:
@@ -188,6 +208,11 @@ description: 智能盯盘插件,用于监控A股、港股、比特币等金融
188
208
  回执说明:
189
209
  - 直连通知发送完成后,客户端会收到 `notify.sent` 事件(`overall_status/success_count/failed_count/deliveries`)
190
210
 
211
+ 离线补发识别(`watch.triggered`):
212
+ - 若事件包含 `delivery_mode = "offline_replay"` 或 `replayed = true`,表示这是用户离线期间触发后补发的消息
213
+ - `trigger_time` 表示原始触发时间,`replayed_at` 表示补发时间
214
+ - 向用户描述时应明确区分:例如“这条是离线期间触发,现已补发到当前会话”
215
+
191
216
  ## 调用判定规则
192
217
 
193
218
  只有在用户明确表达以下意图时调用插件:
@@ -164,6 +164,10 @@ export class OpenClawBridgeClient extends EventEmitter {
164
164
  return this._request('watch.pause', { strategy_id: strategyId });
165
165
  }
166
166
 
167
+ async listWatches(payload = {}) {
168
+ return this._request('watch.list', payload || {});
169
+ }
170
+
167
171
  async deleteWatch(strategyId) {
168
172
  return this._request('watch.delete', { strategy_id: strategyId });
169
173
  }
@@ -150,4 +150,8 @@ export class OpenClawPluginAdapter {
150
150
  async remove(strategyId) {
151
151
  return this.client.deleteWatch(strategyId);
152
152
  }
153
+
154
+ async listWatches(params = {}) {
155
+ return this.client.listWatches(params || {});
156
+ }
153
157
  }
@@ -56,6 +56,52 @@ async function getReadyRuntime(startupPromise) {
56
56
  return activeRuntime;
57
57
  }
58
58
 
59
+ function pickFirstString(...values) {
60
+ for (const value of values) {
61
+ if (typeof value === 'string' && value.trim()) {
62
+ return value.trim();
63
+ }
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ function deriveOpenclawRouting({ params = {}, context = {} } = {}) {
69
+ const metadata = context?.event?.metadata || {};
70
+
71
+ const channel = pickFirstString(
72
+ params?.channel,
73
+ params?.source_channel,
74
+ metadata?.channel,
75
+ metadata?.channelId,
76
+ context?.channel,
77
+ context?.channelId
78
+ );
79
+ const accountId = pickFirstString(params?.account_id, context?.accountId, metadata?.accountId);
80
+ const sessionKey = pickFirstString(
81
+ params?.session_key,
82
+ params?.sessionKey,
83
+ context?.sessionKey,
84
+ metadata?.sessionKey
85
+ );
86
+ const conversationId = pickFirstString(
87
+ params?.conversation_id,
88
+ params?.conversationId,
89
+ params?.chat_id,
90
+ params?.chatId,
91
+ context?.conversationId,
92
+ metadata?.conversationId,
93
+ metadata?.chatId,
94
+ metadata?.groupId
95
+ );
96
+
97
+ const routing = {};
98
+ if (channel) routing.channel = channel;
99
+ if (accountId) routing.account_id = accountId;
100
+ if (sessionKey) routing.session_key = sessionKey;
101
+ if (conversationId) routing.conversation_id = conversationId;
102
+ return routing;
103
+ }
104
+
59
105
  function mapDemandToCreatePayload(demand = {}) {
60
106
  const channels = Array.isArray(demand.channels)
61
107
  ? demand.channels
@@ -101,6 +147,33 @@ function mapDemandToCreatePayload(demand = {}) {
101
147
  };
102
148
  }
103
149
 
150
+ function mergeOpenclawChannelConfig(payload = {}, context = {}) {
151
+ const merged = { ...(payload || {}) };
152
+ const channelConfigs = { ...(merged.channel_configs || {}) };
153
+ const openclawConfig = { ...(channelConfigs.openclaw || {}) };
154
+ const routing = deriveOpenclawRouting({ params: merged, context });
155
+
156
+ if (Object.keys(routing).length === 0) {
157
+ return merged;
158
+ }
159
+
160
+ channelConfigs.openclaw = {
161
+ ...openclawConfig,
162
+ ...routing
163
+ };
164
+
165
+ const channels = Array.isArray(merged.channels)
166
+ ? merged.channels
167
+ .filter((x) => typeof x === 'string' && x.trim())
168
+ .map((x) => x.trim().toLowerCase())
169
+ : [];
170
+ if (!channels.includes('openclaw')) channels.unshift('openclaw');
171
+
172
+ merged.channels = channels;
173
+ merged.channel_configs = channelConfigs;
174
+ return merged;
175
+ }
176
+
104
177
  function buildControlApi(startupPromise) {
105
178
  return {
106
179
  async queryTickerData(query = {}) {
@@ -117,9 +190,10 @@ function buildControlApi(startupPromise) {
117
190
  product_type: productType
118
191
  });
119
192
  },
120
- async createWatch(payload = {}) {
193
+ async createWatch(payload = {}, context = {}) {
121
194
  const runtime = await getReadyRuntime(startupPromise);
122
- return runtime.request('watch.create', payload);
195
+ const normalized = mergeOpenclawChannelConfig(payload, context);
196
+ return runtime.request('watch.create', normalized);
123
197
  },
124
198
  async sendNotification(input = {}) {
125
199
  const runtime = await getReadyRuntime(startupPromise);
@@ -142,14 +216,19 @@ function buildControlApi(startupPromise) {
142
216
  async sendDingtalk(payload = {}) {
143
217
  return this.sendNotification({ channel: 'dingtalk', payload });
144
218
  },
145
- async submitWatchDemand(demand = {}) {
219
+ async submitWatchDemand(demand = {}, context = {}) {
146
220
  const runtime = await getReadyRuntime(startupPromise);
147
- return runtime.request('watch.create', mapDemandToCreatePayload(demand));
221
+ const payload = mapDemandToCreatePayload(demand);
222
+ return runtime.request('watch.create', mergeOpenclawChannelConfig(payload, context));
148
223
  },
149
224
  async pauseWatch(strategyId) {
150
225
  const runtime = await getReadyRuntime(startupPromise);
151
226
  return runtime.request('watch.pause', { strategy_id: strategyId });
152
227
  },
228
+ async listWatches(payload = {}) {
229
+ const runtime = await getReadyRuntime(startupPromise);
230
+ return runtime.request('watch.list', payload || {});
231
+ },
153
232
  async activateWatch(strategyId) {
154
233
  const runtime = await getReadyRuntime(startupPromise);
155
234
  return runtime.request('watch.activate', { strategy_id: strategyId });
@@ -176,7 +255,8 @@ function tryRegisterTool(registerTool, name, description, parameters, handler) {
176
255
  parameters: schema,
177
256
  inputSchema: schema,
178
257
  handler,
179
- execute: async (_toolCallId, params) => handler(params || {})
258
+ execute: async (_toolCallId, params, _onUpdate, context) =>
259
+ handler(params || {}, { context: context || {} })
180
260
  };
181
261
  const meta = {
182
262
  name,
@@ -289,7 +369,7 @@ function registerControlTools(api, controlApi) {
289
369
  channel_configs: { type: 'object' }
290
370
  }
291
371
  },
292
- (args) => controlApi.createWatch(args || {})
372
+ (args, meta = {}) => controlApi.createWatch(args || {}, meta?.context || {})
293
373
  );
294
374
 
295
375
  const strategySchema = {
@@ -301,6 +381,22 @@ function registerControlTools(api, controlApi) {
301
381
  }
302
382
  };
303
383
 
384
+ tryRegisterTool(
385
+ registerTool,
386
+ 'watch_list',
387
+ 'List watch strategies for current user',
388
+ {
389
+ type: 'object',
390
+ additionalProperties: true,
391
+ properties: {
392
+ status: { type: 'string' },
393
+ product_code: { type: 'string' },
394
+ productCode: { type: 'string' }
395
+ }
396
+ },
397
+ (args) => controlApi.listWatches(args || {})
398
+ );
399
+
304
400
  tryRegisterTool(
305
401
  registerTool,
306
402
  'watch_pause',
@@ -1,3 +1,43 @@
1
+ function pickFirstString(...values) {
2
+ for (const value of values) {
3
+ if (typeof value === 'string' && value.trim()) {
4
+ return value.trim();
5
+ }
6
+ }
7
+ return undefined;
8
+ }
9
+
10
+ function extractRoutingFromTriggeredEvent(event) {
11
+ const payload = event?.payload || {};
12
+ const openclaw = payload?.channel_configs?.openclaw || payload?.openclaw || {};
13
+
14
+ return {
15
+ channel: pickFirstString(
16
+ payload?.channel,
17
+ payload?.source_channel,
18
+ openclaw?.channel,
19
+ openclaw?.source_channel
20
+ ),
21
+ accountId: pickFirstString(payload?.account_id, openclaw?.account_id),
22
+ sessionKey: pickFirstString(
23
+ payload?.session_key,
24
+ payload?.sessionKey,
25
+ openclaw?.session_key,
26
+ openclaw?.sessionKey
27
+ ),
28
+ conversationId: pickFirstString(
29
+ payload?.conversation_id,
30
+ payload?.conversationId,
31
+ payload?.chat_id,
32
+ payload?.chatId,
33
+ openclaw?.conversation_id,
34
+ openclaw?.conversationId,
35
+ openclaw?.chat_id,
36
+ openclaw?.chatId
37
+ )
38
+ };
39
+ }
40
+
1
41
  export class PluginDispatcher {
2
42
  constructor({ runtime }) {
3
43
  this.runtime = runtime;
@@ -7,12 +47,22 @@ export class PluginDispatcher {
7
47
  if (!this.runtime?.dispatchReply) {
8
48
  return;
9
49
  }
10
- await this.runtime.dispatchReply({
50
+
51
+ const routing = extractRoutingFromTriggeredEvent(event);
52
+ const dispatchPayload = {
11
53
  text: event?.payload?.message || '',
12
54
  metadata: {
13
55
  source: 'watch.triggered',
14
- event
56
+ event,
57
+ routing
15
58
  }
16
- });
59
+ };
60
+
61
+ if (routing.channel) dispatchPayload.channel = routing.channel;
62
+ if (routing.accountId) dispatchPayload.accountId = routing.accountId;
63
+ if (routing.sessionKey) dispatchPayload.sessionKey = routing.sessionKey;
64
+ if (routing.conversationId) dispatchPayload.conversationId = routing.conversationId;
65
+
66
+ await this.runtime.dispatchReply(dispatchPayload);
17
67
  }
18
68
  }