DeepSeek Harness 插件

jinsiyu/dsh-code-server-app

Star 数 ★ 5 下载量(近 30 天) 14,238 分类 文档与渲染 收录于 2026-08-27 npm dsh-code-server-app

将code-server(VSCode网页版)打包安装到dsh内的插件,快速实现专业的文件编辑。

安装

# npm 包(预构建)

dsh plugin --profile web add dsh-code-server-app

# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)

dsh plugin --profile web add github:jinsiyu/dsh-code-server-app

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。

README

源码仓库地址见 package.json 的 repository / homepage 字段。

⚠️ 扩展市场说明(重要)

  • code-server 的扩展商店是 Open VSX,不是微软 Visual Studio Marketplace;
  • 微软 Marketplace 的条款禁止第三方产品(含 code-server)使用其 API,所以 code-server 无法查询微软市场的扩展列表;
  • 因此微软商业/专有扩展(如 GitHub Copilot、Remote-SSH 等 Remote 系列、Azure 系列、IntelliCode)在商店里找不到——这是微软发行策略,不是缺失;
  • 微软开源系扩展(Python、TypeScript 调试、ESLint 等)在 Open VSX 有镜像,搜索正常可装;
  • 需要微软专有扩展时:从 Marketplace 网页下载 .vsix,用 code-server --install-extension <文件>(或放入 --extensions-dir)手动安装,即可在插件列表使用。

