DeepSeek Harness 匿名用户标识设计解析:`$DSH_HOME/.anonymous-user-id` 与 OTel Resource `user.id`
2026/9/20 20:10:45 网站建设 项目流程

DeepSeek Harness 匿名用户标识设计解析:$DSH_HOME/.anonymous-user-id与 OTel Resourceuser.id

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

本文以 DeepSeek Harness 仓库中的架构决策记录(Agent Note)为主体,结合其落盘实现与三大消费方源码,完整讲解会话遥测(Session Telemetry)中匿名用户标识的设计权衡、存储契约与并发语义。读完你可以掌握:匿名 UUID 如何被生成与持久化、为何"随机而非派生"是匿名性的根基、user.id为何挂在 OTel Resource 而非每条记录上,以及删除文件、只读 home、多进程首启等边界情况下的行为。该标识的最终归属包是@deepseek-ai/dsh-anonymous-user-id,被会话遥测 OTel 后端、/feedback命令与 DeepSeek 直连请求三方共享。

背景:遥测已就位,但缺少"用户"这一维度

会话遥测默认被挂载进 dsh web 组合(见 default-mount Note),然而当时的 OTel Resource 只携带service.name/service.version完全没有用户级身份。这意味着收集端(collector)既无法按用户聚合记录,也无法统计活跃用户数。此前唯一相关的裁决是"用 hostname/本地 IP 哈希派生用户 ID"的未实现提案。OTel 数据流迫切需要一种语义干净的匿名用户身份。

决策总览:一个 UUID、一个文件、三方共享

核心决策一句话概括:getOrCreateAnonymousUserId()返回$DSH_HOME/.anonymous-user-id文件中的裸 UUID 行(home 由resolveDshHome解析,优先级$DSH_HOME>~/.dsh),首次调用时铸造并持久化一个随机 UUID v4;后端构造函数将其作为 Resource 的user.id(OTel semconv 标准用户属性)携带,每个导出批次(batch)携带一次

该实现最初内嵌在session-telemetry-otel中,因为当时不存在第二个真实消费方。/feedback后来成为第二个消费方,于是 共享 ID 决策 将所有权迁移至独立包@deepseek-ai/dsh-anonymous-user-id,存储、匿名性、并发与丢失语义均保持不变;DeepSeek 直连请求身份 则是同一 ID 的第三个消费方。

设计裁决表

裁决项取值理由
ID 来源随机 UUID v4,绝不从 hostname、网络地址或 git remote 派生派生 ID 可逆,会让"匿名"沦为虚构
存储形式.anonymous-user-id,裸 UUID 行 + 换行,无 JSON 包装身份是独立事实,不应归档在某个遥测数据流的文件名/格式之下
IO 形式同步 IO + 以解析后文件路径为键的进程生命周期 memoOpenTelemetrySessionBackend构造函数是同步的(异步会重塑插件加载流程);每个进程只碰一次磁盘,运行中删除文件不影响当前进程
并发首启用排他创建(wx)写入决出胜负;失败者重读胜利者的 ID覆盖常见并发场景(若重读恰好落在胜者"创建→写入"的微秒窗口内,本次运行仍可能每进程各持一个 ID,下次启动收敛到持久化值——遥测级后果,可接受)
丢失语义文件被删 → 下次启动铸造全新 ID;丢失被接受匿名身份没有恢复价值;可恢复性需要派生材料,这与匿名性冲突
写入失败尽力而为:返回内存中的 IDSessionTelemetryBackend绝不被只读 home 阻塞
上报位置Resource 属性,而非每条记录属性每个批次一次足以支撑 Resource 维度聚合;逐条注入会触碰 seam 契约并增大线上体积
semconv 依赖不引入@opentelemetry/semantic-conventions一个字符串常量不值得引入一个依赖
归属包@deepseek-ai/dsh-anonymous-user-id,由 OTel 后端、/feedback、DeepSeek 直连请求共享消费者共享同一存储契约,而不依赖某个导出器后端
独立开关任何消费方都可创建该身份;DSH_TELEMETRY_DISABLED只停止遥测上报,不关闭 feedback 确认或 DeepSeek 请求头

