DeepSeek Harness 收不收数据:遥测子系统记了什么、记在哪、拿什么关

2026-08-17

先把问题问具体:你在公司的开发机上跑 dsh,它会把你的会话往外传吗?传的话传的是什么——只有「用了几次工具」这类计数,还是连命令输出和文件内容一起?关掉的开关在哪一层、关得干不干净?

这几个问题在 deepseek-harness 仓库里能一条条核到源码,不用猜。开头要先把限定说清楚:该仓库 README 有一节标题就叫 Developer preview,原文写明项目处于开发者预览阶段,并用加粗大写强调会有破坏兼容性的变更。下面提到的环境变量名、默认值、端点、期限,都是当前这个快照的样子,随时会变,别照着写死在你的部署脚本里。

先看开关摆在哪一层

遥测在这个仓库里被切成两个包:packages/session/session-telemetry 是能力定义与捕获协调器(seam 一侧),packages/session/session-telemetry-otel 是部署方加载的后端实现。前者的 src/ 下我们数出 3 个文件(index.tscoordinator.tsinvariant.ts),后者 2 个(index.tsinvariant.ts)。

真正决定「开还是关」的不在这两个包里,而在 packages/bundle/base/cordis.patch.yml。那张 row 表里有一条 id: session-telemetry-otel,它的 config 第一行是:

mode: !!js process.env.DSH_TELEMETRY_MODE || 'DISABLED'

也就是说:这一行默认是挂载着的,但模式落在 DISABLED。源码那边也是同一口径——packages/session/session-telemetry-otel/src/index.tsDEFAULT_TELEMETRY_MODE 就是 SessionTelemetryMode.DISABLEDresolveMode()undefined 解析成它。三个可选值是 FULLFEEDBACK_ONLYDISABLED

DISABLED 分支做的事很干脆:不构造 LoggerProvider、不构造 processor、不构造 exporter,也不组合捕获协调器,构造函数直接 return。它只留了一个监听器,用来在会话里出现 feedback/record 事件时打一条 warn,文案原文是 session telemetry is DISABLED; nothing will be shared and this feedback remains local。所以「挂载」和「在采集」在这里不是一回事——这一点如果只看 row 存在与否会看反。

第二层开关是环境变量 DSH_TELEMETRY_DISABLED,它在 apps/cli/src/profile-boot.tsresolveTelemetryPatch() 里处理。这个函数的判断是 if ((disabledEnv ?? '') === '' || !hasRow) return undefined——换句话说,任何非空值都算「关」,包括 '0''false'。函数上方的注释自述了这么写的理由:隐私开关宁可误关也不要误开。它产出的是一条 { id: 'session-telemetry-otel', disabled: true } 的 patch,作用在加载期,比配置里写的 mode 更靠前。

打开之后,采集点接在哪几个地方

假设一个部署显式把 DSH_TELEMETRY_MODE 设成了 FULL。这时后端会在构造函数里 new SessionTelemetryCoordinator(ctx, backend, 'live'),捕获侧才装上去。packages/session/session-telemetry/src/coordinator.ts 里能数清楚它挂了几个监听:session/created(收养会话)、session/disposedsession/eventsession/flushagent/error,外加一个 dispose effect;构造末尾还会 for (const session of ctx.sessions.list()) 扫一遍已经活着的会话——注释自述是因为热重载不会重放 session/created

FEEDBACK_ONLY 走的是另一条路:协调器以 'on-demand' 建立,上面那串连续监听一个都不注册,只有当会话里落下一条 feedback/record 时才调 coordinator.captureSession(session, event.seq),把游标之后、到这条事件为止的日志后缀补送出去。这里有个挺硬的细节:源码会先检查 session.events[event.seq] !== event,不是规范日志里那一条就直接 warn 丢弃。README 把这句话说成「同意」的判据——只有已经落进规范会话日志的那个对象才算数,总线上单独发出来的同名事件不算。

一条记录到底带了什么

SessionTelemetryRecordpackages/session/session-telemetry/src/index.ts 里只有五个字段:channel'ledger' | 'ops')、time(epoch 毫秒)、severity'info' | 'warn' | 'error')、attributes,以及装载荷的 body。前四个是信封,量都在最后一个里。

attributes 是刻意做窄的白名单,看 coordinator.ts 里的 identityOf() 就一清二楚:固定三项 session.idevent.typeevent.seq,然后从 session.header 里按存在与否补 session.cwdsession.parent_idsession.seed_length。注意 session.cwd 是本机路径。ops 记录(agent-errorshutdown)走另一套,带 telemetry.op,并且故意不带 event.seq / event.type——docs/subsystems/session-telemetry.md 自述的理由是它们是用来告警的信号,不是用来累加的条目,因此也容忍重复。

真正的量在 bodycaptureEvent() 里这一行是 body: structuredClone(event.data):整个事件的 data 深拷贝,原样带走。session-telemetry-otel 的 README 有一节标题就叫 What leaves the machine,逐项列了在上传模式下会出去的东西:用户与助手的消息正文、工具参数与工具结果(命令输出、文件内容)、request/header 里的完整系统提示词与工具 schema、todo 文本、compaction 摘要、hook 的 stderrSummary、反馈文本,以及会话 cwd。同一节也写明一个例外:适配器的 API key 是构造参数、不是会话事件,结构上就不在日志里,因此也不在遥测里。

唯一被削掉的是流式分片。captureEvent()assistant/chunk${turn}:${step} 做键,只放行每个 (turn, step) 的第一条,其余在捕获处丢弃。所以线上看到的 seq 是有洞的——文档反复强调这是常态,不是丢数据的信号。

