@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,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表 → 对话流的宿主侧事件通道(**first-party 口径**)。
|
|
3
|
+
*
|
|
4
|
+
* 问题:图表由 `visualization_specialist`(主 Agent 的**孙** Agent)生成,它的 tool call
|
|
5
|
+
* 落在自己的会话里;用户在客户端看的是主会话,子会话的事件不会流过来。
|
|
6
|
+
*
|
|
7
|
+
* 做法:`render_chart` 成功后,宿主往**用户正在看的那条会话**追加官方
|
|
8
|
+
* `deliverables/presented` 事件(`present` 工具用的同一条通道),把
|
|
9
|
+
* `capital-analysis/charts/<chart_id>/chart.html` 登记为本轮交付物。用户在收尾的
|
|
10
|
+
* 「本轮文件改动 / 交付」行点开该文件,右侧即用官方 document preview 的
|
|
11
|
+
* script-enabled iframe 渲染自包含图表。
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ 为什么**绝不能**再发明自己的事件类型(2026-09-18 事故,实测根因):
|
|
14
|
+
* 会话日志的事件词汇表是**闭集**——`@deepseek-ai/dsh-session` 的
|
|
15
|
+
* `KNOWN_SESSION_EVENT_TYPES` 由 harness 仓库自己的 `SessionEventMap` 生成,
|
|
16
|
+
* 注释明写 "downstream (out-of-repo) plugin events are outside this list by
|
|
17
|
+
* construction"。读取侧 `validateStoredEvents()` 是 fail-closed 的:
|
|
18
|
+
*
|
|
19
|
+
* `KNOWN_SESSION_EVENT_TYPES.has(event.type) || event.ignorable === true`
|
|
20
|
+
*
|
|
21
|
+
* 否则抛 `SessionFormatUnsupportedError` 并拒绝解释**整份日志**。而 `Session.append`
|
|
22
|
+
* 的 options 只转发 `sourceEventSeqs` / `surfaceOp`——**调用方无法设置 `ignorable`**
|
|
23
|
+
* (实测 0.1.5-rc.2 与 0.1.6-alpha.2 都没有该选项)。于是自定义事件是"写时静默通过、
|
|
24
|
+
* 冷加载时整份会话打不开":本仓曾发 `capital/chart-rendered`,导致 6 份会话在重启后
|
|
25
|
+
* 全部 `failed to observe session`。只有 first-party 事件能持久化。
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ 回合归属(实测教训 2026-09-17):交付行是**按回合**渲染的,只会出现在它所属回合的
|
|
28
|
+
* 收尾位置。而主 Agent 在等子 Agent 时会**先结束自己的回合**,于是出图往往发生在两个回合
|
|
29
|
+
* 之间——此时 `turnBoundary.lastTurn` 指向那个**已经结束**的回合,照抄它会把交付登记到
|
|
30
|
+
* 上一轮。因此:
|
|
31
|
+
* - 有回合打开(`openTurnStartSeq` 是数字)→ 立即写入该回合;
|
|
32
|
+
* - 没有回合打开 → 先寄存,等 owner 的**下一个 `turn/start`** 再写入(那正是会消费这份
|
|
33
|
+
* 子 Agent 结果、写下最终答复的回合)。
|
|
34
|
+
* 寄存的冲刷必须延到微任务:`session/event` 观察者在 `Session.append` 的 appending 窗口内同步执行,
|
|
35
|
+
* 在那里再次 append 会被 "session append cannot reenter" 拒绝。
|
|
36
|
+
*
|
|
37
|
+
* 为什么这不"污染 transcripts":
|
|
38
|
+
* - `deliverables/presented` 不在 surface 事件表里,`deriveEventMessage()` 返回 null,
|
|
39
|
+
* 因此它**永不进入** `deriveMessages()`(模型看到的消息历史);
|
|
40
|
+
* - payload 只有 `turn` / `callId`(`capital-chart:<chart_id>` 句柄)/ `files[]`
|
|
41
|
+
* (工作区相对 `html_path` + 标题),没有 rows、series、HTML、绝对路径。
|
|
42
|
+
*
|
|
43
|
+
* 容错:`sessions` / `sessionProjections` 任一不可用、或 append 抛错,都只告警并跳过——
|
|
44
|
+
* 出图本身绝不因此失败(文件已落盘、回执照样给 html_path)。
|
|
45
|
+
*/
|
|
46
|
+
import { chartSessionScopeId } from './artifact-ref.js';
|
|
47
|
+
/**
|
|
48
|
+
* 登记本轮交付物用的官方事件名。
|
|
49
|
+
*
|
|
50
|
+
* 必须留在 `KNOWN_SESSION_EVENT_TYPES` 内——它是 `dsh-tool-present` 追加的同一类型,
|
|
51
|
+
* 因此在**任何** harness 版本上都能被读回。改这里等于改会话日志契约,
|
|
52
|
+
* `test/chart-turn-events.test.mjs` 有一条断言把它钉在 first-party 词汇表上。
|
|
53
|
+
*/
|
|
54
|
+
export const CHART_DELIVERABLE_EVENT = 'deliverables/presented';
|
|
55
|
+
/** `callId` 前缀:交付事件里携带的图表句柄(官方契约只要求非空字符串)。 */
|
|
56
|
+
export const CHART_CALL_ID_PREFIX = 'capital-chart:';
|
|
57
|
+
/** 同一 (session, turn) 的事件上限:防模型循环,超出只告警。 */
|
|
58
|
+
export const MAX_CHARTS_PER_TURN = 8;
|
|
59
|
+
/** 向上找根会话的最大跳数(main → data_junior → specialist 只有一跳,留余量)。 */
|
|
60
|
+
export const MAX_OWNER_HOPS = 4;
|
|
61
|
+
/**
|
|
62
|
+
* 最近一次 publish 的逐步轨迹(模块级:发布器每次出图都新建,轨迹要跨实例可读)。
|
|
63
|
+
*/
|
|
64
|
+
let lastPublishTrace = '';
|
|
65
|
+
/** 只读:最近一次 publish 轨迹。 */
|
|
66
|
+
export function chartEventPublishTrace() {
|
|
67
|
+
return lastPublishTrace;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 交付通道的**诊断开关**:`CAPITAL_CHART_TRACE=1` 时才输出进度日志与落盘 trace。
|
|
71
|
+
*
|
|
72
|
+
* 默认关闭——对外发布不该往用户终端刷进度、也不该往 workspace 写调试文件。
|
|
73
|
+
* 排查"图表/交付行不出现"时打开它(见 `deliverable-trace.txt` 与 docs/dev/chart-delivery-events.md §6.3 的说明)。
|
|
74
|
+
*/
|
|
75
|
+
export function chartTraceEnabled() {
|
|
76
|
+
try {
|
|
77
|
+
const env = globalThis.process?.env;
|
|
78
|
+
const value = env?.CAPITAL_CHART_TRACE;
|
|
79
|
+
return value === '1' || value === 'true';
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isAppendableSession(value) {
|
|
86
|
+
return Boolean(value)
|
|
87
|
+
&& typeof value.append === 'function'
|
|
88
|
+
&& typeof value.id === 'string';
|
|
89
|
+
}
|
|
90
|
+
function parentSessionOf(session) {
|
|
91
|
+
const parent = session.header?.parentSession;
|
|
92
|
+
return typeof parent === 'string' && parent.length > 0 ? parent : undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 从 owner scope 出发向上走到根会话:用户看的是根会话的对话流,卡片必须挂在它上面。
|
|
96
|
+
* 找不到(会话已回收)时返回 undefined,调用方静默跳过。
|
|
97
|
+
*/
|
|
98
|
+
export function resolveOwnerSession(sessions, ownerSessionId, maxHops = MAX_OWNER_HOPS) {
|
|
99
|
+
let current;
|
|
100
|
+
try {
|
|
101
|
+
const first = sessions.get(ownerSessionId);
|
|
102
|
+
current = isAppendableSession(first) ? first : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
if (current === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
for (let hop = 0; hop < maxHops; hop += 1) {
|
|
110
|
+
const parentId = parentSessionOf(current);
|
|
111
|
+
if (parentId === undefined)
|
|
112
|
+
break;
|
|
113
|
+
let parent;
|
|
114
|
+
try {
|
|
115
|
+
parent = sessions.get(parentId);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (!isAppendableSession(parent))
|
|
121
|
+
break;
|
|
122
|
+
current = parent;
|
|
123
|
+
}
|
|
124
|
+
return current;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 官方 `deliverables/presented` 载荷。
|
|
128
|
+
*
|
|
129
|
+
* 形状与 `dsh-tool-present` 逐字段对齐(那边是 `{ turn, callId, files }`,
|
|
130
|
+
* 客户端 `isPresentedData()` 要求 `turn` 是 >=1 的安全整数、`callId` 非空字符串、
|
|
131
|
+
* `files` 是数组)。`files[].path` 必须是**工作区相对**路径:官方打开动作会拿它去
|
|
132
|
+
* `workspaceFiles.stat({ sessionId, workspaceRoot: session.cwd })` 解析,绝对路径过不了
|
|
133
|
+
* 「Presented file has no verified Host path」这一关。
|
|
134
|
+
*
|
|
135
|
+
* `callId` 只是官方契约里的一个非空字符串句柄,没有配对约束(`present` 那边用来关联
|
|
136
|
+
* tool/result),这里借它携带 `chart_id`,便于在日志里把一条交付回溯到具体图表。
|
|
137
|
+
*/
|
|
138
|
+
export function buildChartEventPayload(input, turn) {
|
|
139
|
+
return {
|
|
140
|
+
turn,
|
|
141
|
+
callId: `${CHART_CALL_ID_PREFIX}${input.chart_id}`,
|
|
142
|
+
files: [
|
|
143
|
+
{
|
|
144
|
+
path: input.html_path,
|
|
145
|
+
description: input.title,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 待登记队列(**进程级**,不能放进发布器实例)。
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ 2026-09-20 真机事故的根因:`createChartEventPublisher()` 是在**每次 `render_chart`
|
|
154
|
+
* 时**惰性调用的(见 `src/index.ts` 的 `chartEvents: () => createChartEventPublisher(...)`),
|
|
155
|
+
* 所以每个实例都拿到一份**自己的** `pending`。于是:
|
|
156
|
+
* 1. 出图时回合是关的 → 图被寄存进**实例 A** 的 pending;
|
|
157
|
+
* 2. 稍后 owner 那一轮关闭 → 冲刷回调跑在**实例 B**(甚至没有实例)上;
|
|
158
|
+
* 3. B 的 pending 是空的 → 什么都没写 → 图**永久丢失**,且静默无报错。
|
|
159
|
+
* 旧代码之所以看不出这个问题,是因为"回合打开时立即 append"这条路径占多数,
|
|
160
|
+
* 只有"回合之间出图"(本项目的主 Agent 等子 Agent 时必然发生)才走寄存。
|
|
161
|
+
*
|
|
162
|
+
* 因此 pending 必须与进程内的服务实例同生命周期:模块级,并在注册冲刷监听时才清理。
|
|
163
|
+
*/
|
|
164
|
+
const pending = new Map();
|
|
165
|
+
/**
|
|
166
|
+
* 进程内待登记条目的总量上限(安全阀)。
|
|
167
|
+
*
|
|
168
|
+
* 正常流程下条目会在 owner 那一轮关闭时被冲刷清空;但如果冲刷时机始终没接上
|
|
169
|
+
* (上游事件名消失、监听注册失败且 turn/start 也没来),模块级 Map 会一直攒着。
|
|
170
|
+
* 这是纯内存元数据(只有 chart_id / 标题 / 相对路径),上限只为防无界增长,
|
|
171
|
+
* 与 `MAX_CHARTS_PER_TURN` 的"防模型循环"是两件事。
|
|
172
|
+
*/
|
|
173
|
+
export const MAX_PENDING_CHARTS = 64;
|
|
174
|
+
/**
|
|
175
|
+
* 只给测试用:清空待登记队列。
|
|
176
|
+
*
|
|
177
|
+
* `pending` 是模块级的(必须跨发布器实例存活,见下方说明),因此进程内所有测试
|
|
178
|
+
* 共享它。测试之间要隔离,就在用例开始时调用一次。
|
|
179
|
+
*/
|
|
180
|
+
export function resetChartEventPending() {
|
|
181
|
+
pending.clear();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 惰性构造发布器:`sessions` / `sessionProjections` 缺一不可(都是宿主平面服务,
|
|
185
|
+
* 在 preset scope 里正常可解析;缺失表示宿主组合不完整,此时静默降级)。
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* 进程级单例发布器。
|
|
189
|
+
*
|
|
190
|
+
* ⚠️ 2026-09-20 真机事故的**真根因**:`src/index.ts` 的 `chartEvents: () => createChartEventPublisher(...)`
|
|
191
|
+
* 是**每次 `render_chart` 都调用一次**的。若每次都新建发布器:
|
|
192
|
+
* 1. 实例 A 出图时把图寄存进自己的队列,并注册了**绑定在 A 上**的冲刷监听;
|
|
193
|
+
* 2. 下一次出图再建实例 B(又注册一套监听);
|
|
194
|
+
* 3. 冲刷回调里 `flush` 闭包引用的是某个实例的队列,而实例不断增多,
|
|
195
|
+
* 寄存与冲刷落在不同实例上 —— 于是"专家确实出图,但主会话一条交付事件都没有"。
|
|
196
|
+
*
|
|
197
|
+
* 所以发布器必须**进程内唯一**:队列、监听、perTurn 计数都只有一份。
|
|
198
|
+
* 缓存键用 `ctx.root ?? ctx`(cordis 的 root context 是稳定对象;`index.ts` 传进来的
|
|
199
|
+
* 是个每次新建的 spread 字面量,不能用它做身份判断)。构造失败不缓存——宿主服务
|
|
200
|
+
* 可能稍后才就绪,下次出图要能重新解析(保留原有的惰性语义)。
|
|
201
|
+
*/
|
|
202
|
+
let cachedPublisher;
|
|
203
|
+
export function createChartEventPublisher(ctx) {
|
|
204
|
+
const ref = ctx.root ?? ctx;
|
|
205
|
+
if (cachedPublisher !== undefined && cachedPublisher.ref === ref)
|
|
206
|
+
return cachedPublisher.publisher;
|
|
207
|
+
const publisher = buildChartEventPublisher(ctx);
|
|
208
|
+
if (publisher !== undefined)
|
|
209
|
+
cachedPublisher = { ref, publisher };
|
|
210
|
+
return publisher;
|
|
211
|
+
}
|
|
212
|
+
/** 只给测试用:丢弃单例缓存(配合 resetChartEventPending 做用例隔离)。 */
|
|
213
|
+
export function resetChartEventPublisher() {
|
|
214
|
+
cachedPublisher = undefined;
|
|
215
|
+
}
|
|
216
|
+
function buildChartEventPublisher(ctx) {
|
|
217
|
+
const sessions = ctx.get('sessions');
|
|
218
|
+
if (sessions === undefined || typeof sessions.get !== 'function')
|
|
219
|
+
return undefined;
|
|
220
|
+
const projections = ctx.get('sessionProjections');
|
|
221
|
+
if (projections === undefined || typeof projections.stateOf !== 'function')
|
|
222
|
+
return undefined;
|
|
223
|
+
const perTurn = new Map();
|
|
224
|
+
let warned = false;
|
|
225
|
+
/**
|
|
226
|
+
* 输出一条诊断。
|
|
227
|
+
*
|
|
228
|
+
* ⚠️ 实测(2026-09-20):cordis 的 `ctx.logger` **只挂了一个内存 exporter**
|
|
229
|
+
* (`cordis/lib/index.js` 的 `LoggerService` 构造器),全仓没有任何地方注册 console
|
|
230
|
+
* exporter —— 也就是说 `ctx.logger.warn` 只进内存缓冲,**不会出现在 dsh 终端里**。
|
|
231
|
+
* 这正是"卡片不出现却零报错"、两轮排查都只能靠猜的原因。
|
|
232
|
+
* 因此这里**同时**写 `process.stderr`(宿主 stderr 一定能在终端看到);
|
|
233
|
+
* logger 照发,保留结构化疗日志,将来有人接 exporter 就能收到。
|
|
234
|
+
*/
|
|
235
|
+
const emit = (message) => {
|
|
236
|
+
const line = `capital-generation: ${message}`;
|
|
237
|
+
try {
|
|
238
|
+
ctx.logger?.warn(line);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// logger 不可用不影响诊断输出。
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const stderr = globalThis.process?.stderr;
|
|
245
|
+
if (typeof stderr?.write === 'function')
|
|
246
|
+
stderr.write(`${line}\n`);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// 没有 process(非 Node 载体)时静默跳过。
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const warn = (message) => {
|
|
253
|
+
if (warned)
|
|
254
|
+
return;
|
|
255
|
+
warned = true;
|
|
256
|
+
emit(message);
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* 进度日志:**只在 `CAPITAL_CHART_TRACE=1` 时输出**。
|
|
260
|
+
*
|
|
261
|
+
* 寄存/登记这两处状态迁移是排查"交付行不出现"的关键线索,但对**普通用户是噪音**
|
|
262
|
+
* (每张图两条)。所以默认静默、按需打开:
|
|
263
|
+
*
|
|
264
|
+
* CAPITAL_CHART_TRACE=1 dsh web
|
|
265
|
+
*
|
|
266
|
+
* 失败用的 `warn` 不受此开关影响——真出问题必须让人看见。
|
|
267
|
+
*/
|
|
268
|
+
const note = (message) => {
|
|
269
|
+
if (!chartTraceEnabled())
|
|
270
|
+
return;
|
|
271
|
+
emit(message);
|
|
272
|
+
};
|
|
273
|
+
const boundaryOf = (owner) => {
|
|
274
|
+
try {
|
|
275
|
+
return projections.stateOf(owner, 'turnBoundary');
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
/** 真正写一条事件;返回是否写入。 */
|
|
282
|
+
const append = (owner, turn, input) => {
|
|
283
|
+
const key = `${owner.id}:${turn}`;
|
|
284
|
+
const used = perTurn.get(key) ?? 0;
|
|
285
|
+
if (used >= MAX_CHARTS_PER_TURN) {
|
|
286
|
+
warn(`图表对话流事件跳过:turn ${turn} 已达到 ${MAX_CHARTS_PER_TURN} 张上限`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
owner.append(CHART_DELIVERABLE_EVENT, buildChartEventPayload(input, turn));
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
warn(`图表对话流事件写入失败(图表已落盘):${error instanceof Error ? error.message : String(error)}`);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
perTurn.set(key, used + 1);
|
|
297
|
+
// 与"已寄存"成对的成功日志:排查交付行不出现时,一眼能看出是没写还是没渲染。
|
|
298
|
+
note(`图表已登记为本轮交付(owner=${owner.id},turn=${turn},${input.chart_id})`);
|
|
299
|
+
return true;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* 把寄存的图表写进某一轮。
|
|
303
|
+
*
|
|
304
|
+
* 冲刷时机决定交付行的落点:首选 `agent/turn-stopping`(该轮即将关闭),
|
|
305
|
+
* 只有在拿不到这个钩子时才退回 `turn/start`。
|
|
306
|
+
*/
|
|
307
|
+
const flush = (ownerSessionId, turn) => {
|
|
308
|
+
const queued = pending.get(ownerSessionId);
|
|
309
|
+
if (queued === undefined || queued.length === 0)
|
|
310
|
+
return;
|
|
311
|
+
const owner = resolveOwnerSession(sessions, ownerSessionId);
|
|
312
|
+
if (owner === undefined) {
|
|
313
|
+
pending.delete(ownerSessionId);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
pending.delete(ownerSessionId);
|
|
317
|
+
for (const item of queued)
|
|
318
|
+
append(owner, turn, item);
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* 首选时机是否可用。
|
|
322
|
+
*
|
|
323
|
+
* 关键:`turn-stopping` 缺席时**必须**退回 `turn/start`,否则寄存的图会永远不写(静默丢失)。
|
|
324
|
+
* 安装成功才置 true;effect 卸载时复位,避免失效后仍以为首选可用。
|
|
325
|
+
*/
|
|
326
|
+
let turnStoppingActive = false;
|
|
327
|
+
/** 不能在任何 appending 窗口内重入 append,统一延到微任务。 */
|
|
328
|
+
const flushSoon = (ownerSessionId, turn) => {
|
|
329
|
+
queueMicrotask(() => {
|
|
330
|
+
try {
|
|
331
|
+
flush(ownerSessionId, turn);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
warn(`图表对话流事件冲刷失败:${error instanceof Error ? error.message : String(error)}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
const listenCtx = ctx.root ?? ctx;
|
|
339
|
+
if (typeof listenCtx.on === 'function') {
|
|
340
|
+
const sessionEventDispose = listenCtx.on('session/event', (...args) => {
|
|
341
|
+
try {
|
|
342
|
+
const session = args[0];
|
|
343
|
+
const event = args[1];
|
|
344
|
+
if (event?.type !== 'turn/start')
|
|
345
|
+
return;
|
|
346
|
+
// 首选钩子在场时,`turn/start` 不得抢跑:那一轮往往还是过程轮(2026-09-20 真机教训),
|
|
347
|
+
// 交付行会被落到空轮里。只有拿不到 `agent/turn-stopping` 时才用它兜底。
|
|
348
|
+
if (turnStoppingActive)
|
|
349
|
+
return;
|
|
350
|
+
const sessionId = typeof session?.id === 'string' ? session.id : undefined;
|
|
351
|
+
const turn = event.data?.turn;
|
|
352
|
+
if (sessionId === undefined || !pending.has(sessionId))
|
|
353
|
+
return;
|
|
354
|
+
if (typeof turn !== 'number' || !Number.isSafeInteger(turn) || turn < 1)
|
|
355
|
+
return;
|
|
356
|
+
flushSoon(sessionId, turn);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// 事件观察者永不抛出。
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
if (typeof ctx.effect === 'function') {
|
|
363
|
+
ctx.effect(() => () => {
|
|
364
|
+
if (typeof sessionEventDispose === 'function')
|
|
365
|
+
sessionEventDispose();
|
|
366
|
+
}, 'capital-generation.chart-turn-events()');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (typeof listenCtx.onTurnStopping === 'function') {
|
|
370
|
+
const disposeTurnStopping = listenCtx.onTurnStopping((...args) => {
|
|
371
|
+
try {
|
|
372
|
+
const payload = args[0];
|
|
373
|
+
const sessionId = payload?.agent?.session?.id;
|
|
374
|
+
const turn = payload?.turn;
|
|
375
|
+
if (typeof sessionId !== 'string' || !pending.has(sessionId))
|
|
376
|
+
return;
|
|
377
|
+
if (typeof turn !== 'number' || !Number.isSafeInteger(turn) || turn < 1)
|
|
378
|
+
return;
|
|
379
|
+
flushSoon(sessionId, turn);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// 事件观察者永不抛出。
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
// ⚠️ 只有**真的拿到 disposer** 才算首选可用(2026-09-20 真机事故):
|
|
386
|
+
// 早先这里无条件置 true,于是 `ctx.on('agent/turn-stopping')` 注册失败
|
|
387
|
+
// (适配器 catch 后返回 undefined)时,闸门仍被打开、`turn/start` 兜底被永久关掉,
|
|
388
|
+
// 图就**永远滞留 pending**——表现是交付卡片从头到尾不出现,且零报错。
|
|
389
|
+
// 注册失败必须让 `turnStoppingActive` 保持 false,由 `turn/start` 接上。
|
|
390
|
+
if (typeof disposeTurnStopping === 'function')
|
|
391
|
+
turnStoppingActive = true;
|
|
392
|
+
else
|
|
393
|
+
warn('交付行的首选冲刷时机不可用(agent/turn-stopping 注册失败),已退回 turn/start 兜底;落点可能偏早一轮');
|
|
394
|
+
if (typeof ctx.effect === 'function') {
|
|
395
|
+
ctx.effect(() => () => {
|
|
396
|
+
// 卸载即视为"首选不可用",让 `turn/start` 兜底重新接上,避免图静默丢失。
|
|
397
|
+
turnStoppingActive = false;
|
|
398
|
+
if (typeof disposeTurnStopping === 'function')
|
|
399
|
+
disposeTurnStopping();
|
|
400
|
+
}, 'capital-generation.chart-turn-stopping()');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
publish(input) {
|
|
405
|
+
const trace = [];
|
|
406
|
+
const owner = resolveOwnerSession(sessions, input.ownerSessionId);
|
|
407
|
+
if (owner === undefined) {
|
|
408
|
+
lastPublishTrace = `owner 未解析:ownerSessionId=${input.ownerSessionId}(sessions.get 拿不到可直接 append 的会话)`;
|
|
409
|
+
warn(`图表对话流事件跳过:找不到 owner 会话 ${input.ownerSessionId}(图表已落盘,走 html_path 降级)`);
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
trace.push(`owner=${owner.id}`);
|
|
413
|
+
const boundary = boundaryOf(owner);
|
|
414
|
+
const openSeq = boundary?.openTurnStartSeq;
|
|
415
|
+
const lastTurn = boundary?.lastTurn;
|
|
416
|
+
if (typeof openSeq !== 'number') {
|
|
417
|
+
// 回合之间出图(主 Agent 等子 Agent 时会先关轮):寄存,等 owner 那一轮
|
|
418
|
+
// **即将关闭**(`agent/turn-stopping`)时再写——那才是消费这份图、写下答复的回合。
|
|
419
|
+
//
|
|
420
|
+
// ⚠️ 寄存队列必须按 **owner**(根会话)的 id 归档,不能按 `input.ownerSessionId`:
|
|
421
|
+
// 后者是发起出图的 scope(visualization_specialist,孙会话),而冲刷时机
|
|
422
|
+
// (`turn/start` / `agent/turn-stopping`)拿到的都是 owner 的 id。
|
|
423
|
+
// 两边键不一致时 `flush()` 永远查不到,图会**永久滞留在 pending**、静默不登记
|
|
424
|
+
// (2026-09-20 实测:3 个 specialist 会话 render_chart=2 而 deliverables=0)。
|
|
425
|
+
const queued = pending.get(owner.id) ?? [];
|
|
426
|
+
if (queued.length >= MAX_CHARTS_PER_TURN) {
|
|
427
|
+
warn(`图表对话流事件跳过:${owner.id} 已寄存 ${MAX_CHARTS_PER_TURN} 张,等不到新回合`);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
// 安全阀:正常不该触发(冲刷会清空);触发说明冲刷链路坏了,宁可丢图也要有界。
|
|
431
|
+
let total = 0;
|
|
432
|
+
for (const list of pending.values())
|
|
433
|
+
total += list.length;
|
|
434
|
+
if (total >= MAX_PENDING_CHARTS) {
|
|
435
|
+
warn(`图表对话流事件跳过:进程内已寄存 ${total} 张仍未登记(冲刷链路异常),超出 ${MAX_PENDING_CHARTS} 上限`);
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
queued.push(input);
|
|
439
|
+
pending.set(owner.id, queued);
|
|
440
|
+
// 出图时回合是关的 ⇒ 走寄存。这条日志是**必要的可观测性**:它之后若没有对应的
|
|
441
|
+
// "已登记" 日志,就说明冲刷时机没接上(2026-09-20 的静默丢失正是死在这里,
|
|
442
|
+
// 两轮排查都因为没有日志而只能猜)。只在进程内报一次,不刷屏。
|
|
443
|
+
note(`图表已寄存,待 owner 回合关闭时登记(owner=${owner.id},lastTurn=${String(lastTurn)},寄存 ${queued.length} 张)`);
|
|
444
|
+
lastPublishTrace = `${trace.join(' ')} 回合未打开(openTurnStartSeq=${String(openSeq)} lastTurn=${String(lastTurn)}) → 已寄存 ${queued.length} 张,等 turn-stopping/turn-start 冲刷`;
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
// turn 0 / 非法值表示该会话还没真正开过回合。
|
|
448
|
+
if (typeof lastTurn !== 'number' || !Number.isSafeInteger(lastTurn) || lastTurn < 1) {
|
|
449
|
+
lastPublishTrace = `${trace.join(' ')} 回合号非法(lastTurn=${String(lastTurn)}) → 未写入`;
|
|
450
|
+
warn('图表对话流事件跳过:owner 会话没有打开中的 turn');
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const written = append(owner, lastTurn, input);
|
|
454
|
+
lastPublishTrace = `${trace.join(' ')} 回合打开(turn=${lastTurn}) → ${written ? '已直接登记' : '写入失败'}`;
|
|
455
|
+
return written;
|
|
456
|
+
},
|
|
457
|
+
/** 诊断/测试用:当前寄存未冲刷的图表数。 */
|
|
458
|
+
pendingCount(ownerSessionId) {
|
|
459
|
+
if (ownerSessionId !== undefined)
|
|
460
|
+
return pending.get(ownerSessionId)?.length ?? 0;
|
|
461
|
+
let total = 0;
|
|
462
|
+
for (const list of pending.values())
|
|
463
|
+
total += list.length;
|
|
464
|
+
return total;
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/** 出图时的 owner scope:与 chart_ref 同源(specialist 出图时正好是主会话)。 */
|
|
469
|
+
export function chartEventOwnerSessionId(session, sourceSession) {
|
|
470
|
+
return chartSessionScopeId(sourceSession ?? session);
|
|
471
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChartPayload } from './series.js';
|
|
2
|
+
export interface VendoredChartLibrary {
|
|
3
|
+
source: string;
|
|
4
|
+
version: string;
|
|
5
|
+
license: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 读取随包发布的 lightweight-charts standalone 构建。
|
|
9
|
+
*
|
|
10
|
+
* 路径相对 `import.meta.url`:编译后是 `lib/chart/vendor/...`(由 scripts/copy-assets.mjs
|
|
11
|
+
* 从 `vendor/` 复制过去)。缺失时给出可操作错误,而不是让 HTML 里静默少一段脚本。
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadVendoredChartLibrary(): VendoredChartLibrary;
|
|
14
|
+
export interface BuildStandaloneHtmlInput {
|
|
15
|
+
payload: ChartPayload;
|
|
16
|
+
library: VendoredChartLibrary;
|
|
17
|
+
/** 生成时刻(epoch ms),仅用于页脚署名,不参与渲染。 */
|
|
18
|
+
generatedAt: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildStandaloneHtml(input: BuildStandaloneHtmlInput): string;
|
|
21
|
+
export declare function escapeHtml(value: string): string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自包含 HTML 图表的生成器。
|
|
3
|
+
*
|
|
4
|
+
* 这是整个方案的**降级底线**(内部设计文档 docs/design/chart-visualization.md §5.4 的 L2):
|
|
5
|
+
* 它只依赖"用户有一个浏览器",不依赖 DSH 的客户端插件系统、不依赖网络、
|
|
6
|
+
* 不依赖任何上游扩展点。即使 Slot / bundle / 路由全部被上游改动打坏,
|
|
7
|
+
* 用户依然能拿到一张可交互的图。
|
|
8
|
+
*
|
|
9
|
+
* 因此它必须:内联 vendored 库 + 内联数据 + 内联运行时,**零外部请求**。
|
|
10
|
+
*/
|
|
11
|
+
import { ChartError } from './errors.js';
|
|
12
|
+
import { CHART_RUNTIME_SOURCE } from './runtime.js';
|
|
13
|
+
function nodeFs() {
|
|
14
|
+
const processLike = globalThis.process;
|
|
15
|
+
const fs = processLike?.getBuiltinModule?.('node:fs');
|
|
16
|
+
if (!fs || typeof fs.readFileSync !== 'function') {
|
|
17
|
+
throw new ChartError('chart_runtime_unavailable', 'Node fs is unavailable in this host; the standalone HTML artifact cannot embed the vendored chart library');
|
|
18
|
+
}
|
|
19
|
+
return fs;
|
|
20
|
+
}
|
|
21
|
+
let cachedLibrary;
|
|
22
|
+
/**
|
|
23
|
+
* 读取随包发布的 lightweight-charts standalone 构建。
|
|
24
|
+
*
|
|
25
|
+
* 路径相对 `import.meta.url`:编译后是 `lib/chart/vendor/...`(由 scripts/copy-assets.mjs
|
|
26
|
+
* 从 `vendor/` 复制过去)。缺失时给出可操作错误,而不是让 HTML 里静默少一段脚本。
|
|
27
|
+
*/
|
|
28
|
+
export function loadVendoredChartLibrary() {
|
|
29
|
+
if (cachedLibrary)
|
|
30
|
+
return cachedLibrary;
|
|
31
|
+
try {
|
|
32
|
+
const fs = nodeFs();
|
|
33
|
+
const source = fs.readFileSync(new URL('vendor/lightweight-charts.standalone.production.js', import.meta.url), 'utf8');
|
|
34
|
+
const manifest = JSON.parse(fs.readFileSync(new URL('vendor/VERSION.json', import.meta.url), 'utf8'));
|
|
35
|
+
cachedLibrary = { source, version: manifest.version ?? 'unknown', license: manifest.license ?? 'Apache-2.0' };
|
|
36
|
+
return cachedLibrary;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof ChartError)
|
|
40
|
+
throw error;
|
|
41
|
+
throw new ChartError('chart_runtime_unavailable', `vendored lightweight-charts is missing (${error instanceof Error ? error.message : String(error)}); run npm run vendor:charts && npm run build`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** 内联脚本的安全转义:`</script` 会提前结束 script 元素。 */
|
|
45
|
+
function escapeScript(source) {
|
|
46
|
+
return source.replace(/<\/script/gi, '<\\/script');
|
|
47
|
+
}
|
|
48
|
+
/** 内联 JSON 的安全转义:`<` 一律写成 `\u003c`(JSON 里合法,且彻底杜绝标签闭合)。 */
|
|
49
|
+
function escapeJson(value) {
|
|
50
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
51
|
+
}
|
|
52
|
+
/** 数据截至时间:按本仓数据口径用 Asia/Shanghai 显示,同时保留 UTC。 */
|
|
53
|
+
function formatCapturedAt(epochMs) {
|
|
54
|
+
try {
|
|
55
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
56
|
+
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
57
|
+
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
58
|
+
}).formatToParts(epochMs);
|
|
59
|
+
const map = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
60
|
+
return `${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute} (Asia/Shanghai)`;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return new Date(epochMs).toISOString();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function buildStandaloneHtml(input) {
|
|
67
|
+
const { payload, library } = input;
|
|
68
|
+
const meta = payload.meta;
|
|
69
|
+
const facts = [];
|
|
70
|
+
facts.push(`${meta.points} 个数据点`);
|
|
71
|
+
if (meta.downsampled)
|
|
72
|
+
facts.push(`已从 ${meta.original_points} 点下采样(保留极值形状,非全量)`);
|
|
73
|
+
if (meta.dataset_id)
|
|
74
|
+
facts.push(`dataset ${meta.dataset_id}`);
|
|
75
|
+
if (meta.source_label)
|
|
76
|
+
facts.push(meta.source_label);
|
|
77
|
+
if (meta.captured_at !== undefined)
|
|
78
|
+
facts.push(`数据截至 ${formatCapturedAt(meta.captured_at)}`);
|
|
79
|
+
if (meta.time_field)
|
|
80
|
+
facts.push(`时间字段 ${meta.time_field}`);
|
|
81
|
+
if (meta.axis === 'index')
|
|
82
|
+
facts.push('分类轴(按行序)');
|
|
83
|
+
const warnings = meta.warnings.length > 0
|
|
84
|
+
? `<ul class="warnings">${meta.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>`
|
|
85
|
+
: '';
|
|
86
|
+
return `<!doctype html>
|
|
87
|
+
<html lang="zh-CN">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="utf-8">
|
|
90
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
91
|
+
<title>${escapeHtml(payload.title)}</title>
|
|
92
|
+
<style>
|
|
93
|
+
/* 同一批 CSS 变量名由 DSH 壳提供;自包含 HTML 自己定义,因此两边渲染完全一致。 */
|
|
94
|
+
:root {
|
|
95
|
+
--dsw-alias-bg-base: #ffffff;
|
|
96
|
+
--dsw-alias-border-l1: rgba(0,0,0,0.10);
|
|
97
|
+
--dsw-alias-label-primary: #1f2328;
|
|
98
|
+
--dsw-alias-label-secondary: #6b7280;
|
|
99
|
+
--dsw-alias-brand-primary: #2563eb;
|
|
100
|
+
--dsw-alias-state-error-primary: #d94a3d;
|
|
101
|
+
--dsw-alias-state-success-primary: #2f9e44;
|
|
102
|
+
}
|
|
103
|
+
@media (prefers-color-scheme: dark) {
|
|
104
|
+
:root {
|
|
105
|
+
--dsw-alias-bg-base: #16181d;
|
|
106
|
+
--dsw-alias-border-l1: rgba(255,255,255,0.12);
|
|
107
|
+
--dsw-alias-label-primary: #e8eaed;
|
|
108
|
+
--dsw-alias-label-secondary: #9aa0a6;
|
|
109
|
+
--dsw-alias-brand-primary: #6ea8fe;
|
|
110
|
+
--dsw-alias-state-error-primary: #f0776c;
|
|
111
|
+
--dsw-alias-state-success-primary: #57cc72;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
html, body { margin: 0; padding: 0; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); }
|
|
115
|
+
body { font: 14px/1.6 -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; padding: 16px 20px 24px; }
|
|
116
|
+
h1 { margin: 0 0 4px; font-size: 16px; font-weight: 600; }
|
|
117
|
+
.facts { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
118
|
+
main { margin-top: 12px; height: 460px; min-height: 320px; }
|
|
119
|
+
.warnings { margin: 12px 0 0; padding-left: 18px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
120
|
+
footer { margin-top: 16px; color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
<header>
|
|
125
|
+
<h1>${escapeHtml(payload.title)}</h1>
|
|
126
|
+
<p class="facts">${facts.map((fact) => escapeHtml(fact)).join(' · ')}</p>
|
|
127
|
+
</header>
|
|
128
|
+
<main id="chart"></main>
|
|
129
|
+
${warnings}
|
|
130
|
+
<footer>数据来源见上方标注;图表仅用于呈现,不构成投资建议。Lightweight Charts™ v${escapeHtml(library.version)} (${escapeHtml(library.license)})。</footer>
|
|
131
|
+
<script>${escapeScript(library.source)}</script>
|
|
132
|
+
<script>var __capitalChart = ${escapeScript(CHART_RUNTIME_SOURCE)};</script>
|
|
133
|
+
<script>
|
|
134
|
+
(function () {
|
|
135
|
+
var payload = ${escapeJson(payload)};
|
|
136
|
+
var container = document.getElementById('chart');
|
|
137
|
+
try {
|
|
138
|
+
__capitalChart.render(container, payload, globalThis.LightweightCharts);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
container.textContent = '图表渲染失败:' + (error && error.message ? error.message : String(error));
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
export function escapeHtml(value) {
|
|
149
|
+
return value
|
|
150
|
+
.replace(/&/g, '&')
|
|
151
|
+
.replace(/</g, '<')
|
|
152
|
+
.replace(/>/g, '>')
|
|
153
|
+
.replace(/"/g, '"');
|
|
154
|
+
}
|