news 2026/9/20 13:42:16

DeepSeek Harness Read Card:让 read 工具的结构化行窗口以结构化形态直达客户端

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek Harness Read Card:让 read 工具的结构化行窗口以结构化形态直达客户端
  • 人工智能
  • AI Agent
  • Agent 框架
  • DeepSeek

【免费下载链接】deepseek-harness

DeepSeek Harness: Everything is a Plugin.

项目地址:https://gitcode.com/gh_mirrors/de/deepseek-harness
点击查看免费下载

导读

DeepSeek Harness 中read工具的规范化输出是一个结构完整、带行号的行窗口对象{ path, offset, lines, totalLines },但旧版展示层将其压平成一段行号内嵌的纯文本,导致具备渲染能力的客户端无法像渲染 diff 那样渲染一次带行号栏、语法高亮的代码视图。本文依据仓库内已落地的实现笔记(2026-07-30-web-read-card.md),完整剖析本次"Read Card"设计:如何通过为渲染意图联合类型新增第四个card: 'read'标签、借助presentationMeta持久化通道把结构化行窗口投影到会话日志,使实时与回放两条路径上的客户端都能拿到lines/totalLines/lang结构化数据。读完本文,你将理解这套"生产者投影结构化数据、消费方按能力回退"的展示契约,并掌握langFromPathreadMetaFromMeta的防御性收窄策略及其在 read-render.ts 中的源码级实现。

背景:read 工具的规范化输出与扁平化展示之间的落差

在 规范化工具输出约定 落地之后,DeepSeek Harness 的每个工具都必须声明一个规范化输出对象read工具返回的是:

{ path: string; offset: number; lines: [{ number: number; text: string }]; totalLines: number }

这个结构在 read.ts 的output.schema中按 JSON Schema 逐字段声明,并在执行期由buildWindow严格构造。与此同时,面向模型的文本渲染由render投影器负责,输出一段 OpenCode 风格的行号文本:

<path>src/index.ts</path> <type>file</type> <content> 10: import { foo } from './foo' 11: export function bar() { ... } (Showing lines 10-11 of 132. Use offset=12 to continue.) </content>

问题出在"规范化输出对象"与"面向模型文本"之间的展示投影层。旧版read的展示回调是这样声明渲染意图的:

  • presentCall返回GenericCallViewkind: 'read',一个跟随定位);
  • presentResult返回GenericResultView,其唯一内容是被剥掉<path>/<type>/<content>信封后的文本。

也就是说,一个收到该视图的 UI 只看到一个压平的文本块:行号以N:前缀烘焙进文本、文件的编程语言未知、totalLines丢失。有相应能力的客户端无法像渲染 diff 那样渲染一次 read——它想要的是行号栏与内容分离、支持语法高亮的代码视图。

核心问题:结构化数据在线上(wire)无法恢复

为什么不能由客户端自行从文本里解析回结构化数据?因为工具结果的线上形态(on the wire)只包含两样东西:

  1. 面向模型的ContentBlock[](已渲染的文本);
  2. 一个不透明的meta字段。

规范化输出对象留在工具内部,从不到达客户端,也不写入会话日志。依据 规范化工具输出约定,agent loop持久化tool/result事件时只记录contenterror和可选的meta,规范化中间值被刻意排除在会话格式之外。因此想要行数组、总数和语言提示的客户端:

  • 无法从N: text文本里解析回它们——按第一个:切分存在歧义、脚注只覆盖部分分支、渲染格式一变就失效;
  • 唯一的出路是:工具在产生结果时,把结构化窗口投影到那个会随会话日志持久化的通道上——即meta

这正是本次 Read Card 设计的出发点。

决策一:渲染意图联合类型新增第四个card标签read(仅结果侧)

渲染意图 union 架构 Note 定义了一套card标签化的封闭判别联合类型:工具的presentCall/presentResult各自声明一种渲染意图,桥接层按card标签switch分发渲染。本次设计为这套词汇新增第四个标签:

// presentResult → ToolResultView(新增 read 分支后) type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | ReadResultView interface ReadResultView { card: 'read' title?: string path: string offset: number lines: ReadFileLine[] // ReadFileLine { number: number; text: string } totalLines: number lang?: string // 语法高亮语言提示,缺失时 UI 渲染纯文本 content?: ContentBlock[] // 剥信封后的文本,供无 read 能力的 UI 兜底 }

