openclaw-glance-plugin 0.1.18 → 0.1.21

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,43 @@
1
+ # 典型示例
2
+
3
+ ## 比特币监控
4
+
5
+ ```javascript
6
+ operator_type: 'rule'
7
+ operator_parameters: {
8
+ condition: 'price >= threshold and change_percent >= cp_threshold',
9
+ variables: { threshold: 73000, cp_threshold: 0.01, product_name: 'Bitcoin' }
10
+ }
11
+ ```
12
+
13
+ 说明:`crypto` 不支持 `turnover_rate`。
14
+
15
+ ## A 股监控
16
+
17
+ ```javascript
18
+ operator_type: 'rule'
19
+ operator_parameters: {
20
+ condition: 'price >= threshold and turnover_rate >= tr_threshold',
21
+ variables: { threshold: 12.5, tr_threshold: 0.01, product_name: '平安银行' }
22
+ }
23
+ ```
24
+
25
+ ## 港股监控
26
+
27
+ ```javascript
28
+ operator_type: 'rule'
29
+ operator_parameters: {
30
+ condition: 'price >= threshold',
31
+ variables: { threshold: 420, product_name: '腾讯控股' }
32
+ }
33
+ ```
34
+
35
+ ## 查询行情
36
+
37
+ ```javascript
38
+ await runtime.queryTickerData({
39
+ stockCode: '00700',
40
+ market: 'HK',
41
+ productType: 'hk_stock'
42
+ })
43
+ ```
@@ -0,0 +1,69 @@
1
+ # 标的检索与行情查询
2
+
3
+ ## 标的检索规则(必须遵循)
4
+
5
+ 当不能直接确定 `product_code` / `product_type` 时,先查本地 CSV,再让用户确认。
6
+
7
+ 数据文件(字段:`类型,代码,名称,完整代码,市场`):
8
+ - `data/stock_a.csv` -> A 股个股(`productType=stock`)
9
+ - `data/stock_hk.csv` -> 港股个股(`productType=hk_stock`)
10
+ - `data/index_a.csv` -> A 股指数(`productType=index`)
11
+ - `data/index_hk.csv` -> 港股指数(`productType=index`,查询常配 `market=HK`)
12
+
13
+ ## 场景 1:用户只说名称
14
+
15
+ - 在 CSV 里按名称模糊搜索。
16
+ - 命中多条时,必须给出候选(代码 + 名称 + 市场)让用户确认。
17
+ - 不可自行猜测后直接创建策略。
18
+
19
+ ## 场景 2:用户不知道代码或市场
20
+
21
+ - 用 `rg` 在 4 个 CSV 搜索名称或代码。
22
+ - 映射规则:
23
+ - A 股个股 -> `stock`
24
+ - 港股个股 -> `hk_stock`
25
+ - A 股指数 -> `index`
26
+ - 港股指数 -> `index`(`market=HK`)
27
+ - 结果不唯一时先追问。
28
+
29
+ ## 推荐检索命令
30
+
31
+ ```bash
32
+ # 按名称模糊查找
33
+ rg -n "平安银行|腾讯|沪深300|BTC" data/stock_a.csv data/stock_hk.csv data/index_a.csv
34
+ rg -n "恒生科技指数|恒生指数|HSTECH|HSI" data/index_hk.csv
35
+
36
+ # 按代码查找
37
+ rg -n "000001|00700|399001" data/stock_a.csv data/stock_hk.csv data/index_a.csv
38
+ rg -n "HSTECH|HSI|VHSI" data/index_hk.csv
39
+
40
+ # 无 rg 时兜底
41
+ grep -nE "平安银行|腾讯|沪深300|000001|00700|恒生科技指数|HSTECH" \
42
+ data/stock_a.csv data/stock_hk.csv data/index_a.csv data/index_hk.csv
43
+ ```
44
+
45
+ ## 行情查询流程(`watch.query_ticker`)
46
+
47
+ 说明:
48
+ - 对外统一动作名是 `watch.query_ticker`。
49
+ - 文档中的 `runtime.queryTickerData(...)` 仅用于说明宿主运行时内部调用形态。
50
+
51
+ 1. 先确定标的代码、市场、`productType`。
52
+ 2. 调用查询动作。
53
+ 3. 成功后反馈价格/涨跌幅,失败则返回错误并让用户确认代码或市场。
54
+
55
+ 示例:
56
+
57
+ ```javascript
58
+ await runtime.queryTickerData({
59
+ stockCode: '00700',
60
+ market: 'HK',
61
+ productType: 'hk_stock'
62
+ })
63
+ ```
64
+
65
+ 成功判定:
66
+ - `code = "000000"` 或 `success = true`
67
+
68
+ 失败处理:
69
+ - 直接返回失败原因,不静默重试
@@ -0,0 +1,40 @@
1
+ # 重试与排障
2
+
3
+ ## 统一重试原则
4
+
5
+ - 超时或网络波动时可重试。
6
+ - 重试必须使用同一组 payload(字段和值不变)。
7
+ - `request_id` 由插件运行时自动生成并在同 payload 上复用。
8
+
9
+ ## 创建策略失败(`watch.create`)
10
+
11
+ 处理规则:
12
+ - 明确返回失败原因,不静默重试。
13
+ - 若报“未注册的算子类型”,将 `operator_type` 修正为 `rule` 后重试。
14
+
15
+ ## 通知失败(`notify.*`)
16
+
17
+ 处理规则:
18
+ - 优先返回 `code/error/hint`。
19
+ - `MISSING_REQUIRED_FIELD`:仅补缺失字段后重试。
20
+ - `UNSUPPORTED_MESSAGE_TYPE`:提示 bridge 版本不支持,需升级并重启。
21
+ - `UPSTREAM_UNAVAILABLE`:提示上游不可用或超时,稍后重试。
22
+
23
+ ## 离线补发识别(`watch.triggered`)
24
+
25
+ 满足任一条件视为离线补发:
26
+ - `delivery_mode = "offline_replay"`
27
+ - `replayed = true`
28
+
29
+ 时间语义:
30
+ - `trigger_time`:原始触发时间
31
+ - `replayed_at`:补发时间
32
+
33
+ 用户文案需明确“离线期间触发,当前为补发”。
34
+
35
+ ## 触发后动作
36
+
37
+ 1. 解析 `market_data` 提取价格、涨跌幅。
38
+ 2. 通过 `openclaw` 回当前群/私聊。
39
+ 3. 附加渠道按用户要求发送。
40
+ 4. 生成简洁提醒文案。
@@ -0,0 +1,132 @@
1
+ # Watch 动作契约
2
+
3
+ ## `watch.query_ticker`
4
+
5
+ 参数:
6
+ - `stockCode`(或 `productCode`)
7
+ - `productType`
8
+ - `market`(`crypto` 可传空字符串)
9
+
10
+ 成功判定:
11
+ - `code = "000000"` 或 `success = true`
12
+
13
+ 失败处理:
14
+ - 返回失败原因
15
+ - 引导用户确认代码/市场后重试
16
+
17
+ ## `watch.create`
18
+
19
+ 最小参数:
20
+ - `product_code`
21
+ - `product_type`
22
+ - `operator_type`(固定 `rule`)
23
+ - `operator_parameters`(包含 `condition`、`variables`)
24
+
25
+ 建议参数:
26
+ - `channels`(默认至少 `openclaw`)
27
+ - `channel_configs.*`
28
+
29
+ 固定结构(字段名不可改):
30
+
31
+ ```javascript
32
+ {
33
+ product_code: 'BTCUSDT',
34
+ product_type: 'crypto',
35
+ operator_type: 'rule',
36
+ operator_parameters: {
37
+ condition: 'change_percent <= cp_threshold',
38
+ variables: {
39
+ cp_threshold: -0.02,
40
+ product_name: '比特币'
41
+ }
42
+ },
43
+ channels: ['openclaw', 'dingtalk', 'sms'],
44
+ channel_configs: {
45
+ openclaw: {
46
+ channel: 'dingtalk',
47
+ account_id: 'default',
48
+ session_key: 'agent:main:dingtalk:group:<conversation_id>',
49
+ conversation_id: '<conversation_id>'
50
+ },
51
+ dingtalk: {
52
+ cas_id: 'jinguo.xie',
53
+ template_id: 3,
54
+ msg_type: 'text',
55
+ content: '比特币跌幅超2%!当前价格 ${price},跌幅 ${change_percent}%。建议卖出!'
56
+ },
57
+ sms: {
58
+ receiver: '13800138000',
59
+ template_id: 90010,
60
+ content: '比特币跌幅超2%!当前价格 ${price},建议卖出!'
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ 禁止项:
67
+ - `operator_type` 非 `rule`
68
+ - 顶层 `condition`
69
+ - `channel_configs` 渠道配置是字符串
70
+ - 用户未要求渠道却默认附加
71
+
72
+ 成功判定:
73
+ - `success = true`
74
+
75
+ 失败处理:
76
+ - 明确返回失败原因,不静默重试
77
+ - 超时/网络波动重试时,使用同一 payload(字段和值不变)
78
+ - `request_id` 由插件运行时自动生成并复用,大模型不手动传
79
+
80
+ ## `watch.pause` / `watch.activate` / `watch.remove`
81
+
82
+ 参数:
83
+ - `strategyId`(或 `strategy_id`)
84
+
85
+ 成功判定:
86
+ - `success = true`
87
+
88
+ 失败处理:
89
+ - 返回失败原因并提示用户确认策略 ID
90
+
91
+ ## `watch.list`
92
+
93
+ 可选参数:
94
+ - `status`: `active/paused/completed/failed/expired`
95
+ - `product_code`(或 `productCode`)
96
+
97
+ 成功判定:
98
+ - `success = true`
99
+ - `data.total` 为命中数量
100
+ - `data.strategies` 为策略列表
101
+
102
+ 失败处理:
103
+ - 返回失败原因,不静默重试
104
+ - 空结果明确告知“当前条件下没有策略”
105
+
106
+ 安全约束:
107
+ - 仅可查询当前连接用户自己的策略
108
+ - 不通过 `user_id/use_id` 越权查询
109
+
110
+ ## 调用前最终检查
111
+
112
+ 1. `watch.create` 使用 snake_case:`product_code/product_type/operator_type/operator_parameters/channel_configs`
113
+ 2. `operator_type` 固定 `rule`
114
+ 3. `operator_parameters.condition` 与 `variables` 同时存在
115
+ 4. `channels` 与 `channel_configs` 一一对应
116
+ 5. 渠道配置是对象,不是 JSON 字符串
117
+ 6. 默认不手动传 `request_id`
118
+
119
+ ## 买卖意图与条件方向
120
+
121
+ | 用户意图 | 条件方向 |
122
+ |---------|---------|
123
+ | 买入(逢低) | `price <= threshold` |
124
+ | 卖出(止盈/止损) | `price >= threshold` |
125
+
126
+ 判断规则:
127
+ 1. 用户明确说“涨到/跌到”时,按方向直接生成条件。
128
+ 2. 用户只说“到了 XX 提醒我”时,必须追问“买还是卖”。
129
+ 3. 常见映射:
130
+ - `涨到/涨过/突破/冲到` -> `price >= threshold`
131
+ - `跌到/跌破/回调到/回到` -> `price <= threshold`
132
+ - `到了/到达/价格到` -> 方向不明确,需追问
@@ -2,19 +2,6 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import process from 'node:process';
4
4
 
5
- export function resolveContactsStorePath({ env = process.env, pluginConfig = {} } = {}) {
6
- const explicit = pick(
7
- pluginConfig,
8
- ['contactsStorePath', 'contacts_store_path'],
9
- pick(env, ['OPENCLAW_CONTACTS_STORE_PATH'])
10
- );
11
- if (explicit) {
12
- return String(explicit).trim();
13
- }
14
- const homeDir = pick(env, ['HOME', 'USERPROFILE']) || os.homedir();
15
- return path.join(String(homeDir), '.openclaw', 'workspace', 'memory', 'watch-notify-contacts.json');
16
- }
17
-
18
5
  import { ProcessLock } from '../runtime/lock/ProcessLock.js';
19
6
 
20
7
  const DEFAULT_BASE_WS_URL = 'wss://glanceup-pre.100credit.cn';
@@ -78,7 +65,6 @@ export function resolveRuntimeConfig({ env = process.env, pluginConfig = {} } =
78
65
  baseWsUrl,
79
66
  token,
80
67
  lockDir,
81
- lockKey,
82
- contactsStorePath: resolveContactsStorePath({ env, pluginConfig })
68
+ lockKey
83
69
  };
84
70
  }
@@ -1,10 +1,6 @@
1
- import { resolveRuntimeConfig, resolveContactsStorePath } from '../config/runtime-config.js';
1
+ import { resolveRuntimeConfig } from '../config/runtime-config.js';
2
2
  import { extractOpenclawRoutingFromRecord, deriveOpenclawRouting } from '../openclawRouting.js';
3
3
  import { BridgeRuntime } from '../runtime/BridgeRuntime.js';
4
- import {
5
- mergeAndPersistNotifyContacts,
6
- mergeAndPersistWatchContacts
7
- } from './watch-notify-contacts.js';
8
4
  import { PluginDispatcher } from '../runtime/dispatchers/PluginDispatcher.js';
9
5
  import { ProcessLock } from '../runtime/lock/ProcessLock.js';
10
6
 
@@ -145,7 +141,7 @@ function mergeOpenclawChannelConfig(payload = {}, context = {}) {
145
141
  return merged;
146
142
  }
147
143
 
148
- function buildControlApi(startupPromise, contactsStorePath) {
144
+ function buildControlApi(startupPromise) {
149
145
  return {
150
146
  async queryTickerData(query = {}) {
151
147
  const runtime = await getReadyRuntime(startupPromise);
@@ -164,14 +160,9 @@ function buildControlApi(startupPromise, contactsStorePath) {
164
160
  async createWatch(payload = {}, context = {}) {
165
161
  const runtime = await getReadyRuntime(startupPromise);
166
162
  const normalized = mergeOpenclawChannelConfig(payload, context);
167
- const filled = await mergeAndPersistWatchContacts(
168
- contactsStorePath,
169
- normalized,
170
- context
171
- );
172
- return runtime.request('watch.create', filled);
163
+ return runtime.request('watch.create', normalized);
173
164
  },
174
- async sendNotification(input = {}, context = {}) {
165
+ async sendNotification(input = {}) {
175
166
  const runtime = await getReadyRuntime(startupPromise);
176
167
  const ch = String(input.channel ?? '')
177
168
  .trim()
@@ -183,39 +174,28 @@ function buildControlApi(startupPromise, contactsStorePath) {
183
174
  );
184
175
  }
185
176
  const payload = { ...(input.payload || {}) };
186
- const merged = await mergeAndPersistNotifyContacts(
187
- contactsStorePath,
188
- ch,
189
- payload,
190
- context
191
- );
192
177
  return runtime.request('notify.send', {
193
- ...merged,
178
+ ...payload,
194
179
  channel: ch
195
180
  });
196
181
  },
197
- async sendSms(payload = {}, context = {}) {
198
- return this.sendNotification({ channel: 'sms', payload }, context);
182
+ async sendSms(payload = {}) {
183
+ return this.sendNotification({ channel: 'sms', payload });
199
184
  },
200
- async sendCall(payload = {}, context = {}) {
201
- return this.sendNotification({ channel: 'call', payload }, context);
185
+ async sendCall(payload = {}) {
186
+ return this.sendNotification({ channel: 'call', payload });
202
187
  },
203
- async sendEmail(payload = {}, context = {}) {
204
- return this.sendNotification({ channel: 'email', payload }, context);
188
+ async sendEmail(payload = {}) {
189
+ return this.sendNotification({ channel: 'email', payload });
205
190
  },
206
- async sendDingtalk(payload = {}, context = {}) {
207
- return this.sendNotification({ channel: 'dingtalk', payload }, context);
191
+ async sendDingtalk(payload = {}) {
192
+ return this.sendNotification({ channel: 'dingtalk', payload });
208
193
  },
209
194
  async submitWatchDemand(demand = {}, context = {}) {
210
195
  const runtime = await getReadyRuntime(startupPromise);
211
196
  const payload = mapDemandToCreatePayload(demand);
212
197
  const normalized = mergeOpenclawChannelConfig(payload, context);
213
- const filled = await mergeAndPersistWatchContacts(
214
- contactsStorePath,
215
- normalized,
216
- context
217
- );
218
- return runtime.request('watch.create', filled);
198
+ return runtime.request('watch.create', normalized);
219
199
  },
220
200
  async pauseWatch(strategyId) {
221
201
  const runtime = await getReadyRuntime(startupPromise);
@@ -310,7 +290,7 @@ function registerControlTools(api, controlApi) {
310
290
  additionalProperties: true,
311
291
  properties: {}
312
292
  },
313
- (args, meta = {}) => controlApi.sendSms(args || {}, meta?.context || {})
293
+ (args) => controlApi.sendSms(args || {})
314
294
  );
315
295
 
316
296
  tryRegisterTool(
@@ -322,7 +302,7 @@ function registerControlTools(api, controlApi) {
322
302
  additionalProperties: true,
323
303
  properties: {}
324
304
  },
325
- (args, meta = {}) => controlApi.sendCall(args || {}, meta?.context || {})
305
+ (args) => controlApi.sendCall(args || {})
326
306
  );
327
307
 
328
308
  tryRegisterTool(
@@ -334,7 +314,7 @@ function registerControlTools(api, controlApi) {
334
314
  additionalProperties: true,
335
315
  properties: {}
336
316
  },
337
- (args, meta = {}) => controlApi.sendEmail(args || {}, meta?.context || {})
317
+ (args) => controlApi.sendEmail(args || {})
338
318
  );
339
319
 
340
320
  tryRegisterTool(
@@ -346,7 +326,7 @@ function registerControlTools(api, controlApi) {
346
326
  additionalProperties: true,
347
327
  properties: {}
348
328
  },
349
- (args, meta = {}) => controlApi.sendDingtalk(args || {}, meta?.context || {})
329
+ (args) => controlApi.sendDingtalk(args || {})
350
330
  );
351
331
 
352
332
  tryRegisterTool(
@@ -432,8 +412,6 @@ const plugin = {
432
412
  api?.config?.plugins?.glanceBridge?.config ||
433
413
  {};
434
414
 
435
- const contactsStorePath = resolveContactsStorePath({ pluginConfig });
436
-
437
415
  const startupPromise = startPluginRuntime({
438
416
  runtime: api?.runtime,
439
417
  pluginConfig
@@ -442,7 +420,7 @@ const plugin = {
442
420
  api?.runtime?.logger?.error?.(`[openclaw-glance-plugin] runtime start failed: ${err.message}`);
443
421
  });
444
422
 
445
- const controlApi = buildControlApi(startupPromise, contactsStorePath);
423
+ const controlApi = buildControlApi(startupPromise);
446
424
  api.glanceBridge = controlApi;
447
425
  registerControlTools(api, controlApi);
448
426