尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

写 DSH 的 profile 补丁,TaoToken 让 HMR 生效

写 DSH 的 profile 补丁,TaoToken 让 HMR 生效 1. 一份只剩注释的补丁文件把 HMR 变成了「什么都没发生」上周在 web profile 上换 llm 凭据我做的操作再普通不过打开$DSH_HOME/profiles/web/cordis.patch.yml把 baseUrl 指到https://taotoken.net/apiapiKey 换成从 TaoToken 官网 控制台取的新值保存然后等 HMR 把新配置推上去。结果 Web UI 里模型调用照样鉴权失败再跑一次--dump-config输出的还是旧的 baseUrl。排查了二十分钟才反应过来我在编辑前把文件里原有的几行内容删了只留了一段注释。DSH 的用户补丁层要求解析结果是一个非空的列表空文件或纯注释会被判定为加载失败补丁根本没进组合HMR 自然也就没有东西可热更。这跟「改了没生效」完全是两回事——前者是文件没被接受后者才是热更链路的问题。这篇把原文那套「观察 → 修改 → 验证」的四个练习按一个 DSH 配置维护者的日常视角重走一遍。重点落在练习 4怎么把 llm 凭据的覆盖持久化进 profile 补丁改完不重启直接看 Web UI 或者下一次 dump 出来的值有没有变。中间会把 dump-config 的分层、patch 的整行替换语义、!!js的求值时机、以及加载顺序与激活顺序这几件事钉死。所有涉及 Key 的步骤都走 TaoTokenBase URL 统一填https://taotoken.net/api不绕任何弯路。2. 先读出层次base 与 full 的 diff 到底差在哪DSH 里「组合即真相」这句话不是修辞。你看到的每一个运行时行为背后都是一棵已经求值完成的配置树能不能读懂这棵树决定了你是在维护系统还是在猜系统。练习 1 的目标很朴素对 web profile 的完整组合建立行级直觉。先把两份快照落盘一份只含 bundle 默认层一份是叠加了用户补丁之后的最终值export DSH_HOME${DSH_HOME:-$HOME/.dsh} # 只看 bundle 层不含任何用户补丁 pnpm dsh --profile web --dump-config --default-only /tmp/base.yml # 完整组合bundle profile 补丁 家目录补丁 pnpm dsh --profile web --dump-config /tmp/full.yml # 差异行就是「用户侧引入的部分」 diff -u /tmp/base.yml /tmp/full.yml | head -n 80diff 的输出信息量比你想象的大。多出来的行只有三类来源profile 级补丁、家目录级补丁、以及命令行--patch临时叠加的内容。如果 diff 里出现了你没印象的条目优先去$DSH_HOME/cordis.patch.yml找——家目录级的补丁会作用于所有 profile是最容易被遗忘的一层。接着在/tmp/full.yml里定位那七行关键条目llm、session、agent-loop、tools、system-prompt、sandbox-policy、agent-presets。对每一行你要能说出两件事它由哪个包提供name 字段以及它携带的关键 config 有哪些。按我自己的归类习惯这七行大致覆盖基础设施、编排、凭据设置、持久化、安全策略、模型面工具、以及委派预设。功能分组的具体命名因人而异但你必须形成一套自己的映射否则每次读配置都是重新认识一遍。这里有个容易被忽略的互斥关系bash-sandbox与pwsh-sandbox不能同时作为生效项。两者提供的是同一类能力——命令执行的沙箱策略——注册到同一个服务键下。在同一份组合里同一个服务键只允许一个提供者处于激活态所以它们必须二选一选择依据是你的实际运行平台与 shell 环境。理解了这条你就理解了 DSH 组合里「同名服务只能有一个赢家」这条底层规则后面system-prompt、llm的覆盖行为都是它的推论。验收方式很直接合上文件凭记忆把 base 的七个分组说一遍再把full.yml存成文件用--patch叠加一次看看输出是否和你的预期一致。这一步做完你手上就有了后续所有练习的对照基线。3.--patch只做整行替换验证「后写的层赢」练习 2 要验证两个语义后写的层覆盖先写的层以及覆盖的粒度是整行而不是深合并。准备一个最小补丁文件只改system-prompt这一行# /tmp/persona-override.yml - name: system-prompt config: persona: 你是一名严谨的 SRE先给结论再给证据链。叠加到现有组合上然后和基线对比pnpm dsh --profile web --dump-config \ --patch /tmp/persona-override.yml /tmp/patched.yml # 只看被改动的行 diff -u /tmp/full.yml /tmp/patched.yml预期结果是system-prompt行的 persona 变成了你写的那句其余条目原封不动。如果你在补丁里只写了persona却指望原来的其他字段比如工具白名单、输出约束继续保留那你一定会失望——patch 是整行替换不是字段级合并。想让某个字段保持原值就必须在新行里把它重新写出来。base 层里那句「mode-specific values live in mode bundles」的注释讲的就是这个道理既然覆盖是整行语义那跟模式相关的取值就应该集中放在模式 bundle 里避免每次覆盖都要重述一大堆无关字段。这条规则带来一个很实用的习惯写补丁前先把你打算覆盖的那一整行从full.yml里复制出来改掉需要变的字段再整体贴进补丁。这样既不会丢字段也方便你在 diff 里一眼看出哪几行真的被替换了。4.!!js的求值时机在挂载不在每次读取练习 3 的对象是运行时表达式。在--dump-config的输出里把所有!!js出现的位置找出来逐个判断两件事什么时候求值在什么上下文里求值。答案是固定的求值发生在插件挂载那一刻而不是每次有人读取这个字段的时候求值环境是 Loader 上下文你能拿到的是process、ctx、dshHomePath、baseUrl这几个入口。「挂载时求值」这一条有几个直接推论值得单独记住第一写在!!js里的环境变量只会在挂载时读一次。你在进程已经跑起来之后再去改环境变量不会影响已经求值完成的结果。想让新值生效得让这一行重新挂载。第二ctx.serviceName只有在inject对应的服务激活之后才可引用。如果这一行声明了inject那么它在服务出现之前处于 waiting 状态此时去取ctx上的服务引用是没有意义的。这也是为什么同一个表达式在不同行里表现不一样差异不在表达式本身而在那一行的inject声明。第三dshHomePath与baseUrl这类由 Loader 注入的值会跟着运行时环境走。用dshHomePath(sessions)拼路径比手写~/.dsh/sessions更稳因为$DSH_HOME被改到别处时你不用回头改配置。把这三个变量拼进llm行的补丁里就是你后面换凭据时最实用的写法- name: llm config: baseUrl: https://taotoken.net/api apiKey: !!js process.env.TAOTOKEN_API_KEY注意这里的取舍把 Key 交给环境变量补丁文件本身就不含明文凭据可以安全地进 Git。代价是改环境变量必须重新挂载该行才能生效。如果你更在意「改完立刻热更」那就得走后面那节讲的补丁文件热更路径用明文或外部密钥文件承载凭据。5. 练习 4 主菜把 llm 凭据写进 cordis.patch.yml前面三个练习都是在命令行上临时验证练习 4 要做的是把它持久化。这是本篇的核心动作。第一步确认 profile 目录位置。默认情况下$DSH_HOME是~/.dshweb profile 的补丁路径就是ls -l ${DSH_HOME:-$HOME/.dsh}/profiles/web/cordis.patch.yml如果这个文件不存在可以新建但新建之后必须是合法的 YAML 列表不能是空文件。第二步去 TaoToken 控制台取 Key。入口在 TaoToken 官网登录后在控制台创建 API Key拿到以sk-之类的形式开头的字符串。本文所有示例里统一用占位符YOUR_API_KEY你替换成自己的即可。Base URL 固定为https://taotoken.net/api这个地址在工具配置里不加任何查询参数。第三步把 llm 覆盖写进 profile 补丁# ~/.dsh/profiles/web/cordis.patch.yml - name: llm config: provider: openai-compatible # 字段名以你 dump 出的 llm 行为准 baseUrl: https://taotoken.net/api apiKey: !!js process.env.TAOTOKEN_API_KEY model: 在 TaoToken 控制台选定的模型 ID这里要强调一句上面config下的字段名只是结构示意真正生效的键名请以你本机--dump-config输出的llm行为准。不同版本、不同 bundle 的字段可能有差异照抄字段名而不对照实际输出是最常见的一类无效补丁。第四步在 shell 里提供环境变量然后再启动export TAOTOKEN_API_KEYYOUR_API_KEY pnpm dsh --profile web第五步也是这一节的关键不要重启。保持进程运行直接编辑cordis.patch.yml把baseUrl改成另一个合法值或者把model换掉保存。用户补丁层是有 HMR 的文件落盘之后新的值会被重新求值并应用到组合上。如果你不想用环境变量希望直接在文件里写凭据那就把那一行改成- name: llm config: baseUrl: https://taotoken.net/api apiKey: YOUR_API_KEY明文的代价是这份文件不能进版本库收益是改完保存能立刻看到热更效果不用去动进程环境。两种写法选一种别混着用否则你会分不清到底哪一层的值在生效。第六步验证家目录级的优先级。在$DSH_HOME/cordis.patch.yml里再写一条补丁# ~/.dsh/cordis.patch.yml —— 对所有 profile 生效 - name: llm config: baseUrl: https://taotoken.net/api apiKey: YOUR_API_KEY model: 全局默认模型 ID最终值的推导顺序是bundle 默认 → profile 补丁 → 家目录补丁 → 命令行--patch。后面一层赢覆盖粒度是整行。所以上面这条家目录级补丁会把 profile 里的 llm 行整个顶掉包括你为 web profile 单独挑的 model。这正是需要警惕的地方家目录级适合放「所有 profile 都该一致」的东西比如统一的 baseUrl一旦把 model 这类需要按 profile 区分的字段塞进去就会静默覆盖掉下层更精细的设置。6. 不重启看 HMRWeb UI 与 dump-config 两条验证路径补丁写完了怎么确认它真的生效两条路径一快一准。快的那条是 Web UI。打开正在运行的 Web 界面进到模型或会话相关的设置页直接发一条最短的请求。如果 llm 行的 baseUrl 和 apiKey 都对鉴权会通过如果补丁没被接受你会看到和改之前一模一样的报错。这条路的好处是反馈快坏处是它只能证明「能用」不能告诉你最终值到底是什么。准的那条是 dump-config。新开一个终端跑pnpm dsh --profile web --dump-config /tmp/after.yml diff -u /tmp/full.yml /tmp/after.ymldiff里应当出现你刚加的那行 llm 覆盖。注意这里读的是磁盘上最新的补丁文件跟运行中的进程是不是热更成功没有必然关系——dump 反映的是「现在加载会得到什么」不是「进程里现在是什么」。想验证热更本身还是要回到 Web UI 那条路径或者观察进程日志。把两条路径合起来用你就有了一个可靠的判断链diff 确认文件被正确解析并参与组合Web UI 确认运行中的实例确实拿到了新值。两者结果不一致时问题大概率出在 HMR 这一环而不是补丁语法。值得注意的是HMR 覆盖的是用户补丁层。bundle 层的内容变化不会走这条热更路径因为 bundle 属于组合的骨架改动它意味着重新挂载。理解了这一点你就不会去纠结「为什么我改了默认 bundle 里的东西没热更」——它本来就不在设计范围内。7. 层级优先级与三个高频卡点这一节把练习 4 里最容易翻车的地方集中列一下。卡点一补丁文件为空或只有注释。前面已经踩过一次。补丁层的解析结果必须是非空列表纯注释会被当成空解析结果直接加载失败。规则是要么至少写一条有效条目要么这个文件干脆不存在。卡点二想禁用某一行时直接删行。删掉一条上层定义过的行会让上层补丁「静默失效」——你以为自己只是去掉了它实际上覆盖链断了后续依赖这行的逻辑可能拿不到预期的值。正确的做法是保留行、显式声明禁用- name: agent-presets disabled: true这也是 web-app 那一层坚持「禁用而非删除」的原因组合的层叠关系是显式的删行会让层级之间的对应关系对不上号。卡点三把家目录级的覆盖当成「兜底默认」。它不是兜底它是覆盖。优先级排在 profile 之后意味着它会赢。想让某个值成为低优先级兜底正确的位置是 bundle 层或者 profile 层家目录级只应该放那些你希望强制统一的项。把这三个卡点记住再回头看--dump-config的输出你会发现「为什么值是现在这样」这个问题大部分时候都能在三步内定位先看最终值再用--default-only做 diff 定位来源层最后去改那一层并重新验证。8. 加载顺序 ≠ 激活顺序行放在哪里不重要新手常有一个误解把新行插在cordis.patch.yml的开头还是结尾会影响它的行为。答案是不会。组合里行的先后顺序没有语义真正决定行为的是依赖关系。规则只有一条行声明了inject: [service...]它就会进入 waiting 状态直到对应的服务被提供出来。服务的出现驱动激活而不是行在文件里的位置驱动激活。这条规则解释了两件事。第一调整行的位置对激活结果没有任何影响你不需要为了「让它先加载」而把某行挪到最上面。第二调试激活问题时该看的是服务键不是行号。某个插件没启动先确认它inject的那些服务键有没有被别人提供如果提供者本身还在 waiting那问题要往上游追一层而不是在原地反复改配置。在补丁语境下这条规则还有一个衍生结论覆盖某一行时你改变的是那一行的 config不是它在组合里的激活时序。inject声明通常也应该在覆盖时一并重述——毕竟整行替换不重述就等于把原来的依赖声明也抹掉了。这是个很容易被漏掉的坑改完 config 发现插件不激活了多半就是inject没跟着写回去。9. 顺手把同一套凭据思路带到 Claude Code / Codex / CC SwitchDSH 的补丁层只是入口之一。同一个 TaoToken 账号同样的 Base URL在别的 AI 编程工具里也能直接复用。这里给出三套可以照抄的配置注意别把 Anthropic 的环境变量套到 Codex 上——那是两套完全不同的读取逻辑。Claude Code 走settings.json位置在~/.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY } }Claude Code 读的是ANTHROPIC_*系列变量ANTHROPIC_BASE_URL指向 TaoToken 的接口地址ANTHROPIC_AUTH_TOKEN填你的 Key。改完之后重开一个会话鉴权失败会立刻暴露不用等半天。完整字段说明和排障步骤可以参考 Claude Code 接入文档。Codex 走config.toml位置在~/.codex/config.tomlmodel_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY然后在 shell 里导出TAOTOKEN_API_KEY。注意这里的键名是base_url和env_key跟 Claude Code 的ANTHROPIC_*毫无关系。把 Claude Code 的环境变量原样搬到 Codex 的配置文件里是新手最典型的无效配置。如果你同时维护好几个工具、好几套供应商用 CC Switch 这类切换器会省事很多。每个档案要填的核心就是三样Base URLhttps://taotoken.net/api、API KeyYOUR_API_KEY、以及默认模型。切档案本质上是切这三样和 DSH 里换个补丁覆盖 llm 行是同一件事只是作用在工具层面而不是组合层面。10. 收尾产物清单与三步排查法回到开头那四个练习你手上应该留下两份可复用的东西一是/tmp/full.ymlweb profile 在某个时间点的完整组合快照后续任何「值为什么变了」的问题都可以跟它做 diff二是一份自己写的 profile 补丁至少包含 llm 行指向https://taotoken.net/api与YOUR_API_KEY的覆盖外加一条家目录级的统一设置。排查链路固定成三步--dump-config看最终值 → 用--default-only做 diff 定位来源层 → 改对应层并重新验证。这三步能覆盖绝大多数「配置改了不生效」的场景剩下的那部分基本都落在空补丁、删除行、以及忘记重述inject这三个坑里。最后补一句平面意识每次动手改一行配置之前先想清楚它属于宿主组合还是 agent preset。这两者的覆盖方式、生效路径和 HMR 行为都不一样把它们混在一起改是长期维护里最贵的错误。需要实际动手的时候按这个顺序走最省时间先在 模型对话 里确认目标模型可用再看 Coding Plan 选合适的档位接着去 创建 API Key 拿到YOUR_API_KEY把它填进cordis.patch.yml的 llm 行保存然后按第 6 节的两条路径验证 HMR 是否真的把新值推了上去。
返回列表