脱敏这一层,出厂是空的

投影和 emit() 之间还夹着一道 session-telemetry/record waterfall。这一层值得单独说,因为它的默认状态反直觉:seam 自己不带任何规则coordinator.ts 里的 redact() 就一句 this.ctx.waterfall('session-telemetry/record', record, () => record),最内层的 next() 原样返回。没有部署方挂监听器的时候,记录就是捕获时的样子送到后端。seam README 的 Known Limitations 一节把这条列成了明写的限制,原话大意是:没有挂监听器时,记录离开进程时就是捕获时的原样,包括嵌在文件内容或命令输出里的凭据。apps/cli/reference/README.md 里也重复了一遍——出厂的 base 没有遥测脱敏规则,所以显式打开之后导出的内容可以包含消息正文、工具参数与结果、以及工作区路径。

这一层的失败语义是 fail-closed:监听器抛异常时,那一条记录被扣下不发(协调器的 contain() 兜住并打 warn),不会窜到 agent loop 里。另外脱敏只作用于导出副本,规范会话日志永不改写。

送到哪、送多久、什么时候放弃

上传模式下,后端把每条记录映射到 OTel JS SDK 的 logger.emit(),ledger 和 ops 分在两个 instrumentation scope 下。Resource 上带着 service.name / service.version,还有一个 user.id——来自 packages/identity/anonymous-user-id,是存在 $DSH_HOME/.anonymous-user-id$DSH_HOME 未设时回落 ~/.dsh)里的一个随机 UUID,该包 README 自述它不由主机名、网络地址、git remote 或任何其它可识别来源派生,删掉文件下次启动就换一个。

cordis.patch.yml 那条 row 里的传输参数是逐个写死的:默认端点 https://harness-telemetry.deepseeksvc.com/v1/logs(可用 DSH_TELEMETRY_OTLP_URL 覆盖)、compression: gzipexporter.timeoutMillis: 1000processor.scheduledDelayMillis: 10000maxQueueSize: 2048maxExportBatchSize: 2048exportTimeoutMillis: 1500,以及后端自己的 shutdownTimeoutMillis: 3000。最后这个值在源码里也有常量兜底:DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 3_000。注意这些都是配置里的默认值,不是对实际表现的承诺。

shutdownTimeoutMillis 为什么要单独存在,源码注释自述得很清楚:OTel 的 exportTimeoutMillis 只包住导出完成那一段,shutdown 会先 await exporter.forceFlush(),而在拿不到 socket 的网络环境里这个 Promise 可能一直挂着。所以后端用 Promise.race 加了个外层期限,超时就抛,协调器 dispose 时捕获这个失败、打一条 warn,不让它拖垮应用退出。仓库 .agents/notes/ 下有一条 bug-fix 记录写到过这个现象:有人反馈命令在观测 URL 之后挂住、Ctrl+C 也不响应,把 DSH_TELEMETRY_DISABLED=1 加上就不挂了。配置里另一个 maxExportBatchSize 也有专门的加载期校验——不是正整数就在插件加载时直接抛错,注释自述的理由是 SDK 本身会接受这个值,但它的 shutdown 排空会切出一批批空批次、不消费队列,队列里还压着记录时 dispose 就永远回不来;与其运行时挂死,不如加载期报错。

投递本身是尽力而为的:交接游标(coordinator.ts 里那个模块级 WeakMap<Session, number>)标记的是「已交接」而不是「已送达」,崩溃、重载窗口会丢,无游标的重新收养和 SDK 重试会重,所以文档要求接收端按 (session.id, event.seq) 去重。

文档与源码口径对不上,按源码认

翻这块的时候有几处 README 与源码不一致,按仓库规矩只陈述、不揣测。两个包的 README(packages/session/session-telemetry/README.mdpackages/session/session-telemetry-otel/README.md)里,waterfall 都写作 sessionTelemetry/record;ops 属性写作 sessionTelemetry.op 的是前者;示例 row 的 id 写作 sessionTelemetry-otel、instrumentation scope 写作 @deepseek-ai/dsh-session-sessionTelemetry-otel 的是后者。而 packages/session/session-telemetry/src/index.tscoordinator.tssession-telemetry-otel/src/index.tspackages/bundle/base/cordis.patch.yml 里对应的写法分别是 session-telemetry/recordtelemetry.opsession-telemetry-otel@deepseek-ai/dsh-session-telemetry-otel。要挂监听器或者写 patch 层的话,以源码里的写法为准。

还有一处容易误读:packages/identity/anonymous-user-id/README.md 明写 DSH_TELEMETRY_DISABLED 只停遥测导出,它不抑制反馈的本地确认,也不抑制 DeepSeek 服务商请求头里那个 id。所以「我设了那个变量所以什么都不发了」这句话,按仓库里的说法是不成立的。

落到实处

如果你的诉求是「这台机器上不要有遥测上报」,能核到的动作是:确认 DSH_TELEMETRY_MODE 没有被设成 FULLFEEDBACK_ONLY(未设即 DISABLED),以及在启动环境里给 DSH_TELEMETRY_DISABLED 一个非空值当硬开关。如果你的诉求相反——要往自建 collector 收会话——那么在按下 FULL 之前,先读一遍 What leaves the machine 那一节,再决定要不要自己挂 session-telemetry/record 监听器,因为出厂那一层是空的。这两件事都建议顺着上面给的文件路径自己再核一遍,毕竟这是个明说会破坏兼容的开发者预览版本。


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。