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.
@@ -0,0 +1,62 @@
1
+ # 实时行情与基金估值
2
+
3
+ ## 适用范围
4
+
5
+ - 股票/指数/加密实时行情:`watch_query_ticker`
6
+ - 基金当日估值:`watch_query_fund_estimates`
7
+ - 注意:基金不支持 `watch_create` 创建盯盘策略
8
+
9
+ ## 1) 股票/指数/加密实时行情:`watch_query_ticker`
10
+
11
+ 必填参数:
12
+ - `market`:`a` / `hk` / `crypto`(支持中文别名)
13
+ - `symbol`
14
+
15
+ 可选参数:
16
+ - `segment`:`auto` / `stock` / `index`
17
+
18
+ 成功判定:
19
+ - `success === true`
20
+ - `http_status === 200`
21
+ - 行情在 `quote`(英文键,如 `last`、`name`、`pct_change`)
22
+
23
+ 失败处理:
24
+ - 读取 `error` 与 `http_status` 直接反馈,不静默重试
25
+
26
+ 示例:
27
+
28
+ ```javascript
29
+ await watch_query_ticker({ market: 'hk', symbol: '00700', segment: 'stock' })
30
+ await watch_query_ticker({ market: 'A股', symbol: '600000.SH' })
31
+ await watch_query_ticker({ market: 'crypto', symbol: 'BTCUSDT' })
32
+ ```
33
+
34
+ ## 2) 基金估值:`watch_query_fund_estimates`
35
+
36
+ 参数(二选一):
37
+ - `fund_codes`
38
+ - `fundCodes`
39
+
40
+ 值格式:
41
+ - 单只:`"000006.OF"`
42
+ - 多只:`["000006.OF", "110011.OF"]`
43
+
44
+ 成功判定:
45
+ - `success === true`
46
+ - `http_status === 200`
47
+ - `data` 为基金代码映射
48
+
49
+ 失败处理:
50
+ - 读取 `error` 与 `http_status` 反馈用户
51
+
52
+ 示例:
53
+
54
+ ```javascript
55
+ await watch_query_fund_estimates({ fund_codes: '000006.OF' })
56
+ await watch_query_fund_estimates({ fund_codes: ['000006.OF', '110011.OF'] })
57
+ ```
58
+
59
+ ## 3) 硬边界
60
+
61
+ - 不要用 `watch_query_ticker` 查基金当日估值
62
+ - 不要对基金调用 `watch_create`
@@ -0,0 +1,74 @@
1
+ # 标的检索与交易日查询
2
+
3
+ ## 适用范围
4
+
5
+ - 名称/模糊输入 -> 本地 CSV 匹配或代码:`watch_search_*_basic`
6
+ - 是否开市/交易日:`watch_trade_calendar`
7
+
8
+ ## 1) 标的检索顺序(必须遵循)
9
+
10
+ 1. 先查本地 CSV(`skills/glance-watch/data/*.csv`)
11
+ 2. 本地未命中,再调用 `watch_search_*_basic`
12
+ 3. 多候选时必须让用户确认后再创建策略
13
+
14
+ CSV 映射:
15
+ - `stock_a.csv` -> `product_type=stock`
16
+ - `stock_hk.csv` -> `product_type=hk_stock`
17
+ - `index_a.csv` -> `product_type=index`
18
+ - `index_hk.csv` -> `product_type=index`
19
+ - 基金 `xxxxxx.OF` -> 仅估值/基础信息,不创建策略
20
+
21
+ ### 本地 CSV 匹配算法(执行约束)
22
+
23
+ - 可使用 `rg`/`grep` 检索,不要求固定命令模板。
24
+ - 匹配优先级:
25
+ 1. `完整代码` 精确匹配(如 `600000.SH`、`00700.HK`)
26
+ 2. `代码` 精确匹配(如 `600000`、`00700`)
27
+ 3. `名称` 精确匹配
28
+ 4. `名称` 模糊匹配
29
+ - 命中后优先使用 `完整代码` + `市场` 来确定后续 `market/symbol`,避免仅用短代码。
30
+ - 若出现重名/重码或返回多条候选:必须先向用户确认具体标的,再执行 `watch_query_ticker` 或 `watch_create`。
31
+
32
+ ## 2) 网关基础信息检索工具
33
+
34
+ - `watch_search_a_stock_basic`(A股)
35
+ - `watch_search_hk_stock_basic`(港股)
36
+ - `watch_search_index_basic`(指数)
37
+ - `watch_search_fund_basic`(基金基础信息)
38
+
39
+ 统一规则:
40
+ - 名称检索至少给 `keyword` 或 `q`
41
+ - 成功返回 `finance.table.result`,结果在 `data[]`
42
+
43
+ 示例:
44
+
45
+ ```javascript
46
+ await watch_search_a_stock_basic({ keyword: '平安银行', limit: 5 })
47
+ await watch_search_hk_stock_basic({ q: '腾讯' })
48
+ await watch_search_index_basic({ keyword: '沪深300' })
49
+ await watch_search_fund_basic({ ts_code: '000006.OF' })
50
+ ```
51
+
52
+ ## 3) 交易日查询:`watch_trade_calendar`
53
+
54
+ 参数:
55
+ - `exchange`(A股常用 `SSE` 或 `SZSE`)
56
+ - `start_date`
57
+ - `end_date`
58
+
59
+ 可兼容:
60
+ - `startDate` / `endDate`(会归一化)
61
+
62
+ 成功判定:
63
+ - `success === true`
64
+ - `data[]` 可读 `is_open` 等字段
65
+
66
+ 示例:
67
+
68
+ ```javascript
69
+ await watch_trade_calendar({
70
+ exchange: 'SSE',
71
+ start_date: '2026-04-10',
72
+ end_date: '2026-04-10'
73
+ })
74
+ ```
@@ -6,13 +6,16 @@
6
6
  - 重试必须使用同一组 payload(字段和值不变)。