源码级拆解:getOrCreateAnonymousUserId()的实现机制

完整实现位于 packages/identity/anonymous-user-id/src/index.ts,包内只暴露一个主函数与一个文件名字符串常量:

export type AnonymousUserId = Branded<'AnonymousUserId'> /** File inside the harness home storing the id: a bare UUID line, no wrapper format. */ export const ANONYMOUS_USER_ID_FILE_NAME = '.anonymous-user-id' const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i export function getOrCreateAnonymousUserId(options: AnonymousUserIdOptions = {}): AnonymousUserId { const file = join(resolveDshHome(undefined, options.env ?? process.env), ANONYMOUS_USER_ID_FILE_NAME) const cached = memo.get(file) if (cached !== undefined) return cached let id = readPersistedId(file) if (id === undefined) { const generate = options.randomUUID ?? randomUUID const created = generate() as AnonymousUserId try { mkdirSync(dirname(file), { recursive: true }) writeFileSync(file, `${created}\n`, { encoding: 'utf8', flag: 'wx' }) id = created } catch { // wx 拒绝(EEXIST)同时覆盖并发胜者与既有损坏文件两种情况…… id = readPersistedId(file) if (id === undefined) { try { writeFileSync(file, `${created}\n`, 'utf8') } catch { // 尽力而为持久化:home 不可写时也在本次运行中返回一致 ID } id = created } } } memo.set(file, id) return id }

(完整代码见 src/index.ts。)

四个关键机制

  1. 随机铸造:ID 来自node:cryptorandomUUID()(src/index.ts),即标准 UUID v4。源码注释明确:它绝不从 hostname、网络地址、git remote 或任何其他可识别来源派生——匿名是铸造行为本身的属性,而非事后修饰。
  2. 逐路径 memomemo是一个以解析后文件路径为键的Map(src/index.ts),所以不同的测试 home / 不同的$DSH_HOME永远不会共享 ID,且同一进程只触碰一次磁盘。
  3. 读取校验readPersistedId读取文件后trim()并与 UUID 正则匹配(src/index.ts),不合法或不可读返回undefined,由调用方走"铸造并持久化"路径。
  4. wx排他创建 + 失败回退:首写用writeFileSync(file, ..., { flag: 'wx' })。若抛错(EEXIST等),先重读文件以采纳并发胜者写入的合法 ID;若重读仍无效(损坏文件),则回退到普通覆盖写;覆盖写再失败(只读 home)则直接返回内存中新 ID——telemetry 与 feedback 永远不会被不可写的 home 阻塞。

可测试性设计

AnonymousUserIdOptions提供两个默认值钩子(src/index.ts):env(默认为process.env,用于注入DSH_HOME)与randomUUID(默认为crypto.randomUUID,用于模拟并发)。测试套件 tests/anonymous-user-id.spec.ts 用这两个钩子覆盖了全部边界:

  • 首次使用创建、持久化并返回裸 UUID 行(含末尾换行);
  • home 目录缺失时自动递归创建;
  • 后续调用返回持久化 ID,容忍首尾空白;
  • 损坏文件被新 ID 覆盖;
  • 并发胜者采纳:在初始读与wx写之间植入胜者文件,返回的是胜者的 ID;
  • home 无法容纳文件时返回可用 ID 且不持久化(最佳努力);
  • 进程生命周期 memo:删除文件后仍返回同一 ID;
  • 不同 home 持不同 ID;
  • 默认读取process.env

存储契约:一个裸 UUID 行

.anonymous-user-id的存储契约被刻意保持极简:裸 UUID 行 + 换行,无 JSON 包装、无版本标记。裁决理由写得很清楚——"身份是独立事实,不是归档在某条遥测数据流的文件名/格式之下"。读取时对整行trim()后做 UUID 校验;首个写入者使用排他创建(wx),并发失败者重读并采纳胜者值;损坏或不可读文件落入"铸造并覆盖"路径。memo 以解析后路径为键,保证不同 home 不共享 ID。

一个值得注意的开放问题(记录在包 README 的 Dev Note):由于契约是"无版本标记的裸 UUID 行",未来若要在 ID 旁增加第二个值或将行包装进容器,对存量文件没有迁移方案——带版本的行格式是让这类变更变安全的一种方式。

三大消费方:同一 ID 贯穿遥测、反馈与请求

1. OTel 会话遥测后端:Resourceuser.id

在 packages/session/session-telemetry-otel/src/index.ts 中,后端构造LoggerProvider时通过resourceFromAttributes组装 Resource:

this.provider = new LoggerProvider({ resource: resourceFromAttributes({ 'service.name': APP_IDENTITY.product, 'service.version': APP_IDENTITY.version, // OTel semconv 标准用户属性,每导出批次在 Resource 上携带一次而非逐条记录 'user.id': getOrCreateAnonymousUserId(), }), ... })

正如裁决表"上报位置"一栏所述:collector 按 Resource 聚合,ID 在进程内本来就是稳定的,每批次一次足矣;逐条注入不仅多余,还会触碰 session-telemetry seam 契约并增大线上传输体积。同时该包没有引入@opentelemetry/semantic-conventions依赖——一个'user.id'字符串常量不值得增加一个依赖。调用发生在后端构造函数内,这与"同步 IO + memo"的裁决直接呼应:构造函数同步,插件加载流程不被重塑。

2./feedback命令:确认消息中的匿名用户行

在 packages/feedback/command-feedback/src/index.ts 中,feedback 成功确认消息形如:

Feedback recorded for session {sessionId} Anonymous user: {userId}. {sharingDisclosure(telemetry)}

这样运维方可以把确认消息与导出的遥测记录关联起来。设计上,feedback 包只依赖身份能力本身,而非遥测 seam 或 OTel SDK——避免了"直接命令依赖可选导出器后端"以及"遥测导出 feedback 时形成反向依赖环"的问题(详见 共享 ID 决策 的备选方案分析)。

3. DeepSeek 直连请求:x-deepseek-harness-user-id请求头

在 packages/llm/llm-deepseek/src/adapter.ts 中,请求头集合包含:

const headers = { 'authorization': `Bearer ${apiKey}`, 'content-type': 'application/json', 'accept': 'text/event-stream', ...attributionHeaders(), 'x-deepseek-harness-user-id': String(userId), ...options.sessionId !== undefined ? { 'x-deepseek-harness-session-id': String(options.sessionId) } : {}, ...options.purpose === 'compaction' ? { 'x-deepseek-harness-compact': '1' } : {}, }

这样用量可以按安装实例(harness home)归属。注意共享 ID 决策中的两个时序细节:无效 feedback 在解析 ID 之前就被拒绝DeepSeek 适配器在凭据验证成功后才解析 ID——因此一次空命令或一次凭据失败都不会创建.anonymous-user-id文件(详见 共享 ID 决策)。

消费方聚合图

┌──────────────────────────────────────┐ │ $DSH_HOME/.anonymous-user-id (UUID) │ └──────────────────┬───────────────────┘ getOrCreateAnonymousUserId() ┌───────────────────┬───────┴──────────┬──────────────────┐ ▼ ▼ ▼ ▼ OTel Resource /feedback 确认消息 x-deepseek-harness- (未来新消费方 'user.id' "Anonymous user: …" user-id 请求头 直接导入复用)

备选方案回顾:为何这些路被否决

被否决方案一句话理由
Hostname/IP 哈希派生 ID(此前的旧裁决)可逆就意味着不匿名;随机 UUID 语义干净——用户裁决予以取代
在每条记录属性上带user.id(Claude Code 的形状)触碰 session-telemetry seam 契约或逐条注入、增大线上体积;每批次放 Resource 上已能完成聚合
/feedback需要 ID 之前就抽出共享包(初版做法)当时唯一真实消费方是 OTel 后端;只有直连 feedback 需要同一关联 ID 时才证明抽包是合理的
AppCLIEntry 读取 ID 并通过配置 patch 注入每个表层入口都要接线;把运行时事实塞进部署配置混淆了两类关注点
将能力放进@deepseek-ai/dsh-home-pathspaths 是纯路径计算、零 IO;把"持久化身份"能力塞进去会污染包边界

其中"先在 OTel 后端内实现、待出现第二个消费方再抽包"的演进路径尤其值得注意:它体现了"共享库只有在确有两个以上真实消费者时才值得抽离"的工程原则。/feedback成为第二个消费方后,共享 ID 决策将所有权迁移到独立包,并完整保留了既有的随机 UUID、home 解析、进程 memo、排他创建并发、损坏替换与最佳努力写语义——不改变本 Note 记录的任何存储、匿名性、并发或丢失语义。

后果与运维视角

  • 一个$DSH_HOME即 OTel 数据流中的一个稳定用户;不同 home 天然是不同用户,且不存在跨 home 关联机制。
  • OTel 数据流、/feedback与 DeepSeek 直连请求共享同一个.anonymous-user-id
  • 删除.anonymous-user-id即重置身份(下次启动生效);home 不可写时,每个进程在 home 变可写之前持有自己的内存 ID。
  • 就匿名用户 ID 部分而言,default-mount Note 中"身份跟进"事项已由此决策闭环;hostname/表面维度、脱敏规则与用量指标跟踪仍保持开放。

实际操作速查

  • 观察 ID:查看$DSH_HOME/.anonymous-user-id(默认~/.dsh/.anonymous-user-id),文件内容即裸 UUID 行。
  • 重置 ID:删除该文件,下次启动铸造新 ID;运行中的进程因 memo 机制保持当前 ID 直到退出。
  • 多个 home:为不同部署设置不同DSH_HOME,即可获得相互独立、不可关联的匿名身份。
  • 在自己的包中使用(来自包 README 的官方示例,README.md):
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id' const userId = getOrCreateAnonymousUserId() // 进程生命周期内稳定
  • 遥测总开关DSH_TELEMETRY_DISABLED非空即禁用遥测上报行(含0/false),但它不会禁用 feedback 确认或 DeepSeek 请求头中的匿名 ID。

已知限制

  • 删除后不可恢复:文件丢失即铸造新匿名身份,这是有意设计——恢复需要稳定的派生材料,而派生材料会削弱匿名性。
  • 并发为最佳努力:落在"并发进程排他创建与写入完成"之间狭窄窗口的读取者,本次运行可能使用不同的内存 UUID;后续启动会收敛到持久化值。
  • 无跨 home 身份:不同$DSH_HOME之间无法关联。
  • 配置的 DeepSeek 网关会收到该 IDdsh-llm-deepseek向其解析出的baseURL(含部署覆盖)发送稳定请求头,与遥测共享模式无关。
  • 删除文件不会重置当前进程:memo 让本次运行的 ID 保持到下次启动。

延伸阅读

  • 决策原始记录:2026-07-31-telemetry-anonymous-user-id.md(及中文版)
  • 遥测默认挂载决策:2026-07-31-web-telemetry-default-mount.md
  • 共享 ID 抽包决策:2026-08-07-shared-feedback-telemetry-user-id.md
  • 包级说明:identity/anonymous-user-id/README.md,身份组地图:identity/README.md
  • home 路径解析:util/home-paths/README.md(拥有$DSH_HOME~/.dsh的解析语义)
  • 消费方源码:session-telemetry-otel/src/index.ts、command-feedback/src/index.ts、llm-deepseek/src/adapter.ts

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询