静态 profile 插件(npm 包形态,host + client bundle),把 code-server 发行版里的 VS Code server 树 作为平台无关依赖随插件安装(打包期产物 vendor/vscode → @jinsiyu/dshcs-vscode-server, 无安装脚本、无 postinstall);code-server 的 Node 服务层已由插件自带的 lib/launcher.mjs 取代 (它直接驱动 <树>/lib/vscode/out/server-main.js 的 loadCodeWithNls() / createServer() / handleRequest() / handleUpgrade(),并补上 /healthz、/manifest.json、/_static/*、/proxy/:port 这几条 code-server 原本提供的 HTTP 面); 原生模块(node-pty / @vscode/sqlite3 / spdlog …)由 @jinsiyu/dshcs-* 子包按真名直接挂在插件依赖上、按 os/cpu 自动选中 —— 无需全局 npm 安装、无需配置 bin、无需改 profile 配置、无需第二条安装命令、无需 argon2/C++ 工具链。

打包形态:插件不随包分发 argon2 与 code-server 的 136 个运行时依赖 (express / proxy-agent / js-yaml / pem / limiter …);IDE 由插件自己的 launcher 拉起内置的 VS Code 树, 服务方式见下方「服务方式(serve)」。依据与实测证据见 docs/analysis-code-server-as-dsh-plugin.md。

「问 DSH」对话框:面板是 lib/client.js 里手写的 React 组件,渲染器直接 require DSH 页面模块表里的 react-dom/client 与 @deepseek-ai/dsh-client-ui-primitives(与界面同一份实例 ⇒ 排版、代码高亮、 公式都一致,而且不可能版本错配),授权也在同一个对话框里就地处理 —— 详见 「与 DSH 的协同:编辑器桥」。整条链上没有任何构建步骤。

UI 载体与 DSH 支持范围(0.2.3 起只支持带右侧栏的 DSH;2026-10-01 收敛为一代:≥ 0.2.0-rc.2)

判定一律是能力是否被声明,不做版本号比较。支持范围只有一代:DSH ≥ 0.2.0-rc.2 (web 与 desktop 同一代 —— 两套界面都已升到 0.2.0-rc.2)。此前为 rc 线 0.1.5-rc.x 与 alpha 线 0.1.6-alpha.2…0.1.7-rc.x 并存而写的兼容分支已全部删除:旧座位 settings.plugin.item、 旧数据通道 settingsScope、会话列表快照上的 current 兜底与 recentWorkspaceId 兜底都不再支持 (见「设置」与「旧版 DSH」两节):

DSH 版本 载体 入口
≥ 0.2.0-rc.2(web 与 desktop 同一代)—— 判据是有没有 sidebarRight / sidebarRightTabs 服务,不按版本号硬判 右侧栏标签(kind=code-server,标签名 Code Server),并认领文件地址(见下) ① DSH 官方的产物 chip / 「交付」卡片预览 / 正文里的文件名(0.2.5 起,走官方 openFile → 文件地址 → 本 tab);② 右侧栏「开始」页的 Code Server 入口框;③ 设置区(位置见「设置」)→ 「在右侧栏打开」
更早(无右侧栏服务) 不受支持:除设置区的一条提示外不提供任何入口 无(设置区显示升级提示)
  • 检测方式:先 ctx.get('sidebarRightTabs') / ctx.get('sidebarRight') 同步探测; 服务可能晚于本插件就绪,则 ctx.inject(['sidebarRightTabs','sidebarRight'], …) 等待, 2.5 s 内仍未就绪即判定旧版 DSH(不按版本号硬判,也不影响插件激活)。 0.2.4 起判定可逆、且注册不再依赖 ctx 属性访问(desktop 上曾因此静默不注册、表现为"设置卡正常但侧栏没有入口"):
    • 服务查找先读 ctx.<name>,再回退 ctx.get(name)——两种上下文形态都能注册;
    • 同步已能看到服务、但 inject 迟迟不回调时,1.5 s 后用同步服务兜底注册;
    • 2.5 s 只给设置页提示,10 s 仍无服务才通知 host 回收/停止预启动(避免误杀慢启动的宿主);
    • 服务晚到 → 自动撤销旧版判定、补注册侧栏,并上报 {sidebar:true} 让 host 恢复;
    • 注册失败不再静默:控制台报错,设置卡入口行显示"已探测到右侧栏服务,但标签注册失败"。
  • 0.2.3 起不再兼容旧版 DSH:悬浮球与内部浮动窗口回退已删除。判定为旧版时:
    • 只在设置区留一条升级提示(位置见「设置」),不注册悬浮球/浮窗/文件地址认领,也不预热 IDE;
    • 客户端向 host 上报 /api/code-server/ui-mode { sidebar:false }(在上面的 10 s 宽限之后),host 据此回收自动预启动的实例 并停止预启动(用户手动启动的实例不受影响);服务随后才出现时会再上报 {sidebar:true} 撤销;
    • 升级 DSH 后无需重装插件,刷新页面即可,本页会恢复为完整设置表单。
  • 侧栏标签内即 code-server 页面(iframe),跟随当前会话工作区;面板可折叠/分屏/浮动/全屏(由 DSH 右侧栏提供)。
  • 打开即全屏(0.2.9 起,默认开):打开 Code Server 标签(含点开产物 chip / 交付卡片 / 正文文件名)时, 自动把右侧栏从"与对话并排"切到全屏(铺满窗口)——IDE 在窄栏里太挤。 只影响"打开那一刻":随时点右侧栏的「退出全屏」不会被抢回去;切走再切回、再次打开文件 tab 会重新切全屏。 不想要就在设置卡片里关掉(fullscreenOnOpen=false)。
    • 实现说明:DSH 没有把"模式"开放给插件 —— ctx.sidebarRight 只有 isExpanded/toggleExpanded(展开/收起), push ⟷ fullscreen 记在 ui-sidebar-right 自己的 store 里(actions.setMode,只发给它的 seat 内部组件); ctx.layout.openRightbar(track, fullscreen) 也不是控制面,而是 seat 用来汇报 presentation 的通道 (上游源码注释:the occupant reports it; nothing else writes it)。 所以本插件做的是"用户那个动作本身":closest('[data-sidebar-right-panel]') 定位自己所在面板, 再点面板 chrome 上的 [data-sidebar-right-mode="fullscreen"] 按钮(与手点完全同一条路径, 含窄视口下的连带处理)。按钮找不到时保持原模式并 console.warn 一条,绝不影响面板渲染。
  • IDE 常驻(0.2.2 起,默认开):切到别的标签/收起侧栏再回来不再重载 code-server—— 未保存的编辑缓冲区、终端、调试会话都留在原处(见下方「为什么切标签不再重载」)。
  • 设置卡片只有四组设置:「认领类型」「打开即全屏」「FIM 补全(实验性,默认关;含停顿毫秒数 / 允许多行 / 按 glob 禁用三个子项)」与「后台常驻(切标签不重载)」——没有其它行(0.2.7 起移除「入口」「依赖安装」「环境检测」)。 打开 IDE 用右侧栏「开始」页的 Code Server 入口框,或直接点官方的产物 chip / 交付卡片 / 正文文件名; 诊断看 DSH host 日志里的 [code-server] 输出(/api/code-server/status 仍返回 env 供脚本排查)。 旧版的 windowedOpen(窗口化打开)、reserveComposer 已在 0.2.6 移除:旧设置文档里残留的这两个键不会报错,只是被忽略(不再出现在 schema 里)。 需要在新标签页用 IDE 时,从设置卡/空态提示里复制完整地址(含路径令牌,http://127.0.0.1:<port>/<token>/; serve: dsh 时为 DSH 的 /code-server/)—— 少了令牌那一段会 404。

文件打开(0.2.5 起走官方入口)

DSH 用资源地址命名文件,openFile 只负责把地址交给右侧栏去认领:

官方产物 chip / 「交付」卡片预览 / 正文内联提及
      → openFile(path, { line? })                     (ui-chat 提供)
      → dsh-resource://file/session/<sessionId>/<path> (或 …/file/absolute/<path>)
      → ctx.sidebarRight.openResource(address)
      → 由注册了匹配 patterns 的 tab 类型认领(优先级 extension(3) > builtin(2) > fallback(1),
        同带内按 pattern 长度、再按注册顺序)

本插件注册时带上:

字段 值 作用
patterns ['dsh-resource://file/**'] 认领文件地址(含 : 的 pattern 按整址 glob 匹配)
priority 'extension' 高于官方纯文本预览的 fallback——DSH 源码注释写明后者是"VS Code 文本编辑器在编辑器中的位次,任何更具体的类型都应当击败它"
canOpen 见下 按「认领类型」设置否决,未认领的地址由官方预览兜底
title 地址末段(=文件名) tab chip 显示文件名;页面 tab(sidebar://code-server)仍是 Code Server
  • 认领类型(设置卡片里的文本框,claimExtensions,0.2.11 起): 不再区分作用域 —— dsh-resource://file/session/… 与 …/file/absolute/… 一视同仁,只看扩展名。 文本框语法(分号分隔,,/空白/换行也认;写 py、.py、*.py 等价;大小写不敏感):
    • * = 其余类型也认领(兜底);
    • py = 认领 .py;
    • !md = 不认领 .md(排除优先于认领与 *);
    • 默认(0.3.51 起) 排除三组: ① DSH 预览渲染得好的四类 —— md markdown html htm png jpg jpeg gif webp bmp ico svg pdf; ② 可执行文件与二进制产物 —— exe com msi msix msixbundle appx appxbundle dll sys scr cpl ocx drv efi mui、 obj o a lib pdb class jar pyc pyo wasm node、so dylib ko elf bin out、apk ipa deb rpm dmg iso img cab; ③ Office 与版式文档 —— doc docx docm dot dotx dotm docb rtf odt、xls xlsx xlsm xlsb xlt xltx xltm xla xlam ods、 ppt pptx pptm pot potx potm pps ppsx ppam odp、vsd vsdx vssx vstx vsdm vssm vstm one onetoc2 mpt mpp pub msg xps oxps odg。 其余(代码、json/yaml、txt、日志、无扩展名如 Makefile、未知扩展名)都进 IDE;清空文本框 = 不认领任何文件(只保留页面 tab)。
      • 判据是"进编辑器有没有意义",不是"能不能被执行":文本形态的脚本(bat cmd ps1 sh py js…) 与 csv / tsv 仍然进 IDE —— 它们是可编辑的文本。
      • 想放开某一组:把文本框换回短白名单 *;!md;!markdown;!html;!htm;!png;!jpg;!jpeg;!gif;!webp;!bmp;!ico;!svg;!pdf 即可(Office 与可执行文件重新进 IDE,预览友好那几类仍留给 DSH)。
      • 三组清单是导出的常量(PREVIEW_FRIENDLY_EXTENSIONS / EXECUTABLE_EXTENSIONS / OFFICE_EXTENSIONS), 默认值就是它们的并集;设置卡里的「实际规则」摘要会点名"含可执行文件、Office 文档"。
      • 控件形态(0.3.52):认领类型是多行文本域(按内容自动长高:默认值 542 字符 → 9 行,上限 12 行,可手动纵向拉伸); 下面依次是语法提示、「实际规则」摘要、三组说明,以及折成多行的默认值代码块 —— 换行与分号在解析器里等价,那段连换行一起复制回输入框,得到的策略与默认值逐字相同(有单测钉住)。
    • 三种实际形态:纯白名单(py;ts,无 * → 其余不认领)、兜底(*)、兜底加排除(默认)。
    • 语法、默认值与解析都在 lib/claim-types.js(host 的 Config 默认值与客户端 canOpen 共用同一份, 随包发布,不会两边漂移);单测 scripts/test-claim-types.mjs。
  • tab body 怎么定位文件:从 useTabInfo().tab.navigation.address 解析出会话与路径 (lib/client.js 的"地址语法"段,与 DSH parseFileAddress 同语义),相对路径按该会话 cwd 展开成绝对路径, 再把绝对路径 + 可选 line 交给 host 的 /api/code-server/open-file;内建扩展 (dshcs-open-file)在 workbench 里 showTextDocument(带行号时定位到该行)。
  • 只留一个 tab(0.3.57 起):官方语义本是"一个地址 = 一个 tab"(contentId 就是地址:同址幂等、 异址必新开),而 replaceTab 只有发起方(产品自己的 openFile/openResource)能传 —— 所以插件改成:新 tab 的 body 挂载时把同窗格里旧的 code-server tab 关掉。 于是连点两个文件时,你看到的是同一个 tab 在换内容(chip 标题跟着变),而不是越开越多。 两条刻意留的边界:① 只收同窗格的 —— 多窗格是用户主动切分的布局,官方自己也是"每窗格一份" (SidebarRightTabDefinition.multiple 的注释:"one page per kind in each pane"); ② 只有"首次可见"的那个新 tab 负责收 —— 标签页恢复/激活顺序不可控,若每个可见的都收别人, 关掉一个会让下一个变可见,互相收成乒乓(用 ref 钉住"每次挂载只收一次")。 这些 tab 本来就共用同一个常驻 workbench(IDE 是单实例),关掉一个不会重载它: 常驻 iframe 由 lib/client.js 的"常驻 IDE 面"段持有,tab 只是它的停靠宿主(Element.moveBefore)。 回归:scripts/test-client-bundle-tabs.mjs —— 对入口渲染两个 tab,断言旧的被关、新的还在、 跨窗格不动、不可见时不动、缺 actions 的老 DSH 也不崩(负向对照:摘掉合并调用 → 该用例 FAIL)。
  • 为什么还留着那个内建扩展:VS Code Web 没有"从外部打开文件"的官方 API(唯一入口是 ?folder= 指定工作区),所以"让 workbench 定位到某个文件"只能由树内的扩展完成; host 写信号文件、扩展轮询并 showTextDocument,失败保留重试(实例尚未就绪时也不会丢)。

为什么切标签不再重载(IDE 常驻)

过去的坑:DSH 的右侧栏(ui-dockkit)TabPanel 只渲染当前激活标签的 body (TabPanel.tsx:412 → renderTab(active))——切到别的标签 = React 卸载该 body = iframe 被移出文档 = 浏览上下文销毁,切回来就是一次完整的 VS Code 重载(未保存的缓冲区丢失)。把标签浮动成独立面板只是绕开它, 并没有解决。

现在的做法(客户端 lib/client.js 的常驻面段,0.2.2):插件把 iframe 从 React 手里接管,做成单例常驻面:

场景 动作 结果
标签激活 host.moveBefore(frame, null) 移进当前可见的停靠位 状态保持型原子移动,不重载
标签失活 / 收起侧栏 移回文档级 park 容器(离屏、保留最后停靠尺寸、inert + aria-hidden) 面不销毁,后台继续跑
工作区 / 端口变化 显式设置 src 这是唯一正常的"重载"入口
  • 为什么是 moveBefore:浏览器实测(Edge/Chromium 151)普通 appendChild 移动 iframe 会让内部计时器归零 (等价重载),而 Element.moveBefore()(Chromium ≥133)保持状态(计时器 1→2 连续)。
  • 降级不静默:moveBefore 缺失、或宿主已被 React 摘除而抛 HierarchyRequestError: invalid hierarchy (passive effect cleanup 晚于 DOM 卸载)时,退回 appendChild——会重载一次,但绝不丢帧; 状态里 degraded/lastMoveError 明示,界面据此提示"常驻不可用"。
  • 重绘兜底(实测坑):真实 GUI 里观测到一次"元素在、画面不重绘"——iframe 尺寸、命中测试、visibility 全部正常,面板却一片白(连续两张截图哈希相同,确认没有新帧);translateZ(0)、opacity 微调无效, display:none → 强制重排 → 还原(同一个 JS 任务内)可恢复,且 iframe 文档不重载、内部状态不变、无可见闪烁。 触发条件未能复现:探针页里 moveBefore 停放 337 s(超过 Chrome 对不可见跨源 iframe 的 ~5 min 节流窗口) 后移回、且关掉修复,仍正常绘制。因此把它当兜底保留:每次「停放 → 停靠」补一次 nudgeRepaint()(surfaceSnapshot().nudgeCount 计数,setNudgeEnabled(false) 可现场 A/B)。
  • 后台预热:配置 keepResident(默认 true)时,宿主在插件启动后就把面建好并停在停放区, 首次点开标签无需冷启动等待;preload 不会把正在使用的面拽走。
  • 排障句柄:控制台可用 window.__dshcsSurface(snapshot() / setParkStrategy('offscreen'|'behind') / dock() / park() / nudge() / setNudgeEnabled(false) / destroy())。

实测记录(DSH web GUI,sidebar 标签间真实鼠标切换):切走 → docked:false、iframe 仍为同一节点、内部探针存活、 degraded:false;切回 → docked:true、src 不变、IDE 画面与编辑状态保持(无整页重载)。 完整证据与探针脚本见 docs/analysis-code-server-as-dsh-plugin.md。

服务方式(serve)

方式 说明 需要
loopback(默认) 插件自己起一个回环端口(默认 port: 0 = 每次启动由系统分配随机端口),右侧栏 iframe 跨源直连;URL 带随机路径令牌(http://127.0.0.1:<port>/<token>/,见下「回环端口的安全模型」);进程可被 adopt(DSH host 重启后接管) 无
dsh IDE 挂到 DSH 自己的 HTTP 端口上的 /code-server/*(HTTP prefix 路由)+ /code-server/<quality>-<commit>(WS 精确路由),转发到 launcher 的命名管道;没有额外端口;每条请求(含 WS 握手)先过 ctx.connection.requestRejection() —— 与 /api 同一套 Host/Origin fence + 浏览器 cookie 认证 DSH 提供 webServer 服务(web profile);desktop 无此服务 → 自动回退 loopback

回环端口的安全模型(0.2.14 起)

loopback 是 desktop 端唯一的通路(无 webServer、无同源挂载),所以它单独加固了一层:

  • 随机端口:port 默认 0 → 由系统分配空闲端口,launcher 把实际端口写进 $DSH_HOME/code-server/endpoint.json, host 读回(因此端口每次都变、也不存在"8090 被占用"这类冲突)。要固定地址就显式配 port。
  • 路径令牌:每次新启动生成 32 位随机令牌([0-9A-Za-z_-],24 字节随机),写在 $DSH_HOME/code-server/path-token(用户 profile 下,默认 ACL 仅本人可读),成为 URL 的路径前缀。 没有这个前缀的请求一律 404(不泄露"这里跑着 IDE"),前缀不带结尾斜杠会 302 补上。
  • 为什么不用 VS Code 自带的 connection-token:它靠 ?tkn= → 302 + Set-Cookie: vscode-tkn; SameSite=Lax; 而 desktop 的 iframe 是跨源的(dsh-app:// → 127.0.0.1),Lax cookie 在跨站子框架里不会被带上 → 会让 IDE 直接打不开。路径前缀不需要 cookie:workbench 的资源与 WS 全部由 location.pathname 派生 (serve: dsh 挂在 /code-server/ 下已验证同一机制),前缀天然跟随每个子请求与 WS 握手。 (已用真实 Edge + CDP 在跨源 iframe 里验证:随机端口 + 令牌下 workbench 正常渲染并建立 WS。)
  • Host 白名单:回环模式只接受 127.0.0.1 | localhost | [::1] : <实际端口>。挡的是 DNS rebinding —— 这类攻击构造的请求可以不带 Origin,只靠 Origin == Host 那条检查拦不住。
  • Referrer-Policy: no-referrer:令牌在路径里,不能让它在加载站外资源时经 Referer 漏出去。
  • 令牌不落 argv、不进日志:命令行对本机任意进程可见,所以走文件传递;日志里只打印"已启用"。

边界(说清楚,不夸大):这一层挡的是"本机其它应用/端口扫描器/浏览器页面"顺手访问你的 IDE; 同用户的本地恶意程序本来就能直接读你的文件、也能读那个令牌文件 —— 那不在本插件的威胁模型内。

  • 在 cordis.patch.yml 的 config.serve(或设置文档里的 code-server.serve)切换,下次启动生效 —— 设置卡片不提供这一行(卡片只有认领类型/打开即全屏/后台常驻/FIM 补全四个设置)。

  • dsh 模式的实际收益:单一 URL/单一端口(远程访问 DSH 即可用 IDE)、不再暴露额外回环端口、认证与 DSH 同级。

  • dsh 模式的两点已知取舍:

    1. iframe 与 DSH 同源 → 该模式下不再挂 sandbox(同源 + allow-same-origin 可被 frame 自行摘除,属"看起来有防护"); loopback 模式跨源,sandbox 保持原样作为真防护。剪贴板仍由 allow="clipboard-read; clipboard-write" 提供。
    2. 转发端口(Ports 面板)的 WebSocket 无法用精确升级路由覆盖(端口号在路径里)→ 该功能在 dsh 模式下不可用; HTTP 转发端口正常;需要端口转发 WS 时请用 loopback 模式。
  • loopback 模式下 upgrade 会做 code-server 同款 Origin 校验(0.2.1 起):带 Origin 时其 host 必须等于 Host (含 Forwarded: host= / X-Forwarded-Host 的反代语义),否则回 403;缺 Origin 的非浏览器请求放行。 没有这道检查时,本机任意浏览器页面都能对 ws://127.0.0.1:<port>/stable-<commit> 完成握手并驱动 IDE。

与 DSH 的协同:编辑器桥(0.3.0 起,默认开)

"IDE 就在旁边"和"agent 真的知道编辑器里发生了什么"是两件事。编辑器桥补的是后一半:只读地把 只有编辑器才知道的信息交给 agent,并让用户在编辑器里的动作能反过来驱动当前会话。

双向能力

方向 能力 落地方式
编辑器 → agent 未保存缓冲区(磁盘内容 ≠ 用户所见)、活动文件与选区、语言服务器诊断(含 file:line、来源、code) agent 工具 editor_context / editor_diagnostics;写脏文件前额外附一条提醒
编辑器 → DSH 选中代码 → 右键「DSH: 针对选中内容提问」→ DSH 页面右下角弹出提问对话框(标题栏带 文件:行);提问以用户输入进当前会话,该会话的新内容用 DSH 官方 markdown 渲染器显示在对话框里 命令 dsh-code-server.askAboutSelection(编辑器右键菜单最上面两条之一)→ 桥 POST /event {kind:'ask-open'} → 客户端半部 POST /api/code-server/ask/send
DSH → 编辑器(授权) agent 要写工作区外的文件 / 执行命令时的授权请求 → 对话框里就地弹卡片(工具名 + 原因 + 倒计时),点「允许一次 / 拒绝」立刻生效 /api/code-server/ask/state 的 approvals + POST /api/code-server/ask/approve(全插件唯一的写口令,约束见「安全模型」)
agent → 编辑器 agent 改了哪个文件 → 开原生 diff 审阅(左 = 写前的完整原文,右 = 磁盘现状);缓冲区有未保存改动时告警而不覆盖 host 在 tools/post-execute 取 result.value.before(完整写前全文)存入有界快照缓存 → tools/result 的事件带不透明 key → 扩展轮询后取回原文并开 diff + 非模态告警
  • 工具只在桥就绪时注册(IDE 没起来时模型看不到"有个用不了的工具");提示词段落也只在桥存活时渲染。
  • 提问对话框:右键命令只向宿主上报意图,真正的对话框由 DSH 页面里的插件客户端弹出 —— 可拖动、可缩放(右下角,✕ 关闭),不占编辑器版面;对话框开着的同时还能改选区再问。 宿主证明不了对话框活着(页面没开 / 浏览器还缓存着旧客户端)时只给一条 「请在 DSH 页面里打开(或刷新)Code Server 标签」的提示 —— 扩展里没有第二套提问 UI。
  • 两个命令的意图分开记:「针对选中内容提问」只有真的选了内容才带行区间 + 选区正文; 「针对当前文件提问」永远不带行号、不带选区 —— 光标停在哪一行跟问题无关,行号只会误导 agent; 没选区时用选中命令提问也会退化成纯文件。上下文由宿主从它缓存的编辑器状态里取,扩展只上报意图。
  • 追问跟着 DSH 自己的设置投递:DSH 的 ui-conversation.busyEnter(设置 → 对话:「忙碌时按 Enter」) 取值只有 queue(默认)与 steer。面板里按 Enter 与主界面里按 Enter 是同一个手势,所以读同一个值: steer ⇒ 宿主用 agent.steer(),追问在当前轮的下一个步骤边界被读到(当轮就能回应); queue ⇒ 宿主用 agent.followup(),排到下一轮、不打扰当前轮。读不到这个设置(命名空间未注册 / 极简组合)· 老宿主上没有 agent.steer ⇒ 一律退回 queue,绝不因为设置而投不出去。 面板状态行会说清用的是哪种(「已插入当前轮…」/「已排入下一轮…」)—— 因为 queue 期间 DSH 主界面看不到这条消息:它进的是宿主侧待发队列(next-turn),而主界面客户端不渲染待发队列 (只有它成为自己那一轮时才进聊天流)。这不是消息丢了。
  • 注入的上下文是折叠的:桥拼进消息的位置行 + 选区代码块会被拆出来,显示成一行默认收起的 「上下文」(点开才看得到那段代码),气泡里只留你的原话 —— 与 DSH 界面处理注入上下文的方式一致。
  • 正文就是 DSH 的渲染结果:正文交给 DSH 官方的 markdown 渲染器 (@deepseek-ai/dsh-client-ui-primitives 的 MarkdownText)—— 同一套 micromark/mdast 管线、 同一个增量流式解析器、同一个 shiki 高亮(走 DSH 自己的懒加载语法集)、KaTeX 公式、 同样的标题与表格排版。只渲染新内容(从对话框订阅那一刻起),不重放历史、没有"加载更早"。 官方部件取不到时降级成纯文本 <pre>,不白屏。
  • 思考过程也照官方显示:助手的 reasoning 以「思考」行出现 —— 默认收起、收起时显示首行 (流式时显示最新一行)、点整行展开全文,用的就是官方 DisclosureRow + 官方的思考图标与排版语言。
  • 授权就在对话框里处理:对话框打开着的时候,该会话的授权请求先问对话框(5 分钟窗口), 点「允许一次」/「拒绝」立刻生效;关掉对话框或等满窗口就把请求原样交回官方链路(DSH 界面照旧弹卡)。 永不自动放行 —— allowed-once 只能来自你的一次点击,对话框里没有"以后都允许"这种入口。
  • 提问进 DSH 会话时是普通用户消息(source: { kind: 'user' }):来源信息靠正文首行 From the editor: <file>[:<行>] 保留,面板把它折成「上下文」行。
  • 桥完全只读:不写文件、不改文档、不执行命令 —— 四条路由(/health、/sync、/old、/event)都是读的。 唯一能改状态的是 DSH 同源的 POST /api/code-server/ask/approve,它只能回答已经存在的授权请求 (见「安全模型」第 2 条)。agent 的写操作仍然全部走它自己的 fs 工具,桥只是"知道它写了什么"、 并把你对授权的答复带回去。
  • 编辑器侧的入口还有状态栏的 $(plug) DSH(连通时显示,点击打开日志),日志在输出面板 「DSH Editor Bridge」里 —— 出问题时先看它。
  • 扩展装在内置目录:dshcs-editor-bridge 与 dshcs-open-file 一样装进 <树>/lib/vscode/extensions/ —— 用户级目录里那个会被 VS Code 服务端标进 .obsolete(日志 Marked extension as removed)并永久跳过。 要关掉桥请用插件设置 editorBridge=false(不挂桥、不注册工具),不要再指望在扩展视图里卸载它。

三条通道(0.3.13 起走本机 IPC:Windows 命名管道 / unix socket)

扩展 → host     POST /code-server-bridge/sync    一趟来回:上报编辑器状态 + 取回待处理事件与能力位
扩展 → host     GET  /code-server-bridge/health  无鉴权探活(便于重启后一眼确认)
扩展 → host     GET  /code-server-bridge/old     取一份"写前原文"快照(事件里只带不透明 key)
扩展 → host     POST /code-server-bridge/event   上报意图:请宿主打开对话框 / 打开·关闭文件等(进 host 日志尾)
host  → 扩展    <extensionsDir>/.dshcs-bridge/bridge.json  端点 + 令牌(扩展每 5s 重读)
                (同一份内容还会写到**内置扩展旁边** `<树>/lib/vscode/extensions/.dshcs-bridge/` ——
                 环境变量只在 host spawn IDE 时注入,而被**接管**的 IDE 是上一次启动的进程、拿不到它,
                 扩展得能只靠自身位置读到配置)

请求走 http.request({ socketPath })(fetch 不支持 socket),不开任何端口。 这四条全部只读;提问与授权答复不走桥,而在 DSH 同源的 /api/code-server/ask/* 上 (调用方是 DSH 页面里的插件客户端,吃 DSH 自己的 cookie/Origin 校验)。

对话框真正用到的四段状态(GET /api/code-server/ask/state?rev=N;没变化只回一个数字):

字段 内容 面板怎么用
entries 被对话框 watch 的会话的新内容条目(user / assistant / tool / approval),有界:每会话 ≤120 条、单条正文 ≤8000 字符、同时 watch ≤4 个会话 助手正文交给官方渲染器;工具与授权是紧凑摘要行
approvals 待决授权请求 [{id, toolName, reason, at}](≤4 条) 弹卡片 + 倒计时;点按后 POST /ask/approve
approvalHoldMs 授权窗口长度(默认 300000ms = 5 分钟) 倒计时基准
contextText / mode 标题栏那一行(来自宿主缓存的编辑器状态)+ 提问意图 标题栏文案;mode 决定发送时带不带行号/选区

为什么不是 HTTP(0.3.13 定论,三条都实测过)

  1. desktop 根本没有 HTTP 面:渲染进程经 Electron IPC 调 host.fetch() (apps/desktop-host/src/index.ts:308 的 createSharedFetchHandler('/api'))—— 那是进程内函数调用, 进程外不可达;插件能挂 HTTP 的只有 web profile 的 webServer。
  2. /api 也不行:Connection 给 /api 装了 Host/Origin/cookie fence (packages/client/connection/src/index.ts 里 requestRejection → 无 cookie 即 401),而桥的客户端 是扩展宿主里的 Node 进程 —— 它永远拿不到浏览器 cookie。实测(0.3.7):扩展按 /api/code-server/bridge/sync 轮询,要么 405(打到 launcher/VS Code)、要么 401(打到 /api fence), 桥从来没有真正同步过。
  3. 桥的两端本来就是同一台机器上的两个进程(扩展宿主 ← 插件 spawn 的 IDE ← 插件)。 本机 IPC 比开端口更小:没有网络面、没有 Host/Origin 混淆代理问题,web 与 desktop 走同一条路。 令牌校验照旧保留(见下),Windows 管道名带随机后缀、POSIX socket 文件 chmod 0600。

历史:0.3.9–0.3.12 挂在 DSH 的 webServer 前缀下 —— 于是 desktop 永远休眠(没有 webServer)。

为什么状态是"推"而不是"拉":扩展宿主是 VS Code server 的一个子进程,不监听任何端口 —— host 反向请求不到它。所以编辑器状态只能在扩展主动发起的那趟轮询里带上来,host 缓存后给工具读 (缓存滞后最多一个轮询周期 600ms,超过 10s 没更新就判为过期,工具会明说"状态已过期");

为什么不用 SSE/WebSocket:扩展宿主里没有 HTTP 服务器,而桥的形态是"每 600ms 一趟请求/响应"。 轮询给了两条好性质:幂等(丢一次事件只是少一次提示,数据本身永远在编辑器里),以及状态天然最新(每趟都刷新)。

事件的送达契约(0.3.56 修正):agent-edit 这类提示走环形缓冲,靠 since=<游标> 取"比我新的那些"; seq 在一次桥端点(宿主进程)生命周期内单调递增,客户端游标只增不减,端点换了(管道名里带宿主 pid) 才用 since=0 重新对齐。0.3.9–0.3.55 的宿主在每趟 /sync 后都调用 reset(),而它当时会把 seq 归零 ⇒ 扩展第一次收到 seq=1 后,新事件又从 1 开始编号,seq > since 永远不成立 ⇒ 每个 IDE 会话最多只送达 第一条事件(用户看到的现象就是"偶尔才有一条 diff,而那条还是空 old")。现在:reset() 只清缓冲、不回退 seq,并且 /sync 回应里带 lastSeq(高水位),扩展据此自查游标是否超前(超前就退回 0 重新对齐,自愈)。 回归:scripts/test-bridge-routes.mjs 里有一条按真实调用顺序复刻"推送→取→清空"两次的用例。 注意这仍是提示通道,不是可靠队列:缓冲 64 条,超出丢最旧,漏一次只是少一次提示。

安全模型(五条不变量,改 lib/bridge.mjs 之前先读)

桥的令牌写在 <extensionsDir>/.dshcs-bridge/bridge.json(对本机同用户进程可读),所以:

  1. /code-server-bridge/* 只读,只有 /approve 一个例外。 没有写文件、改文档、执行命令、 拉起进程的路由。令牌泄露的爆炸半径被封在"看到编辑器里的信息",不会变成任意文件写/任意命令执行。 scripts/test-bridge-routes.mjs 里有一条白名单断言盯着这件事(未知后缀一律 404)。 /old(0.3.55 新增)也在这条不变量里:它只能按不透明 key 读"最近几次 agent 写操作的写前副本" 这一份有界缓存(条数 ≤8 / 单份 ≤1MB / 总量 ≤4MB / 5 分钟过期),取不到就 404; 它不接受路径参数,所以读不到任意文件,也不消费(重复轮询拿到同一份)。
  2. /approve 的四条约束(缺一条就等于开了任意命令执行的后门,不许放宽): (a) 只能回答已经存在的授权请求,请求体只有 {id, outcome},不接受任何自由文本 / 路径 / 命令参数 —— 它只能"回答问题",不能"发起动作";(b) id 必须是本进程自己发起、且仍未决的请求(用后即废); (c) outcome 只接受 allowed-once / rejected,没有"永久允许"; (d) 没有面板在看 / 面板关掉 / 窗口超时(默认 5 分钟)→ 交回官方链路,绝不自动放行 (DSH 的 approval/request 本身 fail closed,这里只能把"没人答"保持成"没人答")。 pnpm test:ask-dialog 里有针对这四条与宿主侧白名单的断言。
  3. 带 Origin 的请求一律 403。 浏览器发起必带 Origin(含沙箱 iframe 的 Origin: null), 扩展宿主是 Node 进程、不带。判定顺序上 Origin 先于令牌 —— 否则等于给浏览器一个 "令牌猜对没有"的 oracle。 实现细节:Node 路由 → Fetch 适配器把原始 headers 挂在 request 上(dshcsRawHeaders), 因为 undici 的 Request 构造器会把 origin 当 forbidden header 归一化掉 —— 读 request.headers 会让这道 403 静默失效(测试里有这条实测记录)。
  4. 路径收敛在编辑器当前工作区(workspaceFolder 之外的诊断直接丢弃)。
  5. 有界:诊断默认 200 条 / 单条截断 500 字符 / 上报体上限 256KB / 事件环形缓冲 64 条 / 对话流每会话 ≤120 条(单条正文 ≤8000 字符、同时 watch ≤4 个会话)/ 待决授权 ≤4 条 / 写前原文快照 ≤8 份、单份 ≤1MB、总量 ≤4MB、5 分钟过期(见 /old)。

这一层挡的是"本机其它应用或浏览器页面拿到那个文件后乱调桥";同用户的本地恶意程序 本来就能直接读你的文件与令牌文件 —— 那不在本插件的威胁模型内(与「回环端口的安全模型」同一句话)。

开关与诊断

怎么关 效果
cordis.patch.yml 的 config.editorBridge: false 下次启动不写 bridge.json、不注册工具
设置文档里的 code-server.editorBridge: false 即时生效:删配置 + 注销工具,扩展随即休眠
在 IDE 里禁用扩展 dshcs-editor-bridge 桥自然不可用(工具会注册但立刻报"状态未上报";IDE 侧无任何动作)

诊断:GET /api/code-server/status 的 bridge 字段返回 { enabled, live, toolsRegistered, supported, url, file } —— 不含令牌(令牌只在那个文件里)。

FIM 补全(实验性,0.3.61 起,默认关)

在编辑器里打字停顿时,光标后出现灰色续写(Tab 接受、Esc 丢弃)。默认关闭,在设置卡片里那一行开启,开启后即时生效(不用重启宿主、也不用重装插件)。

项 值
端点 DeepSeek FIM(Beta):POST https://api.deepseek.com/beta/completions,参数 prompt(前缀)+ suffix(后缀)
模型 deepseek-flash(官方 FIM 文档的示例模型,也就是本部署的默认模型)
凭据 复用 DSH 里已有的 DEEPSEEK_API_KEY:ctx.get('credentials').resolve(...),与官方适配器同一条路(取不到再退回启动环境变量)
实测延迟 112–416 ms(非流式;流式反而更慢 ⇒ 故意用非流式)
触发条件 停顿 ≥250ms 且通过门控:有选区 / 非 file 文档 / 空上下文 / 文档 >2 万行 —— 这几种根本不发请求

它怎么接进来的(方案 A):FIM 走的是 Completions API,而 ctx.llm.stream(GenerateOptions) 的词汇表里 只有 messages(没有 prompt/suffix,purpose 还是封闭联合 'compaction' | 'session-title')⇒ 插件 自己注册一个 LLM 适配器路由 dshcs-fim:把前缀/后缀装进一条带固定前缀(dshcs-fim/1 )的 messages 信封, 适配器解出信封再打那个端点。这样这条调用仍然走 DSH 的 LLM 服务 —— 取消、超时、终态 chunk、稳定错误码 都按服务契约走(而不是插件自己直连绕过服务层)。依据与实测数据见 docs/analysis-continuedev-reuse.md 的 B6/B7。

三个可调项(0.3.62,都在设置卡里,即时生效):

设置 默认 作用
停顿毫秒数 250 打字停下多久才请求一次。范围 100–3000ms(保存时夹取);实测端点往返 112–416ms,所以停顿基本就是"感知到的延迟"
允许多行补全 开 关掉后宿主只回第一行;首行是空白 ⇒ 这次不补。想更克制(少被打扰)就关掉它
按 glob 禁用 空 这些文件里根本不发请求。语义:* 不跨目录、** 跨目录、不含 / 的模式只匹配文件名、含 / 的模式按路径尾段匹配(所以 vendor/**、src/*.ts 在任何层级都生效)、/ 结尾视作 /**。例:*.md;vendor/**;**/dist/**

这三项的语义在宿主与扩展各有一份实现(扩展是随包分发的静态文件,不能 import 宿主代码), 一致性由 scripts/test-fim.mjs 用同一组(模式,路径)语料对两侧做等价断言钉住。

安全与边界(这是本插件唯一会把内容发出去的能力):

  • 只读不变:扩展仍然不写文件、不执行命令 —— 它只"提议"一段文本,插入由你按 Tab 完成;
  • 只有开启后桥才有第五条路由 POST /complete(唯一一条会发起模型调用的);没开一律 403,不发任何请求;
  • 有界:前后缀各 ≤6000/2000 字符(120/40 行,字符与行数取先到者)、输出 ≤2000 字符、单次 4s 超时、 最小间隔 120ms、并发 1、每分钟 60 次;超限的请求被拒(409/429),扩展这一轮就当"这次没有补全";
  • 扩展侧还有一层:关掉开关后立刻注销 provider(连请求都不发),相同前后文 2 分钟内直接命中本地缓存;

用量只在两处可见:这条调用不是会话请求,不写会话日志 ⇒ DSH 自己的 token 计量(逐轮用量、 上下文压力、遥测)都不含它。所以用量显示在:① IDE 状态栏的 DSH 项 —— 开启后多一格 $(zap) 1.2k, 悬停看输入 / 输出 / 缓存命中 / 最近耗时 / 最近错误;② GET /api/code-server/status 的 fim 字段(同一份快照)。

已知限制:模型偶尔会在"并不需要补"的位置硬凑一段(实测在不该补的位置 2/2 复现);过滤管线会剥掉代码围栏与 控制标记,但"该不该补"最终由你判断 —— Esc 或继续打字都会让它消失。更稳的形态要等更快的完成路由, 或者 DSH 把 completion 做成一等请求(那时把适配器里的取数换成新 API 即可,调用方一行不用改)。

旧版 DSH(0.2.3 起不再支持)

行为:探测不到 sidebarRightTabs / sidebarRight 时,插件只注册一条升级提示(位置同「设置」一节):

Code Server — 当前 DSH 版本不受支持(缺少右侧栏服务) 本插件自 0.2.3 起不再兼容旧版 DSH。 未检测到右侧栏插件服务 sidebarRightTabs / sidebarRight,因此插件不提供任何入口(旧版的悬浮球与浮动窗口已移除), 也不会后台启动 IDE。升级 DSH 到 0.2.0-rc.2 及以上后,Code Server 会出现在右侧栏标签里, 本页同时显示完整设置项;升级后无需重装本插件,刷新页面即可。

  • 提示挂在哪:它是唯一的设置座位(plugins.bundle.config,经 configForms 通道)上的一块只读内容; 旧代 DSH 上座位与通道都不存在,所以在那类部署里实际只剩控制台的一条 [code-server] 未探测到右侧栏服务… 警告。行为不变:不启动 IDE、不给任何入口。
  • 没有任何其他 UI:不注册 shell.overlay(悬浮球)、不认领文件地址、不做常驻预热。
  • host 侧:客户端会 POST /api/code-server/ui-mode { sidebar:false };host 收到后 ① 不再自动预启动 IDE(maybePrestart 直接返回),② 若 IDE 是本插件刚自动预启动且尚未被 adopt,则回收该进程, 避免留下一个用不上的 IDE 与端口。用户手动启动的实例(adopted)不会被停。
  • 为什么删除而不是保留:内部浮动窗口是 2026 年早期 DSH(无右侧栏服务)时代的临时载体, 常驻面、剪贴板、快捷键、面板折叠等能力都建立在 DSH 右侧栏之上;维护两套载体的成本高于其残余价值。 旧代兼容分支(rc 线 / alpha 线的座位、通道与 current 兜底)也在 2026-10-01 一并删除 —— 还需要旧代 DSH(≤ 0.1.7-rc.x)的用户请钉 dsh-code-server-app@0.3.69 (dsh plugin --profile web add dsh-code-server-app@0.3.69),那是最后一个还带旧代兼容分支的版本。
  • 回滚:需要旧代 DSH 时降到 0.3.69 即可(dsh plugin --profile web add dsh-code-server-app@0.3.69); 在 0.2 这一代 DSH 上不需要任何回滚。

code-server 服务目录与进程生命周期

  • code-server 服务目录跟随活动工作区/会话:打开期间切换 DSH 会话/工作区,code-server 自动切到新目录 (解析优先级:当前会话 cwd → 会话所属 workspace.path → 最近活跃会话所属 workspace → 首个 workspace.path; "当前会话"只有一个信源:会话作用域标准 prop sessionId(0.2 这一代;旧代 rc 线读的会话列表快照 current 兜底已随旧代分支删除)—— 详见下面 0.3.48 那条;纯逻辑内联在 lib/client.js 的"工作区解析"段,契约由 scripts/test-client-bundle-cwd.mjs 直接对入口钉住); 打开目录显示在 code-server 页面内(?folder=<cwd>,跟随切换时页面自动重新加载); 实现要点:iframe src 必须带 ?folder=<cwd>——code-server 前端会记住“最近工作区”并自行恢复, 仅用裸根 URL 只会显示上一次打开的目录、不会跟随切换(本机实测确认)。 Windows 路径格式(实测):folder 参数必须以 / 开头且全部正斜杠,形如 /C:/Users/User/Desktop/biss; 裸 Windows 路径(C:\...)会被前端当 URI scheme 而剥掉盘符(页面显示 \Users\User\... 且文件树为空), file:///C:/... 形式则报 “Workspace does not exist”。
  • 0.3.48 修的是"打开 Code Server 不打开对应工作区":DSH 0.1.6-alpha.2 把 current 从 SessionListState 移出了会话列表 store(refactor 原话:view selection remains outside the Controller),而 0.3.46 及更早版本 正是从 useSessions(s => s).current 里取"当前会话" ⇒ cwd 恒为 undefined ⇒ 客户端不再向宿主发 cwd ⇒ IDE 以空工作区启动(本机实测:$DSH_HOME/code-server/pid.json 里 cwd/launchCwd 双空, UI 上看不出任何报错)。0.3.48 起改读会话作用域标准 prop sessionId(与官方右侧栏标签 ui-deliverables 的 ReviewTab 同一信源:useSessions(s => s.byId[sessionId]?.cwd)); 旧代那条 current 兜底已随旧代分支删除(2026-10-01),拿不到时不猜目录(不发 cwd,workbench 保持当前目录), 并在控制台留一条 [code-server] 未能解析当前工作区目录… 警告 —— 这个坑当初就是"静默"才难查。
  • 切换是"轻量"的(0.2.12 起):运行中切工作区不重启 IDE 进程,host 只把 state.cwd 改成新目录, 由 workbench 拿新的 ?folder= 重新导航(工作区目录本来就由客户端 URL 决定,进程 cwd 只影响它自己 spawn 时的相对路径解析)。因此切换不再丢扩展宿主/后台任务/服务端状态,也快得多。
    • status 里 cwd = 当前 workbench 目录;launchCwd = 进程启动时的目录(诊断用,不随切换变化)。
    • 代价(要说清楚):旧目录里由 IDE 拉起的后台进程/终端不再被自动杀掉(以前靠"整进程重启"顺带收走)—— 需要时手动收;这也是"不丢状态"的同一枚硬币。
    • 触发时机与本标签是否可见无关:侧栏收起时标签 body 并不卸载,所以后台也会跟随(0.2.12 明确保留此行为)。
    • 回归:scripts/test-workspace-switch.mjs(5 项:接管实例、切目录不改 pid/不换状态、进程存活、 同目录幂等、无 cwd 不切换;改回旧行为必挂)。
  • process 生命周期由 host 插件管理:启动写 $DSH_HOME/code-server/pid.json,停止树级终止(taskkill /T 或进程组 SIGKILL), 崩溃/退出实时更新状态;DSH host 重启后自动 adopt 仍在运行的实例(校验 pid + /healthz),不重复启动、不误杀别的进程;
  • node_modules、vendor/ 与 repack/ 已被 .gitignore 排除,推送/克隆仓库后按下方 "打包(如何出包)"执行 pnpm install → pnpm run vendor:vscode → (发布预编译原生包)→ pnpm pack + dsh plugin --profile web add 即可 (客户端半部与提问面板都是入库的手写源码 —— 整条链上没有任何构建步骤)。

本机(BM: Windows 11 ARM64)实测:树/依赖全链路是"平台子包直挂插件依赖"供给 —— 树包 @jinsiyu/dshcs-vscode-server(当前 4.141.0,tgz 约 44 MB)、纯 JS 内部依赖与 7 个平台无关 重打包包直接进插件 dependencies、8 个平台专属重打包包按 win32-arm64/x64 进 optionalDependencies(包自带 os/cpu 自动选),原始名字由 lib/native.js 补 junction 还原 → healthz 200 → 停止 → 回收全链路验证。 (0.1.37 时代的主包形态已废弃,见下方"升级 VS Code 树"。)

打包(如何出包)

cd C:\Users\User\Desktop\dsh-code-server-app
pnpm install             # 开发依赖只剩 1 个(@deepseek-ai/schemastery);allowBuilds 已显式声明 → 不执行任何 postinstall
pnpm run vendor:check    # 可选:查看内置 VS Code 树版本 vs code-server 最新版
pnpm run vendor:vscode                            # ① 生成 vendor/vscode(精简 VS Code 树,≈197MB)
pnpm run repack:build -- --target win32-arm64,win32-x64 --pack   # ② 统一脚本产出全部子包(见下表)
pnpm run publish:repacks                         # ③ 发布全部 @jinsiyu/* 子包(默认 dist-tag = next)
pnpm pack                                        # ④ → dsh-code-server-app-<version>.tgz
pnpm run publish:plugin                          # ⑤ 发布插件本体(默认 dist-tag = next)
# 用户重启 dsh web 确认无误后,再把 latest 推进到该版本:
pnpm run promote -- <version>

这条链上没有构建步骤:lib/client.js —— 包含「问 DSH」对话框的面板 —— 就是入库的手写源码, 扩展也是纯 JS。所以 prepack 只剩 vendor:vscode 一步,发布包里没有任何前端产物, devDependencies 只剩一个(@deepseek-ai/schemastery,测试用)。 面板与 DSH 页面的历史分析(按版本分段)见 docs/analysis-code-server-as-dsh-plugin.md。

dist-tag 政策(必须遵守):发布一律发到 next,不动 latest; latest 只保留「最近一个确认无 bug 的版本」,由 pnpm run promote -- <version> (= npm dist-tag add dsh-code-server-app@<version> latest)在用户重启 dsh web 确认无误后才推进。 这样 dsh plugin add dsh-code-server-app(不带版本)和任何按 latest 安装的流程都不会拿到未验证的版本。 子包(@jinsiyu/dshcs-*)被依赖以精确版本引用(平台专属的按目标各钉一份),dist-tag 不影响解析,但同样默认发 next。 查看当前标签:npm dist-tag ls dsh-code-server-app。

desktop profile 不走命令行安装(2026-09-13 起的约定):对 desktop 只做 pnpm pack + publish:plugin(发 next), 由用户在 DSH Desktop 里用官方安装方式自行安装;不要再把 tarball 文件级覆盖进 ~/.dsh/profiles/desktop —— 那条路会绕过 desktop 应用自己的依赖闭包检查,把真实的解析问题掩盖成"装上了但行为怪"。 web profile 仍可照旧安装验证。

0.3.45 起没有平台聚合包,requires missing @microsoft/mxc-sdk@npm:… 那类报错不会再出现。根因(实测): pnpm 的增量 hoisted 安装会漏链「可选子树里的 npm: 别名包」,16 个里漏 9 个(第一个就是 mxc-sdk),而 dsh-desktop 在 pnpm add 之后立刻校验依赖图 ⇒ 首次安装必失败;重启后应用走「删 node_modules + 完整安装」 才补齐 ⇒ 就是你看到的"重启自己装好了"。复现命令与两条修法见 docs/desktop-first-install-root-cause.md。

repack:build(scripts/vendor-repacks.mjs)是唯一的子包产出脚本,一次生成:

子包 内容 os/cpu
@jinsiyu/dshcs-vscode-server@<code-server 版本> 精简 VS Code 树(lib/vscode + out/browser + src/browser,不含 code-server 的 out/node 与 136 个运行时依赖) 平台无关
@jinsiyu/dshcs-<名字>[-win32-<arch>] ×15 VS Code 内部依赖里需要构建的原生包(node-pty / @vscode/sqlite3 / kerberos / ssh2 / spdlog / …) 平台专属带 os/cpu
lib/vendored.json(不是包) 「原名 → 重打包子包」表,随插件发布;运行时由 lib/native.js 据此补 junction。0.3.45 起不再产出平台聚合包 —

argon2 已随 code-server 服务层一起移除(0.2.0):auth 固定 none,需要对外访问请用 serve: dsh。

目标 命令
打最新版(上游 code-server 发行版) pnpm run vendor:latest(= --force):从 registry 取 code-server@latest 的树到 vendor/vscode;之后必须重跑 repack:build 并重发全部子包
指定版本 pnpm run vendor:vscode -- --version 4.141.0
从已装好的树快照 pnpm run vendor:vscode -- --from <code-server 目录>(秒级)
开发期让树可直接跑 pnpm run vendor:vscode -- --dev-links(额外把 lib/vscode/node_modules 用 junction 补上)
完整重打子包 pnpm run repack:build -- --target win32-arm64,win32-x64 --pack(不给 --from 会自动 npm install 解包 + 编译,耗时)。--target 只决定本次在这台机器上构建哪些目标(原生包要现编,所以 Linux 目标在 Linux 机器或 repacks.yml 的 Linux 腿上构建);产品覆盖哪些目标、每模块在哪些目标上有子包,由 scripts/repack-platforms.json 决定
只重打树包 + 依赖表 node scripts/vendor-repacks.mjs --reuse --target win32-arm64,win32-x64 --pack(复用 repack/build 里已有的原生包,不重新分析源树;顺带重写 lib/vendored.json 与插件依赖表)
发布子包 推荐用 CI:.github/workflows/repacks.yml(Actions → repacks → Run workflow,勾 publish);本地等价命令 pnpm run publish:repacks(--dry-run 预览;--only <子串> 过滤;--otp <code> / --limit N 应对 2FA;默认跳过已存在的版本,可反复重跑)
发布插件本体 pnpm run publish:plugin(发布已验证过的那份 tarball,不会重新打包;默认 dist-tag = next)
推进 latest pnpm run promote -- <version>(用户重启确认无误后;--dry-run 先看当前标签)
只报告版本 pnpm run vendor:check
盯上游有没有发新版 pnpm run watch:upstream(本机看门狗:只盯 npm 上 code-server 的 latest,有新版本就发 Windows 系统通知并打印升级链;见下一节)

pnpm pack 的 prepack 会自动跑一次 vendor-vscode-server 脚本;vendor/vscode 已存在时它是 秒级 no-op,所以日常只改插件代码的话直接 pnpm pack 即可(不会偷偷升级 VS Code)。 升级树必须显式 pnpm run vendor:latest(或 --force/--version),并重发子包。

上游更新监控(本机自动化任务)

升级链的起点是上游发版,而这件事没人会主动去查:仓库三个工作流全是 push/PR/手动触发(没有 schedule), CI 里那条 vendor:check 又是 continue-on-error —— 于是「上游早就发了新版、内置树还停在上一个」 会在一整片绿灯里完全看不见。scripts/watch-upstream.mjs 就是补这个缺口的本机看门狗。

node scripts/watch-upstream.mjs            # 检查一次;有新版本 → 报告 + Windows 系统通知;无更新 → 一行结论
pnpm run watch:upstream                    # 同上,但会先过 pnpm 的依赖预检(见下面的注意)
node scripts/watch-upstream.mjs --json     # 机器可读(DSH 会话内提醒解析这一份);人类输出转 stderr
node scripts/watch-upstream.mjs --no-notify # 只报告、不发通知(回归 / 无人值守)
node scripts/watch-upstream.mjs --fixture f.json # 用本地 JSON 当 registry 应答(离线)
node scripts/watch-upstream.mjs --local 4.141.0  # 覆盖本地基线试算(不读 vendor/)
node scripts/test-upstream-watch.mjs       # 回归(离线、不发通知)

为什么文档里主推 node scripts/… 而不是 pnpm run …:pnpm run 会先做一次依赖状态预检, 不同步就自动 pnpm install。升级做到一半时这一定会咬人:package.json 先钉上新的 @jinsiyu/dshcs-vscode-server@<新版本>,而那个版本要等 pnpm run publish:repacks 才真的存在 —— 中间这段时间里 pnpm run / pnpm test 会在预检阶段直接 ERR_PNPM_NO_MATCHING_VERSION 失败, 根本跑不到脚本(2026-09-30 升 4.139.1 时实际踩到:当时 registry 上只有 4.136.2 / 4.137.0 / 4.138.0)。 看门狗不该被发布流程的状态卡住,所以定时提醒里用的是直接跑 node 的那条;依赖同步时两者等价。

退出码 含义
0 检查成功 —— 有新版本也算成功(它是"报告",不是"门禁")
1 检查失败(网络不通 / registry 应答异常 / 本地基线读不到)。与"没有更新"严格区分
2 有新版本,且显式给了 --fail-on-update(想拿它当门禁时才用)

信号口径(只盯上游版本,刻意收窄):registry.npmjs.org/code-server/latest 对比本地基线, 基线三级回退 —— vendor/VENDOR.json → vendor/vscode/package.json → package.json 里的 @jinsiyu/dshcs-vscode-server 钉版(CI 全新 clone 上 vendor/ 不存在,走的就是第三条)。 不查 @jinsiyu/* 子包漂移,也不判断"我们的重打包是否已发布对应版本" —— 那是升级流程本身的事。

通知走 Windows 操作中心(不是弹窗,是系统通知):

  • 署名 dsh-code-server-app 上游监控。首次运行会在 HKCU\SOFTWARE\Classes\AppUserModelId\dsh-code-server-app.UpstreamWatch 写一个 DisplayName —— 这是本脚本唯一的系统副作用,--no-register-notify-id 可关,删掉那个键即完全还原。
  • 送达是可核验的:发完立刻用 History.GetHistory($AppId) 回读通知历史,报告里给的是"已送达"的实证, 而不是"命令没报错所以大概发出去了";查不到就如实写"已发出但未能核验到"。 (踩过的坑:不带参数的 GetHistory() 重载查的是"调用进程自己的 AUMID",在 powershell.exe 里必然报 0x80070490 ELEMENT_NOT_FOUND —— 别被它误导。)
  • 节流:同一个上游版本只通知一次;7 天后仍未升级会再提醒一次(免得"提醒过一次"退化成"永远不再提")。 状态在 .upstream-watch.json(已 gitignore)。只有真的发出去了才记账 —— --no-notify 的例行检查 不会把额度用掉,否则那个版本就再也不会被通知,而那正是"静默漏报"。
  • 网络抖动会重试(3 次退避),避免把偶发 ECONNRESET 报成"检查失败"的假警报;假警报多了提醒就会被无视。

也可以挂到 DSH 的会话内定时提醒上:按日唤起时跑 pnpm run watch:upstream,有更新就贴报告、无更新一句话带过。

回归脚本(改完跑一遍)

pnpm test                    # 一次跑完下面全部(scripts/run-all-tests.mjs;CI 与发布前用的也是它)
# ↑ 是唯一清单:新增回归脚本只改 scripts/run-all-tests.mjs,CI/README 都跟着它走
pnpm test:apply              # 桩 ctx 下跑通 apply(回归:apply 期的 ReferenceError)+ 设置数据面:按 volatile 活叶子读值并随 settings/document-updated 更新(旧线注册 schema + 订阅 scope.watch 已随旧代分支删除)
pnpm test:claim-types        # 认领类型语法与默认值
pnpm test:bridge-routes      # 编辑器桥:路由表白名单(只读 + /approve + /old)/ Origin 与令牌的判定顺序 / 令牌头三处一致
pnpm test:edit-snapshot      # 写前原文快照:从 tools/post-execute 的 value 取完整 before / 路径按会话 cwd 绝对化 / 缓存三重有界 / /old 的 400-404-200
pnpm test:bridge-extension   # 编辑器桥扩展侧纯逻辑:未保存缓冲区上报、诊断排序截断、diff 判据(old 侧优先级)、投递降级、提问意图与"对话框不可用给提示"
pnpm test:ask-dialog         # 「问 DSH」对话框的接线:没有产物/构建链了、宿主 4 条 ask 路由、扩展只上报编辑器状态、授权四条、桥的安全不变式
pnpm test:launcher-routes    # launcher 的 HTTP 面(起真进程,较慢)
pnpm test:workspace-switch   # 切工作区不重启进程
pnpm test:workspace-cwd      # "当前工作区目录"解析:会话标准 prop `sessionId` 一种形状(旧代 rc 线快照上的 `current` 兜底已删除)
pnpm test:client-cwd         # 同一件事但直接对**客户端入口** lib/client.js 验(注册出来的 body 真发不发 cwd、URL 带不带 folder)
pnpm test:client-tabs        # "DSH 侧只留一个 code-server 标签页":新标签挂载时收掉同窗格旧标签(跨窗格/不可见时不动)
pnpm test:client-entry       # 客户端入口守卫:经典脚本+工厂包装、require 白名单(= DSH 模块表种子词)、src/ 已消失、与 lib/claim-types.js 逐字一致
pnpm test:ask-panel          # 「问 DSH」对话框面板(0.3.59 起手写):注入机制已下线、视图白名单、注入 CSS 的选择器/var() 安全、六种条目与授权卡片、三条消息落点、拿不到官方部件时的降级
pnpm test:client-seat        # 设置面(**单一座位** `plugins.bundle.config` + **单一通道** `configForms`):座位声明 × 通道有无 4 种组合都要能 apply(注入守卫)
                             # 唯一写路径只有 mutate、注册受 whileServed 门禁,外加**反断言**:旧座位 `settings.plugin.item` / 旧通道 `settingsScope` 不许复活,通道缺失时侧栏标签与常驻预热必须照旧
pnpm test:fullscreen         # 打开标签即全屏
pnpm test:vendored           # 重打包表 ↔ 插件依赖表一致(无 npm: 别名 / 无聚合包 / vendored.json 进了 files)
pnpm test:dsh-resolve        # 部署位置表:各平台全局装布局(npm --prefix / nvm / pnpm global / %APPDATA%)都能找到 DSH 部署
pnpm test:child-node         # Electron 宿主(桌面版)下给 IDE 子进程挑真 Node:候选顺序 / 剥离 ELECTRON_RUN_AS_NODE / 找不到时如实退回
pnpm test:package-files      # 发布物白名单守卫:files 里的模块**递归**import 到的本地文件也必须在 files 里(0.3.53 漏 lib/child-node.mjs 的事故)
                             # 以及反向:files 每条都得存在 —— **打包期生成物除外**(vendor/VENDOR.json 在
                             # .gitignore 里,由 prepack 的 vendor:vscode 生成;ci.yml 不下树,干净克隆上它必然不存在)
pnpm test:installed          # 安装冒烟:对**已装进 profile 的产物**做断言(默认 <DSH_HOME>/profiles/web)
                             # files 白名单每条都在 / 重打包包在当前平台齐全 / 原生模块无缺失 /
                             # 已安装副本能 import / 树在位 —— 仓库回归看不出这一类
pnpm test:metadata           # 插件页图标与文案守约:icon 的相对性/格式白名单/包内 realpath/≤256 KiB/files 覆盖,
                             # 词典的 en.json 锚点/语言 id/键集一致/长度上限,以及"词典必须在 exports 里"这条静默失败守卫

test:bridge-routes 会把 DSH_HOME 指向临时目录(否则它会 adopt 开发机上正在跑的那个实例, 并改写真实的 bridge.json);脚本最后有一条"隔离自检"断言真实配置一字未动。 pnpm test 失败也继续跑完其余脚本(一次看到全部坏点);有的脚本在环境不满足时自己 SKIP 并 exit 0(如 test:launcher-routes 找不到"内部依赖已建链接"的 VS Code 树),属于通过。 @deepseek-ai/schemastery 是 devDependency(钉 3.18.2,与部署同版本):lib/index.js 本来 从 DSH 部署里取它(生产里就是 profile 中 hoist 的那一份),而干净 clone / CI runner 上没有 DSH, 不钉一份的话 apply 类测试会直接抛 schemastery not found。它不进发布物(devDependencies 不会给使用者安装)。 反过来在"已安装副本"里就取不到了(那不是仓库目录):lib/dsh-resolve.mjs 的部署位置表 得把用户真实布局都覆盖上 —— 0.3.49 起包括"正在跑的部署(argv[1]/execPath 向上解析)、 %APPDATA%\npm、npm --prefix(~/.npm-global)、pnpm global、nvm、系统 /usr/local|/usr、 以及 $DSH_HOME 的 profile 层"。0.3.48 的 Linux 安装冒烟腿(CLI 装到 ~/.npm-global)就是 栽在这张表太窄上(冒烟 ④ 抛 schemastery not found,publish 被 skip);回归见 scripts/test-dsh-resolve.mjs(造一整套临时布局逐个验,本机不会自然碰到那些布局)。

GitHub Actions(CI + 打 tag 发布)

两个工作流都在 .github/workflows/,回归清单只有一份(scripts/run-all-tests.mjs,即 pnpm test):

工作流 触发 做什么
ci.yml push main / PR / 手动 ubuntu-latest + windows-latest 双平台:pnpm install --frozen-lockfile → pnpm test(全套回归;0.3.59 起前面没有任何构建步骤)→ vendor:check 只报告版本差
release.yml 推 v<version> 标签 / 手动(演练,不发布) 按 dependencies 钉的版本准备 vendor/vscode → 全套回归 → pnpm pack → 校验 tarball 清单 → 真装两遍(windows-latest 验 win32 的 16 个子包、ubuntu-latest 验 Linux 的 10 个:各部署一份真 DSH,走官方路径 dsh plugin --profile web add <tgz>,再跑 test:installed + dump-config 断言;两条腿都过才允许发布)→ 发 npm next → 建 GitHub Release(附 tgz)
repacks.yml 手动(publish / probe_oidc 默认 false,四条腿的 build_* 默认 true)/ push 本文件 / push .github/oidc-probe.enabled 平台专属子包(@jinsiyu/dshcs-*)的构建与发布:同架构宿主 runner 各打一条(win32-x64 → windows-latest、win32-arm64 → windows-11-arm、linux-x64 → ubuntu-latest、linux-arm64 → ubuntu-24.04-arm),默认只构建 + 传 repack/tgz/*.tgz(不发布,所以它同时就是 Linux 可行性验证的正式位置);勾上 publish 才发 npm(默认 next)。发布归属与顺序(五条腿、集合不相交):先跑 independent(windows-latest,产 VS Code 树包 + 8 个平台无关重打包包 —— 它们在四个目标上是同一份产物,所以只发这一次);四条平台专属腿 needs: independent、构建带 --skip-independent、发布带 --only <自己的目标> ⇒ 基础层出问题时后面不会发出"半套"子包,也不会有人重复发同一个包名。认证:没配 NPM_TOKEN 就走 OIDC(per-package Trusted Publisher,workflow 都填 repacks.yml,见下)。Linux 腿还会顺带校验「Linux 上生成的 lib/vendored.json / package.json 与仓库里的一致」(平台政策应当宿主无关)。额外有一个 probe-oidc job:对几个真实子包名做只暂存、不发正式版的巡检,用来证明这条 OIDC 通道真的可用

Linux 适配(x64 / arm64):改了什么、还差什么

支持 Linux 的关键不是「多编几个包」,而是把平台政策从"宿主扫描"改成"显式声明":

  • 上游包基本不写 os/cpu(实测 16 个模块里只有 @vscode/windows-ca-certs 写了),而树清单把这 8 个 原生模块全放在普通 dependencies ⇒ Linux 上照样会装出 Windows-only 的包;analyze() 又是按 「宿主有没有 .node」分类的 ⇒ 换宿主平台,分类会漂移(Linux 上 windows-registry 会被判成 平台无关、写进 dependencies,于是 Windows 运行时反而找不到 -win32-* 子包)。
  • 所以新增 scripts/repack-platforms.json 作为人工评审的唯一声明:每个模块的 platform(要不要按平台 分包)与 targets(在哪些目标上有子包)。生成器只读不写,并据此产出: lib/vendored.json 的每模块 targets(运行时 lib/native.js 据此跳过本平台不适用的模块, 不再把 dshcs-vscode-windows-registry-linux-x64 这种永远不会存在的包报成缺包)、以及插件 optionalDependencies(不再盲目 × 全部目标)。
  • 生成器同时:保留本次分析看不到的条目(用 --target 只构建本机目标时,别的平台的模块必须原样留下)、 平台专属包在该目标上编不出 .node 时跳过而不是发空壳、Linux 目标补 ELF e_machine 校验 (与 win32 的 PE machine 校验对称)。
  • 版本政策(宿主与时点都无关):lib/vendored.json 里每个模块的版本 = registry 上我们已经发布的那个号, 上游版本漂移不会自动改它。实测两例:同一个 code-server@4.137.0,kerberos 源树里是上游 2.1.1 而我们发布的是 2.1.1-dshcs.1(聚合包时代的后缀);@vscode/proxy-agent 维护者当时装到 0.44.0、 今天全新装到 0.45.0(依赖范围允许漂移,而 0.45.0 这个子包我们根本没发过)。照源树写,换个宿主平台 或换一天重跑,插件依赖就会指向不存在的版本号、装上去直接解析失败。 要让插件升到新的上游版本:在 scripts/repack-platforms.json 里给该模块钉 "version" → 重跑 vendor-repacks.mjs → 发布新子包 → 刷新依赖表与 pnpm-lock.yaml。生成器每次都会把漂移打出来 (· <模块>:沿用表里已发布的版本 …(源树里是 …)),照着那行做即可。

    要区分两类依赖:上面这条只管我们自己的重打包子包(@jinsiyu/dshcs-*,版本必须是发布过的号); 而上游纯 JS 直装依赖(declare 那批:cookie / ws / @vscode/proxy-agent …)的版本取自源树 —— 树里的版本一定来自 npm,跟着树走是安全的。所以后者的差异是时间相关(同一 commit 换一天 全新装就可能不同),CI 的「生成表与仓库一致吗」把它当 notice 报,不当 warning。

  • Linux 上构建原生包的系统依赖:kerberos 要 GSSAPI 头(gssapi/gssapi.h)⇒ 两条 Linux 腿都会先 跑 sudo apt-get install -y libkrb5-dev 并校验头文件在位。缺它时 make 直接失败、kerberos 的子包 产不出来(2026-09-16 第一次跑硬闸门时暴露)。本地在 Linux 上重打时同样要先装它。
  • Linux 上实测(linux-x64 / linux-arm64 两条腿各真编译一遍,结论行: [repack] linux-x64: 平台专属产出 5 个(…)):这 5 个模块编出了 .node —— @vscode/deviceid / @vscode/native-watchdog / @vscode/spdlog / @vscode/sqlite3 / kerberos; windows-ca-certs / windows-process-tree / windows-registry 是 Windows-only(白名单里只给 win32), 在 Linux 上被白名单排除(一次运行里报「白名单排除 N 个」)。 早先那个手工探针 linux-repack-probe.yml 已删除:它的职责(不发布地试编)现在由这两条腿承担, 而它按模块发 notice 会撞上「每个 check run 约 20 条 annotation」的上限、结论只能读到尾部。

Linux 上线现状(2026-09-17 更新):

  1. ✅ 10 个 Linux 子包已首发(5 个模块 × x64/arm64)。新包名没法预先建 Trusted Publisher, 所以首发由维护者本机带 2FA 完成(npm login --auth-type=web → 逐个 npm publish <tgz>), 版本与 lib/vendored.json 逐字一致,os=linux / cpu=x64|arm64 都在 registry 上核对过。

  2. ⏳ 给这 10 个新包名各加一条信任关系(一条命令一条,浏览器确认即可,不需要 OTP):

    $env:npm_config_auth_type = 'web'; npm login     # 已登录可跳过
    $names = @(
      '@jinsiyu/dshcs-kerberos-linux-arm64','@jinsiyu/dshcs-kerberos-linux-x64',
      '@jinsiyu/dshcs-vscode-deviceid-linux-arm64','@jinsiyu/dshcs-vscode-deviceid-linux-x64',
      '@jinsiyu/dshcs-vscode-native-watchdog-linux-arm64','@jinsiyu/dshcs-vscode-native-watchdog-linux-x64',
      '@jinsiyu/dshcs-vscode-spdlog-linux-arm64','@jinsiyu/dshcs-vscode-spdlog-linux-x64',
      '@jinsiyu/dshcs-vscode-sqlite3-linux-arm64','@jinsiyu/dshcs-vscode-sqlite3-linux-x64')
    foreach ($n in $names) {
      npm trust github $n --file repacks.yml --repo jinsiyu/dsh-code-server-app --allow-publish -y
    }
    

    加完就可以把仓库 Secrets 里的 NPM_TOKEN 删掉 —— CI 之后走 OIDC,不会再撞 "token 没勾 bypass 2FA ⇒ EOTP" 那个坑(它只在首发新包名时才是必需的)。

  3. ✅ 依赖表已接线:scripts/repack-platforms.json 的 publishedTargets 已含 linux-*; package.json 的 optionalDependencies 现在是 26 项(16 win32 + 10 linux,由 「每模块白名单 ∩ 已发布目标」公式决定,test-vendored-table.mjs 会逐项校验); pnpm-lock.yaml 已刷新(只新增 10 条,无其它改动);pnpm-workspace.yaml 的 minimumReleaseAgeExclude 也补了这 10 个 @版本(新发布的包会被供应链冷却期挡住)。

    再下一步就是我们自己的发版流程:bump 插件版本 → pnpm pack → 本机 dsh plugin --profile web add <tgz> 确认 → 打 tag 走 release.yml。Linux 用户装到这个版本后,lib/native.js 会自动把 -linux-* 子包按原名补成 junction(与 Windows 同一条路径)。

dist-tag 政策不变:release.yml 只发 next,绝不碰 latest;latest 仍由 pnpm run promote -- <version> 在重启 dsh web 确认无误后手动推进(README 上方「打包」一节)。

发布流程(现在只差打一个 tag):

# 1) bump package.json 的 version → 提交到 main → 等 ci.yml 绿
# 2) 本机照「打包」在 web profile 装一次、重启 DSH 确认无误(desktop 仍只发包、不命令行安装)
git tag v0.3.47; git push origin v0.3.47   # 3) release.yml 自动:重建 tarball → 发 next → 建 Release
pnpm run promote -- 0.3.47                 # 4) 确认无误后推 latest(手动,不经 CI)

一次性配置(仓库 / 账号侧,非文件改动):

  • npm trusted publishing(推荐,无长期凭据):两条路都行,结果一样 —— 给包建一条"只允许本仓库 这个 workflow 发布"的信任关系。
    • CLI(一条命令,本机实测 dry-run 通过):

      npm login                                   # 已是 jinsiyu 可跳过
      npm trust github dsh-code-server-app --file release.yml `
        --repo jinsiyu/dsh-code-server-app --allow-publish
      
      • --allow-publish 必须显式给,否则信任关系建了也不能发布;
      • --file 只写文件名(release.yml),npm 会自己拼成 .github/workflows/release.yml;
      • 这条命令要求 2FA(会要一次 OTP;npm trust 属于账号变更类操作);
      • 沙箱/受限 shell 里若报 EPERM … npm-cache,把缓存挪到可写目录即可: $env:npm_config_cache='<可写目录>'(不要把 --cache 写在 trust 前面 —— 会干扰 npm 的子命令解析,报 Unknown positional argument: github);
      • 核对:npm trust list dsh-code-server-app(该接口对旧式 token 可能返回 403, 以 npm 网页上的 Trusted Publisher 列表为准)。
    • 网页:npmjs.com → dsh-code-server-app → Settings → Trusted Publisher → GitHub Actions: Organization/user = jinsiyu,Repository = dsh-code-server-app,Workflow filename = release.yml, Environment name 留空(本仓库 release job 没有 environment:,填了就对不上)。

    • 语义提醒:这条信任关系等于"任何对本仓库有写权限的人都能发布这个包"(npm 的原文: "Anyone with GitHub repository write access can publish")。

    • 备选:仓库 Secrets 加 NPM_TOKEN,并设仓库 Variables NPM_AUTH_MODE=token(该模式不发 provenance)。注意 npm 正在收紧"绕过 2FA 的旧式 token"(见 registry 的 bypass2fa-deprecation 提示),所以 OIDC 是更长期的做法;它一旦可用就该把 NPM_TOKEN 删掉。

  • repacks.yml(平台专属子包)的认证 —— 两条路,workflow 自己判断(没配 NPM_TOKEN 就走 OIDC):
    • A. NPM_TOKEN(目前最省事)
      1. npmjs.com → 头像 → Access Tokens → Generate New Token → Granular Access Token;
      2. 名字随意(如 github-actions-repacks),给一个有效期(如 90 天);
      3. Packages and scopes 选 Read and write,只勾 @jinsiyu 作用域(别选 All packages);
      4. 必须勾 "Bypass two-factor authentication (2FA)" —— 否则无人值守发布会卡在要一次性口令;
      5. 生成后令牌只显示一次,立刻复制;
      6. GitHub 仓库 → Settings → Secrets and variables → Actions → New repository secret, 名字必须是 NPM_TOKEN(workflow 里读的就是它),值粘贴令牌。 ⚠️ npm 官方已公告:2026-07-31 起 bypass-2FA 令牌不能再做账号/包管理类操作,并且 2027-01 起将失去直接发布(只剩读取私有包 + 暂存发布,要维护者用 2FA 批准)⇒ 长期请迁到 B。
      • 发布失败怎么读(publish-repacks.mjs 会先做一次 token 体检:只打印长度与形状、绝不打印内容, 再用 npm whoami 确认能不能认证): · E401 / ENEEDAUTH ⇒ token 值不对:带了引号或末尾换行、复制被截断、或已被撤销 ⇒ 重新生成再粘贴 (Granular token 形如 npm_…,长随机串;classic token 是 36 位 UUID); · npm whoami 通过、但发布报 EOTP(This operation requires a one-time password) ⇒ 值是对的, 缺的是权限属性:该 token 没勾第 4 步的 "Bypass two-factor authentication (2FA)"; 另外 npm 对首次发布一个新包名本身就要求 2FA,而新包名没法预先建 Trusted Publisher ⇒ 首发只能本机 npm publish <tgz> --access public --tag next 带 OTP 走一次(只发该目标自己的 -<目标> 包,别重发已存在的树包),之后给这些包名各加一条信任关系即可转 OIDC。
    • B. per-package trusted publishing(长期方案):npm 的信任关系是按包配的(2026-09 起一个包 可以配多条,但没有作用域级),有多少个子包就要多少条(win32 阶段是 25 条;Linux 上线后按 lib/vendored.json 里每模块的 targets 增加)。先生成命令,再在浏览器授权一次后逐条执行:
      node -e "const t=require('./lib/vendored.json');const p=require('./package.json');const tree=Object.keys(p.dependencies).find(n=>n.endsWith('/dshcs-vscode-server'));const names=[tree,...t.modules.flatMap(m=>m.platform?m.targets.map(x=>m.package+'-'+x):[m.package])];require('fs').writeFileSync('trust-all.txt',names.map(n=>'npm trust github '+n+' --file repacks.yml --allow-publish -y').join('\n')+'\n')"
      Get-Content trust-all.txt | ForEach-Object { Invoke-Expression $_ }
      

      注意这里用的是每模块的 targets(不是「模块 × 全部目标」):@vscode/windows-registry 只在 win32-* 有子包,-linux-* 的包名永远不会存在 —— 给不存在的包建信任关系只会白跑一遍。 配完把 NPM_TOKEN 删掉即可(workflow 会自动走 OIDC)。npm 也允许把每条配置设成只允许暂存发布 (版本要你 2FA 批准才生效)—— 更安全,但每批子包都要你手动批准多个版本,按需取舍。 核对:对任意子包执行 npm trust list <包名>,应显示 file: repacks.yml 与 repository: jinsiyu/dsh-code-server-app(25 个子包一个都不能少 —— 漏掉的那个包发布时会报 "没有匹配的信任配置",而那一行会以 annotation 出现在 run 里,不需要 token 就能读)。

    • 验证这条通道(不必等真发布):repacks.yml 里有一个 probe-oidc job —— 对 4 个真实子包名(vscode-fs-copyfile / node-pty / kerberos-win32-arm64 / vscode-server) 各做一次 staged(暂存)发布,版本形如 2.0.1-oidc-probe.<run>。 为什么必须放 CI 里:OIDC 令牌只在运行时签发,信任关系按「仓库 + workflow 文件名 + 包名」匹配 ⇒ 离线验证不了;也正因为后者,巡检只能由 repacks.yml 这个文件发起(换个文件名就会被 npm 拒)。 为什么用 npm stage publish 而不是 npm publish:npm stage 走的是同一条认证路径,但版本进的是 暂存队列,不进 registry 的正式版本列表(脚本自己会用 npm view <pkg> versions 复核一遍), 所以不会占用任何版本号、也不影响依赖解析。 触发方式(两种,第二种不需要任何令牌):
      # ① Actions → repacks → Run workflow,勾上 probe_oidc
      # ② 提交哨兵文件后 push(工作流会跑一遍构建演练 + 巡检)
      New-Item .github/oidc-probe.enabled -ItemType File -Force
      git add .github/oidc-probe.enabled; git commit -m "chore: OIDC 通道巡检"; git push
      
      结果怎么读:每个包成功/失败都会发 ::notice:: / ::error:: annotation,公开仓库不需要登录 就能读(GET /repos/jinsiyu/dsh-code-server-app/check-runs/<id>/annotations);job 摘要里也有一张表。 收尾:把暂存的探针版本reject 掉(npm stage list 看 id、npm stage reject <id>,需要你本机的 2FA; npm 网页上也有对应的 staged 列表)——不要 approve,approve 才会让它变成正式版本。 验证完删掉哨兵文件,工作流就恢复"不自动巡检"。

几条必须知道的:

  • release.yml 是在 runner 上重建 tarball,发的不是本机那份文件:同一 commit + 同一个钉死的树版本 ⇒ 内容一致,唯一差异是 vendor/VENDOR.json 里的 platform / preparedAt / sizeMB 元数据(运行时只读 codeServerVersion 与 productPath)。所以第 4 步 promote 之前,照旧在 web profile 装一次验证。
  • CI 不打 pnpm pack:全新 clone 上 prepack 会去 npm 取 code-server@latest,与 dependencies 里钉的 树版本不同步(反而会把「树包精确钉版本」搞挂)。打包只在 release.yml 里做,且显式 node scripts/vendor-vscode-server.mjs --version <pinned>。

…

内容来自项目 README(GitHub)↗

链接

同类插件

查看整个分类 →

社区评论

评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。