7
7
  - `request_id` 由插件运行时自动生成并在同 payload 上复用。
8
8
 
9
- ## 创建策略失败(`watch.create`)
9
+ ## 创建策略失败(`watch_create`)
10
10
 
11
11
  处理规则:
12
12
  - 明确返回失败原因,不静默重试。
13
13
  - 若报“未注册的算子类型”,将 `operator_type` 修正为 `rule` 后重试。
14
+ - 若报 `UNSUPPORTED_PRODUCT_TYPE`,说明命中了基金边界(如 `fund` 或 `000006.OF`):
15
+ - 不再重试 `watch_create`
16
+ - 改用 `watch_query_fund_estimates` 或 `watch_search_fund_basic`
14
17
 
15
- ## 通知失败(`notify.*`)
18
+ ## 通知失败(`notify_*`)
16
19
 
17
20
  处理规则:
18
21
  - 优先返回 `code/error/hint`。
@@ -1,30 +1,46 @@
1
- # Watch 动作契约
1
+ # 盯盘策略(watch_*)
2
2
 
3
- ## `watch.query_ticker`
3
+ ## 适用工具
4
4
 
5
- 参数:
6
- - `stockCode`(或 `productCode`)
7
- - `productType`
8
- - `market`(`crypto` 可传空字符串)
5
+ - `watch_create`
6
+ - `watch_list`
7
+ - `watch_pause`
8
+ - `watch_activate`
9
+ - `watch_remove`
9
10
 
10
- 成功判定:
11
- - `code = "000000"` 或 `success = true`
11
+ ## 0. 产品边界
12
12
 
13
- 失败处理:
14
- - 返回失败原因
15
- - 引导用户确认代码/市场后重试
13
+ - 支持盯盘:`stock` / `hk_stock` / `index` / `crypto`
14
+ - 不支持盯盘:基金(`product_type=fund` 或代码形如 `xxxxxx.OF`)
15
+ - 基金相关请改用:`watch_query_fund_estimates` / `watch_search_fund_basic`
16
16
 
17
- ## `watch.create`
17
+ ## 1. `watch_create`
18
18
 
19
19
  最小参数:
20
20
  - `product_code`
21
21
  - `product_type`
22
22
  - `operator_type`(固定 `rule`)
23
- - `operator_parameters`(包含 `condition`、`variables`)
23
+ - `operator_parameters.condition`
24
+ - `operator_parameters.variables`
24
25
 
25
26
  建议参数:
26
- - `channels`(默认至少 `openclaw`)
27
- - `channel_configs.*`
27
+ - `channels`(至少 1 个渠道;是否包含 `openclaw` 见下方渠道规则)
28
+ - `channel_configs`
29
+
30
+ 渠道规则:
31
+ - 用户明确“仅/只用某几个渠道”时:严格按用户指定,可不含 `openclaw`。
32
+ - 用户只说“用某个渠道/某几个渠道”但未强调“仅限”时:默认补 `openclaw`。
33
+ - 不得擅自附加除 `openclaw` 之外的其他渠道。
34
+
35
+ OpenClaw 路由约束(当 `channels` 包含 `openclaw`):
36
+ - `channel_configs.openclaw` 必须能定位当前会话。
37
+ - 常用字段:`channel`(或 `source_channel`)、`account_id`、`session_key`、`conversation_id`(或 `chat_id`)。
38
+ - 禁止传空对象 `openclaw: {}` 充当已配置路由。
39
+ - 若宿主提供 `context` 路由信息,优先使用宿主信息。
40
+
41
+ 若 `channels` 包含 `call/sms/email/dingtalk`:
42
+ - 调用前必须读取联系人 CSV 并补齐参数
43
+ - 不可省略对应渠道必填配置
28
44
 
29
45
  固定结构(字段名不可改):
30
46
 
@@ -63,70 +79,90 @@
63
79
  }
64
80
  ```
65
81
 
82
+ 成功判定:
83
+ - `success === true`
84
+
85
+ 失败处理:
86
+ - 返回失败原因,不静默重试
87
+ - 网络波动可重试,但 payload 字段和值保持一致
88
+ - 不手动传 `request_id`
89
+
66
90
  禁止项:
67
91
  - `operator_type` 非 `rule`
68
- - 顶层 `condition`
69
- - `channel_configs` 渠道配置是字符串
70
- - 用户未要求渠道却默认附加
92
+ - 顶层放 `condition`
93
+ - `channel_configs.*` 传 JSON 字符串
94
+ - 用户明确“仅/只用某几个渠道”时仍强行附加渠道
95
+ - 基金标的调用 `watch_create`
71
96
 
72
- 成功判定:
73
- - `success = true`
97
+ ## 联系人 CSV(创建策略场景必遵守)
74
98
 
75
- 失败处理:
76
- - 明确返回失败原因,不静默重试
77
- - 超时/网络波动重试时,使用同一 payload(字段和值不变)
78
- - `request_id` 由插件运行时自动生成并复用,大模型不手动传
99
+ 联系人真源:`~/.openclaw/workspace/memory/watch-notify-contacts.csv`
79
100
 
80
- ## `watch.pause` / `watch.activate` / `watch.remove`
101
+ 规则:
102
+ 1. `watch_create` 涉及 `call/sms/email/dingtalk` 时,调用前必须先查 CSV。
103
+ 2. 补值优先级:用户本轮输入 > CSV 历史值 > 追问。
104
+ 3. 调用成功后,如联系方式有新增或变化,必须回写 CSV。
105
+ 4. 只认该 CSV,不读取其他联系人文件。
81
106
 
82
- 参数:
83
- - `strategyId`(或 `strategy_id`)
107
+ 建议表头:
84
108
 
85
- 成功判定:
86
- - `success = true`
109
+ ```text
110
+ channel,sender_id,sender_name,phone,email,dingtalk_cas_id,customer_name,updated_at,notes
111
+ ```
87
112
 
88
- 失败处理:
89
- - 返回失败原因并提示用户确认策略 ID
113
+ 查询示例:
114
+
115
+ ```bash
116
+ rg -n '^sms,jinguo\.xie,' ~/.openclaw/workspace/memory/watch-notify-contacts.csv
117
+ ```
118
+
119
+ 回写要求:
120
+
121
+ - 已有同一 `(channel, sender_id)`:更新该行,不重复追加
122
+ - 不存在:按表头顺序追加
123
+ - `updated_at`:写 ISO-8601 时间
90
124
 
91
- ## `watch.list`
125
+ ## 2. `watch_list`
92
126
 
93
127
  可选参数:
94
- - `status`: `active/paused/completed/failed/expired`
128
+ - `status`:`active/paused/completed/failed/expired`
95
129
  - `product_code`(或 `productCode`)
96
130
 
97
131
  成功判定:
98
- - `success = true`
99
- - `data.total` 为命中数量
100
- - `data.strategies` 为策略列表
132
+ - `success === true`
133
+ - `data.total` 与 `data.strategies` 可读
101
134
 
102
- 失败处理:
103
- - 返回失败原因,不静默重试
104
- - 空结果明确告知“当前条件下没有策略”
135
+ 空结果文案:
136
+ - 当 `data.total === 0` 时,明确告知“当前条件下没有策略”。
105
137
 
106
138
  安全约束:
107
- - 仅可查询当前连接用户自己的策略
108
- - 不通过 `user_id/use_id` 越权查询
139
+ - 仅查询当前连接用户策略
140
+ - 禁止通过 `user_id/use_id` 越权查询
141
+
142
+ ## 3. `watch_pause` / `watch_activate` / `watch_remove`
143
+
144
+ 参数:
145
+ - `strategy_id`(或 `strategyId`)
146
+
147
+ 成功判定:
148
+ - `success === true`
109
149
 
110
- ## 调用前最终检查
150
+ ## 调用前最终检查(`watch_create`)
111
151
 
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`
152
+ 1. 字段名使用 snake_case:`product_code/product_type/operator_type/operator_parameters/channels/channel_configs`
153
+ 2. `operator_type` 固定为 `rule`
154
+ 3. `operator_parameters.condition` 与 `operator_parameters.variables` 同时存在
155
+ 4. `channels` 至少一个,且与 `channel_configs` 对应
156
+ 5. 渠道配置必须是对象,不能是 JSON 字符串
157
+ 6. 不手动传 `request_id`
118
158
 
