ui-conversation now owns only the conversation.chat.turnTail hole; the row, its derivation, and its copy live in @deepseek-ai/dsh-client-ui-deliverables, composed in or out by one cordis.yml line.
7.2 KiB
Agent Note:从 web UI 打开产出的文件
Status: implemented
English | 中文
范围:完成的一轮以其产出文件收尾的那一行、读得出是链接的文件路径链接,以及 Host 打开器对浏览器可渲染文档优先选用默认浏览器。经决定不在范围内:以 HTTP 提供工作区文件,以及为不在 Host 机器上的客户端提供预览。
问题
一个产出了文件的 web 会话,没有办法看到那个文件。agent 写出了 deepseek-homepage.html 并如实告知,而用户唯一的办法是把 /private/tmp/dsh-client-hotplug.ygPvsm/workspaces/plugin-hotplug/deepseek-homepage.html 这样的绝对路径复制进终端。
这背后是两个不同的缺陷。转录从不说明一轮产出了什么:ToolCallView.locations——文件工具早已填好的跟随文件词汇——在客户端没有任何消费方,因此读者对产出的唯一交代,就是收尾消息恰好拼出来的那点内容。而已经存在的那个交互是隐形的:ToolRow 早已把改写行或读取行的路径渲染成一个接到 host.openPath 的真按钮,但它的样式与周围正文一模一样、只有悬停才有下划线,于是没人发现。所报告的“做完了打不开”,是一个可发现性失败叠在一项本就可用的能力之上。
决定
完成的一轮以它产出的文件收尾。 该行是独立插件 @deepseek-ai/dsh-client-ui-deliverables,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 conversation.chat.turnTail 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、openFile),插件拥有全部策略。producedForClosing 从改写工具自身的跟随文件 locations 中读出路径——diff 卡片,或 kind 为 edit 的 generic 卡片(即 str_replace_editor 的 insert 所呈现的形状)——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。
路径链接读得出是链接。 静止状态下就带下划线,而不只在悬停时。这是本次改动中更小的那一半,却是修复中更大的那一半。
打开仍然是 Host 的职责,并且优先选用默认浏览器。 host.openPath 把路径交给操作系统,得到的是真实浏览器里的一份 file:// 文档:页面能力完整,且够不到 /api——因为 file:// 文档与它并不同源。在所报告的那份产物上实测:localStorage 可用、主题切换生效、tabs 可切换,而对 API 的 fetch 失败。对浏览器能渲染的文档——.html、.htm、.xhtml、.svg——打开器解析的是默认浏览器而非该类型的默认应用,因为把 .html 绑给编辑器的开发者,否则点开一个产出的页面得到的会是源码。每个平台在自己能力范围内回答“哪个浏览器”(macOS 取 LaunchServices 的 https 处理程序,Linux 取 $BROWSER),任何一步失败都回退到默认应用,而不是把失败抛给用户。
以 HTTP 提供工作区文件不在范围内,非本机客户端亦然。 更早的一版由 harness 自己提供文件——先是与 /api 同源,随后加上 CSP: sandbox,再后来交给一个以自身端口给所服务文档独立源的第二监听器。每一步都在回答一个真实问题,而整条线在产品范围定下之后被整体退役:不为“浏览器不在 Host 机器上”的场景提供预览。这一点定下之后,Host 打开器完整回答了受支持的场景,而那套 HTTP 机制回答的只是不受支持的那个。
考虑过的替代方案
- 由 harness 提供
/f/<sessionId>/<segments…>——已经实现并可用,包含双realpath收敛、浏览器信任 fence、流式读取,以及一个以自身端口给所服务文档独立源的监听器。它是唯一能把预览呈现给另一台机器上客户端的设计,而那恰恰是被判出范围的场景。因此退役,而不是因为它失败了;它的代价是一个带自身生命周期的第二 socket、一个注入页面的端口,以及一份跨两个包共享的 URL 形状契约。 - 同源 HTTP 提供且不加隔离——经实测不安全,记录在此以免有人重试:与
/api并排提供的文档把settings.describe打到200并拿到完整数据,把session.list打到 35 KB 的全部会话转录,而这个页面根本不必由 agent 撰写(一条 read 行就让 clone 下来的仓库里任何文件变得可打开)。 - 在那套同源提供之上加
Content-Security-Policy: sandbox——它以剥夺文档的源来堵住这个洞,而这经实测会破坏本功能存在的意义所在的那类页面:所报告的产物在加载时抛SecurityError,又因为未捕获异常会中止其<script>的其余部分,该行之后声明的所有监听器——主题切换、移动端菜单、模型 tabs——统统不会绑定。报告者工作区里四份产物有两份在它之下是死页面,而且它们渲染得完美无缺,所以这种破坏是看不见的。 - 把路径在助手的收尾消息里链接化——这是用户开口要的形状(“在结尾附上链接”),但它让渲染取决于模型是否把路径拼写得可识别。工具调用已经把
locations作为结构化事实携带,产出文件行消费的正是它。 - 桌面端外壳中的内嵌 WebView——可得到的最强隔离,因为那时预览跑在产品自己拥有的容器里,而不是用户的浏览器里。它属于桌面端外壳自身的设计,而非本交互面,记录在此作为未来预览能力应走的方向。
影响
现有的每一处文件交互都同时改变了:write、edit、read 与通用单文件卡片都汇到 openFile,因此链接可见性修复与浏览器优先策略无需逐行改动即适用于全部。无密钥 web 通道(apps/web/tests/produced-files.e2e.ts)冷播种一段录制的 write 轮次,在组装后的应用中钉住该行;它刻意不点击,因为点击会把路径交给 Host 打开器,从而在跑测试的机器上启动一个真实应用。产出的文件以 file:// 文档打开,它无法 fetch 自己的同级文件(一个加载 ./data.json 的多文件产物会坏,而 <script src>、<img> 与 CSS @import 不受影响)——这是 HTTP 提供曾有、而此处没有的那一项能力。经网络访问的客户端点击后看不到任何东西:host.openPath 在 Host 上运行,且被 /api 信任 fence 钉在回环。那是范围决定的显现,不是缺陷,也正因如此该行把完整路径保留在 title 中,供只能复制它的读者使用。markdown 会由平台交给 .md 的默认处理程序打开,通常是编辑器而非渲染器;在产品内渲染它是另一个被推迟的交互面。