DeepSeek Harness 密钥放哪:四层解析与五个报错

密钥只有一处会被真正读到,就是 ~/.dsh/.credentials.yaml。设置页里保存的 key 也只写进这里,不会出现在任何模型配置块中。同名环境变量排在它后面。记住这个顺序,MISSING_CREDENTIAL 一类的报错基本十分钟能定位。下面按存哪、解析、报错、验证四步走一遍,命令都能直接复制。

密钥存哪:一个 600 权限的 YAML

桌面端走 设置 → 模型 → 添加提供商 → 自定义模型 API,填 Provider ID、API 地址、协议和模型,保存。配置块进 profile 的补丁文件,密钥单独进 $DSH_HOME/.credentials.yaml。两份文件拆开,是为了让配置能导出、能分享、能进版本控制,密钥永远留在本机。桌面端装在哪、四种会话模式怎么用,安装那篇 有完整流程。

这个文件只有三个顶层键。本机实测的结构如下,值已替换:

version: 1
records:
  llm-pi-ai/openai-codex:
    kind: grant
    payload:
      type: oauth
      access: eyJhbGciOi...
refs:
  DEEPSEEK_API_KEY: sk-...
  MINIMAX_CN_API_KEY: sk-...

refs 按环境变量名存明文密钥。records 按 <owner>/<id> 存插件级授权,每条带 api-key 或 grant 标签。授权记录的内容存储层不解释,原样保留,只有写出它的插件才懂。在文件里看到一坨 base64 不必惊讶,那多半是某次账号登录留下的 token。

权限是这份文件唯一的安全边界。产品创建时就是本人可读,POSIX 系统上发现其他用户也能读,会直接拒绝加载并提示 chmod 600。本机 stat 的结果是 -rw-------,正常。自己确认一条命令够:

stat -f '%Sp %N' ~/.dsh/.credentials.yaml   # macOS
stat -c '%A %n' ~/.dsh/.credentials.yaml    # Linux

有个容易忽略的边界:agent 的工具进程以你的身份运行,读这个文件和读别的文件没有区别。产品不会主动把路径交给 agent,也不把内容塞进环境变量,但这不是内核级隔离。真要隔离供应商密钥,得换存储后端,改权限没用。

四层顺序:受管文件永远赢

同一个引用名可能在不同地方都出现过。解析按固定顺序走,第一个找到值的层胜出,后面的层直接不看。

优先级 位置 能否在这层写 覆盖谁
1 受管文件 $DSH_HOME/.credentials.yaml 的 refs 段 能,设置页与 set 都写这里 全部
2 启动时的环境变量,如 DEEPSEEK_API_KEY=... dsh 不能,只读 下面两个 .env
3 项目 .env,取启动时工作目录 不能,只读 home 的 .env
4 home .env,即 $DSH_HOME/.env 不能,只读 不覆盖任何层

受管文件排第一的理由很实际。在产品里点保存的 key 必须对所有 provider 一致生效,否则同一台机器上两个路由读到两个不同的值,排查成本高到没法接受。启动环境排第二,是为了兼容 CI 密钥、容器 -e 和一次性 DEEPSEEK_API_KEY=... dsh 这类用法。

第三层取的是启动时的工作目录。桌面端双击图标启动,工作目录通常是应用包内部或者用户主目录,落在项目根目录的概率很低。命令行在项目目录里敲 dsh 才会命中这一层。想要稳定就把值写进受管文件,别赌 cwd。

还有一个时间维度,环境层是启动瞬间的快照。启动之后再 export,进程看不到。改完环境变量必须完全退出应用重开,export 完直接切回窗口是不管用的。

受管文件反过来是热加载的。文件监听默认开着,改动落地 100 毫秒后重载,不用重启。手改 YAML 之后切回窗口就能生效,这一点比环境变量好用得多。

apiKeyEnv 的名字必须能当 shell 变量

provider 配置里的 apiKeyEnv 填的是引用名,不是密钥本身。名字合法性有一条硬规则,必须匹配 ^[A-Za-z_][A-Za-z0-9_]*$,字母或下划线开头,后面只允许字母、数字、下划线。带连字符、带点、带中文的名字不算合法引用,解析时按根本没设过处理。

设置页不会让你填这个名字。它按 Provider ID 自动派生,规则是大写化,把非字母数字的字符全换成下划线,末尾拼上 _API_KEY。几个常见 ID 算出来是这样:

Provider ID 命中的 refs 键 说明
siliconflow SILICONFLOW_API_KEY 纯小写,直接大写化
acme-gateway ACME_GATEWAY_API_KEY 短横线换下划线
ollama-local OLLAMA_LOCAL_API_KEY 同上,本地服务也走这条
zai ZAI_API_KEY 三个字母,最短的一类
my.site.v2 MY_SITE_V2_API_KEY 点号同样换成下划线

手写配置块时真正会踩的地方在这里。Provider ID 允许小写字母、数字和短横线,所以 acme-gateway 是合法 ID,但把 acme-gateway 原样填进 apiKeyEnv 就错了,ACME_GATEWAY_API_KEY 才对。判断方法简单到不用记规则:名字里出现连字符就是错的。

手写一条工具本身不认识的 provider,api、baseURL、非空的 models 三项缺一不可。缺项在写入那一刻就被拒,报错会点名是哪条路由、哪个模型。想省事可以用设置页的 获取可用模型,但它有个前置条件:先填好 API 地址,按钮才可用。填了地址再点,是它唯一能用的顺序。

模型列表接口的兼容程度分三档。OpenAI 的两种协议能读标准的 data 数组,也认带 models 映射的增强返回。Anthropic Messages 走它自己的列表路由。其余协议一律问不出来,只能手填模型 ID。列表返回的只是候选信息,不会落盘,真正决定这条路由能服务什么的还是配置文件。

没有鉴权的本地服务也会报 MISSING_CREDENTIAL

把本地 Ollama、vLLM、LM Studio 这类服务接进来时,经常会觉得莫名其妙。服务端根本不检查 key,客户端却报凭据缺失。原因是 OpenAI 兼容的那套实现固定要求一个 API key 或一个 Authorization 头,它不看服务端到底认不认。

两种办法都能过。给 apiKeyEnv 指一个随便有值的引用,比如 LOCAL_DUMMY_KEY,再往受管文件里塞一行任意字符串。或者在这条 provider 的 headers 里直接写 Authorization: Bearer local。前者更省事,以后换关键值不用动配置。

完整的 provider 配置块、compat 开关怎么填、后端上下文要开多大,接本地 Ollama 那篇 已经写透了,本篇不重复。

五个报错码怎么区分

失败码是稳定的,不会随小版本漂移。定位时先看码,再看它有没有触到网络,这两步能砍掉大半无效排查。

报错码 含义 是否在网络请求前返回 先查什么
MISSING_CREDENTIAL 引用解析不到任何值 是 四层里哪层有值,名字拼对没有
INVALID_CREDENTIAL 值找到了但不可用 是 密钥格式、是否过期或被吊销
UNKNOWN_MODEL 模型未在该路由声明 是 models 列表里的 ID 是否字符级对齐
QUOTA 额度或账单层面终止 否 账户余额与套餐,重试无意义
RATE_LIMIT 暂时性限流 否 并发与退避,可恢复

单看文字容易混的是前两个。MISSING_CREDENTIAL 表示这个引用没解析到值,问题在配置或文件层。INVALID_CREDENTIAL 表示值找到了但不可用,比如格式不对、过期、被吊销,报错会点名要修哪个引用,但绝不带密钥的任何片段。看到后者别再翻配置文件,去检查 key 本身。

UNKNOWN_MODEL 值得单独说。它发生在网络 I/O 之前,也就是说请求根本没有发出去。遇到它去查网络、查余额、查代理全是白费。要查的是这条路由的 models 列表里有没有这个 ID,字符级对齐,大小写和斜杠都算数。

QUOTA 与 RATE_LIMIT 的区别决定要不要重试。前者终止,重试只会继续失败。后者暂时,退避之后能恢复。默认策略是普通模式五次重试,需要时可在这条 provider 的 retryPolicy 里单独改,maxRetries 就是次数。

踩坑一:删掉 key 之后反而通了

这个现象第一次见会以为见鬼。把受管文件里那条 key 删了,请求却成功了。删除只删掉最上面一层,下面三层的同名值立刻露出来接管,启动环境里留着的旧 key 就这样无声无息地复活。

产品对空值的处理指向同一件事。受管文件里不允许存空字符串,要停用就删整条记录。空串会被当作非法值拒掉,因为空值在语义上等于没有这条记录,与其留一个假占位,不如让下一层接管。

排查这类问题的动作是看 describe 回的 source 字段,它会说明当前这个值来自哪一层。只看 configured 为真没有意义,四层里任何一层有值都是真。

踩坑二:改完 API 地址保存被拒

设置页里改 baseURL、apiKeyEnv、显示名,点保存,弹出拒绝提示,而这三个字段本身都没错。真凶是同一条路由下另一个模型有问题。配置更新校验整条 provider,只要有一个模型不可服务,整条就被挡住。

典型场景是从聚合网关抄来一组模型 ID,其中某个模型需要单独声明协议。这时候你只想改个显示名,也会连坐失败。解法是把那个模型的 api 补上,或者直接删掉它,在同一个编辑草稿里一起提交。分两次保存是过不去的。

已经写坏但还没修的路由不会消失。加载时会把它保留成一条带诊断的可编辑行,可以继续点开改。修好或者删掉,诊断才清掉。这条行为对排错是好事,至少不会因为一次手滑丢掉整段配置。

踩坑三:权限一松整库不加载

明文 YAML 加 0600 的组合,容错空间很小。用编辑器另存、从备份恢复、跨机器拷贝,都可能把权限改成 644。POSIX 系统上这一改不是警告,是启动直接失败,报错里会给你 chmod 600 的命令。

热加载时的表现又不一样。文件改坏了进程不会崩,它继续用上一次成功解析的内容,同时打一条告警。于是症状变成改了 key 却没有变化。这时候先看日志有没有告警,再回头确认文件本身能不能解析。

还有一个保护值得知道:磁盘上的文件解析不了时,保存操作会失败,不会拿新内容覆盖掉自己读不懂的东西。所以手改到一半去点保存,配置不会丢,但也不会生效。Windows 没有可查的权限位,这项检查在那边直接跳过,靠目录权限兜底。

三个动作确认 key 真被读到

改完不要直接发消息试。前两个动作不发请求就能确认状态,最后一个才真正花 token。

  • 在设置页把这条 provider 的 key 删掉,模型选择器里它应该消失,设置入口仍然在。这是设计好的行为:凭据缺失时模型发现返回空目录,路由被隐藏,配置行保留下来供你修。
  • 看 describe 回读的 configured、source、writable 三个字段。它永远不返回密钥本身,所以把这几个字段贴出来求助是安全的。
  • 命令行确认版本与挂载状态,两条命令都不发请求。
dsh --version
dsh --profile web --dump-config | grep -A2 llm-pi-ai

本机实测第一行输出是 0.2.1-rc.5。第二条能在合成出来的配置树里看到 llm-pi-ai 这一项,且后面不跟 config 段。没有 config 就是休眠态,路由数为零,模型选择器里不会多出任何一项。要让路由活起来必须有 provider 配置,这跟有没有 key 是两件事。

用命令行跑同一套配置还有一处容易误判。图形界面登录得到的账号凭据属于 records 段,跟 refs 段的 key 走两条通道。命令行会话里没有对应账号登录时,会报 MISSING_CREDENTIAL 并点名具体引用,而文件里的 key 一直都在。遇到这种情况别去重填 key。需要连插件一起排查的话,技能手动安装那篇 里有同一套命令行验证手法。

FAQ

问:key 写进项目 .env 了,为什么桌面端读不到?

项目 .env 取的是启动时的工作目录。双击图标启动的桌面端,工作目录通常不是项目根目录,这一层就被跳过了。写进受管文件最稳,或者从项目目录里用命令行启动。

问:两个 provider 能用同一个 API key 吗?

能,但更推荐各用各的引用名。两个 provider 指向同一个 refs 键时,改一处会同时影响两条路由,而且删除某条路由时凭据清理的判断会变得不可预期。清理只在引用与派生的 <ROUTE>_API_KEY 完全一致且可写时才发生,自定义引用会被保留。

问:手改 YAML 会不会覆盖掉界面里刚存的设置?

不会。两边同时改的时候,改动会被合并保留,写入走跨进程的文件锁。注释和未动到的条目也会原样留着,只有你删掉的那条会连同它正上方的注释一起消失。

站内接着看

先把桌面端装上再谈凭据:桌面端安装教程。要接本地模型看这篇:接本地 Ollama 的三个坑。要装插件与技能:SKILL.md 四个失效点。