119
- ## 买卖意图与条件方向
159
+ ## 4. 买卖意图与条件方向
120
160
 
121
161
  | 用户意图 | 条件方向 |
122
- |---------|---------|
162
+ |---|---|
123
163
  | 买入(逢低) | `price <= threshold` |
124
164
  | 卖出(止盈/止损) | `price >= threshold` |
125
165
 
126
- 判断规则:
127
- 1. 用户明确说“涨到/跌到”时,按方向直接生成条件。
128
- 2. 用户只说“到了 XX 提醒我”时,必须追问“买还是卖”。
129
- 3. 常见映射:
130
- - `涨到/涨过/突破/冲到` -> `price >= threshold`
131
- - `跌到/跌破/回调到/回到` -> `price <= threshold`
132
- - `到了/到达/价格到` -> 方向不明确,需追问
166
+ 规则:
167
+ 1. 用户明确“涨到/跌到”时按方向生成。
168
+ 2. 用户只说“到了XX提醒我”时先追问买卖方向。
@@ -3,6 +3,12 @@ import WebSocket from 'ws';
3
3
 
4
4
  const DEFAULT_BASE_WS_URL = 'wss://glanceup-pre.100credit.cn';
5
5
 
6
+ /** 基金估值网关可能较慢,应大于 bridge 侧 OPENCLAW_FUND_ESTIMATES_TIMEOUT_SECONDS(默认 90s) */
7
+ const FUND_ESTIMATES_REQUEST_TIMEOUT_MS = 120_000;
8
+
9
+ /** 白名单表接口(含 fin_news)可能较慢 */
10
+ const FINANCE_TABLE_REQUEST_TIMEOUT_MS = 90_000;
11
+
6
12
  function sleep(ms) {
7
13
  return new Promise((resolve) => setTimeout(resolve, ms));
8
14
  }
@@ -180,6 +186,18 @@ export class OpenClawBridgeClient extends EventEmitter {
180
186
  return this._request('ticker.query', payload || {});
181
187
  }
182
188
 
