首页
/ PrivateGPT Workbench 的"单一事实来源"体系:从 ui/docs/SOURCE_OF_TRUTH.md 读懂单文件 UI 的实现契约

PrivateGPT Workbench 的"单一事实来源"体系:从 ui/docs/SOURCE_OF_TRUTH.md 读懂单文件 UI 的实现契约

2026-09-06 14:34:43作者:秋阔奎Evelyn

本文以 ui/docs/SOURCE_OF_TRUTH.md 为主体,系统讲解 PrivateGPT 工作台(Workbench)演示 UI 的文档治理体系:运行时实现的唯一归属、人类与 Agent 的入口分层、Fern 生成的 OpenAPI 契约位置,以及十余条只有读源码才能验证的"关键实现笔记"。读完本文,你既能掌握如何维护一套人与 AI Agent 共用的 UI 文档结构,也能对照 ui/index.html 的 7974 行单文件实现,理解默认集合(Collection)、有状态引导(Onboarding)、外观变量、推理强度(Reasoning Effort)与 Code Execution 会话连续性等核心机制在代码中的真实落点。

一、文档体系总览:ui/ 目录的单一事实来源

SOURCE_OF_TRUTH.md 开宗明义:"This document defines where the authoritative guidance for ui/ lives."(本文档定义 ui/ 的权威指引存在于何处)。它不是产品文档,也不是样式指南,而是一份文档治理清单:规定哪个文件是唯一运行时实现、哪类内容应该写在哪个文件里、API 契约以哪里为准。ui/README.md 中的目录地图与之一一对应:

