Implement the full QPER intelligent analysis pipeline: - Phase E+: Block-based standardization for all 7 R tools, DynamicReport renderer, Word export enhancement - Phase Q: LLM intent parsing with dynamic Zod validation against real column names, ClarificationCard component, DataProfile is_id_like tagging - Phase P: ConfigLoader with Zod schema validation and hot-reload API, DecisionTableService (4-dimension matching), FlowTemplateService with EPV protection, PlannedTrace audit output - Phase R: ReflectionService with statistical slot injection, sensitivity analysis conflict rules, ConclusionReport with section reveal animation, conclusion caching API, graceful R error classification End-to-end test: 40/40 passed across two complete analysis scenarios. Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
6.1 KiB
Markdown
172 lines
6.1 KiB
Markdown
# **SSA-Pro 动态结果渲染与通信协议规范**
|
||
|
||
**文档版本:** v1.0
|
||
|
||
**创建日期:** 2026-02-20
|
||
|
||
**解决痛点:** 统一 100+ 种统计工具的输出格式,实现后端免维护、前端动态渲染。
|
||
|
||
**核心思想:** R 端输出的不是“数据”,而是“UI 渲染区块 (Blocks)”。
|
||
|
||
## **1\. 核心架构思想:区块化 (Block-based Architecture)**
|
||
|
||
借鉴 Notion 和 Jupyter Notebook 的设计思想,我们将所有可能的统计输出抽象为**有限的几种“基础积木(Blocks)”**。
|
||
|
||
不论是 T 检验、生存分析还是复杂的回归模型,其输出结果最终都可以拆解为以下 4 种基础类型的组合:
|
||
|
||
1. **text / markdown**: 用于 AI 解读、简单结论、警告提示。
|
||
2. **table**: 用于三线表、矩阵、数据框。
|
||
3. **image**: 用于所有的可视化图表。
|
||
4. **key\_value**: 用于展示核心统计量(如 P 值、t 值等高亮卡片)。
|
||
|
||
## **2\. JSON 通信协议定义 (The Universal Protocol)**
|
||
|
||
R 服务最终返回给 Node.js,Node.js 再原封不动透传给前端的 JSON 结构,**必须**是如下的标准协议:
|
||
|
||
{
|
||
"status": "success",
|
||
"trace\_log": \[ ...执行日志... \],
|
||
"reproducible\_code": "library(ggplot2)...",
|
||
|
||
// ⭐ 核心变革:统一的 blocks 数组
|
||
"report\_blocks": \[
|
||
{
|
||
"type": "markdown",
|
||
"content": "\*\*AI 解读:\*\* 结果表明新药组的血压下降幅度显著大于对照组..."
|
||
},
|
||
{
|
||
"type": "table",
|
||
"title": "Table 1\. 组间差异比较",
|
||
"data": {
|
||
"headers": \["Group", "N", "Median \[IQR\]", "P-Value"\],
|
||
"rows": \[
|
||
\["Drug", "60", "14.5 \[12.1-16.8\]", "\< 0.001 \*\*"\],
|
||
\["Placebo", "60", "8.2 \[6.5-10.4\]", ""\]
|
||
\]
|
||
},
|
||
"footer": "Note: IQR \= Interquartile Range; \*\* P \< 0.01"
|
||
},
|
||
{
|
||
"type": "image",
|
||
"title": "Figure 1\. 血压下降值分布",
|
||
"format": "base64",
|
||
"src": "iVBORw0KGgoAAAANSUhEUgAAAAE...", // base64 字符串
|
||
"caption": "箱线图展示了两组的分布情况"
|
||
},
|
||
{
|
||
"type": "key\_value",
|
||
"title": "核心指标",
|
||
"items": \[
|
||
{"label": "W Statistic", "value": "2845.5"},
|
||
{"label": "Effect Size (r)", "value": "0.45", "status": "warning"}
|
||
\]
|
||
}
|
||
\]
|
||
}
|
||
|
||
## **3\. R 端的开发规范 (如何吐出这个协议?)**
|
||
|
||
R 工程师在封装 Wrapper 时,不需要关心前端怎么画图,只需要把结果打包成上述的 list。
|
||
|
||
**R 代码示例 (以 T 检验为例):**
|
||
|
||
run\_tool \<- function(input) {
|
||
\# ... 执行计算 res \<- t.test(...) ...
|
||
\# ... 画图并转 base64 ...
|
||
|
||
\# 统一打包为 Blocks
|
||
report\_blocks \<- list(
|
||
list(
|
||
type \= "markdown",
|
||
content \= sprintf("独立样本 T 检验结果显示,P值为 %.3f。", res$p.value)
|
||
),
|
||
list(
|
||
type \= "table",
|
||
title \= "描述统计与检验结果",
|
||
data \= list(
|
||
headers \= c("统计量", "数值"),
|
||
rows \= list(
|
||
c("t 值", round(res$statistic, 2)),
|
||
c("自由度 df", round(res$parameter, 2)),
|
||
c("P 值", res$p.value)
|
||
)
|
||
)
|
||
),
|
||
list(
|
||
type \= "image",
|
||
title \= "均值差异对比图",
|
||
format \= "base64",
|
||
src \= base64\_image\_string
|
||
)
|
||
)
|
||
|
||
return(list(
|
||
status \= "success",
|
||
trace\_log \= logs,
|
||
report\_blocks \= report\_blocks, \# 直接返回 Blocks 数组
|
||
reproducible\_code \= code\_str
|
||
))
|
||
}
|
||
|
||
## **4\. 前端的动态渲染策略 (Dynamic Renderer)**
|
||
|
||
前端彻底解放。**前端不需要写 TTestResult.tsx 或 AnovaResult.tsx,只需要写一个 DynamicReport.tsx。**
|
||
|
||
前端只需遍历 report\_blocks 数组,根据 type 挂载对应的基础组件即可。
|
||
|
||
**React 伪代码:**
|
||
|
||
// 1\. 准备基础积木组件
|
||
const MarkdownBlock \= ({ content }) \=\> \<ReactMarkdown\>{content}\</ReactMarkdown\>;
|
||
const SciTableBlock \= ({ title, data, footer }) \=\> (
|
||
\<div\>
|
||
\<h4\>{title}\</h4\>
|
||
\<table className="sci-table"\>
|
||
\<thead\>\<tr\>{data.headers.map(h \=\> \<th\>{h}\</th\>)}\</tr\>\</thead\>
|
||
\<tbody\>{data.rows.map(row \=\> \<tr\>{row.map(cell \=\> \<td\>{cell}\</td\>)}\</tr\>)}\</tbody\>
|
||
\</table\>
|
||
{footer && \<p\>{footer}\</p\>}
|
||
\</div\>
|
||
);
|
||
const ImageBlock \= ({ title, src, caption }) \=\> (
|
||
\<div\>
|
||
\<h4\>{title}\</h4\>
|
||
\<img src={\`data:image/png;base64,${src}\`} alt={title} /\>
|
||
\<p\>{caption}\</p\>
|
||
\</div\>
|
||
);
|
||
|
||
// 2\. 核心动态渲染器
|
||
export const DynamicReport \= ({ blocks }) \=\> {
|
||
return (
|
||
\<div className="report-container space-y-8"\>
|
||
{blocks.map((block, index) \=\> {
|
||
switch (block.type) {
|
||
case 'markdown':
|
||
return \<MarkdownBlock key={index} {...block} /\>;
|
||
case 'table':
|
||
return \<SciTableBlock key={index} {...block} /\>;
|
||
case 'image':
|
||
return \<ImageBlock key={index} {...block} /\>;
|
||
case 'key\_value':
|
||
return \<KeyValueBlock key={index} {...block} /\>;
|
||
default:
|
||
return \<div key={index}\>未知的区块类型: {block.type}\</div\>;
|
||
}
|
||
})}
|
||
\</div\>
|
||
);
|
||
};
|
||
|
||
## **5\. Node.js 的角色 (Zero-Maintenance)**
|
||
|
||
通过这套协议,Node.js 后端变成了绝对的 **“零维护 (Zero-Maintenance)”** 状态。
|
||
|
||
如果未来 R 团队新增了第 101 个工具(比如一个极度复杂的神经网络模型,返回 5 张表、10 张图),Node.js 的代码 **一行都不需要改**!因为它只负责把 R 返回的 JSON 原样抛给 React。
|
||
|
||
## **6\. 总结:多表多图的终极解法**
|
||
|
||
* **问:如何展示多表多图?**
|
||
* **答:R 脚本往 report\_blocks 数组里不断 push 即可。想展示几张就 push 几个 image block。**
|
||
|
||
这种“协议化、区块化”的设计,是现代 SaaS 平台(如飞书、Notion、Jupyter)的基石架构。它赋予了 R 团队极大的排版自由度,同时彻底保护了前端和后端的架构稳定性。 |