189
+ async queryFundEstimates(payload) {
190
+ return this._request('fund.estimates', payload || {}, {
191
+ requestTimeoutMs: FUND_ESTIMATES_REQUEST_TIMEOUT_MS
192
+ });
193
+ }
194
+
195
+ async queryFinanceTable(payload) {
196
+ return this._request('finance.table', payload || {}, {
197
+ requestTimeoutMs: FINANCE_TABLE_REQUEST_TIMEOUT_MS
198
+ });
199
+ }
200
+
183
201
  async waitUntilConnected(timeoutMs = this.waitConnectTimeoutMs) {
184
202
  if (this.connected && this.ws && this.ws.readyState === WebSocket.OPEN) {
185
203
  return true;
@@ -216,6 +234,7 @@ export class OpenClawBridgeClient extends EventEmitter {
216
234
  if (!requestId) {
217
235
  requestId = makeRequestId();
218
236
  }
237
+ const timeoutMs = options.requestTimeoutMs ?? this.requestTimeoutMs;
219
238
  const msg = { type, request_id: requestId, payload: normalizedPayload };
220
239
  const { promise, resolve, reject } = this._buildWaiter(type, requestId);
221
240
 
@@ -228,12 +247,12 @@ export class OpenClawBridgeClient extends EventEmitter {
228
247
  reject(new Error(`request queue overflow (max=${this.maxQueueSize})`));
229
248
  return promise;
230
249
  }
231
- this.requestQueue.push({ msg, requestId, type, resolve, reject });
250
+ this.requestQueue.push({ msg, requestId, type, resolve, reject, timeoutMs });
232
251
  this.emit('queued', { type, requestId, queueSize: this.requestQueue.length });
233
252
  return promise;
234
253
  }
235
254
 
236
- this._sendWithTimeout({ msg, requestId, type, resolve, reject });
255
+ this._sendWithTimeout({ msg, requestId, type, resolve, reject, timeoutMs });
237
256
  return promise;
238
257
  }
239
258
 
@@ -247,11 +266,12 @@ export class OpenClawBridgeClient extends EventEmitter {
247
266
  return { promise, resolve, reject, type, requestId };
248
267
  }
249
268
 
250
- _sendWithTimeout({ msg, requestId, type, resolve, reject }) {
269
+ _sendWithTimeout({ msg, requestId, type, resolve, reject, timeoutMs }) {
270
+ const ms = timeoutMs ?? this.requestTimeoutMs;
251
271
  const timer = setTimeout(() => {
252
272
  this.pending.delete(requestId);
253
273
  reject(new Error(`request timeout: ${type} (${requestId})`));
254
- }, this.requestTimeoutMs);
274
+ }, ms);
255
275
 
256
276
  this.pending.set(requestId, { resolve, reject, timer, type });
257
277
  this.ws.send(JSON.stringify(msg));
@@ -1,3 +1,9 @@
1
+ import {
2
+ normalizeFundBasicTableQuery,
3
+ normalizeKeywordTableQuery,
4
+ normalizeTickerQuery,
5
+ normalizeTradeCalendarQuery
6
+ } from './agentQueryNormalize.js';
1
7
  import { OpenClawBridgeClient } from './OpenClawBridgeClient.js';
2
8
  import { extractOpenclawRoutingFromRecord } from './openclawRouting.js';
3
9
 
@@ -134,21 +140,81 @@ export class OpenClawPluginAdapter {
134
140
  }
135
141
 
136
142
  /**
137
- * 查询标的实时行情(透传到 bridge ticker.query)。
143
+ * 查询标的实时行情;参数为 market / symbol / segment,与网关 quote 接口一致;应答为 ticker.query.result。
138
144
  */
139
145
  async queryTickerData(query) {
140
- const stockCode = query?.stockCode || query?.productCode || query?.stock_code || '';
141
- const productType = query?.productType || query?.product_type || '';
142
- let market = query?.market;
146
+ const payload = normalizeTickerQuery(query || {});
147
+ if (!payload.market || !payload.symbol) {
148
+ throw new Error(
149
+ 'queryTickerData requires market and symbol. If user gave a name only, use search*Basic first.'
150
+ );
151
+ }
152
+ return this.client.queryTickerData(payload);
153
+ }
143
154
 
144
- if (market == null && String(productType).toLowerCase() === 'crypto') {
145
- market = '';
155
+ /**
156
+ * 基金实时估值;payload.fund_codes 与网关 POST /v1/realtime/fund/estimates 一致;应答为 fund.estimates.result。
157
+ */
158
+ async queryFundEstimates(query) {
159
+ let fundCodes = query?.fund_codes ?? query?.fundCodes;
160
+ if (fundCodes == null) {
161
+ throw new Error('queryFundEstimates requires fund_codes (or fundCodes)');
162
+ }
163
+ if (typeof fundCodes === 'string') {
164
+ fundCodes = fundCodes.trim();
165
+ } else if (Array.isArray(fundCodes)) {
166
+ fundCodes = fundCodes.map((x) => String(x).trim()).filter(Boolean);
167
+ } else {
168
+ throw new Error('fund_codes must be a string or string[]');
146
169
  }
170
+ return this.client.queryFundEstimates({ fund_codes: fundCodes });
171
+ }
172
+
173
+ async searchAStockBasic(query) {
174
+ const q = normalizeKeywordTableQuery(query, 'searchAStockBasic');
175
+ return this.client.queryFinanceTable({
176
+ path: '/v1/a-stock/basic/search',
177
+ query: q
178
+ });
179
+ }
180
+
181
+ async searchHkStockBasic(query) {
182
+ const q = normalizeKeywordTableQuery(query, 'searchHkStockBasic');
183
+ return this.client.queryFinanceTable({
184
+ path: '/v1/hk-stock/basic/search',
185
+ query: q
186
+ });
187
+ }
188
+
189
+ async searchIndexBasic(query) {
190
+ const q = normalizeKeywordTableQuery(query, 'searchIndexBasic');
191
+ return this.client.queryFinanceTable({
192
+ path: '/v1/index/basic/search',
193
+ query: q
194
+ });
195
+ }
196
+
197
+ async searchFundBasic(query) {
198
+ const q = normalizeFundBasicTableQuery(query);
199
+ return this.client.queryFinanceTable({
200
+ path: '/v1/fund/basic',
201
+ query: q
202
+ });
203
+ }
204
+
205
+ async queryFinNews(query) {
206
+ const q = normalizeKeywordTableQuery(query, 'queryFinNews');
207
+ return this.client.queryFinanceTable({
208
+ path: '/v1/news',
209
+ query: q
210
+ });
211
+ }
147
212
 
148
- return this.client.queryTickerData({
149
- stock_code: stockCode,
150
- market: market == null ? '' : String(market),
151
- product_type: productType
213
+ async queryTradeCalendar(query) {
214
+ const q = normalizeTradeCalendarQuery(query);
215
+ return this.client.queryFinanceTable({
216
+ path: '/v1/trade-calendar',
217
+ query: q
152
218
  });
153
219
  }
154
220
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * OpenClaw / Agent 侧查询参数归一化,与 buildControlApi 行为一致。
3
+ */
4
+
5
+ export function trimQuery(v) {
6
+ if (v == null) return '';
7
+ return String(v).trim();
8
+ }
9
+
10
+ /** 中文/别名 → 网关 market;symbol 去空格 */
11
+ export function normalizeTickerQuery(query = {}) {
12
+ const raw = trimQuery(query.market);
13
+ const lower = raw.toLowerCase();
14
+ const cn = {
15
+ A股: 'a',
16
+ a股: 'a',
17
+ 沪深: 'a',
18
+ 上证: 'a',
19
+ 深市: 'a',
20
+ 港股: 'hk',
21
+ 港交所: 'hk',
22
+ 加密: 'crypto',
23
+ 数字货币: 'crypto',
24
+ 虚拟货币: 'crypto',
25
+ 比特币: 'crypto'
26
+ };
27
+ let market = cn[raw] || lower;
28
+ if (market === 'hongkong' || market === 'hong kong') market = 'hk';
29
+ if (market === 'btc' || market === 'crypto-currency') market = 'crypto';
30
+ const symbol = trimQuery(query.symbol);
31
+ const out = { market, symbol };
32
+ const seg = query.segment != null ? trimQuery(query.segment) : '';
33
+ if (seg) out.segment = seg;
34
+ return out;
35
+ }
36
+
37
+ export function normalizeKeywordTableQuery(query = {}, label) {
38
+ const q = { ...(query || {}) };
39
+ const k = trimQuery(q.keyword);
40
+ const qq = trimQuery(q.q);
41
+ const text = k || qq;
42
+ if (!text) {
43
+ throw new Error(
44
+ `${label} requires keyword or q (search text). If user only gave a name, put it in keyword.`
45
+ );
46
+ }
47
+ q.keyword = text;
48
+ delete q.q;
49
+ return q;
50
+ }
51
+
52
+ export function normalizeFundBasicTableQuery(query = {}) {
53
+ const q = { ...(query || {}) };
54
+ const tc = trimQuery(q.ts_code ?? q.tsCode);
55
+ if (tc) {
56
+ const out = { ...q, ts_code: tc };
57
+ delete out.tsCode;
58
+ delete out.keyword;
59
+ delete out.q;
60
+ return out;
61
+ }
62
+ return normalizeKeywordTableQuery(q, 'searchFundBasic');
63
+ }
64
+
65
+ export function normalizeTradeCalendarQuery(query = {}) {
66
+ const q = { ...(query || {}) };
67
+ const start = trimQuery(q.start_date ?? q.startDate);
68
+ const end = trimQuery(q.end_date ?? q.endDate);
69
+ const ex = trimQuery(q.exchange);
70
+ if (!ex) {
71
+ throw new Error(
72
+ 'queryTradeCalendar requires exchange (SSE=上交所, SZSE=深交所, 等,与网关一致)'
73
+ );
74
+ }
75
+ if (!start || !end) {
76
+ throw new Error(
77
+ 'queryTradeCalendar requires start_date and end_date as YYYY-MM-DD (or startDate/endDate)'
78
+ );
79
+ }
80
+ const out = { ...q, exchange: ex, start_date: start, end_date: end };
81
+ delete out.startDate;
82
+ delete out.endDate;
83
+ return out;
84
+ }