几个关键设计取舍:

  • 仅结果侧(result-side only)ToolCallView完全不动,待定状态仍是GenericCallViewkind: 'read',跟随定位)。理由很直接:一次 read 调用在execute返回之前不携带任何文件内容——没有行数组、没有总数,调用时没有任何可展示的结构化信息。这与 bash 终端 card 形成对比:终端 card 两侧都打标签,是因为终端调用在调用时已经携带命令和 cwd;read 调用则两者皆无,若给调用侧打标签只会平添一个空变体。调用侧展示仍由 read.ts 的presentCall负责:一个以文件路径为标题、带read类型图标、offset作为跟随定位行的 generic card。

  • ReadFileLine是共享行单元{ number, text }与规范化输出对象中的行项、面向模型文本中的N: text行一一对应,保证三处数据语义一致。

  • content兜底:成功路径上presentResult在结构化字段之外总是携带剥信封后的文本。这样,一个不具备 read 卡片渲染能力的 UI,可以经由自己的 generic/default card 分支照常显示文件文本,实现"生产者一次产出、消费方按能力取用"。

决策二:结构化窗口经由presentationMeta投影并持久化

read工具通过output.presentationMeta把结构化窗口投影到meta通道——这与 write/edit 工具把应用后的 diff hunk 投影到同一通道的做法一致(见 规范化工具输出约定)。在 read.ts 中,投影器只是对已有数据的一次轻量复制:

presentationMeta: (_args, value) => { const lang = langFromPath(value.path) return { path: value.path, offset: value.offset, lines: value.lines.map(({ number, text }) => ({ number, text })), totalLines: value.totalLines, ...lang === undefined ? {} : { lang }, } },

执行流程presentationMeta只对一次顶层 surface 调用运行一次,返回的{ path, offset, lines, totalLines, lang? }作为 JSON 被会话校验后存储在结果的meta上。随后presentResult实时与回放两条路径上都把该meta收窄回ReadResultView——回放时原始的规范化输出对象已不在线上,但持久化的meta让行数组、总数和语言提示都能被还原。

为什么offset必须随 meta 持久化?

一个容易被忽略的细节:offset(窗口请求的 1-based 起始行)也必须携带。原因在于字节上限与行号窗口的交互——当readMaxBytes字节上限低于首个选中行时,buildWindow返回的是一个空的lines数组,但totalLines为正数。此时:

  • 没有持久化的offset,回放出的卡片将无法报告窗口从哪一行开始;
  • 续读(Use offset=N to continue)也无从定位应从哪行继续;
  • 末行推断与文本重解析两种兜底方案都有损

换言之,offset是空窗口场景下重建窗口位置的唯一可靠依据。

决策三:presentResult的防御性收窄与降级策略

presentResult的完整逻辑在 read.ts,它只在以下情况全部满足时才返回结构化卡片,否则一律返回undefined(即 generic 回退):

  1. 结果不是错误result.isError为 false);
  2. meta 存在且结构合法——由readMetaFromMeta防御性收窄;
  3. 单个文本块是 read 信封——通过正则/^<path>[^\n]*<\/path>\n<type>file<\/type>\n<content>\n([\s\S]*)\n<\/content>$/u校验并提取正文。

这里有一个至关重要的设计立场:本 card 出现之前记录的旧日志结果——信封合法但没有持久化meta——有意走同一条undefined路径。此时客户端回退到原始result.content,显示带<path>/<type>/<content>信封的原文,而不是旧展示器返回的那种剥信封 generic card。

这是项目 pre-release 立场("foundation over blast radius")下明确接受的降级:拒绝旧的磁盘格式,而不是为兼容而新增一个剥信封分支。理由有二:本次变更已重录全部已发布测试 fixture(fixture 即测试前置数据),且会话格式本身不承诺向后兼容。

语言提示推导:langFromPathLANG_BY_EXTENSION

ReadResultView.langlangFromPath从文件路径推导,实现在 read-render.ts,其查找表LANG_BY_EXTENSION在 read-render.ts 定义:

const LANG_BY_EXTENSION: Readonly<Record<string, string>> = { ts: 'ts', tsx: 'tsx', mts: 'ts', cts: 'ts', js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js', json: 'json', jsonc: 'json', py: 'py', rb: 'rb', go: 'go', rs: 'rs', java: 'java', c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', hpp: 'cpp', cxx: 'cpp', cs: 'cs', kt: 'kotlin', swift: 'swift', php: 'php', sh: 'sh', bash: 'sh', zsh: 'sh', yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini', md: 'md', markdown: 'md', mdx: 'mdx', html: 'html', htm: 'html', css: 'css', scss: 'scss', less: 'less', sql: 'sql', xml: 'xml', lua: 'lua', }

推导规则可以归纳为四点:

  • 取最后路径段:先按/\切出最后一个路径段(同时兼容 POSIX 与 Windows 路径分隔符);
  • 取最后一个点之后的扩展名,且大小写不敏感.TS.ts等价);
  • 以下情况返回undefined,卡片随之省略lang、UI 渲染纯文本:dotfile(如.gitignore,开头的点不算扩展名)、无扩展名(如/etc/hosts)、结尾的点、以及任何未知扩展名;
  • 防御原型污染:查找使用Object.hasOwn(LANG_BY_EXTENSION, ext)做自有属性检查——一个文件名恰好以foo.constructorfoo.__proto__结尾时,绝不能命中继承成员,否则函数值会流入lang并导致工具输出 JSON 校验失败。

这张表不是可调项(tunable):它是 UI 可以忽略的展示提示,而非随部署变化的选择;未知扩展名优雅降级为纯文本而非报错。它刻意保持小规模而非穷尽的语言注册表——扩展它只需新增一行表项。

备选方案回顾:为什么是这些设计

实现笔记(2026-07-30-web-read-card.md)明确记录了四条被否决的路径,理解它们有助于把握设计的边界:

备选方案否决理由
presentResult中重新解析N: text文本按第一个:切分有歧义(行文本自身可含:)、脚注只在部分分支陈述totalLines(精确总数会丢失)、渲染格式一变即失效。presentationMeta直接携带已结构化数据,零解析
调用侧也打标签(ReadCallView),镜像终端 card 的两侧对称read 调用在执行前无内容、无行数组、无总数,调用侧卡片只会是空变体,重复GenericCallViewkind: 'read')已表达的信息;终端两侧打标签是因为调用时确有数据(命令、cwd)
把结构化窗口放进新服务或旁路通道meta已是既定持久化展示通道(write/edit 的应用 diff 也走它),随会话日志免费回放,无需新接线;新服务等于重新发明事件日志已有的持久化与回放
用 merge-extensible union 替代封闭标签与渲染意图 union 封闭的理由一致:新卡片需要消费代码来渲染,被消费方静默丢弃的变体比编译错误更糟。把read加入封闭 union 是受认可的扩展方式——每个在card上 switch 的消费方因新成员落入 generic default 而继续编译,想要富视图的消费方自行新增分支

影响评估

对消费方

ToolResultView从三个成员变成四个。消费方有两个选择:

  • 渲染结构化形态:读取lines/lang/totalLines/offset,渲染成行号栏与内容分离、支持语法高亮的代码视图(与 diff 卡片的渲染体验对齐);
  • 路由到 generic 路径:因 read card 始终携带content(剥信封后的文本),generic/default 分支仍能显示完整文件文本。

这次生产者变更,是让结构化数据可触及的后端——它不要求每个消费方在同一时间实现富视图,具备能力的客户端可以先消费,其余客户端不受影响。

对生产者与存储

  • read 工具现在为每次顶层 read计算presentationMeta:一次lines.map加一次langFromPath调用,是对已有数据的极小投影,成本可忽略;
  • meta随会话日志持久化,read 结果在磁盘上略大——它已渲染为文本的行数组,如今也以结构化形式存在一份。这是换取回放时结构可重建的既定代价。

测试与验证

Read Card 的验证覆盖两层,全部在仓库内可查:

单元测试:packages/fs/tool-fs/tests/read-render.spec.ts

该文件(与 read-render.ts 同目录)逐项钉住两个纯函数:

  • langFromPath:已知扩展名的大小写不敏感匹配;扩展名在最后路径段与最后一个点之后读取;以及所有undefined情形(dotfile、无扩展名、结尾的点、未知扩展名);
  • readMetaFromMeta:含与不含lang的良构收窄;以及每一种拒绝——非对象、数组、缺失或类型错误的path/totalLines/lines、畸形行项、非字符串lang;更关键的是,由于该函数收窄的是不透明的持久化 meta 边界,它还覆盖了"类型正确但语义无效"的回放 JSON:不是 1-based 整数的offset、小于offset的首行number、不是 1-based 整数的行number01.5NaNInfinity)、不是非负整数的totalLines-11.5NaN)、行号重复/递减/超过totalLines,以及正offset处的空窗口(字节上限低于首个选中行)。

