# pi-agent 产物预览设计
pi-agent 在运行过程中会通过 write、edit、bash 等工具生成各类文件(HTML 报告、图片、PDF、Office 文档等)。产物预览机制负责将这些文件以可交互的方式呈现给用户。
# 设计原则
- 文件在本地,不下载 — 产物就在工作区目录里,用户需要的是"打开"而不是"下载"
- 系统默认应用优先 — HTML 用浏览器打开,图片用系统预览,PDF 用 Preview
- 零后端改动 — 复用现有
ArtifactController的 content 接口,前端按 MIME type 分支渲染 - 安全沙箱 — iframe 预览使用
sandbox="allow-scripts",不暴露主页面上下文
# 架构链路
工具执行 (write/edit/bash/present_files)
↓ 返回 details.path / details.artifacts / details.files
WebSseEventWriter.artifactFrom() + artifactsFrom()
↓ 提取路径 → ArtifactController.metadata()
↓ 生成 Artifact 元数据 (id, path, mimeType, previewable...)
SSE 事件 tool_execution_end
↓ payload.artifact = { ... } ← 单工件(write/edit/bash)
↓ payload.artifacts = [{ ... }, ...] ← 多工件(present_files)
前端 reducer + stream
↓ 单工件 → tool.artifact,多工件 → tool.artifacts
↓ 每个工件触发 onArtifact → artifacts 状态更新
前端 ArtifactDrawer
↓ 按 mimeType 分支渲染
↓ text/html → iframe 内嵌 + "新标签页打开"
↓ image/* → <img> 直接展示
↓ .md → ReactMarkdown 渲染
↓ .docx/.xlsx → docx-preview / xlsx 库解析
↓ 其他文本 → <pre> 原样展示
# 关键组件
# 后端:ArtifactController
- 列表接口
GET /api/artifacts?sessionId=xxx— 返回本会话所有工件元数据 - 内容接口
GET /api/artifacts/{id}/content?sessionId=xxx— 返回文件二进制内容 - 自动检测 MIME type(
Files.probeContentType+ fallback 猜测) - 工作区边界校验:
safePath()确保不能越出 workspace 根目录 - 预览大小限制:1MB 以内
# 前端:ArtifactDrawer
位于 web/src/components/DetailsPanels.tsx,按文件类型分支渲染:
| MIME / 后缀 | 渲染方式 | 说明 |
|---|---|---|
| --- | --- | --- |
image/* | <img> | 直接展示 |
text/html / .html | iframe + 新标签页链接 | iframe 快速预览,点击在新标签页完整渲染 |
text/markdown / .md | ReactMarkdown | GFM 支持 |
.docx | docx-preview 库 | 内嵌渲染 |
.xlsx | xlsx 库 | 表格渲染,支持工作表切换 |
| 其他文本 | <pre> | 原样展示 |
| 不可预览 | 提示下载 | previewable=false 时展示 |
# HTML 预览交互
HTML 文件采用 iframe 内嵌 + 新标签页打开 双模式:
- iframe:快速预览,
sandbox="allow-scripts"隔离 - 新标签页:点击链接 →
window.open(contentUrl)→ 浏览器完整渲染(CSS/JS/交互全支持)
不需要额外后端接口 — ArtifactController 已经正确返回 text/html Content-Type。
# 产物自动检测机制
bash 工具在执行前后分别对工作区做快照(artifactSnapshot()),对比文件变化自动识别新生成或修改的文件:
- 最大扫描深度 6 层、最多 2000 个文件
- 跳过
.git/、node_modules/、target/ - 只识别特定后缀:
.png.jpg.pdf.md.html.json.csv等 - 最多返回 8 个变更文件,保持 SSE 轻量
write 和 edit 工具通过 details.path 直接声明产物路径。
# present_files 工具:主动推送产物
除了自动检测,模型还可以通过 present_files 工具主动把文件推送给用户预览。
# 设计思路
- 与 askuser 对比:askuser 是双向交互(模型→用户→模型),present_files 是单向推送(模型→用户),fire-and-forget,不暂停 Agent
- 触发时机:bash/write/edit 产生了值得展示的成果(HTML 报告、图片、数据表等),模型主动调用 present_files 让用户直接看到结果
- 多文件支持:一次调用可推送多个文件,SSE 通过
artifacts列表(而非单个artifact)传递
# 工具 Schema
{
"name": "present_files",
"description": "将工作区内已生成的文件主动推送给用户预览",
"parameters": {
"type": "object",
"properties": {
"files": {
"type": "array",
"description": "相对工作区的文件路径列表",
"items": { "type": "string" }
}
},
"required": ["files"]
}
}
# 交互流程
模型调用 present_files(files: ["report.html", "chart.png"])
↓
PresentFilesTool 校验路径(工作区边界 + 文件存在)
↓
返回 ToolResult(details.files = ["report.html", "chart.png"])
↓
WebSseEventWriter.artifactsFrom() 提取多工件元数据
↓
SSE: tool_execution_end { artifacts: [{id, path, name, mimeType, ...}, ...] }
↓
前端 reducer 存储 tool.artifacts
前端 stream 对每个工件调用 onArtifact → artifacts 状态更新
↓
聊天内联展示工件卡片 + ArtifactDrawer 自动刷新列表
# 与 SessionStore 的集成
SessionStore.artifactReferences() 识别 present_files 工具的 details.files,将每个路径注册为工件引用。会话历史恢复时,ArtifactDrawer 仍能正确展示这些工件。
# 文件位置
- 后端工具:
pi-agent-v1/src/main/java/piagent/tools/interaction/PresentFilesTool.java - 后端工件:
pi-agent-v1/src/main/java/piagent/web/controller/ArtifactController.java - 后端 SSE:
pi-agent-v1/src/main/java/piagent/web/support/WebSseEventWriter.java - 后端存储:
pi-agent-v1/src/main/java/piagent/session/SessionStore.java - 前端渲染:
web/src/components/DetailsPanels.tsx - 前端状态:
web/src/main.tsx(reducer + stream) - 前端类型:
web/src/types.ts(Artifact、Tool.artifacts) - 样式:
web/src/styles.css(.html-preview相关)