Files
AIclinicalresearch/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 动态结果渲染与通信协议规范.md
HaHafeng 428a22adf2 feat(ssa): Complete Phase 2A frontend integration - multi-step workflow end-to-end
Phase 2A: WorkflowPlannerService, WorkflowExecutorService, Python data quality, 6 bug fixes, DescriptiveResultView, multi-step R code/Word export, MVP UI reuse. V11 UI: Gemini-style, multi-task, single-page scroll, Word export. Architecture: Block-based rendering consensus (4 block types). New R tools: chi_square, correlation, descriptive, logistic_binary, mann_whitney, t_test_paired. Docs: dev summary, block-based plan, status updates, task list v2.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 23:09:27 +08:00

6.1 KiB
Raw Blame History

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.jsNode.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 团队极大的排版自由度,同时彻底保护了前端和后端的架构稳定性。