dsh-codespace-workspace 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +793 -2
- package/cordis.patch.yml +11 -0
- package/icon.svg +10 -0
- package/lib/client.js +3648 -0
- package/lib/host/codespaces.js +1100 -0
- package/lib/host/gh.js +271 -0
- package/lib/host/github.js +619 -0
- package/lib/host/remote-tools.js +565 -0
- package/lib/host/routes.js +555 -0
- package/lib/host/session.js +301 -0
- package/lib/host/transport.js +463 -0
- package/lib/index.js +170 -0
- package/lib/shared.js +181 -0
- package/locale/en.json +122 -0
- package/locale/zh.json +122 -0
- package/package.json +80 -4
package/README.md
CHANGED
|
@@ -1,3 +1,794 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-codespace-workspace
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把 **GitHub Codespace 直接当作一个 DSH 工作区**使用。本地 DSH 连接云端
|
|
4
|
+
Codespace,Agent 在 Codespace 里读写文件、执行命令。
|
|
5
|
+
|
|
6
|
+
这不是一个通用的 SSH 远程连接插件:Codespace 就是工作区本身。它不往 DSH 的
|
|
7
|
+
SSH 主机列表里塞任何东西,因此不会和其他云工作区插件冲突。
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────── 本地 DSH ───────────────┐ ┌────── GitHub Codespace ──────┐
|
|
11
|
+
│ 侧边栏工作区行(云朵图标) │ │ │
|
|
12
|
+
│ ▶️ / ⏸️ / 🕐 ← 启停 + 自动暂停倒计时 │ SSH │ /workspaces/<repo> │
|
|
13
|
+
│ │ ─────► │ read / write / edit │
|
|
14
|
+
│ Agent 会话(cwd = 占位目录) │ │ glob / grep / bash │
|
|
15
|
+
└────────────────────────────────────────┘ └──────────────────────────────┘
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 功能
|
|
19
|
+
|
|
20
|
+
### 设置页:Codespace
|
|
21
|
+
|
|
22
|
+
在 DSH 设置里新增一栏 **Codespace**:
|
|
23
|
+
|
|
24
|
+
| 设置项 | 说明 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| GitHub 用户名 | 用于校验与展示;留空则从 token 读取登录名 |
|
|
27
|
+
| 默认分支 | 新建 Codespace 时使用,默认 `main` |
|
|
28
|
+
| 自动暂停等待时间(分钟) | 一轮对话结束后开始倒计时,默认 30 |
|
|
29
|
+
| SSH 密钥(可选) | 仅保存路径;值永不回显,只显示「已设置 / 未设置」 |
|
|
30
|
+
| 始终显示云端工作区按钮 | 开关,默认关;开启后按钮不再跟随 hover 隐藏 |
|
|
31
|
+
| 自动初始化空仓库 | 开关,默认开;空仓库先提交一个 README 再建 Codespace |
|
|
32
|
+
| README 初始内容 | 可选;留空则创建空 README |
|
|
33
|
+
|
|
34
|
+
设置通过 DSH 标准设置存储(`Config` + `.volatile()` + settings 服务)持久化,
|
|
35
|
+
写在 profile 的 `cordis.patch.yml` 里,不占用 `localStorage`,升级不丢。
|
|
36
|
+
|
|
37
|
+
### 工作区列表
|
|
38
|
+
|
|
39
|
+
- 新建云端工作区的按钮放在**工作区分组标题栏的图标行里**,与 DSH 原有的图标
|
|
40
|
+
同地位,紧挨在「添加工作区」的**左边**——不是另做一个独立控件。
|
|
41
|
+
- 云端工作区**沿用原工作区的文件夹图标位置**,把那里的文件夹字形换成**云朵**:位置、
|
|
42
|
+
尺寸、颜色,以及悬停时「让位给展开箭头」的行为都和原来一致,行首始终只有一个图标。
|
|
43
|
+
云朵固定不变,不随 Codespace 状态变化。
|
|
44
|
+
- 操作按钮位于**倒数第三个**——在三个点菜单和新建对话按钮之前。
|
|
45
|
+
- 显示逻辑跟随宿主默认行为:不 hover 时隐藏;打开「始终显示」后常显。
|
|
46
|
+
- **正在使用的工作区,云朵和宿主的文件夹图标一样变蓝**:宿主给「拥有当前会话」的那个工作区
|
|
47
|
+
的图标槽加 `folderActive`,云朵就排在那个槽里,**继承**它的颜色,不自己定颜色。
|
|
48
|
+
- 按钮状态(四个图标都是**照着宿主图标手绘的 SVG**,不是 emoji;转圈用的是宿主自己的
|
|
49
|
+
`IconLoadingOutline` 圆弧,不是 CSS 方块环):
|
|
50
|
+
|
|
51
|
+
| 状态 | 图标 | 悬停提示 | 点击 |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| 已停止 | 播放 | 启动 Codespace | 启动 |
|
|
54
|
+
| 运行中 | 暂停 | 暂停 Codespace | 暂停 |
|
|
55
|
+
| 倒计时中 | 闹钟 | 还剩 X 分钟自动暂停 | 取消倒计时 |
|
|
56
|
+
| 启动/暂停中 | 转圈 | 正在切换状态… | 不可点 |
|
|
57
|
+
|
|
58
|
+
**转圈只表示「状态正在变化」**,不表示「Agent 正在干活」——后者会让按钮整个回合都在转。
|
|
59
|
+
|
|
60
|
+
**双击时钟图标 = 立即暂停。**
|
|
61
|
+
|
|
62
|
+
工作区行的标题用**仓库短名**(`owner/name` 里的 `name`)——Codespace 自己的名字是机器生成的
|
|
63
|
+
(`psychic-goggles-7v9974jwpwr5fpqxq`),看不出是哪个项目。标题装不下的两件事放在 hover
|
|
64
|
+
卡片里:那一行「工作区位置」显示 **`owner/name · Codespace 名字`**。
|
|
65
|
+
|
|
66
|
+
**在 Codespace 会话里发消息时,会先等 Codespace 跑起来**:如果它已经自动暂停(或还在启动),
|
|
67
|
+
这一轮会等到 `Available` 才真正发给模型,而不是让每个工具调用都报连接错误。
|
|
68
|
+
|
|
69
|
+
### 新建云端工作区
|
|
70
|
+
|
|
71
|
+
只有一个窗口,分步显示,不弹一堆窗:
|
|
72
|
+
|
|
73
|
+
1. 拉取 GitHub 仓库列表,选择仓库
|
|
74
|
+
2. 检查该仓库的 Codespace:多个 → 选择;一个 → 直接用;没有 → 创建
|
|
75
|
+
3. 创建前检查 prebuild,**三态**而非二态:
|
|
76
|
+
- 有 → 「该仓库已配置预构建。」
|
|
77
|
+
- 明确没有 → 「未配置预构建,创建可能较慢」,给出 `取消` / `直接创建`
|
|
78
|
+
- **读不出来** → 「无法确认预构建状态,创建可能需要 1–3 分钟」,不给按钮
|
|
79
|
+
(没有可决定的事)。这一态是刻意保留的:把插件自己读取失败说成「你仓库没配
|
|
80
|
+
预构建」,是在陈述一件从未被证实的事实。
|
|
81
|
+
4. 空仓库 → 自动提交一个空 README,然后继续创建
|
|
82
|
+
5. 机器规格可选,默认最小
|
|
83
|
+
6. 启动失败 → 窗口内报错,可重试
|
|
84
|
+
|
|
85
|
+
### 生命周期
|
|
86
|
+
|
|
87
|
+
- **DSH 启动时**:不自动启动任何 Codespace。检查上次遗留的运行中 Codespace 并
|
|
88
|
+
提示用户(带「停止」按钮),但不自动停。
|
|
89
|
+
- **退出 DSH 时**:尝试自动关闭所有本插件管理的 Codespace。
|
|
90
|
+
- **自动暂停**:一轮对话结束后开始倒计时;倒计时期间用户发新消息 → 取消倒计时,
|
|
91
|
+
等 Agent 干完活重新计时;点时钟 → 取消;双击时钟 → 立即暂停。
|
|
92
|
+
- **发消息前保证机器在跑**:挂 `agent/pre-step`(Agent 循环在向模型要 step 之前会
|
|
93
|
+
**await** 这个 waterfall),发现 Codespace 没运行就先启动、等它变成 `Available`
|
|
94
|
+
再放行。启动失败或超时不会把这一轮卡死——照常放行,让模型看到真实的连接错误。
|
|
95
|
+
等待期间你点「停止」/发送中断会立刻结束等待(转发的是这一轮的 abort signal)。
|
|
96
|
+
|
|
97
|
+
### Git
|
|
98
|
+
|
|
99
|
+
Agent 拥有完整的 `git` 命令执行能力。**插件不干预 Git 操作,不封装 push 逻辑。**
|
|
100
|
+
只在创建会话时向系统提示词注入一条规则,告诉 Agent 这是云端 Codespace 工作区、
|
|
101
|
+
改动完成后请 `git push` 回仓库——不需要每次提醒。
|
|
102
|
+
|
|
103
|
+
### 多工作区
|
|
104
|
+
|
|
105
|
+
每个云端工作区独立控制自己的 Codespace 启停,互不干扰。
|
|
106
|
+
|
|
107
|
+
### 删除 Codespace
|
|
108
|
+
|
|
109
|
+
在三个点菜单里,与「删除工作区」**分开**。点击后弹确认框,确认后删除。
|
|
110
|
+
|
|
111
|
+
## 前置条件
|
|
112
|
+
|
|
113
|
+
| 依赖 | 必需 | 说明 |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `git` | 是 | Agent 用它 push 回仓库 |
|
|
116
|
+
| `ssh` | 是 | 连接 Codespace 的数据通道 |
|
|
117
|
+
| `gh`(GitHub CLI) | 推荐 | 用于 `gh codespace ssh`;**未安装时插件仍可用**,改用你在设置里配置的 SSH 别名 |
|
|
118
|
+
| GitHub token | 是 | 控制面(REST API)。取值顺序:设置 → `GITHUB_TOKEN` → `gh auth token` |
|
|
119
|
+
|
|
120
|
+
> **本机现状**:`gh` 未安装,`git` 与 `ssh` 已安装。插件会在设置页明确显示这一
|
|
121
|
+
> 状态,并在缺少 `gh` 时回退到 SSH 别名,而不是直接失败。
|
|
122
|
+
|
|
123
|
+
所需 GitHub 权限范围:`codespace`、`repo`。
|
|
124
|
+
|
|
125
|
+
## 安装
|
|
126
|
+
|
|
127
|
+
```powershell
|
|
128
|
+
# 1. 装进当前 profile(只改写 profile 的 package.json dependencies 与 lockfile)
|
|
129
|
+
dsh plugin --profile desktop add dsh-codespace-workspace
|
|
130
|
+
|
|
131
|
+
# 2. 把包名登记进 dsh.profile.bundles,否则不会被挂载
|
|
132
|
+
# 编辑 %USERPROFILE%\.dsh\profiles\desktop\package.json
|
|
133
|
+
# 在 dsh.profile.bundles 数组里加入 "dsh-codespace-workspace"
|
|
134
|
+
|
|
135
|
+
# 3. 重启 DSH(插件文件改动不会热生效)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
也可以从本地克隆装(改代码时用这个):
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
git clone <this-repo> dsh-codespace-workspace
|
|
142
|
+
dsh plugin --profile desktop add "link:<绝对路径>\dsh-codespace-workspace"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`dsh plugin add` 只改 `dependencies` 与 `pnpm-lock.yaml`,**不会**登记
|
|
146
|
+
`dsh.profile.bundles`,第 3 步必须手工做。它会报一条 peer dependency 警告
|
|
147
|
+
(`@deepseek-ai/cordis`、`@deepseek-ai/schemastery` 由宿主提供),可以忽略。
|
|
148
|
+
|
|
149
|
+
装完可以确认插件已被解析并导入。注意 `desktop` profile 由 Electron 应用独占管理,
|
|
150
|
+
CLI 对它只允许 `dsh plugin`,跑 `--dump-config-schema` 会报
|
|
151
|
+
`profile "desktop" is managed exclusively by the Electron application`。所以要用一个
|
|
152
|
+
临时 profile 来验证(下面的 `cscheck` 只是举例,验证完删掉即可):
|
|
153
|
+
|
|
154
|
+
```powershell
|
|
155
|
+
# 建一个只含 dsh-base 的临时 profile
|
|
156
|
+
dsh plugin --profile cscheck add "@deepseek-ai/dsh-base@0.2.0-rc.2"
|
|
157
|
+
dsh plugin --profile cscheck add "link:<绝对路径>\dsh-codespace-workspace"
|
|
158
|
+
# 手工把 "dsh-codespace-workspace" 加进该 profile 的 dsh.profile.bundles
|
|
159
|
+
# 然后 dump:它会真的 import() 每个插件的 main,并用其 Config 生成 JSON Schema
|
|
160
|
+
dsh --profile cscheck --dump-config-schema | Select-String dsh-codespace-workspace
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
输出里应出现 `"const":"dsh-codespace-workspace"` 指向一个 `config<N>` 定义,
|
|
164
|
+
该定义包含全部 8 个设置字段、正确的默认值、`"volatile":true`,以及
|
|
165
|
+
`githubToken` 上的 `"role":"secret"`。这证明模块能加载、`Config` 合法。
|
|
166
|
+
|
|
167
|
+
确认无误后重启 DSH,设置页就会出现 **Codespace**。
|
|
168
|
+
|
|
169
|
+
## 架构
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
lib/
|
|
173
|
+
shared.js Host/Client 之间的冻结契约(动作名、状态映射、脱敏)
|
|
174
|
+
index.js Host 入口:apply() / Config / inject / 装配
|
|
175
|
+
host/
|
|
176
|
+
github.js GitHub REST 客户端 + token 解析
|
|
177
|
+
gh.js gh CLI 探测与调用(缺失时优雅降级)
|
|
178
|
+
codespaces.js 生命周期、机器规格、prebuild、空仓库、自动暂停计时器
|
|
179
|
+
transport.js 远程执行与文件读写(base64,无字符串插值)
|
|
180
|
+
remote-tools.js 为 Codespace 会话注册远程工具(agent.ctx 作用域)
|
|
181
|
+
session.js agent/created、提示词注入、倒计时、退出停止
|
|
182
|
+
routes.js RPC 路由
|
|
183
|
+
client.js 浏览器半边:设置页 + DOM 增强(标题栏按钮 / 行 / 菜单)+ 多步窗口
|
|
184
|
+
locale/{en,zh}.json 文案
|
|
185
|
+
test/
|
|
186
|
+
check.mjs Node 侧自检(不需要浏览器、token、gh)
|
|
187
|
+
shutdown.mjs 「退出即停」的可靠性自检
|
|
188
|
+
e2e.mjs 载体端到端:真实 webserver + 真实 connection + 真实签名 cookie
|
|
189
|
+
serve.mjs 浏览器自检的静态服务器
|
|
190
|
+
browser/ 浏览器自检:index.html + harness.js + react-lite.js
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 关键设计决策
|
|
194
|
+
|
|
195
|
+
1. **占位目录**:Codespace 工作区在 DSH 自己的 registry 里登记为一个真实存在的
|
|
196
|
+
本地目录 `<DSH_HOME>/codespaces/<name>/`(`dsh-workspace` 要求路径存在且可
|
|
197
|
+
`realpath`)。会话 `cwd` 就是这个路径,DSH 的工作区/会话机制原样工作。
|
|
198
|
+
2. **按 cwd 路由,不替换全局服务**:任何会话只要 `cwd` 落在占位根目录下,就是
|
|
199
|
+
Codespace 会话。其余会话与其他插件完全不受影响。
|
|
200
|
+
3. **按 Agent 注册工具**:在 `agent/created` 时,把远程实现注册到 `agent.ctx`,
|
|
201
|
+
用 `tools.restrict()` 屏蔽本地对应工具。这是 DSH 支持的 per-agent 扩展点。
|
|
202
|
+
4. **一行提示词**:只在会话创建时注入一条规则,不做每轮提醒。
|
|
203
|
+
5. **退出即停**:退出时停止所有本插件管理的 Codespace。挂点是 `ctx.effect` 的
|
|
204
|
+
cleanup —— `runProfile` 的 dispose 会 await 根 fiber 的 `fiber.dispose()`,而
|
|
205
|
+
cordis 会并发 await 每一个 effect disposer(已核对 `fiber.ts` 的 `_unload`)。
|
|
206
|
+
预算很紧:进程 5 秒后强退,崩溃路径只有 2 秒。因此 `stopAll()` 是幂等的、用
|
|
207
|
+
`Promise.allSettled` 并发发出、单个请求 3.5 秒超时、且**绝不 reject**(reject 的
|
|
208
|
+
disposer 会让 `runProfile` 抛 `AggregateError`)。没有停成功的名字写进
|
|
209
|
+
`pending-stop.json`,下次启动的“残留 Codespace”提示会一并列出。
|
|
210
|
+
|
|
211
|
+
## 验证
|
|
212
|
+
|
|
213
|
+
四套离线自检(不需要 token、`gh` 或真实 Codespace),加一套需要令牌的联网自检:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Node 侧:包清单与打包白名单、Config 默认值、RPC 动作表与 HTTP 守卫、
|
|
217
|
+
# 工作区命名、发消息前的就绪检查、shell 引用、文案一致性
|
|
218
|
+
node test/check.mjs # 181 项,全通过
|
|
219
|
+
|
|
220
|
+
# 退出即停:对着真实 CodespaceManager 跑,只把 fetch 换成桩
|
|
221
|
+
node test/shutdown.mjs # 31 项,全通过
|
|
222
|
+
|
|
223
|
+
# 载体端到端:起真实的 dsh-host-webserver + dsh-client-connection,
|
|
224
|
+
# 用真实签名 cookie 走真实 HTTP
|
|
225
|
+
node test/e2e.mjs # 全通过(找不到已安装的 Harness 时自动 SKIP)
|
|
226
|
+
|
|
227
|
+
# 浏览器侧:在真实 DOM 里跑真实 bundle,对着宿主 markup/CSS 的忠实副本
|
|
228
|
+
node test/serve.mjs # 然后打开 http://127.0.0.1:19501/
|
|
229
|
+
# 217 项,全通过
|
|
230
|
+
|
|
231
|
+
# 联网:用 gh 的令牌打真实 GitHub API(无令牌时自动 SKIP)
|
|
232
|
+
node test/live.mjs
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`test/live.mjs` 是唯一会碰网络的一套,它存在的理由很具体:离线套件看不见本机那个
|
|
236
|
+
**TLS 拦截**故障——中间人根证书只在 Windows 系统信任库里,Node 的 `fetch` 只信自带
|
|
237
|
+
CA 包,于是所有 GitHub API 调用都变成 `TypeError: fetch failed`,而 `gh` 走 Go 读系统
|
|
238
|
+
库完全正常。这套测试先量出「直连能不能用」,再断言控制面**两条路都通**,这样回退路径
|
|
239
|
+
真的被走到时才算数。
|
|
240
|
+
|
|
241
|
+
`test/e2e.mjs` 是这套里最有价值的一个:它不驱动桩,而是从已安装的 Harness 里
|
|
242
|
+
`import` **真实的** `dsh-host-webserver` 与 `dsh-client-connection`,注册真实的
|
|
243
|
+
路由,然后对真实监听的端口发真实 HTTP。它证明的正是桩测不了的三件事:
|
|
244
|
+
|
|
245
|
+
1. 路由能穿过 DSH 自己的 `/api` 前缀路由到达;
|
|
246
|
+
2. **没有会话 cookie 的请求被 harness 拒掉(401)**——即路由没有遮住 fence 和鉴权;
|
|
247
|
+
3. 用真实 launch-token 换来的**真实签名 cookie** 能打通,拿到 `{ok:true}`。
|
|
248
|
+
|
|
249
|
+
它还**当场演示了影子风险**以证明自己有牙齿:故意在同路径注册一个 `webServer`
|
|
250
|
+
exact 路由,匿名请求立刻从 401 变成 200(绕过了 fence);撤掉它,401 回来。
|
|
251
|
+
所以「匿名必须是 401」这句断言对载体是敏感的——这正是之前那版实现栽的地方。
|
|
252
|
+
|
|
253
|
+
`test/shutdown.mjs` 覆盖的是「退出即停」这条最重要的可靠性要求——一个没停掉的
|
|
254
|
+
Codespace 会按小时计费,而用户看不见也补不回来。它断言的是真正要紧的性质:
|
|
255
|
+
|
|
256
|
+
- 每个受管 Codespace 都被要求停止,而且是**并发**发出(两个 300ms 的停止必须在
|
|
257
|
+
550ms 内跑完,串行会超)。
|
|
258
|
+
- 请求失败、连接被拒、**API 收下连接但永不回复**这三种情况下,disposer 都**必须
|
|
259
|
+
resolve**。reject 的 disposer 会让 `runProfile` 抛 `AggregateError`——那就从
|
|
260
|
+
「有一个 Codespace 没停掉」变成「DSH 退不出去」,是更糟的失败模式。
|
|
261
|
+
- 最坏情况仍在 5 秒进程预算之内,且不早于单请求的 3.5 秒预算。
|
|
262
|
+
- 幂等:teardown 跑第二次不再发任何请求。
|
|
263
|
+
- 没停成功的名字会写进 `pending-stop.json`,下次启动的残留提示还能兜住。
|
|
264
|
+
- 没有 token 时不去发请求,而是把名字全部记成 pending。
|
|
265
|
+
|
|
266
|
+
其中「API 永不回复」这一例用的是**真实 socket**(本地起一个只收不回的
|
|
267
|
+
`http` server,把真实 `init`——含 signal——转给真实 `fetch`),不是桩。原因值得记:
|
|
268
|
+
Node 里 `AbortSignal.timeout()` 的定时器是 **unref'd** 的,一个「返回永不 settle 的
|
|
269
|
+
promise」的桩不持有任何 ref'd handle,进程会直接退出而不是触发超时——用桩测这一条
|
|
270
|
+
会得到假结果。已单独核实:真实 socket 下 `api()` 在 1215ms 抛 `E_TIMEOUT`。
|
|
271
|
+
|
|
272
|
+
浏览器侧覆盖的是 Node 里没法验证的部分:真实属性反射、`:hover` 驱动的 CSS、
|
|
273
|
+
`MutationObserver` 合并、portal 到 `document.body` 的菜单。它实际抓到过两个 bug:
|
|
274
|
+
|
|
275
|
+
- **时钟双击失效**:倒计时行上,第一次点击会立刻发出「取消倒计时」(这是刻意的,
|
|
276
|
+
因为那里双击没有别的含义),于是剩余时间归零;而 `dblclick` 的处理里原本用
|
|
277
|
+
「剩余时间 > 0」做守卫,结果第二次点击被静默丢弃——需求里的「双击时钟 = 立即暂停」
|
|
278
|
+
从来没生效过。守卫已改为判断**状态**而不是剩余时间。
|
|
279
|
+
- **运行中单击不生效**:`beginClickWindow` 只清了计时器并重绘,从来没有真正发出
|
|
280
|
+
被延后的那个动作,所以运行中的行单击一次什么都不会发生。现在窗口关闭时会按
|
|
281
|
+
**当前**记录重新推导动作(轮询可能在这期间落地),并丢弃已经进入 busy 的记录。
|
|
282
|
+
|
|
283
|
+
另外它抓到过一个真实的时序问题:`alwaysShowCloudButton` 原本只在设置页挂载时才
|
|
284
|
+
读取,因此全新启动时行增强读到的是 `null`,开关实际上不生效。现在由行增强模块
|
|
285
|
+
自己在拿到 owner claim 时读一次设置。
|
|
286
|
+
|
|
287
|
+
### 真机(真实 GUI)上抓到的 bug
|
|
288
|
+
|
|
289
|
+
下面这些都不是桩能测出来的,必须对着跑起来的 DSH 才暴露;而且第一个的成因是
|
|
290
|
+
**我自己的测试把 bug 写进了断言里**,所以它一直是绿的。
|
|
291
|
+
|
|
292
|
+
**一、`状态读取失败:Requests must be same-origin.`**
|
|
293
|
+
|
|
294
|
+
设置页的「状态」卡片一直红着,`GitHub CLI (gh)` 卡在「检查中…」。原因是 RPC
|
|
295
|
+
路由上那道**自己手写的** Host/Origin fence 要求请求必须带 `Origin`,而桌面外壳
|
|
296
|
+
的载体**两者都不带**(`Origin` 和 Fetch-Metadata 都没有),于是每一发都被 403。
|
|
297
|
+
Web GUI 反而正常,因为浏览器会发 `Origin`——这就是它看起来「只坏一半」的原因。
|
|
298
|
+
|
|
299
|
+
修法不是放宽自己那套,而是**逐字对齐 harness 自己的 `isTrustedApiRequest`**:它
|
|
300
|
+
的第一条就是 `if (origin === void 0) return true`。承重的检查是 **Host**,因为
|
|
301
|
+
`Host` 是 DNS rebinding 唯一伪造不了的头;`Origin` 与 `Sec-Fetch-Site` 只是补充。
|
|
302
|
+
另外 `isLoopbackHostname` 接受 `localhost`、`[::1]` 和**整个 127/8**,不只是
|
|
303
|
+
`127.0.0.1`。
|
|
304
|
+
|
|
305
|
+
对着跑着的宿主复现过(旧代码):不带 `Origin` → 403,带上匹配的 `Origin` → 200。
|
|
306
|
+
|
|
307
|
+
**二、我自己的路由遮住了 harness 的鉴权(安全相关)**
|
|
308
|
+
|
|
309
|
+
修好 fence 之后顺手量了一下,发现更值得修的一处:路由原本注册成 `webServer` 的
|
|
310
|
+
**exact** 路由,而 webserver 的 `match()` 是**先查 exact 表、再查前缀表**,所以它
|
|
311
|
+
比 DSH 自己的 `/api` **前缀**路由先命中,直接把 `/api` 那条路整个遮掉了——而
|
|
312
|
+
fence 和浏览器鉴权(cookie)都挂在 `/api` 那条路上。实测(旧代码):
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
POST /api/dsh-codespace-workspace/rpc (无 cookie,Origin 匹配)-> 200
|
|
316
|
+
POST /api/no-such-plugin-xyz (无 cookie,Origin 匹配)-> 401 unauthorized
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
第二行是 DSH 在拒绝未鉴权调用;第一行说明我们的 exact 路由绕过了它。
|
|
320
|
+
|
|
321
|
+
正确做法是注册到 `connection.fetch`:DSH 自己的 `/api` 前缀路由会先 `admit(req)`
|
|
322
|
+
(Host/Origin fence **加上** cookie 鉴权),再 `bridge()` 派发到插件注册的 Fetch
|
|
323
|
+
路由。这样等于白拿 harness 的安全策略,浏览器和桌面外壳还都能用。exact 路由现在
|
|
324
|
+
只作为「composition 里根本没有 `connection` 服务」时的兜底保留。
|
|
325
|
+
|
|
326
|
+
两个细节值得记:
|
|
327
|
+
|
|
328
|
+
- `connection` 不在本模块的 `inject` 里,直接读 `ready.connection` 会被 cordis
|
|
329
|
+
门禁拦下(`cannot get property ... without inject`)。要用 `ctx.get(name, false)`
|
|
330
|
+
——cordis 文档里写明的「不经 inject 读取服务」的公开入口。
|
|
331
|
+
- 服务加载顺序没有保证,所以载体选择**不能是一次性判断**。`ready.inject(['connection'], ...)`
|
|
332
|
+
是公开的触发点(已实测:服务晚到时它会触发,永不出现时保持沉默且不抛),
|
|
333
|
+
于是晚到的 `connection` 会把兜底路由换掉,而不是让进程一直停在较弱的载体上。
|
|
334
|
+
|
|
335
|
+
**三、`仓库读取失败:cannot reach the GitHub API (TypeError)`**
|
|
336
|
+
|
|
337
|
+
`gh` 装好、状态卡全绿之后,仓库列表依然读不出来。原因是**本机有 TLS 拦截**:中间人
|
|
338
|
+
根证书只装在 Windows 系统信任库里,而 Node 的 `fetch` 只信**自带**的 CA 包,于是每次
|
|
339
|
+
调用都失败——真正的成因藏在 `error.cause.code = UNABLE_TO_VERIFY_LEAF_SIGNATURE`,
|
|
340
|
+
而旧代码只把 `error.name` 写进消息,于是用户看到的是一个毫无信息量的 `TypeError`。
|
|
341
|
+
`gh` 用 Go 读系统库,同一个请求完全正常,这才造成「令牌来自 gh auth token 却调不通
|
|
342
|
+
API」这种一半好的假象。
|
|
343
|
+
|
|
344
|
+
修法是给控制面加一条**进程内**的回退:`tls.getCACertificates('system')` 取到系统信任库
|
|
345
|
+
(本机 225 个证书),失败的那一发改走 `node:https` 请求并带上 `ca`,结果包成真的
|
|
346
|
+
`Response`,所以下游的 `status`/`headers.get`/`text()` 全都不用改。**没有**用
|
|
347
|
+
`tls.setDefaultCACertificates()`——那会让本插件偷偷放宽整个 host 进程的 TLS 信任,影响到
|
|
348
|
+
别的插件;改动留在自己的传输层里。错误消息也改成报 `cause` 的 code,不再是 `TypeError`。
|
|
349
|
+
|
|
350
|
+
另有 `NODE_OPTIONS=--use-system-ca` 可用(一次修好所有 Node 代码),但它要改环境变量并
|
|
351
|
+
重启 DSH,所以只作为备选写在这里。
|
|
352
|
+
|
|
353
|
+
**四、启动图标的样式和旁边的按钮不一样**
|
|
354
|
+
|
|
355
|
+
需求里那句「样式要和其他的等价」原来没做到:我的按钮做成了 **16×16**、`radius-xs`、
|
|
356
|
+
hover 只变色,而标题栏真正用的 `_9lTDKa_iconButton` 是 **28×28**、`radius-sm`、
|
|
357
|
+
`--dsw-alias-label-secondary`,**hover 出现背景**而不是变色。图标本身也偏重:我写了
|
|
358
|
+
`stroke-width="1.3"`(那是宿主 `Medium` 档)**并且写在 path 上**,而旁边的加号是
|
|
359
|
+
`IconProjectAddOutlineRegular`,即 **1**,且按 primitives 的规范 `stroke-width` 应该放在
|
|
360
|
+
`<svg>` 上。
|
|
361
|
+
|
|
362
|
+
我一开始把 16×16 当成对的,是因为把 `Rows` 模块的 `hIlkoa_iconButton`(工作区行的按钮,
|
|
363
|
+
确实是 16×16)错当成了标题栏的类——**浏览器 harness 的夹具也照抄了这个错值**,于是
|
|
364
|
+
「launcher 和其它按钮一样大」这条断言一直是绿的。夹具已按真实 CSS 改正,并补上几何、
|
|
365
|
+
圆角、颜色、hover 背景、描边宽度、rail 下字形 18px 的断言(浏览器侧从 107 项涨到 114 项)。
|
|
366
|
+
云朵路径同时抽成一个常量,React 与 DOM 两条渲染路径共用,避免再次漂移。
|
|
367
|
+
|
|
368
|
+
**五、图标会「有时候」跑到「新建工作区」按钮的右边**
|
|
369
|
+
|
|
370
|
+
听起来像时序问题,其实是**确定性**的,只是触发条件不明显:**只有宿主自己的提示框
|
|
371
|
+
正开着时才会发生**。原因是 DSH 的 `Tooltip` 在缺省配置下**不是 portal**(工作区标题栏
|
|
372
|
+
没传 `portal`),所以气泡是以**兄弟节点**的形式渲染在**同一个 `headerActions` 容器**里
|
|
373
|
+
的。我那段「把按钮插到最后一个子元素之前」的代码于是把按钮插到了气泡前面 —— 也就是
|
|
374
|
+
「添加工作区」的右边。
|
|
375
|
+
|
|
376
|
+
更糟的是它**不能自愈**:一旦插错,按钮的 `nextElementSibling` 正是那段代码要去找的
|
|
377
|
+
东西,于是每次重扫都「看起来已经就位」。现在改成**记住锚点元素**、永远贴在它前面,
|
|
378
|
+
而不是每轮重新猜「最后一个控件」;选锚点只看标签名(`BUTTON`),不看子节点顺序。
|
|
379
|
+
浏览器套件补了两条会真的触发它的回归测试:宿主重渲染追加控件、以及一个开着的提示框
|
|
380
|
+
气泡。另外我自己那个提示框**挂到 `document.body`**,从根上不再进这个容器。
|
|
381
|
+
|
|
382
|
+
**六、`typeof` 一个未声明的名字让提示框静默失效**
|
|
383
|
+
|
|
384
|
+
改提示框时踩了个安静的坑:`attachTooltip` 是模块作用域的函数,而 `doc` 是
|
|
385
|
+
`installRowAugmentation` 的**局部变量**。我写了 `typeof doc === 'undefined'` 当守卫 ——
|
|
386
|
+
但 `typeof` 对**未声明的标识符**返回 `'undefined'` **而不抛错**,于是这个函数每次都直接
|
|
387
|
+
返回空操作:**没有报错、console 干净、看起来一切正常,只是提示框永远不出现**。
|
|
388
|
+
|
|
389
|
+
现在改成在函数内部解析 `document`(那是真实全局)。这条也写进了记忆:客户端 bundle 里
|
|
390
|
+
绝不要用 `typeof someUndeclaredName` 当守卫。
|
|
391
|
+
|
|
392
|
+
**七、图标补上了右上角的加号,云朵保持原尺寸、只在交叉处断开**
|
|
393
|
+
|
|
394
|
+
「新建云端工作区」的图标原本是一个单独的云朵,而它旁边宿主自己的按钮是
|
|
395
|
+
`IconProjectAddOutlineRegular`(文件夹 + 加号)。现在这个图标是**云朵 + 加号**:加号
|
|
396
|
+
**逐字抄**宿主那两条 path。
|
|
397
|
+
|
|
398
|
+
第一版我把云朵**缩小**塞到左下角、给加号让位。那不对 —— 宿主的做法是主体**保持原尺寸**,
|
|
399
|
+
只把轮廓在加号穿过的地方**断开**(它的文件夹就是满尺寸,右上角被切开让加号坐进去)。
|
|
400
|
+
现在照这个做:云朵用**未修改的原 path**,套一个 SVG `<mask>`,mask 里先把 16×16 的白底
|
|
401
|
+
铺满,再用 `stroke-width:2.6` 的**黑线**沿着加号两条笔画走一遍 —— 于是云朵轮廓在加号
|
|
402
|
+
经过的那一段被擦掉,云朵本身的尺寸一点没变。
|
|
403
|
+
|
|
404
|
+
几个具体取舍:
|
|
405
|
+
|
|
406
|
+
- 用 mask 而不是手算断点。这条轮廓与加号实际有**两处**相交(竖笔切右瓣于 y≈6.706,横笔
|
|
407
|
+
的**描边带**还会蹭到大弧 x≈9.76 附近),手写子路径就得把这两组交点算准并永久跟着
|
|
408
|
+
path 同步;mask 直接表达「绕开加号」这个意图,不会漂移。
|
|
409
|
+
- 裁切笔画用 `stroke-linecap="butt"`:圆头会多伸出半个带宽,在瓣的下端可能咬下一小块
|
|
410
|
+
孤立的碎线。
|
|
411
|
+
- 每个图标一个独立 mask id(`maskUnits="userSpaceOnUse"`)。id 是全文档唯一的,两个
|
|
412
|
+
图标共用会让 `url(#id)` 解析到第一个,静默用错 mask。
|
|
413
|
+
|
|
414
|
+
校验上补了一条**栅格化对照**:克隆同一个图标、只摘掉 cloud path 上的 `mask` 属性,
|
|
415
|
+
两张都画到 canvas 上逐像素比对 —— 差值就是被 mask 擦掉的那部分墨。这一点很重要,因为
|
|
416
|
+
**结构断言证明不了 mask 真的擦掉了东西**:mask 颜色写错、`maskUnits` 漏了、id 撞了,
|
|
417
|
+
都照样渲染成一朵完整的云。我前两版采样框写法都不稳(按「列」判空会被弧线在别的高度上
|
|
418
|
+
的墨抵消;按固定矩形采样则会被抗锯齿边缘的零星像素和加号自身的墨干扰),最后换成这种
|
|
419
|
+
不依赖几何假设的做法。
|
|
420
|
+
|
|
421
|
+
**八、hover 提示框换成 DSH 那种**
|
|
422
|
+
|
|
423
|
+
原来用的是原生 `title`,画出来是操作系统的气泡,和旁边宿主控件的完全不是一回事。现在
|
|
424
|
+
照抄宿主 Tooltip 的**渲染结果**:`position:fixed`、真实的 tooltip 设计令牌、
|
|
425
|
+
`role="tooltip"`、500ms 悬停延迟(宿主 `delayMs: 500`),并挂在 `document.body` 上。
|
|
426
|
+
CSS 全部只引用 `--dsw-alias-tooltip-bg` / `--dsw-static-neutral-bluish-00` 这些主题
|
|
427
|
+
变量,不硬编码任何颜色值。
|
|
428
|
+
|
|
429
|
+
**九、预构建提示原本是二态,把「读不出来」说成了「你没配」**
|
|
430
|
+
|
|
431
|
+
用户报「仓库明明配了预构建,却仍提示未配置」。查下来那一次**插件是对的** —— 仓库当时
|
|
432
|
+
确实没配好。但顺着这条线查,发现了一个真的缺陷,位置和我一开始猜的不一样。
|
|
433
|
+
|
|
434
|
+
先纠正我自己一个错判:我一度怀疑 `prebuild_availability` 对个人账号根本不填、导致永远
|
|
435
|
+
误报。**实测推翻了它** —— 我扫了 15 个仓库共 23 个机型行(含 `microsoft/vscode`、
|
|
436
|
+
`python/cpython` 这类确实配了预构建的):
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
null x23
|
|
440
|
+
"ready" x3 ← 只出现过这两种值
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**从来没有 `"none"`,也没有 `"in_progress"`**。所以「字段可读但为 `null`」**确实就是
|
|
444
|
+
「没有预构建」**,插件的 `false` 判断是站得住的、不需要改。
|
|
445
|
+
|
|
446
|
+
真正的缺陷在**失败路径**:客户端原来写的是
|
|
447
|
+
|
|
448
|
+
```js
|
|
449
|
+
setPrebuild(data.hasPrebuild === true ? true : false) // null 被压成 false
|
|
450
|
+
.catch(() => setPrebuild(false)) // 失败也被压成 false
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
于是**只要读取失败,就显示「未配置预构建」** —— 那是把插件自己的故障当成你仓库的事实
|
|
454
|
+
来陈述。现在 `hasPrebuild` 的 `true | false | null` **三态一路传到 UI**,`null` 单独
|
|
455
|
+
渲染成「无法确认预构建状态,创建可能需要 1–3 分钟」,而且**不给按钮**(没有可决定的事,
|
|
456
|
+
给了反而让人以为需要决策)。
|
|
457
|
+
|
|
458
|
+
顺带查出并修掉两个**既有 bug**:
|
|
459
|
+
|
|
460
|
+
- **机器步会直接崩溃**。我删掉 `needsNotice` 时漏了「下一步」按钮的 `disabled` 还在引用
|
|
461
|
+
它 —— 一进机器步就 `ReferenceError`。这是浏览器套件当场抓到的。
|
|
462
|
+
- **确认后反而说「已配置预构建」**。点过「直接创建」之后,`prebuildAcknowledged` 会让
|
|
463
|
+
渲染落到「该仓库已配置预构建。」那一支 —— 恰好说了与刚刚相反的话。现在确认后不再显示
|
|
464
|
+
任何提示,而不是显示一句假的。
|
|
465
|
+
|
|
466
|
+
浏览器套件 136 → **147 项**(后续又涨到 217,见下文「十一」「十三」),其中 8 项专测这三态(含
|
|
467
|
+
「`null` 绝不能说成未配置」和「确认后绝不能说成已配置」两条否定断言)。
|
|
468
|
+
|
|
469
|
+
**十、云端会话里本地工具根本没让位 —— 提示词说远程,手上还是本地**
|
|
470
|
+
|
|
471
|
+
用户报「扫描工作区时环境与流程不一致」。顺着这条线查,抓到了这个插件**最核心那条
|
|
472
|
+
承诺**上的一个真缺陷:`installRemoteTools` 从未真正执行过。
|
|
473
|
+
|
|
474
|
+
成因是开头这三行:
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
const tools = agentCtx?.tools;
|
|
478
|
+
if (tools === undefined) return () => {}; // 静默放弃
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
`agent/created` 那一刻的 `agent.ctx` 是**作用域 ctx**,cordis 对没有写进 `inject` 的
|
|
482
|
+
服务本应门禁报错(`cannot get property "x" without inject`)——**但这里偏偏不报**。
|
|
483
|
+
`ReflectService.handler.get` 的最后三行是(已核对 `@deepseek-ai/cordis` `lib/index.js:680`):
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false); // 退化成「不需要 inject」的读法
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
agent 作用域 ctx **没有运行中的 fiber**,于是这次读取降级成 `undefined` 而不是抛错。
|
|
490
|
+
于是整个函数变成空操作:**没有报错、console 干净、远程工具一个没注册、本地
|
|
491
|
+
`read`/`write`/`edit`/`bash` 全部照旧可用**;而 `session.js` 在同一时刻已经把
|
|
492
|
+
「你这个会话操作的是云端 Codespace」写进了系统提示词。两边就是这么对不上的。
|
|
493
|
+
|
|
494
|
+
DSH 自己的写法在 `dsh-file-reference-local` 里给得很清楚
|
|
495
|
+
(`lib/index.js:342`):`agent.ctx.inject(["systemPrompt","tools"], (scope) => …)`。
|
|
496
|
+
**等工具服务就绪再安装**是唯一正确的做法,现在也照这个改:
|
|
497
|
+
|
|
498
|
+
```js
|
|
499
|
+
fiber = agentCtx.inject(['tools'], (scope) => {
|
|
500
|
+
const tools = scope.tools // ← 只能从 scope 拿,不能从 agentCtx 拿
|
|
501
|
+
…
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
关键细节是服务要**从 `inject` 回调的 `scope` 参数上取**:只有 `inject` 新建的那个
|
|
506
|
+
fiber 的 store 里才有 `tools`,`agentCtx` 上永远没有。收尾用 `fiber.dispose()`。
|
|
507
|
+
|
|
508
|
+
顺带澄清一处容易混的地方:上文「二」里 `ctx.get(name, false)` 那个「不经 `inject`
|
|
509
|
+
读服务」的口子,解决的是**「服务可选、要探测在不在」**的问题;而这里的问题是
|
|
510
|
+
**作用域** —— 注册远程工具要的是 agent 作用域里的那个 `tools` 实例,只有 `inject`
|
|
511
|
+
新建的那个 fiber 的 store 里有它。我没有去核实 `get` 能不能从 agent ctx 上摸到同一个
|
|
512
|
+
实例,也没必要:`inject` 是 DSH 自己的做法,抄它就对了。
|
|
513
|
+
|
|
514
|
+
`check` 111 → **122 项**,新增的 11 项专测这条接线,覆盖三种情形:工具已就绪、工具
|
|
515
|
+
**晚到**、以及根本没有 `agent.ctx`。改完后我把源码退回旧写法反向验证了一次,两项立刻
|
|
516
|
+
变红:
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
FAIL when tools arrives, the remote surface is installed -- got "", want "read,write,edit,glob,grep,bash"
|
|
520
|
+
FAIL -> and the local tools are hidden
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
这正是它值得存在的理由:一个「静默空操作」的 bug,只有故意让它失败一次,才能证明测试
|
|
524
|
+
真的看得见它。
|
|
525
|
+
|
|
526
|
+
**十一、行首多了一个图标,状态图标用的是 emoji**
|
|
527
|
+
|
|
528
|
+
两处都是「看起来能用,但和旁边的东西不是一类」的问题。
|
|
529
|
+
|
|
530
|
+
**(一)云朵被加在文件夹**旁边**,而不是**替换**它**
|
|
531
|
+
|
|
532
|
+
需求写的是「替换原来工作区的文件夹图标,样式等相同」,我做成了「在行首再插一个云朵」,
|
|
533
|
+
于是行首出现两个图标。
|
|
534
|
+
|
|
535
|
+
根因不是手滑,是**读错了宿主的 CSS**。`Rows` 模块里三条相关规则是:
|
|
536
|
+
|
|
537
|
+
```css
|
|
538
|
+
.hIlkoa_projectRow .hIlkoa_chevron{display:none} /* 平时:箭头藏起来 */
|
|
539
|
+
.hIlkoa_projectRow:hover .hIlkoa_chevron{display:inline-flex}
|
|
540
|
+
.hIlkoa_projectRow:hover .hIlkoa_folder{display:none} /* 悬停时才轮到文件夹让位 */
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
我当时只看到最后一条,就把它当成了「文件夹在两种状态下都不可见」,于是得出「没有可见的
|
|
544
|
+
文件夹可替换」的结论,选择另加一个。**实际上文件夹在平时是看得见的**,它是行的第一个
|
|
545
|
+
子元素,悬停时才让位给展开箭头。
|
|
546
|
+
|
|
547
|
+
更糟的是,这个错误结论被**夹具固化**了:`test/browser/index.html` 里写着一条
|
|
548
|
+
|
|
549
|
+
```css
|
|
550
|
+
.hIlkoa_folder { display: none; } /* ← 宿主里根本没有这条规则 */
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
于是「文件夹本来就看不见」在测试里成了前提,所有断言都是绿的。这和上文「四」里把
|
|
554
|
+
16×16 当成 28×28 是同一类失误:**夹具抄了一个错的模型,测试就成了 bug 的帮凶**。
|
|
555
|
+
|
|
556
|
+
现在改成把云朵塞进宿主那个 `span.slot.folder`,并用内联 `display:none` 藏掉宿主自己的
|
|
557
|
+
svg(原 `style` 值连同标记一起存下,撤销时还原)。好处是位置、尺寸、颜色、以及「悬停让位
|
|
558
|
+
给箭头」全部**从宿主 CSS 继承**,我一行都不用复刻。夹具里的伪规则已按真实 CSS 修正。
|
|
559
|
+
|
|
560
|
+
补充一个实现细节:文件夹在展开/折叠时会换 svg(`IconFolderClose` ↔ `IconFolderOpen`),
|
|
561
|
+
React 会把节点换掉,所以每次 `sync()` 都要重新藏一遍「现在这个」;循环里要跳过我自己那个
|
|
562
|
+
节点——它也是子元素,第一版没跳过,第二次 `sync()` 就会把云朵自己藏起来。
|
|
563
|
+
|
|
564
|
+
**(二)状态图标是 emoji**
|
|
565
|
+
|
|
566
|
+
四个状态原本用 `⏸️`/`🕐`/`⚠️`/`▶️`。问题是 emoji 是**平台字体的彩色字形**,有自己的
|
|
567
|
+
光学尺寸和粗细,它和旁边两个控件(`Menu` 的三点、`IconNewChatOutline` 的新建对话)——都是
|
|
568
|
+
16px、1px `currentColor` 描边、跟随主题色的 `Icon*OutlineRegular`——放在一起,就是一个
|
|
569
|
+
更重、全彩、且每个系统渲染都不同的方块。它连主题色都不跟随。
|
|
570
|
+
|
|
571
|
+
现在四个图标都**照抄宿主自己的 artwork**(path 逐字复制,只挑用哪个字形):
|
|
572
|
+
|
|
573
|
+
| 状态 | 抄自 |
|
|
574
|
+
|---|---|
|
|
575
|
+
| 暂停/停止 | `IconPauseOutlineRegular` |
|
|
576
|
+
| 倒计时 | `IconAlarmClockOutlineRegular` |
|
|
577
|
+
| 异常 | `IconWarningTriangleOutlineRegular` |
|
|
578
|
+
| 启动 | `IconPlayOutlineRegular` |
|
|
579
|
+
|
|
580
|
+
抽了一个 `iconNode(spec)` 承载公共规范(16px 盒子、`Regular` 权重 = 1 **写在 `<svg>` 上**、
|
|
581
|
+
`fill="none"` + `stroke="currentColor"`),但每个字形保留**它自己的** `viewBox` 和线帽设置
|
|
582
|
+
——闹钟就是画在 **17×17** 网格上的,warning 三角形要用圆角线帽,这些都不能被"统一"掉。
|
|
583
|
+
|
|
584
|
+
顺带把**多步窗口右上角的关闭按钮**也一起改了:那里的 `✕` 是同一类问题——用文本字符
|
|
585
|
+
当图标。现在换成宿主自己的 `IconCloseOutlineRegular`(两条对角线,逐字照抄)。全库扫过
|
|
586
|
+
一遍,`lib/client.js` 里已经**没有任何 emoji 当图标**,只剩两条注释在举例说明旧实现。
|
|
587
|
+
|
|
588
|
+
浏览器套件 147 → **201 项**。新增的不只是数数,而是**能抓到这两类错误**的断言:
|
|
589
|
+
- 云朵必须在 folder 槽**里面**,且 folder 槽仍是行首第一个子元素;
|
|
590
|
+
- 宿主的 svg 真的 `display:none`,云朵真的可见;
|
|
591
|
+
- **静止状态下整个行只有 1 个图标可见**——这条否定了「加一个在旁边」;
|
|
592
|
+
- 承载云朵的 span 正是宿主 `:hover ._folder{display:none}` 规则所命中的那个(悬停行为等价);
|
|
593
|
+
- 每个字形逐条比对 `viewBox` / `stroke-width` 在 svg 上 / 每个 path 都是 `currentColor`
|
|
594
|
+
且**都不带** `stroke-width`,外加一条「glyph 文本里不得含 emoji 码点」;
|
|
595
|
+
- 关闭按钮是 svg 而不是文本,artwork 与 `IconCloseOutlineRegular` 逐字一致,且整个窗口
|
|
596
|
+
标题区**不得出现任何 emoji 码点**。
|
|
597
|
+
|
|
598
|
+
两类错误都反向验证过牙齿。把「替换」退回「在行首另加一个」,**6 项变红**,其中最关键的是
|
|
599
|
+
|
|
600
|
+
```
|
|
601
|
+
FAIL exactly one glyph is visible in the resting row — got 2, want 1
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
把关闭按钮退回 `'✕'`,**5 项变红**,最后一条正是那条 emoji 扫描。
|
|
605
|
+
|
|
606
|
+
**十二、`icon.svg` 没打进安装包,以及工作区名是一串乱码**
|
|
607
|
+
|
|
608
|
+
**(一)`Plugin metadata ... ENOENT ... icon.svg`**
|
|
609
|
+
|
|
610
|
+
`package.json` 里有 `"icon": "./icon.svg"`,仓库里也**确实有**这个文件(第一个提交就在),
|
|
611
|
+
但 `files` 白名单里**漏了它**。`files` 是 npm/pnpm 的打包白名单,而这个 profile 是从 GitHub
|
|
612
|
+
tarball 装的(`github:Ckarefulon/dsh-codespace-workspace`)—— 打包时按白名单过滤,`icon.svg`
|
|
613
|
+
就被丢掉了。DSH 读插件元信息时按 `icon` 字段去找它,`lstat` 失败。
|
|
614
|
+
|
|
615
|
+
关键在于**仓库看起来是好的,只有打包后的副本是坏的**:本地 `git status` 干净、文件就在
|
|
616
|
+
根目录,所以怎么读源码都发现不了。这也是为什么现在加了一条自检:把 `main`、`icon`、
|
|
617
|
+
每个 `exports` 目标、以及 `dsh.bundle.patch` 都拿去和 `files` 白名单比对,缺一个就红。
|
|
618
|
+
`package.json` 本身豁免(npm 无论如何都会打它)。
|
|
619
|
+
|
|
620
|
+
**(二)工作区名是 `psychic-goggles-7v9974jwpwr5fpqxq`**
|
|
621
|
+
|
|
622
|
+
那是 Codespace 自己的名字,GitHub 机器生成的,看不出是哪个项目。现在行标题取**仓库短名**
|
|
623
|
+
(`owner/name` 里的 `name`),标题装不下的两件事放进 hover 卡片那一行:
|
|
624
|
+
**`owner/name · Codespace 名字`**。
|
|
625
|
+
|
|
626
|
+
三处要一起改,否则会出现「新行对、老行还是乱码」或者「有 locale 服务时对、没有时显示原始
|
|
627
|
+
key」:
|
|
628
|
+
|
|
629
|
+
- `titleForCodespace()`(host 侧)决定新行的标题;
|
|
630
|
+
- `listWorkspaces()` 里加了一步**回填**,把已登记记录改成新标题 —— DSH 的 registry 会
|
|
631
|
+
**保留已存在记录的标题**,不回填的话你现有的那一行永远还是旧名字;
|
|
632
|
+
- 客户端 `hoverLocationOf()` 拼 hover 那行。
|
|
633
|
+
|
|
634
|
+
这里还踩到一个自己造的坑:`lib/client.js` 里**另有一份内联的 zh/en 字典**(没有 `locale`
|
|
635
|
+
服务时用它,浏览器 harness 就是这种情况)。我只改了 `locale/*.json`,于是浏览器套件里那行
|
|
636
|
+
直接显示成了字面量 `row.hoverRepo`。原来的 locale 自检只比对「两个 JSON 文件之间」和
|
|
637
|
+
「JSON 与 `t()` 调用之间」,**从没比对过内联字典**——现在补上了,两边键集合必须一致
|
|
638
|
+
(`nav` 豁免,那是宿主读的)。
|
|
639
|
+
|
|
640
|
+
`check` 122 → **147 项**(含 10 项 `titleForCodespace`、11 项打包白名单、2 项内联字典),
|
|
641
|
+
浏览器套件 201 → **203 项**。两处都反向验证过牙齿:把 `icon.svg` 从白名单里删掉 → 1 项变红;
|
|
642
|
+
把 hover 那行退回只显示标签 → 1 项变红(`got "my-box", want "Ckarefulon/app.ckarefulon.main · my-box"`)。
|
|
643
|
+
|
|
644
|
+
**十三、云朵不变蓝、转圈停不下来、转圈是个方块,以及发消息不等机器**
|
|
645
|
+
|
|
646
|
+
四处都是「照理说应该很明显」的问题,四处的原因各不相同。
|
|
647
|
+
|
|
648
|
+
**(一)正在使用的工作区里,云朵不变蓝**
|
|
649
|
+
|
|
650
|
+
宿主是这样标记「当前会话所在的工作区」的:给那一行的**文件夹图标槽**加一个
|
|
651
|
+
`folderActive` 类,规则是 `.…_folderActive{color:var(--dsw-alias-state-business-primary)}`
|
|
652
|
+
—— 也就是变蓝。云朵本来就排在那同一个 span 里、`stroke="currentColor"`,看起来应该自动继承。
|
|
653
|
+
|
|
654
|
+
问题是**我自己给云朵的容器写了一行 `color: var(--dsw-alias-label-secondary)`**。继承是继承,
|
|
655
|
+
但**显式声明会盖掉继承值**,于是云朵永远是灰的。删掉那行就好——位置、尺寸、颜色、悬停让位
|
|
656
|
+
全都该由宿主的 CSS 说了算。
|
|
657
|
+
|
|
658
|
+
这条还牵出一个**夹具的老毛病**(这是第三次了,前两次是 16×16 当 28×28、`:hover` 当无条件):
|
|
659
|
+
夹具里 `.hIlkoa_slot`(设 `label-tertiary`)**排在** `.hIlkoa_folderActive` **后面**,
|
|
660
|
+
而真实宿主里是反过来的。两个都是单类选择器,同优先级,所以**后写的赢** —— 夹具里的
|
|
661
|
+
`folderActive` 根本不起作用,颜色测试一开始是红的。夹具的规则顺序已按宿主改正,并写了注释。
|
|
662
|
+
|
|
663
|
+
**(二)「正在切换状态…」能转一整个回合**
|
|
664
|
+
|
|
665
|
+
`isBusy()` 里有一行 `if (record.busy === true) return true`。而 `record.busy` 是
|
|
666
|
+
**host 在 `user/message` 时置位、在 `turn/end` 时清除**的 —— 它的意思是「Agent 正在这个
|
|
667
|
+
工作区里干活」,可以持续好几分钟。把它接进「该不该转圈」里,按钮就会在整个回合里转,还
|
|
668
|
+
配着「正在切换状态…」这句错误的话,而 GitHub 那边的状态其实很明确。现在 `isBusy` 只认两件事:
|
|
669
|
+
**我们自己的 RPC 在飞**,或者 **GitHub 说它确实在过渡态**(`Provisioning`/`Starting`/
|
|
670
|
+
`ShuttingDown`…)。`record.busy` 保留在契约里,只是不再驱动这个按钮。
|
|
671
|
+
|
|
672
|
+
**(三)转圈图标是个方块**
|
|
673
|
+
|
|
674
|
+
原来是一个 CSS 环:`border:2px solid; border-top-color:transparent; border-radius:50%`。
|
|
675
|
+
但宿主自己的加载指示器是 **`IconLoadingOutlineRegular`** —— 一段圆圆弧,配一个纯旋转动画
|
|
676
|
+
(在 DSH 自己的 bundle 里核对过)。现在照抄它,于是转圈和那四个状态图标、以及旁边宿主的
|
|
677
|
+
控件是同一套语言(16px、`currentColor`、1px 描边)。顺带补了 `prefers-reduced-motion`。
|
|
678
|
+
|
|
679
|
+
**(四)发消息时没有等 Codespace 起来**
|
|
680
|
+
|
|
681
|
+
Codespace 自动暂停之后,直接在会话里发消息,每个远程工具调用都会撞上连接错误。现在挂
|
|
682
|
+
**`agent/pre-step`**:Agent 循环在向模型要 step 之前会 **await** 这个 waterfall
|
|
683
|
+
(`dsh-agent-loop/lib/index.js` ≈L911),所以在这里等是真的在推迟这一轮,而不是和它赛跑。
|
|
684
|
+
DSH 自己的 `archived-session-gate` 用的是同一个钩子 —— 但它是**拦住**这一轮;这里要的是
|
|
685
|
+
**等**,所以拿到结果后照常 `next()`。
|
|
686
|
+
|
|
687
|
+
几个克制的设计:**本地会话完全不碰**(cwd 判断在最前面);`ensureRunning` 永不抛错,失败就
|
|
688
|
+
`{ok:false, reason}` 然后照常放行(否则用户会看到一轮莫名其妙地什么都没发生,而不是一个
|
|
689
|
+
明确的连接错误);等待受这一轮的 abort signal 约束,用户中断立刻结束;成功状态记 20 秒,
|
|
690
|
+
所以一个回合最多查一次 GitHub,而不是每一步都查。
|
|
691
|
+
|
|
692
|
+
`check` 147 → **181 项**(33 项新增:`ensureRunning` 的各种路径 + `agent/pre-step` 网关的
|
|
693
|
+
行为),浏览器套件 203 → **217 项**。三处都反向验证过牙齿:不启动机器 + 把过渡态当成就绪
|
|
694
|
+
→ 6 项变红;给云朵加回颜色 + 让 `busy` 重新驱动转圈 → 8 项变红。
|
|
695
|
+
|
|
696
|
+
## 已知限制
|
|
697
|
+
|
|
698
|
+
- **工作区分组标题栏的图标行、以及工作区行,都没有官方插槽**。DSH 把「添加工作区」
|
|
699
|
+
按钮和 `ProjectRowItem` 的操作按钮、文件夹图标全部写死,因此以下四处通过 DOM 增强
|
|
700
|
+
实现(与 `dsh-pet` 处理设置导航图标同一手法):标题栏里的新建按钮、行首云朵、
|
|
701
|
+
行内启停按钮、hover 文案(外加菜单里的「删除 Codespace…」)。做法是全部打上
|
|
702
|
+
`data-dsh-codespace-*` 标记、一个合并的 `MutationObserver`、dispose 时完整撤销,
|
|
703
|
+
并且**不删除任何 React 拥有的节点**:行首的云朵是把自己塞进宿主那个文件夹
|
|
704
|
+
`span.slot` 里,再把宿主自己的 svg 用内联 `display:none` 藏起来(连同原 `style`
|
|
705
|
+
一起登记,撤销时还原)。这样位置、尺寸、颜色和悬停让位全都从宿主的 CSS 继承,
|
|
706
|
+
不需要我自己复刻。宿主改版可能影响它。已核对:客户端运行时的
|
|
707
|
+
插槽目录共 90 个,其中有 `sidebar.workspaces.session.menu.item`,但**没有**工作区行
|
|
708
|
+
的菜单插槽,也没有标题栏图标行的插槽;`sidebar.footer.action` 是真实存在的插槽,
|
|
709
|
+
但它把控件放在侧边栏底部,不符合「与原有三个图标同地位、紧挨添加工作区左侧」的要求。
|
|
710
|
+
标题栏的 `_sectionHeader` / `_headerActions` 两个类名后缀经核对为
|
|
711
|
+
`dsh-client-ui-workspace` 独有,不会误匹配;行首文件夹槽位按 `_folder` 后缀匹配,
|
|
712
|
+
并限定在 `span[class*="_slot"]` 上,避免误伤同模块的 `_folderActive`。
|
|
713
|
+
- 标题栏图标行是 `justify-content:flex-end` + `max-width:60px`,而标题栏的控件是
|
|
714
|
+
**28×28**(`_9lTDKa_iconButton`),所以 60px 正好放**两个**(28+4+28)。加进第三个
|
|
715
|
+
控件需要放宽容器宽度,放宽规则带了 `:not([class*="_headerActionsHidden"])`,以免盖掉
|
|
716
|
+
宿主自己在展开搜索框时的收起行为。**注意别把 `Rows` 模块的 `hIlkoa_iconButton`
|
|
717
|
+
(16×16,工作区行的按钮)当成标题栏的类**——两者同后缀不同模块,混淆过一次,见上文
|
|
718
|
+
「四」。另外「添加工作区」按钮只在目录流插槽被占用时才渲染,所以插入位置是「最后一个
|
|
719
|
+
子元素之前」而不是固定下标。窄栏(rail)下宿主把盒子放大到 36px、字形放大到 18px。
|
|
720
|
+
- 远程工具以文本方式传输文件(base64),大文件不适合;大仓库建议让 Agent 在
|
|
721
|
+
Codespace 内直接操作。
|
|
722
|
+
- `gh` 缺失时依赖用户自行配置 SSH 别名。设置项名为「SSH 密钥」,但**私钥路径无法
|
|
723
|
+
指定主机**,所以该值被当作 `~/.ssh/config` 的主机别名使用;填成路径会得到一条
|
|
724
|
+
说明如何修正的错误,而不是把路径当主机名交给 `ssh`。
|
|
725
|
+
- 未在本机实测的部分:**真实 SSH 数据面**(没有已配置的 Codespace 目标),以及
|
|
726
|
+
Codespace 的创建/启停/删除这些会改云端状态的调用。控制面本身已联网实测:
|
|
727
|
+
`test/live.mjs` 用 `gh` 的令牌打真实 GitHub API,`getViewer` / `listRepos` /
|
|
728
|
+
`listCodespaces` 都通(本机 18 个仓库、0 个 Codespace)。命令拼装也已用真实 bash
|
|
729
|
+
验证:20 组逐字节往返 + 5 组注入载荷,均保持为单个 shell 词且无副作用。
|
|
730
|
+
|
|
731
|
+
**「退出即停」不在此列**:它的逻辑已按上面的 `test/shutdown.mjs` 实测(并发、
|
|
732
|
+
不 reject、5 秒预算内、幂等、pending 落盘)。仍然未经实测的只有「真实 GitHub
|
|
733
|
+
是否接受这次 stop 调用」这一层——那取决于 token 与账号,不取决于本插件的逻辑。
|
|
734
|
+
另外「宿主在退出时确实会 await 根 effect 的 disposer」这一条是**读 DSH 源码**
|
|
735
|
+
核实的(`runProfile` 的 `dispose` → `fiber.dispose()` → cordis `_unload` 并发
|
|
736
|
+
await 每个 disposer),不是在本机跑一次退出观察到的;本机没有可复现的 Codespace
|
|
737
|
+
目标,无法做端到端退出观察。
|
|
738
|
+
|
|
739
|
+
## 发布(维护者)
|
|
740
|
+
|
|
741
|
+
包名 `dsh-codespace-workspace` 已发布到 npm。`package.json` 里写了 `publishConfig`
|
|
742
|
+
(`access: public` + `registry: https://registry.npmjs.org/`),所以**直接 `npm publish`
|
|
743
|
+
不带任何参数**就会发到正确的地方——即使本机 `~/.npmrc` 指向的是镜像站。
|
|
744
|
+
|
|
745
|
+
> 注意:这个包曾经带 `"private": true`(那时只从 GitHub 装)。npm 会以 `EPRIVATE`
|
|
746
|
+
> 直接拒绝发布,所以该字段已**删除**(不是改成 `false`)。`test/check.mjs` 里有一条
|
|
747
|
+
> 断言专门盯着它「必须不存在」,复制回来会立刻变红。
|
|
748
|
+
|
|
749
|
+
后续版本**自动发布**:推送到 `main` 时 `.github/workflows/publish.yml` 会先跑测试再
|
|
750
|
+
`npm publish`——但**只有 `package.json` 里的 `version` 是 registry 上还没有的版本时才发**。
|
|
751
|
+
所以发版流程就是两步:
|
|
752
|
+
|
|
753
|
+
1. 改 `package.json` 的 `version`。
|
|
754
|
+
2. 推送到 `main`。
|
|
755
|
+
|
|
756
|
+
这次运行要么发出这个版本,要么报告「该版本已在线上」并以成功结束。
|
|
757
|
+
**没改版本号的推送是 no-op,不是失败**——所以每个要发出去的改动都必须自己带一次版本号
|
|
758
|
+
变更;只改文档的提交同样会触发工作流,同样 no-op。
|
|
759
|
+
|
|
760
|
+
认证用 npm **Trusted Publishing(OIDC)**:GitHub 给这个 job 签一个短时 token,npm 拿它换
|
|
761
|
+
一个只对这一个包有效的发布 token。**仓库里没有任何 `NPM_TOKEN` secret。** 工作流刻意
|
|
762
|
+
**不用** `actions/setup-node` 的 `registry-url` 输入——它会往 `~/.npmrc` 写
|
|
763
|
+
`//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}`,而该变量没定义时这个占位符会
|
|
764
|
+
原样留下、被 npm 当作 token 发出去。registry 交给 `publishConfig` 就够了。
|
|
765
|
+
|
|
766
|
+
工作流在发布前会先 `npm install` 那两个 peer(`@deepseek-ai/cordis`、
|
|
767
|
+
`@deepseek-ai/schemastery`):本仓库故意不 vendor 它们,而 `test/check.mjs` 是沿着
|
|
768
|
+
import 路径往上找的,装到仓库自己的 `node_modules` 里才能跑到真实模块。
|
|
769
|
+
|
|
770
|
+
<details>
|
|
771
|
+
<summary>一次性配置:绑定 Trusted Publisher</summary>
|
|
772
|
+
|
|
773
|
+
Trusted Publisher **只能绑到 registry 上已存在的包**,所以**首次发布是手工做的**
|
|
774
|
+
(在检出目录里跑了一次 `npm publish`),从第二个版本起才轮到工作流接管。在 npmjs.com 上:
|
|
775
|
+
包 → Settings → Trusted Publisher → GitHub Actions,填
|
|
776
|
+
|
|
777
|
+
| 字段 | 值 |
|
|
778
|
+
|---|---|
|
|
779
|
+
| Repository | `Ckarefulon/dsh-codespace-workspace` |
|
|
780
|
+
| Workflow filename | `publish.yml` |
|
|
781
|
+
| Environment | *留空* |
|
|
782
|
+
|
|
783
|
+
文件名必须**逐字一致**——npm 把授权绑在它上面,写错会表现为发布报 `ENEEDAUTH`。
|
|
784
|
+
|
|
785
|
+
</details>
|
|
786
|
+
|
|
787
|
+
## 文档
|
|
788
|
+
|
|
789
|
+
- [`docs/CONTRACT.md`](docs/CONTRACT.md) —— 实现契约:经过核实的 DSH API 事实、
|
|
790
|
+
冻结的 RPC 契约、模块接口、以及每条未经核实项的处理分支。
|
|
791
|
+
|
|
792
|
+
## 许可
|
|
793
|
+
|
|
794
|
+
MIT
|