@v587d/capital-generation 0.0.0-stage → 2.6.1

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.
Files changed (117) hide show
  1. package/CHANGELOG.md +814 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -2
  4. package/assets/cg-icon.png +0 -0
  5. package/capital-config/client.js +28 -0
  6. package/capital-config/client.src.cjs +388 -0
  7. package/capital-config/index.js +113 -0
  8. package/capital-config/package.json +36 -0
  9. package/capital-watchlist/client.js +182 -0
  10. package/capital-watchlist/client.src.cjs +1229 -0
  11. package/capital-watchlist/index.js +772 -0
  12. package/capital-watchlist/package.json +37 -0
  13. package/chart-ui/README.md +112 -0
  14. package/chart-ui/client.js +16 -0
  15. package/chart-ui/client.src.cjs +126 -0
  16. package/chart-ui/index.js +205 -0
  17. package/chart-ui/package.json +30 -0
  18. package/cordis.patch.yml +72 -0
  19. package/lib/agents/bash-guard.d.ts +55 -0
  20. package/lib/agents/bash-guard.js +162 -0
  21. package/lib/agents/root-tool-policy.d.ts +164 -0
  22. package/lib/agents/root-tool-policy.js +309 -0
  23. package/lib/chart/artifact-ref.d.ts +65 -0
  24. package/lib/chart/artifact-ref.js +107 -0
  25. package/lib/chart/downsample.d.ts +8 -0
  26. package/lib/chart/downsample.js +53 -0
  27. package/lib/chart/errors.d.ts +16 -0
  28. package/lib/chart/errors.js +15 -0
  29. package/lib/chart/events.d.ts +120 -0
  30. package/lib/chart/events.js +471 -0
  31. package/lib/chart/html.d.ts +21 -0
  32. package/lib/chart/html.js +154 -0
  33. package/lib/chart/runtime.d.ts +20 -0
  34. package/lib/chart/runtime.js +270 -0
  35. package/lib/chart/series.d.ts +102 -0
  36. package/lib/chart/series.js +424 -0
  37. package/lib/chart/source-token.d.ts +64 -0
  38. package/lib/chart/source-token.js +217 -0
  39. package/lib/chart/spec.d.ts +58 -0
  40. package/lib/chart/spec.js +265 -0
  41. package/lib/chart/tool.d.ts +86 -0
  42. package/lib/chart/tool.js +288 -0
  43. package/lib/chart/vendor/LICENSE +201 -0
  44. package/lib/chart/vendor/VERSION.json +8 -0
  45. package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
  46. package/lib/data-collector/args.d.ts +38 -0
  47. package/lib/data-collector/args.js +114 -0
  48. package/lib/data-collector/dataset-tools.d.ts +3 -0
  49. package/lib/data-collector/dataset-tools.js +421 -0
  50. package/lib/data-collector/describe.d.ts +99 -0
  51. package/lib/data-collector/describe.js +256 -0
  52. package/lib/data-collector/hub.d.ts +187 -0
  53. package/lib/data-collector/hub.js +469 -0
  54. package/lib/data-collector/query-time.d.ts +58 -0
  55. package/lib/data-collector/query-time.js +101 -0
  56. package/lib/data-collector/query.d.ts +57 -0
  57. package/lib/data-collector/query.js +473 -0
  58. package/lib/data-collector/store.d.ts +499 -0
  59. package/lib/data-collector/store.js +1486 -0
  60. package/lib/data-collector/time-axis.d.ts +169 -0
  61. package/lib/data-collector/time-axis.js +419 -0
  62. package/lib/data-collector/tools.d.ts +32 -0
  63. package/lib/data-collector/tools.js +216 -0
  64. package/lib/index.d.ts +275 -0
  65. package/lib/index.js +458 -0
  66. package/lib/net/eastmoney-client.d.ts +104 -0
  67. package/lib/net/eastmoney-client.js +118 -0
  68. package/lib/net/throttle.d.ts +16 -0
  69. package/lib/net/throttle.js +34 -0
  70. package/lib/ocr/client.d.ts +82 -0
  71. package/lib/ocr/client.js +281 -0
  72. package/lib/ocr/markdown.d.ts +75 -0
  73. package/lib/ocr/markdown.js +209 -0
  74. package/lib/ocr/store.d.ts +119 -0
  75. package/lib/ocr/store.js +226 -0
  76. package/lib/sources/eastmoney-http.d.ts +2 -0
  77. package/lib/sources/eastmoney-http.js +1253 -0
  78. package/lib/sources/fuyao-core.d.ts +52 -0
  79. package/lib/sources/fuyao-core.js +96 -0
  80. package/lib/sources/fuyao-rest.d.ts +14 -0
  81. package/lib/sources/fuyao-rest.js +2057 -0
  82. package/lib/sources/security-code.d.ts +27 -0
  83. package/lib/sources/security-code.js +104 -0
  84. package/lib/sources/security-identity.d.ts +24 -0
  85. package/lib/sources/security-identity.js +89 -0
  86. package/lib/sources/tencent-http.d.ts +2 -0
  87. package/lib/sources/tencent-http.js +1053 -0
  88. package/lib/sources/wind-mcp.d.ts +8 -0
  89. package/lib/sources/wind-mcp.js +593 -0
  90. package/lib/time/tools.d.ts +74 -0
  91. package/lib/time/tools.js +464 -0
  92. package/lib/tool-exec.d.ts +29 -0
  93. package/lib/tool-exec.js +17 -0
  94. package/lib/web-retriever/engines.d.ts +61 -0
  95. package/lib/web-retriever/engines.js +202 -0
  96. package/lib/web-retriever/html-markdown.d.ts +50 -0
  97. package/lib/web-retriever/html-markdown.js +321 -0
  98. package/lib/web-retriever/local-fetch.d.ts +121 -0
  99. package/lib/web-retriever/local-fetch.js +562 -0
  100. package/lib/web-retriever/retriever.d.ts +58 -0
  101. package/lib/web-retriever/retriever.js +94 -0
  102. package/lib/web-retriever/sources.d.ts +235 -0
  103. package/lib/web-retriever/sources.js +961 -0
  104. package/lib/web-retriever/tools.d.ts +50 -0
  105. package/lib/web-retriever/tools.js +1029 -0
  106. package/lib/web-retriever/wind-client.d.ts +69 -0
  107. package/lib/web-retriever/wind-client.js +311 -0
  108. package/locale/en.json +6 -0
  109. package/locale/zh.json +6 -0
  110. package/package.json +84 -4
  111. package/preset/capital-generation/README.md +161 -0
  112. package/preset/capital-generation/agent.patch.yml +509 -0
  113. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
  114. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
  115. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
  116. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
  117. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
