dsh-server-connector 0.2.2 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.3.0 — 2026-11-09
4
+
5
+ ### 修复
6
+
7
+ - **设置页的「全局设置」无法修改**。九个键此前一个 `.volatile()` 都没有,而 DSH 0.2.2
8
+ 只通过 `volatileForm()` 把插件配置投影成设置表单:一个 volatile 字段都没有时整份
9
+ schema 投影为 `undefined`,`settings.describe()` 不会列出这个 row,
10
+ `ctx.configForms.get('dsh-server-connector')` 永远是 `unavailable`。于是那一块只能
11
+ 显示九个只读数字——不是「值来自 profile」,而是**这个 row 从来没被标记成可编辑**。
12
+
13
+ 现在九键全部 `.volatile()`,设置页用 `ctx.configForms` 渲染真正的表单:改完点保存,
14
+ `mutate(ops, revision)` 一次性写入,**立即生效、不重启、不断开现有连接**。
15
+ 可编辑表单拿不到时(旧 Host、或该部署没有这个 row)降级为只读视图并明确说明原因,
16
+ 不再渲染一排死掉的输入框。
17
+
18
+ - **volatile 值必须按 cell 读,否则保存等于没保存**。`.volatile()` 字段解析出来的是
19
+ cosmokit 的 cell 而不是值,loader 直接**原地改写这个 cell** 并发
20
+ `loader/volatile-update`,插件不会被重新挂载、`apply()` 不会再执行一次。
21
+ 新增 `lib/config.js` 的 `readConfig()` 作为唯一读取入口(各单元在**用值时**才读),
22
+ 并在 `apply()` 里监听 `loader/volatile-update`,把新值转发给已经持有快照的
23
+ `Pool.configure()` / `AuditLog.configure()` / `PendingQueue.configure()`。
24
+ 这些 `configure()` 都是原地生效:改超时不会断开连接,关审计不会删掉已有日志,
25
+ 改 TTL 只影响之后入队的记录(已入队的截止时间不变)。
26
+
27
+ ### 变更
28
+
29
+ - **删除确认改为详情页原地两段式**。此前点「删除」只是入队,然后必须切到设置页找到
30
+ 那条记录再确认;现在详情页点「删除」会在本页展开确认区:显示目标、后果,勾选确认框后
31
+ 「确认删除」才可点。安全模型没变——请求仍然先入队(`DELETE` 只入队返回 202),
32
+ 再调用 `POST /pending/:id/confirm` 才真正删除。设置页的待确认区保留,作为异时确认的
33
+ 第二入口。
34
+
35
+ - **更新后面板消失,重启才回来 —— 已按 dsh-context 的做法修好本插件这一半**。
36
+ 参照对象不是猜的:同一台机器上 `dsh-context` 更新时面板**先消失后自己回来**,
37
+ 而本插件只能重启。差别只有一处——
38
+
39
+ `dsh-context` 每次挂载前先问注册表「我这个 id 是不是已经在位」:
40
+
41
+ ```js
42
+ function alreadyMounted(ctx, slot, id) {
43
+ const entries = ctx.slots.entries(slot) // 注册表的读取面
44
+ return entries.some((entry) => entry.options?.id === id)
45
+ }
46
+ // apply() 里
47
+ if (alreadyMounted(ctx, n_, r_)) return
48
+ ```
49
+
50
+ 本插件此前是**盲注册**。而 DSH 的插槽注册表对同一 id、同一优先级会直接抛:
51
+
52
+ ```
53
+ list slot "settings.section" already has an entry with id "server-connector"
54
+ keyed slot "main" already has an entry for key "server-connector"
55
+ ```
56
+
57
+ (`ui-slots`:list 比 `options.id`,keyed 比 `options.key`。)这个抛错落在
58
+ `slots.inject` 的回调里,而该注入**一旦抛出就在本 fiber 余生不再重试**——
59
+ 于是新代码挂上、入口却没注册,页面要等进程重启才恢复。这正是「先消失、
60
+ 重启才回来」在本插件侧的那一环。
61
+
62
+ 现在三个入口都先查在位情况,在位就原样复用、不再注册;`alreadyMounted()` 同时
63
+ 兼容 `entriesOfSlot()` 与 `entries()` 两种读取面,读不到就退回盲注册(旧 Host 仍能工作)。
64
+ 回归测试按真实注册表的重复判定(list 比 id、keyed 比 key、拒绝时抛错)搭了替身,
65
+ 并断言第二次 `apply()` **一次 register 都不发起**。
66
+
67
+ 保留上一版的兜底:注册真的失败时只丢那一个入口,不会顺带带走整批插件。
68
+
69
+ ### 文档
70
+
71
+ - README 与 `docs/design.md` 的 §8.2 / §8.3 / §10 更新为上述行为。
72
+
73
+ ---
74
+
75
+ ## 0.2.3 — 2026-11-06
76
+
77
+ ### 修复
78
+
79
+ - **深色主题下启用状态的开关显示为一条纯白条**。0.2.2 把滑块写死成白色,而深色主题下
80
+ 轨道解析为 `#f9fafb`(近白)——白滑块落在白轨道上,整个控件看起来没有滑块。
81
+
82
+ DSH 原生开关的做法是**轨道与滑块成对反转**:轨道用 `--dsw-alias-label-primary`,
83
+ 滑块用 `--dsw-alias-label-primary-inverted`。浅色下是深轨道+白滑块,深色下是白轨道+深滑块,
84
+ 两种主题都保证对比度。现已改为这一对 token,并在两种主题下实测确认。
85
+
86
+ - 关闭态轨道改用 `--dsw-alias-border-l4`(深色下自动变为半透明白),滑块用
87
+ `--dsw-alias-label-tertiary`,不再依赖写死的颜色。
88
+
89
+ ---
90
+
3
91
  ## 0.2.2 — 2026-11-06
