@v587d/capital-generation 0.0.0-stage → 2.6.0

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 +794 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -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 +76 -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,424 @@
1
+ /**
2
+ * 把原始 rows 变成**有界、对齐、可渲染**的图表载荷。
3
+ *
4
+ * 边界(这是整个方案的核心不变量):本模块的**输入**是原始 rows,**输出**是可视化载荷。
5
+ * 输出会写进 workspace 产物并交给浏览器,永不进入模型上下文;工具只回一条小回执。
6
+ * 因此这里可以放心读全量行,但必须保证输出有界(点数上限 + 字段数上限 + 标记数上限)。
7
+ */
8
+ import { largestTriangleThreeBuckets } from './downsample.js';
9
+ import { ChartError } from './errors.js';
10
+ import { MAX_CHART_MARKERS, MAX_CHART_SERIES, OHLC_KINDS } from './spec.js';
11
+ /** 单张图保留的最大点数(下采样后)。轻量图表在这个量级仍然流畅,且载荷只有几十 KB。 */
12
+ export const MAX_CHART_POINTS = 2_000;
13
+ /** 字段候选清单最多回报多少个(错误路径本身也不能变成上下文炸弹)。 */
14
+ export const MAX_FIELD_HINTS = 15;
15
+ /** 判定"x 是时间列"的采样解析成功率阈值。 */
16
+ const TIME_AXIS_THRESHOLD = 0.8;
17
+ /** 分类轴的合成时间基准(UTC 秒):1970 太容易被误认为真实时间,取 2000-01-01。 */
18
+ const INDEX_EPOCH_SECONDS = Date.UTC(2000, 0, 1) / 1000;
19
+ const SECONDS_PER_DAY = 86_400;
20
+ /** epoch 毫秒与秒的分界:>= 1e11 视为毫秒(1e11 秒 ≈ 公元 5138 年)。 */
21
+ const MILLISECONDS_THRESHOLD = 1e11;
22
+ function isRecord(value) {
23
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+ /**
26
+ * 从解析好的 JSON 里取出行数组。
27
+ *
28
+ * 真实数据源的同花顺返回常常是 envelope(行数组在 `item` / `data` / `stock_items` …),
29
+ * 所以这里不能只认顶层数组;但也不能乱猜——猜错会让用户看到一张空图或错图。
30
+ * 策略:顶层数组直接用;否则在候选键里找;候选都没有时列出**全部**数组型顶层键让模型
31
+ * 自己指定路径,而不是静默取第一个。
32
+ */
33
+ export function extractRows(value) {
34
+ if (Array.isArray(value))
35
+ return { rows: value, shape: 'array' };
36
+ if (!isRecord(value)) {
37
+ throw new ChartError('chart_no_rows', 'the JSON source must be an array of rows or an object containing one');
38
+ }
39
+ const preferred = ['item', 'items', 'data', 'rows', 'list', 'records', 'result'];
40
+ for (const key of preferred) {
41
+ const candidate = value[key];
42
+ if (Array.isArray(candidate) && candidate.length > 0)
43
+ return { rows: candidate, shape: key };
44
+ }
45
+ const arrayKeys = Object.keys(value).filter((key) => Array.isArray(value[key]) && value[key].length > 0);
46
+ if (arrayKeys.length === 1)
47
+ return { rows: value[arrayKeys[0]], shape: arrayKeys[0] };
48
+ if (arrayKeys.length > 1) {
49
+ throw new ChartError('chart_source_invalid', 'the JSON object has several candidate row arrays; wrap the wanted one in an array or extract it first', { array_keys: arrayKeys.slice(0, MAX_FIELD_HINTS) });
50
+ }
51
+ throw new ChartError('chart_no_rows', 'the JSON object contains no row array', { object_keys: Object.keys(value).slice(0, MAX_FIELD_HINTS) });
52
+ }
53
+ /** 字段候选清单:按出现顺序去重,最多 MAX_FIELD_HINTS 个。 */
54
+ function collectFields(records) {
55
+ const seen = [];
56
+ const set = new Set();
57
+ for (const record of records) {
58
+ for (const key of Object.keys(record)) {
59
+ if (set.has(key))
60
+ continue;
61
+ set.add(key);
62
+ seen.push(key);
63
+ if (seen.length >= 200)
64
+ return seen;
65
+ }
66
+ }
67
+ return seen;
68
+ }
69
+ function assertFields(fields, available) {
70
+ const missing = [...new Set(fields.filter((field) => field !== undefined))].filter((field) => !available.includes(field));
71
+ if (missing.length === 0)
72
+ return;
73
+ // 候选清单写进 message:模型看到的错误文本就是自愈所需的全部信息,
74
+ // 不依赖调用方是否把 details 透传出去。
75
+ const hints = available.slice(0, MAX_FIELD_HINTS);
76
+ const suffix = hints.length > 0 ? `; available fields: ${hints.join(', ')}${available.length > hints.length ? ` (共 ${available.length} 个,其余已省略)` : ''}` : '';
77
+ throw new ChartError('chart_field_not_found', `field not found in the data: ${missing.join(', ')}${suffix}`, {
78
+ missing,
79
+ available: hints,
80
+ available_total: available.length,
81
+ });
82
+ }
83
+ /** 数值读取:数字直接用,纯数字字符串兼容(与 Dataset schema 的 allow_numeric_string 同源)。 */
84
+ function toNumber(value) {
85
+ if (typeof value === 'number')
86
+ return Number.isFinite(value) ? value : undefined;
87
+ if (typeof value === 'string') {
88
+ const text = value.trim();
89
+ if (text.length === 0 || text.length > 32)
90
+ return undefined;
91
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(text))
92
+ return undefined;
93
+ const parsed = Number(text);
94
+ return Number.isFinite(parsed) ? parsed : undefined;
95
+ }
96
+ return undefined;
97
+ }
98
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
99
+ const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
100
+ /**
101
+ * 时间值解析。规则必须**确定**(同一份数据在任何机器上得到同一张图):
102
+ * - 纯日期 `YYYY-MM-DD` → 业务日字符串,交给图表库按业务日显示(不做时区换算,避免差一天);
103
+ * - `YYYY-MM-DD HH:mm[:ss]` 无时区 → 按 UTC 解释(显式规则,不依赖宿主本地时区);
104
+ * - 数字/纯数字字符串 → epoch;`>= 1e11` 视为毫秒,否则视为秒;
105
+ * - 其余字符串 → `Date.parse`;失败即视为该行没有时间。
106
+ */
107
+ export function parseTimeValue(value) {
108
+ if (typeof value === 'number') {
109
+ if (!Number.isFinite(value))
110
+ return undefined;
111
+ const seconds = Math.abs(value) >= MILLISECONDS_THRESHOLD ? Math.round(value / 1000) : Math.round(value);
112
+ return { display: seconds, seconds, dateOnly: false };
113
+ }
114
+ if (typeof value !== 'string')
115
+ return undefined;
116
+ const text = value.trim();
117
+ if (text.length === 0 || text.length > 40)
118
+ return undefined;
119
+ const dateOnly = DATE_ONLY.exec(text);
120
+ if (dateOnly) {
121
+ const year = Number(dateOnly[1]);
122
+ const month = Number(dateOnly[2]);
123
+ const day = Number(dateOnly[3]);
124
+ const check = new Date(Date.UTC(year, month - 1, day));
125
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() + 1 !== month || check.getUTCDate() !== day)
126
+ return undefined;
127
+ return { display: text, seconds: Math.round(check.getTime() / 1000), dateOnly: true };
128
+ }
129
+ const dateTime = DATE_TIME.exec(text);
130
+ if (dateTime) {
131
+ const normalized = `${dateTime[1]}-${dateTime[2]}-${dateTime[3]}T${dateTime[4]}:${dateTime[5]}:${dateTime[6] ?? '00'}${dateTime[7] ?? 'Z'}`;
132
+ const parsed = Date.parse(normalized);
133
+ if (!Number.isFinite(parsed))
134
+ return undefined;
135
+ return { display: Math.round(parsed / 1000), seconds: Math.round(parsed / 1000), dateOnly: false };
136
+ }
137
+ if (/^\d+$/.test(text))
138
+ return parseTimeValue(Number(text));
139
+ const parsed = Date.parse(text);
140
+ if (!Number.isFinite(parsed))
141
+ return undefined;
142
+ return { display: Math.round(parsed / 1000), seconds: Math.round(parsed / 1000), dateOnly: false };
143
+ }
144
+ export function buildChartPayload(input) {
145
+ const { spec } = input;
146
+ const warnings = [];
147
+ const records = input.rows.filter(isRecord);
148
+ if (records.length === 0) {
149
+ throw new ChartError('chart_no_rows', 'the source contains no object rows to chart');
150
+ }
151
+ const available = collectFields(records);
152
+ if (spec.axis === 'time' && spec.timeField !== undefined)
153
+ assertFields([spec.timeField], available);
154
+ for (const series of spec.series)
155
+ assertFields([series.field], available);
156
+ if (spec.ohlc)
157
+ assertFields([spec.ohlc.open, spec.ohlc.high, spec.ohlc.low, spec.ohlc.close], available);
158
+ if (spec.volume)
159
+ assertFields([spec.volume.field], available);
160
+ // ── 1. 时间轴判定 ─────────────────────────────────────────────────────────
161
+ // 显式给了 x 但值不像时间(分类数据)时,自动降级为分类轴并用 x 的取值做刻度标签,
162
+ // 而不是报错让模型改口——"用 x 画柱状图"是最自然的表达。
163
+ const axisField = spec.timeField;
164
+ let axis = axisField === undefined ? 'index' : 'time';
165
+ // OHLC 图形(candlestick / bar)不允许降级:图表库的 OhlcData 需要真实有序时间轴,
166
+ // 拿分类轴硬画只会得到一张错位的图。
167
+ if (axisField !== undefined && !OHLC_KINDS.includes(spec.kind)) {
168
+ // 后续预处理本来就会逐行解析时间;这里也统计全量,避免前 100 行的脏数据把整条
169
+ // 后面的真实时间序列误判成分类轴。
170
+ const parsed = records.filter((record) => parseTimeValue(record[axisField]) !== undefined).length;
171
+ if (parsed / records.length < TIME_AXIS_THRESHOLD) {
172
+ axis = 'index';
173
+ warnings.push(`x field "${axisField}" was treated as a category axis: only ${parsed}/${records.length} values look like time`);
174
+ }
175
+ }
176
+ // ── 2. 逐行预处理 ────────────────────────────────────────────────────────
177
+ const prepared = [];
178
+ let skippedRows = 0;
179
+ records.forEach((record, sourceIndex) => {
180
+ if (axis === 'time' && axisField !== undefined) {
181
+ const time = parseTimeValue(record[axisField]);
182
+ if (time === undefined) {
183
+ skippedRows += 1;
184
+ return;
185
+ }
186
+ prepared.push({ time, raw: record, indexLabel: String(sourceIndex + 1), sourceIndex });
187
+ return;
188
+ }
189
+ const label = axisField === undefined ? String(sourceIndex + 1) : String(record[axisField] ?? sourceIndex + 1);
190
+ prepared.push({
191
+ time: { display: INDEX_EPOCH_SECONDS + prepared.length * SECONDS_PER_DAY, seconds: INDEX_EPOCH_SECONDS + prepared.length * SECONDS_PER_DAY, dateOnly: false },
192
+ raw: record,
193
+ indexLabel: label,
194
+ sourceIndex,
195
+ });
196
+ });
197
+ if (skippedRows > 0)
198
+ warnings.push(`${skippedRows} row(s) skipped: the time field is missing or unparseable`);
199
+ if (prepared.length === 0) {
200
+ // OHLC 图形必须给可解析的时间列:它不是"降级"问题,而是这张图根本画不出来。
201
+ // 错误必须直接把模型引到能用的 kind 上,而不是让它反复试同一个 spec。
202
+ if (OHLC_KINDS.includes(spec.kind)) {
203
+ throw new ChartError('chart_no_rows', `${spec.kind} 需要可解析的时间列,但字段 "${axisField ?? '(未提供 x)'}" 的值都解析不出日期或时间戳;分类数据请改用 kind=column(单值柱)或 line`);
204
+ }
205
+ throw new ChartError('chart_no_rows', 'no row has a usable time value');
206
+ }
207
+ // ── 3. 排序 + 去重 + 区间过滤 ──────────────────────────────────────────────
208
+ const ordered = [...prepared].sort((left, right) => left.time.seconds - right.time.seconds);
209
+ if (ordered.some((row, index) => row !== prepared[index]))
210
+ warnings.push('rows were reordered ascending by time (the chart library requires an ordered axis)');
211
+ const unique = [];
212
+ for (const row of ordered) {
213
+ if (unique.length > 0 && unique[unique.length - 1].time.seconds === row.time.seconds) {
214
+ unique[unique.length - 1] = row;
215
+ continue;
216
+ }
217
+ unique.push(row);
218
+ }
219
+ if (unique.length !== ordered.length)
220
+ warnings.push(`${ordered.length - unique.length} duplicate timestamp(s) collapsed`);
221
+ let filtered = unique;
222
+ if (spec.range) {
223
+ if (axis === 'index') {
224
+ throw new ChartError('chart_spec_invalid', 'spec.range requires a time axis; remove range for category data or provide a parseable time field in spec.x');
225
+ }
226
+ const from = spec.range.from === undefined ? undefined : parseTimeValue(spec.range.from)?.seconds;
227
+ const to = spec.range.to === undefined ? undefined : parseTimeValue(spec.range.to)?.seconds;
228
+ if (spec.range.from !== undefined && from === undefined)
229
+ throw new ChartError('chart_spec_invalid', 'spec.range.from is not a recognizable date');
230
+ if (spec.range.to !== undefined && to === undefined)
231
+ throw new ChartError('chart_spec_invalid', 'spec.range.to is not a recognizable date');
232
+ filtered = unique.filter((row) => (from === undefined || row.time.seconds >= from) && (to === undefined || row.time.seconds <= to));
233
+ if (filtered.length === 0)
234
+ throw new ChartError('chart_no_rows', 'spec.range excludes every row');
235
+ }
236
+ // ── 4. 下采样(一次求下标,所有子序列共用)────────────────────────────────
237
+ const originalPoints = filtered.length;
238
+ const primaryValues = primarySeriesValues(filtered, spec);
239
+ const keep = originalPoints > MAX_CHART_POINTS ? largestTriangleThreeBuckets(primaryValues, MAX_CHART_POINTS) : filtered.map((_row, index) => index);
240
+ const kept = keep.map((index) => filtered[index]).filter((row) => row !== undefined);
241
+ const downsampled = kept.length < originalPoints;
242
+ // ── 5. 时间显示口径:全是纯日期才用业务日字符串 ────────────────────────────
243
+ const useBusinessDay = axis === 'time' && kept.every((row) => row.time.dateOnly);
244
+ if (axis === 'time' && !useBusinessDay && kept.some((row) => row.time.dateOnly)) {
245
+ warnings.push('mixed date and datetime values; the axis is rendered as timestamps');
246
+ }
247
+ const displayTime = (row) => (useBusinessDay ? String(row.time.display) : row.time.seconds);
248
+ // ── 6. 子序列 ────────────────────────────────────────────────────────────
249
+ const series = [];
250
+ for (const item of spec.series.slice(0, MAX_CHART_SERIES)) {
251
+ const data = [];
252
+ let dropped = 0;
253
+ for (const row of kept) {
254
+ const value = toNumber(row.raw[item.field]);
255
+ if (value === undefined) {
256
+ dropped += 1;
257
+ continue;
258
+ }
259
+ data.push({ time: displayTime(row), value });
260
+ }
261
+ if (data.length === 0)
262
+ throw new ChartError('chart_field_not_found', `field "${item.field}" has no numeric value in the selected range`, { field: item.field });
263
+ if (dropped > 0)
264
+ warnings.push(`series "${item.label}": ${dropped} point(s) dropped (non-numeric or missing)`);
265
+ series.push({ id: item.field, label: item.label, type: item.type, data });
266
+ }
267
+ let ohlc;
268
+ if (spec.ohlc) {
269
+ const data = [];
270
+ let dropped = 0;
271
+ // OHLC 自洽性检查:脏数据确实存在,所以**不拦截**(拦截会让用户看不到任何图),
272
+ // 但必须如实披露,否则读者会把一根 high<low 的错柱子当成真行情。
273
+ let inconsistent = 0;
274
+ const inconsistentExamples = [];
275
+ for (const row of kept) {
276
+ const open = toNumber(row.raw[spec.ohlc.open]);
277
+ const high = toNumber(row.raw[spec.ohlc.high]);
278
+ const low = toNumber(row.raw[spec.ohlc.low]);
279
+ const close = toNumber(row.raw[spec.ohlc.close]);
280
+ if (open === undefined || high === undefined || low === undefined || close === undefined) {
281
+ dropped += 1;
282
+ continue;
283
+ }
284
+ if (high < low || open > high || open < low || close > high || close < low) {
285
+ inconsistent += 1;
286
+ if (inconsistentExamples.length < 3)
287
+ inconsistentExamples.push(String(row.time.display));
288
+ }
289
+ data.push({ time: displayTime(row), open, high, low, close });
290
+ }
291
+ if (data.length === 0)
292
+ throw new ChartError('chart_field_not_found', 'the OHLC fields have no numeric values in the selected range');
293
+ if (dropped > 0)
294
+ warnings.push(`OHLC: ${dropped} 行因字段缺失被丢弃`);
295
+ if (inconsistent > 0) {
296
+ warnings.push(`${inconsistent} 根 K 线的 OHLC 不自洽(high < low 或开/收越界),可能是数据问题;图上按原始值呈现,例如 ${inconsistentExamples.join('、')}`);
297
+ }
298
+ ohlc = { label: spec.title ?? 'OHLC', data };
299
+ }
300
+ let volume;
301
+ const volumeSpec = spec.volume;
302
+ if (volumeSpec) {
303
+ const data = [];
304
+ let dropped = 0;
305
+ for (const row of kept) {
306
+ const value = toNumber(row.raw[volumeSpec.field]);
307
+ if (value === undefined) {
308
+ dropped += 1;
309
+ continue;
310
+ }
311
+ // A 股习惯:涨红跌绿。方向由同一行的 OHLC 推得,颜色映射留给运行时(跟随主题)。
312
+ let direction;
313
+ if (spec.ohlc) {
314
+ const open = toNumber(row.raw[spec.ohlc.open]);
315
+ const close = toNumber(row.raw[spec.ohlc.close]);
316
+ if (open !== undefined && close !== undefined)
317
+ direction = close >= open ? 'up' : 'down';
318
+ }
319
+ data.push({ time: displayTime(row), value, ...(direction === undefined ? {} : { direction }) });
320
+ }
321
+ if (data.length === 0)
322
+ throw new ChartError('chart_field_not_found', `field "${volumeSpec.field}" has no numeric value in the selected range`, { field: volumeSpec.field });
323
+ if (dropped > 0)
324
+ warnings.push(`volume: ${dropped} point(s) dropped (non-numeric or missing)`);
325
+ volume = { label: '成交量', data };
326
+ }
327
+ // ── 7. 标记点:吸附到保留下来的时间点 ──────────────────────────────────────
328
+ const markers = [];
329
+ const keptSeconds = kept.map((row) => row.time.seconds);
330
+ for (const marker of spec.markers.slice(0, MAX_CHART_MARKERS)) {
331
+ // 两种定位都先化成"目标秒数",再吸附到**保留**的时间点上:下采样把某个点丢掉时,
332
+ // 标记不该跟着消失(那正是用户最在意的那一天)。
333
+ let targetSeconds;
334
+ if (marker.index !== undefined) {
335
+ const atIndex = filtered[marker.index];
336
+ if (atIndex === undefined) {
337
+ warnings.push(`marker "${marker.text}" dropped: index ${marker.index} is outside the charted range (0-${filtered.length - 1})`);
338
+ continue;
339
+ }
340
+ targetSeconds = atIndex.time.seconds;
341
+ }
342
+ else if (marker.time !== undefined) {
343
+ if (axis === 'index') {
344
+ const labelMatch = filtered.find((row) => row.indexLabel === String(marker.time));
345
+ targetSeconds = labelMatch?.time.seconds;
346
+ }
347
+ if (targetSeconds === undefined)
348
+ targetSeconds = parseTimeValue(marker.time)?.seconds;
349
+ }
350
+ const target = targetSeconds === undefined ? undefined : nearestRow(kept, keptSeconds, targetSeconds);
351
+ if (target === undefined) {
352
+ warnings.push(`marker "${marker.text}" dropped: its time/index is outside the charted range`);
353
+ continue;
354
+ }
355
+ markers.push({ time: displayTime(target), text: marker.text, position: marker.position });
356
+ }
357
+ const labels = {};
358
+ if (axis === 'index')
359
+ for (const row of kept)
360
+ labels[String(row.time.seconds)] = row.indexLabel;
361
+ return {
362
+ chart_id: input.chart_id,
363
+ title: spec.title ?? '数据图表',
364
+ kind: spec.kind,
365
+ axis,
366
+ series,
367
+ ...(ohlc === undefined ? {} : { ohlc }),
368
+ ...(volume === undefined ? {} : { volume }),
369
+ markers,
370
+ ...(axis === 'index' ? { labels } : {}),
371
+ meta: {
372
+ points: kept.length,
373
+ original_points: originalPoints,
374
+ downsampled,
375
+ ...(spec.timeField === undefined ? {} : { time_field: spec.timeField }),
376
+ axis,
377
+ source_kind: input.meta.source_kind,
378
+ ...(input.meta.dataset_id === undefined ? {} : { dataset_id: input.meta.dataset_id }),
379
+ ...(input.meta.source_label === undefined ? {} : { source_label: input.meta.source_label }),
380
+ ...(input.meta.captured_at === undefined ? {} : { captured_at: input.meta.captured_at }),
381
+ skipped_rows: skippedRows,
382
+ warnings,
383
+ },
384
+ };
385
+ }
386
+ /** 主序列取值(用于 LTTB);K 线用收盘价,其余用第一个数值字段。 */
387
+ function primarySeriesValues(rows, spec) {
388
+ const field = spec.ohlc ? spec.ohlc.close : spec.series[0]?.field ?? spec.volume?.field;
389
+ const values = [];
390
+ let last = 0;
391
+ for (const row of rows) {
392
+ const value = field === undefined ? undefined : toNumber(row.raw[field]);
393
+ last = value ?? last;
394
+ values.push(last);
395
+ }
396
+ return values;
397
+ }
398
+ /** 把标记点吸附到最近的**保留**时间点,避免下采样后标记消失。 */
399
+ function nearestRow(rows, keptSeconds, target) {
400
+ if (rows.length === 0)
401
+ return undefined;
402
+ let low = 0;
403
+ let high = keptSeconds.length - 1;
404
+ let best = -1;
405
+ while (low <= high) {
406
+ const middle = (low + high) >> 1;
407
+ if (keptSeconds[middle] <= target) {
408
+ best = middle;
409
+ low = middle + 1;
410
+ }
411
+ else {
412
+ high = middle - 1;
413
+ }
414
+ }
415
+ const candidates = [best, best + 1].filter((index) => index >= 0 && index < keptSeconds.length);
416
+ if (candidates.length === 0)
417
+ return undefined;
418
+ let chosen = candidates[0];
419
+ for (const index of candidates) {
420
+ if (Math.abs(keptSeconds[index] - target) < Math.abs(keptSeconds[chosen] - target))
421
+ chosen = index;
422
+ }
423
+ return rows[chosen];
424
+ }
@@ -0,0 +1,64 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type SessionLike, WorkspaceDatasetStore } from '../data-collector/store.js';
3
+ /**
4
+ * The registry is host-only and never serializes this handle into Agent output.
5
+ * Keeping the live Session object is required because DSH sandbox policies use
6
+ * session.snapshotEvents() when resolving workspace permissions.
7
+ */
8
+ export interface ChartSourceTokenRef {
9
+ chart_source_ref: string;
10
+ dataset_id: string;
11
+ task_id: string;
12
+ expires_at: number;
13
+ }
14
+ interface ChartSourceTokenRecord extends ChartSourceTokenRef {
15
+ /** The direct data-agent session that can already read the Dataset scope. */
16
+ sourceSession: SessionLike;
17
+ issued_at: number;
18
+ }
19
+ export interface ChartSourceTokenStoreOptions {
20
+ now?: () => number;
21
+ ttlMs?: number;
22
+ newToken?: () => string;
23
+ }
24
+ /**
25
+ * Short-lived capability tokens for nested visualization children.
26
+ *
27
+ * The token captures the direct data-agent session rather than asking a nested
28
+ * child to re-prove the Dataset's one-hop session scope. Possession of the
29
+ * opaque token is the capability; it is never converted into a filesystem path.
30
+ */
31
+ export declare class ChartSourceTokenStore {
32
+ private readonly tokens;
33
+ private readonly now;
34
+ private readonly ttlMs;
35
+ private readonly newToken;
36
+ constructor(options?: ChartSourceTokenStoreOptions);
37
+ private purgeExpired;
38
+ issue(input: {
39
+ store: WorkspaceDatasetStore;
40
+ session: SessionLike;
41
+ dataset_id: string;
42
+ task_id?: string;
43
+ signal?: AbortSignal;
44
+ }): Promise<ChartSourceTokenRef>;
45
+ resolve(input: {
46
+ chart_source_ref: string;
47
+ task_id?: string;
48
+ session?: SessionLike;
49
+ now?: number;
50
+ }): ChartSourceTokenRecord;
51
+ revoke(chartSourceRef: string): boolean;
52
+ size(): number;
53
+ }
54
+ /** Register the delegated-child-only token issuer. */
55
+ export declare function registerChartSourceTool(ctx: Context, options: {
56
+ store: WorkspaceDatasetStore;
57
+ tokens: ChartSourceTokenStore;
58
+ }): void;
59
+ export declare function chartSourceTokenForRender(tokens: ChartSourceTokenStore | undefined, args: Record<string, unknown>, session?: SessionLike): {
60
+ dataset_id: string;
61
+ task_id: string;
62
+ sourceSession: SessionLike;
63
+ } | undefined;
64
+ export {};