@v587d/capital-generation 2.8.2 → 2.9.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.
- package/CHANGELOG.md +87 -1
- package/README.md +41 -20
- package/chart-ui/README.md +1 -1
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +3 -8
- package/lib/chart/artifact-ref.js +3 -8
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +40 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +6 -29
- package/lib/chart/tool.js +9 -23
- package/lib/chart-render-entry.d.ts +9 -0
- package/lib/chart-render-entry.js +17 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +40 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -31
- package/lib/sources/ftshare-rest.js +1194 -225
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +39 -64
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +67 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +526 -337
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
package/lib/chart/runtime.js
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 2. **对话流内嵌视图**(Step 3):DSH 客户端 bundle 里再包一次。
|
|
7
|
-
* 写成字符串可以保证两边**逐字节同源**,不会出现"文件里对、页面上错"。
|
|
8
|
-
* 代价是这里没有类型检查,所以 test/chart-runtime.test.mjs 会用 `new Function`
|
|
9
|
-
* 做一次语法与 API 形状校验,把笔误挡在测试里而不是浏览器里。
|
|
10
|
-
*
|
|
11
|
-
* 约束:这段源码里**不能**出现反引号或 `${`(它本身在 TS 模板字符串里)。
|
|
12
|
-
* 依赖:DOM,以及**由调用方传入**的图表库对象(自包含 HTML 传 UMD 全局,Step 3 客户端传
|
|
13
|
-
* ESM import)。刻意不读、也不回退到 `globalThis.LightweightCharts`:那条全局正是与其它
|
|
14
|
-
* 插件带的同库不同版本互相覆盖的来源(见内部设计文档 docs/design/chart-visualization.md §9 风险表)。
|
|
15
|
-
* 形态:IIFE **返回** `{ render, version }`,由各宿主自行捕获(HTML 用 `var __capitalChart =`,
|
|
16
|
-
* 客户端 bundle 存为模块局部),**不往任何全局挂载**。
|
|
17
|
-
* 主题:只读 CSS 变量 `--dsw-*`(见 docs/reference/dsh-surface-ledger.md L7),
|
|
18
|
-
* 不调 DSH 的 theme 服务;自包含 HTML 自己定义同一批变量,因此两边渲染完全一致。
|
|
2
|
+
* Browser render runtime, kept as source text: the same bytes are inlined into standalone HTML
|
|
3
|
+
* and re-packaged in the client bundle. Must contain no backtick or `${`, and must never fall
|
|
4
|
+
* back to `globalThis.LightweightCharts` — another plugin's copy could silently win. The IIFE
|
|
5
|
+
* returns `{ render, version }`; nothing is attached to globals.
|
|
19
6
|
*/
|
|
20
7
|
export const CHART_RUNTIME_SOURCE = `
|
|
21
8
|
(function () {
|
|
@@ -31,8 +18,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
31
18
|
'--dsw-alias-state-success-primary': '#2f9e44'
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
//
|
|
35
|
-
// 十六进制时才追加 alpha;其余格式转换成 rgba,转换不了就退回原色,绝不拼出非法 CSS。
|
|
21
|
+
// Hex colors receive alpha; other formats convert to rgba or stay unchanged, never emit invalid CSS.
|
|
36
22
|
function withAlpha(color, alpha) {
|
|
37
23
|
var text = String(color || '').trim()
|
|
38
24
|
var hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text)
|
|
@@ -52,7 +38,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
52
38
|
return text
|
|
53
39
|
}
|
|
54
40
|
|
|
55
|
-
//
|
|
41
|
+
// First series uses brand color; later series use a stable palette so one symbol keeps its color.
|
|
56
42
|
var PALETTE = ['#f59e0b', '#10b981', '#8b5cf6', '#0ea5e9', '#ec4899']
|
|
57
43
|
|
|
58
44
|
function readToken(name) {
|
|
@@ -60,7 +46,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
60
46
|
var value = getComputedStyle(document.documentElement).getPropertyValue(name)
|
|
61
47
|
if (value && value.trim().length > 0) return value.trim()
|
|
62
48
|
} catch (error) {
|
|
63
|
-
/*
|
|
49
|
+
/* Non-browser or unavailable styles fall back to defaults. */
|
|
64
50
|
}
|
|
65
51
|
return FALLBACK[name]
|
|
66
52
|
}
|
|
@@ -91,18 +77,14 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
91
77
|
var key = String(time)
|
|
92
78
|
return Object.prototype.hasOwnProperty.call(labels, key) ? labels[key] : key
|
|
93
79
|
}
|
|
94
|
-
//
|
|
95
|
-
// 分类轴两边都要换掉,否则标尺上还是 2000 年那种合成日期。
|
|
80
|
+
// Replace both tick and crosshair formatters for category axes or synthetic dates leak through.
|
|
96
81
|
chart.applyOptions({
|
|
97
82
|
localization: { timeFormatter: format },
|
|
98
83
|
timeScale: { tickMarkFormatter: function (time) { return format(time) } }
|
|
99
84
|
})
|
|
100
85
|
}
|
|
101
86
|
|
|
102
|
-
/**
|
|
103
|
-
* 挂标记点。v5 把 markers 从 series API 移到了 createSeriesMarkers(series, markers)
|
|
104
|
-
* 返回的插件对象上;这里同时兼容两种形状,上游再动一次也不会静默丢掉标记。
|
|
105
|
-
*/
|
|
87
|
+
/** Support both v5 plugin markers and the older series API so markers never disappear silently. */
|
|
106
88
|
function attachMarkers(anchor, markers, LWC) {
|
|
107
89
|
if (typeof LWC.createSeriesMarkers === 'function') {
|
|
108
90
|
LWC.createSeriesMarkers(anchor, markers)
|
|
@@ -156,7 +138,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
156
138
|
function renderCapitalChart(container, payload, library) {
|
|
157
139
|
if (!container) throw new Error('renderCapitalChart: container is required')
|
|
158
140
|
if (!payload || typeof payload !== 'object') throw new Error('renderCapitalChart: payload is required')
|
|
159
|
-
//
|
|
141
|
+
// Use the injected library; never fall back to a global copy owned by another plugin.
|
|
160
142
|
var LWC = library
|
|
161
143
|
if (!LWC || typeof LWC.createChart !== 'function') {
|
|
162
144
|
showError(container, '图表库未加载(调用方未提供 LightweightCharts)。')
|
|
@@ -171,7 +153,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
171
153
|
|
|
172
154
|
var chart = LWC.createChart(container, {
|
|
173
155
|
autoSize: true,
|
|
174
|
-
// attributionLogo
|
|
156
|
+
// attributionLogo stays enabled for Apache-2.0 attribution.
|
|
175
157
|
layout: { background: { color: 'transparent' }, textColor: colors.text, fontFamily: 'inherit', attributionLogo: true },
|
|
176
158
|
grid: { vertLines: { color: colors.grid }, horzLines: { color: colors.grid } },
|
|
177
159
|
rightPriceScale: { borderColor: colors.grid, entireTextOnly: true },
|
|
@@ -185,8 +167,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
185
167
|
var anchor = null
|
|
186
168
|
|
|
187
169
|
if (payload.ohlc && Array.isArray(payload.ohlc.data) && payload.ohlc.data.length > 0) {
|
|
188
|
-
// kind=bar
|
|
189
|
-
// 两者的数据契约相同(都继承 OhlcData),区别只在渲染样式。
|
|
170
|
+
// kind=bar uses BarSeries; other OHLC kinds use CandlestickSeries with the same data contract.
|
|
190
171
|
var isOhlcBar = payload.kind === 'bar'
|
|
191
172
|
var candles = chart.addSeries(isOhlcBar ? LWC.BarSeries : LWC.CandlestickSeries, {
|
|
192
173
|
upColor: colors.up,
|
|
@@ -214,8 +195,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
214
195
|
options.lineWidth = 2
|
|
215
196
|
series = chart.addSeries(LWC.AreaSeries, options)
|
|
216
197
|
} else if (item.type === 'column') {
|
|
217
|
-
//
|
|
218
|
-
// 注意不能用 BarSeries —— 它要 OhlcData,塞单值只会画出空图。
|
|
198
|
+
// Single-value histograms need HistogramSeries; BarSeries would render an empty chart.
|
|
219
199
|
options.color = color
|
|
220
200
|
options.base = 0
|
|
221
201
|
series = chart.addSeries(LWC.HistogramSeries, options)
|
|
@@ -254,7 +234,7 @@ export const CHART_RUNTIME_SOURCE = `
|
|
|
254
234
|
|
|
255
235
|
appendLegend(container, payload, colors)
|
|
256
236
|
|
|
257
|
-
//
|
|
237
|
+
// Main/volume panes use a 3:1 ratio; a single pane has no second pane object.
|
|
258
238
|
var panes = typeof chart.panes === 'function' ? chart.panes() : []
|
|
259
239
|
if (panes.length > 1) {
|
|
260
240
|
if (typeof panes[0].setStretchFactor === 'function') panes[0].setStretchFactor(3)
|
package/lib/chart/series.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChartKind, ChartSeriesType, MarkerPosition, NormalizedChartSpec } from './spec.js';
|
|
2
|
-
/**
|
|
2
|
+
/** Post-downsample point cap; keeps the payload tens of KB and rendering fluid. */
|
|
3
3
|
export declare const MAX_CHART_POINTS = 2000;
|
|
4
|
-
/**
|
|
4
|
+
/** Error-path hints are capped too; an error must not become a context bomb. */
|
|
5
5
|
export declare const MAX_FIELD_HINTS = 15;
|
|
6
6
|
export interface ChartSeriesPayload {
|
|
7
7
|
id: string;
|
|
@@ -43,7 +43,7 @@ export interface ChartPayload {
|
|
|
43
43
|
text: string;
|
|
44
44
|
position: MarkerPosition;
|
|
45
45
|
}>;
|
|
46
|
-
/**
|
|
46
|
+
/** Category axis: synthetic time → original label for ticks and crosshair. */
|
|
47
47
|
labels?: Record<string, string>;
|
|
48
48
|
meta: {
|
|
49
49
|
points: number;
|
|
@@ -60,31 +60,24 @@ export interface ChartPayload {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* 真实数据源的同花顺返回常常是 envelope(行数组在 `item` / `data` / `stock_items` …),
|
|
66
|
-
* 所以这里不能只认顶层数组;但也不能乱猜——猜错会让用户看到一张空图或错图。
|
|
67
|
-
* 策略:顶层数组直接用;否则在候选键里找;候选都没有时列出**全部**数组型顶层键让模型
|
|
68
|
-
* 自己指定路径,而不是静默取第一个。
|
|
63
|
+
* Sources often wrap rows in an envelope (item/data/stock_items); prefer known keys, and list
|
|
64
|
+
* candidates instead of silently guessing when several arrays match.
|
|
69
65
|
*/
|
|
70
66
|
export declare function extractRows(value: unknown): {
|
|
71
67
|
rows: unknown[];
|
|
72
68
|
shape: string;
|
|
73
69
|
};
|
|
74
70
|
interface ParsedTime {
|
|
75
|
-
/**
|
|
71
|
+
/** Value handed to the library: 'YYYY-MM-DD' business day, or UTC seconds. */
|
|
76
72
|
display: string | number;
|
|
77
|
-
/**
|
|
73
|
+
/** Comparison key (UTC seconds) for sort/filter/alignment. */
|
|
78
74
|
seconds: number;
|
|
79
|
-
/**
|
|
75
|
+
/** True when the value came from a date-only string; decides business-day rendering. */
|
|
80
76
|
dateOnly: boolean;
|
|
81
77
|
}
|
|
82
78
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* - `YYYY-MM-DD HH:mm[:ss]` 无时区 → 按 UTC 解释(显式规则,不依赖宿主本地时区);
|
|
86
|
-
* - 数字/纯数字字符串 → epoch;`>= 1e11` 视为毫秒,否则视为秒;
|
|
87
|
-
* - 其余字符串 → `Date.parse`;失败即视为该行没有时间。
|
|
79
|
+
* Deterministic across machines: date-only stays a business-day string (no TZ shift), naive
|
|
80
|
+
* datetime is UTC, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
|
|
88
81
|
*/
|
|
89
82
|
export declare function parseTimeValue(value: unknown): ParsedTime | undefined;
|
|
90
83
|
export interface BuildChartPayloadInput {
|
package/lib/chart/series.js
CHANGED
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* 输出会写进 workspace 产物并交给浏览器,永不进入模型上下文;工具只回一条小回执。
|
|
6
|
-
* 因此这里可以放心读全量行,但必须保证输出有界(点数上限 + 字段数上限 + 标记数上限)。
|
|
2
|
+
* Turn raw rows into a bounded, aligned, renderable payload.
|
|
3
|
+
* Output goes to a workspace artifact and the browser, never into model context; input may be
|
|
4
|
+
* read in full, but the payload must stay bounded (points, series, fields, markers).
|
|
7
5
|
*/
|
|
8
6
|
import { largestTriangleThreeBuckets } from './downsample.js';
|
|
9
7
|
import { ChartError } from './errors.js';
|
|
10
8
|
import { MAX_CHART_MARKERS, MAX_CHART_SERIES, OHLC_KINDS } from './spec.js';
|
|
11
|
-
/**
|
|
9
|
+
/** Post-downsample point cap; keeps the payload tens of KB and rendering fluid. */
|
|
12
10
|
export const MAX_CHART_POINTS = 2_000;
|
|
13
|
-
/**
|
|
11
|
+
/** Error-path hints are capped too; an error must not become a context bomb. */
|
|
14
12
|
export const MAX_FIELD_HINTS = 15;
|
|
15
|
-
/**
|
|
13
|
+
/** Share of x values that must parse as time to keep a time axis. */
|
|
16
14
|
const TIME_AXIS_THRESHOLD = 0.8;
|
|
17
|
-
/**
|
|
15
|
+
/** Synthetic category-axis base (2000-01-01); 1970 reads as real data. */
|
|
18
16
|
const INDEX_EPOCH_SECONDS = Date.UTC(2000, 0, 1) / 1000;
|
|
19
17
|
const SECONDS_PER_DAY = 86_400;
|
|
20
|
-
/**
|
|
18
|
+
/** Values >= 1e11 are epoch ms (1e11 seconds is year 5138). */
|
|
21
19
|
const MILLISECONDS_THRESHOLD = 1e11;
|
|
22
20
|
function isRecord(value) {
|
|
23
21
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
24
22
|
}
|
|
25
23
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* 真实数据源的同花顺返回常常是 envelope(行数组在 `item` / `data` / `stock_items` …),
|
|
29
|
-
* 所以这里不能只认顶层数组;但也不能乱猜——猜错会让用户看到一张空图或错图。
|
|
30
|
-
* 策略:顶层数组直接用;否则在候选键里找;候选都没有时列出**全部**数组型顶层键让模型
|
|
31
|
-
* 自己指定路径,而不是静默取第一个。
|
|
24
|
+
* Sources often wrap rows in an envelope (item/data/stock_items); prefer known keys, and list
|
|
25
|
+
* candidates instead of silently guessing when several arrays match.
|
|
32
26
|
*/
|
|
33
27
|
export function extractRows(value) {
|
|
34
28
|
if (Array.isArray(value))
|
|
@@ -50,7 +44,6 @@ export function extractRows(value) {
|
|
|
50
44
|
}
|
|
51
45
|
throw new ChartError('chart_no_rows', 'the JSON object contains no row array', { object_keys: Object.keys(value).slice(0, MAX_FIELD_HINTS) });
|
|
52
46
|
}
|
|
53
|
-
/** 字段候选清单:按出现顺序去重,最多 MAX_FIELD_HINTS 个。 */
|
|
54
47
|
function collectFields(records) {
|
|
55
48
|
const seen = [];
|
|
56
49
|
const set = new Set();
|
|
@@ -66,12 +59,11 @@ function collectFields(records) {
|
|
|
66
59
|
}
|
|
67
60
|
return seen;
|
|
68
61
|
}
|
|
62
|
+
/** Hints live in the message so the model can self-correct even if details are dropped. */
|
|
69
63
|
function assertFields(fields, available) {
|
|
70
64
|
const missing = [...new Set(fields.filter((field) => field !== undefined))].filter((field) => !available.includes(field));
|
|
71
65
|
if (missing.length === 0)
|
|
72
66
|
return;
|
|
73
|
-
// 候选清单写进 message:模型看到的错误文本就是自愈所需的全部信息,
|
|
74
|
-
// 不依赖调用方是否把 details 透传出去。
|
|
75
67
|
const hints = available.slice(0, MAX_FIELD_HINTS);
|
|
76
68
|
const suffix = hints.length > 0 ? `; available fields: ${hints.join(', ')}${available.length > hints.length ? ` (共 ${available.length} 个,其余已省略)` : ''}` : '';
|
|
77
69
|
throw new ChartError('chart_field_not_found', `field not found in the data: ${missing.join(', ')}${suffix}`, {
|
|
@@ -80,7 +72,7 @@ function assertFields(fields, available) {
|
|
|
80
72
|
available_total: available.length,
|
|
81
73
|
});
|
|
82
74
|
}
|
|
83
|
-
/**
|
|
75
|
+
/** Numbers pass through; pure numeric strings are accepted (same tolerance as Dataset allow_numeric_string). */
|
|
84
76
|
function toNumber(value) {
|
|
85
77
|
if (typeof value === 'number')
|
|
86
78
|
return Number.isFinite(value) ? value : undefined;
|
|
@@ -98,11 +90,8 @@ function toNumber(value) {
|
|
|
98
90
|
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
99
91
|
const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
100
92
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* - `YYYY-MM-DD HH:mm[:ss]` 无时区 → 按 UTC 解释(显式规则,不依赖宿主本地时区);
|
|
104
|
-
* - 数字/纯数字字符串 → epoch;`>= 1e11` 视为毫秒,否则视为秒;
|
|
105
|
-
* - 其余字符串 → `Date.parse`;失败即视为该行没有时间。
|
|
93
|
+
* Deterministic across machines: date-only stays a business-day string (no TZ shift), naive
|
|
94
|
+
* datetime is UTC, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
|
|
106
95
|
*/
|
|
107
96
|
export function parseTimeValue(value) {
|
|
108
97
|
if (typeof value === 'number') {
|
|
@@ -157,23 +146,17 @@ export function buildChartPayload(input) {
|
|
|
157
146
|
assertFields([spec.ohlc.open, spec.ohlc.high, spec.ohlc.low, spec.ohlc.close], available);
|
|
158
147
|
if (spec.volume)
|
|
159
148
|
assertFields([spec.volume.field], available);
|
|
160
|
-
//
|
|
161
|
-
// 显式给了 x 但值不像时间(分类数据)时,自动降级为分类轴并用 x 的取值做刻度标签,
|
|
162
|
-
// 而不是报错让模型改口——"用 x 画柱状图"是最自然的表达。
|
|
149
|
+
// Explicit x that rarely parses as time degrades to a category axis instead of erroring.
|
|
163
150
|
const axisField = spec.timeField;
|
|
164
151
|
let axis = axisField === undefined ? 'index' : 'time';
|
|
165
|
-
// OHLC
|
|
166
|
-
// 拿分类轴硬画只会得到一张错位的图。
|
|
152
|
+
// OHLC kinds cannot degrade: the library needs a real ordered time axis, else the result is misaligned.
|
|
167
153
|
if (axisField !== undefined && !OHLC_KINDS.includes(spec.kind)) {
|
|
168
|
-
// 后续预处理本来就会逐行解析时间;这里也统计全量,避免前 100 行的脏数据把整条
|
|
169
|
-
// 后面的真实时间序列误判成分类轴。
|
|
170
154
|
const parsed = records.filter((record) => parseTimeValue(record[axisField]) !== undefined).length;
|
|
171
155
|
if (parsed / records.length < TIME_AXIS_THRESHOLD) {
|
|
172
156
|
axis = 'index';
|
|
173
157
|
warnings.push(`x field "${axisField}" was treated as a category axis: only ${parsed}/${records.length} values look like time`);
|
|
174
158
|
}
|
|
175
159
|
}
|
|
176
|
-
// ── 2. 逐行预处理 ────────────────────────────────────────────────────────
|
|
177
160
|
const prepared = [];
|
|
178
161
|
let skippedRows = 0;
|
|
179
162
|
records.forEach((record, sourceIndex) => {
|
|
@@ -197,14 +180,12 @@ export function buildChartPayload(input) {
|
|
|
197
180
|
if (skippedRows > 0)
|
|
198
181
|
warnings.push(`${skippedRows} row(s) skipped: the time field is missing or unparseable`);
|
|
199
182
|
if (prepared.length === 0) {
|
|
200
|
-
// OHLC
|
|
201
|
-
// 错误必须直接把模型引到能用的 kind 上,而不是让它反复试同一个 spec。
|
|
183
|
+
// OHLC has no fallback; point the model at column/line instead of retrying the same spec.
|
|
202
184
|
if (OHLC_KINDS.includes(spec.kind)) {
|
|
203
185
|
throw new ChartError('chart_no_rows', `${spec.kind} 需要可解析的时间列,但字段 "${axisField ?? '(未提供 x)'}" 的值都解析不出日期或时间戳;分类数据请改用 kind=column(单值柱)或 line`);
|
|
204
186
|
}
|
|
205
187
|
throw new ChartError('chart_no_rows', 'no row has a usable time value');
|
|
206
188
|
}
|
|
207
|
-
// ── 3. 排序 + 去重 + 区间过滤 ──────────────────────────────────────────────
|
|
208
189
|
const ordered = [...prepared].sort((left, right) => left.time.seconds - right.time.seconds);
|
|
209
190
|
if (ordered.some((row, index) => row !== prepared[index]))
|
|
210
191
|
warnings.push('rows were reordered ascending by time (the chart library requires an ordered axis)');
|
|
@@ -233,19 +214,17 @@ export function buildChartPayload(input) {
|
|
|
233
214
|
if (filtered.length === 0)
|
|
234
215
|
throw new ChartError('chart_no_rows', 'spec.range excludes every row');
|
|
235
216
|
}
|
|
236
|
-
//
|
|
217
|
+
// One LTTB index set is shared by every subplot so they stay time-aligned.
|
|
237
218
|
const originalPoints = filtered.length;
|
|
238
219
|
const primaryValues = primarySeriesValues(filtered, spec);
|
|
239
220
|
const keep = originalPoints > MAX_CHART_POINTS ? largestTriangleThreeBuckets(primaryValues, MAX_CHART_POINTS) : filtered.map((_row, index) => index);
|
|
240
221
|
const kept = keep.map((index) => filtered[index]).filter((row) => row !== undefined);
|
|
241
222
|
const downsampled = kept.length < originalPoints;
|
|
242
|
-
// ── 5. 时间显示口径:全是纯日期才用业务日字符串 ────────────────────────────
|
|
243
223
|
const useBusinessDay = axis === 'time' && kept.every((row) => row.time.dateOnly);
|
|
244
224
|
if (axis === 'time' && !useBusinessDay && kept.some((row) => row.time.dateOnly)) {
|
|
245
225
|
warnings.push('mixed date and datetime values; the axis is rendered as timestamps');
|
|
246
226
|
}
|
|
247
227
|
const displayTime = (row) => (useBusinessDay ? String(row.time.display) : row.time.seconds);
|
|
248
|
-
// ── 6. 子序列 ────────────────────────────────────────────────────────────
|
|
249
228
|
const series = [];
|
|
250
229
|
for (const item of spec.series.slice(0, MAX_CHART_SERIES)) {
|
|
251
230
|
const data = [];
|
|
@@ -268,8 +247,7 @@ export function buildChartPayload(input) {
|
|
|
268
247
|
if (spec.ohlc) {
|
|
269
248
|
const data = [];
|
|
270
249
|
let dropped = 0;
|
|
271
|
-
//
|
|
272
|
-
// 但必须如实披露,否则读者会把一根 high<low 的错柱子当成真行情。
|
|
250
|
+
// Inconsistent candles exist in real data: disclose them, never block — blocking hides the chart.
|
|
273
251
|
let inconsistent = 0;
|
|
274
252
|
const inconsistentExamples = [];
|
|
275
253
|
for (const row of kept) {
|
|
@@ -308,7 +286,7 @@ export function buildChartPayload(input) {
|
|
|
308
286
|
dropped += 1;
|
|
309
287
|
continue;
|
|
310
288
|
}
|
|
311
|
-
// A
|
|
289
|
+
// A-share convention: up = red, down = green; the actual colors come from the runtime theme.
|
|
312
290
|
let direction;
|
|
313
291
|
if (spec.ohlc) {
|
|
314
292
|
const open = toNumber(row.raw[spec.ohlc.open]);
|
|
@@ -324,12 +302,10 @@ export function buildChartPayload(input) {
|
|
|
324
302
|
warnings.push(`volume: ${dropped} point(s) dropped (non-numeric or missing)`);
|
|
325
303
|
volume = { label: '成交量', data };
|
|
326
304
|
}
|
|
327
|
-
// ── 7. 标记点:吸附到保留下来的时间点 ──────────────────────────────────────
|
|
328
305
|
const markers = [];
|
|
329
306
|
const keptSeconds = kept.map((row) => row.time.seconds);
|
|
330
307
|
for (const marker of spec.markers.slice(0, MAX_CHART_MARKERS)) {
|
|
331
|
-
//
|
|
332
|
-
// 标记不该跟着消失(那正是用户最在意的那一天)。
|
|
308
|
+
// Resolve both forms to seconds, then snap to a kept point so downsampling cannot drop a marker.
|
|
333
309
|
let targetSeconds;
|
|
334
310
|
if (marker.index !== undefined) {
|
|
335
311
|
const atIndex = filtered[marker.index];
|
|
@@ -383,7 +359,7 @@ export function buildChartPayload(input) {
|
|
|
383
359
|
},
|
|
384
360
|
};
|
|
385
361
|
}
|
|
386
|
-
/**
|
|
362
|
+
/** LTTB source: close for OHLC, else the first numeric field. Field gaps carry the last value forward. */
|
|
387
363
|
function primarySeriesValues(rows, spec) {
|
|
388
364
|
const field = spec.ohlc ? spec.ohlc.close : spec.series[0]?.field ?? spec.volume?.field;
|
|
389
365
|
const values = [];
|
|
@@ -395,7 +371,6 @@ function primarySeriesValues(rows, spec) {
|
|
|
395
371
|
}
|
|
396
372
|
return values;
|
|
397
373
|
}
|
|
398
|
-
/** 把标记点吸附到最近的**保留**时间点,避免下采样后标记消失。 */
|
|
399
374
|
function nearestRow(rows, keptSeconds, target) {
|
|
400
375
|
if (rows.length === 0)
|
|
401
376
|
return undefined;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { type SessionLike, WorkspaceDatasetStore } from '../data-collector/store.js';
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
* Keeping the live Session object is required because DSH sandbox policies use
|
|
6
|
-
* session.snapshotEvents() when resolving workspace permissions.
|
|
7
|
-
*/
|
|
3
|
+
/** Host-only; never serialized into agent output. Keep the live Session: sandbox policies call
|
|
4
|
+
* session.snapshotEvents() to resolve workspace permissions. */
|
|
8
5
|
export interface ChartSourceTokenRef {
|
|
9
6
|
chart_source_ref: string;
|
|
10
7
|
dataset_id: string;
|
|
@@ -21,13 +18,8 @@ export interface ChartSourceTokenStoreOptions {
|
|
|
21
18
|
ttlMs?: number;
|
|
22
19
|
newToken?: () => string;
|
|
23
20
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
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
|
-
*/
|
|
21
|
+
/** Short-lived capability tokens for nested visualization children. Possession is the capability;
|
|
22
|
+
* the token never becomes a filesystem path or raw rows. */
|
|
31
23
|
export declare class ChartSourceTokenStore {
|
|
32
24
|
private readonly tokens;
|
|
33
25
|
private readonly now;
|
|
@@ -51,7 +43,7 @@ export declare class ChartSourceTokenStore {
|
|
|
51
43
|
revoke(chartSourceRef: string): boolean;
|
|
52
44
|
size(): number;
|
|
53
45
|
}
|
|
54
|
-
/**
|
|
46
|
+
/** Preset-based authorization; see authorizedChartSession. */
|
|
55
47
|
export declare function registerChartSourceTool(ctx: Context, options: {
|
|
56
48
|
store: WorkspaceDatasetStore;
|
|
57
49
|
tokens: ChartSourceTokenStore;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ChartError } from './errors.js';
|
|
2
2
|
import { DatasetStoreError, } from '../data-collector/store.js';
|
|
3
|
+
// Authorization has one implementation (src/tool-exec.ts); the error shape stays chart-specific.
|
|
4
|
+
import { authorizeToolSession } from '../tool-exec.js';
|
|
5
|
+
import { CHART_SOURCE_PRESET_IDS } from '../preset-ids.js';
|
|
3
6
|
const CHART_SOURCE_TOKEN_PATTERN = /^cs_[A-Za-z0-9_-]{1,128}$/;
|
|
4
7
|
const DEFAULT_CHART_SOURCE_TTL_MS = 15 * 60 * 1000;
|
|
5
8
|
const jsonObject = (properties = {}, required = []) => ({
|
|
@@ -25,10 +28,14 @@ function sessionOf(exec) {
|
|
|
25
28
|
}
|
|
26
29
|
return session;
|
|
27
30
|
}
|
|
28
|
-
|
|
31
|
+
/** Shared preset-based authorizer; chart_source_scope_mismatch shape stays here (test-pinned). */
|
|
32
|
+
function authorizedChartSession(ctx, exec) {
|
|
29
33
|
const session = sessionOf(exec);
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const decision = authorizeToolSession(ctx, exec, 'prepare_chart_source', CHART_SOURCE_PRESET_IDS);
|
|
35
|
+
if (!decision.allowed) {
|
|
36
|
+
throw new ChartError('chart_source_scope_mismatch', decision.via === 'preset'
|
|
37
|
+
? `prepare_chart_source 不对人设 ${decision.presetId} 开放;签发出图读能力只属于数据角色`
|
|
38
|
+
: 'prepare_chart_source is restricted to delegated data agents');
|
|
32
39
|
}
|
|
33
40
|
return session;
|
|
34
41
|
}
|
|
@@ -55,13 +62,8 @@ function readToken(value) {
|
|
|
55
62
|
}
|
|
56
63
|
return value;
|
|
57
64
|
}
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* The token captures the direct data-agent session rather than asking a nested
|
|
62
|
-
* child to re-prove the Dataset's one-hop session scope. Possession of the
|
|
63
|
-
* opaque token is the capability; it is never converted into a filesystem path.
|
|
64
|
-
*/
|
|
65
|
+
/** Short-lived capability tokens for nested visualization children. Possession is the capability;
|
|
66
|
+
* the token never becomes a filesystem path or raw rows. */
|
|
65
67
|
export class ChartSourceTokenStore {
|
|
66
68
|
tokens = new Map();
|
|
67
69
|
now;
|
|
@@ -86,10 +88,8 @@ export class ChartSourceTokenStore {
|
|
|
86
88
|
const taskId = readTaskId(input.task_id, true);
|
|
87
89
|
const now = this.now();
|
|
88
90
|
this.purgeExpired(now);
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// 重签,跟着就是 7.9K 字符的思考回合——skill 里的"不要重签"警告拦不住,所以把幂等沉到
|
|
92
|
-
// 工具层。token 是读能力而非一次性凭证(resolve 不消费),复用没有语义损失。
|
|
91
|
+
// Idempotent within TTL for the same session+task+dataset: re-issuing triggers huge thinking
|
|
92
|
+
// turns, and resolve() does not consume the token, so reuse has no semantic cost.
|
|
93
93
|
for (const record of this.tokens.values()) {
|
|
94
94
|
if (record.dataset_id === datasetId && record.task_id === taskId && record.sourceSession.id === input.session.id) {
|
|
95
95
|
return {
|
|
@@ -110,6 +110,7 @@ export class ChartSourceTokenStore {
|
|
|
110
110
|
}
|
|
111
111
|
if (this.tokens.has(token))
|
|
112
112
|
throw new ChartError('chart_source_invalid', 'generated chart_source_ref already exists');
|
|
113
|
+
// Never outlive the dataset's retention.
|
|
113
114
|
const expiresAt = Math.min(inspection.retention_until, now + this.ttlMs);
|
|
114
115
|
const record = {
|
|
115
116
|
chart_source_ref: token,
|
|
@@ -166,14 +167,14 @@ const outputSchema = jsonObject({
|
|
|
166
167
|
task_id: { type: 'string' },
|
|
167
168
|
expires_at: { type: 'integer' },
|
|
168
169
|
}, ['chart_source_ref', 'dataset_id', 'task_id', 'expires_at']);
|
|
169
|
-
/**
|
|
170
|
+
/** Preset-based authorization; see authorizedChartSession. */
|
|
170
171
|
export function registerChartSourceTool(ctx, options) {
|
|
171
172
|
const tools = ctx.get('tools');
|
|
172
173
|
if (!tools)
|
|
173
174
|
return;
|
|
174
175
|
const definition = {
|
|
175
176
|
name: 'prepare_chart_source',
|
|
176
|
-
description: '
|
|
177
|
+
description: '为**当前数据角色会话**已授权的 Dataset 签发短期 chart_source_ref,供 visualization_specialist 通过受控 render_chart 读取(授权按人设判定,被委派与独立开启的数据模式都能签发)。' +
|
|
177
178
|
'只返回 opaque token 和小型元数据,不返回原始数据、文件路径或数据行;必须提供 dataset_id,可选 task_id。' +
|
|
178
179
|
'同一 task_id + dataset_id 重复调用幂等返回同一个 chart_source_ref(TTL 内),改了视图方案直接复用即可。',
|
|
179
180
|
parameters: jsonObject({
|
|
@@ -183,7 +184,7 @@ export function registerChartSourceTool(ctx, options) {
|
|
|
183
184
|
output: { schema: outputSchema, render },
|
|
184
185
|
async execute(args, exec) {
|
|
185
186
|
try {
|
|
186
|
-
const session =
|
|
187
|
+
const session = authorizedChartSession(ctx, exec);
|
|
187
188
|
return await options.tokens.issue({
|
|
188
189
|
store: options.store,
|
|
189
190
|
session,
|
package/lib/chart/spec.d.ts
CHANGED
|
@@ -15,14 +15,14 @@ export interface ChartOhlcSpec {
|
|
|
15
15
|
export interface ChartMarkerSpec {
|
|
16
16
|
text: string;
|
|
17
17
|
position: MarkerPosition;
|
|
18
|
-
/**
|
|
18
|
+
/** Time value matching x; mutually exclusive with index. */
|
|
19
19
|
time?: string | number;
|
|
20
|
-
/**
|
|
20
|
+
/** 0-based row index into the filtered series. */
|
|
21
21
|
index?: number;
|
|
22
22
|
}
|
|
23
23
|
export interface NormalizedChartSpec {
|
|
24
24
|
kind: ChartKind;
|
|
25
|
-
/** time =
|
|
25
|
+
/** time = real time column; index = row order (category axis, labels mapped at render time). */
|
|
26
26
|
axis: 'time' | 'index';
|
|
27
27
|
timeField?: string;
|
|
28
28
|
series: ChartSeriesSpec[];
|
|
@@ -38,14 +38,8 @@ export interface NormalizedChartSpec {
|
|
|
38
38
|
title?: string;
|
|
39
39
|
}
|
|
40
40
|
export declare const CHART_KINDS: readonly ChartKind[];
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 依据是图表库的数据契约本身:`CandlestickData` 与 `BarData` 都继承 `OhlcData`,
|
|
45
|
-
* 而 `LineData` / `HistogramData` 继承 `SingleValueData`。把单值序列塞进 BarSeries
|
|
46
|
-
* 不会报错,只会画出一张**空图** —— 所以这个约束必须在 spec 层硬拦,
|
|
47
|
-
* 不能指望"模型会写对"(这是实测出来的坑,不是推演)。
|
|
48
|
-
*/
|
|
41
|
+
/** OHLC kinds need four fields (library OhlcData vs SingleValueData); a single-value series in
|
|
42
|
+
* BarSeries silently draws an empty chart, so reject it at the spec layer. */
|
|
49
43
|
export declare const OHLC_KINDS: readonly ChartKind[];
|
|
50
44
|
export declare const CHART_SERIES_TYPES: readonly ChartSeriesType[];
|
|
51
45
|
export declare const MARKER_POSITIONS: readonly MarkerPosition[];
|
|
@@ -53,6 +47,6 @@ export declare const MAX_CHART_SERIES = 6;
|
|
|
53
47
|
export declare const MAX_CHART_MARKERS = 50;
|
|
54
48
|
export declare const MAX_FIELD_NAME = 128;
|
|
55
49
|
export declare const MAX_CHART_TITLE = 200;
|
|
56
|
-
/**
|
|
50
|
+
/** Shared model-facing kind description so error text and tool docs cannot drift. */
|
|
57
51
|
export declare function describeKinds(): string;
|
|
58
52
|
export declare function normalizeChartSpec(raw: unknown): NormalizedChartSpec;
|
package/lib/chart/spec.js
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 设计要点(见内部设计文档 docs/design/chart-visualization.md §3.3):
|
|
5
|
-
* - spec 用**自由对象**表达,协议细节写在 skill 里按需加载,常驻的只有工具 schema——
|
|
6
|
-
* 这是"可视化不污染上下文"的关键:模型不需要在每轮都看一遍图表 DSL。
|
|
7
|
-
* - 但自由对象不等于放任:这里做一次严格归一化,错误必须**可自愈**(点名字段 + 给候选)。
|
|
8
|
-
* - 与 query_dataset 同源的宽容度:模型经常把对象/数组序列化成 JSON 字符串再传
|
|
9
|
-
* (实测教训见 src/data-collector/dataset-tools.ts 的注释),这里在**工具边界**
|
|
10
|
-
* 做一次宽容解析,内部保持严格。
|
|
11
|
-
*/
|
|
1
|
+
/** Normalize/validate the chart spec at the tool boundary. Errors must be self-correcting
|
|
2
|
+
* (name the field, list candidates); JSON-string forms are tolerated here, strict inside. */
|
|
12
3
|
import { ChartError } from './errors.js';
|
|
13
4
|
export const CHART_KINDS = ['line', 'area', 'column', 'candlestick', 'bar'];
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 依据是图表库的数据契约本身:`CandlestickData` 与 `BarData` 都继承 `OhlcData`,
|
|
18
|
-
* 而 `LineData` / `HistogramData` 继承 `SingleValueData`。把单值序列塞进 BarSeries
|
|
19
|
-
* 不会报错,只会画出一张**空图** —— 所以这个约束必须在 spec 层硬拦,
|
|
20
|
-
* 不能指望"模型会写对"(这是实测出来的坑,不是推演)。
|
|
21
|
-
*/
|
|
5
|
+
/** OHLC kinds need four fields (library OhlcData vs SingleValueData); a single-value series in
|
|
6
|
+
* BarSeries silently draws an empty chart, so reject it at the spec layer. */
|
|
22
7
|
export const OHLC_KINDS = ['candlestick', 'bar'];
|
|
23
8
|
export const CHART_SERIES_TYPES = ['line', 'area', 'column'];
|
|
24
9
|
export const MARKER_POSITIONS = ['aboveBar', 'belowBar', 'inBar'];
|
|
@@ -26,14 +11,7 @@ export const MAX_CHART_SERIES = 6;
|
|
|
26
11
|
export const MAX_CHART_MARKERS = 50;
|
|
27
12
|
export const MAX_FIELD_NAME = 128;
|
|
28
13
|
export const MAX_CHART_TITLE = 200;
|
|
29
|
-
/**
|
|
30
|
-
* 容忍的说法 → canonical 名。
|
|
31
|
-
*
|
|
32
|
-
* `histogram` 是图表库的类名,但它画的是**着色柱**、不是统计分箱直方图;直接把它当
|
|
33
|
-
* canonical 名会让模型以为"可以画分布"。canonical 用 `column`,同时接受旧说法以免硬失败。
|
|
34
|
-
* 同理 `bar` 在中文语境里常被当成"柱状图",而它其实是 OHLC 柱:单值柱状图会得到一条
|
|
35
|
-
* 明确指向 `column` 的错误,而不是一张空图。
|
|
36
|
-
*/
|
|
14
|
+
/** `histogram` maps to `column` (a colored bar, not a statistical bin); `bar` is an OHLC bar. */
|
|
37
15
|
const KIND_ALIASES = {
|
|
38
16
|
histogram: 'column',
|
|
39
17
|
candles: 'candlestick',
|
|
@@ -42,14 +20,14 @@ const KIND_ALIASES = {
|
|
|
42
20
|
candlestick_bar: 'bar',
|
|
43
21
|
};
|
|
44
22
|
const SERIES_TYPE_ALIASES = { histogram: 'column' };
|
|
45
|
-
/**
|
|
23
|
+
/** Shared model-facing kind description so error text and tool docs cannot drift. */
|
|
46
24
|
export function describeKinds() {
|
|
47
25
|
return `${CHART_KINDS.join(' / ')}(candlestick 与 bar 需要 ohlc 四字段与真实时间列;单值柱状图用 column,本库没有统计分箱直方图)`;
|
|
48
26
|
}
|
|
49
27
|
function isRecord(value) {
|
|
50
28
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
51
29
|
}
|
|
52
|
-
/**
|
|
30
|
+
/** Tolerate JSON-string spec fragments at the tool boundary. */
|
|
53
31
|
function coerceObject(value) {
|
|
54
32
|
if (isRecord(value))
|
|
55
33
|
return value;
|
|
@@ -66,7 +44,6 @@ function coerceObject(value) {
|
|
|
66
44
|
return undefined;
|
|
67
45
|
}
|
|
68
46
|
}
|
|
69
|
-
/** 宽容解析对象数组:真数组、JSON 数组字符串、单个对象(或其 JSON 字符串)都接受。 */
|
|
70
47
|
function coerceObjectList(value) {
|
|
71
48
|
if (Array.isArray(value))
|
|
72
49
|
return value;
|
|
@@ -106,7 +83,6 @@ function readOptionalFieldName(value, where) {
|
|
|
106
83
|
return undefined;
|
|
107
84
|
return readFieldName(value, where);
|
|
108
85
|
}
|
|
109
|
-
/** 取第一个出现的别名键;用于容忍模型用 x / time / time_field 等等价说法。 */
|
|
110
86
|
function pickAlias(source, names) {
|
|
111
87
|
for (const name of names)
|
|
112
88
|
if (source[name] !== undefined)
|