# DSH 手工补丁:OpenCode Go 会话头(x-opencode-session)注入 > 打补丁日期:2026-09-09 > 验证环境:`@deepseek-ai/dsh@0.1.2-rc.1`(`dsh-llm-pi-ai@0.1.2-rc.1`,`@earendil-works/pi-ai@0.84.4`),Windows,Node 24 > 上游讨论:https://github.com/deepseek-ai/deepseek-harness/discussions/5495 ## 1. 问题 OpenCode Go 网关(`opencode.ai`)自 2026-09-05 起要求每个推理请求携带稳定的 `x-opencode-session` 头(用于按会话路由/提示词缓存亲和),否则返回 `400 MissingSessionID`。DSH 的请求链路(`dsh-agent-loop` → `dsh-llm` → `dsh-llm-pi-ai`)在 `streamWithSnapshot` 中只把 `sessionId` 作为选项透传给 pi-ai,headers 仅合并静态 `profile.headers`;而 pi-ai 原生亲和头格式是 `session_id` / `x-client-request-id` / `x-session-affinity`(不含 `x-opencode-session`),且被 dsh-llm-pi-ai 的 drift 门控 withhold(compat 开关打不开)。因此必须在 dsh-llm-pi-ai 适配器层手工注入。 ## 2. 补丁内容(两处改动,都在 dsh-llm-pi-ai 的 `lib/index.js`) ### 改动 1:新增 helper 函数 在 `requestHeaders` 函数定义之前(其注释行 `/** Merge deployment headers while removing case-insensitive attribution collisions. */` 之前)插入: ```js /** Session affinity headers for OpenCode gateways (per-conversation id, stable across turns). */ function sessionHeaders(profile, sessionId) { if (!sessionId) return {}; let isOpenCode = typeof profile?.provider === "string" && profile.provider.toLowerCase().startsWith("opencode"); if (!isOpenCode && typeof profile?.baseURL === "string" && profile.baseURL.length > 0) { try { const host = new URL(profile.baseURL).hostname; isOpenCode = host === "opencode.ai" || host.endsWith(".opencode.ai"); } catch { } } return isOpenCode ? { "x-opencode-session": String(sessionId) } : {}; } ``` ### 改动 2:合并会话头到请求(仅一处调用点) `streamWithSnapshot` 内 `streamSimple(...)` 调用中的 headers 行: ```js // 原: headers: requestHeaders(profile.headers) // 改为: headers: requestHeaders({ ...profile.headers, ...sessionHeaders(profile, options.sessionId) }) ``` 语义要点: - 会话头在 `profile.headers` 之后展开 → 同名时运行时值胜过静态值。 - 值直接用 DSH 传入的会话 id(`session-` 形状已被网关验证接受),跨轮次/resume/compaction 稳定。 - 门控:provider 路由键以 `opencode` 开头(大小写不敏感)**或** baseURL 主机为 `opencode.ai`/其子域 → 覆盖 `opencode-go`、`opencode-go-muse` 等路由;模型发现/目录探测请求不受影响。 - 本机 0.1.2-rc.1 验证过 profile 上没有 `id` 字段,路由键是 `provider`(来自 `resolveProfiles` 的构造)。**其他版本重打前先确认该属性名**。 ## 3. 需要修改的文件(每台机器最多两份,内容应保持一致) 1. **安装闭包**:`\node_modules\@deepseek-ai\...\dsh-llm-pi-ai\lib\index.js` - 本机(scoop nodejs):`C:\Users\tricks1\scoop\persist\nodejs-lts\bin\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai\dsh-llm-pi-ai\lib\index.js` - 常规机器:`npm root -g` 结果下,可能在 `@deepseek-ai\dsh\node_modules\@deepseek-ai\dsh-llm-pi-ai\`(嵌套)或 `@deepseek-ai\dsh-llm-pi-ai\`(提升) 2. **运行时 profile 副本**(存在才改,运行时优先加载它):`%DSH_HOME%\profiles\node_modules\@deepseek-ai\dsh-llm-pi-ai\lib\index.js` - `%DSH_HOME%` 未设置时默认 `%USERPROFILE%\.dsh` - 全新机器若尚不存在,可先跑一次 dsh 让其生成,再补;或直接从安装闭包复制已补丁文件过去 定位方法:`Get-Command dsh` 找到 CLI 入口脚本,其引用的 `@deepseek-ai/dsh/lib/bin.js` 所在包即安装位置。 ## 4. 一键脚本(PowerShell 5.1+,幂等,可重复执行) ```powershell $ErrorActionPreference = "Stop" # --- 定位补丁目标 --- $targets = @() $npmRoot = (npm root -g).Trim() $candidates = @( (Join-Path $npmRoot "@deepseek-ai\dsh\node_modules\@deepseek-ai\dsh-llm-pi-ai\lib\index.js"), (Join-Path $npmRoot "@deepseek-ai\dsh-llm-pi-ai\lib\index.js"), (Join-Path $(if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $HOME ".dsh" }) "profiles\node_modules\@deepseek-ai\dsh-llm-pi-ai\lib\index.js") ) foreach ($t in $candidates) { if (Test-Path -LiteralPath $t) { $targets += $t } } if ($targets.Count -eq 0) { throw "未找到 dsh-llm-pi-ai/lib/index.js。用 Get-Command dsh 定位安装目录后把路径加入 `$candidates。" } $anchor = '/** Merge deployment headers while removing case-insensitive attribution collisions. */' $oldCall = 'headers: requestHeaders(profile.headers)' $newCall = 'headers: requestHeaders({ ...profile.headers, ...sessionHeaders(profile, options.sessionId) })' $helper = @' /** Session affinity headers for OpenCode gateways (per-conversation id, stable across turns). */ function sessionHeaders(profile, sessionId) { if (!sessionId) return {}; let isOpenCode = typeof profile?.provider === "string" && profile.provider.toLowerCase().startsWith("opencode"); if (!isOpenCode && typeof profile?.baseURL === "string" && profile.baseURL.length > 0) { try { const host = new URL(profile.baseURL).hostname; isOpenCode = host === "opencode.ai" || host.endsWith(".opencode.ai"); } catch { } } return isOpenCode ? { "x-opencode-session": String(sessionId) } : {}; } '@ $utf8NoBom = New-Object System.Text.UTF8Encoding($false) foreach ($f in $targets) { $text = [System.IO.File]::ReadAllText($f) if ($text.Contains("function sessionHeaders(")) { Write-Host "SKIP(已打过): $f"; continue } if (([regex]::Matches($text, [regex]::Escape($anchor))).Count -ne 1) { throw "锚点异常(不等于1): $f" } if (([regex]::Matches($text, [regex]::Escape($oldCall))).Count -ne 1) { throw "调用点异常(不等于1): $f — dsh 版本可能不同,先人工核对再改" } $backup = "$f.bak-$(Get-Date -Format yyyyMMdd-HHmmss)" Copy-Item -LiteralPath $f -Destination $backup $text = $text.Replace($anchor, $helper + "`r`n" + $anchor).Replace($oldCall, $newCall) [System.IO.File]::WriteAllText($f, $text, $utf8NoBom) node --check $f if ($LASTEXITCODE -ne 0) { throw "语法检查失败: $f(用 $backup 回滚)" } Write-Host "PATCHED: $f`n 备份: $backup" } Write-Host "`n完成。请完全退出并重启 dsh 后用 opencode-go 路由验证。" ``` ## 5. 验证清单 1. 脚本内 `node --check` 无输出即语法通过。 2. 结构确认(每份文件): ```powershell Select-String -Path -Pattern "sessionHeaders|x-opencode-session" # 预期 3 行命中:1647 定义 / 1657 返回 / 1808 调用(0.1.2-rc.1 参考行号) ``` 3. 两份文件 `Get-FileHash` 一致。 4. **完全重启 dsh**(正在运行的会话不会加载新代码)。 5. 用 opencode-go 路由跑一轮对话:patch 前该路由 `400 MissingSessionID`,patch 后应正常完成。 ## 6. 回滚 - 用脚本生成的 `.bak-时间戳` 文件覆盖回去;或 - `npm install -g @deepseek-ai/dsh@0.1.2-rc.1` 重装(会同时清掉补丁)。 ## 7. 注意事项 - **dsh 更新/重装会覆盖补丁**,更新后需重打(先核对第 2 节的属性名与锚点文本)。 - 官方修复推进中(维护者已确认方向,上游跟踪:https://github.com/earendil-works/pi/issues/9326 ,讨论中 Ziphyrien 引用)。官方发布后此补丁应撤除。 - 不想改代码的替代方案(按推荐排序): 1. 社区插件 `dsh-opencode-session`(npm):`dsh plugin --profile web add dsh-opencode-session` 后完全重启;按会话注入、零代码,官方修复后 `remove` 退役。 2. settings.yaml 静态头:给 provider 加 `headers: { x-opencode-session: <固定值> }`——能解 400,但所有会话共享同一 id,破坏后端亲和/提示词缓存,实测更慢更贵,仅应急。 ## 8. 本机(补丁来源机器)最终状态备查 - 两份 `lib/index.js` patch 后 SHA256:`573B3D2383A0EE26FEC4F753B6F0A3BCF9D2A784851C9985141E926A01AB3177` - 改前原始文件 SHA256:`69C387A2F1DE52D7798748737E3CF1B669E2B6A3D4820CDA384F1CF9BE9C23B0` - 行号:helper 1647–1658,改动 2 在 1808(补丁后) - 备份:`C:\Users\tricks1\.dsh\temp\dsh-llm-pi-ai-lib-index.js.bak-20260909` ## 9. 0.1.5-rc.2 重打记录(2026-09-11) - 升级到 `@deepseek-ai/dsh@0.1.5-rc.2`(`dsh-llm-pi-ai@0.1.5-rc.2`,pi-ai `^0.85.1`)后补丁被覆盖,按第 4 节脚本重打成功,锚点无漂移。改前核实:注释锚点 1722 行、调用点 1873 行(各恰 1 处);`options.sessionId` 在 1871 行透传可用;`resolveProfiles`(1051 行)构造的 profile 仍以 `provider` 为路由键(1103 行)。仍无原生 `x-opencode-session`。 - **布局变化**:新版 `%USERPROFILE%\.dsh\profiles\node_modules\@deepseek-ai\dsh-llm-pi-ai` 是指向安装闭包同名目录的 **Junction**(非独立副本)。第 3 节的"两份文件"实为同一物理文件,补一处即两处生效,脚本对第二路径按幂等 SKIP 属正常。未来升级后先用 `Get-Item -Force` 复查链接关系再决定补几处。 - 补丁后 SHA256:`9A07BBC9D6A6404F86722173A3D80F68353BD5D08C06057ACF754F21B0C79960` - 改前备份 SHA256:`1F787EB5CD3D0308E7A2563CDB2B8CC2C1FC153FE06B55D39439FB2446059483`(`lib\index.js.bak-20260911-140415`,两路径同源仅一份备份) - 补丁后行号:helper 定义 1723 / return 1733 / 调用点 1886;`node --check` 通过,UTF-8 无 BOM - 执行脚本留存:`temp\apply-opencode-session-patch-20260911.ps1`