路径 角色
ui/index.html 唯一的运行时实现文件(Workbench 演示 UI 的全部 HTML/CSS/JS)
ui/README.md 顶层导航与目录地图
ui/AGENTS.md Codex / OpenAI 风格 Agent 的工作流说明
ui/CLAUDE.md Claude Code 的工作流说明
ui/docs/PRD.md 产品行为与信息架构(约 999 行)
ui/docs/STYLE_GUIDE.md 视觉与交互方向(约 599 行)
ui/docs/SOURCE_OF_TRUTH.md 权威路径、API 契约与文档归属(本文主题)
ui/references/* 供样式指南使用的非运行时视觉参考图

这种"单文件运行时 + 分门别类的文档"组织方式,使得运行时逻辑保持在一个文件内便于审查(README 明确要求"Keep the app implementation in index.html unless a separate refactor is explicitly requested"),而所有设计决策外置到 docs/,避免规则散落在实现旁边。

二、人类与 Agent 的入口分层

SOURCE_OF_TRUTH.md 将入口分为两类:

  • 运行时实现:仅 ui/index.html 一个文件;
  • 人类与 Agent 入口ui/README.md(顶层导航)、ui/AGENTS.md(Codex 风格 Agent)、ui/CLAUDE.md(Claude Code)。

从源码结构看,两个 Agent 入口文件的分工非常一致。ui/AGENTS.md 要求 Agent 在修改行为、UI 或 API 接线前按序阅读五个文件:

  1. ui/README.md
  2. ui/docs/SOURCE_OF_TRUTH.md
  3. ui/docs/PRD.md
  4. ui/docs/STYLE_GUIDE.md
  5. ui/index.html

并给出两条硬性规则:把 SOURCE_OF_TRUTH.md 视为 API 契约路径与文档归属的权威指针;当实现与文档不一致时,"fix the disagreement in the same change"(在同一次变更中修复分歧)。ui/CLAUDE.md 内容更精简,但同样要求先读上述共享文档,并强调"Use the shared docs above as the source of truth instead of duplicating product or design rules here"——即不在这类入口文件里重复产品或设计规则。这形成了清晰的文档金字塔:入口文件只做指针,规则下沉到 PRD 与样式指南,事实核对以本文件和源码为准

三、API 契约:以 Fern 生成的 OpenAPI 为唯一来源

SOURCE_OF_TRUTH.md 对 API 契约给出了明确且排他的规定:

Workbench should follow the Fern-generated OpenAPI schema at: ../../fern/openapi/openapi.json(相对仓库根目录即 fern/openapi/openapi.json)。Do not maintain a duplicated UI-local OpenAPI snapshot.(不要维护一份 UI 本地的 OpenAPI 副本。)

该约束在仓库中可以得到验证:fern/openapi/openapi.json 确实定义了 Workbench 实际调用的核心路径,包括 /v1/messages(流式对话)、/v1/files(按 scope_id 隔离的文件上传/列表)和 /v1/files/{file_id}/content(按 scope_id 下载文件内容)。在 ui/index.html 中可以逐一找到这些路径的调用方:

  • 流式对话:apiStreamFetch("/v1/messages", ...)ui/index.html),以及外观生成时复用同一端点的 apiFetch("/v1/messages", ...)ui/index.html);
  • 会话文件列表:/v1/files?scope_id={chat.id}ui/index.html);
  • 会话文件下载:/v1/files/{file_id}/content?scope_id={chat.id}ui/index.html)。

此外,UI 还调用了 /v1/models(加载模型,ui/index.html)、/v1/artifacts/list/v1/artifacts/ingest(知识库文档,ui/index.html)、/v1/skills(技能,ui/index.html)等端点。由于仓库规定不保留 UI 本地的 OpenAPI 快照,阅读这份演示 UI 时接口字段的第一手依据始终是 fern/openapi/openapi.json,而不是 UI 代码中的内联类型——这正是"单一事实来源"原则在 API 层的具体化。

四、工作规则:内容归属与同步更新

SOURCE_OF_TRUTH.md 的 "Working Rules" 一节用五条规则划定了文档归属边界:

  • 产品需求 → ui/docs/PRD.md
  • 视觉规则 → ui/docs/STYLE_GUIDE.md
  • Agent 专属工作流规则 → ui/AGENTS.mdui/CLAUDE.md
  • 参考图片 → ui/references/
  • 运行时代码 → ui/index.html

并附上一条关键纪律:"If a change affects behavior, visuals, persistence, security posture, or API request/response handling, update the relevant docs in the same change."(若变更影响行为、视觉、持久化、安全姿态或 API 请求/响应处理,必须在同一次变更中更新相关文档。)ui/README.md 的 "Working Rules" 与之互为镜像("Keep docs and implementation aligned whenever behavior, visuals, persistence, or API wiring changes")。对以 Agent 协作为主的仓库,这条规则实际上把"文档腐化"预防到了提交粒度。

五、关键实现笔记(上):状态模型与行为机制

SOURCE_OF_TRUTH.md 的 "Key Implementation Notes" 自称是"those things not obvious from reading index.html"(单看 ui/index.html 不易察觉、但未来的维护者与 Agent 必须知道的事项)。以下逐条对照源码验证:

5.1 集合(Collection)存于 Settings 而非 Documents 面板

文档指出:集合不是放在文档面板,而是 state.context.documents.defaultCollection所有文档与聊天操作共用的唯一全局集合名。源码印证:状态初始化时默认值为 "pgpt_collection"ui/index.html),设置面板输入框 #defaultCollection 与之双向同步(ui/index.html),聊天副标题(ui/index.html)、技能过滤上下文 skill_filter.collectionui/index.html)以及引导流程(ui/index.html)都直接回退读取该字段——"single global collection name"的描述与调用链完全吻合。

5.2 有状态的 Onboarding

state.onboarding 控制首跑引导浮层:当前步骤与最近一次"活体校验"结果都写在其中;浮层的显示条件只有一个——state.onboarding.completed !== true。源码中的渲染逻辑正是如此:const isOpen = state.onboarding?.completed !== trueui/index.html),步骤由 state.onboarding?.step 决定(ui/index.html),校验结果缓存在 state.onboarding.lastCheckui/index.html)。

5.3 外观覆盖是运行时变量

state.uiAppearance 通过 applyAppearance() 同时驱动文案、功能可见性与 CSS 自定义属性。源码中 applyAppearance() 位于 ui/index.html,调用 paintAppearance()state.uiAppearance || DEFAULT_APPEARANCE 写入 DOM;功能开关统一走 featureEnabled(key),其语义是 state.uiAppearance?.features?.[key] !== falseui/index.html),即默认开启、显式关闭。品牌名、欢迎标题等文案同理回退到 DEFAULT_APPEARANCEui/index.html)。Settings 与 Onboarding 写入的是同一结构,这与文档"Settings and onboarding write into the same structure"一致。

5.4 外观生成复用聊天 API

文档称主题简报(theme brief)走 POST /v1/messages,解析为 JSON 后回写到外观表单中用户可手工编辑的字段。源码验证:引导/外观生成流程调用 apiFetch("/v1/messages", ...)ui/index.html),且状态合并逻辑把用户已有的 uiAppearance(含 palettefeatures)与生成结果做深合并(ui/index.html),保证生成不覆盖手工配置——这正是"written back into the same appearance form fields"的实现方式。

5.5 自定义工具执行被收敛到单条助手气泡

文档说明:初始响应、工具结果与后续回答都渲染在同一个助手消息气泡内;hidden: true 的消息只承载 API 历史、永不渲染。消息渲染的核心函数是 blocksToHtml()ui/index.html),请求构建侧的 sanitizeMessages() 负责把隐藏消息从渲染流中剥离。这一设计让"多轮工具调用"在界面上呈现为一次连贯的助手发言,而非碎片化的多条气泡。

六、关键实现笔记(中):交互与渲染细节

6.1 自定义模型选择器

模型选择器不是原生 <select>,而是由 #modelSelectBtn + #modelDropdown 两个自定义元素构成,由 renderModelSelect()ui/index.html)填充。选择器内部同时承载模型列表与推理强度选项,"选择一个模型或强度时,就地更新现有弹窗 DOM,保留搜索与滚动位置,仅由触发按钮、Esc 或外部点击关闭"——这一行为约束避免了重新渲染导致的输入丢失。

6.2 推理强度与 thinking 请求字段

每个聊天把推理强度存在 chat.settings.reasoningEffort,取值为 nulllowmediumhighmaxxhigh,默认 null(None)。源码中初始化即为 reasoningEffort: nullui/index.html),对旧数据还会做缺省补全(ui/index.html)。请求构建时该值被转换为:

const reasoningEffort = state.enableThinking !== false ? chat.settings.reasoningEffort || null : null;
// ...
body = {
  model: state.selectedModel,
  messages: sanitizeMessages(chat.messages),
  tools, tool_context, mcp_servers,
  stream: true,
  max_tokens: 4096,
  thinking: { enabled: Boolean(reasoningEffort), type: reasoningEffort }
};

ui/index.html。)文档提到"effort options are enabled from the selected model's capabilities.effort response"——源码中当当前选中强度不在模型能力列表内时会被重置为 nullui/index.html),这正是对模型 capabilities.effort 响应的消费。

6.3 Composer 加号菜单与附件复用既有上传路径

Composer 的操作被收敛到一个加号按钮:菜单先列出 "Add files",随后是既有的聊天工具/上下文控件,不存在独立的 Build 模式或 Build 按钮。附件行为按 Code Execution 状态分叉——开启时会发送到当前 code-execution 会话,否则摄取进已配置的 Documents 集合。源码分叉点在 ui/index.htmlif (chat?.settings?.enabledCodeExecution) await uploadToSession(files)

6.4 Hash 导航

syncHash() / restoreFromHash() 让 URL 与当前视图保持同步,格式为 #context/{tab}#chat/{id}#settings#apiDebugger。源码实现(ui/index.html)与文档完全一致:syncHash() 依据 runtime.viewstate.activeChatId 拼出 hash 后用 history.replaceState 静默更新;restoreFromHash() 在页面加载时按前缀解析,且只接受白名单内的 context tab(CONTEXT_TAB_IDS 包含 documentsdatabaseswebmcpskillscustomToolscodeExecution)与已存在的 chat id——防止脏 hash 破坏视图状态。

6.5 滚动渐隐、开关样式与浮动面板"磨砂"

  • 滚动渐隐.chat-list-wrap.messages 使用 mask-image,配合 --fade-top-stop / --fade-bot-stop 两个自定义属性,由 updateMessagesFade()ui/index.html)与 updateChatListFade()ui/index.html)在滚动时更新:距顶部/底部 6px 之内时渐隐宽度归零,否则为 28px(消息区)或 22px(会话列表)。
  • 开关:所有 input[type="checkbox"] 都被样式化为无原生外观的自定义 CSS 药丸开关。
  • 浮动面板磨砂.modal-card.menu-panel.model-dropdown 覆写共享的 glass 组,采用近实心深色背景(rgba(10,12,22,0.82–0.94))、blur(72px) saturate(1.4) 与上浅下深的 to bottom 渐变,用于保证可读性与视觉落定感。

这三条属于纯表现层契约,读 ui/index.html 的样式段固然能看到,但"为什么"(可读性、统一开关手感)只有这份实现笔记给出了意图。

七、关键实现笔记(下):Code Execution 的端到端契约

Code Execution 是这份实现笔记中篇幅最大的一组条目,涉及工具声明、渲染、会话连续性与文件 I/O 四个层面,逐条对照源码:

7.1 工具声明与后端展开

Tools 菜单中的 Code Execution 开关在请求的 tools 数组里发送一个简写声明:

if (chat.settings.enabledCodeExecution) {
  tools.push({ name: "code_execution", type: "code_execution_v1" });
}

ui/index.html。)后端会把该声明展开为 bashtext_editor(view / str_replace / create / insert)、present_filespresent_server 等具体工具。开关状态按聊天粒度持久化在 chat.settings.enabledCodeExecutionui/index.htmlui/index.html)。

7.2 渲染:isCodeExecTool 白名单与块配对

渲染侧的关键函数 isCodeExecTool(name)ui/index.html)维护白名单 bashviewstr_replacecreateinsertpresent_filespresent_serverblocksToHtml() 在遇到这些工具的 tool_use / server_tool_use 块时(ui/index.html),把相邻的 tool_use + tool_result 配对合并为一个 .code-exec-block details 元素,呈现终端输出、带行号的文件视图、diff 高亮与退出码徽章;对应样式集中在 ui/index.html.code-exec-block 样式族中。白名单同时驱动"是否配对"与"逐工具如何渲染"两个决策,避免把无关工具误并入代码执行样式。

7.3 会话连续性:container 字段

文档强调:"the container field must be set whenever code execution tools are active"。请求构建代码印证(ui/index.html):

if (chat.settings.enabledCodeExecution) {
  body.container = chat.id;
}

即把 chat.id 作为 container 放进 ChatBody,后端据此在整个聊天的所有消息间复用同一个沙箱会话。从源码结构看,这解释了为何 Code Execution 的文件与进程状态可以跨消息保持——会话身份由聊天 id 承载,而非每次请求新建。

7.4 文件上传:直接进入会话工作区

开启 Code Execution 后 Composer 工具栏出现 Files 按钮,选择的文件以 multipart/form-data 直接上传到 POST /v1/files?scope_id={chat.id}ui/index.html),落入会话工作区,可被模型的 bash/文件工具访问;随后通过 GET /v1/files?scope_id={chat.id} 列出(ui/index.html)、DELETE /v1/files/{file_id}?scope_id={chat.id} 删除(ui/index.html)。这与 fern/openapi/openapi.json/v1/files 端点按 scope_id 隔离的设计一致。

7.5 文件下载与服务链接

  • 下载present_files 结果中的 local_resource 块被渲染为 .code-exec-download 锚点,指向 GET /v1/files/{file_id}/content?scope_id={chat.id}ui/index.html),file_idmime_type 取自 local_resource 块 schema;
  • 服务链接present_server 结果中的 resource_link 块被渲染为 .code-exec-server-link 锚点(地球图标 + 服务名 + 隧道 URL),新标签页打开;urinamedescription 取自 resource_link 块 schema,tool_use 摘要显示 service_name:port(及可选 initial_path 深链)。

八、验证方式与维护闭环

维护这套单文件 UI 有一个明确的最低验证动作,记录在 ui/README.md(由 ui/AGENTS.md 转引为 Agent 必做步骤):对 ui/index.html 的改动必须验证内联脚本可解析:

node -e "const fs=require('fs'); const html=fs.readFileSync('./ui/index.html','utf8'); const m=html.match(/<script>([\s\S]*)<\/script>/); if(!m) throw new Error('script tag not found'); new Function(m[1]); console.log('script ok')"

该命令提取最后一个 <script> 块并尝试构造函数以做语法级校验,是"单文件运行时"架构下最轻量的回归防线。再结合 SOURCE_OF_TRUTH 的同步更新规则(行为/视觉/持久化/安全/API 处理变更必须同改文档)与 Agent 入口的"发现实现与文档分歧须同次修复"要求,ui/ 形成了一条完整闭环:文档定义权威位置 → 入口文件只做指针 → 源码是最终事实 → 脚本解析校验兜底

九、小结:这份 SOURCE_OF_TRUTH 的工程价值

ui/docs/SOURCE_OF_TRUTH.md 的价值不在于它描述的某个功能,而在于它示范了一套可复用的"人机协作型 UI 文档治理"范式:

  1. 归属清晰:每条内容只有一个权威落点,API 契约指回 fern/openapi/openapi.json 而非 UI 本地快照;
  2. 入口分层:人读 README,Agent 读 AGENTS/CLAUDE,两者共享同一组 docs 作为唯一事实源;
  3. 实现笔记前置隐性知识defaultCollection 的全局性、container = chat.id 的强制约束、thinking: { enabled, type } 的请求形态、hidden: true 消息不渲染等约定,若不写成文档,维护者(尤其是新接入的编码 Agent)极易从 ui/index.html 的局部代码中得出错误结论。

对希望在自己的仓库中引入"单文件演示 UI + 多 Agent 协作"模式的团队,这套 README → SOURCE_OF_TRUTH → PRD / STYLE_GUIDE → references 的分层与"同变更同步文档"纪律,是比任何单条规则都更值得借鉴的部分。

登录后查看全文
热门项目推荐
相关项目推荐