🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

云端工作区 Ckarefulon/dsh-codespace-workspace

DeepSeek 客户端
Ckarefulon21 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

把 GitHub Codespace 当作本地工作区直接读写文件并执行命令

30.2/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/17 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/Ckarefulon/dsh-codespace-workspace.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:待复验
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
装得上吗
曾在 dsh 0.2.0-rc.2 下真实安装成功,但当前 dsh 已更新至 0.2.1-rc.5,本站尚未在新版下复验
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 21 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 18 处,展示前 16 条)
  • 写入本地文件
    lib/host/codespaces.js:274
    mkdirSync(this.root, { recursive: true })
  • 读取本地文件
    lib/host/codespaces.js:280
    if (!existsSync(file)) return
  • 读取本地文件
    lib/host/codespaces.js:282
    const parsed = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/host/codespaces.js:305
    mkdirSync(this.root, { recursive: true })
  • 写入本地文件
    lib/host/codespaces.js:308
    writeFileSync(temporary, JSON.stringify({ version: 1, workspaces: [...this.records.values()] }, null, 2))
  • 写入本地文件
    lib/host/codespaces.js:310
    renameSync(temporary, file)
  • 写入本地文件
    lib/host/codespaces.js:756
    mkdirSync(dir, { recursive: true })
  • 删除本地文件递归
    lib/host/codespaces.js:858
    rmSync(record.path, { recursive: true, force: true })
  • 写入本地文件
    lib/host/codespaces.js:1055
    mkdirSync(this.root, { recursive: true })
  • 删除本地文件
    lib/host/codespaces.js:1068
    rmSync(this.pendingStopFile, { force: true })
  • 读取本地文件
    lib/host/codespaces.js:1081
    if (!existsSync(this.pendingStopFile)) return []
  • 读取本地文件
    lib/host/codespaces.js:1082
    const parsed = JSON.parse(readFileSync(this.pendingStopFile, 'utf8'))
  • 发起网络请求
    lib/host/github.js:271
    response = await fetch(url, init)
  • 读取密钥环境变量
    lib/host/github.js:341
    if (present(process.env.GITHUB_TOKEN)) return { token: process.env.GITHUB_TOKEN.trim(), source: 'env' }
  • 执行外部命令来自配置/变量
    lib/host/gh.js:44
    child = spawn(command, args, {
  • 执行外部命令来自配置/变量
    lib/host/transport.js:159
    child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

README

由 DeepSeek 最新模型翻译生成
dsh-codespace-workspace 把 GitHub Codespace 直接当作一个 DSH 工作区使用。本地 DSH 连接云端 Codespace,智能体在 Codespace 里读写文件、执行命令。 这不是通用的 SSH 远程连接插件:Codespace 就是工作区本身。它不往 DSH 的 SSH 主机列表里塞任何东西,因此不会和其他云工作区插件冲突。 ┌─────────────── 本地 DSH ───────────────┐ ┌────── GitHub Codespace ──────┐ │ 侧边栏工作区行(云朵图标) │ │ │ │ ▶️ / ⏸️ / 🕐 ← 启停 + 自动暂停倒计时 │ SSH │ /workspaces/<repo> │ │ │ ─────► │ read / write / edit │ │ Agent 会话(cwd = 占位目录) │ │ glob / grep / bash │ └────────────────────────────────────────┘ └──────────────────────────────┘ 功能 设置页:Codespace 在 DSH 设置里新增一栏 Codespace: | 设置项 | 说明 |
展开完整文档(README 剩余 1 节)
dsh-codespace-workspace

把 GitHub Codespace 直接当作一个 DSH 工作区使用。本地 DSH 连接云端 Codespace,智能体在 Codespace 里读写文件、执行命令。

这不是通用的 SSH 远程连接插件:Codespace 就是工作区本身。它不往 DSH 的 SSH 主机列表里塞任何东西,因此不会和其他云工作区插件冲突。

┌─────────────── 本地 DSH ───────────────┐ ┌────── GitHub Codespace ──────┐ │ 侧边栏工作区行(云朵图标) │ │ │ │ ▶️ / ⏸️ / 🕐 ← 启停 + 自动暂停倒计时 │ SSH │ /workspaces/<repo> │ │ │ ─────► │ read / write / edit │ │ Agent 会话(cwd = 占位目录) │ │ glob / grep / bash │ └────────────────────────────────────────┘ └──────────────────────────────┘

功能

设置页:Codespace

在 DSH 设置里新增一栏 Codespace:

| 设置项 | 说明 |
| GitHub 用户名 | 用于校验与展示;留空则从 token 读取登录名 |
| 默认分支 | 新建 Codespace 时使用,默认 main |
| 自动暂停等待时间(分钟) | 一轮对话结束后开始倒计时,默认 30 |
| SSH 密钥(可选) | 仅保存路径;值永不回显,只显示「已设置 / 未设置」 |
| 始终显示云端工作区按钮 | 开关,默认关;开启后按钮不再跟随 hover 隐藏 |
| 自动初始化空仓库 | 开关,默认开;空仓库先提交一个 README 再建 Codespace |
| README 初始内容 | 可选;留空则创建空 README |

设置通过 DSH 标准设置存储(Config + .volatile() + settings 服务)持久化,写在配置档的 cordis.patch.yml 里,不占用 localStorage,升级不丢。

工作区列表

- 新建云端工作区的按钮放在工作区分组标题栏的图标行里,与 DSH 原有的图标同地位,紧挨在「添加工作区」的左边——不是另做一个独立控件。
- 云端工作区沿用原工作区的文件夹图标位置,把那里的文件夹字形换成云朵:位置、尺寸、颜色,以及悬停时「让位给展开箭头」的行为都和原来一致,行首始终只有一个图标。云朵固定不变,不随 Codespace 状态变化。
- 操作按钮位于倒数第三个——在三个点菜单和新建对话按钮之前。
- 显示逻辑跟随宿主默认行为:不 hover 时隐藏;打开「始终显示」后常显。
- 正在使用的工作区,云朵和宿主的文件夹图标一样变蓝:宿主给「拥有当前会话」的那个工作区的图标槽加 folderActive,云朵就排在那个槽里,继承它的颜色,不自己定颜色。
- 按钮状态(四个图标都是照着宿主图标手绘的 SVG,不是 emoji;转圈用的是宿主自己的 IconLoadingOutline 圆弧,不是 CSS 方块环):

| 状态 | 图标 | 悬停提示 | 点击 |
| 已停止 | 播放 | 启动 Codespace | 启动 |
| 运行中 | 暂停 | 暂停 Codespace | 暂停 |
| 倒计时中 | 闹钟 | 还剩 X 分钟自动暂停 | 取消倒计时 |
| 启动/暂停中 | 转圈 | 正在切换状态… | 不可点 |

转圈只表示「状态正在变化」,不表示「智能体正在干活」——后者会让按钮整个回合都在转。

双击时钟图标 = 立即暂停。

工作区行的标题用仓库短名(owner/name 里的 name)——Codespace 自己的名字是机器生成的(psychic-goggles-7v9974jwpwr5fpqxq),看不出是哪个项目。标题装不下的两件事放在 hover 卡片里:那一行「工作区位置」显示 owner/name · Codespace 名字。

在 Codespace 会话里发消息时,会先等 Codespace 跑起来:如果它已经自动暂停(或还在启动),这一轮会等到 Available 才真正发给模型,而不是让每个工具调用都报连接错误。

新建云端工作区

只有一个窗口,分步显示,不弹一堆窗:

1. 拉取 GitHub 仓库列表,选择仓库
2. 检查该仓库的 Codespace:多个 → 选择;一个 → 直接用;没有 → 创建
3. 创建前检查 prebuild,三态而非二态:
- 有 → 「该仓库已配置预构建。」
- 明确没有 → 「未配置预构建,创建可能较慢」,给出 取消 / 直接创建
- 读不出来 → 「无法确认预构建状态,创建可能需要 1–3 分钟」,不给按钮(没有可决定的事)。这一态是刻意保留的:把插件自己读取失败说成「你仓库没配预构建」,是在陈述一件从未被证实的事实。
4. 空仓库 → 自动提交一个空 README,然后继续创建
5. 机器规格可选,默认最小
6. 启动失败 → 窗口内报错,可重试

生命周期

- DSH 启动时:不自动启动任何 Codespace。检查上次遗留的运行中 Codespace 并提示用户(带「停止」按钮),但不自动停。
- 退出 DSH 时:尝试自动关闭所有本插件管理的 Codespace。
- 自动暂停:一轮对话结束后开始倒计时;倒计时期间用户发新消息 → 取消倒计时,等智能体干完活重新计时;点时钟 → 取消;双击时钟 → 立即暂停。
- 发消息前保证机器在跑:挂 agent/pre-step(智能体循环在向模型要 step 之前会 await 这个 waterfall),发现 Codespace 没运行就先启动、等它变成 Available 再放行。启动失败或超时不会把这一轮卡死——照常放行,让模型看到真实的连接错误。等待期间你点「停止」/发送中断会立刻结束等待(转发的是这一轮的 abort signal)。

Git

智能体拥有完整的 git 命令执行能力。插件不干预 Git 操作,不封装 push 逻辑。 只在创建会话时向系统提示词注入一条规则,告诉智能体这是云端 Codespace 工作区、改动完成后请 git push 回仓库——不需要每次提醒。

多工作区

每个云端工作区独立控制自己的 Codespace 启停,互不干扰。

删除 Codespace

在三个点菜单里,与「删除工作区」分开。点击后弹确认框,确认后删除。

前置条件

| 依赖 | 必需 | 说明 |
| git | 是 | 智能体用它 push 回仓库 |
| ssh | 是 | 连接 Codespace 的数据通道 |
| gh(GitHub CLI) | 推荐 | 用于 gh codespace ssh;未安装时插件仍可用,改用你在设置里配置的 SSH 别名 |
| GitHub token | 是 | 控制面(REST API)。取值顺序:设置 → GITHUB_TOKEN → gh auth token |

本机现状:gh 未安装,git 与 ssh 已安装。插件会在设置页明确显示这一状态,并在缺少 gh 时回退到 SSH 别名,而不是直接失败。

所需 GitHub 权限范围:codespace、repo。

安装

1. 装进当前 profile(只改写 profile 的 package.json dependencies 与 lockfile)
dsh plugin --profile desktop add dsh-codespace-workspace

2. 把包名登记进 dsh.profile.bundles,否则不会被挂载编辑 %USERPROFILE%\.dsh\profiles\desktop\package.json 在 dsh.profile.bundles 数组里加入 "dsh-codespace-workspace"

3. 重启 DSH(插件文件改动不会热生效)

也可以从本地克隆装(改代码时用这个):

git clone <this-repo> dsh-codespace-workspace
dsh plugin --profile desktop add "link:<绝对路径>\dsh-codespace-workspace"

dsh plugin add 只改 dependencies 与 pnpm-lock.yaml,不会登记 dsh.profile.bundles,第 3 步必须手工做。它会报一条 peer dependency 警告(@deepseek-ai/cordis、@deepseek-ai/schemastery 由宿主提供),可以忽略。

装完可以确认插件已被解析并导入。注意 desktop profile 由 Electron 应用独占管理, CLI 对它只允许 dsh plugin,跑 --dump-config-schema 会报 profile "desktop" is managed exclusively by the Electron application。所以要用一个临时 profile 来验证(下面的 cscheck 只是举例,验证完删掉即可):

建一个只含 dsh-base 的临时 profile
dsh plugin --profile cscheck add "@deepseek-ai/dsh-base@0.2.0-rc.2"
dsh plugin --profile cscheck add "link:<绝对路径>\dsh-codespace-workspace"
手工把 "dsh-codespace-workspace" 加进该 profile 的 dsh.profile.bundles 然后 dump:它会真的 import() 每个插件的 main,并用其 Config 生成 JSON Schema
dsh --profile cscheck --dump-config-schema | Select-String dsh-codespace-workspace

输出里应出现 "const":"dsh-codespace-workspace" 指向一个 config<N> 定义,该定义包含全部 8 个设置字段、正确的默认值、"volatile":true,以及 githubToken 上的 "role":"secret"。这证明模块能加载、Config 合法。

确认无误后重启 DSH,设置页就会出现 Codespace。

架构

lib/
shared.js Host/Client 之间的冻结契约(动作名、状态映射、脱敏) index.js Host 入口:apply() / Config / inject / 装配 host/ github.js GitHub REST 客户端 + token 解析 gh.js gh CLI 探测与调用(缺失时优雅降级) codespaces.js 生命周期、机器规格、prebuild、空仓库、自动暂停计时器 transport.js 远程执行与文件读写(base64,无字符串插值) remote-tools.js 为 Codespace 会话注册远程工具(agent.ctx 作用域) session.js agent/created、提示词注入、倒计时、退出停止 routes.js RPC 路由 client.js 浏览器半边:设置页 + DOM 增强(标题栏按钮 / 行 / 菜单)+ 多步窗口
locale/{en,zh}.json 文案
test/
check.mjs Node 侧自检(不需要浏览器、token、gh) shutdown.mjs 「退出即停」的可靠性自检 e2e.mjs 载体端到端:真实 webserver + 真实 connection + 真实签名 cookie serve.mjs 浏览器自检的静态服务器 browser/ 浏览器自检:index.html + harness.js + react-lite.js

关键设计决策

1. 占位目录:Codespace 工作区在 DSH 自己的 registry 里登记为一个真实存在的本地目录 <DSH_HOME>/codespaces/<name>/(dsh-workspace 要求路径存在且可 realpath)。会话 cwd 就是这个路径,DSH 的工作区/会话机制原样工作。
2. 按 cwd 路由,不替换全局服务:任何会话只要 cwd 落在占位根目录下,就是 Codespace 会话。其余会话与其他插件完全不受影响。
3. 按 Agent 注册工具:在 agent/created 时,把远程实现注册到 agent.ctx,用 tools.restrict() 屏蔽本地对应工具。这是 DSH 支持的 per-agent 扩展点。
4. 一行提示词:只在会话创建时注入一条规则,不做每轮提醒。
5. 退出即停:退出时停止所有本插件管理的 Codespace。挂点是 ctx.effect 的 cleanup —— runProfile 的 dispose 会 await 根 fiber 的 fiber.dispose(),而 cordis 会并发 await 每一个 effect disposer(已核对 fiber.ts 的 _unload)。预算很紧:进程 5 秒后强退,崩溃路径只有 2 秒。因此 stopAll() 是幂等的、用 Promise.allSettled 并发发出、单个请求 3.5 秒超时、且绝不 reject(reject 的 disposer 会让 runProfile 抛 AggregateError)。没有停成功的名字写进 pending-stop.json,下次启动的「残留 Codespace」提示会一并列出。

验证

四套离线自检(不需要 token、gh 或真实 Codespace),加一套需要令牌的联网自检: bash

Node 侧:包清单与打包白名单、Config 默认值、RPC 动作表与 HTTP 守卫、

工作区命名、发消息前的就绪检查、shell 引用、文案一致性

node test/check.mjs # 181 项,全通过

退出即停:对着真实 CodespaceManager 跑,只把 fetch 换成桩

node test/shutdown.mjs # 31 项,全通过

载体端到端:起真实的 dsh-host-webserver + dsh-client-connection,

用真实签名 cookie 走真实 HTTP

node test/e2e.mjs # 全通过(找不到已安装的 Harness 时自动 SKIP)

浏览器侧:在真实 DOM 里跑真实 bundle,对着宿主 markup/CSS 的忠实副本

node test/serve.mjs # 然后打开 http://127.0.0.1:19501/

217 项,全通过

联网:用 gh 的令牌打真实 GitHub API(无令牌时自动 SKIP)

node test/live.mjs

test/live.mjs 是唯一会碰网络的一套,它存在的理由很具体:离线套件看不见本机那个 TLS 拦截故障——中间人根证书只在 Windows 系统信任库里,Node 的 fetch 只信自带 CA 包,于是所有 GitHub API 调用都变成 TypeError: fetch failed,而 gh 走 Go 读系统库完全正常。这套测试先量出「直连能不能用」,再断言控制面两条路都通,这样回退路径真的被走到时才算数。

test/e2e.mjs 是这套里最有价值的一个:它不驱动桩,而是从已安装的 Harness 里 import 真实的 dsh-host-webserver 与 dsh-client-connection,注册真实的路由,然后对真实监听的端口发真实 HTTP。它证明的正是桩测不了的三件事:

1. 路由能穿过 DSH 自己的 /api 前缀路由到达;
2. 没有会话 cookie 的请求被 harness 拒掉(401)——即路由没有遮住 fence 和鉴权;
3. 用真实 launch-token 换来的真实签名 cookie 能打通,拿到 {ok:true}。

它还当场演示了影子风险以证明自己有牙齿:故意在同路径注册一个 webServer exact 路由,匿名请求立刻从 401 变成 200(绕过了 fence);撤掉它,401 回来。所以「匿名必须是 401」这句断言对载体是敏感的——这正是之前那版实现栽的地方。

test/shutdown.mjs 覆盖的是「退出即停」这条最重要的可靠性要求——一个没停掉的 Codespace 会按小时计费,而用户看不见也补不回来。它断言的是真正要紧的性质:

- 每个受管 Codespace 都被要求停止,而且是并发发出(两个 300ms 的停止必须在 550ms 内跑完,串行会超)。
- 请求失败、连接被拒、API 收下连接但永不回复这三种情况下,disposer 都必须 resolve。reject 的 disposer 会让 runProfile 抛 AggregateError——那就从「有一个 Codespace 没停掉」变成「DSH 退不出去」,是更糟的失败模式。
- 最坏情况仍在 5 秒进程预算之内,且不早于单请求的 3.5 秒预算。
- 幂等:teardown 跑第二次不再发任何请求。
- 没停成功的名字会写进 pending-stop.json,下次启动的残留提示还能兜住。
- 没有 token 时不去发请求,而是把名字全部记成 pending。

其中「API 永不回复」这一例用的是真实 socket(本地起一个只收不回的 http server,把真实 init——含 signal——转给真实 fetch),不是桩。原因值得记:Node 里 AbortSignal.timeout() 的定时器是 unref'd 的,一个「返回永不 settle 的 promise」的桩不持有任何 ref'd handle,进程会直接退出而不是触发超时——用桩测这一条会得到假结果。已单独核实:真实 socket 下 api() 在 1215ms 抛 E_TIMEOUT。

浏览器侧覆盖的是 Node 里没法验证的部分:真实属性反射、:hover 驱动的 CSS、 MutationObserver 合并、portal 到 document.body 的菜单。它实际抓到过两个 bug:

- 时钟双击失效:倒计时行上,第一次点击会立刻发出「取消倒计时」(这是刻意的,因为那里双击没有别的含义),于是剩余时间归零;而 dblclick 的处理里原本用「剩余时间 > 0」做守卫,结果第二次点击被静默丢弃——需求里的「双击时钟 = 立即暂停」从来没生效过。守卫已改为判断状态而不是剩余时间。
- 运行中单击不生效:beginClickWindow 只清了计时器并重绘,从来没有真正发出被延后的那个动作,所以运行中的行单击一次什么都不会发生。现在窗口关闭时会按当前记录重新推导动作(轮询可能在这期间落地),并丢弃已经进入 busy 的记录。

另外它抓到过一个真实的时序问题:alwaysShowCloudButton 原本只在设置页挂载时才读取,因此全新启动时行增强读到的是 null,开关实际上不生效。现在由行增强模块自己在拿到 owner claim 时读一次设置。

真机(真实 GUI)上抓到的 bug

下面这些都不是桩能测出来的,必须对着跑起来的 DSH 才暴露;而且第一个的成因是我自己的测试把 bug 写进了断言里,所以它一直是绿的。

一、状态读取失败:Requests must be same-origin.

设置页的「状态」卡片一直红着,GitHub CLI (gh) 卡在「检查中…」。原因是 RPC 路由上那道自己手写的 Host/Origin fence 要求请求必须带 Origin,而桌面外壳的载体两者都不带(Origin 和 Fetch-Metadata 都没有),于是每一发都被 403。
Web GUI 反而正常,因为浏览器会发 Origin——这就是它看起来「只坏一半」的原因。

修法不是放宽自己那套,而是逐字对齐 harness 自己的 isTrustedApiRequest:它的第一条就是 if (origin === void 0) return true。承重的检查是 Host,因为 Host 是 DNS rebinding 唯一伪造不了的头;Origin 与 Sec-Fetch-Site 只是补充。
另外 isLoopbackHostname 接受 localhost、[::1] 和整个 127/8,不只是
127.0.0.1。

对着跑着的宿主复现过(旧代码):不带 Origin → 403,带上匹配的 Origin → 200。

二、我自己的路由遮住了 harness 的鉴权(安全相关)

修好 fence 之后顺手量了一下,发现更值得修的一处:路由原本注册成 webServer 的 exact 路由,而 webserver 的 match() 是先查 exact 表、再查前缀表,所以它比 DSH 自己的 /api 前缀路由先命中,直接把 /api 那条路整个遮掉了——而 fence 和浏览器鉴权(cookie)都挂在 /api 那条路上。实测(旧代码):

POST /api/dsh-codespace-workspace/rpc (无 cookie,Origin 匹配)-> 200 POST /api/no-such-plugin-xyz (无 cookie,Origin 匹配)-> 401 unauthorized

第二行是 DSH 在拒绝未鉴权调用;第一行说明我们的 exact 路由绕过了它。

正确做法是注册到 connection.fetch:DSH 自己的 /api 前缀路由会先 admit(req) (Host/Origin fence 加上 cookie 鉴权),再 bridge() 派发到插件注册的 Fetch 路由。这样等于白拿 harness 的安全策略,浏览器和桌面外壳还都能用。exact 路由现在只作为「composition 里根本没有 connection 服务」时的兜底保留。

两个细节值得记:

- connection 不在本模块的 inject 里,直接读 ready.connection 会被 cordis 门禁拦下(cannot get property ... without inject)。要用 ctx.get(name, false) ——cordis 文档里写明的「不经 inject 读取服务」的公开入口。
- 服务加载顺序没有保证,所以载体选择不能是一次性判断。ready.inject(['connection'], ...) 是公开的触发点(已实测:服务晚到时它会触发,永不出现时保持沉默且不抛),于是晚到的 connection 会把兜底路由换掉,而不是让进程一直停在较弱的载体上。

三、仓库读取失败:cannot reach the GitHub API (TypeError)

gh 装好、状态卡全绿之后,仓库列表依然读不出来。原因是本机有 TLS 拦截:中间人根证书只装在 Windows 系统信任库里,而 Node 的 fetch 只信自带的 CA 包,于是每次调用都失败——真正的成因藏在 error.cause.code = UNABLE_TO_VERIFY_LEAF_SIGNATURE,而旧代码只把 error.name 写进消息,于是用户看到的是一个毫无信息量的 TypeError。
gh 用 Go 读系统库,同一个请求完全正常,这才造成「令牌来自 gh auth token 却调不通 API」这种一半好的假象。

修法是给控制面加一条进程内的回退:tls.getCACertificates('system') 取到系统信任库
(本机 225 个证书),失败的那一发走 node:https 请求并带上 ca,结果包成真的 Response,所以下游的 status/headers.get/text() 全都不用改。没有用
tls.setDefaultCACertificates()——那会让本插件偷偷放宽整个 host 进程的 TLS 信任,影响到
别的插件;改动留在自己的传输层里。错误消息也改成报 cause 的 code,不再是 TypeError。
另有 NODE_OPTIONS=--use-system-ca 可用(一次修好所有 Node 代码),但它要改环境变量并重启 DSH,所以只作为备选写在这里。

四、启动图标的样式和旁边的按钮不一样

需求里那句「样式要和其他的等价」原来没做到:我的按钮做成了 16×16、radius-xs、hover 只变色,而标题栏真正用的 _9lTDKa_iconButton 是 28×28、radius-sm、--dsw-alias-label-secondary,hover 出现背景而不是变色。图标本身也偏重:我写了 stroke-width="1.3"(那是宿主 Medium 档)并且写在 path 上,而旁边的加号是 IconProjectAddOutlineRegular,即 1,且按 primitives 的规范 stroke-width 应该放在 上。

我一开始把 16×16 当成对的,是因为把 Rows 模块的 hIlkoa_iconButton(工作区行的按钮,确实是 16×16)错当成了标题栏的类——浏览器 harness 的夹具也照抄了这个错值,于是「launcher 和其它按钮一样大」这条断言一直是绿的。夹具已按真实 CSS 改正,并补上几何、圆角、颜色、hover 背景、描边宽度、rail 下字形 18px 的断言(浏览器侧从 107 项涨到 114 项)。云朵路径同时抽成一个常量,React 与 DOM 两条渲染路径共用,避免再次漂移。

五、图标会「有时候」跑到「新建工作区」按钮的右边

听起来像时序问题,其实是确定性的,只是触发条件不明显:只有宿主自己的提示框正开着时才会发生。原因是 DSH 的 Tooltip 在缺省配置下不是 portal(工作区标题栏没传 portal),所以气泡是以兄弟节点的形式渲染在同一个 headerActions 容器里的。我那段「把按钮插到最后一个子元素之前」的代码于是把按钮插到了气泡前面 —— 也就是「添加工作区」的右边。

更糟的是它不能自愈:一旦插错,按钮的 nextElementSibling 正是那段代码要去找的东西,于是每次重扫都「看起来已经就位」。现在改成记住锚点元素、永远贴在它前面,而不是每轮重新猜「最后一个控件」;选锚点只看标签名(BUTTON),不看子节点顺序。浏览器套件补了两条会真的触发它的回归测试:宿主重渲染追加控件、以及一个开着的提示框气泡。另外我自己那个提示框挂到 document.body,从根上不再进这个容器。

六、typeof 一个未声明的名字让提示框静默失效

改提示框时踩了个安静的坑:attachTooltip 是模块作用域的函数,而 doc 是 installRowAugmentation 的局部变量。我写了 typeof doc === 'undefined' 当守卫 —— 但 typeof 对未声明的标识符返回 'undefined' 而不抛错,于是这个函数每次都直接返回空操作:没有报错、console 干净、看起来一切正常,只是提示框永远不出现。

现在改成在函数内部解析 document(那是真实全局)。这条也写进了记忆:客户端 bundle 里绝不要用 typeof someUndeclaredName 当守卫。

七、图标补上了右上角的加号,云朵保持原尺寸、只在交叉处断开

「新建云端工作区」的图标原本是一个单独的云朵,而它旁边宿主自己的按钮是 IconProjectAddOutlineRegular(文件夹 + 加号)。现在这个图标是云朵 + 加号:加号逐字抄宿主那两条 path。

第一版我把云朵缩小塞到左下角、给加号让位。那不对 —— 宿主的做法是主体保持原尺寸,只把轮廓在加号穿过的地方断开(它的文件夹就是满尺寸,右上角被切开让加号坐进去)。现在照这个做:云朵用未修改的原 path,套一个 SVG <mask>,mask 里先把 16×16 的白底铺满,再用 stroke-width:2.6 的黑线沿着加号两条笔画走一遍 —— 于是云朵轮廓在加号经过的那一段被擦掉,云朵本身的尺寸一点没变。

几个具体取舍:

- 用 mask 而不是手算断点。这条轮廓与加号实际有两处相交(竖笔切右瓣于 y≈6.706,横笔的描边带还会蹭到大弧 x≈9.76 附近),手写子路径就得把这两组交点算准并永久跟着 path 同步;mask 直接表达「绕开加号」这个意图,不会漂移。
- 裁切笔画用 stroke-linecap="butt":圆头会多伸出半个带宽,在瓣的下端可能咬下一小块孤立的碎线。
- 每个图标一个独立 mask id(maskUnits="userSpaceOnUse")。id 是全文档唯一的,两个图标共用会让 url(#id) 解析到第一个,静默用错 mask。

校验上补了一条栅格化对照:克隆同一个图标、只摘掉 cloud path 上的 mask 属性,两张都画到 canvas 上逐像素比对 —— 差值就是被 mask 擦掉的那部分墨。这一点很重要,因为结构断言证明不了 mask 真的擦掉了东西:mask 颜色写错、maskUnits 漏了、id 撞了,都照样渲染成一朵完整的云。我前两版采样框写法都不稳(按「列」判空会被弧线在别的高度上的墨抵消;按固定矩形采样则会被抗锯齿边缘的零星像素和加号自身的墨干扰),最后换成这种不依赖几何假设的做法。

八、hover 提示框换成 DSH 那种

原来用的是原生 title,画出来是操作系统的气泡,和旁边宿主控件的完全不是一回事。现在照抄宿主 Tooltip 的渲染结果:position:fixed、真实的 tooltip 设计令牌、role="tooltip"、500ms 悬停延迟(宿主 delayMs: 500),并挂在 document.body 上。CSS 全部只引用 --dsw-alias-tooltip-bg / --dsw-static-neutral-bluish-00 这些主题变量,不硬编码任何颜色值。

九、预构建提示原本是二态,把「读不出来」说成了「你没配」

用户报「仓库明明配了预构建,却仍提示未配置」。查下来那一次插件是对的 —— 仓库当时确实没配好。但顺着这条线查,发现了一个真的缺陷,位置和我一开始猜的不一样。

先纠正我自己一个错判:我一度怀疑 prebuild_availability 对个人账号根本不填、导致永远误报。实测推翻了它 —— 我扫了 15 个仓库共 23 个机型行(含 microsoft/vscode、 python/cpython 这类确实配了预构建的):

null x23
"ready" x3 ← 只出现过这两种值

从来没有 "none",也没有 "in_progress"。所以「字段可读但为 null」*确实就是「没有预构建」,插件的 false 判断是站得住的、不需要改。

真正的缺陷在失败路径:客户端原来写的是 js
setPrebuild(data.hasPrebuild === true ? true : false) // null 被压成 false
.catch(() => setPrebuild(false)) // 失败也被压成 false

于是只要读取失败,就显示「未配置预构建」 —— 那是把插件自己的故障当成你仓库的事实来陈述。现在 hasPrebuild 的 true | false | null 三态一路传到 UI,null 单独渲染成「无法确认预构建状态,创建可能需要 1–3 分钟」,而且不给按钮(没有可决定的事,给了反而让人以为需要决策)。

顺带查出并修掉两个既有 bug:

- 机器步会直接崩溃。我删掉 needsNotice 时漏了「下一步」按钮的 disabled 还在引用它 —— 一进机器步就 ReferenceError。这是浏览器套件当场抓到的。
- 确认后反而说「已配置预构建」。点过「直接创建」之后,prebuildAcknowledged 会让渲染落到「该仓库已配置预构建。」那一支 —— 恰好说了与刚刚相反的话。现在确认后不再显示任何提示,而不是显示一句假的。

浏览器套件 136 → 147 项(后续又涨到 217,见下文「十一」「十三」),其中 8 项专测这三态(含「null 绝不能说成未配置」和「确认后绝不能说成已配置」两条否定断言)。

十、云端会话里本地工具根本没让位 —— 提示词说远程,手上还是本地

用户报「扫描工作区时环境与流程不一致」。顺着这条线查,抓到了这个插件最核心那条承诺上的一个真缺陷:installRemoteTools 从未真正执行过。

成因是开头这三行: js const tools = agentCtx?.tools;
if (tools === undefined) return () => {}; // 静默放弃

agent/created 那一刻的 agent.ctx 是作用域 ctx,cordis 对没有写进 inject 的服务本应门禁报错(cannot get property "x" without inject)——但这里偏偏不报。
ReflectService.handler.get 的最后三行是(已核对 @deepseek-ai/cordis lib/index.js:680): js
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false); // 退化成「不需要 inject」的读法

agent 作用域 ctx 没有运行中的 fiber,于是这次读取降级成 undefined 而不是抛错。
于是整个函数变成空操作:没有报错、console 干净、远程工具一个没注册、本地 read/write/edit/bash 全部照旧可用;而 session.js 在同一时刻已经把「你这个会话操作的是云端 Codespace」写进了系统提示词。两边就是这么对不上的。

DSH 自己的写法在 dsh-file-reference-local 里给得很清楚
(lib/index.js:342):agent.ctx.inject(["systemPrompt","tools"], (scope) => …)。
等工具服务就绪再安装是唯一正确的做法,现在也照这个改: js
fiber = agentCtx.inject(['tools'], (scope) => {
const tools = scope.tools // ← 只能从 scope 拿,不能从 agentCtx 拿…
})

关键细节是服务要从 inject 回调的 scope 参数上取:只有 inject 新建的那个 fiber 的 store 里才有 tools,agentCtx 上永远没有。收尾用 fiber.dispose()。

顺带澄清一处容易混的地方:上文「二」里 ctx.get(name, false) 那个「不经 inject 读服务」的口子,解决的是「服务可选、要探测在不在」的问题;而这里的问题是作用域 —— 注册远程工具要的是 agent 作用域里的那个 tools 实例,只有 inject 新建的那个 fiber 的 store 里有它。我没有去核实 get 能不能从 agent ctx 上摸到同一个实例,也没必要:inject 是 DSH 自己的做法,抄它就对了。

check 111 → 122 项,新增的 11 项专测这条接线,覆盖三种情形:工具已就绪、工具晚到、以及根本没有 agent.ctx。改完后我把源码退回旧写法反向验证了一次,两项立刻变红:

FAIL when tools arrives, the remote surface is installed -- got "", want "read,write,edit,glob,grep,bash"
FAIL -> and the local tools are hidden

这正是它值得存在的理由:一个「静默空操作」的 bug,只有故意让它失败一次,才能证明测试真的看得见它。

十一、行首多了一个图标,状态图标用的是 emoji

两处都是「看起来能用,但和旁边的东西不是一类」的问题。

(一)云朵被加在文件夹旁边,而不是替换它

需求写的是「替换原来工作区的文件夹图标,样式等相同」,我做成了「在行首再插一个云朵」,于是行首出现两个图标。

根因不是手滑,是读错了宿主的 CSS。Rows 模块里三条相关规则是: css .hIlkoa_projectRow .hIlkoa_chevron{display:none} / 平时:箭头藏起来 / .hIlkoa_projectRow:hover .hIlkoa_chevron{display:inline-flex} .hIlkoa_projectRow:hover .hIlkoa_folder{display:none} / 悬停时才轮到文件夹让位 /

我当时只看到最后一条,就把它当成了「文件夹在两种状态下都不可见」,于是得出「没有可见的文件夹可替换」的结论,选择另加一个。实际上文件夹在平时是看得见的,它是行的第一个子元素,悬停时才让位给展开箭头。

更糟的是,这个错误结论被夹具固化了:test/browser/index.html 里写着一条 css
.hIlkoa_folder { display: none; } / ← 宿主里根本没有这条规则 /

于是「文件夹本来就看不见」在测试里成了前提,所有断言都是绿的。这和上文「四」里把 16×16 当成 28×28 是同一类失误:夹具抄了一个错的模型,测试就成了 bug 的帮凶。

现在改成把云朵塞进宿主那个 span.slot.folder,并用内联 display:none 藏掉宿主自己的 svg(原 style 值连同标记一起存下,撤销时还原)。好处是位置、尺寸、颜色、以及「悬停让位给箭头」全部从宿主 CSS 继承,我一行都不用复刻。夹具里的伪规则已按真实 CSS 修正。

补充一个实现细节:文件夹在展开/折叠时会换 svg(IconFolderClose ↔ IconFolderOpen), React 会把节点换掉,所以每次 sync() 都要重新藏一遍「现在这个」;循环里要跳过我自己那个节点——它也是子元素,第一版没跳过,第二次 sync() 就会把云朵自己藏起来。

(二)状态图标是 emoji

四个状态原本用 ⏸️/🕐/⚠️/▶️。问题是 emoji 是平台字体的彩色字形,有自己的光学尺寸和粗细,它和旁边两个控件(Menu 的三点、IconNewChatOutline 的新建对话)——都是 16px、1px currentColor 描边、跟随主题色的 IconOutlineRegular——放在一起,就是一个更重、全彩、且每个系统渲染都不同的方块。它连主题色都不跟随。

现在四个图标都照抄宿主自己的 artwork(path 逐字复制,只挑用哪个字形):

| 状态 | 抄自 |
| 暂停/停止 | IconPauseOutlineRegular |
| 倒计时 | IconAlarmClockOutlineRegular |
| 异常 | IconWarningTriangleOutlineRegular |
| 启动 | IconPlayOutlineRegular |

抽了一个 iconNode(spec) 承载公共规范(16px 盒子、Regular 权重 = 1 写在 上、
fill="none" + stroke="currentColor"),但每个字形保留它自己的 viewBox 和线帽设置
——闹钟就是画在 17×17 网格上的,warning 三角形要用圆角线帽,这些都不能被"统一"掉。

顺带把多步窗口右上角的关闭按钮也一起改了:那里的 ✕ 是同一类问题——用文本字符当图标。现在换成宿主自己的 IconCloseOutlineRegular(两条对角线,逐字照抄)。全库扫过一遍,lib/client.js 里已经没有任何 emoji 当图标,只剩两条注释在举例说明旧实现。

浏览器套件 147 → 201 项。新增的不只是数数,而是能抓到这两类错误的断言:
- 云朵必须在 folder 槽里面,且 folder 槽仍是行首第一个子元素;
- 宿主的 svg 真的 display:none,云朵真的可见;
- 静止状态下整个行只有 1 个图标可见——这条否定了「加一个在旁边」;
- 承载云朵的 span 正是宿主 :hover ._folder{display:none} 规则所命中的那个(悬停行为等价);
- 每个字形逐条比对 viewBox / stroke-width 在 svg 上 / 每个 path 都是 currentColor 且都不带 stroke-width,外加一条「glyph 文本里不得含 emoji 码点」;
- 关闭按钮是 svg 而不是文本,artwork 与 IconCloseOutlineRegular 逐字一致,且整个窗口标题区不得出现任何 emoji 码点。

两类错误都反向验证过牙齿。把「替换」退回「在行首另加一个」,6 项变红,其中最关键的是

FAIL exactly one glyph is visible in the resting row — got 2, want 1

把关闭按钮退回 '✕',5 项变红,最后一条正是那条 emoji 扫描。

十二、icon.svg 没打进安装包,以及工作区名是一串乱码

(一)Plugin metadata ... ENOENT ... icon.svg

package.json 里有 "icon": "./icon.svg",仓库里也确实有这个文件(第一个提交就在),但 files 白名单里漏了它。files 是 npm/pnpm 的打包白名单,而这个 profile 是从 GitHub tarball 装的(github:Ckarefulon/dsh-codespace-workspace)—— 打包时按白名单过滤,icon.svg 就被丢掉了。DSH 读插件元信息时按 icon 字段去找它,lstat 失败。

关键在于仓库看起来是好的,只有打包后的副本是坏的:本地 git status 干净、文件就在根目录,所以怎么读源码都发现不了。这也是为什么现在加了一条自检:把 main、icon、每个 exports 目标、以及 dsh.bundle.patch 都拿去和 files 白名单比对,缺一个就红。
package.json 本身豁免(npm 无论如何都会打它)。

(二)工作区名是 psychic-goggles-7v9974jwpwr5fpqxq

那是 Codespace 自己的名字,GitHub 机器生成的,看不出是哪个项目。现在行标题取仓库短名(owner/name 里的 name),标题装不下的两件事放进 hover 卡片那一行: owner/name · Codespace 名字。
三处要一起改,否则会出现「新行对、老行还是乱码」或者「有 locale 服务时对、没有时显示原始 key」:

- titleForCodespace()(host 侧)决定新行的标题;
- listWorkspaces() 里加了一步回填,把已登记记录改成新标题 —— DSH 的 registry 会保留已存在记录的标题,不回填的话你现有的那一行永远还是旧名字;
- 客户端 hoverLocationOf() 拼 hover 那行。

这里还踩到一个自己造的坑:lib/client.js 里另有一份内联的 zh/en 字典(没有 locale 服务时用它,浏览器 harness 就是这种情况)。我只改了 locale/.json,于是浏览器套件里那行直接显示成了字面量 row.hoverRepo。原来的 locale 自检只比对「两个 JSON 文件之间」和「JSON 与 t() 调用之间」,从没比对过内联字典——现在补上了,两边键集合必须一致(nav 豁免,那是宿主读的)。

check 122 → 147 项(含 10 项 titleForCodespace、11 项打包白名单、2 项内联字典),浏览器套件 201 → 203 项。两处都反向验证过牙齿:把 icon.svg 从白名单里删掉 → 1 项变红;把 hover 那行退回只显示标签 → 1 项变红(got "my-box", want "Ckarefulon/app.ckarefulon.main · my-box")。

十三、云朵不变蓝、转圈停不下来、转圈是个方块,以及发消息不等机器

四处都是「照理说应该很明显」的问题,四处的原因各不相同。

(一)正在使用的工作区里,云朵不变蓝

宿主是这样标记「当前会话所在的工作区」的:给那一行的文件夹图标槽加一个 folderActive 类,规则是 .…_folderActive{color:var(--dsw-alias-state-business-primary)} —— 也就是变蓝。云朵本来就排在那同一个 span 里、stroke="currentColor",看起来应该自动继承。

问题是我自己给云朵的容器写了一行 color: var(--dsw-alias-label-secondary)。继承是继承,但显式声明会盖掉继承值,于是云朵永远是灰的。删掉那行就好——位置、尺寸、颜色、悬停让位全都该由宿主的 CSS 说了算。

这条还牵出一个夹具的老毛病(这是第三次了,前两次是 16×16 当 28×28、:hover 当无条件):夹具里 .hIlkoa_slot(设 label-tertiary)排在 .hIlkoa_folderActive 后面,而真实宿主里是反过来的。两个都是单类选择器,同优先级,所以后写的赢 —— 夹具里的 folderActive 根本不起作用,颜色测试一开始是红的。夹具的规则顺序已按宿主改正,并写了注释。

(二)「正在切换状态…」能转一整个回合

isBusy() 里有一行 if (record.busy === true) return true。而 record.busy 是host 在 user/message 时置位、在 turn/end 时清除的 —— 它的意思是「Agent 正在这个工作区里干活」,可以持续好几分钟。把它接进「该不该转圈」里,按钮就会在整个回合里转,还配着「正在切换状态…」这句错误的话,而 GitHub 那边的状态其实很明确。现在 isBusy 只认两件事:我们自己的 RPC 在飞,或者 GitHub 说它确实在过渡态(Provisioning/Starting/ShuttingDown…)。record.busy 保留在契约里,只是不再驱动这个按钮。

(三)转圈图标是个方块

原来是一个 CSS 环:border:2px solid; border-top-color:transparent; border-radius:50%。但宿主自己的加载指示器是 IconLoadingOutlineRegular —— 一段圆圆弧,配一个纯旋转动画(在 DSH 自己的 bundle 里核对过)。现在照抄它,于是转圈和那四个状态图标、以及旁边宿主的控件是同一套语言(16px、currentColor、1px 描边)。顺带补了 prefers-reduced-motion。

(四)发消息时没有等 Codespace 起来

Codespace 自动暂停之后,直接在会话里发消息,每个远程工具调用都会撞上连接错误。现在挂 agent/pre-step:Agent 循环在向模型要 step 之前会 await 这个 waterfall(dsh-agent-loop/lib/index.js ≈L911),所以在这里等是真的在推迟这一轮,而不是和它赛跑。DSH 自己的 archived-session-gate 用的是同一个钩子 —— 但它是拦住这一轮;这里要的是等,所以拿到结果后照常 next()。

几个克制的设计:本地会话完全不碰(cwd 判断在最前面);ensureRunning 永不抛错,失败就 {ok:false, reason} 然后照常放行(否则用户会看到一轮莫名其妙地什么都没发生,而不是一个明确的连接错误);等待受这一轮的 abort signal 约束,用户中断立刻结束;成功状态记 20 秒,所以一个回合最多查一次 GitHub,而不是每一步都查。

check 147 → 181 项(33 项新增:ensureRunning 的各种路径 + agent/pre-step 网关的行为),浏览器套件 203 → 217 项。三处都反向验证过牙齿:不启动机器 + 把过渡态当成就绪 → 6 项变红;给云朵加回颜色 + 让 busy 重新驱动转圈 → 8 项变红。

已知限制

- 工作区分组标题栏的图标行、以及工作区行,都没有官方插槽。DSH 把「添加工作区」按钮和 ProjectRowItem 的操作按钮、文件夹图标全部写死,因此以下四处通过 DOM 增强实现(与 dsh-pet 处理设置导航图标同一手法):标题栏里的新建按钮、行首云朵、行内启停按钮、hover 文案(外加菜单里的「删除 Codespace…」)。做法是全部打上 data-dsh-codespace- 标记、一个合并的 MutationObserver、dispose 时完整撤销,并且不删除任何 React 拥有的节点:行首的云朵把自己塞进宿主那个文件夹 span.slot 里,再把宿主自己的 svg 用内联 display:none 藏起来(连同原 style 一起登记,撤销时还原)。这样位置、尺寸、颜色和悬停让位全都从宿主的 CSS 继承,不需要我自己复刻。宿主改版可能影响它。已核对:客户端运行时的插槽目录共 90 个,其中有 sidebar.workspaces.session.menu.item,但没有工作区行的菜单插槽,也没有标题栏图标行的插槽;sidebar.footer.action 是真实存在的插槽,但它把控件放在侧边栏底部,不符合「与原有三个图标同地位、紧挨添加工作区左侧」的要求。标题栏的 _sectionHeader / _headerActions 两个类名后缀经核对为 dsh-client-ui-workspace 独有,不会误匹配;行首文件夹槽位按 _folder 后缀匹配,并限定在 span[class="_slot"] 上,避免误伤同模块的 _folderActive。
- 标题栏图标行是 justify-content:flex-end + max-width:60px,而标题栏的控件是 28×28(_9lTDKa_iconButton),所以 60px 正好放两个(28+4+28)。加进第三个控件需要放宽容器宽度,放宽规则带了 :not([class="_headerActionsHidden"]),以免盖掉宿主自己在展开搜索框时的收起行为。注意别把 Rows 模块的 hIlkoa_iconButton(16×16,工作区行的按钮)当成标题栏的类——两者同后缀不同模块,混淆过一次,见上文「四」。另外「添加工作区」按钮只在目录流插槽被占用时才渲染,所以插入位置是「最后一个子元素之前」而不是固定下标。窄栏(rail)下宿主把盒子放大到 36px、字形放大到 18px。
- 远程工具以文本方式传输文件(base64),大文件不适合;大仓库建议让 Agent 在 Codespace 内直接操作。
- gh 缺失时依赖用户自行配置 SSH 别名。设置项名为「SSH 密钥」,但私钥路径无法指定主机,所以该值被当作 ~/.ssh/config 的主机别名使用;填成路径会得到一条说明如何修正的错误,而不是把路径当主机名交给 ssh。
- 未在本机实测的部分:真实 SSH 数据面(没有已配置的 Codespace 目标),以及 Codespace 的创建/启停/删除这些会改云端状态的调用。控制面本身已联网实测:test/live.mjs 用 gh 的令牌打真实 GitHub API,getViewer / listRepos / listCodespaces 都通(本机 18 个仓库、0 个 Codespace)。命令拼装也已用真实 bash 验证:20 组逐字节往返 + 5 组注入载荷,均保持为单个 shell 词且无副作用。

「退出即停」不在此列:它的逻辑已按上面的 test/shutdown.mjs 实测(并发、不 reject、5 秒预算内、幂等、pending 落盘)。仍然未经实测的只有「真实 GitHub 是否接受这次 stop 调用」这一层——那取决于 token 与账号,不取决于本插件的逻辑。另外「宿主在退出时确实会 await 根 effect 的 disposer」这一条是读 DSH 源码核实的(runProfile 的 dispose → fiber.dispose() → cordis _unload 并发 await 每个 disposer),不是在本机跑一次退出观察到的;本机没有可复现的 Codespace 目标,无法做端到端退出观察。

发布(维护者)

包名 dsh-codespace-workspace 已发布到 npm。package.json 里写了 publishConfig(access: public + registry: https://registry.npmjs.org/),所以直接 npm publish 不带任何参数就会发到正确的地方——即使本机 ~/.npmrc 指向的是镜像站。

注意:这个包曾经带 "private": true(那时只从 GitHub 装)。npm 会以 EPRIVATE 直接拒绝发布,所以该字段已删除(不是改成 false)。test/check.mjs 里有一条断言专门盯着它「必须不存在」,复制回来会立刻变红。

后续版本自动发布:推送到 main 时 .github/workflows/publish.yml 会先跑测试再 npm publish——但只有 package.json 里的 version 是 registry 上还没有的版本时才发。所以发版流程就是两步:

1. 改 package.json 的 version。
2. 推送到 main。

这次运行要么发出这个版本,要么报告「该版本已在线上」并以成功结束。没改版本号的推送是 no-op,不是失败——所以每个要发出去的改动都必须自己带一次版本号变更;只改文档的提交同样会触发工作流,同样 no-op。

认证用 npm Trusted Publishing(OIDC):GitHub 给这个 job 签一个短时 token,npm 拿它换一个只对这一个包有效的发布 token。仓库里没有任何 NPM_TOKEN secret。 工作流刻意不用 actions/setup-node 的 registry-url 输入——它会往 ~/.npmrc 写 //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN},而该变量没定义时这个占位符会原样留下、被 npm 当作 token 发出去。registry 交给 publishConfig 就够了。

工作流在发布前会先 npm install 那两个 peer(@deepseek-ai/cordis、@deepseek-ai/schemastery):本仓库故意不 vendor 它们,而 test/check.mjs 是沿着 import 路径往上找的,装到仓库自己的 node_modules 里才能跑到真实模块。

一次性配置:绑定 Trusted Publisher Trusted Publisher 只能绑到 registry 上已存在的包,所以首次发布是手工做的(在检出目录里跑了一次 npm publish),从第二个版本起才轮到工作流接管。在 npmjs.com 上:包 → Settings → Trusted Publisher → GitHub Actions,填

| 字段 | 值 |
| Repository | Ckarefulon/dsh-codespace-workspace |
| Workflow filename | publish.yml |
| Environment | 留空 |

文件名必须逐字一致——npm 把授权绑在它上面,写错会表现为发布报 ENEEDAUTH。

文档

- docs/CONTRACT.md —— 实现契约:经过核实的 DSH API 事实、冻结的 RPC 契约、模块接口、以及每条未经核实项的处理分支。

许可

MIT
预览分享卡片长什么样 ↓Ckarefulon/dsh-codespace-workspace
分享 · DeepSeek Harness Hub
Ckarefulon/dsh-codespace-workspace

把 GitHub Codespace 当作本地工作区直接读写文件并执行命令

回到顶部