PrivateGPT Workbench 的"单一事实来源"体系:从 ui/docs/SOURCE_OF_TRUTH.md 读懂单文件 UI 的实现契约
本文以 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 接线前按序阅读五个文件:
ui/README.mdui/docs/SOURCE_OF_TRUTH.mdui/docs/PRD.mdui/docs/STYLE_GUIDE.mdui/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.md与ui/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.collection(ui/index.html)以及引导流程(ui/index.html)都直接回退读取该字段——"single global collection name"的描述与调用链完全吻合。
5.2 有状态的 Onboarding
state.onboarding 控制首跑引导浮层:当前步骤与最近一次"活体校验"结果都写在其中;浮层的显示条件只有一个——state.onboarding.completed !== true。源码中的渲染逻辑正是如此:const isOpen = state.onboarding?.completed !== true(ui/index.html),步骤由 state.onboarding?.step 决定(ui/index.html),校验结果缓存在 state.onboarding.lastCheck(ui/index.html)。
5.3 外观覆盖是运行时变量
state.uiAppearance 通过 applyAppearance() 同时驱动文案、功能可见性与 CSS 自定义属性。源码中 applyAppearance() 位于 ui/index.html,调用 paintAppearance() 将 state.uiAppearance || DEFAULT_APPEARANCE 写入 DOM;功能开关统一走 featureEnabled(key),其语义是 state.uiAppearance?.features?.[key] !== false(ui/index.html),即默认开启、显式关闭。品牌名、欢迎标题等文案同理回退到 DEFAULT_APPEARANCE(ui/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(含 palette、features)与生成结果做深合并(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,取值为 null、low、medium、high、max 或 xhigh,默认 null(None)。源码中初始化即为 reasoningEffort: null(ui/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"——源码中当当前选中强度不在模型能力列表内时会被重置为 null(ui/index.html),这正是对模型 capabilities.effort 响应的消费。
6.3 Composer 加号菜单与附件复用既有上传路径
Composer 的操作被收敛到一个加号按钮:菜单先列出 "Add files",随后是既有的聊天工具/上下文控件,不存在独立的 Build 模式或 Build 按钮。附件行为按 Code Execution 状态分叉——开启时会发送到当前 code-execution 会话,否则摄取进已配置的 Documents 集合。源码分叉点在 ui/index.html:if (chat?.settings?.enabledCodeExecution) await uploadToSession(files)。
6.4 Hash 导航
syncHash() / restoreFromHash() 让 URL 与当前视图保持同步,格式为 #context/{tab}、#chat/{id}、#settings、#apiDebugger。源码实现(ui/index.html)与文档完全一致:syncHash() 依据 runtime.view 与 state.activeChatId 拼出 hash 后用 history.replaceState 静默更新;restoreFromHash() 在页面加载时按前缀解析,且只接受白名单内的 context tab(CONTEXT_TAB_IDS 包含 documents、databases、web、mcp、skills、customTools、codeExecution)与已存在的 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。)后端会把该声明展开为 bash、text_editor(view / str_replace / create / insert)、present_files 与 present_server 等具体工具。开关状态按聊天粒度持久化在 chat.settings.enabledCodeExecution(ui/index.html、ui/index.html)。
7.2 渲染:isCodeExecTool 白名单与块配对
渲染侧的关键函数 isCodeExecTool(name)(ui/index.html)维护白名单 bash、view、str_replace、create、insert、present_files、present_server。blocksToHtml() 在遇到这些工具的 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_id与mime_type取自local_resource块 schema; - 服务链接:
present_server结果中的resource_link块被渲染为.code-exec-server-link锚点(地球图标 + 服务名 + 隧道 URL),新标签页打开;uri、name、description取自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 文档治理"范式:
- 归属清晰:每条内容只有一个权威落点,API 契约指回 fern/openapi/openapi.json 而非 UI 本地快照;
- 入口分层:人读 README,Agent 读 AGENTS/CLAUDE,两者共享同一组 docs 作为唯一事实源;
- 实现笔记前置隐性知识:
defaultCollection的全局性、container = chat.id的强制约束、thinking: { enabled, type }的请求形态、hidden: true消息不渲染等约定,若不写成文档,维护者(尤其是新接入的编码 Agent)极易从 ui/index.html 的局部代码中得出错误结论。
对希望在自己的仓库中引入"单文件演示 UI + 多 Agent 协作"模式的团队,这套 README → SOURCE_OF_TRUTH → PRD / STYLE_GUIDE → references 的分层与"同变更同步文档"纪律,是比任何单条规则都更值得借鉴的部分。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00