pi-agent 产物预览设计

pi-agent 在运行过程中会通过 writeeditbash 等工具生成各类文件(HTML 报告、图片、PDF、Office 文档等)。产物预览机制负责将这些文件以可交互的方式呈现给用户。

设计原则

  1. 文件在本地,不下载 — 产物就在工作区目录里,用户需要的是"打开"而不是"下载"
  2. 系统默认应用优先 — HTML 用浏览器打开,图片用系统预览,PDF 用 Preview
  3. 零后端改动 — 复用现有 ArtifactController 的 content 接口,前端按 MIME type 分支渲染
  4. 安全沙箱 — 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

前端:ArtifactDrawer

位于 web/src/components/DetailsPanels.tsx,按文件类型分支渲染:

MIME / 后缀渲染方式说明
---------
image/*<img>直接展示
text/html / .htmliframe + 新标签页链接iframe 快速预览,点击在新标签页完整渲染
text/markdown / .mdReactMarkdownGFM 支持
.docxdocx-preview 库内嵌渲染
.xlsxxlsx 库表格渲染,支持工作表切换
其他文本<pre>原样展示
不可预览提示下载previewable=false 时展示

HTML 预览交互

HTML 文件采用 iframe 内嵌 + 新标签页打开 双模式:

不需要额外后端接口 — ArtifactController 已经正确返回 text/html Content-Type。

产物自动检测机制

bash 工具在执行前后分别对工作区做快照(artifactSnapshot()),对比文件变化自动识别新生成或修改的文件:

writeedit 工具通过 details.path 直接声明产物路径。

present_files 工具:主动推送产物

除了自动检测,模型还可以通过 present_files 工具主动把文件推送给用户预览。

设计思路

工具 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 仍能正确展示这些工件。

文件位置