dsh-plugin-show-me-data 0.1.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/LICENSE +27 -0
- package/README.md +96 -0
- package/cordis.patch.yml +40 -0
- package/docs/01-product-effect.md +178 -0
- package/docs/02-architecture.md +275 -0
- package/docs/03-data-contracts.md +291 -0
- package/docs/04-sources.md +342 -0
- package/docs/05-ui-spec.md +167 -0
- package/docs/06-ai-layer.md +194 -0
- package/docs/07-implementation-plan.md +399 -0
- package/docs/08-test-plan.md +133 -0
- package/docs/09-packaging-install.md +249 -0
- package/docs/10-kickoff-prompt.md +94 -0
- package/docs/11-decisions.md +203 -0
- package/docs/12-runtime-verified.md +115 -0
- package/docs/13-acceptance.md +153 -0
- package/docs/14-progress.md +150 -0
- package/docs/15-publish.md +185 -0
- package/lib/app/ai-deterministic.js +327 -0
- package/lib/app/ai-validate.js +284 -0
- package/lib/app/ai.js +440 -0
- package/lib/app/health.js +77 -0
- package/lib/app/overview.js +349 -0
- package/lib/app/propose-indicator.js +122 -0
- package/lib/app/refresh.js +251 -0
- package/lib/app/series-view.js +195 -0
- package/lib/app/watchlist.js +102 -0
- package/lib/client.js +4322 -0
- package/lib/core/ai/prompts.js +213 -0
- package/lib/core/chart/axis.js +133 -0
- package/lib/core/chart/bar.js +58 -0
- package/lib/core/chart/candle.js +216 -0
- package/lib/core/chart/line.js +186 -0
- package/lib/core/chart/scale.js +132 -0
- package/lib/core/format.js +143 -0
- package/lib/core/indicators/catalog.js +1011 -0
- package/lib/core/indicators/resolve.js +196 -0
- package/lib/core/insight/digest.js +250 -0
- package/lib/core/insight/rank.js +115 -0
- package/lib/core/insight/related.js +90 -0
- package/lib/core/insight/rules.js +417 -0
- package/lib/core/stats/derive.js +123 -0
- package/lib/core/stats/series.js +465 -0
- package/lib/core/time/range.js +242 -0
- package/lib/core/types.js +478 -0
- package/lib/host/ai/discussion.js +559 -0
- package/lib/host/ai/dsh-llm-gateway.js +333 -0
- package/lib/host/config.js +194 -0
- package/lib/host/http/respond.js +165 -0
- package/lib/host/http/routes.js +689 -0
- package/lib/host/index.js +293 -0
- package/lib/host/infra/fs-repos.js +179 -0
- package/lib/host/infra/memory-fallback.js +64 -0
- package/lib/host/tools/define-tool.js +295 -0
- package/lib/host/tools/register.js +431 -0
- package/lib/host.js +7 -0
- package/lib/ports/clock.js +57 -0
- package/lib/ports/snapshot-repo.js +48 -0
- package/lib/sources/eastmoney-macro.js +197 -0
- package/lib/sources/eastmoney-quote.js +201 -0
- package/lib/sources/ecb.js +179 -0
- package/lib/sources/fred.js +207 -0
- package/lib/sources/http.js +136 -0
- package/lib/sources/ohlc.js +36 -0
- package/lib/sources/quote-cascade.js +177 -0
- package/lib/sources/registry.js +153 -0
- package/lib/sources/sina-cn.js +197 -0
- package/lib/sources/sina-us.js +187 -0
- package/lib/sources/tencent.js +158 -0
- package/lib/sources/us-treasury-rates.js +275 -0
- package/lib/sources/us-treasury.js +196 -0
- package/lib/sources/worldbank.js +170 -0
- package/package.json +69 -0
- package/src/app/ai-deterministic.js +327 -0
- package/src/app/ai-validate.js +284 -0
- package/src/app/ai.js +440 -0
- package/src/app/health.js +77 -0
- package/src/app/overview.js +349 -0
- package/src/app/propose-indicator.js +122 -0
- package/src/app/refresh.js +251 -0
- package/src/app/series-view.js +195 -0
- package/src/app/watchlist.js +102 -0
- package/src/client/api.js +323 -0
- package/src/client/components.js +1877 -0
- package/src/client/copy.js +368 -0
- package/src/client/index.js +169 -0
- package/src/client/store.js +219 -0
- package/src/core/ai/prompts.js +213 -0
- package/src/core/chart/axis.js +133 -0
- package/src/core/chart/bar.js +58 -0
- package/src/core/chart/candle.js +216 -0
- package/src/core/chart/line.js +186 -0
- package/src/core/chart/scale.js +132 -0
- package/src/core/format.js +143 -0
- package/src/core/indicators/catalog.js +1011 -0
- package/src/core/indicators/resolve.js +196 -0
- package/src/core/insight/digest.js +250 -0
- package/src/core/insight/rank.js +115 -0
- package/src/core/insight/related.js +90 -0
- package/src/core/insight/rules.js +417 -0
- package/src/core/stats/derive.js +123 -0
- package/src/core/stats/series.js +465 -0
- package/src/core/time/range.js +242 -0
- package/src/core/types.js +478 -0
- package/src/host/ai/discussion.js +559 -0
- package/src/host/ai/dsh-llm-gateway.js +333 -0
- package/src/host/config.js +194 -0
- package/src/host/http/respond.js +165 -0
- package/src/host/http/routes.js +689 -0
- package/src/host/index.js +293 -0
- package/src/host/infra/fs-repos.js +179 -0
- package/src/host/infra/memory-fallback.js +64 -0
- package/src/host/tools/define-tool.js +295 -0
- package/src/host/tools/register.js +431 -0
- package/src/ports/clock.js +57 -0
- package/src/ports/snapshot-repo.js +48 -0
- package/src/sources/eastmoney-macro.js +197 -0
- package/src/sources/eastmoney-quote.js +201 -0
- package/src/sources/ecb.js +179 -0
- package/src/sources/fred.js +207 -0
- package/src/sources/http.js +136 -0
- package/src/sources/ohlc.js +36 -0
- package/src/sources/quote-cascade.js +177 -0
- package/src/sources/registry.js +153 -0
- package/src/sources/sina-cn.js +197 -0
- package/src/sources/sina-us.js +187 -0
- package/src/sources/tencent.js +158 -0
- package/src/sources/us-treasury-rates.js +275 -0
- package/src/sources/us-treasury.js +196 -0
- package/src/sources/worldbank.js +170 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# 05 · UI 规格(浏览器半)
|
|
2
|
+
|
|
3
|
+
浏览器半是**手写的**:一个经典脚本,调用 `window.__ModuleLoader__.load({ id, factory })`,
|
|
4
|
+
在 `factory(require)` 里 `require("react")`,用 `React.createElement`(**不能用 JSX**,因为不经打包器)。
|
|
5
|
+
经实测确认这是可行且正式的形态(`dsh-client-ui-jobs/lib/client.js` 就是这个结构)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 槽位选型(**必须在 M0 用 `cordis_inspect` 现场确认**)
|
|
10
|
+
|
|
11
|
+
浏览器半只能往**已存在的槽位**注册 UI。以下为候选,协议(`single`/`list`/`keyed`/`chain`)、
|
|
12
|
+
注册选项、props 形状**一律以 `cordis_inspect what:"client"` 的查询结果为准**。
|
|
13
|
+
|
|
14
|
+
| 用途 | 候选槽位 | 说明 / 风险 |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| **主面板(浮层)** | `shell.overlay` | 全体帧级浮层;Cordis 面板用的就是这个。风险:`kind` 可能是 `list`,需给出唯一 `key`/`id` 与 `order`,并且要自行处理 pointer-events 与层级 |
|
|
17
|
+
| 设置分区 | `settings.section` | 完整内容区,适合「数据看板」设置页 |
|
|
18
|
+
| 常驻触发器 | 若 `shell.overlay` 只允许一个入口,则把徽标与面板**同注册在一个 entry 内**(触发器 + 条件渲染面板) | 最稳的做法:一个 `shell.overlay` entry 内部自管理开合 |
|
|
19
|
+
| 会话内图表卡 | `tool.call.toolview`(key = 工具名,如 `data_overview`) | 覆盖同名工具默认卡;需先 `Tool.listTools` 确认工具名与 `ToolCallOwnerProps` |
|
|
20
|
+
| 一轮对话后的摘要 | `conversation.chat.turnTail` | 可选,用于在对话尾部挂"数据快照"卡 |
|
|
21
|
+
|
|
22
|
+
**降级路径**:若 `shell.overlay` 不可用或不适合承载大型面板,
|
|
23
|
+
退化为「`settings.section` 全页看板 + `conversation.chat.turnTail` 入口」,
|
|
24
|
+
并在 `12-runtime-verified.md` 记录原因。
|
|
25
|
+
|
|
26
|
+
> 纪律(来自 DSH 的插件开发规范):**不要**替换 `root` / `sidebar` / `conversation` / `details`
|
|
27
|
+
> 这类根级槽位——替换整个占位者会连带移除它声明的子槽位。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. 组件树
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
DataRadarOverlay // apply() 注册的根组件;管理 open/close、范围、数据获取、错误边界
|
|
35
|
+
├── TriggerBadge // 常驻徽标:图标 + 新增条数角标 + 折叠态
|
|
36
|
+
└── Panel // 打开时渲染
|
|
37
|
+
├── PanelHeader // 标题 · Tab(今日/核心/我的/设置) · RangeTabs · 刷新时间 · 齿轮 · 关闭
|
|
38
|
+
├── NoteworthySection // 值得关注:NoteworthyCard[](最多 3 张 + "更多"展开)
|
|
39
|
+
├── MetricsGrid // 核心指标:分组栏(美国/中国/全球/我的) × MetricCard[]
|
|
40
|
+
│ └── MetricCard // 名称 · 最新值 · 变化 · Sparkline · SourceBadge · 状态点
|
|
41
|
+
├── DetailDrawer // 点击卡片后滑出:ChartTabs + 主图 + StatsStrip + DataTable + AiPanel
|
|
42
|
+
├── AiPanel // AI 解析(流式)+ 引用列表 + 模式标注(deterministic/llm)
|
|
43
|
+
├── DigestSection // 时段 AI 总结(4 段结构化)
|
|
44
|
+
├── QaBox // 提问输入 + 流式回答 + 使用到的数据点
|
|
45
|
+
└── AddIndicatorDialog // 目录检索 + 自然语言解析预览 + 确认落盘
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**状态持有规则**(避免踩 DSH 的坑):**不要**把运行状态放在会被重挂载的组件 state 里。
|
|
49
|
+
面板状态放在模块级 `createStore()`(极简订阅式 store,自带单测),组件只订阅。
|
|
50
|
+
这部分与 `dsh-client-ui-cordis` 的做法一致(它把事实放在"能负责关闭它的那一方拥有的 observable"里)。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. 数据获取(唯一传输层 `client/api.js`)
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// 全部走宿主半的 HTTP 路由,浏览器不直连外部数据源
|
|
58
|
+
GET /api/show-me-data/overview?range=1Y&groups=US,CN,CUSTOM
|
|
59
|
+
GET /api/show-me-data/series?indicator=us.payrolls.change&range=1Y
|
|
60
|
+
GET /api/show-me-data/watchlist
|
|
61
|
+
POST /api/show-me-data/watchlist { action:'add'|'remove'|'update', item }
|
|
62
|
+
GET /api/show-me-data/catalog/search?q=国债
|
|
63
|
+
POST /api/show-me-data/ai/explain { indicator, range }
|
|
64
|
+
POST /api/show-me-data/ai/summary { range, indicators[] }
|
|
65
|
+
POST /api/show-me-data/ai/ask { question, context:{range, indicators[]} }
|
|
66
|
+
POST /api/show-me-data/ai/propose { text }
|
|
67
|
+
GET /api/show-me-data/health
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **AI 三个端点用 SSE 流式**(`text/event-stream`),前端边收边渲染;
|
|
71
|
+
非流式降级(`mode=deterministic`)时是一次性 JSON。
|
|
72
|
+
- 任何路由失败必须返回**结构化错误** `{ error: { kind, detail, retryable } }`,前端据此显示,
|
|
73
|
+
不允许出现裸 500 HTML。
|
|
74
|
+
- 前端 fetch 统一 8s 超时 + 取消(切换范围时取消旧请求,避免旧响应覆盖新视图)。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 4. 图表规格(纯 SVG,可单测)
|
|
79
|
+
|
|
80
|
+
**没有图表库可 require**,所以全部手写。这是优势:几何计算是纯函数,能 100% 单测。
|
|
81
|
+
|
|
82
|
+
### 4.1 纯几何函数(`src/core/chart/*.js`)
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
// scale.js
|
|
86
|
+
linearScale({ domain:[min,max], range:[a,b] }) → (v:number)=>number // 处理 min===max 的退化
|
|
87
|
+
niceTicks(min, max, count=5) → number[] // 1/2/5×10^n 取整
|
|
88
|
+
// line.js
|
|
89
|
+
buildLinePath(points, { width, height, padding, xDomain, yDomain }) → string // 'M…L…'
|
|
90
|
+
buildAreaPath(points, {...}) → string // 闭合到基线
|
|
91
|
+
buildSparkline(values, { width, height, padding }) → string
|
|
92
|
+
// bar.js
|
|
93
|
+
buildBarRects(points, { width, height, padding, xDomain, yDomain }) → {x,y,w,h}[]
|
|
94
|
+
// axis.js
|
|
95
|
+
buildYAxis(yDomain, { width, height, padding, ticks }) → { y, label, text }[]
|
|
96
|
+
buildXAxis(xDomain, {...}) → { x, label }[]
|
|
97
|
+
buildZeroLine(yDomain, {height, padding}) → number|null
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**必测边界**(先写这些测试,再写实现):
|
|
101
|
+
|
|
102
|
+
| 输入 | 期望 |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `points = []` | `buildLinePath` 返回 `''`;不抛错 |
|
|
105
|
+
| `points = [{t,v:5}]` | 返回 `'M…'`(起点,不画 L);sparkline 返回单点圆或 `''`(二选一,测试固化) |
|
|
106
|
+
| 所有 `v` 相同(min===max) | y 缩放不产生 `NaN`/`Infinity`,路径为水平线 |
|
|
107
|
+
| 含 `NaN` 的点 | 视为断点:路径用 `M` 重新起笔(不连线) |
|
|
108
|
+
| `width <= 0` | 抛 `Error`(编程错误,不静默) |
|
|
109
|
+
| 1 万个点 | 路径字符串长度有上限(抽稀:每像素最多 2 点),并断言性能预算 < 30ms |
|
|
110
|
+
|
|
111
|
+
### 4.2 图表类型
|
|
112
|
+
|
|
113
|
+
| 类型 | 用途 | 要点 |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| Sparkline | 卡片内 12–60 点 | 无坐标轴、无网格,末点加圆点;负值区可染色 |
|
|
116
|
+
| 主图 Line | 详情默认 | Y 轴 5 刻度 + 网格 3 条 + 零线(若跨零)+ hover 十字线 + tooltip |
|
|
117
|
+
| 主图 Bar | 变化量类指标(非农月增) | 正负分色;零线明显 |
|
|
118
|
+
| 主图 DualLine | 对比模式(如 CPI vs 核心 CPI) | 双 Y 轴各自 `niceTicks`;图例可切换显隐 |
|
|
119
|
+
| Area | 累计/水平类 | 半透明填充 |
|
|
120
|
+
|
|
121
|
+
**交互**:hover 显示最近点 tooltip(日期 + 数值 + 单位 + 来源);点击点位 → 高亮并在数据表滚动到该行。
|
|
122
|
+
**可访问性**:`<svg role="img" aria-label="…">`,并提供一个"查看数据表"按钮作为等价文本表达
|
|
123
|
+
(不做纯图形信息通道)。
|
|
124
|
+
|
|
125
|
+
### 4.3 颜色与主题
|
|
126
|
+
|
|
127
|
+
- **只用主题令牌**(`Theme.listTokens` 查询结果)与主题 CSS 变量,不硬编码产品色。
|
|
128
|
+
- 语义色:涨/跌/中性/告警四类,**由 `display.polarity` 决定方向语义**(CPI 上升不一定是"红",
|
|
129
|
+
失业率上升才是恶化)——所以颜色不能简单按数值正负决定,必须读 `polarity`。
|
|
130
|
+
- 自定义样式用 `styles.insert(css)`(DSH 提供的样式注入),作用域限定在面板根 class 下。
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 5. 状态与异常表现(逐条实现,逐条测试)
|
|
135
|
+
|
|
136
|
+
| 场景 | 表现 |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| 首次加载 | 骨架屏(卡片位置固定,避免跳动);>3s 显示「正在获取 FRED 数据…」 |
|
|
139
|
+
| 有缓存 | **立即**渲染旧值 + 顶部「6 分钟前 · 正在刷新」;刷新完成后原地替换,不重排 |
|
|
140
|
+
| 单项失败 | 该卡:灰色遮罩 + 「数据源暂不可用」+ 最后成功时间 + 重试按钮 |
|
|
141
|
+
| 全部失败 | 顶部红色横幅 + 全部卡片显示快照值(若有);无快照则空态文案含「检查网络 / 数据源可用性」 |
|
|
142
|
+
| 陈旧数据 | 卡片右上黄点 + tooltip 说明(阈值按 `freq`:日 2 交易日 / 周 10 天 / 月 40 天 / 季 100 天 / 年 400 天) |
|
|
143
|
+
| AI 不可用 | AI 区域显示 `确定性摘要模式` 标签 + 规则生成的文本;按钮仍可用,不报错 |
|
|
144
|
+
| AI 超时 | 30s 超时,保留已流式输出的部分 + 「已中断,可重试」 |
|
|
145
|
+
| 自定义指标冲突 | 添加时同 id 已存在 → 提示「已存在,是否更新?」而不是静默覆盖 |
|
|
146
|
+
| 无权限/路由 404 | 面板显示「插件未正确挂载(/api/show-me-data 不可达)」,并在设置页给出挂载检查清单 |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 6. 国际化与文案
|
|
151
|
+
|
|
152
|
+
- 面板主语言中文,`label` 同时带 `zh`/`en`;语言选择读客户端 locale 服务(若可用),否则默认 `zh`。
|
|
153
|
+
- 所有面向用户的固定文案集中在一个 `copy.js` 常量表(便于测试"没有裸英文/裸 key 泄漏到 UI")。
|
|
154
|
+
- **免责声明**固定显示在面板页脚,不进 AI 提示词。
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 7. 性能预算(验收指标)
|
|
159
|
+
|
|
160
|
+
| 指标 | 预算 |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| 面板首屏(有缓存) | < 300ms 到可见骨架,< 800ms 到完整渲染 |
|
|
163
|
+
| 范围切换(命中缓存) | < 150ms |
|
|
164
|
+
| 范围切换(冷缓存、10 指标) | < 4s(并发取数,单项 15s 超时) |
|
|
165
|
+
| 单指标详情(含图) | < 200ms |
|
|
166
|
+
| SVG 路径生成(1 万点) | < 30ms |
|
|
167
|
+
| 面板 DOM 节点 | < 3000(列表虚拟化仅在「我的」超过 60 项时启用) |
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# 06 · AI 层设计(解析 / 总结 / 问答 / 对话式扩展)
|
|
2
|
+
|
|
3
|
+
设计原则一句话:**AI 只做"把已有数据讲清楚",不做"产生数据",也不做"断言无引用"。**
|
|
4
|
+
所有 AI 能力都走一个端口,因此可以替换、可以离线、可以单测。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. `AiGateway` 端口
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
/** @typedef {Object} AiGateway
|
|
12
|
+
* @property {(req: ExplainRequest) => AsyncIterable<AiChunk>} explain
|
|
13
|
+
* @property {(req: SummaryRequest) => AsyncIterable<AiChunk>} summarize
|
|
14
|
+
* @property {(req: AskRequest) => AsyncIterable<AiChunk>} answer
|
|
15
|
+
* @property {(req: ProposeRequest) => Promise<ProposeResult>} propose
|
|
16
|
+
* @property {() => { mode: 'llm'|'deterministic'|'relay', provider?: string, model?: string }} describe
|
|
17
|
+
*/
|
|
18
|
+
/** @typedef {{type:'text', text:string} | {type:'done', result: AiResult} | {type:'error', error: SourceError}} AiChunk */
|
|
19
|
+
/** @typedef {Object} AiResult
|
|
20
|
+
* @property {string} markdown // 最终可直接渲染的文本
|
|
21
|
+
* @property {CitedPoint[]} usedPoints // ★ 必须非空(除非显式声明数据不足)
|
|
22
|
+
* @property {string} [insufficient] // 非空时表示"数据不足"及原因
|
|
23
|
+
* @property {'llm'|'deterministic'|'relay'} mode
|
|
24
|
+
* @property {string} fingerprint // 输入指纹,用于缓存
|
|
25
|
+
*/
|
|
26
|
+
/** @typedef {Object} CitedPoint
|
|
27
|
+
* @property {string} indicatorId @property {string} t @property {number} v
|
|
28
|
+
* @property {string} sourceRefUrl
|
|
29
|
+
*/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 适配器(三个,全都要实现)
|
|
33
|
+
|
|
34
|
+
| 适配器 | 何时用 | 实现要点 |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `DshLlmGateway`(`src/host/ai/dsh-llm-gateway.js`) | 部署里有可用 LLM 路由 | `ctx.llm`(`prepareCall` / `stream`)+ `ctx.agentDefaultModel.currentSelection()` 兜底 provider/model;把 `StreamChunk` 映射成 `AiChunk` |
|
|
37
|
+
| `AgentRelayGateway`(可选,M9 后半) | 希望回答出现在**对话流**里 | 把请求作为一条消息提交给当前会话(需在 M0 核对是否有受支持的提交入口),返回 `mode:'relay'` |
|
|
38
|
+
| `DeterministicGateway`(`src/app/ai-deterministic.js`) | **无 LLM / 测试 / 降级** | 由 `core/insight/digest.js` + 模板渲染出真实可读的摘要,`mode:'deterministic'`,**这不是 mock,是产品功能** |
|
|
39
|
+
|
|
40
|
+
**装配顺序**(`host/index.js`):能拿到 `llm` 且设置里 AI 开启 → `DshLlmGateway`;
|
|
41
|
+
设置里选「对话模式」→ `AgentRelayGateway`;否则 → `DeterministicGateway`。
|
|
42
|
+
**任何情况下不可抛错导致面板不可用。**
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. 提示词(纯函数,全部可单测)
|
|
47
|
+
|
|
48
|
+
`src/core/ai/prompts.js` 导出四个**纯函数**,输入是结构化数据,输出是字符串。
|
|
49
|
+
测试断言:包含必要数据点、**不含**任何未传入的数字、长度在预算内。
|
|
50
|
+
|
|
51
|
+
### 2.1 通用系统提示(所有场景共用)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
你是宏观数据分析助手。你的唯一数据来源是下面 <data> 块中提供的观测值。
|
|
55
|
+
|
|
56
|
+
硬性规则:
|
|
57
|
+
1. 只能引用 <data> 中出现过的指标 ID、日期与数值。禁止引入 <data> 之外的数据、事件或新闻。
|
|
58
|
+
2. 每个结论句后面用 [指标ID@日期, 值] 的形式标注依据。
|
|
59
|
+
3. 如果 <data> 不足以回答问题,直接说明"数据不足"并列出缺少什么,不要推测。
|
|
60
|
+
4. 区分事实与解释:先用一句话陈述数据事实,再给出可能解释,并明确标注为"可能"。
|
|
61
|
+
5. 不做点位预测,不给买卖建议,不使用"必然/一定/肯定"等绝对措辞。
|
|
62
|
+
6. 单位与口径必须与 <data> 一致(% 就是 %,不要写成小数)。
|
|
63
|
+
7. 输出中文,Markdown,最多 {maxSections} 个要点,总长不超过 {maxChars} 字。
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2.2 四个场景提示(差异部分)
|
|
67
|
+
|
|
68
|
+
| 场景 | 任务 | 期望输出结构 |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `explain`(单指标解析) | 这个指标是什么 / 本期读数意味着什么 / 与历史对比 / 可能影响的资产或政策 / 反向风险 | 5 段 |
|
|
71
|
+
| `summarize`(时段总结) | 关键变化 / 相互印证 / 背离与矛盾 / 下一个该盯的数据 | 4 段,每段 1–3 条 |
|
|
72
|
+
| `answer`(问答) | 只回答被问到的;先给结论再给依据;标注数据边界 | 结论文 + 依据列表 + 数据边界说明 |
|
|
73
|
+
| `propose`(对话式扩展) | 把自然语言解析成 `IndicatorDef` 候选 | **JSON**,见 §4 |
|
|
74
|
+
|
|
75
|
+
### 2.3 `<data>` 块构造(`core/insight/digest.js`)
|
|
76
|
+
|
|
77
|
+
控 token 是硬需求(一个面板可能 40 个指标 × 数百点)。规则:
|
|
78
|
+
|
|
79
|
+
1. **只送统计量 + 关键点**,不送全序列:
|
|
80
|
+
每个指标送 `{ id, label, unit, freq, latest, latestAt, prev, changeAbs, changePct, yoy, mom,
|
|
81
|
+
range:{from,to}, mean, min, max, stdDev, zScore, percentile, slope, status, sourceRef.url }`;
|
|
82
|
+
另送 **最近 12 个点**(超过 12 点的范围只送降采样后的 12 个代表点 + 极值点)。
|
|
83
|
+
2. 单次 `digest` 预算:**≤ 8000 字符**;超预算按 `importance × score` 截断,并在文本里注明
|
|
84
|
+
「(已省略 N 个次要指标)」——**必须让模型知道自己看到的是子集**。
|
|
85
|
+
3. 数字统一格式化为 `decimals` 位数,避免 `115.004000000000005` 污染。
|
|
86
|
+
4. `digest` 的产物同时是 `DeterministicGateway` 的输入,因此它必须有单测:
|
|
87
|
+
给定 40 指标 → 断言字符数 ≤ 8000、含全部 `importance=5` 的指标、含省略说明。
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. 输出校验(防幻觉的强制执行点)
|
|
92
|
+
|
|
93
|
+
`src/app/ai-validate.js`(纯函数,重点单测):
|
|
94
|
+
|
|
95
|
+
1. **引用校验**:把回答里 `[id@date, value]` 全部提取,逐一与 `<data>` 比对;
|
|
96
|
+
①`id` 不存在 ② 日期不在数据里 ③ 数值不匹配 → 记 `violation`。
|
|
97
|
+
2. **数字校验**:扫描回答中所有数字,若某数字既不在引用列表里、也不在 `<data>` 的统计量里,
|
|
98
|
+
且不在白名单(年份、百分比符号、序数词、"第 1 段"等)→ 记 `unsourced-number`。
|
|
99
|
+
3. **结论处置**:
|
|
100
|
+
- 违规数 = 0 → 通过,`usedPoints` 由引用反推生成。
|
|
101
|
+
- 有违规 → **重试一次**(把违规项作为反馈追加到提示词);仍失败 →
|
|
102
|
+
**降级为确定性摘要**并在 UI 标注「AI 输出未通过溯源校验,已回退」。
|
|
103
|
+
绝不把无法溯源的文本直接给用户看。
|
|
104
|
+
4. **数据不足**:回答若含"数据不足",则 `usedPoints` 允许为空,但必须在 `insufficient` 字段里说明。
|
|
105
|
+
|
|
106
|
+
> 这套校验让"AI 解释"变成**可验证**的功能,而不是一段看起来很像的文本。
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 4. `propose`:对话式添加指标的契约
|
|
111
|
+
|
|
112
|
+
`propose` 返回 JSON(**结构化,不走自由文本**):
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"candidates": [
|
|
117
|
+
{
|
|
118
|
+
"id": "us.mortgage30",
|
|
119
|
+
"label": { "zh": "美国30年期房贷利率", "en": "US 30-Year Mortgage Rate" },
|
|
120
|
+
"unit": "%", "freq": "weekly", "group": "US", "importance": 3,
|
|
121
|
+
"source": { "adapter": "fred", "seriesRef": "MORTGAGE30US", "params": {} },
|
|
122
|
+
"display": { "transform": "raw", "decimals": 2, "polarity": "down-is-good" },
|
|
123
|
+
"confidence": 0.93,
|
|
124
|
+
"rationale": "FRED 有 MORTGAGE30US 周频系列,口径为 30 年期固定利率均值"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"unsupported": [
|
|
128
|
+
{ "request": "德国 Ifo 商业景气指数", "reason": "当前适配器集合无覆盖来源",
|
|
129
|
+
"alternatives": ["新增 ECB/Ifo 适配器", "使用德国 DAX 或欧元区 HICP 作为代理"] }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
强制校验(在 `app/propose-indicator.js` 里,不是靠提示词自觉):
|
|
135
|
+
|
|
136
|
+
1. 每个候选必须过 `validateIndicatorDef`。不合规 → 丢弃并记入 `rejected`。
|
|
137
|
+
2. `source.adapter` 必须在 `sources/registry.js` 里存在;`seriesRef` 必须能**实测取到数据**
|
|
138
|
+
(确认流程:先取数 → 成功才允许加入;失败则降级为 `unsupported`)。
|
|
139
|
+
3. 与现有目录 id 冲突 → 标 `conflict: true`,UI 提示"已存在,是否更新"。
|
|
140
|
+
4. **绝不允许** AI 直接发明 `seriesRef`。若不确定,必须落进 `unsupported` 而**不是**猜一个 ID。
|
|
141
|
+
(这条通过"取数验证"硬性保证:猜的 ID 取不到数,自然无法加入。)
|
|
142
|
+
5. 用户确认后写 `WatchlistRepository`(自定义指标进 `group:'CUSTOM'`),并记 `createdBy:'ai'`。
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 5. 模型工具(让"和 AI 对话"成为一等入口)
|
|
147
|
+
|
|
148
|
+
宿主半注册以下工具(`ctx.tools.register`,`output` 声明必填;具体 schema 在 M0 对照
|
|
149
|
+
`cordis_inspect what:"api" name:"tools"` 与现有工具的写法确定):
|
|
150
|
+
|
|
151
|
+
| 工具名 | 参数 | 返回 | 用途 |
|
|
152
|
+
| --- | --- | --- | --- |
|
|
153
|
+
| `data_overview` | `{ range?, groups?, limit? }` | `{ metrics: Metric[], noteworthy: Noteworthy[], sources }` | 「今天有什么值得关注的」;同时驱动会话内图表卡 |
|
|
154
|
+
| `data_series` | `{ indicator, range, transform?, compareWith? }` | `SeriesView` | 取单指标明细,供模型解释 |
|
|
155
|
+
| `data_search` | `{ q, kind:'indicator'|'source' }` | 候选列表 | 找指标/找源 |
|
|
156
|
+
| `data_watchlist` | `{ action:'list'|'add'|'remove'|'update', item? }` | 最新清单 | **对话式增删自定义指标** |
|
|
157
|
+
| `data_explain` | `{ indicator, range }` | `AiResult` | 让模型主动触发一次解析(通常模型自己就能答,此工具用于深挖) |
|
|
158
|
+
| `data_digest` | `{ range, indicators? }` | 摘要文本 + 引用 | 时段总结 |
|
|
159
|
+
| `data_refresh` | `{ indicator? }` | 刷新结果 | 强制刷新缓存 |
|
|
160
|
+
| `data_health` | `{}` | 各源可用性与最后成功时间 | 排查「今天怎么没数据」 |
|
|
161
|
+
|
|
162
|
+
**渲染**:为 `data_overview` / `data_series` 注册 `tool.call.toolview`(key = 工具名),
|
|
163
|
+
在对话里直接画图 + 列出可点击的来源,而不是只给模型一段文字。
|
|
164
|
+
|
|
165
|
+
**幂等与安全**:`data_watchlist` 的 `add` 必须幂等(同 id 重复 add 不产生重复项);
|
|
166
|
+
`data_refresh` 有节流(60s 内重复调用返回缓存并注明)。
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 6. 缓存与成本控制
|
|
171
|
+
|
|
172
|
+
| 层 | key | TTL | 说明 |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| 数据快照 | `sourceId+seriesRef+range` | 按频率(见 `02` §4) | 与 AI 无关 |
|
|
175
|
+
| AI 结果 | `sha256(op + indicatorDigest + model)` | 数据有更新才失效 | 同一指标同一范围重复点"解析"不应重复计费 |
|
|
176
|
+
| 提示词组装 | 无缓存 | — | 便宜 |
|
|
177
|
+
|
|
178
|
+
- AI 结果的缓存命中在返回里标 `cached: true`,UI 显示「来自缓存」。
|
|
179
|
+
- 并发同一 key 的 AI 请求合并(同 `02` §4 的 in-flight 去重)。
|
|
180
|
+
- 每次调用记录 `tokensIn/tokensOut/ms` 到日志(不落会话,只进插件自己的日志文件)。
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 7. AI 能力的验收标准(可机械核对)
|
|
185
|
+
|
|
186
|
+
| # | 标准 |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| 1 | 关掉 LLM(不配置 provider)时,面板三个 AI 入口仍然给出可读输出,并明确标注「确定性摘要模式」 |
|
|
189
|
+
| 2 | 任何 AI 输出都能展开出 `usedPoints`,且每条的 `sourceRefUrl` 可打开 |
|
|
190
|
+
| 3 | 人为构造一个"引用不存在指标"的假 LLM 适配器 → 校验器必须拦下并触发降级(有专门单测) |
|
|
191
|
+
| 4 | 问一个数据范围外的问题(如"明天油价会涨吗")→ 输出"数据不足",不产生预测 |
|
|
192
|
+
| 5 | `propose` 对不存在的源(Ifo 指数)必须返回 `unsupported` + 替代方案,**不得**返回虚构 seriesRef(有单测:给它一个强制编造的假 LLM,校验器必须拦住) |
|
|
193
|
+
| 6 | 同一问题连续问两次,第二次命中缓存(`cached:true`,无重复模型调用) |
|
|
194
|
+
| 7 | `digest` 在 40 指标下 ≤ 8000 字符且 `importance=5` 全覆盖(单测) |
|