@v587d/capital-generation 2.9.1 → 2.10.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 +73 -0
- package/README.md +6 -3
- package/chart-ui/README.md +3 -3
- package/chart-ui/package.json +1 -1
- package/config/package.json +1 -1
- package/lib/agents/bash-guard.d.ts +10 -6
- package/lib/agents/bash-guard.js +15 -12
- package/lib/agents/root-tool-policy.js +2 -0
- package/lib/agents/settled-wake.d.ts +43 -0
- package/lib/agents/settled-wake.js +81 -0
- package/lib/agents/wait-for-agents.d.ts +79 -0
- package/lib/agents/wait-for-agents.js +256 -0
- package/lib/chart/artifact-ref.d.ts +4 -1
- package/lib/chart/artifact-ref.js +2 -1
- package/lib/chart/delivery.d.ts +80 -0
- package/lib/chart/delivery.js +157 -0
- package/lib/chart/events.d.ts +3 -1
- package/lib/chart/events.js +52 -1
- package/lib/chart/html.js +16 -0
- package/lib/chart/present-tool.d.ts +8 -0
- package/lib/chart/present-tool.js +92 -22
- package/lib/chart/tool.js +63 -9
- package/lib/data-collector/dataset-tools.js +1 -1
- package/lib/data-collector/store.d.ts +1 -0
- package/lib/data-collector/store.js +2 -2
- package/lib/orchestration-entry.d.ts +11 -0
- package/lib/orchestration-entry.js +6 -0
- package/lib/present-entry.d.ts +13 -3
- package/lib/present-entry.js +123 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +8 -2
- package/preset/capital-generation/README.md +34 -16
- package/preset/capital-generation/agent.patch.yml +30 -34
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +3 -2
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +9 -4
- package/preset/capital-generation/skills/capital-data-query/SKILL.md +47 -19
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +5 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +12 -2
- package/watchlist/package.json +1 -1
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import type { ChartArtifactRegistry } from './artifact-ref.js';
|
|
12
12
|
import { type ChartEventPublisher } from './events.js';
|
|
13
|
+
import type { ChartDelivery } from './delivery.js';
|
|
14
|
+
import type { SessionLike } from '../data-collector/store.js';
|
|
15
|
+
/** One card can carry many files (the client collapses past four); the cap only bounds one event. */
|
|
16
|
+
export declare const MAX_PRESENT_CHARTS = 32;
|
|
13
17
|
export interface PresentChartsInput {
|
|
14
18
|
artifacts: ChartArtifactRegistry;
|
|
15
19
|
/** Lazy: the host projection may mount after this row; an absent publisher fails the call loudly. */
|
|
16
20
|
chartEvents: () => ChartEventPublisher | undefined;
|
|
21
|
+
/** Records the selection; the closing stream chunk writes it (normal path). */
|
|
22
|
+
delivery?: ChartDelivery;
|
|
23
|
+
/** Fallback-only guard when the stream listener is unavailable. */
|
|
24
|
+
readiness?: (session: SessionLike) => Promise<string | undefined>;
|
|
17
25
|
}
|
|
18
26
|
export interface PresentChartsReceipt {
|
|
19
27
|
presented: Array<{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MAX_CHARTS_PER_TURN } from './events.js';
|
|
2
2
|
import { callerSession } from '../tool-exec.js';
|
|
3
|
+
/** One card can carry many files (the client collapses past four); the cap only bounds one event. */
|
|
4
|
+
export const MAX_PRESENT_CHARTS = 32;
|
|
3
5
|
const jsonObject = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
4
6
|
const parameters = jsonObject({
|
|
5
7
|
chart_refs: {
|
|
@@ -41,8 +43,8 @@ function normalizeRefs(value) {
|
|
|
41
43
|
if (!refs.includes(ref))
|
|
42
44
|
refs.push(ref);
|
|
43
45
|
}
|
|
44
|
-
if (refs.length >
|
|
45
|
-
throw new Error(`present_charts 一次最多登记 ${
|
|
46
|
+
if (refs.length > MAX_PRESENT_CHARTS) {
|
|
47
|
+
throw new Error(`present_charts 一次最多登记 ${MAX_PRESENT_CHARTS} 张图(收到 ${refs.length} 个 chart_ref)`);
|
|
46
48
|
}
|
|
47
49
|
return refs;
|
|
48
50
|
}
|
|
@@ -53,6 +55,52 @@ function normalizeTaskId(value) {
|
|
|
53
55
|
throw new Error('present_charts 的 task_id 必须是非空字符串');
|
|
54
56
|
return value.trim();
|
|
55
57
|
}
|
|
58
|
+
const TITLE_SUBJECT_CODE = /(?<!\d)(\d{6})(?!\d)/g;
|
|
59
|
+
function titleSubjectCodes(title) {
|
|
60
|
+
return [...title.matchAll(TITLE_SUBJECT_CODE)].map((match) => match[1]);
|
|
61
|
+
}
|
|
62
|
+
/** Strip six-digit codes and punctuation so “同花顺 300033 日K” and “同花顺 300803 日K” share a key. */
|
|
63
|
+
function titleDedupeKey(title) {
|
|
64
|
+
return title
|
|
65
|
+
.normalize('NFKC')
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(TITLE_SUBJECT_CODE, '')
|
|
68
|
+
.replace(/[\s\p{P}\p{S}]+/gu, '');
|
|
69
|
+
}
|
|
70
|
+
function artifactQualityFailure(artifact) {
|
|
71
|
+
const codes = titleSubjectCodes(artifact.title);
|
|
72
|
+
if (codes.length === 0 || artifact.subject_code === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
const mismatched = [...new Set(codes.filter((code) => code !== artifact.subject_code))];
|
|
75
|
+
if (mismatched.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
return `标题里的股票代码 ${mismatched.join('、')} 与 dataset 对应的 ${artifact.subject_code} 不一致;请改正标题后重绘,未登记这张图`;
|
|
78
|
+
}
|
|
79
|
+
function dedupeArtifacts(artifacts) {
|
|
80
|
+
const kept = [];
|
|
81
|
+
const failed = [];
|
|
82
|
+
const seen = new Map();
|
|
83
|
+
for (const artifact of artifacts) {
|
|
84
|
+
const qualityFailure = artifactQualityFailure(artifact);
|
|
85
|
+
if (qualityFailure !== undefined) {
|
|
86
|
+
failed.push({ chart_ref: artifact.chart_ref, reason: qualityFailure });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const titleKey = titleDedupeKey(artifact.title);
|
|
90
|
+
const key = `${artifact.dataset_id ?? 'no-dataset'}|${artifact.kind}|${titleKey.length > 0 ? titleKey : artifact.chart_ref}`;
|
|
91
|
+
const previous = seen.get(key);
|
|
92
|
+
if (previous !== undefined) {
|
|
93
|
+
failed.push({
|
|
94
|
+
chart_ref: artifact.chart_ref,
|
|
95
|
+
reason: `重复图表:与 ${previous.chart_ref} 的 dataset/kind/标题相同;只登记第一张`,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seen.set(key, artifact);
|
|
100
|
+
kept.push(artifact);
|
|
101
|
+
}
|
|
102
|
+
return { kept, failed };
|
|
103
|
+
}
|
|
56
104
|
/** Present refs first; otherwise present the whole task so compaction cannot strand the charts. */
|
|
57
105
|
function selectArtifacts(input, session, refs, taskId) {
|
|
58
106
|
const failed = [];
|
|
@@ -75,9 +123,9 @@ function selectArtifacts(input, session, refs, taskId) {
|
|
|
75
123
|
const all = input.artifacts.list({ session, task_id: taskId });
|
|
76
124
|
if (all.length === 0)
|
|
77
125
|
failed.push({ chart_ref: '(task)', reason: `本会话该 task_id 没有可登记的图表:${taskId}` });
|
|
78
|
-
const selected = all.slice(0,
|
|
79
|
-
if (all.length >
|
|
80
|
-
failed.push({ chart_ref: '(task)', reason: `该 task_id 有 ${all.length}
|
|
126
|
+
const selected = all.slice(0, MAX_PRESENT_CHARTS);
|
|
127
|
+
if (all.length > MAX_PRESENT_CHARTS) {
|
|
128
|
+
failed.push({ chart_ref: '(task)', reason: `该 task_id 有 ${all.length} 张图,单次最多登记 ${MAX_PRESENT_CHARTS} 张;其余未展示` });
|
|
81
129
|
}
|
|
82
130
|
return { selected, failed };
|
|
83
131
|
}
|
|
@@ -87,10 +135,11 @@ export function registerPresentChartsTool(ctx, input) {
|
|
|
87
135
|
return;
|
|
88
136
|
const definition = {
|
|
89
137
|
name: 'present_charts',
|
|
90
|
-
description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented
|
|
91
|
-
+ '
|
|
92
|
-
+ '
|
|
93
|
-
+ '
|
|
138
|
+
description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 HTML(文件名带代码/标题或图形类型,如 `300803-kline-<短id>.html`)。'
|
|
139
|
+
+ '**由主 Agent 调用一次**:参数是 data_junior 回传的 chart_refs(一次传入,最多 32 个);refs 已丢失时改传 task_id 登记该任务的全部图表。'
|
|
140
|
+
+ '登记前会按 dataset + kind + 标题去重,并校验标题里的 6 位股票代码与 dataset 一致;重复或错标题的 ref 会进 failed,不会出现在交付卡片里。'
|
|
141
|
+
+ '这里只记录本轮要交付的图,宿主会在**最终答复之前**统一写入交付行——提前调用不会落进中间轮,不必等“最后一刻”,也不要在过程轮反复调用。'
|
|
142
|
+
+ '不要传文件路径或 HTML(路径由宿主解析);不调用时宿主仍会在收尾时自动登记本 exchange 已生成的图表。',
|
|
94
143
|
parameters,
|
|
95
144
|
async execute(args, exec) {
|
|
96
145
|
const session = callerSession(exec);
|
|
@@ -99,23 +148,44 @@ export function registerPresentChartsTool(ctx, input) {
|
|
|
99
148
|
if (refs.length === 0 && taskId === undefined) {
|
|
100
149
|
throw new Error('present_charts 需要 chart_refs 或 task_id:两者都不给就无法确定要登记哪几张图');
|
|
101
150
|
}
|
|
151
|
+
const { selected: selectedRaw, failed } = selectArtifacts(input, session, refs, taskId);
|
|
152
|
+
const { kept: selected, failed: qualityFailed } = dedupeArtifacts(selectedRaw);
|
|
153
|
+
failed.push(...qualityFailed);
|
|
154
|
+
const receipt = { presented: [], failed };
|
|
155
|
+
if (selected.length === 0)
|
|
156
|
+
return receipt;
|
|
157
|
+
const inputs = selected.map((artifact) => ({
|
|
158
|
+
ownerSessionId: session.id,
|
|
159
|
+
chart_id: artifact.chart_id,
|
|
160
|
+
title: artifact.title,
|
|
161
|
+
html_path: artifact.html_path,
|
|
162
|
+
}));
|
|
163
|
+
// Normal path: remember the selection; the closing stream chunk writes it before the answer.
|
|
164
|
+
if (input.delivery?.streaming() === true) {
|
|
165
|
+
input.delivery.request(session, inputs);
|
|
166
|
+
for (const artifact of selected)
|
|
167
|
+
receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
|
|
168
|
+
return receipt;
|
|
169
|
+
}
|
|
170
|
+
// Fallback path (stream listener unavailable): write now, but only when the turn is idle.
|
|
171
|
+
const blocked = await input.readiness?.(session);
|
|
172
|
+
if (blocked !== undefined)
|
|
173
|
+
throw new Error(blocked);
|
|
102
174
|
const publisher = input.chartEvents();
|
|
103
175
|
if (publisher === undefined) {
|
|
104
|
-
throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开
|
|
176
|
+
throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 html_path 指向的 HTML');
|
|
105
177
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
chart_id: artifact.chart_id,
|
|
112
|
-
title: artifact.title,
|
|
113
|
-
html_path: artifact.html_path,
|
|
114
|
-
});
|
|
115
|
-
if (accepted)
|
|
178
|
+
const accepted = publisher.publishMany !== undefined
|
|
179
|
+
? publisher.publishMany(session.id, inputs)
|
|
180
|
+
: inputs.every((item) => publisher.publish(item));
|
|
181
|
+
if (accepted) {
|
|
182
|
+
for (const artifact of selected)
|
|
116
183
|
receipt.presented.push({ chart_ref: artifact.chart_ref, title: artifact.title });
|
|
117
|
-
|
|
118
|
-
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
for (const artifact of selected) {
|
|
187
|
+
receipt.failed.push({ chart_ref: artifact.chart_ref, reason: `本轮登记已达 ${MAX_CHARTS_PER_TURN} 次上限或回合已关闭,未写入交付行` });
|
|
188
|
+
}
|
|
119
189
|
}
|
|
120
190
|
return receipt;
|
|
121
191
|
},
|
package/lib/chart/tool.js
CHANGED
|
@@ -16,6 +16,49 @@ function newChartId() {
|
|
|
16
16
|
const suffix = cryptoLike?.randomUUID ? cryptoLike.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
17
17
|
return `ch_${suffix}`;
|
|
18
18
|
}
|
|
19
|
+
const SUBJECT_KEYS = ['thscode', 'ts_code', 'ticker', 'symbol', 'stock_code', 'sec_code', 'code'];
|
|
20
|
+
/** Six-digit A-share/HK/US ticker; `thscode` wins over generic API status fields such as `code`. */
|
|
21
|
+
function subjectCodeOf(value) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
return null;
|
|
24
|
+
const record = value;
|
|
25
|
+
for (const key of SUBJECT_KEYS) {
|
|
26
|
+
const raw = record[key];
|
|
27
|
+
if (typeof raw === 'string') {
|
|
28
|
+
const match = /(?<!\d)(\d{6})(?!\d)/.exec(raw);
|
|
29
|
+
if (match)
|
|
30
|
+
return match[1];
|
|
31
|
+
}
|
|
32
|
+
if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 100_000 && raw <= 999_999) {
|
|
33
|
+
return String(raw);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function chartSubjectCode(envelope, rows) {
|
|
39
|
+
const fromEnvelope = subjectCodeOf(envelope);
|
|
40
|
+
if (fromEnvelope !== null)
|
|
41
|
+
return fromEnvelope;
|
|
42
|
+
for (const row of rows.slice(0, 3)) {
|
|
43
|
+
const fromRow = subjectCodeOf(row);
|
|
44
|
+
if (fromRow !== null)
|
|
45
|
+
return fromRow;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** Readable, collision-resistant filename: ASCII title slug + short chart id. The store's artifact
|
|
50
|
+
* name gate is deliberately ASCII, so Chinese titles fall back to the chart kind (code stays when
|
|
51
|
+
* the title/envelope carries one). */
|
|
52
|
+
function chartHtmlName(title, kind, chartId, subjectCode) {
|
|
53
|
+
const kindSlug = kind === 'candlestick' ? 'kline' : kind === 'bar' ? 'ohlc' : kind === 'column' ? 'column' : kind === 'area' ? 'area' : 'line';
|
|
54
|
+
const words = title.normalize('NFKC').match(/[A-Za-z0-9]+/g) ?? [];
|
|
55
|
+
const parts = subjectCode !== null && !words.includes(subjectCode) ? [subjectCode, ...words] : [...words];
|
|
56
|
+
if (parts.length === 0 || (subjectCode !== null && parts.every((part) => part === subjectCode)))
|
|
57
|
+
parts.push(kindSlug);
|
|
58
|
+
const safe = parts.join('-').replace(/-+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '').slice(0, 48);
|
|
59
|
+
const suffix = chartId.replace(/^ch_/, '').slice(0, 8);
|
|
60
|
+
return `${safe.length > 0 ? safe : kindSlug}-${suffix}.html`;
|
|
61
|
+
}
|
|
19
62
|
function sessionOf(exec) {
|
|
20
63
|
const session = exec.agent?.session;
|
|
21
64
|
if (!session || typeof session.id !== 'string' || session.id.length === 0) {
|
|
@@ -125,18 +168,27 @@ export async function renderChart(input, args, exec) {
|
|
|
125
168
|
spec.title = args.title.trim().slice(0, 200);
|
|
126
169
|
// The token carries the direct data-agent session; the nested child never sees raw rows.
|
|
127
170
|
let rows;
|
|
171
|
+
let subjectCode;
|
|
128
172
|
let meta;
|
|
129
173
|
if (datasetId !== undefined) {
|
|
130
174
|
const readSession = tokenSource?.sourceSession ?? session;
|
|
131
|
-
const { ref, rows: datasetRows } = await input.store.readPresentationRows({ session: readSession, dataset_id: datasetId, signal: exec.signal });
|
|
175
|
+
const { ref, rows: datasetRows, envelope } = await input.store.readPresentationRows({ session: readSession, dataset_id: datasetId, signal: exec.signal });
|
|
132
176
|
rows = datasetRows;
|
|
133
|
-
|
|
177
|
+
subjectCode = chartSubjectCode(envelope, datasetRows);
|
|
178
|
+
meta = {
|
|
179
|
+
source_kind: 'dataset',
|
|
180
|
+
dataset_id: ref.dataset_id,
|
|
181
|
+
source_label: ref.source_label,
|
|
182
|
+
captured_at: ref.captured_at,
|
|
183
|
+
...(subjectCode === null ? {} : { subject_code: subjectCode }),
|
|
184
|
+
};
|
|
134
185
|
}
|
|
135
186
|
else {
|
|
136
187
|
const parsed = await input.store.readWorkspaceJson({ session, path: path, signal: exec.signal });
|
|
137
188
|
const extracted = extractRows(parsed);
|
|
138
189
|
rows = extracted.rows;
|
|
139
|
-
|
|
190
|
+
subjectCode = chartSubjectCode(parsed, extracted.rows);
|
|
191
|
+
meta = { source_kind: 'path', ...(subjectCode === null ? {} : { subject_code: subjectCode }) };
|
|
140
192
|
}
|
|
141
193
|
const chartId = newChartId();
|
|
142
194
|
const payload = buildChartPayload({ chart_id: chartId, spec, rows, meta });
|
|
@@ -144,13 +196,14 @@ export async function renderChart(input, args, exec) {
|
|
|
144
196
|
const generatedAt = now();
|
|
145
197
|
const html = buildStandaloneHtml({ payload, library, generatedAt });
|
|
146
198
|
const dir = `${CHART_ARTIFACT_DIR}/${chartId}`;
|
|
199
|
+
const htmlName = chartHtmlName(payload.title, payload.kind, chartId, subjectCode);
|
|
147
200
|
const written = await input.store.writeWorkspaceFiles({
|
|
148
201
|
session,
|
|
149
202
|
dir,
|
|
150
203
|
files: [
|
|
151
|
-
{ name: 'spec.json', content: `${JSON.stringify({ chart_id: chartId, created_at: generatedAt, source: { dataset_id: meta.dataset_id ?? null, path: path ?? null }, spec }, null, 2)}\n` },
|
|
204
|
+
{ name: 'spec.json', content: `${JSON.stringify({ chart_id: chartId, created_at: generatedAt, source: { dataset_id: meta.dataset_id ?? null, path: path ?? null, subject_code: subjectCode }, spec }, null, 2)}\n` },
|
|
152
205
|
{ name: 'series.json', content: `${JSON.stringify(payload)}\n` },
|
|
153
|
-
{ name:
|
|
206
|
+
{ name: htmlName, content: html },
|
|
154
207
|
],
|
|
155
208
|
signal: exec.signal,
|
|
156
209
|
});
|
|
@@ -175,13 +228,14 @@ export async function renderChart(input, args, exec) {
|
|
|
175
228
|
dataset_id: meta.dataset_id ?? null,
|
|
176
229
|
source_label: meta.source_label ?? null,
|
|
177
230
|
captured_at: meta.captured_at ?? null,
|
|
231
|
+
subject_code: subjectCode,
|
|
178
232
|
kind: payload.kind,
|
|
179
233
|
axis: payload.axis,
|
|
180
234
|
title: payload.title,
|
|
181
235
|
chart_url: chartUrl,
|
|
182
236
|
spec_path: pathOf('spec.json'),
|
|
183
237
|
series_path: pathOf('series.json'),
|
|
184
|
-
html_path: pathOf(
|
|
238
|
+
html_path: pathOf(htmlName),
|
|
185
239
|
created_at: generatedAt,
|
|
186
240
|
});
|
|
187
241
|
// Unit callers without the registry still get a stable receipt.
|
|
@@ -202,7 +256,7 @@ export async function renderChart(input, args, exec) {
|
|
|
202
256
|
ownerSessionId,
|
|
203
257
|
chart_id: chartId,
|
|
204
258
|
title: payload.title,
|
|
205
|
-
html_path: pathOf(
|
|
259
|
+
html_path: pathOf(htmlName),
|
|
206
260
|
});
|
|
207
261
|
deliverableTrace = `publish=${accepted ? '受理' : '拒绝'} ownerSessionId=${ownerSessionId} | ${publisher.lastTrace?.() ?? '(无轨迹)'}`;
|
|
208
262
|
}
|
|
@@ -242,7 +296,7 @@ export async function renderChart(input, args, exec) {
|
|
|
242
296
|
chart_url: chartUrl,
|
|
243
297
|
spec_path: pathOf('spec.json'),
|
|
244
298
|
series_path: pathOf('series.json'),
|
|
245
|
-
html_path: pathOf(
|
|
299
|
+
html_path: pathOf(htmlName),
|
|
246
300
|
warnings: payload.meta.warnings,
|
|
247
301
|
};
|
|
248
302
|
}
|
|
@@ -252,7 +306,7 @@ export function registerChartTool(ctx, options) {
|
|
|
252
306
|
return;
|
|
253
307
|
const definition = {
|
|
254
308
|
name: 'render_chart',
|
|
255
|
-
description: `把已经取到的数据渲染成一张可交互图,产物落在 workspace 的 ${CHART_ARTIFACT_DIR}/<chart_id>/(
|
|
309
|
+
description: `把已经取到的数据渲染成一张可交互图,产物落在 workspace 的 ${CHART_ARTIFACT_DIR}/<chart_id>/(HTML 以标题/代码命名,如「600519-kline-<短id>.html」,可离线打开)。` +
|
|
256
310
|
'**只回一条小回执,不回任何原始数据行**:序列走旁路,不进入上下文。' +
|
|
257
311
|
'来源三选一:dataset_id(已授权的 DatasetRef)、path(workspace 相对路径的 JSON,支持顶层数组或含行数组的 envelope),或 chart_source_ref(宿主签发给 visualization_specialist 的短期 token;使用它时还要提供 task_id)。' +
|
|
258
312
|
'spec 是自由对象,至少给出 kind(line / area / column / candlestick / bar;后两者要 ohlc 四字段与真实时间列)与字段选择(series 或 ohlc),' +
|
|
@@ -364,7 +364,7 @@ export function registerDatasetTools(ctx, store, options = {}) {
|
|
|
364
364
|
withIsoTimeColumns(result, prepared.axis);
|
|
365
365
|
return result;
|
|
366
366
|
}),
|
|
367
|
-
tool('describe_dataset', `读取一份 Dataset 的**默认入口**:一次调用完成 inspect(元数据与可读性/形状)、profile(宿主算基础质量与四类事实,写入 profile_ref)与受控 query(可选);原始 rows 不进入模型上下文。一次只处理一个 dataset_id
|
|
367
|
+
tool('describe_dataset', `读取一份 Dataset 的**默认入口**:一次调用完成 inspect(元数据与可读性/形状)、profile(宿主算基础质量与四类事实,写入 profile_ref)与受控 query(可选);原始 rows 不进入模型上下文。一次只处理一个 dataset_id——本轮有多份数据时请并发调用本工具(PTC 在同一个 run_code 程序里用 Promise.all,native 在同一条消息里一次发出多个调用),不要一份一份等。行集合返回:① 元数据 artifact_ref/capability/source_label/captured_at/retention_until 与 query_access.shape(array / envelope_item = 行集合;document = 顶层文档对象,只能 profile,queries 由宿主跳过);② profile 四类事实——statistics(count/sum/min/max/mean/分位数)、categories(distinct + 最多 5 个高频取值)、time_facts(覆盖范围与首行/末行取值;回答"最新值、区间涨跌"必须用它,不要用 min/max 代替)、structure(嵌套路径摘要);③ queries——每条的 QueryResult,或该条自己的 error(单条失败不影响其余)。columns_of_interest 只对这些列返回 statistics/categories/schema(并同样收窄 time_facts 的首末值投影,时间列永远保留),是收窄结果体积的唯一手段。结果超过 ${SAFE_RESULT_CHARS} 码点时宿主不返回超长载荷,而返回 {status:'too_large', profile_ref, columns, bytes, facts_bytes, queries_bytes, hint}:facts_bytes/queries_bytes 是体积归因(哪边大砍哪边),按 hint 重发一次即可——queries 大就先去掉 queries(聚合改单独走 query_dataset),facts 大就用 columns_of_interest 收窄列(或减少 queries,最多 ${MAX_DESCRIBE_QUERIES} 条)。单点复核仍可用 inspect_dataset / profile_dataset / query_dataset;不接受任何文件路径、脚本内容或自定义表达式。`, describeParametersSchema, describeResultSchema, async (args, exec) => describeDataset({
|
|
368
368
|
store,
|
|
369
369
|
session: authorizedSession(ctx, exec, 'describe_dataset', DATASET_READING_PRESET_IDS),
|
|
370
370
|
request: normalizeDescribeArgs(args),
|
|
@@ -353,6 +353,7 @@ export declare class WorkspaceDatasetStore {
|
|
|
353
353
|
}): Promise<{
|
|
354
354
|
ref: DatasetRef;
|
|
355
355
|
rows: unknown[];
|
|
356
|
+
envelope?: Record<string, unknown>;
|
|
356
357
|
}>;
|
|
357
358
|
/**
|
|
358
359
|
* Host-internal workspace text read; with readWorkspaceBytes it shares containment and size
|
|
@@ -332,7 +332,7 @@ export class WorkspaceDatasetStore {
|
|
|
332
332
|
if (raw.rows.length > maxRows) {
|
|
333
333
|
throw new DatasetStoreError('dataset_too_large', `Dataset has ${raw.rows.length} rows; presentation reads are capped at ${maxRows}`);
|
|
334
334
|
}
|
|
335
|
-
return { ref, rows: raw.rows };
|
|
335
|
+
return { ref, rows: raw.rows, ...(raw.envelope === undefined ? {} : { envelope: raw.envelope }) };
|
|
336
336
|
}
|
|
337
337
|
/**
|
|
338
338
|
* Host-internal workspace text read; with readWorkspaceBytes it shares containment and size
|
|
@@ -500,7 +500,7 @@ export class WorkspaceDatasetStore {
|
|
|
500
500
|
if (isRecord(value)) {
|
|
501
501
|
const rows = rowsAtPath(value, rowKey ?? 'item');
|
|
502
502
|
if (rows !== undefined)
|
|
503
|
-
return { kind: 'rows', rows, shape: 'envelope_item' };
|
|
503
|
+
return { kind: 'rows', rows, shape: 'envelope_item', envelope: value };
|
|
504
504
|
}
|
|
505
505
|
if (isRecord(value))
|
|
506
506
|
return { kind: 'document', value };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tools/orchestration` row: the root-only `wait_for_agents` tool.
|
|
3
|
+
*
|
|
4
|
+
* Leaves don't mount this row: a child has no delegated tree to wait for and must use send_message
|
|
5
|
+
* to report upward. The tool also rejects the subagent origin at call time, so a mis-mounted row
|
|
6
|
+
* cannot turn into a child-side polling loop.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
export declare const name = "capital-orchestration-tools";
|
|
10
|
+
export declare const inject: string[];
|
|
11
|
+
export declare function apply(ctx: Context): void;
|
package/lib/present-entry.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tools/present` row: present_charts
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `tools/present` row: present_charts + the closing-answer delivery listener.
|
|
3
|
+
*
|
|
4
|
+
* The model only records which charts belong to the answer. The actual first-party
|
|
5
|
+
* `deliverables/presented` append happens in the terminal assistant-stream chunk, before the closing
|
|
6
|
+
* message commits, gated on an idle root (no actionable inbox input, no running direct children). That is the only
|
|
7
|
+
* point where "the final answer" is knowable, so early/no `present_charts` calls cannot strand the
|
|
8
|
+
* card in a folded process turn (rationale: docs/dev/chart-delivery-events.md §6.3).
|
|
5
9
|
*/
|
|
6
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { SessionLike } from './data-collector/store.js';
|
|
7
12
|
export declare const name = "capital-present-tools";
|
|
8
13
|
export declare const inject: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
|
|
16
|
+
* pending turn. The normal path defers to the closing stream chunk and does not consult this.
|
|
17
|
+
*/
|
|
18
|
+
export declare function chartDeliveryBlocker(ctx: Context, session: SessionLike): Promise<string | undefined>;
|
|
9
19
|
export declare function apply(ctx: Context): void;
|
package/lib/present-entry.js
CHANGED
|
@@ -1,13 +1,135 @@
|
|
|
1
|
+
import { createChartDelivery } from './chart/delivery.js';
|
|
1
2
|
import { createChartEventPublisher } from './chart/events.js';
|
|
2
3
|
import { chartEventContextOf } from './chart/event-context.js';
|
|
3
4
|
import { registerPresentChartsTool } from './chart/present-tool.js';
|
|
5
|
+
import { createSettledWakeGuard } from './agents/settled-wake.js';
|
|
4
6
|
import { capitalSharedRuntimeOf } from './chart-source-entry.js';
|
|
5
7
|
export const name = 'capital-present-tools';
|
|
6
8
|
export const inject = ['tools'];
|
|
9
|
+
/**
|
|
10
|
+
* Fallback only: when the stream listener never fires, immediate delivery still has to avoid a
|
|
11
|
+
* pending turn. The normal path defers to the closing stream chunk and does not consult this.
|
|
12
|
+
*/
|
|
13
|
+
export async function chartDeliveryBlocker(ctx, session) {
|
|
14
|
+
const projections = ctx.get('sessionProjections');
|
|
15
|
+
try {
|
|
16
|
+
const inbox = projections?.stateOf(session, 'inbox');
|
|
17
|
+
// A settlement notice can race the closing answer; it must not block the card. Any relay or
|
|
18
|
+
// human input still means another model step is pending.
|
|
19
|
+
const pending = [...(inbox?.['next-turn'] ?? []), ...(inbox?.['next-step'] ?? [])]
|
|
20
|
+
.filter((message) => {
|
|
21
|
+
const source = message?.source;
|
|
22
|
+
return source?.kind !== 'subagent-settled';
|
|
23
|
+
}).length;
|
|
24
|
+
if (pending > 0) {
|
|
25
|
+
return `还有 ${pending} 条待处理消息(子 Agent 回传 / 结算):现在登记会让卡片留在被折叠的中间轮。`
|
|
26
|
+
+ '等这些消息处理完、写最终答复的那一步再调用 present_charts(refs 记不清就传 task_id)。';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Projection unavailable: delivery availability matters more than the guard.
|
|
31
|
+
}
|
|
32
|
+
const subagents = ctx.get('subagents');
|
|
33
|
+
if (typeof subagents?.listDescendants === 'function') {
|
|
34
|
+
try {
|
|
35
|
+
const rows = await subagents.listDescendants(session.id);
|
|
36
|
+
const running = rows.filter((row) => row?.kind === 'child' && row?.activity === 'running').length;
|
|
37
|
+
if (running > 0) {
|
|
38
|
+
return `还有 ${running} 个子 Agent 在运行:现在登记会让卡片留在被折叠的中间轮。`
|
|
39
|
+
+ '等它们全部结算、写最终答复的那一步再调用 present_charts(refs 记不清就传 task_id)。';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Subagent service unavailable: don't block delivery for a missing optional check.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function inboxOf(ctx) {
|
|
49
|
+
const projections = ctx.get('sessionProjections');
|
|
50
|
+
return (session) => {
|
|
51
|
+
try {
|
|
52
|
+
return projections?.stateOf(session, 'inbox');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
7
59
|
export function apply(ctx) {
|
|
8
60
|
const shared = capitalSharedRuntimeOf(ctx);
|
|
61
|
+
const publisherOf = () => createChartEventPublisher(chartEventContextOf(ctx));
|
|
62
|
+
const eventCtx = ctx;
|
|
63
|
+
const wakeGuard = createSettledWakeGuard();
|
|
64
|
+
const delivery = createChartDelivery({
|
|
65
|
+
artifacts: shared.artifacts,
|
|
66
|
+
publish: (sessionId, inputs) => publisherOf()?.publishMany(sessionId, inputs) ?? false,
|
|
67
|
+
inboxOf: inboxOf(ctx),
|
|
68
|
+
onClosing: (_session, agent) => wakeGuard.acceptClosing(agent),
|
|
69
|
+
});
|
|
70
|
+
// The preset row is applied in the root agent's scope: these events are the root's own.
|
|
71
|
+
const disposers = [];
|
|
72
|
+
if (typeof eventCtx.on === 'function') {
|
|
73
|
+
disposers.push(eventCtx.on('agent/assistant-stream', (...args) => {
|
|
74
|
+
const payload = args[0];
|
|
75
|
+
if (payload?.frame === undefined)
|
|
76
|
+
return;
|
|
77
|
+
delivery.acceptStream(payload.agent?.session, payload.frame, payload.agent);
|
|
78
|
+
}));
|
|
79
|
+
disposers.push(eventCtx.on('subagent/start', (...args) => {
|
|
80
|
+
const info = args[0];
|
|
81
|
+
delivery.acceptSubagentStart(info ?? {});
|
|
82
|
+
wakeGuard.acceptSubagentStart(info ?? {});
|
|
83
|
+
}));
|
|
84
|
+
disposers.push(eventCtx.on('subagent/end', (...args) => delivery.acceptSubagentEnd(args[0])));
|
|
85
|
+
// Scoped to the root agent: only the root inbox is inspected. A settled notice inserted after
|
|
86
|
+
// the closing frame would otherwise make the loop open a step for a redundant "done" message.
|
|
87
|
+
disposers.push(eventCtx.on('agent/inbox/inserted', (...args) => {
|
|
88
|
+
const payload = args[0];
|
|
89
|
+
if (payload?.agent === undefined || payload.message === undefined)
|
|
90
|
+
return;
|
|
91
|
+
wakeGuard.acceptInboxInserted(payload.agent, payload.message);
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
// Session events are not agent-scoped: listen on the unscoped root and filter by payload.
|
|
95
|
+
const listen = (eventCtx.root ?? ctx);
|
|
96
|
+
if (typeof listen.on === 'function') {
|
|
97
|
+
disposers.push(listen.on('session/event', (...args) => {
|
|
98
|
+
const session = args[0];
|
|
99
|
+
const event = args[1];
|
|
100
|
+
if (event?.type !== 'user/message')
|
|
101
|
+
return;
|
|
102
|
+
const source = event.data?.source;
|
|
103
|
+
// The settlement notice is authoritative even when the lifecycle edge was missed; clearing
|
|
104
|
+
// here keeps the stream-time idle check from waiting on a child that is already done.
|
|
105
|
+
if (source?.kind === 'subagent-settled') {
|
|
106
|
+
if (typeof source.senderSessionId === 'string')
|
|
107
|
+
delivery.acceptSubagentEnd({ id: source.senderSessionId });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (typeof session?.id !== 'string' || source === undefined)
|
|
111
|
+
return;
|
|
112
|
+
if (source.kind === 'user') {
|
|
113
|
+
delivery.acceptUserMessage(session.id, typeof event.time === 'number' ? event.time : Date.now());
|
|
114
|
+
wakeGuard.acceptUserMessage(session, source);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// `user/message` is committed only when admitted, so this is exactly the "child already
|
|
118
|
+
// reported its result" marker the closing guard needs.
|
|
119
|
+
if (source.kind === 'agent-message')
|
|
120
|
+
wakeGuard.acceptUserMessage(session, source);
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
ctx.effect(() => () => {
|
|
124
|
+
for (const dispose of disposers) {
|
|
125
|
+
if (typeof dispose === 'function')
|
|
126
|
+
dispose();
|
|
127
|
+
}
|
|
128
|
+
}, 'capital-generation.chart-delivery()');
|
|
9
129
|
registerPresentChartsTool(ctx, {
|
|
10
130
|
artifacts: shared.artifacts,
|
|
11
|
-
chartEvents:
|
|
131
|
+
chartEvents: publisherOf,
|
|
132
|
+
readiness: (session) => chartDeliveryBlocker(ctx, session),
|
|
133
|
+
delivery,
|
|
12
134
|
});
|
|
13
135
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Known limit: DNS resolves per request but the IP is not pinned to the connection,
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
|
-
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.
|
|
6
|
+
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.10.0";
|
|
7
7
|
export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
8
8
|
export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
|
|
9
9
|
export declare class LocalFetchError extends Error {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* so a rebinding window remains (same trade-off as dsh-search-first).
|
|
5
5
|
*/
|
|
6
6
|
import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
|
|
7
|
-
export const LOCAL_FETCH_CLIENT_VERSION = '2.
|
|
7
|
+
export const LOCAL_FETCH_CLIENT_VERSION = '2.10.0';
|
|
8
8
|
export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
|
|
9
9
|
export class LocalFetchError extends Error {
|
|
10
10
|
code;
|
|
@@ -35,7 +35,7 @@ export interface WindCallResult {
|
|
|
35
35
|
code?: WindErrorCode;
|
|
36
36
|
}
|
|
37
37
|
/** Must match `package.json` version; a test pins the two together. */
|
|
38
|
-
export declare const WIND_CLIENT_VERSION = "2.
|
|
38
|
+
export declare const WIND_CLIENT_VERSION = "2.10.0";
|
|
39
39
|
export interface WindClient {
|
|
40
40
|
callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
|
|
41
41
|
}
|
|
@@ -19,7 +19,7 @@ export const WIND_SERVER_ENDPOINTS = {
|
|
|
19
19
|
analytics_data: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
|
|
20
20
|
};
|
|
21
21
|
/** Must match `package.json` version; a test pins the two together. */
|
|
22
|
-
export const WIND_CLIENT_VERSION = '2.
|
|
22
|
+
export const WIND_CLIENT_VERSION = '2.10.0';
|
|
23
23
|
function errorText(error) {
|
|
24
24
|
return error instanceof Error ? error.message : String(error);
|
|
25
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v587d/capital-generation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Capital Generation:面向中国散户的 AI 投资助手插件(自动模式 Agent Preset)",
|
|
5
5
|
"icon": "assets/cg-icon.png",
|
|
6
6
|
"author": "v587d",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"import": "./lib/ocr-entry.js",
|
|
58
58
|
"default": "./lib/ocr-entry.js"
|
|
59
59
|
},
|
|
60
|
+
"./tools/orchestration": {
|
|
61
|
+
"types": "./lib/orchestration-entry.d.ts",
|
|
62
|
+
"import": "./lib/orchestration-entry.js",
|
|
63
|
+
"default": "./lib/orchestration-entry.js"
|
|
64
|
+
},
|
|
60
65
|
"./tools/present": {
|
|
61
66
|
"types": "./lib/present-entry.d.ts",
|
|
62
67
|
"import": "./lib/present-entry.js",
|
|
@@ -140,7 +145,8 @@
|
|
|
140
145
|
"contract:probe": "node scripts/contract-probe.mjs",
|
|
141
146
|
"docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
|
|
142
147
|
"verify:sessions": "node scripts/verify-sessions.mjs",
|
|
143
|
-
"audit:tool-errors": "node scripts/audit-tool-errors.mjs"
|
|
148
|
+
"audit:tool-errors": "node scripts/audit-tool-errors.mjs",
|
|
149
|
+
"ptc:report": "node scripts/ptc-session-report.mjs"
|
|
144
150
|
},
|
|
145
151
|
"peerDependencies": {
|
|
146
152
|
"@deepseek-ai/cordis": "^4.0.2",
|