@@ -0,0 +1,8 @@
1
+ import type { DataSource } from '../data-collector/hub.js';
2
+ export type WindApiKeyResolver = () => Promise<string | undefined>;
3
+ /**
4
+ * §6.7 改口后的 P1 三条。`wind_edb_*` 是 P0(EDB 是唯一来源),`wind_stock_kline` 是 P1。
5
+ * 不接的一概有理由:price_indicators 与全部 NL 工具(中文键 / 列名是 question 的函数)、
6
+ * bond 与 fund/index(重叠或未测)、analytics_data(实测连 initialize 都挂)。
7
+ */
8
+ export declare function createWindSources(resolveApiKey: WindApiKeyResolver): DataSource[];
@@ -0,0 +1,593 @@
1
+ import { buildDataKey } from '../data-collector/hub.js';
2
+ import { isoDateFromEpoch } from '../data-collector/time-axis.js';
3
+ import { getDataTimeContract } from '../time/tools.js';
4
+ import { createWindClient, WIND_SERVER_ENDPOINTS } from '../web-retriever/wind-client.js';
5
+ /**
6
+ * Wind(万得)MCP 数据源——`docs/design/wind-market-source.md` §6.7 改口后的 P1 三条能力。
7
+ *
8
+ * 只接 fuyao 明确没有的:宏观/行业/汇率 EDB 序列(唯一来源)、按日期区间取的历史 K 线、
9
+ * 每根带成交额/换手/均价的 K 线。与腾讯重叠的港美股快照与日周月线不接,NL 工具一概不接
10
+ * (列名是 question 的函数,`output_schema` 无处锚定,§6.3)。
11
+ *
12
+ * ⛔ 下面每条解析约束都来自真报文(§6.2–§6.6 与 2026-10-06 第二轮),不是从上游文档抄的:
13
+ * 1. 表格工具回 `{columns:[{name}], rows:[[标量]]}`,行**不是对象**,必须按列名转置;
14
+ * 2. `columns[].type` 一律写 `string`,而 K 线/分钟的数值确实是字符串、EDB 的是 number——
15
+ * 类型元数据不可信,数值由本文件解析,解析不了就响亮失败(§9.5);
16
+ * 3. 时间写法一张表三种:K 线带**该行自己的**交易所偏移,EDB 是 `20260731` 紧凑串。
17
+ * epoch 逐行按自带偏移换算,整段按 +08:00 处理会把美股错位 12 小时(§6.4);
18
+ * 4. 行键全部 ASCII snake_case:`updateDate` → `update_date`、`CHANGEHANDRATE` →
19
+ * `turnover_rate_pct`(§10.6,映射不到的列丢弃而不是透传)。
20
+ */
21
+ /**
22
+ * 数据域的上游预算。健康响应实测 0.59–2.3 秒(EDB 取数 590ms、A 股分钟 653ms、日 K 2296ms),
23
+ * 而"该日没有数据"与"上游故障"在线上都表现为**挂到超时**(§6.6;2026-10-06 连昨天还正常的
24
+ * 港股日线也挂了三次)。Hub 的执行超时是 30 秒,而 `callTool` 每次要走 initialize + tools/call
25
+ * 两跳,所以单次取 12 秒:健康请求有 5 倍余量,病态请求在 Hub 给出无信息的
26
+ * `request timed out` 之前先由本文件给出**带原因**的失败。
27
+ */
28
+ const WIND_SOURCE_TIMEOUT_MS = 12_000;
29
+ /** 只放**每档都打过真报文**的 period(§6.5);`60 分钟`两次都挂,所以不放。 */
30
+ const KLINE_PERIOD_CODES = {
31
+ m1: '1', m5: '3', m15: '5', m30: '6', day: '10', week: '11', month: '12',
32
+ };
33
+ const KLINE_PERIOD_LIST = Object.keys(KLINE_PERIOD_CODES).join('/');
34
+ const MINUTE_PERIODS = new Set(['m1', 'm5', 'm15', 'm30']);
35
+ /** 分钟档区间上限:单日约 240 条(§1.3),而本能力不暴露 count,跨日体积没有别的闸门。 */
36
+ const MINUTE_MAX_SPAN_DAYS = 10;
37
+ const DAILY_MAX_SPAN_DAYS = 3660;
38
+ /** 上游 K 线的列名(§6.3:`MATCH` 就是收盘价,Wind 的 K 线没有 CLOSE 列)。 */
39
+ const KLINE_TIME = 'TIME';
40
+ const KLINE_OPEN = 'OPEN';
41
+ const KLINE_MATCH = 'MATCH';
42
+ const KLINE_HIGH = 'HIGH';
43
+ const KLINE_LOW = 'LOW';
44
+ const KLINE_TURNOVER = 'TURNOVER';
45
+ const KLINE_VOLUME = 'VOLUME';
46
+ const KLINE_RATE = 'CHANGEHANDRATE';
47
+ const KLINE_AVG = 'AVPRICE';
48
+ /** 缺任何一列就构不成一根 K 线:回别的列形态即"薄表"(§6.6 的不存在代码只回两列)。 */
49
+ const KLINE_REQUIRED = [KLINE_TIME, KLINE_OPEN, KLINE_MATCH, KLINE_HIGH, KLINE_LOW];
50
+ function isRecord(value) {
51
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
52
+ }
53
+ function assertKnown(values, allowed) {
54
+ for (const key of Object.keys(values))
55
+ if (!allowed.includes(key))
56
+ throw new Error(`unsupported parameter: ${key}`);
57
+ }
58
+ function windError(message, code) {
59
+ const error = new Error(message);
60
+ error.code = code;
61
+ return error;
62
+ }
63
+ function strictDate(value, name) {
64
+ const text = typeof value === 'string' ? value.trim() : '';
65
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
66
+ if (match === null)
67
+ throw new Error(`parameter ${name} must be YYYY-MM-DD`);
68
+ const parts = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
69
+ const check = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
70
+ if (Number.isNaN(check.getTime()) || check.getUTCFullYear() !== parts.year || check.getUTCMonth() + 1 !== parts.month || check.getUTCDate() !== parts.day) {
71
+ throw new Error(`parameter ${name} must be a valid YYYY-MM-DD date`);
72
+ }
73
+ return text;
74
+ }
75
+ function dayDistance(from, to) {
76
+ return Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
77
+ }
78
+ /** 上游紧凑日期 `20260731` → `2026-07-31`;写不出合法日历日就失败,不放行畸形标签。 */
79
+ function compactDate(value, context) {
80
+ if (value === null || value === undefined || value === '')
81
+ return null;
82
+ const match = /^(\d{4})(\d{2})(\d{2})$/.exec(String(value).trim());
83
+ if (match === null)
84
+ throw windError(`Wind ${context} has an unparseable date ${JSON.stringify(String(value).slice(0, 40))}`, 'wind_invalid_response');
85
+ const date = `${match[1]}-${match[2]}-${match[3]}`;
86
+ const check = new Date(`${date}T00:00:00Z`);
87
+ if (Number.isNaN(check.getTime()) || check.toISOString().slice(0, 10) !== date) {
88
+ throw windError(`Wind ${context} has an impossible date ${JSON.stringify(date)}`, 'wind_invalid_response');
89
+ }
90
+ return date;
91
+ }
92
+ /** EDB 的报告期标签没有时区;按本仓宏观表(东财那九张)同一口径落 Asia/Shanghai 零点。 */
93
+ function reportDateMs(date) {
94
+ return Date.parse(`${date}T00:00:00+08:00`);
95
+ }
96
+ /**
97
+ * 单元格 → 数值。`columns[].type` 一律写 `string`,所以必须自己解析(§6.2):K 线/分钟全是
98
+ * 字符串(`"1318.00"`),EDB 与快照是真 number,两种都得吃。`INVALID` 是后端的"缺失/不适用",
99
+ * 一律 null,**不许当 0 参与计算**(§1.4)。认不出的字符串是形状变了,不是缺数——响亮失败。
100
+ */
101
+ function windNumber(cell, context, field) {
102
+ if (cell === null || cell === undefined)
103
+ return null;
104
+ if (typeof cell === 'number')
105
+ return Number.isFinite(cell) ? cell : null;
106
+ if (typeof cell !== 'string')
107
+ throw windError(`Wind ${context} returned ${typeof cell} for ${field}`, 'wind_invalid_response');
108
+ const text = cell.trim().replace(/,/g, '');
109
+ if (text === '' || /^invalid$/i.test(text) || /^(?:-{1,2}|n\/?a|null|none)$/i.test(text))
110
+ return null;
111
+ const value = Number(text);
112
+ if (!Number.isFinite(value))
113
+ throw windError(`Wind ${context} returned unparseable ${field} ${JSON.stringify(cell)}`, 'wind_invalid_response');
114
+ return value;
115
+ }
116
+ /** 文本单元格:空串与 INVALID 都是"没有值";其余保留上游原文(含中文列值)。 */
117
+ function windText(cell) {
118
+ if (cell === null || cell === undefined)
119
+ return null;
120
+ const text = String(cell).trim();
121
+ if (text === '' || /^invalid$/i.test(text))
122
+ return null;
123
+ return text;
124
+ }
125
+ /**
126
+ * `unit` 的键是 `"HIGH 单位:"`——**空格 + 全角冒号**(§6.3)。逐列把上游原文摘进行里,
127
+ * 不翻译也不猜币种:同一列在港美股可能是"港元"/"美元",抄下上游那三个字是唯一不骗人的做法。
128
+ */
129
+ function windUnit(unit, column) {
130
+ if (!isRecord(unit))
131
+ return null;
132
+ for (const [key, value] of Object.entries(unit)) {
133
+ if (key.replace(/\s*单位[::]\s*$/u, '').trim() === column)
134
+ return windText(value);
135
+ }
136
+ return null;
137
+ }
138
+ /** 上游的业务错误有两种形态:能解析成 JSON 的走 `error` 字段,不能的整段就是文本。 */
139
+ function requirePayload(result, context) {
140
+ if (!result.ok) {
141
+ const message = result.error ?? 'unknown Wind error';
142
+ const code = result.code === 'AUTH' ? 'wind_api_key_missing'
143
+ : result.code === 'RATE_LIMIT' ? 'wind_rate_limit'
144
+ : result.code === 'NETWORK' ? 'wind_upstream_timeout'
145
+ : result.code === 'INVALID' ? 'wind_invalid_response' : 'wind_backend_error';
146
+ const hint = result.code === 'NETWORK'
147
+ ? ' Wind 未在预算内应答;该日缺数与上游故障在线上不可区分,不要盲目重试同一区间——改更早日期或改走日线。'
148
+ : result.code === 'AUTH' || result.code === 'RATE_LIMIT' ? ' 积分计费,不自动重试。' : '';
149
+ throw windError(`Wind ${context} failed (${String(result.code ?? 'ERROR')}): ${message.slice(0, 400)}.${hint}`, code);
150
+ }
151
+ if (isRecord(result.data)) {
152
+ const error = result.data.error;
153
+ if (error !== null && error !== undefined && !(typeof error === 'object' && !Array.isArray(error) && Object.keys(error).length === 0)) {
154
+ throw windError(`Wind ${context} returned a backend error: ${JSON.stringify(error).slice(0, 400)}`, 'wind_backend_error');
155
+ }
156
+ return result.data;
157
+ }
158
+ // 正文不是 JSON 时 wind-client 原样透传 `content`。上游的参数性抱怨(实测
159
+ // "observation或者[beginDate、endDate]必须填一个")就是这一类文本:必须带着原文失败,
160
+ // 不许退化成 0 行——那是把"你说错了"报成"没有数据"。
161
+ const text = typeof result.content === 'string' ? result.content.trim() : '';
162
+ throw windError(`Wind ${context} returned a non-table response: ${text.slice(0, 400) || '(empty)'}`, 'wind_unexpected_response');
163
+ }
164
+ /** 表格解包:`{data:{columns,rows,unit}}`(K 线/分钟)与顶层直接是表身的形态都接住。 */
165
+ function requireTable(payload, context) {
166
+ const node = isRecord(payload.data) ? payload.data : payload;
167
+ const { columns, rows } = node;
168
+ if (!Array.isArray(columns) || !Array.isArray(rows) || !columns.every((column) => isRecord(column) && typeof column.name === 'string')) {
169
+ throw windError(`Wind ${context} response is not a table (keys: ${Object.keys(node).join(', ') || 'none'})`, 'wind_invalid_response');
170
+ }
171
+ for (const row of rows)
172
+ if (!Array.isArray(row))
173
+ throw windError(`Wind ${context} row is not a scalar array`, 'wind_invalid_response');
174
+ return { columns: columns.map((column) => String(column.name)), rows: rows, unit: node.unit };
175
+ }
176
+ function columnIndex(table, name, context) {
177
+ const at = table.columns.indexOf(name);
178
+ if (at < 0)
179
+ throw windError(`Wind ${context} table has no ${name} column (columns: ${table.columns.join(', ')})`, 'wind_invalid_response');
180
+ return at;
181
+ }
182
+ function optionalColumnIndex(table, name) {
183
+ return table.columns.includes(name) ? table.columns.indexOf(name) : -1;
184
+ }
185
+ /**
186
+ * 指标元信息 → 行键。键集**不固定**:2026-10-06 实测汇率类没有 `unit`/`magnitude`/`currency`,
187
+ * GDP 类三者齐全;缺的键落 null,不猜单位(§10.6 第 2 条的显式映射表就在这五行里)。
188
+ * `series_begin_date`/`series_end_date` 是**该指标序列**的起止,不是本次请求的窗口。
189
+ */
190
+ function metricMeta(meta, context) {
191
+ const code = windText(meta.code);
192
+ if (code === null)
193
+ throw windError(`Wind ${context} returned a metric without a code`, 'wind_invalid_response');
194
+ return {
195
+ code,
196
+ name: windText(meta.name),
197
+ unit: windText(meta.unit),
198
+ freq: windText(meta.freq),
199
+ source: windText(meta.source),
200
+ report_date: null,
201
+ report_date_ms: null,
202
+ value: null,
203
+ update_date: compactDate(meta.updateDate, context),
204
+ series_begin_date: compactDate(meta.beginDate, context),
205
+ series_end_date: compactDate(meta.endDate, context),
206
+ magnitude: windText(meta.magnitude),
207
+ currency: windText(meta.currency),
208
+ };
209
+ }
210
+ function requireMetrics(payload, context) {
211
+ if (Array.isArray(payload.metrics))
212
+ return payload.metrics;
213
+ const nested = isRecord(payload.data) ? payload.data.metrics : undefined;
214
+ if (Array.isArray(nested))
215
+ return nested;
216
+ throw windError(`Wind ${context} returned no metrics list (keys: ${Object.keys(payload).join(', ')})`, 'wind_invalid_response');
217
+ }
218
+ async function executeEdbSearch(client, params, signal) {
219
+ const payload = requirePayload(await client.callTool('search_economic_indicator', { question: params.question }, signal), 'search_economic_indicator');
220
+ const metrics = requireMetrics(payload, 'search_economic_indicator');
221
+ const rows = metrics.map((item) => {
222
+ if (!isRecord(item))
223
+ throw windError('Wind search_economic_indicator returned a non-object metric', 'wind_invalid_response');
224
+ return metricMeta(item, 'search_economic_indicator');
225
+ });
226
+ return { data: rows, schema: edbSearchOutput };
227
+ }
228
+ /**
229
+ * EDB 取数:`{metrics:[{meta, date[], value[]}]}` 的**并行数组**转置成一行一期(§3.3)。
230
+ * 日期与数值长度不一致时不猜——多出来的日期没有值、多出来的值没有期间,两种都是把数值挂错
231
+ * 期间,与 §1.4 里"excelTotalCount 不可信"同一族。
232
+ */
233
+ async function executeEdbQuery(client, params, signal) {
234
+ const payload = requirePayload(await client.callTool('query_economic_indicator_data', {
235
+ question: params.indicator,
236
+ beginDate: params.start_date,
237
+ endDate: params.end_date,
238
+ }, signal), 'query_economic_indicator_data');
239
+ const rows = [];
240
+ for (const item of requireMetrics(payload, 'query_economic_indicator_data')) {
241
+ if (!isRecord(item))
242
+ throw windError('Wind query_economic_indicator_data returned a non-object metric', 'wind_invalid_response');
243
+ const meta = isRecord(item.meta) ? item.meta : item;
244
+ const context = `query_economic_indicator_data(${windText(meta.code) ?? String(params.indicator)})`;
245
+ const base = metricMeta(meta, context);
246
+ if (item.date === undefined && item.value === undefined)
247
+ continue;
248
+ const { date, value } = item;
249
+ if (!Array.isArray(date) || !Array.isArray(value) || date.length !== value.length) {
250
+ throw windError(`Wind ${context} returned ${Array.isArray(date) ? date.length : 'non-array'} dates beside ${Array.isArray(value) ? value.length : 'non-array'} values`, 'wind_invalid_response');
251
+ }
252
+ for (let index = 0; index < date.length; index += 1) {
253
+ const reportDate = compactDate(date[index], context);
254
+ if (reportDate === null)
255
+ throw windError(`Wind ${context} has a missing period at row ${index}`, 'wind_invalid_response');
256
+ rows.push({ ...base, report_date: reportDate, report_date_ms: reportDateMs(reportDate), value: windNumber(value[index], context, 'value') });
257
+ }
258
+ }
259
+ return { data: rows, schema: edbQueryOutput };
260
+ }
261
+ /**
262
+ * K 线时间戳**必须自带偏移**(§6.4:A股/港股 +08:00、美股 -04:00,是交易所当地时间)。
263
+ * 没写偏移就没法确定这是哪一刻,`Date.parse` 会按宿主本地时区猜——所以先失败。
264
+ */
265
+ function klineInstant(value, context) {
266
+ const text = String(value ?? '').trim();
267
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:?\d{2})$/.test(text)) {
268
+ throw windError(`Wind ${context} has a timestamp without an explicit UTC offset: ${JSON.stringify(text.slice(0, 40))}`, 'wind_invalid_response');
269
+ }
270
+ const epoch = Date.parse(text);
271
+ if (Number.isNaN(epoch))
272
+ throw windError(`Wind ${context} has an unparseable timestamp ${JSON.stringify(text)}`, 'wind_invalid_response');
273
+ return { datetime: text, time_ms: epoch };
274
+ }
275
+ async function executeKline(client, params, signal) {
276
+ const period = String(params.period);
277
+ const adjust = String(params.adjust);
278
+ const code = String(params.code);
279
+ const args = {
280
+ windcode: code,
281
+ begin_date: params.start_date,
282
+ end_date: params.end_date,
283
+ period: KLINE_PERIOD_CODES[period],
284
+ aftype: adjust === 'qfq' ? '0' : '2',
285
+ };
286
+ // 前复权必须给 afdate,且不得早于区间——两者任一缺失都是挂满 60 秒而不是报错(§6.5)。
287
+ // 锁"当日"而不是暴露成参数:后复权以 afdate 为锚向未来放大,把锚默认成今天会得到
288
+ // 一个逐位等于不复权却叫 hfq 的序列,所以 hfq 根本不放(参数层已拒)。
289
+ if (adjust === 'qfq')
290
+ args.afdate = isoDateFromEpoch(Date.now(), 'Asia/Shanghai');
291
+ const payload = requirePayload(await client.callTool('get_stock_kline', args, signal), 'get_stock_kline');
292
+ const table = requireTable(payload, 'get_stock_kline');
293
+ // 空表是合法成功:非交易日实测快回 rows=[] 且 unit=null(§6.2)——"这一区间没有行情"
294
+ // 与"取不到"是两种结论,前者落 0 行 Dataset,后者由上面的错误分类负责。
295
+ if (table.rows.length === 0)
296
+ return { data: [], schema: klineOutput };
297
+ const at = KLINE_REQUIRED.reduce((acc, name) => ({ ...acc, [name]: columnIndex(table, name, 'get_stock_kline') }), {});
298
+ const turnoverAt = optionalColumnIndex(table, KLINE_TURNOVER);
299
+ const volumeAt = optionalColumnIndex(table, KLINE_VOLUME);
300
+ const rateAt = optionalColumnIndex(table, KLINE_RATE);
301
+ const avgAt = optionalColumnIndex(table, KLINE_AVG);
302
+ const context = `get_stock_kline(${code})`;
303
+ const cell = (row, index) => (index >= 0 && index < row.length ? row[index] : null);
304
+ const rows = table.rows.map((row) => {
305
+ const instant = klineInstant(cell(row, at[KLINE_TIME]), context);
306
+ const open = windNumber(cell(row, at[KLINE_OPEN]), context, KLINE_OPEN);
307
+ const close = windNumber(cell(row, at[KLINE_MATCH]), context, KLINE_MATCH);
308
+ const high = windNumber(cell(row, at[KLINE_HIGH]), context, KLINE_HIGH);
309
+ const low = windNumber(cell(row, at[KLINE_LOW]), context, KLINE_LOW);
310
+ for (const [name, value] of [[KLINE_OPEN, open], [KLINE_MATCH, close], [KLINE_HIGH, high], [KLINE_LOW, low]]) {
311
+ if (value === null)
312
+ throw windError(`Wind ${context} has a missing ${name} inside a non-empty row`, 'wind_invalid_response');
313
+ }
314
+ if (high < low)
315
+ throw windError(`Wind ${context} ${instant.datetime} has high < low`, 'wind_invalid_response');
316
+ return {
317
+ code,
318
+ period,
319
+ adjust,
320
+ datetime: instant.datetime,
321
+ time_ms: instant.time_ms,
322
+ open,
323
+ high,
324
+ low,
325
+ close,
326
+ volume: windNumber(cell(row, volumeAt), context, KLINE_VOLUME),
327
+ turnover: windNumber(cell(row, turnoverAt), context, KLINE_TURNOVER),
328
+ turnover_rate_pct: windNumber(cell(row, rateAt), context, KLINE_RATE),
329
+ avg_price: windNumber(cell(row, avgAt), context, KLINE_AVG),
330
+ price_unit: windUnit(table.unit, KLINE_MATCH),
331
+ amount_unit: windUnit(table.unit, KLINE_TURNOVER),
332
+ volume_unit: windUnit(table.unit, KLINE_VOLUME),
333
+ };
334
+ });
335
+ return { data: rows, schema: klineOutput };
336
+ }
337
+ const nullableNumber = { oneOf: [{ type: 'number' }, { type: 'null' }] };
338
+ const nullableString = { oneOf: [{ type: 'string' }, { type: 'null' }] };
339
+ const metricMetaProperties = {
340
+ code: { type: 'string', description: 'Wind EDB 指标代码(如 M0000612),取数时原样填进 wind_edb_query 的 indicator' },
341
+ name: { ...nullableString, description: '指标全称原文(中文,含口径与期间)' },
342
+ unit: { ...nullableString, description: '上游标的单位原文(亿元 / % …);该族没有这个键时为 null,不猜单位' },
343
+ freq: { ...nullableString, description: '频率原文(日/周/月/季/半年/年,中文字面量)' },
344
+ source: { ...nullableString, description: '数据来源原文(国家统计局 / 美联储 …)' },
345
+ };
346
+ const metricSeriesProperties = {
347
+ update_date: { ...nullableString, description: '该指标**最后一次更新日期**(YYYY-MM-DD),不是本 Dataset 的时间序列' },
348
+ series_begin_date: { ...nullableString, description: '该指标序列的最早期间' },
349
+ series_end_date: { ...nullableString, description: '该指标序列的最新期间:要"最新的数"就到这里为止' },
350
+ magnitude: { ...nullableString, description: '量纲原文(亿 / 万 …),单位可能为 % 的指标族不给这一键' },
351
+ currency: { ...nullableString, description: '币种原文(人民币 / 美元 …)' },
352
+ };
353
+ const edbSearchOutput = {
354
+ type: 'array',
355
+ items: {
356
+ type: 'object',
357
+ properties: {
358
+ ...metricMetaProperties,
359
+ report_date: { type: 'null', description: '本能力只找指标不取数,恒为 null' },
360
+ report_date_ms: { type: 'null', description: '恒为 null(同上)' },
361
+ value: { type: 'null', description: '恒为 null:数值请走 wind_edb_query' },
362
+ ...metricSeriesProperties,
363
+ },
364
+ additionalProperties: true,
365
+ },
366
+ };
367
+ const edbQueryOutput = {
368
+ type: 'array',
369
+ items: {
370
+ type: 'object',
371
+ properties: {
372
+ ...metricMetaProperties,
373
+ report_date: { type: 'string', description: '报告期/观测日 YYYY-MM-DD(上游给的是 20260731 紧凑串);本 Dataset 的时间轴' },
374
+ report_date_ms: { type: 'integer', description: 'report_date 按 Asia/Shanghai 零点的毫秒戳(EDB 的日期本身没有时区)' },
375
+ value: { ...nullableNumber, description: '指标数值,单位见 unit;后端 INVALID 一律 null,不得当 0 参与计算' },
376
+ ...metricSeriesProperties,
377
+ },
378
+ additionalProperties: true,
379
+ },
380
+ };
381
+ const klineOutput = {
382
+ type: 'array',
383
+ items: {
384
+ type: 'object',
385
+ properties: {
386
+ code: { type: 'string', description: '本次请求的代码回显(上游 K 线表里没有代码列,不拿它当上游确认)' },
387
+ period: { type: 'string', description: '本次请求的档:day/week/month/m1/m5/m15/m30' },
388
+ adjust: { type: 'string', description: '复权口径:none 或 qfq(后复权不提供,理由见能力说明)' },
389
+ datetime: { type: 'string', description: '上游原样时间戳,带**该行自己的**交易所偏移(A股/港股 +08:00、美股夏令时 -04:00)' },
390
+ time_ms: { type: 'integer', description: '按该行自带偏移换算的 epoch 毫秒;日线/周/月那根是该期间末的当地 00:00。本 Dataset 的时间轴' },
391
+ open: { type: 'number' },
392
+ high: { type: 'number' },
393
+ low: { type: 'number' },
394
+ close: { type: 'number', description: '来自上游 MATCH 列(Wind 的 K 线没有 CLOSE 列,MATCH 即收盘价)' },
395
+ volume: { ...nullableNumber, description: '成交量,单位见 volume_unit。⚠️ 实测 A 股是**股**,而腾讯 A 股 K 线是**手**——跨源比先换单位' },
396
+ turnover: { ...nullableNumber, description: '成交额,单位见 amount_unit' },
397
+ turnover_rate_pct: { ...nullableNumber, description: '换手率**百分数原值**(0.2892 表示 0.2892%);上游 unit 表不含这一列,分母口径未逐值核验' },
398
+ avg_price: { ...nullableNumber, description: '均价(上游 AVPRICE);实测 avg_price × volume = turnover' },
399
+ price_unit: { ...nullableString, description: '上游 unit 表里 OHLC/均价的币种原文(实测 A 股为「元」)' },
400
+ amount_unit: { ...nullableString, description: '成交额单位原文' },
401
+ volume_unit: { ...nullableString, description: '成交量单位原文(实测 A 股为「股」)' },
402
+ },
403
+ additionalProperties: true,
404
+ },
405
+ };
406
+ function normalizeEdbSearch(params) {
407
+ const question = typeof params.question === 'string' ? params.question.trim().replace(/\s+/g, ' ') : '';
408
+ if (question.length === 0)
409
+ throw new Error('parameter question is required');
410
+ if (question.length > 500)
411
+ throw new Error('parameter question must be at most 500 characters');
412
+ return { question };
413
+ }
414
+ function normalizeEdbQuery(params) {
415
+ // `observation` 走这道显式拒绝,而不是靠 assertKnown:默默忽略时间参数是最坏的一种失败,
416
+ // 而实测上游对 observation "给 3 只回 2 期"(§6.7),期数不可信。
417
+ if (params.observation !== undefined)
418
+ throw new Error('parameter observation is not supported: 实测该口径给 3 期只回 2 期,期数不可信;请给 start_date 与 end_date');
419
+ const raw = typeof params.indicator === 'string' ? params.indicator.trim() : '';
420
+ if (raw.length === 0)
421
+ throw new Error('parameter indicator is required');
422
+ if (raw.length > 200)
423
+ throw new Error('parameter indicator must be at most 200 characters');
424
+ if (/[,,]/.test(raw))
425
+ throw new Error('parameter indicator accepts exactly one indicator: 2026-10-06 实测逗号分隔的多码挂满 60 秒无应答,要多个指标请分次调用');
426
+ const start_date = strictDate(params.start_date, 'start_date');
427
+ const end_date = strictDate(params.end_date, 'end_date');
428
+ if (start_date > end_date)
429
+ throw new Error('parameter start_date must not be later than end_date');
430
+ return { indicator: raw.replace(/\s+/g, ' '), start_date, end_date };
431
+ }
432
+ /**
433
+ * 代码照 Wind 的写法收下,**不补也不猜交易所后缀**(§3.3):实测美股 `AAPL.O` 与 `AAPL.N`
434
+ * 回完全相同的行(后缀不参与解析),所以猜一个后缀不会报错、只会静默认错标的。
435
+ */
436
+ function normalizeWindCode(value) {
437
+ const raw = typeof value === 'string' ? value.trim().toUpperCase() : '';
438
+ if (!/^[A-Z0-9][A-Z0-9.]*\.[A-Z]{1,3}$/.test(raw) || raw.includes('..')) {
439
+ throw new Error('parameter code must carry a Wind exchange suffix (600519.SH / 00700.HK / AAPL.O); 本能力不猜后缀——实测后缀不参与上游解析');
440
+ }
441
+ return raw;
442
+ }
443
+ function normalizeKline(params) {
444
+ // `count` 进 allowed 只为了给出**可用**的拒绝理由(默默忽略"我要 N 根"是最坏的一种失败,
445
+ // 同 tencent 港美股那两条对 start/end 的处理):负值/0 的语义只有上游蓝本的说法、没打过真报文(§6.8 第 2 条)。
446
+ if (params.count !== undefined)
447
+ throw new Error('parameter count is not supported: 本能力按 start_date..end_date 取全区间,count 的正负/零语义(往后 N 条 / 往前 N 条 / 全区间)未实测,不暴露');
448
+ const code = normalizeWindCode(params.code);
449
+ const period = params.period === undefined ? 'day' : String(params.period).trim().toLowerCase();
450
+ if (!Object.prototype.hasOwnProperty.call(KLINE_PERIOD_CODES, period)) {
451
+ throw new Error(`parameter period must be one of ${KLINE_PERIOD_LIST}(每档都有真报文;60 分钟实测两次都挂满超时,不提供)`);
452
+ }
453
+ const adjust = params.adjust === undefined ? (MINUTE_PERIODS.has(period) ? 'none' : 'qfq') : String(params.adjust).trim().toLowerCase();
454
+ if (adjust !== 'none' && adjust !== 'qfq') {
455
+ throw new Error(`parameter adjust must be none or qfq(hfq 不提供:后复权以 afdate 为锚向未来放大,锚点默认成今天会给出一个逐位等于不复权却叫 hfq 的序列)`);
456
+ }
457
+ if (MINUTE_PERIODS.has(period) && adjust !== 'none') {
458
+ throw new Error('parameter adjust must be none for minute periods(分钟档的 qfq/afdate 组合没有打过真报文,不暴露未验证的组合)');
459
+ }
460
+ const start_date = strictDate(params.start_date, 'start_date');
461
+ const end_date = strictDate(params.end_date, 'end_date');
462
+ if (start_date > end_date)
463
+ throw new Error('parameter start_date must not be later than end_date');
464
+ const span = dayDistance(start_date, end_date);
465
+ const maxSpan = MINUTE_PERIODS.has(period) ? MINUTE_MAX_SPAN_DAYS : DAILY_MAX_SPAN_DAYS;
466
+ if (span > maxSpan)
467
+ throw new Error(`parameter start_date..end_date spans ${span} days; ${MINUTE_PERIODS.has(period) ? 'minute periods accept' : 'day/week/month accept'} at most ${maxSpan} days`);
468
+ if (adjust === 'qfq' && end_date > isoDateFromEpoch(Date.now(), 'Asia/Shanghai')) {
469
+ throw new Error('parameter end_date must not be later than today when adjust=qfq(前复权的 afdate 由本能力锁成当日,而实测 afdate 早于区间会挂满超时)');
470
+ }
471
+ return { code, period, adjust, start_date, end_date };
472
+ }
473
+ /**
474
+ * 单能力的 DataSource:客户端在**定义期**构造是安全的(它不持有 Key——每次 callTool 现解析,
475
+ * 也不持有会话),所以这里没有任何"装配期读到凭据才注册"的门槛(§9.7⑤,桌面端那 61 颗
476
+ * 静默缺席的事故就在这一条上)。
477
+ */
478
+ function createWindSource(options) {
479
+ const schema = {
480
+ capability: options.capability,
481
+ time_contract: getDataTimeContract(options.capability),
482
+ name: options.tool,
483
+ source: `mcp:wind.${options.server}.${options.tool}`,
484
+ data_key: buildDataKey('wind', 'mcp', `${options.server}.${options.capability.replace(/^wind_/, '')}`),
485
+ source_label: 'wind',
486
+ paginated: false,
487
+ cacheMaxAgeMs: options.cacheMaxAgeMs,
488
+ rowShape: { rootArray: true },
489
+ summary: options.summary,
490
+ description: options.description,
491
+ input_schema: options.inputSchema,
492
+ output_schema: options.outputSchema,
493
+ };
494
+ const client = createWindClient({
495
+ endpoint: WIND_SERVER_ENDPOINTS[options.server],
496
+ timeoutMs: WIND_SOURCE_TIMEOUT_MS,
497
+ resolveApiKey: options.resolveApiKey,
498
+ });
499
+ // 归一化会被跑两遍(Hub 入队前一次、execute 一次,§10.7),所以只换值不改键名、且必须幂等。
500
+ const normalize = (params) => {
501
+ if (!isRecord(params))
502
+ throw new Error('params must be an object');
503
+ assertKnown(params, options.allowed);
504
+ return options.normalize(params);
505
+ };
506
+ return {
507
+ schema,
508
+ normalizeParams: normalize,
509
+ // 空结果是合法成功(非交易日分钟线实测快回 rows=[]),所以这道护栏只认形状、不认条数(§3.3)。
510
+ validateOutput: (data) => Array.isArray(data),
511
+ execute: (request, signal) => options.execute(client, normalize(request.params), signal),
512
+ };
513
+ }
514
+ const EDB_SEARCH_DESCRIPTION = 'Wind EDB(经济数据库)指标**检索**:只找指标不取数,一行一个候选指标(code/name/unit/freq/source + 该序列的起止与更新日期)。这是 fuyao 与东财都没有的面:宏观、行业、汇率、全球序列。⛔ question 走后端 NER,字面请求不等于字面结果——实测「人民币兑美元汇率 指标」回的是**美元兑人民币**(source 为 美联储 / 台湾统计局),先核对 name 与 source 再取数。键集随指标族变化:实测汇率类没有 unit/magnitude/currency 三个键,缺的一律 null,不猜单位。取数走 wind_edb_query:确定性靠"先 search 拿 code、再按 code 取数"这两步拿到,不是靠 NL 稳定。没有匹配时返回 0 行,是合法空结果不是失败。';
515
+ const EDB_QUERY_DESCRIPTION = 'Wind EDB 时间序列**取数**:上游回 {meta, date[], value[]} 并行数组,本能力转置成**一行一个观测期**(meta 逐行拼接,多指标可分次取后按 code 拼对比)。⛔ indicator 一次只填**一个**指标代码或一条中文问句:2026-10-06 实测逗号分隔的多码挂满 60 秒无应答,上游"多个代码用英文逗号分隔"的说法不成立,要多个就分次调用。start_date/end_date 必填——上游在日期区间与 observation 之间二选一,而 observation 实测"给 3 期只回 2 期"、期数不可信,本能力直接拒绝它。report_date 由上游紧凑串 20260731 归一而来,report_date_ms 按 Asia/Shanghai 零点(EDB 的日期本身没有时区)。unit 是上游单位原文;后端 INVALID 一律 null,**不得当 0 参与计算**。已实测指标代码直接当 indicator 与同义中文问句回同一份序列(M0000612 = 中国 CPI 当月同比):先 wind_edb_search 拿码再回来,确定性就在这两步。';
516
+ const KLINE_DESCRIPTION = 'Wind K 线:单只标的按**日期区间**取 日/周/月 与 1/5/15/30 分钟。相对腾讯的增量(§6.6.1):按日期区间取 640 根之前的历史、每根带成交额/成交量/换手/均价的美股 K 线、以及分钟档;⚠️ **分钟档只有 A 股取到过行**,港股与美股的分钟到 2026-10-06 为止四次全挂、仍未证实,要港美股分时先小范围试一次。港美股日线与快照请走 tencent_*。⛔ code 必须自带交易所后缀(600519.SH / 00700.HK / AAPL.O),本能力不猜后缀——实测 AAPL.O 与 AAPL.N 回完全相同的行(后缀不参与解析),所以 BRK.B 一类双市场代码有认错标的的风险;每行的 code 只是请求写法回显,上游 K 线表里没有代码列。⛔ period 只放逐档打过真报文的七档,60 分钟实测两次都挂满超时、不提供。adjust 只有 none 与 qfq:后复权以 afdate 为锚向未来放大,锚默认成今天会得到一个逐位等于不复权却叫 hfq 的序列,所以宁可不给;qfq 的 afdate 由本能力锁成当日,因此 end_date 不得晚于当日;分钟档只允许 none。close 来自上游 MATCH 列(Wind 没有 CLOSE 列);上游多给的列不收录,行里以 output_fields 为准。数值列上游全是字符串(columns[].type 却一律写 string),解析不了即失败。time_ms 按**该行自带偏移**换算(A股/港股 +08:00、美股 -04:00 是交易所当地时间),整段按北京时间处理会让美股错位 12 小时。⚠️ volume 实测 A 股是**股**(腾讯 A 股 K 线是手,跨源先换);turnover_rate_pct 是百分数原值(0.2892 = 0.2892%),上游不给它的单位与分母口径。非交易日快回 0 行、是合法成功(实测 A 股国庆休市日 653ms 回空表);而"该日该市场分钟尚未落地"与上游故障都表现为挂到 12 秒超时(wind_upstream_timeout)——不要盲目重试同一区间,改更早日期或改走日线。积分计费:AUTH/RATE_LIMIT/后端错误一律不自动重试。';
517
+ /**
518
+ * §6.7 改口后的 P1 三条。`wind_edb_*` 是 P0(EDB 是唯一来源),`wind_stock_kline` 是 P1。
519
+ * 不接的一概有理由:price_indicators 与全部 NL 工具(中文键 / 列名是 question 的函数)、
520
+ * bond 与 fund/index(重叠或未测)、analytics_data(实测连 initialize 都挂)。
521
+ */
522
+ export function createWindSources(resolveApiKey) {
523
+ return [
524
+ createWindSource({
525
+ capability: 'wind_edb_search',
526
+ tool: 'search_economic_indicator',
527
+ server: 'economic_data',
528
+ // 指标目录与序列发布都是日/季粒度;缓存拉长是为了少烧积分,而不是省 RTT。
529
+ cacheMaxAgeMs: 6 * 3_600_000,
530
+ summary: 'Wind EDB 宏观/行业/汇率指标检索(只找码不取数)',
531
+ description: EDB_SEARCH_DESCRIPTION,
532
+ inputSchema: {
533
+ type: 'object',
534
+ properties: { question: { type: 'string', maxLength: 500, description: '中文自然语言问句,只描述要找的指标(实体 + 指标),不要写时间范围与换算' } },
535
+ required: ['question'],
536
+ additionalProperties: false,
537
+ },
538
+ outputSchema: edbSearchOutput,
539
+ allowed: ['question'],
540
+ normalize: normalizeEdbSearch,
541
+ execute: executeEdbSearch,
542
+ resolveApiKey,
543
+ }),
544
+ createWindSource({
545
+ capability: 'wind_edb_query',
546
+ tool: 'query_economic_indicator_data',
547
+ server: 'economic_data',
548
+ cacheMaxAgeMs: 6 * 3_600_000,
549
+ summary: 'Wind EDB 指标时间序列(宏观/行业/汇率,按日期区间)',
550
+ description: EDB_QUERY_DESCRIPTION,
551
+ inputSchema: {
552
+ type: 'object',
553
+ properties: {
554
+ indicator: { type: 'string', maxLength: 200, description: '一个 EDB 指标代码(如 M0000612,先走 wind_edb_search 拿码)或一条中文指标问句;不接受逗号分隔的多个' },
555
+ start_date: { type: 'string', description: '起始日期 YYYY-MM-DD(含)' },
556
+ end_date: { type: 'string', description: '结束日期 YYYY-MM-DD(含)' },
557
+ },
558
+ required: ['indicator', 'start_date', 'end_date'],
559
+ additionalProperties: false,
560
+ },
561
+ outputSchema: edbQueryOutput,
562
+ allowed: ['indicator', 'start_date', 'end_date', 'observation'],
563
+ normalize: normalizeEdbQuery,
564
+ execute: executeEdbQuery,
565
+ resolveApiKey,
566
+ }),
567
+ createWindSource({
568
+ capability: 'wind_stock_kline',
569
+ tool: 'get_stock_kline',
570
+ server: 'stock_data',
571
+ cacheMaxAgeMs: 60_000,
572
+ summary: 'Wind 日周月与 1–30 分钟 K 线(按日期区间,A股+港股+美股)',
573
+ description: KLINE_DESCRIPTION,
574
+ inputSchema: {
575
+ type: 'object',
576
+ properties: {
577
+ code: { type: 'string', description: 'Wind 代码,必须自带交易所后缀:600519.SH、00700.HK、AAPL.O' },
578
+ start_date: { type: 'string', description: '起始日期 YYYY-MM-DD(含)' },
579
+ end_date: { type: 'string', description: '结束日期 YYYY-MM-DD(含);adjust=qfq 时不得晚于当日' },
580
+ period: { type: 'string', enum: ['day', 'week', 'month', 'm1', 'm5', 'm15', 'm30'], description: '缺省 day;只有逐档打过真报文的这七档(60 分钟不提供)' },
581
+ adjust: { type: 'string', enum: ['none', 'qfq'], description: '缺省:日/周/月为 qfq,分钟档为 none(分钟档只接受 none)' },
582
+ },
583
+ required: ['code', 'start_date', 'end_date'],
584
+ additionalProperties: false,
585
+ },
586
+ outputSchema: klineOutput,
587
+ allowed: ['code', 'start_date', 'end_date', 'period', 'adjust', 'count'],
588
+ normalize: normalizeKline,
589
+ execute: executeKline,
590
+ resolveApiKey,
591
+ }),
592
+ ];
593
+ }