兼容基线(2026-10-08):面向
@deepseek-ai/[email protected](DSH Docker 使用的 alpha 通道),而非 npm 默认latest对应的0.2.0-rc.2。CI 在 Node 24 下固定检查此版本的 DSH 启动命令;原有本地 Git/Host/Client 模拟验证继续运行。CLI smoke 不等同于真实浏览器内完成插件安装和端到端验证。升级 DSH 时先运行npm test并检查 DSH Web 的右侧栏、主页面和 Agent 工具。
为 DeepSeek Harness Web UI 提供 VS Code 风格的源代码管理(Source Control):无需离开应用即可管理某个 Session 工作目录的 Git 仓库。
同一个面板之上有两个界面:
- 右侧边栏中的一个源代码管理标签页(Source Control tab),其仓库跟随该标签页所在的 Session,以及
- 主区域中的一个源代码管理页面(Source Control page),带有自己的仓库选择器和提交历史,通过侧边栏图标进入。
两者都提供带 ahead/behind 计数的分支显示、文件列表上方的提交消息输入框、可折叠的已暂存的更改 / 更改 / 合并冲突(Staged Changes / Changes / Merge Conflicts)分组、针对单个文件的暂存、取消暂存和放弃更改(带确认对话框),以及位于分支和更多操作控件之后的显式 Fetch / Pull / Push。未跟踪的文件以 U 徽标出现在更改中;文件名优先,目录次之,状态徽标和悬停/聚焦操作位于右侧。
主页面是一个工作台:左侧是源代码管理导航器,右侧是差异编辑器。拖动它们之间的分隔条可改变导航器宽度。打开差异时列表保持可见,并标记所选行。已更改的文件可以筛选,并以列表或目录树的形式显示。
提交图在两个界面的更改列表下方都可用。拖动其水平分隔条可改变相对高度;接近常见的常用位置时会吸附,拖到底部会将其折叠,双击会重置该分隔。分隔条也支持键盘调整。各区域独立滚动。在小屏幕上,页面导航器与编辑器垂直堆叠;紧凑标签页保持其 列表 → 差异 → Back 的流程。shell 现有的外层边栏分隔条继续负责右侧边栏的宽度。
提交图的边来自实际的父提交,而不是历史列表的顺序。本地/远程分支和标签都有标注。选中一个提交可查看其完整消息和已更改文件,然后打开一个只读的历史差异。根提交、合并提交、重命名和删除文件的比较使用第一父提交的更改(根提交则使用空树);历史视图从不提供对工作区的写入。
差异会显示旧/新行号、工作区/索引一侧,以及对已跟踪、无冲突文件的**按块(per-hunk)**暂存、取消暂存和经确认的放弃更改。未跟踪的文件保留整文件控件。有冲突的路径会被列出,以便冲突可见,但它不带有暂存或放弃控件:命令层无法对未合并的路径执行操作,而解决冲突不属于此面板的范围。提交只使用已暂存的索引;Ctrl/Cmd+Enter 与按钮遵循相同的可用性和单次写入防护。切换主页面仓库会重置所选文件、提交草稿和分组状态,而不是把它们带到另一个仓库。
当已配置的 Git remote 指向官方 github.com(HTTPS 或 SSH、且 URL 不含凭据)时,更多 Git 操作菜单提供三个只读外部入口:
- 打开 GitHub 仓库:访问该仓库主页;
- Pull Requests:打开该仓库的 PR 列表;
- Issues:打开该仓库的 Issue 列表。
优先选择 origin;没有时选择第一个可识别的 GitHub remote。这些链接来自宿主端白名单解析,浏览器不会获得 Git 远程 URL 中的原始凭据,也不会自动发起 GitHub API 调用或 Git 传输。不支持的 GitHub Enterprise / GitLab / 含 Token 的 HTTPS remote 会隐藏这些入口,原有 Git 命令仍正常使用。
当前尚未实现: 插件内的 PR 列表/审核/创建/合并、Issue 搜索、GitHub 账号 OAuth 和 GitHub Actions 面板。后续按只读 → 明确用户授权的写操作逐步扩展,避免一开始就复制 VS Code 插件的全部权限边界。
在已识别 github.com remote 的仓库里点击 GitHub 审查,无需离开 DSH Web 即可:
- Pull Requests:按打开/关闭状态查看最近更新的前 30 条 PR,选择 PR 阅读标题、描述、源/目标分支;
- Changed files:查看前 100 个变更文件,选择文件查看 GitHub 提供的统一 Diff 文本补丁,新增/删除行高亮;
- Checks:读取所选 PR 最新 head commit 的 GitHub Checks(最多 100 条),展示完成状态与结论;
- Issues:查看公开 Issues 首页,选择后可跳转到 GitHub 处理;响应中的 PR 项会排除。
网络与权限:以上接口均为认证后的 DSH 内部 GET 路由,由宿主通过 GitHub 公开 REST API 读取。目标仓库仅来自 Git 已配置 remote 经白名单解析的 github.com URL。不会向浏览器暴露 Git remote 的 Token,也不会向外发送 DSH Cookie;不接受任意 API URL,不跟随重定向,不在后台进行 Git Fetch/Pull/Push。GitHub API 的匿名限流、私有仓库不可见、文本 Diff 太大等情况会显示明确错误或跳转入口。
当前只读工作台不包含 PR 创建/审核/合并、Issue 编辑、OAuth 或 GitHub Enterprise。它们需要独立授权、审计、确认机制后才能作为写操作加入。后续可扩展 PR 评论、关联 Issue、分页以及可选私有仓库 OAuth;不应把此只读功能描述成完整 VS Code GitHub 扩展替代品。
为确保旧用户不受影响,默认 无 GitHub 凭据、只读匿名公共 API。需要私有仓库或创建 PR 时,使用 DSH Host 进程的环境变量,并在插件配置里明确列出允许操作的 GitHub 仓库:
- id: source-control
config:
timeoutMs: 30000
githubAuth:
repositories:
- PaiMonCai/private-repo
allowWrites: false # 只有需要创建 PR 时才明确改为 true在 DSH Host 服务环境(不是浏览器,也不要写到 Git 仓库或 profile 的 YAML 中)配置 DSH_SOURCE_CONTROL_GITHUB_TOKEN。推荐使用仅授权上述仓库、仅授予实际所需权限的 GitHub fine-grained Personal Access Token。读 PR/Issues、文件差异、CI Checks 时配置相应 Read 权限;需要创建 PR 时额外授予 Pull requests: Write,并把 allowWrites 显式打开。重启 DSH Host/重载插件以更新配置,不要在聊天、前端输入框或仓库中粘贴 token。
重要的信任边界:这是一个Host 级别令牌,而不是用户各自的 OAuth 身份。能够使用此 DSH Host 已认证 Web 界面、访问该仓库的用户,可能共享访问白名单 GitHub 仓库的能力。多租户/不受信任的多人 DSH 部署请保持禁用,在支持真正的每用户 OAuth 和权限隔离之前不要启用。白名单只是控制令牌能够被使用的仓库范围,不是每用户 ACL。
创建 PR 要求:当前本地分支已推送到被识别的 GitHub 远程且设置了正确上游,远程跟踪对象 ID 与本地 HEAD 一致,目标分支已存在于远程跟踪列表;表单需要两次明确点击确认。该 POST 只创建 GitHub PR,不执行 git push、不自动拉取、不合并、不执行仓库操作。插件不向 Agent 开放此写操作,也不储存 token。
仍未支持:GitHub OAuth 账号登录、跨账号/不同权限的独立授权、私有 GitHub Enterprise、PR 评论与审核、自动合并。启用前需要在目标部署中手工验证访问隔离及权限设置。
source_control 通过 UI 调用的同一批服务方法,把本地操作子集暴露给 agent——一份实现,两个调用方。分支变更和远程同步仍是仅限人工的 UI 操作:
| 操作 | status, stage, unstage, diff, commit |
| 范围 | 调用 agent 自己 Session 的工作目录;没有仓库参数,因此无法指向另一个检出 |
| 不可用 | discard——不可逆,因此在 UI 中仍是仅限人工的操作 |
diff 返回单个文件的比较结果——块头部原样保留,+/-/空格前缀保留——因此 agent 可以在编写提交消息前读取某个更改。超过 MAX_DIFF_LINES(500)后,其余内容会被切掉,并且这次截断会连同数量一起说明,因为一个被静默缩短的差异读起来会像是完整的更改。
它通过一个可选的 inject 注册,因此即使某个组合没有 tools registry,插件仍会加载,只是没有面向 agent 的接口。
拒绝会以消息前加上其 code 的形式到达模型(repo/not-a-repository: …)。tool registry 只给自己的 HarnessError 附加结构化的 code,而本包无法导入它——本包是作为 link: 从工作区安装的,所以裸的 @deepseek-ai/... specifier 会在工作区而非 profile 中解析。HTTP 路由的结构化信封保持不变。
提交是本地操作;同步是显式的。 只有当人工通过已认证的 POST 路由调用 Fetch、Pull 或 Push 时,才会发生 Git 传输。读取状态、分支或提交图从不自动获取,面向 agent 的工具也无法执行远程操作。有两类本地程序在这道栅栏之外,这里明确指出而不是留作隐含:配置的 clean/smudge 过滤器(见下文)以及仓库自身的钩子,后者会运行,因为 commit 有意不使用 --no-verify。
仅限本地是强制执行的,而不是假定的。其他所有 Git 调用都在 protocol.allow=never 和 GIT_NO_LAZY_FETCH=1 下运行,因此**部分克隆(promisor)**无法让一次读取去获取缺失的对象——一次读取要么从本地对象作答,要么以 git/object-unavailable 拒绝。历史读取还会在禁用替换对象、关闭签名显示的情况下运行,因此 refs/replace 条目无法改写精确的提交 ID,宿主的 log.showSignature 设置也无法把签名程序的输出注入到机器解析的记录中。提交 ID 必须是仓库自身格式中精确的完整对象 ID:SHA-256 ID 的 40 字符前缀会被拒绝。
有一处边界仍在此控制之外:诸如 Git LFS 的已配置 clean/smudge 过滤器是 Git 在 add/switch 期间运行的本地程序,而该程序可能联系其自己的服务器。禁用它会让一个期望真实文件内容的工作区损坏,因此插件保持过滤器启用,并且不声称它们无法访问网络。
分支选择器可以切换已知的本地分支、在当前提交处创建分支,或从已知的远程跟踪分支创建本地跟踪分支。切换、创建分支和拉取要求工作区完全干净,包括未跟踪文件。没有自动 stash、放弃更改或强制切换。
- Fetch 更新已配置远程的引用,但不更新工作区文件,也不递归获取子模块。
- Pull 是仅快进。出现分叉时会拒绝,而不是自动合并或变基。请选择明确的目标或使用当前上游。
- Push 以明确的、非强制的 refspec 发送当前本地分支。首次推送需要已确认的远程/分支目标以及上游设置。不提供强制、镜像、删除或标签推送。
只接受已配置的现有远程名称;调用方不能提交远程 URL 或 shell 命令。拉取/推送会携带操作对话框打开时捕获的源分支;排入队列的分支变更会导致拒绝,而不是把过期的目标重新解释为针对新的源。它们的默认目标来自分支真正的上游(%(upstream:remotename) 加上 %(upstream:remoteref)),而不是缩写形式的 origin/main 显示名,因此一个被修剪或从未获取过的跟踪引用无法悄悄地把推送重定向到第一个已配置的远程。如果该上游远程已不存在,对话框会显示它、拒绝提交它,并等待一个刻意的选择。. 上游只跟踪本地分支:首次远程推送仍会要求设置上游。Git 使用宿主侧凭据。浏览器中不存在凭据编辑器或令牌存储;交互式提示被禁用,带凭据的诊断信息会被脱敏——URL userinfo(包括包含 / 或 @ 的密码,以及不带点的主机名)、整个查询字符串、api-key=… 或 X-Amz-Signature=… 这类以 secret 命名的赋值、Basic/Bearer/Digest 值,以及 JWT。脱敏的范围是有意限定的:无标签的不透明 blob 会被放过,因为任何宽到足以捕获它的规则同时也会毁掉普通诊断信息(绝对路径、冲突路径、引用名,以及恰好以 = 结尾的文件名)。承诺对它脱敏就意味着经常隐藏那句话——它说明出了什么错。认证、传输或主机密钥失败会被报告,而不是被绕过。支持标准 SSH 配置、密钥、agent 和 known-hosts。使用自定义 core.sshCommand、GIT_SSH_COMMAND 或 GIT_SSH 包装器的 SSH 端点会被明确拒绝:插件无法在不悄悄替换其身份语义的情况下保证批量认证。请改为在宿主的 SSH 配置中配置标准 SSH 身份。
所有由 service 负责的变更操作,包括暂存和提交,都按规范化仓库根(canonical repository root)串行化。这不会把外部 Git 进程锁在外面。显式指定某个仓库的请求绝不会在 Session 缓存中发布为调用 Session 自己的仓库,因此之后仅针对 Session 的调用——包括 agent 变更——无法被重定向到它那里。暂存支持整文件和已跟踪更改中的单个块。克隆、删除分支、reset、强制推送、amend、rebase、merge、stash 管理和标签操作仍不在范围内。验证使用一次性的本地仓库和裸远程,绝不使用用户真实的远程。
plugin_manager install_bundle /workspace/dsh-source-control
包目录必须留在它被安装的位置:
plugin_manager会把它链接进 profile,因此移动或删除它会破坏该 bundle。
如果 profile 已经在运行,bundle 的选择项会写入 package.json,但其 fiber 可能仍处于非活动状态。将该 bundle 关闭再打开(plugin_manager set_bundle)即可实时应用,或者重启 dsh web。
验证它确实已生效,而不仅仅是已保存:
cordis_inspect_query→ hostConfig.listConfigs,其name: "dsh-source-control"应列出include:source-control条目;cordis_inspect_query→ clientSlots.listSubTree应显示:在sidebar.right.pane.tab中有一个以dsh-source-control为键的活动占用者,在main中有一个活动的source-control键,以及在sidebar.panellist中的source-control。
宿主侧和客户端代码有不同的规则,而这一差异很容易搞错:
- 客户端(
lib/client.js)——bundle 以内容寻址的修订版本提供服务,因此刷新页面即可获取更改。 - 宿主(
lib/index.js及其导入的一切)——普通的import()按 URL 缓存。如果没有限定范围的 Host HMR 监视器,更改需要重启dsh web。启用下面的监视器后,HMR 会使入口及其导入的模块失效并重新加载它们;请验证该监视器和成功重新加载,而不要假定浏览器刷新也会替换宿主代码。
profile 自带 hmr.config.root: [],这会完全禁用模块监视。本包在 profile patch 中的 cordis.patch.yml 说明设置了一个限定范围的 root,从而开启监视:
- id: hmr
config:
root:
- /workspace/dsh-source-control有了它之后,触碰 lib/index.js 会重新加载入口,HMR 也会使该文件的后代失效——因此编辑 lib/tool.js 然后触碰入口就足够了。Node 的 node_modules 忽略规则,正是这里指名包的真实路径、而不是 profile 指向它的符号链接的原因。
要验证重新加载,而不是假定它发生:修改一个模型可见的字符串,触碰入口,然后读回工具列表。
所有值都位于 cordis.patch.yml 的 source-control 行中,并在激活时校验;未知字段或非正值会使该行失败,而不是被忽略。
| 字段 | 默认值 | 含义 |
|---|---|---|
timeoutMs |
30000 |
单条 git 命令在被放弃前可运行的毫秒数。 |
maxOutputBytes |
8388608 |
每条命令保留的 stdout 字节数。更大的差异会被拒绝,绝不截断,因为被静默切掉的差异读起来会像是完整的更改。 |
maxDiffLines |
20000 |
在告知查看器差异过大之前,单个文件的差异可携带的行数。 |
maxLogEntries |
50 |
每个历史页返回的提交数。 |
- id: source-control
config:
timeoutMs: 30000
maxLogEntries: 100React panel ──fetch('/api/source-control/<op>?sessionId=…|repo=…')
│ cookie-authenticated, inside the Host/Origin fence
▼
connection.fetch exact route ──► SourceControlService
│ │ cwd from ctx.sessions
│ │ assertInside() on every path
▼ ▼
JSON envelope git.js ── ctx.subprocess (argv only)
| 文件 | 负责 |
|---|---|
lib/git.js |
仅使用 argv 的 GitRunner、状态/差异/日志解析器以及规范化操作工厂。不涉及 Cordis 或 HTTP。 |
lib/git-repository.js |
内部的分支/同步操作与不可变的提交/图操作,使用同一个 runner。 |
lib/repo.js |
assertInside,唯一的文件系统路径权限防护,以及仓库定位。 |
lib/service.js |
Session→仓库解析、路径权限、操作表和按根的写入串行化。 |
lib/routes.js |
二十条已认证的 /api/source-control/* 路由以及不变的 JSON 信封。 |
lib/index.js |
Cordis 入口:配置校验、装配、拆卸。 |
lib/tool.js |
agent 工具:操作集、参数校验、差异渲染。 |
lib/client.js |
浏览器侧:标签页类型、页面、共享面板、块控件、样式。 |
- 信封。 每条路由都以 HTTP 200 作答并返回
{ok: true, value}或{ok: false, error: {code, message, details}}。业务性拒绝(“没有任何已暂存的内容”)是一个结果,而不是传输失败。非 200 意味着请求从未到达处理程序。 - 读与写。 每条路由声明
read: true|false。该标志就是全部权限边界:grep -n "read: false" lib/routes.js会枚举所有可能更改仓库的操作。 - 路径权限。 工作区变更/差异路径会经过
service.withPaths()。防护会通过真实文件系统解析最近一层已存在的祖先,因此逃逸的目录符号链接会被拒绝;叶子符号链接是允许的,因为 Git 存储的是链接本身。历史差异则校验完整的提交 ID 以及是否属于该提交的已更改路径集合;它们只读取 Git 对象,因此一个被正常删除的路径不需要存在的文件系统祖先。 - 远程权限。 分支/引用名会被校验,而不是作为任意的 revision/refspec 接受。远程操作只选择已配置的名称,保持为已认证的 POST 写入,且绝不扩展 agent 工具的操作。
- 写入串行化。 所有由 service 负责的写入在解析出的规范化根上共享同一个队列,跨各 UI 界面和 agent 调用。失败或被取消的请求不会污染后续工作。读取和外部 Git 进程不会被事务性锁定。
- 块操作通过编号寻址,绝不通过补丁文本。 客户端发送
{path, hunkIndex};宿主从实时的git diff输出重新生成补丁,从中切出该块,并通过 stdin 喂给git apply。任何调用方都无法提供补丁、指定第二个路径,或触达防护尚未放行的文件——而这正是块暂存通常会打开的那一整类攻击面。slicePatch会整块取出(从其@@到下一个),因为git apply会拒绝头部与其下方各行不一致的补丁。 - 放弃块仅限人工。
discard-hunk作为路由存在,但没有 agent 操作,原因与整文件放弃相同:它不可逆。两份面向 agent 的操作列表都在verify/tools.mjs中断言。 U3上下文使各块保持可分离。 间距小于该上下文的块会被 git 合并,因此hunkIndex寻址的是 git 实际报告的内容;宿主会拒绝它没有的索引,而不是猜测。- Pathspec 是字面量。 runner 设置
GIT_LITERAL_PATHSPECS=1。没有它,git 会把路径参数当作 glob 读取:暂存一个名字字面为report[1].txt的文件也会暂存report1.txt,而放弃更改背后的git clean -f会删除仅仅是匹配到的同级文件(star*.txt也会移除starZZZ.txt)。不要移除该变量。 - loader 的值在被塑形之前不是数据。
useLoad存储路由解析出的任何内容,包括在未选中任何内容时差异 loader 返回的null。在一次点击之后、effect 重新运行之前的那次渲染中,该null仍在 state 中,因此任何读取已加载值的代码都必须针对它需要的形状做检测(Array.isArray(value.hunks)),而不仅仅是!== undefined。搞错这一点会让面板在用户第一次点击时变空白。 - 每个界面自行编写 DOM id。 侧边栏标签页和主页面可以同时挂载,因此用一个面板常量硬编码出来的 id 会出现两次,并把
<label>绑定到浏览器最先找到的那个。 - 提交的
date并不保证存在。 请通过String(commit.date ?? '').slice(0, 10)读取它;直接.slice会抛错,随后错误边界会替换整个页面。 - 幂等写入。
stage/unstage/discard从实时工作区计算其生效集合,因此过期的 UI 行会作为被报告的 no-op(alreadyStaged、alreadyUnstaged)出现,而不是 pathspec 错误。 - 两种放弃粒度。 整文件放弃从 HEAD 恢复已跟踪路径或删除未跟踪路径;块放弃反向应用单个块。两者都会请求确认,且两者都不允许 agent 触达。
- 放弃的分类。 已跟踪路径从 HEAD 恢复;未跟踪路径用
git clean -f删除,且范围精确限定为这些路径(有意不加-x,因此被忽略的文件得以保留)。与stage和unstage一样,放弃会针对 git 报告的更改做正向过滤,因此不是当前更改的路径会以unchanged返回,而不是到达 git。 - 达到上限的响应会被拒绝,而不是被解析。 collect 模式在
maxOutputBytes处停止 stdout;解析该前缀会把一个 3000 行的更改变成“新增 0 行,删除 410 行”,并把被截断的日期变成损坏的日期。GitRunner.run反而会抛出git/output-too-large,这正是cordis.patch.yml中“拒绝而不是截断”一直所声称的。 - 仓库保留其护栏。 Commit 不传递
--no-verify,因此仓库自身的 pre-commit 和 commit-msg 钩子会运行;钩子失败会表现为普通的提交失败。 - 读取差异不会运行仓库定义的任何东西。 产生可审阅输出的差异参数——
/diff、块应用和历史提交差异——都带有--no-ext-diff和--no-textconv,因此被克隆的.gitattributes/config 中声明的 diff driver 或 textconv 命令不会仅仅因为点击了某个文件就执行。提交按钮背后的已暂存索引探测同样带有这两个标志,因此一次提交尝试也无法触发其中之一。(git add仍然遵循filter.<x>.clean,对此外 git 没有提供退出标志——这是一处有记录的残留,而非疏忽。) - 不触碰 git 自身的元数据。
assertInside会拒绝任何第一段为.git的路径,因此没有路由能指定钩子、配置文件或对象。 - 客户端导入。
lib/client.js只从 shell 的模块表中 requirereact,不导入任何 Harness Client 包(纯 JS 插件没有类型检查,而一个抛错的组件会让整个席位变空白)。样式是它自己的,在data-plugin-css标签下注入一次,只使用--dsw-*token。每个界面都在错误边界内渲染,因此缺陷会显示一条消息,而不是让面板变空白。
npm test 会运行所有套件并打印一份合并结果。这些套件让真实代码针对真实的临时(scratch)仓库运行——不 mock 被测行为——并且不需要任何依赖,因此一次全新的检出即可立即运行它们。
node verify/git-layer.mjs # git operations, path guard, unborn branch, pathspec literals
node verify/repository-operations.mjs # branches, graph, historical diffs, local-only reads, remote safety
node verify/routes.mjs # every route through real Request objects
node verify/repository-routes.mjs # service authority, canonical-root queue, real scratch remotes
node verify/apply.mjs # Host wiring: routes registered, disposed, config
node verify/client-bundle.mjs # the browser bundle actually executes and registers
node verify/client-render.mjs # the panel renders real payloads; diff line numbering; dialogs
node verify/hardening.mjs # regressions for the defects an adversarial review found
node verify/tools.mjs # the agent tool, diff rendering, and both callers agreeing
node verify/hunks.mjs # hunk stage/unstage/discard, and that no patch text is accepted
node verify/release.mjs # packaging and the static capability boundarynpm test 打印合并结果;计数位于每个套件自己的输出中,而不是本文件里,因此它们不会漂移。
仅限本地的读取是一个内部标志,而不是类型层面的保证:传输豁免是 lib/git-repository.js 中唯一一处 transportBatchMode 调用点,而 verify/repository-operations.mjs 设置同一个标志,以准备它自己那些需要传输的 fixture。
verify/repository-operations.mjs 和 verify/browser-workbench.mjs 是两个超出普通单元检查范围的套件。前者让真实 Git 针对一次性仓库运行,其中包括一个部分克隆,其 promisor 远程指向一个会记录任何执行的辅助程序——从而证明一次读取既不获取它也不运行它。后者是可选的,需要浏览器、运行在 http://127.0.0.1:3080 上的 GUI(它提供该套件所断言的产物)以及安装在 /usr/local/lib/node_modules/playwright 的 Playwright。它会获取所提供的客户端产物,与磁盘上的源做逐字节比较,并针对隔离的 fixture 渲染它,以检查真实的指针/键盘调整尺寸、桌面和移动端几何布局、只读历史审阅,以及捕获源的远程对话框。它从不写入活动仓库,也从不绕过认证:
npm install --prefix node_modules/.scm-browser --no-audit --no-fund --package-lock=false [email protected] [email protected] [email protected]
CHROME_BIN=/usr/local/bin/chromium node verify/browser-workbench.mjs它们是普通脚本:每条断言打印 ok/FAIL,并在失败时以非零状态退出。verify/routes.mjs 和 verify/client-bundle.mjs 自带 node:child_process/假上下文适配器,正是为了让它们不需要运行中的宿主。
它们未覆盖的内容包括:浏览器会话栅栏(用未认证的请求断言——预期 401,用外来的 Host 头断言——预期 403)、针对实时服务器的真实宿主 SSH/HTTP 认证,以及运行中的已认证 GUI。verify/browser-workbench.mjs 把真实的客户端针对隔离的 fixture 渲染,这是布局和交互方面的证据,而不是关于实时部署的证明:本宿主上的 GUI 对未认证请求返回 401,而未认证的 401 无法区分“路由已注册”与“什么都没注册”。请把这一边界视为未验证,而不是通过。
本包直接发布它运行的 JavaScript。没有编译、没有打包器,也没有 devDependencies,因此“构建”它没有任何意义——这里的 build 脚本只会是做戏。因此这些工作流不进行构建:
| 工作流 | 触发条件 | 作用 |
|---|---|---|
ci.yml |
每次 push 和 PR | 在 Node 20.3、22 和 24 上运行 npm test;断言运行时下限;检查 npm install 成功 |
release.yml |
一个 v* 标签 |
运行 npm test,检查标签与 package.json 匹配,运行 npm pack,并把 tarball 及其校验和附加到 GitHub Release |
下限是 Node 20.3:git runner 使用 AbortSignal.any,这是本包中任何地方所用的最新 API(engines.node 声明了同样的界限)。
# 1. bump the version
npm version patch # or minor / major; this commits and tags
# 2. push the commit and the tag
git push --follow-tags当标签与 package.json 不一致时,release.yml 拒绝发布,因此一个 release 绝不会标称代码并未携带的版本。所附的 tarball 就是可安装的 bundle;它的 sha1 和 sha512 会打印在 release notes 中。
打标签前在本地验证:
npm test
npm pack --dry-run # confirm the file list a consumer receives