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 + 以解析后文件路径为键的进程生命周期 memo | OpenTelemetrySessionBackend构造函数是同步的(异步会重塑插件加载流程);每个进程只碰一次磁盘,运行中删除文件不影响当前进程 |
| 并发首启 | 用排他创建(wx)写入决出胜负;失败者重读胜利者的 ID | 覆盖常见并发场景(若重读恰好落在胜者"创建→写入"的微秒窗口内,本次运行仍可能每进程各持一个 ID,下次启动收敛到持久化值——遥测级后果,可接受) |
| 丢失语义 | 文件被删 → 下次启动铸造全新 ID;丢失被接受 | 匿名身份没有恢复价值;可恢复性需要派生材料,这与匿名性冲突 |
| 写入失败 | 尽力而为:返回内存中的 ID | SessionTelemetryBackend绝不被只读 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。)
四个关键机制
- 随机铸造:ID 来自
node:crypto的randomUUID()(src/index.ts),即标准 UUID v4。源码注释明确:它绝不从 hostname、网络地址、git remote 或任何其他可识别来源派生——匿名是铸造行为本身的属性,而非事后修饰。 - 逐路径 memo:
memo是一个以解析后文件路径为键的Map(src/index.ts),所以不同的测试 home / 不同的$DSH_HOME永远不会共享 ID,且同一进程只触碰一次磁盘。 - 读取校验:
readPersistedId读取文件后trim()并与 UUID 正则匹配(src/index.ts),不合法或不可读返回undefined,由调用方走"铸造并持久化"路径。 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-paths | paths 是纯路径计算、零 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 网关会收到该 ID:
dsh-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),仅供参考