4
92
 
5
93
  ### 修复
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -40,7 +40,7 @@ DSH(DeepSeek Harness)插件:在本机配置好可运维的 SSH 服务器
40
40
  | 避免 agent 手滑执行破坏性命令 | 20 条内置高危规则先判定:`block` 直接拒绝,`confirm` 弹审批框,`allow` 直接执行 |
41
41
  | 事后追查谁执行了什么 | 每次 `srv_exec` 写一行 JSON 到 `audit.log`,面板里也能看到最近 20 条 |
42
42
  | 长命令不阻塞对话 | 超过同步窗口自动转后台 job,模型用 `srv_job_output` 增量读取、`srv_job_kill` 取消 |
43
- | 删服务器、关主机密钥校验这类危险改动先缓一缓 | 先进设置页的「待确认操作」,确认后才真正生效 |
43
+ | 删服务器、关主机密钥校验这类危险改动先缓一缓 | 先进「待确认」,确认后才真正生效(删除在详情页原地确认) |
44
44
 
45
45
  **不做什么**:不做文件上传/SFTP、端口转发、跳板机、Kerberos(见[已知限制](#已知限制))。
46
46
  另外,策略是**遏制不是安全边界**——真正的硬边界仍然是远端那个低权限用户与 sudo 白名单。
@@ -201,7 +201,11 @@ dsh --profile web --dump-config | grep -A2 dsh-server-connector
201
201
 
202
202
  ### 详情
203
203
 
204
- 连接字段只读、逐条高危策略的当前取值、最近 20 条审计,以及底部的 **删除**(删除会先进待确认区)。
204
+ 连接字段只读、逐条高危策略的当前取值、最近 20 条审计,以及底部的 **删除**。
205
+
206
+ 点「删除」不会立刻删掉任何东西:本页展开确认区,显示目标与后果,
207
+ 勾选确认框后「确认删除」才可点。确认时插件先把请求写入待确认区(`DELETE` 只入队),
208
+ 紧接着确认这一条;服务器在你按下「确认删除」之前一直可用。
205
209
 
206
210
  ![服务器详情](./docs/usage/detail.png)
207
211
 
@@ -397,7 +401,7 @@ srv_exec(command)
397
401
  ### 面板操作的「待确认区」
398
402
 
399
403
  面板本身不在 turn 内,没法弹审批框,所以三类危险操作改成两段式:
400
- 先记入 `pending.json`(目标保持原样),你回到设置页看到完整上下文后再确认。
404
+ 先记入 `pending.json`(目标保持原样),看到完整上下文后再确认。
401
405
 
402
406
  | 操作 | 请求后立即 | 确认后 |
403
407
  |---|---|---|
@@ -405,11 +409,47 @@ srv_exec(command)
405
409
  | 主机密钥改为 `off` | 仍按原策略校验 | 改为 `off`,审计写 `hostkey-disabled` |
406
410
  | 停用某条高危规则 | 规则仍生效 | 加入 `disabledRules`,审计写 `rule-disabled` |
407
411
 
412
+ 删除的确认在**详情页原地**完成(点删除 → 确认区 → 勾选后确认),不必再切面板;
413
+ 另外两类仍在设置页的「待确认操作」里确认,那里也是所有请求的统一入口,
414
+ 包括删除这种「关了页面才想起来」的情况。
415
+
408
416
  每条记录 24 小时后自动过期(`pendingTtlHours`),过期后必须重新申请;过期记录无法确认。
409
417
 
410
418
  ![设置页的待确认区](./docs/usage/settings.png)
411
419
 
412
- 设置页同时只读展示当前生效的全局配置——这些值来自 profile 配置,改完需要重新加载插件。
420
+ 设置页同时提供**可编辑的全局设置**:九个键直接改、点保存即生效,
421
+ 不用重启插件,也不会断开已经在用的连接。
422
+
423
+ ---
424
+
425
+ ## 全局设置(可编辑)
426
+
427
+ 在「设置 → 服务器连接器」里直接改,保存后立即生效。
428
+
429
+ ```yaml
430
+ config:
431
+ commandTimeoutMs: 90000
432
+ auditEnabled: false
433
+ ```
434
+
435
+ | 键 | 默认 | 说明 |
436
+ |---|---|---|
437
+ | `connectTimeoutMs` | `15000` | SSH 连接超时 |
438
+ | `commandTimeoutMs` | `60000` | 单条命令默认超时,工具参数 `timeoutMs` 可覆盖 |
439
+ | `syncTimeoutMs` | `20000` | 同步等待窗口,超过就转后台 job |
440
+ | `maxOutputChars` | `200000` | stdout、stderr **各自**的输出上限 |
441
+ | `idleTimeoutMs` | `600000` | 连接空闲多久后关闭 |
442
+ | `auditEnabled` | `true` | 是否写审计(关闭后命令照常执行) |
443
+ | `auditMaxBytes` | `0` | `0` 表示不轮转;大于 0 时超限轮转一次 |
444
+ | `pendingTtlHours` | `24` | 待确认记录的有效期(只影响之后入队的记录) |
445
+ | `maxListedServers` | `8` | 系统提示里最多列几台服务器,其余折叠成「另有 N 台」 |
446
+
447
+ 所有键都有默认值,空配置也能启动。
448
+
449
+ 九个键都声明为 **volatile**:DSH 只把 volatile 字段投影成设置表单,一个都没有时这一页
450
+ 只剩只读展示。volatile 字段由 loader 原地改写并广播 `loader/volatile-update`,
451
+ 插件不会被重新挂载,因此生效是即时的,而不是「保存后重启」。
452
+ 面板手里的表单拿不到时(旧版 DSH 等),这一块会明确显示为只读,而不是渲染一排点不动的输入框。
413
453
 
414
454
  ---
415
455
 
@@ -453,7 +493,8 @@ srv_exec(command)
453
493
 
454
494
  ## 配置
455
495
 
456
- 配置写在 profile 的 `cordis.patch.yml`(Web 界面里是「打开配置文件」),按插件行 id 覆盖:
496
+ 平时在「设置 → 服务器连接器」里改(见上面的[全局设置](#全局设置可编辑)),保存即生效。
497
+ 也可以直接写 profile 的 `cordis.patch.yml`(Web 界面里是「打开配置文件」),按插件行 id 覆盖:
457
498
 
458
499
  ```yaml
459
500
  - id: dsh-server-connector
@@ -471,10 +512,11 @@ srv_exec(command)
471
512
  | `idleTimeoutMs` | `600000` | 连接空闲多久后关闭 |
472
513
  | `auditEnabled` | `true` | 是否写审计(关闭后命令照常执行) |
473
514
  | `auditMaxBytes` | `0` | `0` 表示不轮转;大于 0 时超限轮转一次 |
474
- | `pendingTtlHours` | `24` | 待确认记录的有效期 |
515
+ | `pendingTtlHours` | `24` | 待确认记录的有效期(只影响之后入队的记录) |
475
516
  | `maxListedServers` | `8` | 系统提示里最多列几台服务器,其余折叠成「另有 N 台」 |
476
517
 
477
- 所有键都有默认值,空配置也能启动。改完保存后由 profile 重新加载;没有启用 HMR 的环境需要重启 `dsh web`。
518
+ 所有键都有默认值,空配置也能启动。两条路径都改的是同一份 profile 配置:
519
+ 面板保存走 `configEditor`,把它写回 YAML 并保留注释;手改文件同样即时生效。
478
520
 
479
521
  ### 数据文件
480
522
 
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # dsh-server-connector bundle patch: inserts only this plugin into a profile's
2
- # layer stack. `name` is the module specifier the loader imports, so it must
3
- # equal package.json's `name` verbatim; `id` is the row handle in the config tree.
4
- - insert:
5
- - id: dsh-server-connector
6
- name: 'dsh-server-connector'
1
+ # dsh-server-connector bundle patch: inserts only this plugin into a profile's
2
+ # layer stack. `name` is the module specifier the loader imports, so it must
3
+ # equal package.json's `name` verbatim; `id` is the row handle in the config tree.
4
+ - insert:
5
+ - id: dsh-server-connector
6
+ name: 'dsh-server-connector'
package/icon.svg CHANGED
@@ -1,15 +1,15 @@
1
- <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g transform="translate(1.5 1.5) scale(0.9166666667)">
3
- <path d="M6 8.5C6 6.57 9.13 5 13 5H23C26.87 5 30 6.57 30 8.5V13.5C30 15.43 26.87 17 23 17H13C9.13 17 6 15.43 6 13.5V8.5Z" fill="url(#a)" fill-opacity="0.85" />
4
- <path d="M6 22.5C6 20.57 9.13 19 13 19H23C26.87 19 30 20.57 30 22.5V27.5C30 29.43 26.87 31 23 31H13C9.13 31 6 29.43 6 27.5V22.5Z" fill="url(#a)" fill-opacity="0.45" />
5
- <circle cx="12.5" cy="11" r="1.8" fill="#FFFFFF" />
6
- <circle cx="12.5" cy="25" r="1.8" fill="#FFFFFF" />
7
- <path d="M17 11H24M17 25H24" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" />
8
- </g>
9
- <defs>
10
- <linearGradient id="a" x1="18" y1="5" x2="18" y2="31" gradientUnits="userSpaceOnUse">
11
- <stop stop-color="#7AC2FF" />
12
- <stop offset="1" stop-color="#4A65E8" />
13
- </linearGradient>
14
- </defs>
15
- </svg>
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g transform="translate(1.5 1.5) scale(0.9166666667)">
3
+ <path d="M6 8.5C6 6.57 9.13 5 13 5H23C26.87 5 30 6.57 30 8.5V13.5C30 15.43 26.87 17 23 17H13C9.13 17 6 15.43 6 13.5V8.5Z" fill="url(#a)" fill-opacity="0.85" />
4
+ <path d="M6 22.5C6 20.57 9.13 19 13 19H23C26.87 19 30 20.57 30 22.5V27.5C30 29.43 26.87 31 23 31H13C9.13 31 6 29.43 6 27.5V22.5Z" fill="url(#a)" fill-opacity="0.45" />
5
+ <circle cx="12.5" cy="11" r="1.8" fill="#FFFFFF" />
6
+ <circle cx="12.5" cy="25" r="1.8" fill="#FFFFFF" />
7
+ <path d="M17 11H24M17 25H24" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" />
8
+ </g>
9
+ <defs>
10
+ <linearGradient id="a" x1="18" y1="5" x2="18" y2="31" gradientUnits="userSpaceOnUse">
11
+ <stop stop-color="#7AC2FF" />
12
+ <stop offset="1" stop-color="#4A65E8" />
13
+ </linearGradient>
14
+ </defs>
15
+ </svg>