之前我写过几个 DeepSeek Harness 相关项目:替换产品名称和 Logo 的品牌插件、给 Web profile 增加独立窗口的桌面插件,以及帮助普通用户安装和启动 DSH 的脚本。
这些项目主要解决「怎么打开、怎么配置、怎么看起来像自己的产品」。
这次我想换一个方向:让 DSH 帮人完成一件具体的日常工作——分析导出的 CSV。
销售流水、费用清单、工单记录、运营报表,很多后台都能导出 CSV。面对这些文件,我们经常想问的并不复杂:
- 各地区有多少笔已支付订单,金额合计是多少?
- 哪些费用超过了 500,能不能按金额从高到低列出来?
- 每种状态有多少条工单?
- 报表里的零,究竟是真的零,还是金额没有填写?
dsh-csv-analyst 就是围绕这些问题做的一个只读工具插件。用户用自然语言描述需求,模型负责选择条件和解释结果,插件负责读取数据和执行确定的计算。
截图来自真实 DSH Web 页面和真实模型调用,使用的是仓库内的合成示例数据,不是客户报表。页面中的简洁表格是模型根据已查询结果、按追问整理的版本。
一、为什么还要专门做一个 CSV 插件
DSH 本来就能读取文件、执行命令,也可以让模型写 Python 脚本分析数据。这个插件不是因为「DSH 不会处理 CSV」,也不是要替代 Excel 或 pandas。
我更关心的是:对于常见的筛选和统计,能不能把容易出错的环节固定下来,让使用者不用每次都依赖模型重新写一段计算代码?
例如,把文件内容交给模型后,它可能直接根据预览计算;让模型写脚本,它又需要决定怎么处理空值、用什么数值类型、要不要去掉负数、返回多少行。
这些选择不一定错,但如果没有明确说明,就很难判断结果代表什么。
所以这个插件把工作分成两部分:
用户用自然语言提问
↓
模型选择列、筛选条件和统计指标
↓
CSV 工具读取完整的限额内文件并执行计算
↓
返回数据、记录数、参与计算的数量和分页信息
↓
模型整理成适合阅读的说明或表格
模型仍然参与工作,但它不需要凭文字预览「心算」合计,也不需要为了这些基础统计生成一段可执行代码。
二、先看一个最容易被误读的例子
仓库提供了一份很小的 sales.csv:
region,amount,status
广州,0.10,paid
广州,0.20,paid
上海,10.00,pending
上海,,paid
广州,-0.05,paid
现在问 DSH:
帮我分析 sales.csv:先检查数据质量,再按地区统计已支付订单数和金额合计,指出各地区金额缺失的记录数。
只统计 status = paid,会得到下面的结果:
| 地区 | 已付订单数 | 有效金额条数 | 金额缺失条数 | 已知金额合计 |
|---|---|---|---|---|
| 广州 | 3 | 3 | 0 | 0.25 |
| 上海 | 1 | 0 | 1 | 0(没有可参与求和的金额) |
| 合计 | 4 | 3 | 1 | 0.25 |
广州的合计是:
0.10 + 0.20 − 0.05 = 0.25
这里保留负数参与计算,不自动判断它是不是退款,也不擅自把它当成异常删除。
上海则有一条已支付记录,但金额为空。工具对空数值集合的求和返回 "0",同时告诉模型:实际参与金额计算的记录数是 0。
所以,这个结果不能解释为「上海真实收入就是零」。在实际报表里,可以把它标为「金额未知,待补充」,而不是直接当成完整的业务金额。
我希望这个插件解决的,不只是「算出一个数」,还有「让人知道这个数是怎么算出来的」。
三、两个工具,分别负责发现数据和执行查询
插件没有额外做一套聊天界面,而是向 DSH 注册两个模型可调用的工具。
1. csv_inspect:先看清文件
csv_inspect 返回:
- 原始列名;
- 全文件的数据记录数;
- 每列的空值、合法十进制数和其他值数量;
- 有上限的样本预览;
- 文件字节数和所读取内容的 SHA-256。
列名不被自动改写,数据也不被自动转成 JavaScript 数字。
例如,订单编号 00123 仍然保留为字符串,不会因为看起来像数字就丢掉前导零。是否按数值进行比较,由后续查询条件明确指定。
预览默认显示 5 行,但质量计数针对整个限额内文件,不是只检查这 5 行。需要减少预览数据时,也可以设置 sample_rows: 0。
2. csv_query:筛选、分组、统计和分页
csv_query 支持选列、同时满足的筛选条件、分组、聚合、多字段排序和分页。
下面是模型可以提交的一次查询,不是要求普通用户手写的操作步骤:
{
"path": "sales.csv",
"filters": [
{ "column": "status", "op": "eq", "value": "paid" }
],
"group_by": ["region"],
"metrics": [
{ "op": "count", "as": "orders" },
{ "op": "sum", "column": "amount", "as": "revenue" }
]
}
聚合支持 count、sum、avg、min、max。文本匹配区分大小写;数值比较需要明确使用十进制模式,不猜测货币单位或千位分隔符。
这张截图展开了真实查询的返回数据:原始文件有 5 条记录,匹配到 4 条,分组结果有 2 行。contributingRows 则记录每个指标实际使用了多少条数据。
四、我最在意的三个统计细节
1. 金额求和不用二进制浮点数
JavaScript 里一个很熟悉的例子是:
0.1 + 0.2
结果不是字面上的 0.3,而是 0.30000000000000004。
这并不意味着所有脚本都会把金额算错,而是金额计算需要明确选择数值类型。
插件使用 decimal.js 做十进制计算,数值结果以字符串返回,例如 "0.25",避免再次经过 JSON 浮点数转换而丢失精度。
在配置的数字位数限制内,求和保持精确。均值是另一回事:有些平均数无法用有限小数精确表示,因此按配置进行 half-up 四舍五入,默认保留最多 12 位小数。
2. 不把空金额偷偷转换成零
对于数值聚合:
- 空白金额不参与计算;
- 非空但不能按约定解析的金额直接报错;
- 空集合的求和是
"0"; - 没有有效数值时,均值和极值是
null; - 每个指标都有实际参与记录数。
例如,¥100、1,000、1e3 不会被插件擅自解释成某个金额。应该先确认格式并转换数据,而不是边计算边猜测。
实际页面演示里,模型也曾漏掉数值筛选所需的 mode: "decimal"。工具明确拒绝了这次请求,模型补上条件后再继续查询,而不是悄悄按另一种比较方式执行。
模型可能写错参数,工具不能因此返回一份看起来正常、实际口径不同的结果。
3. 分页不改变统计范围
「只返回前十行」和「只统计前十行」是两件不同的事。
这个插件先处理整个限额内文件,完成筛选和聚合,再对输出结果排序、分页。返回值分别说明:
| 字段 | 含义 |
|---|---|
totalRows | 原始数据记录总数 |
matchedRows | 满足条件的记录数 |
resultRows | 分页前的全部输出行数 |
returnedRows | 本页返回的行数 |
truncated | 本页是否遗漏了其他输出行 |
改变 limit 不会改变统计合计。
文件超出配置上限时,插件直接拒绝计算,不读取一部分之后把局部结果当成全量结果返回。
五、核心实现:复用 DSH 的工具和文件系统
和之前的插件一样,这个项目没有修改 DeepSeek Harness 核心代码。
它是一个标准 bundle:
- insert:
- id: csv-analyst
name: dsh-csv-analyst
插件入口声明所需的宿主能力:
export const name = 'csv-analyst'
export const inject = ['tools', 'fs', 'workingDirectory']
ctx.tools 负责工具注册和正常的执行策略,ctx.fs 负责文件读取,ctx.workingDirectory 提供会话当前的工作目录。插件不另建文件系统,也不替换模型或 agent loop。
读取文件时,字节上限直接传给文件系统服务:
const bytes = await ctx.fs.readBytes(target, signal, config.maxBytes)
这样不是「先把大文件读进内存,再判断超限」,而是在读取操作本身就限制完整文件的大小。
CSV 解析使用 Papa Parse,不是重新写一个引号和换行解析器。工具返回值有明确的 schema,模型可见的内容由同一个标准结果生成,DSH 的会话日志负责保存工具调用和结果。
另外,工具注册会随插件卸载清理;卸载过程中也会取消并等待正在进行的操作结束,避免留下仍在读取文件的后台任务。
六、安装之后,用自然语言提问即可
已安装 DSH 的用户,可以把插件加入 Web profile:
dsh plugin --profile web add dsh-csv-analyst
dsh web
如果是在 DeepSeek Harness 源码仓库里运行,使用当前仓库的入口:
pnpm dsh plugin --profile web add dsh-csv-analyst
pnpm dsh web
npm 发布包已经包含构建产物,普通安装不需要再手动构建插件。
把 UTF-8 CSV 放到会话工作目录,然后直接描述需求,例如:
检查 sales.csv 的数据质量,按地区统计已支付订单数和金额合计。请同时列出金额缺失的条数,不要把空金额解释成真实收入为零。
在 expenses.csv 中找出金额大于 500 的记录,返回商户、分类和金额,按金额降序排列。
统计 support.tsv 中各状态的工单数量,这个文件的分隔符是制表符。
插件基于 DSH 0.2.2-rc.2、Node 25.6.1 开发和测试。版本只是开发基线的说明:插件不通过 engines、限定 peer 版本或运行时版本判断拦截其他版本。但取消限制,不等于已经验证未来所有 API 变化都兼容。
需要卸载时:
dsh plugin --profile web remove dsh-csv-analyst
插件不创建专属用户数据文件。卸载不会删除 DSH 历史会话中已经记录的工具结果。
七、哪些限制需要提前说清楚
当前版本是只读 CSV 分析工具,不是 Excel 编辑器、数据库或图表生成器。
主要限制包括:
- 只接受 UTF-8 文本,支持 UTF-8 BOM;
.xlsx、GBK、UTF-16 等数据需要先转换; - 默认使用逗号,不自动猜分隔符;制表符、分号、竖线可以明确指定;
- 表头必须非空且唯一,缺列、多列和解析器检测到的引用错误会报错;
- 默认最多读取 8 MiB、10 万条数据记录,返回页面最多 200 行;
- 每次调用重新读取文件,跨页查询应核对 SHA-256,不能把多次调用当成一个固定快照。
这些限额可以通过 profile 的 cordis.patch.yml 配置:
- id: csv-analyst
config:
maxBytes: 8388608
maxRows: 100000
maxReturnedRows: 200
defaultPreviewRows: 5
defaultPageRows: 100
averageDecimalPlaces: 12
还有一个更重要的提醒:只读不等于离线,也不等于保密。
插件自身不写文件、不请求网络、不启动子进程,也不执行模型生成的代码。但它返回的单元格和统计结果会进入正常的模型消息,并可能保留在 DSH 会话日志里;文件访问权限仍由宿主配置决定,其他已启用工具也有各自的能力。
所以,处理真实客户、财务或个人数据之前,仍然要确认模型提供方、权限和会话保存策略。表格里的文字同样是不可信输入,不能因为来自 CSV 就自动把它当成指令。
八、发布前做了哪些验证
目前发布的 0.1.1 包含 103 个测试用例,源码的语句、分支、函数和行覆盖率都达到 100%。
除了基础解析和查询,还覆盖了大整数与小数求和、空值、排序、超限拒绝、取消、会话目录、权限拒绝、卸载和重新挂载。金额求和另用独立生成的整数分值做属性测试,而不只是比较几组手写答案。
GitHub CI 在 Linux、macOS、Windows 上执行检查和依赖审计;另外使用真实 DSH 组合安装打包后的插件,并通过已记录会话完成无密钥重放。真实模型调用和页面截图也使用了实际工具结果,没有注入模拟回复。
发布时依赖审计未发现已知漏洞。但测试覆盖率和审计报告都不是「永远没有缺陷」的证明,也不能代替对模型结论的核对。
结语
这次我没有再增加一个打开 DSH 的入口,也没有给它换一套界面,而是把一个常见的数据处理场景做成了可安装的工具。
dsh-csv-analyst 的价值不在于模型能写出更长的分析,而在于它能拿到一份更明确的统计结果:计算范围是什么,金额是否有效,多少条记录参与了计算,返回的是全部结果还是其中一页。
用户仍然可以自然语言提问,模型仍然可以整理说明;只是金额求和、空值处理和分页口径,不再交给一段临时生成的文字或代码来决定。
项目地址:
npm 包: