@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.
- package/CHANGELOG.md +814 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/assets/cg-icon.png +0 -0
- package/capital-config/client.js +28 -0
- package/capital-config/client.src.cjs +388 -0
- package/capital-config/index.js +113 -0
- package/capital-config/package.json +36 -0
- package/capital-watchlist/client.js +182 -0
- package/capital-watchlist/client.src.cjs +1229 -0
- package/capital-watchlist/index.js +772 -0
- package/capital-watchlist/package.json +37 -0
- package/chart-ui/README.md +112 -0
- package/chart-ui/client.js +16 -0
- package/chart-ui/client.src.cjs +126 -0
- package/chart-ui/index.js +205 -0
- package/chart-ui/package.json +30 -0
- package/cordis.patch.yml +72 -0
- package/lib/agents/bash-guard.d.ts +55 -0
- package/lib/agents/bash-guard.js +162 -0
- package/lib/agents/root-tool-policy.d.ts +164 -0
- package/lib/agents/root-tool-policy.js +309 -0
- package/lib/chart/artifact-ref.d.ts +65 -0
- package/lib/chart/artifact-ref.js +107 -0
- package/lib/chart/downsample.d.ts +8 -0
- package/lib/chart/downsample.js +53 -0
- package/lib/chart/errors.d.ts +16 -0
- package/lib/chart/errors.js +15 -0
- package/lib/chart/events.d.ts +120 -0
- package/lib/chart/events.js +471 -0
- package/lib/chart/html.d.ts +21 -0
- package/lib/chart/html.js +154 -0
- package/lib/chart/runtime.d.ts +20 -0
- package/lib/chart/runtime.js +270 -0
- package/lib/chart/series.d.ts +102 -0
- package/lib/chart/series.js +424 -0
- package/lib/chart/source-token.d.ts +64 -0
- package/lib/chart/source-token.js +217 -0
- package/lib/chart/spec.d.ts +58 -0
- package/lib/chart/spec.js +265 -0
- package/lib/chart/tool.d.ts +86 -0
- package/lib/chart/tool.js +288 -0
- package/lib/chart/vendor/LICENSE +201 -0
- package/lib/chart/vendor/VERSION.json +8 -0
- package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
- package/lib/data-collector/args.d.ts +38 -0
- package/lib/data-collector/args.js +114 -0
- package/lib/data-collector/dataset-tools.d.ts +3 -0
- package/lib/data-collector/dataset-tools.js +421 -0
- package/lib/data-collector/describe.d.ts +99 -0
- package/lib/data-collector/describe.js +256 -0
- package/lib/data-collector/hub.d.ts +187 -0
- package/lib/data-collector/hub.js +469 -0
- package/lib/data-collector/query-time.d.ts +58 -0
- package/lib/data-collector/query-time.js +101 -0
- package/lib/data-collector/query.d.ts +57 -0
- package/lib/data-collector/query.js +473 -0
- package/lib/data-collector/store.d.ts +499 -0
- package/lib/data-collector/store.js +1486 -0
- package/lib/data-collector/time-axis.d.ts +169 -0
- package/lib/data-collector/time-axis.js +419 -0
- package/lib/data-collector/tools.d.ts +32 -0
- package/lib/data-collector/tools.js +216 -0
- package/lib/index.d.ts +275 -0
- package/lib/index.js +458 -0
- package/lib/net/eastmoney-client.d.ts +104 -0
- package/lib/net/eastmoney-client.js +118 -0
- package/lib/net/throttle.d.ts +16 -0
- package/lib/net/throttle.js +34 -0
- package/lib/ocr/client.d.ts +82 -0
- package/lib/ocr/client.js +281 -0
- package/lib/ocr/markdown.d.ts +75 -0
- package/lib/ocr/markdown.js +209 -0
- package/lib/ocr/store.d.ts +119 -0
- package/lib/ocr/store.js +226 -0
- package/lib/sources/eastmoney-http.d.ts +2 -0
- package/lib/sources/eastmoney-http.js +1253 -0
- package/lib/sources/fuyao-core.d.ts +52 -0
- package/lib/sources/fuyao-core.js +96 -0
- package/lib/sources/fuyao-rest.d.ts +14 -0
- package/lib/sources/fuyao-rest.js +2057 -0
- package/lib/sources/security-code.d.ts +27 -0
- package/lib/sources/security-code.js +104 -0
- package/lib/sources/security-identity.d.ts +24 -0
- package/lib/sources/security-identity.js +89 -0
- package/lib/sources/tencent-http.d.ts +2 -0
- package/lib/sources/tencent-http.js +1053 -0
- package/lib/sources/wind-mcp.d.ts +8 -0
- package/lib/sources/wind-mcp.js +593 -0
- package/lib/time/tools.d.ts +74 -0
- package/lib/time/tools.js +464 -0
- package/lib/tool-exec.d.ts +29 -0
- package/lib/tool-exec.js +17 -0
- package/lib/web-retriever/engines.d.ts +61 -0
- package/lib/web-retriever/engines.js +202 -0
- package/lib/web-retriever/html-markdown.d.ts +50 -0
- package/lib/web-retriever/html-markdown.js +321 -0
- package/lib/web-retriever/local-fetch.d.ts +121 -0
- package/lib/web-retriever/local-fetch.js +562 -0
- package/lib/web-retriever/retriever.d.ts +58 -0
- package/lib/web-retriever/retriever.js +94 -0
- package/lib/web-retriever/sources.d.ts +235 -0
- package/lib/web-retriever/sources.js +961 -0
- package/lib/web-retriever/tools.d.ts +50 -0
- package/lib/web-retriever/tools.js +1029 -0
- package/lib/web-retriever/wind-client.d.ts +69 -0
- package/lib/web-retriever/wind-client.js +311 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +84 -4
- package/preset/capital-generation/README.md +161 -0
- package/preset/capital-generation/agent.patch.yml +509 -0
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { ChartError } from './errors.js';
|
|
2
|
+
import { DatasetStoreError, } from '../data-collector/store.js';
|
|
3
|
+
const CHART_SOURCE_TOKEN_PATTERN = /^cs_[A-Za-z0-9_-]{1,128}$/;
|
|
4
|
+
const DEFAULT_CHART_SOURCE_TTL_MS = 15 * 60 * 1000;
|
|
5
|
+
const jsonObject = (properties = {}, required = []) => ({
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties,
|
|
8
|
+
required,
|
|
9
|
+
additionalProperties: false,
|
|
10
|
+
});
|
|
11
|
+
function render(_args, value) {
|
|
12
|
+
return [{ type: 'text', text: JSON.stringify(value) }];
|
|
13
|
+
}
|
|
14
|
+
function defaultTokenId() {
|
|
15
|
+
const cryptoLike = globalThis.crypto;
|
|
16
|
+
const suffix = cryptoLike?.randomUUID
|
|
17
|
+
? cryptoLike.randomUUID()
|
|
18
|
+
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
19
|
+
return `cs_${suffix}`;
|
|
20
|
+
}
|
|
21
|
+
function sessionOf(exec) {
|
|
22
|
+
const session = exec.agent?.session;
|
|
23
|
+
if (!session || typeof session.id !== 'string' || session.id.length === 0) {
|
|
24
|
+
throw new ChartError('chart_source_invalid', 'the calling agent session was not provided');
|
|
25
|
+
}
|
|
26
|
+
return session;
|
|
27
|
+
}
|
|
28
|
+
function delegatedSession(exec) {
|
|
29
|
+
const session = sessionOf(exec);
|
|
30
|
+
if (typeof session.header?.parentSession !== 'string' || session.header.parentSession.length === 0) {
|
|
31
|
+
throw new ChartError('chart_source_scope_mismatch', 'prepare_chart_source is restricted to delegated data agents');
|
|
32
|
+
}
|
|
33
|
+
return session;
|
|
34
|
+
}
|
|
35
|
+
function readTaskId(value, required) {
|
|
36
|
+
if (value === undefined || value === null || value === '') {
|
|
37
|
+
if (required)
|
|
38
|
+
throw new ChartError('chart_source_invalid', 'task_id is required when using chart_source_ref');
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.length > 256) {
|
|
42
|
+
throw new ChartError('chart_source_invalid', 'task_id must be a non-empty string of at most 256 characters');
|
|
43
|
+
}
|
|
44
|
+
return value.trim();
|
|
45
|
+
}
|
|
46
|
+
function readDatasetId(value) {
|
|
47
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
48
|
+
throw new ChartError('chart_source_invalid', 'dataset_id is invalid');
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function readToken(value) {
|
|
53
|
+
if (typeof value !== 'string' || !CHART_SOURCE_TOKEN_PATTERN.test(value)) {
|
|
54
|
+
throw new ChartError('chart_source_invalid', 'chart_source_ref is invalid');
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Short-lived capability tokens for nested visualization children.
|
|
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
|
+
export class ChartSourceTokenStore {
|
|
66
|
+
tokens = new Map();
|
|
67
|
+
now;
|
|
68
|
+
ttlMs;
|
|
69
|
+
newToken;
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.now = options.now ?? (() => Date.now());
|
|
72
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_CHART_SOURCE_TTL_MS;
|
|
73
|
+
this.newToken = options.newToken ?? defaultTokenId;
|
|
74
|
+
if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) {
|
|
75
|
+
throw new Error('ChartSourceTokenStore ttlMs must be a positive safe integer');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
purgeExpired(now) {
|
|
79
|
+
for (const [token, record] of this.tokens) {
|
|
80
|
+
if (record.expires_at <= now)
|
|
81
|
+
this.tokens.delete(token);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async issue(input) {
|
|
85
|
+
const datasetId = readDatasetId(input.dataset_id);
|
|
86
|
+
const taskId = readTaskId(input.task_id, true);
|
|
87
|
+
const inspection = await input.store.inspectDataset(datasetId, input.session, input.signal);
|
|
88
|
+
if (!inspection.query_access.readable || !['array', 'envelope_item'].includes(inspection.query_access.shape)) {
|
|
89
|
+
throw new ChartError('chart_source_invalid', `Dataset ${datasetId} cannot be used as a chart source (${inspection.query_access.reason ?? inspection.query_access.shape})`);
|
|
90
|
+
}
|
|
91
|
+
const now = this.now();
|
|
92
|
+
this.purgeExpired(now);
|
|
93
|
+
const token = this.newToken();
|
|
94
|
+
if (!CHART_SOURCE_TOKEN_PATTERN.test(token)) {
|
|
95
|
+
throw new ChartError('chart_source_invalid', 'generated chart_source_ref is invalid');
|
|
96
|
+
}
|
|
97
|
+
if (this.tokens.has(token))
|
|
98
|
+
throw new ChartError('chart_source_invalid', 'generated chart_source_ref already exists');
|
|
99
|
+
const expiresAt = Math.min(inspection.retention_until, now + this.ttlMs);
|
|
100
|
+
const record = {
|
|
101
|
+
chart_source_ref: token,
|
|
102
|
+
dataset_id: inspection.dataset_id,
|
|
103
|
+
task_id: taskId,
|
|
104
|
+
expires_at: expiresAt,
|
|
105
|
+
sourceSession: input.session,
|
|
106
|
+
issued_at: now,
|
|
107
|
+
};
|
|
108
|
+
this.tokens.set(token, record);
|
|
109
|
+
return {
|
|
110
|
+
chart_source_ref: record.chart_source_ref,
|
|
111
|
+
dataset_id: record.dataset_id,
|
|
112
|
+
task_id: record.task_id,
|
|
113
|
+
expires_at: record.expires_at,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
resolve(input) {
|
|
117
|
+
const token = readToken(input.chart_source_ref);
|
|
118
|
+
const now = input.now ?? this.now();
|
|
119
|
+
this.purgeExpired(now);
|
|
120
|
+
const record = this.tokens.get(token);
|
|
121
|
+
if (!record)
|
|
122
|
+
throw new ChartError('chart_source_expired', 'chart_source_ref is unknown or expired');
|
|
123
|
+
const taskId = readTaskId(input.task_id, true);
|
|
124
|
+
if (record.task_id !== null && record.task_id !== taskId) {
|
|
125
|
+
throw new ChartError('chart_source_scope_mismatch', 'chart_source_ref does not belong to this task');
|
|
126
|
+
}
|
|
127
|
+
if (input.session !== undefined) {
|
|
128
|
+
const caller = input.session;
|
|
129
|
+
const source = record.sourceSession;
|
|
130
|
+
const isSource = caller.id === source.id;
|
|
131
|
+
const isDirectChild = caller.header?.parentSession === source.id;
|
|
132
|
+
if (!isSource && !isDirectChild) {
|
|
133
|
+
throw new ChartError('chart_source_scope_mismatch', 'chart_source_ref is not authorized for this session');
|
|
134
|
+
}
|
|
135
|
+
if (source.header?.cwd !== undefined && caller.header?.cwd !== undefined && source.header.cwd !== caller.header.cwd) {
|
|
136
|
+
throw new ChartError('chart_source_scope_mismatch', 'chart_source_ref does not belong to this workspace');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return record;
|
|
140
|
+
}
|
|
141
|
+
revoke(chartSourceRef) {
|
|
142
|
+
return this.tokens.delete(chartSourceRef);
|
|
143
|
+
}
|
|
144
|
+
size() {
|
|
145
|
+
this.purgeExpired(this.now());
|
|
146
|
+
return this.tokens.size;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const outputSchema = jsonObject({
|
|
150
|
+
chart_source_ref: { type: 'string' },
|
|
151
|
+
dataset_id: { type: 'string' },
|
|
152
|
+
task_id: { type: 'string' },
|
|
153
|
+
expires_at: { type: 'integer' },
|
|
154
|
+
}, ['chart_source_ref', 'dataset_id', 'task_id', 'expires_at']);
|
|
155
|
+
/** Register the delegated-child-only token issuer. */
|
|
156
|
+
export function registerChartSourceTool(ctx, options) {
|
|
157
|
+
const tools = ctx.get('tools');
|
|
158
|
+
if (!tools)
|
|
159
|
+
return;
|
|
160
|
+
const definition = {
|
|
161
|
+
name: 'prepare_chart_source',
|
|
162
|
+
description: '为当前 delegated data agent 的指定 Dataset 签发短期 chart_source_ref,供 visualization_specialist 通过受控 render_chart 读取。' +
|
|
163
|
+
'只返回 opaque token 和小型元数据,不返回原始数据、文件路径或数据行;必须提供 dataset_id,可选 task_id。',
|
|
164
|
+
parameters: jsonObject({
|
|
165
|
+
dataset_id: { type: 'string', description: '已授权的 DatasetRef dataset_id' },
|
|
166
|
+
task_id: { type: 'string', description: '当前可视化任务标识;用于绑定 token,必填' },
|
|
167
|
+
}, ['dataset_id', 'task_id']),
|
|
168
|
+
output: { schema: outputSchema, render },
|
|
169
|
+
async execute(args, exec) {
|
|
170
|
+
try {
|
|
171
|
+
const session = delegatedSession(exec);
|
|
172
|
+
return await options.tokens.issue({
|
|
173
|
+
store: options.store,
|
|
174
|
+
session,
|
|
175
|
+
dataset_id: args.dataset_id,
|
|
176
|
+
task_id: args.task_id,
|
|
177
|
+
signal: exec.signal,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error instanceof ChartError) {
|
|
182
|
+
const visible = new Error(JSON.stringify(error.toEnvelope()));
|
|
183
|
+
visible.name = error.name;
|
|
184
|
+
visible.code = error.code;
|
|
185
|
+
throw visible;
|
|
186
|
+
}
|
|
187
|
+
if (error instanceof DatasetStoreError) {
|
|
188
|
+
const visible = new Error(JSON.stringify({ error: 'chart_source_invalid', detail: error.message }));
|
|
189
|
+
visible.name = 'ChartError';
|
|
190
|
+
visible.code = 'chart_source_invalid';
|
|
191
|
+
throw visible;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
ctx.effect(() => tools.register(definition), 'capital-generation.tool(prepare_chart_source)');
|
|
198
|
+
}
|
|
199
|
+
export function chartSourceTokenForRender(tokens, args, session) {
|
|
200
|
+
const chartSourceRef = typeof args.chart_source_ref === 'string' && args.chart_source_ref.trim().length > 0
|
|
201
|
+
? args.chart_source_ref.trim()
|
|
202
|
+
: undefined;
|
|
203
|
+
if (chartSourceRef === undefined)
|
|
204
|
+
return undefined;
|
|
205
|
+
if (!tokens)
|
|
206
|
+
throw new ChartError('chart_source_invalid', 'chart_source_ref is not available in this runtime');
|
|
207
|
+
const record = tokens.resolve({
|
|
208
|
+
chart_source_ref: chartSourceRef,
|
|
209
|
+
task_id: typeof args.task_id === 'string' ? args.task_id : undefined,
|
|
210
|
+
session,
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
dataset_id: record.dataset_id,
|
|
214
|
+
task_id: record.task_id,
|
|
215
|
+
sourceSession: record.sourceSession,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type ChartKind = 'line' | 'area' | 'column' | 'candlestick' | 'bar';
|
|
2
|
+
export type ChartSeriesType = 'line' | 'area' | 'column';
|
|
3
|
+
export type MarkerPosition = 'aboveBar' | 'belowBar' | 'inBar';
|
|
4
|
+
export interface ChartSeriesSpec {
|
|
5
|
+
field: string;
|
|
6
|
+
label: string;
|
|
7
|
+
type: ChartSeriesType;
|
|
8
|
+
}
|
|
9
|
+
export interface ChartOhlcSpec {
|
|
10
|
+
open: string;
|
|
11
|
+
high: string;
|
|
12
|
+
low: string;
|
|
13
|
+
close: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ChartMarkerSpec {
|
|
16
|
+
text: string;
|
|
17
|
+
position: MarkerPosition;
|
|
18
|
+
/** 与 x 同口径的时间值;与 index 二选一。 */
|
|
19
|
+
time?: string | number;
|
|
20
|
+
/** 过滤后序列中的 0 基行号。 */
|
|
21
|
+
index?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface NormalizedChartSpec {
|
|
24
|
+
kind: ChartKind;
|
|
25
|
+
/** time = 有真实时间列;index = 按行序(分类轴),标签由 labels 映射。 */
|
|
26
|
+
axis: 'time' | 'index';
|
|
27
|
+
timeField?: string;
|
|
28
|
+
series: ChartSeriesSpec[];
|
|
29
|
+
ohlc?: ChartOhlcSpec;
|
|
30
|
+
volume?: {
|
|
31
|
+
field: string;
|
|
32
|
+
};
|
|
33
|
+
markers: ChartMarkerSpec[];
|
|
34
|
+
range?: {
|
|
35
|
+
from?: string | number;
|
|
36
|
+
to?: string | number;
|
|
37
|
+
};
|
|
38
|
+
title?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const CHART_KINDS: readonly ChartKind[];
|
|
41
|
+
/**
|
|
42
|
+
* 需要 OHLC 四字段(而不是单值字段选择)的 kind。
|
|
43
|
+
*
|
|
44
|
+
* 依据是图表库的数据契约本身:`CandlestickData` 与 `BarData` 都继承 `OhlcData`,
|
|
45
|
+
* 而 `LineData` / `HistogramData` 继承 `SingleValueData`。把单值序列塞进 BarSeries
|
|
46
|
+
* 不会报错,只会画出一张**空图** —— 所以这个约束必须在 spec 层硬拦,
|
|
47
|
+
* 不能指望"模型会写对"(这是实测出来的坑,不是推演)。
|
|
48
|
+
*/
|
|
49
|
+
export declare const OHLC_KINDS: readonly ChartKind[];
|
|
50
|
+
export declare const CHART_SERIES_TYPES: readonly ChartSeriesType[];
|
|
51
|
+
export declare const MARKER_POSITIONS: readonly MarkerPosition[];
|
|
52
|
+
export declare const MAX_CHART_SERIES = 6;
|
|
53
|
+
export declare const MAX_CHART_MARKERS = 50;
|
|
54
|
+
export declare const MAX_FIELD_NAME = 128;
|
|
55
|
+
export declare const MAX_CHART_TITLE = 200;
|
|
56
|
+
/** 面向模型的 kind 说明(错误信息与工具描述共用一处,避免两处措辞漂移)。 */
|
|
57
|
+
export declare function describeKinds(): string;
|
|
58
|
+
export declare function normalizeChartSpec(raw: unknown): NormalizedChartSpec;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表 spec 的归一化与校验。
|
|
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
|
+
*/
|
|
12
|
+
import { ChartError } from './errors.js';
|
|
13
|
+
export const CHART_KINDS = ['line', 'area', 'column', 'candlestick', 'bar'];
|
|
14
|
+
/**
|
|
15
|
+
* 需要 OHLC 四字段(而不是单值字段选择)的 kind。
|
|
16
|
+
*
|
|
17
|
+
* 依据是图表库的数据契约本身:`CandlestickData` 与 `BarData` 都继承 `OhlcData`,
|
|
18
|
+
* 而 `LineData` / `HistogramData` 继承 `SingleValueData`。把单值序列塞进 BarSeries
|
|
19
|
+
* 不会报错,只会画出一张**空图** —— 所以这个约束必须在 spec 层硬拦,
|
|
20
|
+
* 不能指望"模型会写对"(这是实测出来的坑,不是推演)。
|
|
21
|
+
*/
|
|
22
|
+
export const OHLC_KINDS = ['candlestick', 'bar'];
|
|
23
|
+
export const CHART_SERIES_TYPES = ['line', 'area', 'column'];
|
|
24
|
+
export const MARKER_POSITIONS = ['aboveBar', 'belowBar', 'inBar'];
|
|
25
|
+
export const MAX_CHART_SERIES = 6;
|
|
26
|
+
export const MAX_CHART_MARKERS = 50;
|
|
27
|
+
export const MAX_FIELD_NAME = 128;
|
|
28
|
+
export const MAX_CHART_TITLE = 200;
|
|
29
|
+
/**
|
|
30
|
+
* 容忍的说法 → canonical 名。
|
|
31
|
+
*
|
|
32
|
+
* `histogram` 是图表库的类名,但它画的是**着色柱**、不是统计分箱直方图;直接把它当
|
|
33
|
+
* canonical 名会让模型以为"可以画分布"。canonical 用 `column`,同时接受旧说法以免硬失败。
|
|
34
|
+
* 同理 `bar` 在中文语境里常被当成"柱状图",而它其实是 OHLC 柱:单值柱状图会得到一条
|
|
35
|
+
* 明确指向 `column` 的错误,而不是一张空图。
|
|
36
|
+
*/
|
|
37
|
+
const KIND_ALIASES = {
|
|
38
|
+
histogram: 'column',
|
|
39
|
+
candles: 'candlestick',
|
|
40
|
+
candle: 'candlestick',
|
|
41
|
+
ohlc: 'bar',
|
|
42
|
+
candlestick_bar: 'bar',
|
|
43
|
+
};
|
|
44
|
+
const SERIES_TYPE_ALIASES = { histogram: 'column' };
|
|
45
|
+
/** 面向模型的 kind 说明(错误信息与工具描述共用一处,避免两处措辞漂移)。 */
|
|
46
|
+
export function describeKinds() {
|
|
47
|
+
return `${CHART_KINDS.join(' / ')}(candlestick 与 bar 需要 ohlc 四字段与真实时间列;单值柱状图用 column,本库没有统计分箱直方图)`;
|
|
48
|
+
}
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
/** 宽容解析:真对象直接用;JSON 字符串解析成对象后使用;其余返回 undefined。 */
|
|
53
|
+
function coerceObject(value) {
|
|
54
|
+
if (isRecord(value))
|
|
55
|
+
return value;
|
|
56
|
+
if (typeof value !== 'string')
|
|
57
|
+
return undefined;
|
|
58
|
+
const text = value.trim();
|
|
59
|
+
if (text.length === 0 || text[0] !== '{')
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(text);
|
|
63
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** 宽容解析对象数组:真数组、JSON 数组字符串、单个对象(或其 JSON 字符串)都接受。 */
|
|
70
|
+
function coerceObjectList(value) {
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value;
|
|
73
|
+
if (isRecord(value))
|
|
74
|
+
return [value];
|
|
75
|
+
if (typeof value !== 'string')
|
|
76
|
+
return undefined;
|
|
77
|
+
const text = value.trim();
|
|
78
|
+
if (text.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (text[0] === '[' || text[0] === '{') {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(text);
|
|
83
|
+
if (Array.isArray(parsed))
|
|
84
|
+
return parsed;
|
|
85
|
+
if (isRecord(parsed))
|
|
86
|
+
return [parsed];
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function readFieldName(value, where) {
|
|
96
|
+
if (typeof value !== 'string')
|
|
97
|
+
throw new ChartError('chart_spec_invalid', `${where} must be a string field name`);
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (trimmed.length === 0 || trimmed.length > MAX_FIELD_NAME) {
|
|
100
|
+
throw new ChartError('chart_spec_invalid', `${where} must be a field name of 1-${MAX_FIELD_NAME} characters`);
|
|
101
|
+
}
|
|
102
|
+
return trimmed;
|
|
103
|
+
}
|
|
104
|
+
function readOptionalFieldName(value, where) {
|
|
105
|
+
if (value === undefined || value === null || value === '')
|
|
106
|
+
return undefined;
|
|
107
|
+
return readFieldName(value, where);
|
|
108
|
+
}
|
|
109
|
+
/** 取第一个出现的别名键;用于容忍模型用 x / time / time_field 等等价说法。 */
|
|
110
|
+
function pickAlias(source, names) {
|
|
111
|
+
for (const name of names)
|
|
112
|
+
if (source[name] !== undefined)
|
|
113
|
+
return source[name];
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function readKind(value) {
|
|
117
|
+
if (typeof value !== 'string') {
|
|
118
|
+
throw new ChartError('chart_spec_invalid', `spec.kind is required and must be one of: ${describeKinds()}`);
|
|
119
|
+
}
|
|
120
|
+
const raw = value.trim().toLowerCase();
|
|
121
|
+
const kind = (KIND_ALIASES[raw] ?? raw);
|
|
122
|
+
if (!CHART_KINDS.includes(kind)) {
|
|
123
|
+
throw new ChartError('chart_spec_invalid', `spec.kind must be one of: ${describeKinds()} (got ${JSON.stringify(value)})`, { allowed: [...CHART_KINDS] });
|
|
124
|
+
}
|
|
125
|
+
return kind;
|
|
126
|
+
}
|
|
127
|
+
function readSeries(value) {
|
|
128
|
+
const list = coerceObjectList(value);
|
|
129
|
+
if (list === undefined || list.length === 0) {
|
|
130
|
+
throw new ChartError('chart_spec_invalid', 'spec.series is required for this kind: an array of 1-6 entries, each a field name or { field, label?, type? }');
|
|
131
|
+
}
|
|
132
|
+
if (list.length > MAX_CHART_SERIES) {
|
|
133
|
+
throw new ChartError('chart_spec_invalid', `spec.series accepts at most ${MAX_CHART_SERIES} entries (got ${list.length})`);
|
|
134
|
+
}
|
|
135
|
+
return list.map((entry, index) => {
|
|
136
|
+
if (typeof entry === 'string')
|
|
137
|
+
return { field: readFieldName(entry, `spec.series[${index}]`), label: entry.trim(), type: 'line' };
|
|
138
|
+
if (!isRecord(entry))
|
|
139
|
+
throw new ChartError('chart_spec_invalid', `spec.series[${index}] must be a field name or an object`);
|
|
140
|
+
const field = readFieldName(entry.field ?? entry.column ?? entry.name, `spec.series[${index}].field`);
|
|
141
|
+
const label = typeof entry.label === 'string' && entry.label.trim().length > 0 ? entry.label.trim() : field;
|
|
142
|
+
let type = 'line';
|
|
143
|
+
if (entry.type !== undefined) {
|
|
144
|
+
const raw = typeof entry.type === 'string' ? entry.type.trim().toLowerCase() : '';
|
|
145
|
+
const resolved = (SERIES_TYPE_ALIASES[raw] ?? raw);
|
|
146
|
+
if (!CHART_SERIES_TYPES.includes(resolved)) {
|
|
147
|
+
throw new ChartError('chart_spec_invalid', `spec.series[${index}].type must be one of: ${CHART_SERIES_TYPES.join(', ')} — 序列是单值字段,OHLC 图形(candlestick / bar)要用 spec.ohlc`, { allowed: [...CHART_SERIES_TYPES] });
|
|
148
|
+
}
|
|
149
|
+
type = resolved;
|
|
150
|
+
}
|
|
151
|
+
return { field, label, type };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function readOhlc(value) {
|
|
155
|
+
const ohlc = coerceObject(value);
|
|
156
|
+
if (!ohlc) {
|
|
157
|
+
throw new ChartError('chart_spec_invalid', 'candlestick requires spec.ohlc = { open, high, low, close } with four field names');
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
open: readFieldName(ohlc.open, 'spec.ohlc.open'),
|
|
161
|
+
high: readFieldName(ohlc.high, 'spec.ohlc.high'),
|
|
162
|
+
low: readFieldName(ohlc.low, 'spec.ohlc.low'),
|
|
163
|
+
close: readFieldName(ohlc.close, 'spec.ohlc.close'),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function readVolume(value) {
|
|
167
|
+
if (value === undefined || value === null || value === '')
|
|
168
|
+
return undefined;
|
|
169
|
+
if (typeof value === 'string')
|
|
170
|
+
return { field: readFieldName(value, 'spec.volume') };
|
|
171
|
+
const volume = coerceObject(value);
|
|
172
|
+
if (!volume)
|
|
173
|
+
throw new ChartError('chart_spec_invalid', 'spec.volume must be a field name or { field }');
|
|
174
|
+
const field = readOptionalFieldName(volume.field ?? volume.column, 'spec.volume.field');
|
|
175
|
+
if (field === undefined)
|
|
176
|
+
throw new ChartError('chart_spec_invalid', 'spec.volume requires a field name');
|
|
177
|
+
return { field };
|
|
178
|
+
}
|
|
179
|
+
function readMarkers(value) {
|
|
180
|
+
if (value === undefined || value === null)
|
|
181
|
+
return [];
|
|
182
|
+
const list = coerceObjectList(value);
|
|
183
|
+
if (list === undefined)
|
|
184
|
+
throw new ChartError('chart_spec_invalid', 'spec.markers must be an array of { time|index, text, position? }');
|
|
185
|
+
if (list.length > MAX_CHART_MARKERS) {
|
|
186
|
+
throw new ChartError('chart_spec_invalid', `spec.markers accepts at most ${MAX_CHART_MARKERS} entries (got ${list.length})`);
|
|
187
|
+
}
|
|
188
|
+
return list.map((entry, index) => {
|
|
189
|
+
if (!isRecord(entry))
|
|
190
|
+
throw new ChartError('chart_spec_invalid', `spec.markers[${index}] must be an object`);
|
|
191
|
+
const text = typeof (entry.text ?? entry.label) === 'string' ? String(entry.text ?? entry.label).trim() : '';
|
|
192
|
+
if (text.length === 0 || text.length > 120) {
|
|
193
|
+
throw new ChartError('chart_spec_invalid', `spec.markers[${index}].text must be 1-120 characters`);
|
|
194
|
+
}
|
|
195
|
+
const rawPosition = typeof entry.position === 'string' ? entry.position.trim() : 'aboveBar';
|
|
196
|
+
if (!MARKER_POSITIONS.includes(rawPosition)) {
|
|
197
|
+
throw new ChartError('chart_spec_invalid', `spec.markers[${index}].position must be one of: ${MARKER_POSITIONS.join(', ')}`, { allowed: [...MARKER_POSITIONS] });
|
|
198
|
+
}
|
|
199
|
+
const rawTime = pickAlias(entry, ['time', 'date', 'x']);
|
|
200
|
+
const rawIndex = entry.index ?? entry.i;
|
|
201
|
+
if (rawTime === undefined && rawIndex === undefined) {
|
|
202
|
+
throw new ChartError('chart_spec_invalid', `spec.markers[${index}] needs a time (matching spec.x) or a 0-based index`);
|
|
203
|
+
}
|
|
204
|
+
const marker = { text, position: rawPosition };
|
|
205
|
+
if (rawTime !== undefined) {
|
|
206
|
+
if (typeof rawTime !== 'string' && typeof rawTime !== 'number') {
|
|
207
|
+
throw new ChartError('chart_spec_invalid', `spec.markers[${index}].time must be a string or number`);
|
|
208
|
+
}
|
|
209
|
+
marker.time = rawTime;
|
|
210
|
+
}
|
|
211
|
+
if (rawIndex !== undefined) {
|
|
212
|
+
if (typeof rawIndex !== 'number' || !Number.isSafeInteger(rawIndex) || rawIndex < 0) {
|
|
213
|
+
throw new ChartError('chart_spec_invalid', `spec.markers[${index}].index must be a non-negative integer`);
|
|
214
|
+
}
|
|
215
|
+
marker.index = rawIndex;
|
|
216
|
+
}
|
|
217
|
+
return marker;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function readRange(value) {
|
|
221
|
+
if (value === undefined || value === null || value === '')
|
|
222
|
+
return undefined;
|
|
223
|
+
const range = coerceObject(value);
|
|
224
|
+
if (!range)
|
|
225
|
+
throw new ChartError('chart_spec_invalid', 'spec.range must be an object { from?, to? }');
|
|
226
|
+
const from = pickAlias(range, ['from', 'start']);
|
|
227
|
+
const to = pickAlias(range, ['to', 'end']);
|
|
228
|
+
const read = (raw, where) => {
|
|
229
|
+
if (raw === undefined || raw === null || raw === '')
|
|
230
|
+
return undefined;
|
|
231
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
232
|
+
return raw;
|
|
233
|
+
if (typeof raw === 'string')
|
|
234
|
+
return raw.trim();
|
|
235
|
+
throw new ChartError('chart_spec_invalid', `spec.range.${where} must be a date string or an epoch number`);
|
|
236
|
+
};
|
|
237
|
+
const result = { from: read(from, 'from'), to: read(to, 'to') };
|
|
238
|
+
if (result.from === undefined && result.to === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
export function normalizeChartSpec(raw) {
|
|
243
|
+
const spec = coerceObject(raw);
|
|
244
|
+
if (!spec) {
|
|
245
|
+
throw new ChartError('chart_spec_invalid', 'spec must be an object (a JSON object string is also accepted); see skill capital-chart-protocol');
|
|
246
|
+
}
|
|
247
|
+
const kind = readKind(spec.kind);
|
|
248
|
+
const timeField = readOptionalFieldName(pickAlias(spec, ['x', 'time_field', 'time']), 'spec.x');
|
|
249
|
+
const volume = readVolume(pickAlias(spec, ['volume', 'volume_field']));
|
|
250
|
+
const markers = readMarkers(spec.markers);
|
|
251
|
+
const range = readRange(spec.range);
|
|
252
|
+
const title = typeof spec.title === 'string' && spec.title.trim().length > 0 ? spec.title.trim().slice(0, MAX_CHART_TITLE) : undefined;
|
|
253
|
+
if (OHLC_KINDS.includes(kind)) {
|
|
254
|
+
const ohlc = readOhlc(spec.ohlc ?? spec.candles);
|
|
255
|
+
if (timeField === undefined) {
|
|
256
|
+
throw new ChartError('chart_spec_invalid', `${kind} requires spec.x: the time field name (OHLC 图形需要真实时间轴,不能退化成分类轴)`);
|
|
257
|
+
}
|
|
258
|
+
return { kind, axis: 'time', timeField, series: [], ohlc, volume, markers, range, title };
|
|
259
|
+
}
|
|
260
|
+
if (spec.ohlc !== undefined && spec.ohlc !== null && spec.ohlc !== '') {
|
|
261
|
+
throw new ChartError('chart_spec_invalid', `spec.ohlc 只用于 candlestick / bar;kind=${kind} 请改用 spec.series(单值字段选择)`);
|
|
262
|
+
}
|
|
263
|
+
const series = readSeries(spec.series ?? spec.fields ?? spec.y);
|
|
264
|
+
return { kind, axis: timeField === undefined ? 'index' : 'time', timeField, series, volume, markers, range, title };
|
|
265
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render_chart`:把已经取到的数据渲染成图,**只回一条小回执**。
|
|
3
|
+
*
|
|
4
|
+
* 上下文经济学(这是本工具存在的理由):
|
|
5
|
+
* - 常驻成本 = 一条工具 schema;协议细节在 skill `capital-chart-protocol` 里按需加载;
|
|
6
|
+
* - 每次调用成本 = spec(几十到一百多 token)+ 回执(几十 token);
|
|
7
|
+
* - 序列数据 **0 token**:由宿主写进 workspace 产物,浏览器走旁路取。
|
|
8
|
+
* 对照:让模型把 250 点日线写进参数 ≈ 5–8k token,还会被工具结果剪枝器截断。
|
|
9
|
+
*
|
|
10
|
+
* 权限边界(docs/dev/data-roles.md §1.1「数据调度纪律」的显式豁免):本工具是**呈现**工具,不是数据工具。
|
|
11
|
+
* 它读 Dataset 只在宿主进程内进行,回执不含任何 rows —— 因此不越过"数据内容不进主 Agent
|
|
12
|
+
* 上下文"这条真正的边界。需要数值结论仍走 data_junior 的 profile / query。
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import { WorkspaceDatasetStore } from '../data-collector/store.js';
|
|
16
|
+
import { type ChartSourceTokenStore } from './source-token.js';
|
|
17
|
+
import { ChartArtifactRegistry } from './artifact-ref.js';
|
|
18
|
+
import { type ChartEventPublisher } from './events.js';
|
|
19
|
+
type AgentExecutionLike = {
|
|
20
|
+
session?: {
|
|
21
|
+
id?: string;
|
|
22
|
+
header?: {
|
|
23
|
+
cwd?: string;
|
|
24
|
+
parentSession?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
type ToolExecLike = {
|
|
29
|
+
agent?: AgentExecutionLike;
|
|
30
|
+
signal: AbortSignal;
|
|
31
|
+
};
|
|
32
|
+
export declare const CHART_ARTIFACT_DIR = "capital-analysis/charts";
|
|
33
|
+
/** 回执字段全部是可以进上下文的小元数据;序列本体永远不在这里。 */
|
|
34
|
+
export interface ChartReceipt {
|
|
35
|
+
chart_ref: string;
|
|
36
|
+
chart_id: string;
|
|
37
|
+
kind: string;
|
|
38
|
+
axis: 'time' | 'index';
|
|
39
|
+
title: string;
|
|
40
|
+
points: number;
|
|
41
|
+
original_points: number;
|
|
42
|
+
downsampled: boolean;
|
|
43
|
+
series_labels: string[];
|
|
44
|
+
has_volume: boolean;
|
|
45
|
+
markers: number;
|
|
46
|
+
dataset_id: string | null;
|
|
47
|
+
source_label: string | null;
|
|
48
|
+
captured_at: number | null;
|
|
49
|
+
chart_url: string | null;
|
|
50
|
+
spec_path: string;
|
|
51
|
+
series_path: string;
|
|
52
|
+
html_path: string;
|
|
53
|
+
warnings: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface ChartPublisher {
|
|
56
|
+
publish(input: {
|
|
57
|
+
chartId: string;
|
|
58
|
+
filePath: string;
|
|
59
|
+
}): string | null;
|
|
60
|
+
}
|
|
61
|
+
export interface RenderChartInput {
|
|
62
|
+
store: WorkspaceDatasetStore;
|
|
63
|
+
sourceTokens?: ChartSourceTokenStore;
|
|
64
|
+
artifacts?: ChartArtifactRegistry;
|
|
65
|
+
/**
|
|
66
|
+
* 惰性解析 host 平面图表服务(`@v587d/capital-charts` 提供)。
|
|
67
|
+
*
|
|
68
|
+
* 每次画图时解析,而不是在 `apply` 时解析一次:host 平面行与本 preset 行的挂载先后
|
|
69
|
+
* 不由我们决定,一次性解析会把"服务稍后才可用"永久固化成 `chart_url=null`。
|
|
70
|
+
* 服务始终缺席(未安装/无 Web 载体/坏掉)时返回 undefined,整条链路照常降级:产物照样
|
|
71
|
+
* 落盘、`html_path` 照样可 present,只是 `chart_url` 为 null。
|
|
72
|
+
*/
|
|
73
|
+
charts?: () => ChartPublisher | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* 惰性解析"图表 → 本轮交付"发布器(官方 `deliverables/presented`)。
|
|
76
|
+
*
|
|
77
|
+
* 与 `charts` 同理每次出图解析一次:宿主平面的 `sessions` / `sessionProjections`
|
|
78
|
+
* 不可用时返回 undefined,整条链路照常降级——图仍在 workspace 里,回执照样给 html_path,
|
|
79
|
+
* 只是本轮交付行不会登记这张图。
|
|
80
|
+
*/
|
|
81
|
+
chartEvents?: () => ChartEventPublisher | undefined;
|
|
82
|
+
now?: () => number;
|
|
83
|
+
}
|
|
84
|
+
export declare function renderChart(input: RenderChartInput, args: Record<string, unknown>, exec: ToolExecLike): Promise<ChartReceipt>;
|
|
85
|
+
export declare function registerChartTool(ctx: Context, options: RenderChartInput): void;
|
|
86
|
+
export {};
|