openclaw-glance-plugin 0.1.22 → 0.1.27
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 +7 -3
- package/package.json +2 -1
- package/skills/glance-watch/SKILL.md +117 -49
- package/skills/glance-watch/references/channels.md +61 -66
- package/skills/glance-watch/references/examples.md +145 -25
- package/skills/glance-watch/references/news-briefing.md +41 -0
- package/skills/glance-watch/references/quote-realtime.md +62 -0
- package/skills/glance-watch/references/symbol-search-and-calendar.md +74 -0
- package/skills/glance-watch/references/troubleshooting.md +5 -2
- package/skills/glance-watch/references/watch-contract.md +93 -57
- package/src/OpenClawBridgeClient.js +24 -4
- package/src/OpenClawPluginAdapter.js +76 -10
- package/src/agentQueryNormalize.js +84 -0
- package/src/plugin/index.js +272 -18
- package/src/runtime/BridgeRuntime.js +19 -5
- package/skills/glance-watch/references/markets.md +0 -56
- package/skills/glance-watch/references/query-and-symbol.md +0 -69
package/src/plugin/index.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeFundBasicTableQuery,
|
|
3
|
+
normalizeKeywordTableQuery,
|
|
4
|
+
normalizeTickerQuery,
|
|
5
|
+
normalizeTradeCalendarQuery
|
|
6
|
+
} from '../agentQueryNormalize.js';
|
|
1
7
|
import { resolveRuntimeConfig } from '../config/runtime-config.js';
|
|
2
8
|
import { extractOpenclawRoutingFromRecord, deriveOpenclawRouting } from '../openclawRouting.js';
|
|
3
9
|
import { BridgeRuntime } from '../runtime/BridgeRuntime.js';
|
|
4
10
|
import { PluginDispatcher } from '../runtime/dispatchers/PluginDispatcher.js';
|
|
5
11
|
import { ProcessLock } from '../runtime/lock/ProcessLock.js';
|
|
6
12
|
|
|
13
|
+
/** 与 BridgeRuntime FINANCE_TABLE_REQUEST_TIMEOUT_MS 一致 */
|
|
14
|
+
const GATEWAY_TABLE_REQUEST_TIMEOUT_MS = 90_000;
|
|
15
|
+
const FUND_CODE_PATTERN = /^\d{6}\.OF$/i;
|
|
16
|
+
|
|
7
17
|
let activeRuntime = null;
|
|
8
18
|
|
|
9
19
|
function installProcessShutdown(runtime) {
|
|
@@ -141,25 +151,105 @@ function mergeOpenclawChannelConfig(payload = {}, context = {}) {
|
|
|
141
151
|
return merged;
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
function assertWatchCreateSupported(payload = {}) {
|
|
155
|
+
const code = String(payload.product_code ?? payload.productCode ?? '')
|
|
156
|
+
.trim()
|
|
157
|
+
.toUpperCase();
|
|
158
|
+
const productType = String(payload.product_type ?? payload.productType ?? '')
|
|
159
|
+
.trim()
|
|
160
|
+
.toLowerCase();
|
|
161
|
+
if (productType === 'fund' || FUND_CODE_PATTERN.test(code)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'watch_create does not support fund strategies. Supported product_type: stock, hk_stock, index, crypto. For funds use watch_query_fund_estimates or watch_search_fund_basic.'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
function buildControlApi(startupPromise) {
|
|
145
169
|
return {
|
|
146
170
|
async queryTickerData(query = {}) {
|
|
147
171
|
const runtime = await getReadyRuntime(startupPromise);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
const payload = normalizeTickerQuery(query || {});
|
|
173
|
+
if (!payload.market || !payload.symbol) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'queryTickerData requires market and symbol. If user gave a company/index name only, call watch_search_*_basic first to get ts_code, then map to market+symbol.'
|
|
176
|
+
);
|
|
153
177
|
}
|
|
154
|
-
return runtime.request('ticker.query',
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
return runtime.request('ticker.query', payload);
|
|
179
|
+
},
|
|
180
|
+
async queryFundEstimates(query = {}) {
|
|
181
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
182
|
+
let fundCodes = query.fund_codes ?? query.fundCodes;
|
|
183
|
+
if (fundCodes == null) {
|
|
184
|
+
throw new Error('queryFundEstimates requires fund_codes (or fundCodes): string or string[]');
|
|
185
|
+
}
|
|
186
|
+
if (typeof fundCodes === 'string') {
|
|
187
|
+
fundCodes = fundCodes.trim();
|
|
188
|
+
} else if (Array.isArray(fundCodes)) {
|
|
189
|
+
fundCodes = fundCodes.map((x) => String(x).trim()).filter(Boolean);
|
|
190
|
+
} else {
|
|
191
|
+
throw new Error('fund_codes must be a string or string[]');
|
|
192
|
+
}
|
|
193
|
+
return runtime.request('fund.estimates', { fund_codes: fundCodes });
|
|
194
|
+
},
|
|
195
|
+
async searchAStockBasic(query = {}) {
|
|
196
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
197
|
+
const q = normalizeKeywordTableQuery(query, 'searchAStockBasic');
|
|
198
|
+
return runtime.request(
|
|
199
|
+
'finance.table',
|
|
200
|
+
{ path: '/v1/a-stock/basic/search', query: q },
|
|
201
|
+
{ requestTimeoutMs: GATEWAY_TABLE_REQUEST_TIMEOUT_MS }
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
async searchHkStockBasic(query = {}) {
|
|
205
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
206
|
+
const q = normalizeKeywordTableQuery(query, 'searchHkStockBasic');
|
|
207
|
+
return runtime.request(
|
|
208
|
+
'finance.table',
|
|
209
|
+
{ path: '/v1/hk-stock/basic/search', query: q },
|
|
210
|
+
{ requestTimeoutMs: GATEWAY_TABLE_REQUEST_TIMEOUT_MS }
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
async searchIndexBasic(query = {}) {
|
|
214
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
215
|
+
const q = normalizeKeywordTableQuery(query, 'searchIndexBasic');
|
|
216
|
+
return runtime.request(
|
|
217
|
+
'finance.table',
|
|
218
|
+
{ path: '/v1/index/basic/search', query: q },
|
|
219
|
+
{ requestTimeoutMs: GATEWAY_TABLE_REQUEST_TIMEOUT_MS }
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
async searchFundBasic(query = {}) {
|
|
223
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
224
|
+
const q = normalizeFundBasicTableQuery(query);
|
|
225
|
+
return runtime.request(
|
|
226
|
+
'finance.table',
|
|
227
|
+
{ path: '/v1/fund/basic', query: q },
|
|
228
|
+
{ requestTimeoutMs: GATEWAY_TABLE_REQUEST_TIMEOUT_MS }
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
async queryFinNews(query = {}) {
|
|
232
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
233
|
+
const q = normalizeKeywordTableQuery(query, 'queryFinNews');
|
|
234
|
+
return runtime.request(
|
|
235
|
+
'finance.table',
|
|
236
|
+
{ path: '/v1/news', query: q },
|
|
237
|
+
{ requestTimeoutMs: GATEWAY_TABLE_REQUEST_TIMEOUT_MS }
|
|
238
|
+
);
|
|
239
|
+
},
|
|
240
|
+
async queryTradeCalendar(query = {}) {
|
|
241
|
+
const runtime = await getReadyRuntime(startupPromise);
|
|
242
|
+
const q = normalizeTradeCalendarQuery(query);
|
|
243
|
+
return runtime.request(
|
|
244
|
+
'finance.table',
|
|
245
|
+
{ path: '/v1/trade-calendar', query: q },
|
|
246
|
+
{ requestTimeoutMs: GATEWAY_TABLE_REQUEST_TIMEOUT_MS }
|
|
247
|
+
);
|
|
159
248
|
},
|
|
160
249
|
async createWatch(payload = {}, context = {}) {
|
|
161
250
|
const runtime = await getReadyRuntime(startupPromise);
|
|
162
251
|
const normalized = mergeOpenclawChannelConfig(payload, context);
|
|
252
|
+
assertWatchCreateSupported(normalized);
|
|
163
253
|
return runtime.request('watch.create', normalized);
|
|
164
254
|
},
|
|
165
255
|
async sendNotification(input = {}) {
|
|
@@ -195,6 +285,7 @@ function buildControlApi(startupPromise) {
|
|
|
195
285
|
const runtime = await getReadyRuntime(startupPromise);
|
|
196
286
|
const payload = mapDemandToCreatePayload(demand);
|
|
197
287
|
const normalized = mergeOpenclawChannelConfig(payload, context);
|
|
288
|
+
assertWatchCreateSupported(normalized);
|
|
198
289
|
return runtime.request('watch.create', normalized);
|
|
199
290
|
},
|
|
200
291
|
async pauseWatch(strategyId) {
|
|
@@ -264,23 +355,185 @@ function tryRegisterTool(registerTool, name, description, parameters, handler) {
|
|
|
264
355
|
function registerControlTools(api, controlApi) {
|
|
265
356
|
const registerTool = api?.registerTool || api?.runtime?.registerTool;
|
|
266
357
|
|
|
358
|
+
const DESC_TICKER =
|
|
359
|
+
'【实时行情】当前价、涨跌幅等撮合侧快照。' +
|
|
360
|
+
'When: 用户已明确标的代码/简称对应的代码时用;仅说公司名时先用 watch_search_*_basic 再调本工具。' +
|
|
361
|
+
'Returns: type=ticker.query.result;success=true 时读 quote(英文键 last、name、pct_change 等)。' +
|
|
362
|
+
'Do NOT: 场外基金估值勿用本工具(用 watch_query_fund_estimates)。market 可用 a|hk|crypto 或「A股」「港股」「加密」等别名。';
|
|
363
|
+
|
|
267
364
|
tryRegisterTool(
|
|
268
365
|
registerTool,
|
|
269
366
|
'watch_query_ticker',
|
|
270
|
-
|
|
367
|
+
DESC_TICKER,
|
|
271
368
|
{
|
|
272
369
|
type: 'object',
|
|
370
|
+
description:
|
|
371
|
+
'实时行情。必填 market + symbol;segment 仅 A/港股指数场景建议 index 或省略。插件会将「A股」等映射为 a。',
|
|
273
372
|
additionalProperties: true,
|
|
274
373
|
properties: {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
374
|
+
market: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
description:
|
|
377
|
+
'市场:a | hk | crypto;或中文别名 A股/港股/加密/数字货币(插件会归一化)。'
|
|
378
|
+
},
|
|
379
|
+
symbol: {
|
|
380
|
+
type: 'string',
|
|
381
|
+
description:
|
|
382
|
+
'标的代码:A 股如 600000.SH 或 600000;港股 00700;加密 BTCUSDT。名称请先用 search 工具解析。'
|
|
383
|
+
},
|
|
384
|
+
segment: {
|
|
385
|
+
type: 'string',
|
|
386
|
+
description: '可选。A/港股时 auto|stock|index;加密不传。'
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
required: ['market', 'symbol']
|
|
280
390
|
},
|
|
281
391
|
(args) => controlApi.queryTickerData(args || {})
|
|
282
392
|
);
|
|
283
393
|
|
|
394
|
+
const DESC_FUND_EST =
|
|
395
|
+
'【基金当日估值】场外开放式基金估算净值/涨跌,非股票盘口。' +
|
|
396
|
+
'When: 用户问「基金今天估值、估算涨跌」且代码形如 xxxxxx.OF。' +
|
|
397
|
+
'Returns: fund.estimates.result;可能较慢(~90s)。' +
|
|
398
|
+
'Do NOT: 股票/指数/加密行情用 watch_query_ticker。';
|
|
399
|
+
|
|
400
|
+
tryRegisterTool(
|
|
401
|
+
registerTool,
|
|
402
|
+
'watch_query_fund_estimates',
|
|
403
|
+
DESC_FUND_EST,
|
|
404
|
+
{
|
|
405
|
+
type: 'object',
|
|
406
|
+
description: '单只或多只基金代码;fund_codes 与 fundCodes 等价。',
|
|
407
|
+
additionalProperties: true,
|
|
408
|
+
properties: {
|
|
409
|
+
fund_codes: {
|
|
410
|
+
description: '单只 "000006.OF" 或字符串数组;与 fundCodes 二选一即可',
|
|
411
|
+
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }]
|
|
412
|
+
},
|
|
413
|
+
fundCodes: {
|
|
414
|
+
description: 'camelCase 别名,含义同 fund_codes',
|
|
415
|
+
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }]
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
anyOf: [{ required: ['fund_codes'] }, { required: ['fundCodes'] }]
|
|
419
|
+
},
|
|
420
|
+
(args) => controlApi.queryFundEstimates(args || {})
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const keywordSearchSchema = {
|
|
424
|
+
type: 'object',
|
|
425
|
+
description:
|
|
426
|
+
'keyword 与 q 等价,至少填一个(插件会合并为网关 keyword)。可选 limit(条数)。返回 finance.table.result,行在 data[]。',
|
|
427
|
+
additionalProperties: true,
|
|
428
|
+
properties: {
|
|
429
|
+
keyword: { type: 'string', description: '搜索词:名称或代码片段,优先使用' },
|
|
430
|
+
q: { type: 'string', description: '与 keyword 二选一' },
|
|
431
|
+
limit: { type: 'number', description: '最大行数,默认由网关决定' }
|
|
432
|
+
},
|
|
433
|
+
anyOf: [{ required: ['keyword'] }, { required: ['q'] }]
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
tryRegisterTool(
|
|
437
|
+
registerTool,
|
|
438
|
+
'watch_search_a_stock_basic',
|
|
439
|
+
'【A 股基础信息】按名称/代码找 ts_code、简称、行业等(日终/静态库表)。' +
|
|
440
|
+
'When: 用户只说中文名或模糊代码、要映射到 600000.SH 再查 watch_query_ticker。' +
|
|
441
|
+
'Returns: finance.table.result,data[] 每行含 ts_code 等字段。',
|
|
442
|
+
keywordSearchSchema,
|
|
443
|
+
(args) => controlApi.searchAStockBasic(args || {})
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
tryRegisterTool(
|
|
447
|
+
registerTool,
|
|
448
|
+
'watch_search_hk_stock_basic',
|
|
449
|
+
'【港股基础信息】名称/拼音/代码检索港股代码。' +
|
|
450
|
+
'When: 港股名称→代码后再 watch_query_ticker(market=hk)。' +
|
|
451
|
+
'Returns: finance.table.result。',
|
|
452
|
+
keywordSearchSchema,
|
|
453
|
+
(args) => controlApi.searchHkStockBasic(args || {})
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
tryRegisterTool(
|
|
457
|
+
registerTool,
|
|
458
|
+
'watch_search_index_basic',
|
|
459
|
+
'【指数基础信息】按简称/ts_code 找指数。' +
|
|
460
|
+
'When: 用户说「沪深300」「恒生指数」等需解析为指数代码再 watch_query_ticker。' +
|
|
461
|
+
'Returns: finance.table.result。',
|
|
462
|
+
keywordSearchSchema,
|
|
463
|
+
(args) => controlApi.searchIndexBasic(args || {})
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
tryRegisterTool(
|
|
467
|
+
registerTool,
|
|
468
|
+
'watch_search_fund_basic',
|
|
469
|
+
'【基金档案】按 ts_code 或名称查基金元数据(非估值)。' +
|
|
470
|
+
'When: 确认基金代码、或名称反查 000xxx.OF;查当日估算涨跌用 watch_query_fund_estimates。' +
|
|
471
|
+
'Returns: finance.table.result。',
|
|
472
|
+
{
|
|
473
|
+
type: 'object',
|
|
474
|
+
description: 'ts_code 精确查优先;否则 keyword 或 q 模糊查。可选 limit。',
|
|
475
|
+
additionalProperties: true,
|
|
476
|
+
properties: {
|
|
477
|
+
ts_code: { type: 'string', description: '基金代码如 000006.OF' },
|
|
478
|
+
tsCode: { type: 'string', description: 'camelCase 别名' },
|
|
479
|
+
keyword: { type: 'string', description: '基金名称关键词' },
|
|
480
|
+
q: { type: 'string', description: '同 keyword' },
|
|
481
|
+
limit: { type: 'number' }
|
|
482
|
+
},
|
|
483
|
+
anyOf: [
|
|
484
|
+
{ required: ['ts_code'] },
|
|
485
|
+
{ required: ['tsCode'] },
|
|
486
|
+
{ required: ['keyword'] },
|
|
487
|
+
{ required: ['q'] }
|
|
488
|
+
]
|
|
489
|
+
},
|
|
490
|
+
(args) => controlApi.searchFundBasic(args || {})
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
tryRegisterTool(
|
|
494
|
+
registerTool,
|
|
495
|
+
'watch_fin_news',
|
|
496
|
+
'【财经快讯】标题/摘要类新闻,非实时逐笔。' +
|
|
497
|
+
'When: 用户问「有什么新闻、快讯」并给出主题词。' +
|
|
498
|
+
'Returns: finance.table.result;可选 pub_time_start/pub_time_end 收窄时间。' +
|
|
499
|
+
'Do NOT: 与行情混淆;无关键词时先追问。',
|
|
500
|
+
{
|
|
501
|
+
...keywordSearchSchema,
|
|
502
|
+
properties: {
|
|
503
|
+
...keywordSearchSchema.properties,
|
|
504
|
+
pub_time_start: { type: 'string', description: '可选,发布时间下界,如 YYYY-MM-DD HH:MM:SS' },
|
|
505
|
+
pub_time_end: { type: 'string', description: '可选,发布时间上界' }
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
(args) => controlApi.queryFinNews(args || {})
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
tryRegisterTool(
|
|
512
|
+
registerTool,
|
|
513
|
+
'watch_trade_calendar',
|
|
514
|
+
'【交易日历】某日是否开市、上一交易日等,无价量。' +
|
|
515
|
+
'When: 「今天A股开不开盘」「五一休市吗」等;A 股常用 exchange=SSE(沪)或 SZSE(深),可各查一次或问用户。' +
|
|
516
|
+
'Returns: finance.table.result,行内 is_open 等字段依网关。' +
|
|
517
|
+
'Required: exchange + start_date + end_date(YYYY-MM-DD);可用 startDate/endDate。',
|
|
518
|
+
{
|
|
519
|
+
type: 'object',
|
|
520
|
+
description: '区间宜覆盖所问日期;单日则 start=end。',
|
|
521
|
+
additionalProperties: true,
|
|
522
|
+
properties: {
|
|
523
|
+
exchange: {
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: '交易所代码,如 SSE(上交所)、SZSE(深交所),与网关文档一致'
|
|
526
|
+
},
|
|
527
|
+
start_date: { type: 'string', description: '区间起点 YYYY-MM-DD' },
|
|
528
|
+
end_date: { type: 'string', description: '区间终点 YYYY-MM-DD' },
|
|
529
|
+
startDate: { type: 'string', description: 'camelCase,同 start_date' },
|
|
530
|
+
endDate: { type: 'string', description: 'camelCase,同 end_date' }
|
|
531
|
+
},
|
|
532
|
+
required: ['exchange']
|
|
533
|
+
},
|
|
534
|
+
(args) => controlApi.queryTradeCalendar(args || {})
|
|
535
|
+
);
|
|
536
|
+
|
|
284
537
|
tryRegisterTool(
|
|
285
538
|
registerTool,
|
|
286
539
|
'notify_sms',
|
|
@@ -332,7 +585,7 @@ function registerControlTools(api, controlApi) {
|
|
|
332
585
|
tryRegisterTool(
|
|
333
586
|
registerTool,
|
|
334
587
|
'watch_create',
|
|
335
|
-
'Create watch strategy',
|
|
588
|
+
'Create watch strategy for stock/hk_stock/index/crypto. Do NOT use for funds (.OF).',
|
|
336
589
|
{
|
|
337
590
|
type: 'object',
|
|
338
591
|
additionalProperties: true,
|
|
@@ -343,7 +596,8 @@ function registerControlTools(api, controlApi) {
|
|
|
343
596
|
operator_parameters: { type: 'object' },
|
|
344
597
|
channels: { type: 'array', items: { type: 'string' } },
|
|
345
598
|
channel_configs: { type: 'object' }
|
|
346
|
-
}
|
|
599
|
+
},
|
|
600
|
+
required: ['product_code', 'product_type', 'operator_parameters']
|
|
347
601
|
},
|
|
348
602
|
(args, meta = {}) => controlApi.createWatch(args || {}, meta?.context || {})
|
|
349
603
|
);
|
|
@@ -9,6 +9,12 @@ function makeRequestId() {
|
|
|
9
9
|
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** 与 OpenClawBridgeClient 一致:基金估值回源较慢 */
|
|
13
|
+
const FUND_ESTIMATES_REQUEST_TIMEOUT_MS = 120_000;
|
|
14
|
+
|
|
15
|
+
/** 网关表查询(新闻等)可能较慢 */
|
|
16
|
+
const FINANCE_TABLE_REQUEST_TIMEOUT_MS = 90_000;
|
|
17
|
+
|
|
12
18
|
function stableStringify(value) {
|
|
13
19
|
if (Array.isArray(value)) {
|
|
14
20
|
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
@@ -118,7 +124,7 @@ export class BridgeRuntime extends EventEmitter {
|
|
|
118
124
|
await this.lock?.release().catch(() => {});
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
async request(type, payload = {}) {
|
|
127
|
+
async request(type, payload = {}, options = {}) {
|
|
122
128
|
let requestId = makeRequestId();
|
|
123
129
|
let normalizedPayload = payload || {};
|
|
124
130
|
if (IDEMPOTENT_REQUEST_TYPES.has(type)) {
|
|
@@ -126,6 +132,13 @@ export class BridgeRuntime extends EventEmitter {
|
|
|
126
132
|
requestId = resolved.requestId;
|
|
127
133
|
normalizedPayload = resolved.payload;
|
|
128
134
|
}
|
|
135
|
+
const timeoutMs =
|
|
136
|
+
options.requestTimeoutMs ??
|
|
137
|
+
(type === 'fund.estimates'
|
|
138
|
+
? FUND_ESTIMATES_REQUEST_TIMEOUT_MS
|
|
139
|
+
: type === 'finance.table'
|
|
140
|
+
? FINANCE_TABLE_REQUEST_TIMEOUT_MS
|
|
141
|
+
: this.requestTimeoutMs);
|
|
129
142
|
const msg = { type, request_id: requestId, payload: normalizedPayload };
|
|
130
143
|
const { promise, resolve, reject } = this._buildWaiter();
|
|
131
144
|
|
|
@@ -138,10 +151,10 @@ export class BridgeRuntime extends EventEmitter {
|
|
|
138
151
|
reject(new Error(`request queue overflow (max=${this.maxQueueSize})`));
|
|
139
152
|
return promise;
|
|
140
153
|
}
|
|
141
|
-
this.requestQueue.push({ msg, requestId, resolve, reject });
|
|
154
|
+
this.requestQueue.push({ msg, requestId, resolve, reject, timeoutMs });
|
|
142
155
|
return promise;
|
|
143
156
|
}
|
|
144
|
-
this._sendWithTimeout({ msg, requestId, resolve, reject });
|
|
157
|
+
this._sendWithTimeout({ msg, requestId, resolve, reject, timeoutMs });
|
|
145
158
|
return promise;
|
|
146
159
|
}
|
|
147
160
|
|
|
@@ -155,11 +168,12 @@ export class BridgeRuntime extends EventEmitter {
|
|
|
155
168
|
return { promise, resolve, reject };
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
_sendWithTimeout({ msg, requestId, resolve, reject }) {
|
|
171
|
+
_sendWithTimeout({ msg, requestId, resolve, reject, timeoutMs }) {
|
|
172
|
+
const ms = timeoutMs ?? this.requestTimeoutMs;
|
|
159
173
|
const timer = setTimeout(() => {
|
|
160
174
|
this.pending.delete(requestId);
|
|
161
175
|
reject(new Error(`request timeout: ${msg.type} (${requestId})`));
|
|
162
|
-
},
|
|
176
|
+
}, ms);
|
|
163
177
|
|
|
164
178
|
this.pending.set(requestId, { resolve, reject, timer });
|
|
165
179
|
try {
|
|
@@ -1,56 +0,0 @@
|
|
|
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
|
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
- 直接返回失败原因,不静默重试
|