集成测试:packages/fs/tool-fs/tests/tools.spec.ts

钉住工具接线:execute把结构化窗口(含与不含lang提示)作为meta附上;presentResult把它收窄为携带剥信封contentcard: 'read'视图;以及各拒绝路径(错误结果、非单文本内容、meta 有效但信封畸形、信封有效但 meta 缺失或畸形)一律回退到undefined。两个改动的源文件保持逐文件 100% 覆盖率

快照证据

本变更携带的是持久化 meta 与扩展后联合类型的快照证据,而非新渲染视图的证据:

  • 重录的 ACP 会话 fixture(fs-readfs-read-windowfs-editfs-policy-rejectfs-write-overwriteparallel-tool-callsagent-instructionsworkspace-edit)钉住持久化的 readmeta(含{{cwd}}令牌化的路径);
  • cordis-inspect-jsdoc快照钉住四成员ToolResultView联合类型;
  • 当时的终端快照还钉住消费方的 generic dim-Markdown 回退保持逐字节一致;结构化卡片自身的组装应用 transcript 则归属于消费它的前端变更。

相关文档导航

  • 工具调用展示的带标签渲染意图 union—— 本次以read结果分支扩展的card标签词汇,含GenericCallView/TerminalCallView/DiffCallView及封闭联合的来龙去脉;
  • 规范化工具输出约定—— 拥有presentationMeta持久化通道与output.schema契约,是本次投影方案的底层依据;
  • Web 终端 card—— 客户端消费结构化卡片的前置先例,read card 遵循相同的"生产者投影、仅结果侧"模式;
  • 核心实现:read-render.ts(窗口构建、文本信封、langFromPathreadMetaFromMeta)、read.ts(read工具注册与三个展示回调);
  • 测试:read-render.spec.ts、tools.spec.ts;
  • 项目立场:AGENTS.md(pre-release stance:foundation over blast radius)。
  • 人工智能
  • AI Agent
  • Agent 框架
  • DeepSeek

【免费下载链接】deepseek-harness

DeepSeek Harness: Everything is a Plugin.

项目地址:https://gitcode.com/gh_mirrors/de/deepseek-harness
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 13:40:29

ZeroOmega 3.4.0 安装与配置实战:Chrome/Edge/Firefox 代理切换全指南

简介&#xff1a;ZeroOmega是一款面向新版Chrome浏览器的代理管理插件&#xff0c;作为Proxy SwitchyOmega的继任者&#xff0c;解决了旧插件无法使用的问题。它适合开发、测试以及需要频繁切换网络代理的进阶用户&#xff0c;通过弹出面板快速管理多套代理配置&#xff0c;并可…

作者头像 李华
网站建设 2026/9/20 13:40:04

3DGS投影变换矩阵全解析:从三维高斯到屏幕椭圆的数学推导

第一次把3DGS整个渲染管线读通的时候&#xff0c;我踩了一个特别蠢的坑&#xff1a;我以为只需要把每个高斯中心当成普通点云&#xff0c;用一个MVP矩阵投到屏幕上&#xff0c;再叠上一个固定大小的圆斑当模糊效果就行。结果跑出来的图全是边缘发亮的空心圈和奇怪的条纹&#x…

作者头像 李华
网站建设 2026/9/20 13:39:28

电视盒子播放管理完整教程:3 步装好 TVBoxOSC 就能播

电视盒子播放管理完整教程&#xff1a;3 步装好 TVBoxOSC 就能播 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库&#xff0c;用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC 追更前翻两分钟盒子应用列表&am…

作者头像 李华
网站建设 2026/9/20 13:34:43

DMA技术与DMA控制器原理详解:从STM32实战到双缓冲与调试

简介&#xff1a;面向计算机组成原理、微机接口技术等课程学习者&#xff0c;DMA技术PPT课件系统梳理了直接内存访问的完整知识体系。内容涵盖DMA传送方式特点与操作过程&#xff0c;详细讲解DMAC的基本功能、内部结构、三种工作方式&#xff0c;并深入剖析8237A可编程DMA控制器…

作者头像 李华