@hyzyn/dsh-docker 0.13.3 → 0.13.5
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/README.en.md +98 -345
- package/README.md +98 -299
- package/client-src/index.js +240 -63
- package/client-src/log-stream.js +48 -3
- package/client.js +24 -24
- package/lib/docker.d.ts +24 -5
- package/lib/docker.js +33 -7
- package/lib/docker.js.map +1 -1
- package/lib/index.js +450 -88
- package/lib/index.js.map +1 -1
- package/package.json +4 -4
- package/scripts/client-smoke.mjs +62 -17
- package/scripts/route-smoke.mjs +36 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- **显式一键连接本机**:两个**非常驻**入口——一个目标都没有时是「还没有配置 Docker 目标」空态里的主按钮,已有别的目标时是**目标下拉的末一项** `+ 连接本机`(工具条上**不挂**:常驻时它既占位又招人点,纯噪音)——一次点击(或选中该项)就把宿主所在机器加成本机目标并选中(已有本机目标则直接复用,不重复创建;SSH / 自定义目标原样保留),agent 侧同一条操作是 `docker_connect_local`。它**不**改空 `targets` 的语义、**不**凭空造隐式默认目标、**不**自动装 Docker / 起 daemon / 切 docker context,也不需要任何能力授权(只加一条指向插件自身的本机目标)。
|
|
12
12
|
- **多目标聚合取数**:总览页对全部 `targets[]` 并行请求,单个目标不可达只污染自己那一格;agent 侧同一口径由 `docker_ps target:"*"` / `docker_attention target:"*"` 暴露,跨目标不互相阻塞。
|
|
13
13
|
- **「需关注」读权威字段**:不健康 / 反复重启 / OOM 被杀 / 非零退出 / 僵死;OOM 与真实退出码由一次 `docker inspect` 补齐——`docker ps` 摘要里的 137 分不出 OOM 与手动 kill,只按摘要筛必然误报。
|
|
14
|
-
- **四条 SSE 长流共用一套基建**:日志 FOLLOW、`docker stats`、`docker events`、`docker pull` 走同一个 `openSseStream`(心跳 / 活跃流登记 / 断开清理 / 管道起来后先发一帧 `ready`),差异只在收尾语义——日志与拉取自然结束,统计与事件由前端主动断。「连上了」与「在收数据」是两件事:客户端只认 `ready`,响应头到达不算(见「SSE
|
|
14
|
+
- **四条 SSE 长流共用一套基建**:日志 FOLLOW、`docker stats`、`docker events`、`docker pull` 走同一个 `openSseStream`(心跳 / 活跃流登记 / 断开清理 / 管道起来后先发一帧 `ready`),差异只在收尾语义——日志与拉取自然结束,统计与事件由前端主动断。「连上了」与「在收数据」是两件事:客户端只认 `ready`,响应头到达不算(见「SSE 事件协议」)。多选 / Compose 项目的聚合日志把多个容器装进**一条** `/logs/stream?ids=…`(D168),按 `--timestamps` 前缀还原跨容器真实时序,「暂停」只冻结渲染(流继续接收,恢复时一次性补齐)。
|
|
15
15
|
- **默认只读,能力开关分三级**:启停删 / exec / 镜像变更各自独立开关,未开启时 agent 工具**不注册**、HTTP 路由 403(能力不存在,而非调用后报错);开关的**提权走两条带外通道**(启动环境变量 / 卡片里就地确认,见下条);容器名与 ID 过白名单,命令一律 argv 构造 + 单引号转义,密码 / 口令以 `env:NAME` **凭据引用**(官方凭据层解析,缺失时退回环境变量)且永不回传浏览器。
|
|
16
16
|
- **与 dsh-tty 数据级复用、代码级不耦合**:不 import 任何 tty 代码,tty 也无需改一行源码,两者可各自安装与升级;装了 tty 则消费三个可选扩展点——连接栏动作(`ttyConnbar`)、终端承载(`ttyTerminal`:标签 / dock 承载下经 `open` 新开标签,模态下经 `mount` 就地嵌入抽屉)、以及只在兜底路径用到的终端右侧 dock(`ttyPanel.mountPane`);未装或版本不足逐项静默降级。
|
|
17
17
|
|
|
@@ -43,8 +43,8 @@ dsh plugin --profile web add link:$(pwd)/packages/docker # 仓库开发调试
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
聚合包 `@hyzyn/dsh-all`(或仓库根 bundle)已包含本插件,一次装齐时不必单独
|
|
46
|
-
add。装完重启 `dsh web
|
|
47
|
-
|
|
46
|
+
add。装完重启 `dsh web`,侧边栏出现「容器」入口;「Docker 容器面板」卡片维护目标与开关,
|
|
47
|
+
**保存即热生效**(`settings/updated`
|
|
48
48
|
触发重解析,无需重启)。
|
|
49
49
|
|
|
50
50
|
> 已在 web profile 里装过 `@hyzyn/dsh-all` 或根 bundle 时**不要**再 add 本包,
|
|
@@ -198,10 +198,11 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
198
198
|
左侧出现勾选框,点卡片本体变成**勾选 / 取消**(不再进详情;动作条暂时收起,
|
|
199
199
|
免得一边多选一边误点启停删),工具条与列表之间出现操作条:「已选 N 个容器」+
|
|
200
200
|
`聚合日志` + `取消`。`聚合日志` 至少需要 2 个容器;选到第 3 个时给一条软提示,
|
|
201
|
-
超过 **3
|
|
201
|
+
超过 **3 个**按钮置灰并提示上限(**上限怎么来的见「已知限制」的聚合上限**,
|
|
202
202
|
操作条上也写着那句说明)。点 `聚合日志`
|
|
203
|
-
进入聚合视图:直接复用 Compose
|
|
204
|
-
|
|
203
|
+
进入聚合视图:直接复用 Compose 项目视图那套聚合日志(**一条** `/logs/stream?ids=…`
|
|
204
|
+
装下所有选中容器,帧里带 `id`,按 `[service]` / 容器名前缀混流,带过滤与自动滚动),
|
|
205
|
+
返回即退出选择态并清空。
|
|
205
206
|
聚合视图的内容控制与**单容器日志完全对齐**(文本过滤 + 级别门槛 + `⬇ .log` / `⬇ .md`
|
|
206
207
|
导出当前显示内容 + 行数统计),另有聚合独有的两项:**按时间 / 按到达**排序、**暂停**冻结
|
|
207
208
|
(暂停时流继续接收、恢复一次性补齐)。
|
|
@@ -290,10 +291,10 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
290
291
|
服务端发 `end`(reason=`stats-exit`),界面提示并自动切回快照轮询。
|
|
291
292
|
- **Compose 项目视图**:工具条第三段切换。把 `composeProject` / `composeService`
|
|
292
293
|
标签聚合成「项目 → 服务 → 容器」,每个项目一行看全运行数 / 不健康数 / 服务数与
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
各容器状态;点进项目可看服务表,或打开**项目级聚合日志**——**一条**
|
|
295
|
+
`/logs/stream?ids=…` 装下项目内所有容器(帧里带 `id`),客户端按 `[service]` 前缀
|
|
296
|
+
混流,带自动滚动开关与过滤框。混流是到达序,不保证跨容器严格时序。某个容器的子流
|
|
297
|
+
失败只标注「部分出错」,其余容器的日志照常收(连接不关)。
|
|
297
298
|
- **镜像**:`docker images` 列表(reference / 大小 / 创建时间 / 短 ID);
|
|
298
299
|
`<none>:<none>` 的 dangling 镜像带 `dangling` 标记。搜索框与「N / M 个镜像」
|
|
299
300
|
计数**固定在工具条里**(不随列表滚走),表头列名在表体内吸顶。
|
|
@@ -357,7 +358,7 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
357
358
|
|
|
358
359
|
`kind=ssh` 有两种填法:
|
|
359
360
|
|
|
360
|
-
1. **引用 tty 连接簿条目**:`book`
|
|
361
|
+
1. **引用 tty 连接簿条目**:`book` 填条目名(在「终端面板」的
|
|
361
362
|
连接簿里维护),主机 / 端口 / 用户名 / 认证方式随之生效。设置卡片的下拉
|
|
362
363
|
只列出 tty 已保存的连接簿条目;tty 未安装或条目不存在时,该目标解析失败,
|
|
363
364
|
面板与 agent 工具都会给出明确错误。
|
|
@@ -427,8 +428,9 @@ context、不改任何环境变量;空 `targets`(= 用户没配)的语义
|
|
|
427
428
|
|
|
428
429
|
### 聚合日志(多选 / Compose 项目)
|
|
429
430
|
|
|
430
|
-
多选容器或打开一个 Compose
|
|
431
|
-
|
|
431
|
+
多选容器或打开一个 Compose 项目,都能把多个容器的日志聚合成一条流:**一条**
|
|
432
|
+
`/logs/stream?ids=…` 装下全部容器(帧里带 `id`;宿主侧仍为每个容器各跑一条
|
|
433
|
+
`docker logs -f`),客户端按到达顺序混流。工具栏提供:
|
|
432
434
|
|
|
433
435
|
| 控件 | 语义 |
|
|
434
436
|
| --- | --- |
|
|
@@ -472,7 +474,7 @@ context、不改任何环境变量;空 `targets`(= 用户没配)的语义
|
|
|
472
474
|
- **排序**:OOM > 僵死 > 不健康 > 反复重启 > 非零退出;同权重按「最近一次结束时间」倒序,
|
|
473
475
|
刚崩的排在最上面(行 hover 显示结束/启动时间、重启次数、退出码)。
|
|
474
476
|
|
|
475
|
-
##
|
|
477
|
+
## 配置(保存即热生效)
|
|
476
478
|
|
|
477
479
|
未获宿主授权时,设置卡片里那两个开关**点它会就地发起一次授权**:面板给出一条「在宿主终端执行」
|
|
478
480
|
的命令,执行完十秒内自动解锁并替你打开开关(免重启);面板里另有「另一种方式(最强)」说明
|
|
@@ -504,7 +506,6 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
504
506
|
|---|---|
|
|
505
507
|
| `0.2.0-rc.1` 起 | **插件详情页「说明」正下方**(`plugins.bundle.config`,key = **本包自己的** bundle 包名)——设置就在详情页里,不必再点一次「>」进二级页 |
|
|
506
508
|
| `0.1.6` 线 | 侧边栏「插件」→ 该 bundle 的**行** → 「>」行详情(`plugins.row.config`) |
|
|
507
|
-
| `≤0.1.5` | 设置 → 插件 → 该插件的可折叠卡片(`settings.plugin.item`) |
|
|
508
509
|
| 以上各版 | 设置 → **插件配置** 里的同一条目(`settings.kit.item`,由 `@hyzyn/dsh-kit-settings` 提供) |
|
|
509
510
|
|
|
510
511
|
新宿主上 bundle 槽可用时**不再注册本包那份 row 槽**——同一份表单两个入口只会让人以为有两套设置;
|
|
@@ -581,30 +582,36 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
581
582
|
| 工具 | 注册条件 | 参数 | 作用 / 典型用法 |
|
|
582
583
|
| --- | --- | --- | --- |
|
|
583
584
|
| `docker_targets` | 恒注册 | `probe?: boolean` | 列出目标(name / kind / label);`probe:true` 逐个探测 docker 版本与 daemon 可达性(SSH 目标会建连接,较慢)。其他工具的 `target` 取自这里 |
|
|
584
|
-
| `docker_connect_local` | 恒注册 | —(无参数) | **连接本机**:确保存在一条 `kind=local` 目标并选中它(已存在则复用,不重复创建;SSH / 自定义目标原样保留),随后只读探测 daemon:返回 `{name, kind, created, saved, reachable, serverVersion?}
|
|
585
|
-
| `docker_ps` |
|
|
586
|
-
| `docker_attention` |
|
|
587
|
-
| `docker_inspect` |
|
|
588
|
-
| `docker_logs` |
|
|
589
|
-
| `docker_logs_grep` |
|
|
590
|
-
| `docker_stats` |
|
|
591
|
-
| `docker_images` |
|
|
592
|
-
| `docker_events` |
|
|
593
|
-
| `docker_networks` |
|
|
594
|
-
| `docker_volumes` |
|
|
595
|
-
| `docker_image_inspect` |
|
|
596
|
-
| `docker_action` |
|
|
597
|
-
| `docker_image_remove` |
|
|
598
|
-
| `docker_image_prune` |
|
|
599
|
-
| `docker_image_pull` |
|
|
600
|
-
| `docker_exec` |
|
|
585
|
+
| `docker_connect_local` | 恒注册 | —(无参数) | **连接本机**:确保存在一条 `kind=local` 目标并选中它(已存在则复用,不重复创建;SSH / 自定义目标原样保留),随后只读探测 daemon:返回 `{name, kind, created, saved, reachable, serverVersion?}`。没有任何目标时其余工具不注册(见下方「零目标」那条),用它一步补上。**不需要任何能力授权**(只写插件自己的配置 + 只读探测);不会安装 Docker / 启动 daemon / 切 context / 改环境变量。daemon 不可达时在结果文本里说清成因(CLI 缺失 / 探测超时 / daemon 未启动 / socket 无权;CLI 缺失时还会点名 PATH 里找到的别的容器 CLI,但不会替你改 dockerBin),不是只报空列表 |
|
|
586
|
+
| `docker_ps` | 有目标即注册 | `target?`(**传 `*` = 全部目标**)、`all?: boolean`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 列容器(名称 / 状态 / 健康 / 镜像 / 端口 / compose 项目与服务 / 短 ID);默认只列运行中,`all:true` 含已停止。`target:'*'` 时按目标分组返回,**单个目标不可达不影响其他目标**(该组带 `error`)。端口是 IPv4/IPv6 双栈归并后的映射(同一次 `-p` 不再出现两遍,D130);`ports` 为空**不等于**没暴露端口——host 网络容器的端口即宿主机端口,这种情况会给 `net` 字段(D131)。排障第一步 |
|
|
587
|
+
| `docker_attention` | 有目标即注册 | `target?`(支持 `*`)、`limit?: number` | **需关注汇总**:不健康 / 反复重启 / 被 OOM 杀 / 非零退出 / 僵死;每条带 `reasons`、`exitCode`、`oomKilled`、`restartCount`。OOM 与真实退出码来自一次 `docker inspect`(ps 摘要里 137 无法区分手动 kill)。排障入口:不确定从哪台/哪个容器看起时先调它 |
|
|
588
|
+
| `docker_inspect` | 有目标即注册 | `target?`、`id`(必填) | `docker inspect` 的权威详情:状态 / 健康检查 / 退出码 / 重启次数 / 端口 / 挂载 / 网络 / 启动命令 |
|
|
589
|
+
| `docker_logs` | 有目标即注册 | `target?`、`id`、`tail?`(1~5000,默认 `logTailDefault`)、`timestamps?`、`since?`、`until?` | `docker logs --tail` 尾部;`since` / `until` 用 docker 语法(如 `10m`、`2026-09-09T10:00:00`)圈时间窗;超上限标记 `truncated`;截断时(`truncated`)结果里会说明可传更小的 `tail`、加 `since`/`until`,或改用 `docker_logs_grep` |
|
|
590
|
+
| `docker_logs_grep` | 有目标即注册 | `target?`、`id`、`pattern`(必填,≤512 字符)、`regexp?`、`ignoreCase?`、`since?`、`until?`、`context?`(0~20,默认 2)、`limit?`(1~500,默认 200)、`tail?`(缺省 = 扫全部历史)、`timestamps?`(默认开) | **日志服务端检索**:宿主侧逐行过滤(不碰远端 shell、不拼 `\| grep`),返回命中行 + 前后上下文,并如实报告 `matched`(全量命中数,即使没返回)/ `scanned` / `limited` / `truncated`——「只扫到这些」与「扫完了没更多」可区分;扫描窗口**对齐到行边界**(按字节截断处的半行不当作一行返回,所以 `lines` 里每一行都是完整的行);返回行数另有上限:最多 4000 行(`MAX_RESULT_LINES`),超了截到上限并给 `truncatedForSize` 与 `notes`(缩小 `limit` / 减小 `context` / 加 `since`·`until` 圈时间窗),`matched` 仍报全量。与 `docker_logs` 的分工:报错就在最近尾部 → `docker_logs`(便宜);埋在深处 / 特定时间窗 / 找关键词、异常码、请求 ID → 本工具。默认字面子串(grep -F 语义,零 ReDoS),`regexp: true` 才按 JS 正则 |
|
|
591
|
+
| `docker_stats` | 有目标即注册 | `target?`、`ids?`(逗号分隔的容器名/ID) | `docker stats --no-stream` 快照:CPU% / 内存用量与占比 / 网络 IO / 块 IO / PIDs;`ids` 省略 = 全部运行中容器。实时跟随是面板能力(SSE),工具保持单值快照语义 |
|
|
592
|
+
| `docker_images` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 镜像列表(仓库:标签 / 大小 / 创建时间 / 短 ID) |
|
|
593
|
+
| `docker_events` | 有目标即注册 | `target?`、`since?`(docker `--since` 语法,默认 `10m`) | 容器事件快照(`docker events --since <d> --until <now>`,同样过服务端白名单):start / die / stop / kill / oom / health_status / destroy / rename / update 九类,`exec_*` 等噪音已在服务端丢掉。要持续观察请让用户看面板容器列表的「活动」条 |
|
|
594
|
+
| `docker_networks` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 网络列表(名称 / 驱动 / 范围 / 是否 internal / 短 ID)。接入的容器列表不进列表行——详情页会连坐 inspect,列表逐行 inspect 就是 N 次 docker 调用 |
|
|
595
|
+
| `docker_volumes` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 卷列表(名称 / 驱动 / 范围 / 挂载点) |
|
|
596
|
+
| `docker_image_inspect` | 有目标即注册 | `target?`、`ref`(必填) | `docker image inspect` + `docker history`:大小 / 含父层大小 / 创建时间 / 平台 / 层数与层列表 / 入口与命令 / 暴露端口 / digest / 构建历史(每步命令与大小) |
|
|
597
|
+
| `docker_action` | `allowMutations` + 有目标 | `target?`、`action`(`start` \| `stop` \| `restart` \| `remove`)、`id` | 容器生命周期操作。`remove` 是破坏性的:删除容器配置与可写层(数据卷不在其中),执行前必须向用户确认目标容器 |
|
|
598
|
+
| `docker_image_remove` | `allowMutations` + 有目标 | `target?`、`ref`(必填) | 删除镜像(`docker image rm`,不带 `-f`)。**破坏性**:被容器或子镜像引用时会失败;执行前须确认目标镜像并复述后果 |
|
|
599
|
+
| `docker_image_prune` | `allowMutations` + 有目标 | `target?` | 清理 dangling(无标签)镜像(`docker image prune -f`)。刻意不加 `--all`,只删无标签镜像 |
|
|
600
|
+
| `docker_image_pull` | `allowMutations` + 有目标 | `target?`、`ref`(必填)、`timeoutSec?`(10~1800,默认 600) | `docker pull` 快照形态(**可能耗时数分钟**);交互式看逐层进度请让用户到面板镜像页的拉取图标看 SSE 进度流 |
|
|
601
|
+
| `docker_exec` | `allowExec` + 有目标 | `target?`、`id`、`command`(必填,经容器内 `sh -c` 执行)、`timeoutSec?`(1~120,默认 `execTimeoutSec`) | 一次性 `docker exec`,返回退出码 / stdout / stderr;无 TTY,交互式排障请让用户到 tty 面板跑 `docker exec -it <容器> sh` |
|
|
601
602
|
|
|
602
603
|
- `target` 省略时回落到**唯一**已配置目标;配置了多个目标则必填,错误信息会
|
|
603
604
|
列出可用目标名。
|
|
605
|
+
- **零目标时只有 `docker_targets` / `docker_connect_local` 注册**:其余工具没有可寻址对象,
|
|
606
|
+
注册了只会让模型白试一次;配好目标后**立即补注册**,无需重启。表中标「恒注册」的就是这两条。
|
|
607
|
+
- **四条列表工具支持分页**:`docker_ps` / `docker_images` / `docker_networks` / `docker_volumes`
|
|
608
|
+
都接受 `limit`(1~500,默认 100)与 `offset`(默认 0),结果带 `total` / `truncated`,
|
|
609
|
+
截断时渲染文本会给出下一页的 `offset`。分页只解决「结果长到读不下去」,不改变单条
|
|
610
|
+
docker 命令 512KB 的上限(超了照旧报错)。
|
|
604
611
|
- 两个开关变化会**立即重注册**工具:关掉 `allowMutations` / `allowExec` 后,
|
|
605
612
|
对应工具(含三个镜像变更工具)从 agent 侧消失,无需重启。
|
|
606
|
-
- **已注册的调用还受会话权限档位闸(tier gate)约束**:按类分——`read`
|
|
607
|
-
|
|
613
|
+
- **已注册的调用还受会话权限档位闸(tier gate)约束**:按类分——`read` 类(上表
|
|
614
|
+
标「有目标即注册」的读工具)全档放行;`docker_action` / `docker_image_remove` / `docker_image_prune` /
|
|
608
615
|
`docker_image_pull` 归 `write` 类,`docker_exec` 归 `exec` 类,这两类在受限档(仅可查看 /
|
|
609
616
|
工作区内修改)下**每次调用弹授权询问**(人允许这一次才执行;无人值守即审批策略 `never`
|
|
610
617
|
时确定性拒绝),完全权限档零询问,Auto review 档不叠加(宿主的模型预审已在管)。宿主未组合
|
|
@@ -616,8 +623,10 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
616
623
|
`docker_logs_grep` 带 `pattern` / `since` / `until` 检索深处)→
|
|
617
624
|
`docker_inspect` → `docker_stats`;镜像排查 `docker_images` →
|
|
618
625
|
`docker_image_inspect`。
|
|
619
|
-
- `announceToAgent` 开启时,插件向 systemPrompt
|
|
620
|
-
|
|
626
|
+
- `announceToAgent` 开启时,插件向 systemPrompt 注入一段精简公告:插件与面板位置、「默认
|
|
627
|
+
只读」与「docker socket ≈ 目标主机 root」的约束、「`allowExec` 只管一次性 exec 通道」的
|
|
628
|
+
边界(卡片「终端」按钮不受它门控)、`target` 的 `*` 适用范围,以及跨工具排障顺序;它不
|
|
629
|
+
逐条复述工具用法——那是各工具 description 的职责。
|
|
621
630
|
|
|
622
631
|
## HTTP 路由(`/api/dsh-docker` 前缀,全部 loopback 围栏)
|
|
623
632
|
|
|
@@ -655,7 +664,7 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
655
664
|
| `/inspect` | POST | `{target?, id}` | `{ok:true, details: ContainerDetail[]}` |
|
|
656
665
|
| `/stats` | POST | `{target?, ids?: string[]}` | `{ok:true, stats: ContainerStats[]}` |
|
|
657
666
|
| `/logs` | POST | `{target?, id, tail?, timestamps?, since?}` | `{ok:true, logs:{id, text, truncated}}` |
|
|
658
|
-
| `/logs/stream` | GET | query:`target?`、`id
|
|
667
|
+
| `/logs/stream` | GET | query:`target?`、`id`(单容器,与 `ids` 二选一)或 `ids`(逗号分隔的多个容器,**一条连接装下全部**,最多 32 个)、`tail?`(**0**~5000;`0` = 不补历史只跟随,客户端重连用)、`timestamps?`(`1`/`true`)、`since?` | `200 text/event-stream` 长连接,事件协议见下;参数错误 / 未知目标 / 非 loopback 返回常规 JSON 错误 |
|
|
659
668
|
| `/stats/stream` | GET | query:`target?`、`ids?`(逗号分隔;省略 = 全部运行中) | `200 text/event-stream`:每秒一帧 `stats`(ContainerStats,形状与 /stats 快照一致);不会自然结束,靠客户端断连收尾 |
|
|
660
669
|
| `/events/stream` | GET | query:`target?` | `200 text/event-stream`:一帧 `event` 一个容器事件(服务端已过白名单,值缺失的字段省略);不会自然结束,靠客户端断连收尾 |
|
|
661
670
|
| `/images` | POST | `{target?}` | `{ok:true, images: ImageSummary[]}` |
|
|
@@ -691,17 +700,26 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
691
700
|
| 事件 | data | 说明 |
|
|
692
701
|
| --- | --- | --- |
|
|
693
702
|
| `ready` | `{}` | **执行器真的起来了**才发一帧(本机子进程 `spawn` / 远端通道已开 = `StreamHandlers.onStart`):客户端据此才认为「在收数据」。**响应头到达(`onopen`)不算**——响应头由 `flushHeaders()` 立刻写出去,而建连(SSH `acquire` / 远端开通道)可能卡十几秒,目标不可达时干脆只有 `error`。起不来就**没有** `ready` |
|
|
694
|
-
| `line` | `{"d":"..."}` / `{"e":"..."}` | stdout / stderr
|
|
703
|
+
| `line` | `{"d":"..."}` / `{"e":"..."}`;多源时 `{"id":"<容器 id>","d"\|"e":"..."}` | stdout / stderr 分片(不保证按行切,客户端自行拼行)。日志流与拉取流用。**`ids=` 多源时每帧带 `id`**,客户端按它把分片派回对应容器 |
|
|
704
|
+
| `source-end` | `{"id":"<容器 id>","code":N\|null}` | **仅多源**:该容器的 `docker logs -f` 自然退出(容器停止)。整条连接**不关**——其余容器照常推;`end` 要等所有源都结束 |
|
|
705
|
+
| `source-error` | `{"id":"<容器 id>","message":"..."}` | **仅多源**:该容器这一条子流失败(spawn 错 / 远端通道被拒)。整条连接也不关,其余容器继续;客户端标注「部分出错」 |
|
|
695
706
|
| `stats` | `ContainerStats` | 统计流专用:每秒每个容器一帧,字段与 `/stats` 快照完全一致。服务端按**扁平 `{...}` 抽取**(`docker stats` 即便 stdout 是管道也走 TTY 渲染器,帧里混着 `ESC[H/ESC[K/ESC[J`,按行解析会整行丢掉)并折叠同一采样的重复渲染,客户端不必再解析 docker 的 PascalCase 字符串 |
|
|
696
707
|
| `event` | `{action, name, image, composeProject?, time?, exitCode?}` | 事件流专用:一帧一个容器事件(白名单外的事件行在服务端就丢了;值为 null 的字段省略) |
|
|
697
|
-
| `end` | `{"reason":"container-exit"\|"stats-exit"\|"events-exit"\|"pull-exit","code":N, ...}` |
|
|
708
|
+
| `end` | `{"reason":"container-exit"\|"stats-exit"\|"events-exit"\|"pull-exit","code":N, ...}` | 执行器自然退出。日志流附容器退出码(多源时取**按 `ids` 顺序最后一个非 null** 的退出码,各容器的真实退出码在各自的 `source-end` 里)、统计流 reason=`stats-exit`、事件流 reason=`events-exit`、拉取流附 `ref` |
|
|
698
709
|
| `error` | `{"message":"..."}` | 参数 / 执行失败,随后关闭;连接层断路不会发这个事件 |
|
|
699
710
|
|
|
711
|
+
**多容器单连接(`ids=`,D168)**:聚合日志(多选 / Compose 项目日志)把 N 个容器塞进
|
|
712
|
+
**一条** SSE——宿主为每个容器各起一条 `docker logs -f`,帧里带 `id` 分流。这样聚合视图
|
|
713
|
+
无论选几个容器都只占浏览器**一条**同源连接(旧实现是每容器一条 `EventSource`,叠加活动条
|
|
714
|
+
的事件流会直接吃满 D165 实测出来的 4 条常驻流)。单源 `id=` 的形状**逐字节未变**:帧不带
|
|
715
|
+
`id`、不发 `source-end`/`source-error`——老客户端仍按原协议解析。`/config` 的
|
|
716
|
+
`streamProtocol` 因此升到 **3**:客户端据此决定走单连接还是退回「每容器一条」。
|
|
717
|
+
|
|
700
718
|
四条流的差异只有「执行器 + 结束原因」:
|
|
701
719
|
|
|
702
720
|
| 流 | 执行器 | 自然结束条件 | 关闭语义 |
|
|
703
721
|
| --- | --- | --- | --- |
|
|
704
|
-
| `/logs/stream` | `docker logs --follow
|
|
722
|
+
| `/logs/stream` | `docker logs --follow`(`ids=` 时每个容器各一条) | 容器停止(`container-exit`;多源时全部容器的流都结束) | 前端关 FOLLOW / 切页 / 关面板 |
|
|
705
723
|
| `/stats/stream` | `docker stats`(**无** `--no-stream`) | 全部被统计容器退出(`stats-exit`) | **前端主动断**(这条流不会自己停) |
|
|
706
724
|
| `/events/stream` | `docker events`(`--filter type=container`) | daemon 侧流结束(`events-exit`) | 切页 / 切目标 / 关面板 |
|
|
707
725
|
| `/images/pull/stream` | `docker pull` | 拉取完成 / 失败(`pull-exit`) | 前端离开拉取视图 |
|
|
@@ -808,36 +826,40 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
808
826
|
SSH 长流会占住连接池中的该连接(busy),同一主机上的其它命令复用同一条连接——
|
|
809
827
|
但**不是互不影响**:通道额度是共享的,见下一条。**统计流不会自然结束**,关闭必须
|
|
810
828
|
由前端主动断 `EventSource`。
|
|
811
|
-
-
|
|
829
|
+
- **浏览器同源连接预算:docker 面板现在最多只占 1 条常驻流(D165 实测 + D168 收口)**:
|
|
812
830
|
宿主是 **HTTP/1.1**(没有 h2 多路复用),浏览器对**每个源**只放这么几条 TCP,而
|
|
813
831
|
**每条 `EventSource` 独占一条连接、且不会主动释放**。吃满之后,同一个页面里
|
|
814
832
|
**所有**普通请求(列表刷新、`/api/*`、会话加载……)一起排队——用户看到的是
|
|
815
|
-
**整个 DSH 页面卡住**,不只是 docker
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
| 4 条 | 8/8 成功,中位 25ms | — |
|
|
822
|
-
| **5 条** | **0/10 全部超时** | **41ms(正常)** |
|
|
823
|
-
|
|
824
|
-
最后一列是关键:**堵在浏览器,不在宿主**。所以常驻流预算取 **4 条**;面板停在容器页时
|
|
825
|
-
活动条的事件流恒开(占 1 条),聚合日志因此只能用 **3 条**——这就是聚合上限
|
|
826
|
-
(`PICK_MAX = 4 − 1`)的由来,操作条上写着同一句说明。
|
|
833
|
+
**整个 DSH 页面卡住**,不只是 docker 面板(实测:常驻流 0/4 条时页面请求正常,
|
|
834
|
+
**5 条时 10 次全部超时**;完整数据与现场见 [DEFECTS.md](./DEFECTS.md) 的 D165)。
|
|
835
|
+
docker 面板的常驻流**在任何视图下都只有 1 条**:容器列表 = 活动条事件流;详情 =
|
|
836
|
+
日志 FOLLOW 或统计 FOLLOW(同一时刻只有一个页签);聚合 / Compose 项目日志 =
|
|
837
|
+
**一条** `ids=` 多容器流;拉取 = 一条进度流。**进入详情 / 聚合视图时事件流会主动断开**
|
|
838
|
+
(列表被整段替掉、活动条不可见),回列表时重连并补一次全量刷新(D168)。
|
|
827
839
|
**这不是「性能建议」而是硬约束**:超了不是「慢一点」,是同页面所有请求一起失效。
|
|
828
|
-
|
|
829
|
-
到第 5
|
|
830
|
-
|
|
840
|
+
想自己复核:在 devtools 里连开
|
|
841
|
+
`new EventSource('/api/dsh-docker/events/stream?target=<目标>')` 到第 5 条,再打一次
|
|
842
|
+
普通请求即可看到它挂住;关掉就恢复。(WebSocket **不**占这份预算——tty 终端那条 WS
|
|
843
|
+
与 4 条 SSE 共存时页面仍正常,实测过。)
|
|
844
|
+
- **多标签页要按「全部插件加起来」算,不是 docker 一家**:那 6 条连接是整个页面共享的,
|
|
845
|
+
官方 `dsh-client-hmr`、其它插件的 SSE 都算在内。如果每个标签页里宿主自身就挂着 2 条
|
|
846
|
+
常驻 SSE,**3 个标签页 = 6 条**,已经到悬崖——这一层不是本插件能改的:真要治本得让
|
|
847
|
+
宿主换 WebSocket / h2,或让各插件共用一条多路复用连接。docker 这边已经把自家的账压到
|
|
848
|
+
1 条(见上一条),剩下的要按现场实际装了哪些插件去数。
|
|
849
|
+
- **聚合上限是「可读性 + 宿主通道」的上限,不再是浏览器连接数(D168)**:聚合日志是一条
|
|
850
|
+
连接装下所有选中容器,所以 `PICK_MAX = 3` 不再由「4 条预算 − 1 条事件流」推出来;
|
|
851
|
+
保留 3 是因为一屏混三份以上日志会看不清,且每个被选容器在宿主侧仍各跑一条
|
|
852
|
+
`docker logs -f`(SSH 目标上就是一条 channel,见下一条的 8 条额度)。操作条上写着
|
|
853
|
+
同一句说明。
|
|
831
854
|
- **SSH 目标上的通道额度是共享的(`MaxSessions`)**:一个目标只维持**一条** TCP 连接,
|
|
832
855
|
所有长流与短命令共用这条连接上的通道,而 OpenSSH 的 `MaxSessions` 默认只有 10。
|
|
833
|
-
长流(日志 / 统计 /
|
|
834
|
-
|
|
835
|
-
`(SSH) Channel open failure: open failed
|
|
836
|
-
8 条长流**(= 10 − 2,留两条给刷新 / inspect
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
折叠右侧栏标签会主动断流、把通道还回去。
|
|
856
|
+
长流(日志 / 统计 / 事件)会一直占到用户关掉面板为止,聚合日志里每个被选容器仍各占
|
|
857
|
+
**一条** channel(选中上限 3,所以最坏 3 条)——叠上刷新 / inspect 这类短命令就可能
|
|
858
|
+
逼近额度,于是远端拒绝时看到的是 `(SSH) Channel open failure: open failed`。所以插件
|
|
859
|
+
对每个 SSH 目标限制**同时最多 8 条长流**(= 10 − 2,留两条给刷新 / inspect 这类短命令),
|
|
860
|
+
聚合选择的上限也定在 3。超限与远端拒通道时给出的都是带指向性的提示,而不是 ssh2 的
|
|
861
|
+
原始文案。若你的 sshd 调过 `MaxSessions`(`sshd -T | grep maxsessions`),当前上限是
|
|
862
|
+
编译期常量,需要跟着改就提 issue。折叠右侧栏标签会主动断流、把通道还回去。
|
|
841
863
|
(`D150`)这条约束另有两道补救:① **短命令按目标排队** —— 每条连接同时最多 2 条
|
|
842
864
|
并发(`DSH_DOCKER_SHORT_CHANNELS` 可覆盖,仅测试 / 排障),多出来的等前一条收尾,
|
|
843
865
|
不再出现「一次点击(概览 inspect + 日志快照 + 统计快照)就去撞 10 的硬上限」;
|
|
@@ -850,7 +872,7 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
850
872
|
—— `AbortSignal` 触发时除了向远端 `KILL` + 关通道,还会**直接结束这条流**,不再等远端
|
|
851
873
|
回话。原先只等远端的 `close` 事件:远端不确认(上面那种被拒的 KILL,或通道静默)时,
|
|
852
874
|
那条流的 `busy` 计数**永远不会减**,累积到 8 之后该目标的每条长流都被配额拒绝,
|
|
853
|
-
|
|
875
|
+
且没有任何自愈路径(现场与完整因果见 [DEFECTS.md](./DEFECTS.md) 的 D164)。同一类坑
|
|
854
876
|
`run()` 在 `D112` / `D161` 已经踩过两次,现在三条收尾路径统一成「自己 settle」。
|
|
855
877
|
**根治办法在 sshd 一侧**:把那台主机的
|
|
856
878
|
`MaxSessions` 调大 —— 例如 `/etc/ssh/sshd_config.d/10-dsh-maxsessions.conf` 写一行
|
|
@@ -956,7 +978,8 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
956
978
|
│ ├─ 事件 → EventSource /events/stream(SSE:活动条环形缓冲 50 条 +
|
|
957
979
|
│ │ 500ms 防抖触发列表重取;重连成功后补一次全量刷新)
|
|
958
980
|
│ ├─ 拉取 → EventSource /images/pull/stream(SSE:逐层进度按层键 upsert)
|
|
959
|
-
│ └─ 聚合日志 →
|
|
981
|
+
│ └─ 聚合日志 → **一条** EventSource /logs/stream?ids=…(多容器共用;帧带 id,
|
|
982
|
+
│ 客户端按 [service] 混流;老宿主退回每容器一条)
|
|
960
983
|
├─ 可选消费 tty 的 ttyConnbar 服务 → SSH 连接栏「容器」按钮
|
|
961
984
|
│ (按 book 名 / host:port 匹配已配置目标,命中才出现)
|
|
962
985
|
├─ 可选消费 tty 的 ttyPanel 服务(0.16.0)→ 终端面板开着时挂右侧 dock
|
|
@@ -984,7 +1007,8 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
984
1007
|
│ ├─ loopback 围栏 + assertRef / assertImageRef + tail 夹紧(与快照路由一致)
|
|
985
1008
|
│ ├─ 写头 + flushHeaders / 15s ping 心跳 / 活跃流登记 / 前端断开静默 abort
|
|
986
1009
|
│ │ 管道起来后先发 ready{}(StreamHandlers.onStart:客户端只认它当「在收数据」)
|
|
987
|
-
│ ├─ /logs/stream:docker logs -f → line{"d"|"e"} + end{container-exit,code}
|
|
1010
|
+
│ ├─ /logs/stream:docker logs -f → line{"d"|"e"} + end{container-exit,code};
|
|
1011
|
+
│ │ ids= 模式:一条连接 N 个执行器 → line{id,d|e} + 每容器 source-end/source-error
|
|
988
1012
|
│ ├─ /stats/stream(只读):docker stats 逐行归一 → stats{ContainerStats};
|
|
989
1013
|
│ │ 不会自然结束,前端断连即 abort,docker stats 自退则 end{stats-exit}
|
|
990
1014
|
│ ├─ /events/stream(只读):docker events --filter type=container →
|
|
@@ -996,9 +1020,9 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
996
1020
|
├─ 网络 / 卷路由:/networks · /volumes 与各自的 inspect(只读)、remove / prune(allowMutations)
|
|
997
1021
|
└─ agent 工具:docker_targets / docker_ps / docker_inspect /
|
|
998
1022
|
docker_logs(快照语义不变)/ docker_logs_grep(服务端检索)/ docker_stats / docker_events / docker_images /
|
|
999
|
-
docker_image_inspect / docker_networks / docker_volumes
|
|
1023
|
+
docker_image_inspect / docker_networks / docker_volumes(有目标即注册)
|
|
1000
1024
|
+ docker_action / docker_image_remove / docker_image_prune / docker_image_pull
|
|
1001
|
-
(allowMutations
|
|
1025
|
+
(allowMutations + 有目标)/ docker_exec(allowExec + 有目标)
|
|
1002
1026
|
```
|
|
1003
1027
|
|
|
1004
1028
|
## 开发与验收
|
|
@@ -1007,7 +1031,7 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
1007
1031
|
pnpm --filter @hyzyn/dsh-docker build # tsc → lib/(宿主半体)+ esbuild → client.js(浏览器半体)
|
|
1008
1032
|
pnpm --filter @hyzyn/dsh-docker typecheck
|
|
1009
1033
|
pnpm --filter @hyzyn/dsh-docker smoke # 三套离线回归,都不需要 docker daemon(项数以脚本尾部自报为准)
|
|
1010
|
-
pnpm test # 仓库级 vitest
|
|
1034
|
+
pnpm test # 仓库级 vitest(本包 test/ 下的用例都在里面;项数以运行输出为准)
|
|
1011
1035
|
```
|
|
1012
1036
|
|
|
1013
1037
|
> **改了哪一半、怎么才生效**(踩过两次的坑):
|
|
@@ -1022,235 +1046,10 @@ pnpm test # 仓库级 vitest(含本包 conf
|
|
|
1022
1046
|
> 逻辑全都不生效,看起来像「改了没用」。判断依据是**文案**——宿主侧新增的提示语如果没出现,
|
|
1023
1047
|
> 那就是旧进程。
|
|
1024
1048
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
inspect 解析(状态 / 健康 / 退出码 / 挂载 / 网络 / 端口 / 缺字段不抛异常)、
|
|
1030
|
-
`assertRef` / **`assertImageRef`(放行 registry/digest、拒绝 flag 与注入)** 注入
|
|
1031
|
-
拒绝、`assertBin`、`formatBytes`、`shJoin` 转义、`DockerApi` 的 argv 构造
|
|
1032
|
-
(ps / logs / action / exec / probe 成败 / **image inspect·rm·prune·pull·statsStream·pullStream**)、
|
|
1033
|
-
`normalizeConfig` 默认值与夹紧、`sanitizeTargets` / `sanitizeHostKeys`、
|
|
1034
|
-
`resolveTarget` 的四种路径、`mergeTargetSecrets` 的凭证保留语义。
|
|
1035
|
-
|
|
1036
|
-
`scripts/route-smoke.mjs` 用**假 cordis ctx + 假 docker CLI 脚本**跑端到端:
|
|
1037
|
-
插件挂载(settings / 工具 / 路由 / 能力公告注册)、**27 条路由**的实际调用与返回
|
|
1038
|
-
(含 `/logs/stream`、`/stats/stream`、`/events/stream`、`/images/pull/stream` 四条 SSE
|
|
1039
|
-
的事件序列与参数校验,事件流另断噪音被白名单丢掉)、`docker_events` 工具的快照
|
|
1040
|
-
输出与 `since` 字符集校验、`/images/inspect` 的详情 + 构建历史、`/config` 凭证脱敏、未知配置键
|
|
1041
|
-
400、默认只读时 `/action`、`/exec` 与镜像变更(remove / prune / pull 流)403 且
|
|
1042
|
-
对应工具不注册、打开开关后(含 `settings/updated` 热更新路径)立即解锁、非
|
|
1043
|
-
loopback 403(含三条流路由)、容器名 / 镜像引用注入尝试被白名单拒绝、省略
|
|
1044
|
-
`target` 的回落与多目标报错、禁用后流路由 403。
|
|
1045
|
-
|
|
1046
|
-
`scripts/client-smoke.mjs` 在 Node 里用最小 DOM / React 桩执行构建产物
|
|
1047
|
-
`client.js`:验证注册 id 与 factory 形状、只 require 平台 seed 提供的模块
|
|
1048
|
-
(`react` / `react/jsx-runtime` / `react-dom/client`)、`apply` 注册的 settings
|
|
1049
|
-
卡片 key 等于命名空间 `docker`、找不到宿主侧边栏时安静降级且卸载可重复调用,ttyConnbar 集成的四条路径(连接簿名命中 / host:port 命中 / 未配置主机不加按钮 / tty 未安装静默跳过),FOLLOW 的 SSE 订阅与「回到底部」交互(静态断言),**镜像详情 / 拉取流 / 删除 / prune 入口**、**统计 FOLLOW + sparkline 钩子**、**Compose 分组与聚合日志**、**「活动」条装配 + 事件环形缓冲 / 动作标签 / 防抖**(纯逻辑经 `__events` 测试缝),以及侧边栏折叠态(`data-sidebar-collapsed`)隐藏入口标签的样式规则。
|
|
1050
|
-
需要真 daemon 的验证走下面的手工清单。
|
|
1051
|
-
|
|
1052
|
-
`test/logs-stream.test.ts`(例数不写死,`vitest run` 的输出是唯一真相源)覆盖日志实时流的四层:
|
|
1053
|
-
`logsStream` 的 argv 构造与 `assertRef` 白名单、SSE 帧的单行 JSON 封装(换行 /
|
|
1054
|
-
多字节)、本地流生命周期(假 spawn:跨 chunk 多字节、SIGTERM→SIGKILL 阶梯、
|
|
1055
|
-
close resolve、spawn error)与 SSH 长流的 busy 计数配对 / sweeper 跳过、以及
|
|
1056
|
-
路由层的事件序列 / 心跳 / 客户端断开静默中止 / 插件禁用统一收尾,以及
|
|
1057
|
-
**`ready` 只在 `spawn` 之后发**(响应头已 flush 也一帧不发,且排在第一条日志行前面)、
|
|
1058
|
-
**`tail=0` 只跟随不补历史**(D133)、**背压不重写已写过的帧**与**积压时丢最旧 + `skip` 通知、
|
|
1059
|
-
不掐流**(D153)、**skip 账目精确**(D156:skip 排队期间再丢弃,两段缺口都要报——
|
|
1060
|
-
46 块逐块对账「每一帧要么送达、要么被 skip 记账」)、
|
|
1061
|
-
**中止长流后 `busy` 必须归零**(D164:远端不确认关通道、累积 8 次都不会把目标钉死,
|
|
1062
|
-
且收尾后的分片不再投递)、
|
|
1063
|
-
以及**分片合帧**(D151:十个分片只出一个 `line` 帧;未到窗口就收尾时 `end` 仍排在
|
|
1064
|
-
这些行之后)、**`ready` 帧只在管道起来后发**(统计 / 事件 / 拉取三条路由各一例:
|
|
1065
|
-
`spawn` 之前一帧都没有——响应头已经 flush 了也不算;另两例钉住「`ready` 排在第一条
|
|
1066
|
-
数据帧前面」与「起不来(`spawn` 失败)不发 `ready`」)。合帧器本身的规则(窗口合并 /
|
|
1067
|
-
攒满即推 / flush 幂等 / dispose 停表)在 `test/sse-coalesce.test.ts`(6 例)。
|
|
1068
|
-
|
|
1069
|
-
`test/log-window.test.ts`(17 例)覆盖**窗口化挂载的纯逻辑**(D152,`client-src/log-window.js`):
|
|
1070
|
-
实测高度进缓存而没量过的行用估算、`layout` 非贴底时的窗口定位与上下垫高自洽、贴底时锚到
|
|
1071
|
-
列表尾部且下垫为 0(贴底判定精确的前提)、`reanchor` 只在锚点上方行高变化时产生等量补偿、
|
|
1072
|
-
`offsetOf` 与「淘汰头部后补偿量 = 被淘汰行总高」(D154 跨代锚点的数学)、
|
|
1073
|
-
缓存 FIFO 淘汰与空表边界。真 DOM 那一层(量高、锚点修正、贴底钉住)由性能门禁在真 Chrome
|
|
1074
|
-
里验证,离线侧另有 `scripts/client-smoke.mjs` 的 `__logWindow` 缝。
|
|
1075
|
-
|
|
1076
|
-
`pnpm --filter @hyzyn/dsh-docker perf:logs`(`scripts/log-perf.mjs`)是**日志页的浏览器侧
|
|
1077
|
-
性能门禁**:用真 Chrome 量四个场景——「5000 行首屏耗时 / DOM 规模」、「FOLLOW 突发 20000 行
|
|
1078
|
-
的处理耗时与事件循环最大延迟」、「事件洪泛」(同一 6.2k 行/秒下每事件 1 行 vs 每事件 50 行,
|
|
1079
|
-
量的就是 D151 合掉的那笔每事件固定开销)、以及「**持续洪泛** 20k 行/秒 × 6s」的稳态
|
|
1080
|
-
(缓冲满了之后每帧都在淘汰 + 插入,这才是用户报的「日志一快整页就卡」),并**校验两件容易被误解的事**:① 快照被「输出上限(KB)」字节闸截断时**必须有**「已截断」黄色横幅
|
|
1081
|
-
(否则「LINES 选 1000 只回来 985 行」会被当成丢行——实测那 985 行是 docker 自己的记录粒度,不是丢行);
|
|
1082
|
-
② 「上滚暂停跟随」只认**真手势**(wheel / 触摸 / 滚动条 / 键盘):真手势上滚必须出现
|
|
1083
|
-
「回到底部」且点一下能恢复,没有手势的位置漂移(洪泛里的夹紧/抖动)**必须不停跟随**;
|
|
1084
|
-
③ **洪泛中驻留历史**(D154):跟随流没停、缓冲还在淘汰最旧的行时驻留 1.2s,可见区顶行
|
|
1085
|
-
必须还是同一行(环形缓冲淘汰的位移没补偿的话,读历史会自动「倒带」);**换代**
|
|
1086
|
-
(D155):快照先刷成一代会折行的高行并扫位量进缓存、再刷回普通行,垫高必须由**这一代**
|
|
1087
|
-
的高度算出来(不作废缓存则滚动高虚高 35644px);
|
|
1088
|
-
④ **校验窗口算得对**:
|
|
1089
|
-
快照内容是确定的,滚到顶第一行必须是 `seq=0`、滚到底最后一行必须是 `seq=4999`,洪泛结束后
|
|
1090
|
-
最后一行必须是最新行(贴底跟随还生效)。超预算即非零退出。D152 之后的实测:
|
|
1091
|
-
5000 行快照 179ms / **229 个正文节点**(此前 ~10000)、挂 53~57 行;突发 20k 行 933ms /
|
|
1092
|
-
最大延迟 **2ms**(此前 52~97ms);事件洪泛 10ms vs 7ms;持续洪泛 20k 行/秒 × 6s
|
|
1093
|
-
最大延迟 **6ms**、>50ms 长帧 **0 个**(此前 70~97ms / 25~26 个)、213 节点。预算里另有一条
|
|
1094
|
-
「挂载行数 ≤ 200」——虚拟滚动失效会立刻变红。它依赖 tty 的预览夹具
|
|
1095
|
-
(`node packages/tty/scripts/preview.mjs` 生成),夹具或 Chrome 缺失时**跳过并 exit 0**
|
|
1096
|
-
—— 它是 npm script,不是 vitest 必跑项。
|
|
1097
|
-
|
|
1098
|
-
`test/streams.test.ts`(46 例)覆盖**统计流 / 事件流 / 拉取流 / 网络卷 / 通用 SSE 基建**:
|
|
1099
|
-
`statsStream` 不带 `--no-stream`(与快照同一构造点)、`pullStream` 的
|
|
1100
|
-
`assertImageRef` 白名单、`/stats/stream` 把逐行 JSON(含跨 chunk 的半行)归一成
|
|
1101
|
-
`stats` 事件且形状与 `/stats` 快照一致、心跳、客户端断开静默中止、
|
|
1102
|
-
`eventsStream` 的 argv(无 `--since` / `--until`,带 `type=container` 过滤)与
|
|
1103
|
-
`events()` 快照的 `--since` + `--until`(不传 until 会永不退出)、
|
|
1104
|
-
`parseContainerEvent` 的白名单 / 坏行丢弃 / health_status 后缀 / 字段抽取 /
|
|
1105
|
-
`status` 老字段兼容、`/events/stream` 的事件序列与跨 chunk 半行、
|
|
1106
|
-
`/images/pull/stream` 的 allowMutations 门禁(403 不建流)/ `pull-exit` 收尾 /
|
|
1107
|
-
禁用时统一收尾、**`assertName` 的校验矩阵(`/` 与 `:` 必须被拒——这两条正是
|
|
1108
|
-
`assertImageRef` 会放行的)、网络 / 卷的 ls·inspect·rm·prune argv(prune 必带 `-f`)、
|
|
1109
|
-
network / volume 的 ls·inspect 解析容错(字符串布尔、缺 `Mountpoint` 的老版本、
|
|
1110
|
-
空输出 / 坏行)、`/networks` 与 `/volumes` 八条端点的门控(未开门 remove/prune 403、
|
|
1111
|
-
缺 name 400、非法名 500)**,以及 `formatBytes`。
|
|
1112
|
-
|
|
1113
|
-
### 手工验收清单
|
|
1114
|
-
|
|
1115
|
-
1. **本机目标**:加一条 `kind=local` 的 `本机`,`probe` 返回 server 版本;
|
|
1116
|
-
容器列表与 `docker ps -a` 一致(含已停止容器)。
|
|
1117
|
-
2. **SSH 目标**:tty 连接簿里已有条目时,用 `book` 引用它 → 容器列表 / 详情 /
|
|
1118
|
-
日志正常;首次连接日志里出现「已记录 host key 指纹(TOFU)」,第二次不再
|
|
1119
|
-
提示;手动改掉 `hostKeys` 里的指纹后重连,应**被拒绝**并给出重置指引。
|
|
1120
|
-
3. **一键连接本机**(两处入口都要试):
|
|
1121
|
-
- **没有目标时**:把目标全部删空保存(或用一个全新 profile),打开面板 → 正文空态里
|
|
1122
|
-
有一个「连接本机」主按钮,**下拉里不该重复列**这一项 → 点一下应当出现一条
|
|
1123
|
-
`kind=local` 的 `local` 并立刻选中、容器列表出来;
|
|
1124
|
-
- **已有别的目标时**:留着几台 SSH,打开目标下拉 → 末一项是 `+ 连接本机`,
|
|
1125
|
-
**工具条上不该有**任何常驻入口 → 选中该项,应当同样加上 `local` 并切过去,
|
|
1126
|
-
且该项随即从下拉里消失(已经有本机目标了)。
|
|
1127
|
-
两处都**不该**出现「同一个入口列了两次」;全程**不该**有任何 Docker 安装 / daemon 启动 /
|
|
1128
|
-
context 切换动作。宿主上没有 docker 时,应当看到一句点名的原因(CLI 缺失 / 探测超时 /
|
|
1129
|
-
daemon 未启动 / socket 无权访问),而不是一个空列表。
|
|
1130
|
-
- **只有 podman 的机器**(把 PATH 里的 docker 拿掉、只留 podman):那句话应当
|
|
1131
|
-
**点名 podman** 并说明「把『docker CLI』填成 podman 就能用」,同时**不该**替你
|
|
1132
|
-
改 `dockerBin`(回头去设置卡片看,值仍是 `docker`)。
|
|
1133
|
-
4. **只读拦截**:两个开关都关时,`/action`、`/exec`、`/images/remove`、
|
|
1134
|
-
`/images/prune`、`/images/pull/stream` 全部 403;agent 侧恒注册 13 个只读工具
|
|
1135
|
-
(`docker_targets` / `connect_local` / `ps` / `attention` / `inspect` / `logs` /
|
|
1136
|
-
`logs_grep` / `stats` / `events` / `images` / `image_inspect` / `networks` / `volumes`,数量以工具表为准),
|
|
1137
|
-
面板的启停删 / 镜像删除 / 清理 / 拉取按钮置灰。打开「允许变更操作」后这些
|
|
1138
|
-
路由与工具立即出现(无需重启)。
|
|
1139
|
-
5. **日志 / 统计 / 镜像**:`tail` 与 `timestamps` / `since` 生效;统计显示
|
|
1140
|
-
CPU、内存、网络与块 IO;镜像列表含 dangling 条目标记。
|
|
1141
|
-
6. **FOLLOW 实时日志流**:日志页打开 `FOLLOW` → 状态行先「正在连接」后
|
|
1142
|
-
「实时跟随中」,`docker logs -f` 的新行即时出现(`docker run --rm alpine sh
|
|
1143
|
-
-c 'i=0; while :; do echo line-$i; i=$((i+1)); sleep 1; done'` 可观察);
|
|
1144
|
-
高吞吐护栏:换成长行快速打印(如 `head -c 2000 /dev/zero | tr "\\0" x; echo` 循环)
|
|
1145
|
-
页面应保持可交互、内存平稳,行数 / 字节超限时出现「已丢弃最早内容」提示;
|
|
1146
|
-
开 FOLLOW 时 `AUTO REFRESH` 置灰、轮询停止;向上滚动出现「回到底部」、
|
|
1147
|
-
点击回到底部并恢复自动贴底;关闭 FOLLOW 立即回到快照。停掉容器 → 流收到
|
|
1148
|
-
`end` 提示「容器已退出(退出码 N)」并自动补一次快照。杀掉 `dsh web` 再拉起
|
|
1149
|
-
(或热改插件配置)→ 状态行短暂「正在重连」后自愈,不弹错误横幅。SSH 目标
|
|
1150
|
-
同样跑一遍,确认流跑着时同一主机的 `docker ps` 面板操作不受影响(连接复用),
|
|
1151
|
-
且空闲回收(120s)不会掐断流。
|
|
1152
|
-
7. **统计实时跟随**:统计页打开 `FOLLOW` →「正在连接统计流…」→「实时跟随中
|
|
1153
|
-
(docker stats)」,CPU / 内存 sparkline 每秒长一点(跑 `docker run --rm
|
|
1154
|
-
alpine sh -c 'while :; do :; done'` 观察 CPU 起来);关 FOLLOW 立即回快照并
|
|
1155
|
-
恢复轮询。停掉被统计的容器 → 流收到 `end`(stats-exit)提示后自动回轮询。
|
|
1156
|
-
注意**这条流不会自然结束**:切页 / 关面板必须断掉 `EventSource`(宿主侧应
|
|
1157
|
-
看到 `docker stats` 被 SIGTERM)。
|
|
1158
|
-
8. **镜像详情 / 删除 / 清理**:镜像页点某行「详情」→ 概览里的层数与
|
|
1159
|
-
`docker image inspect` 一致、构建历史与 `docker history` 一致(Docker ≥ 26
|
|
1160
|
-
走 `--format`,老版本走纯文本表格兜底);dangling 行用镜像 ID 查。点「删除」→
|
|
1161
|
-
二次确认后执行 `docker image rm`;删除被容器引用的镜像应失败并给出提示。
|
|
1162
|
-
「清理 dangling」只删无标签镜像,输出末尾带 `Total reclaimed space`。
|
|
1163
|
-
9. **拉取进度流**:镜像页工具条的拉取图标(悬停显示「拉取镜像」)→ 输入本地没有的小镜像(如 `alpine:3.20`)
|
|
1164
|
-
→ 逐层状态行实时出现且按层原地更新;完成后提示「拉取完成」并自动刷新列表。
|
|
1165
|
-
拉取中点「停止」或离开视图 → `docker pull` 被 SIGTERM 结束,不残留进程。
|
|
1166
|
-
未开启「允许变更操作」时该按钮置灰、直接访问 `/images/pull/stream` 返回 403。
|
|
1167
|
-
10. **Compose 项目视图**:用一份 compose 起两三个服务(`docker compose up -d`)→
|
|
1168
|
-
工具条切到「Compose」→ 服务归到一个项目卡片下,运行数 / 服务数 / 状态正确;
|
|
1169
|
-
点进项目看服务表,切「聚合日志」→ 各服务日志按 `[service]` 前缀混流出现
|
|
1170
|
-
(`docker compose logs -f` 的等价物),过滤框可按服务名与内容过滤;关「自动
|
|
1171
|
-
滚动」后新日志继续进缓冲但视图不跳。无 compose 标签的容器归入
|
|
1172
|
-
「(非 compose 容器)」。
|
|
1173
|
-
11. **`allowMutations` 打开后**:stop / start / restart 成功;对运行中容器
|
|
1174
|
-
remove 报错并附「先停止再删除」提示,先 stop 再 remove 成功。
|
|
1175
|
-
12. **`allowExec` 打开后**:`ls -la /app` 之类命令返回 stdout 与退出码;把命令
|
|
1176
|
-
换成 `sleep 60`(`timeoutSec` 调小)应被中断并报超时;`command` 超过 8000
|
|
1177
|
-
字符被拒绝。
|
|
1178
|
-
13. **exec 抽屉与正文共存**(tty ≥ 0.15):卡片「终端」开抽屉后切到日志 / 统计 /
|
|
1179
|
-
另一台容器的详情,抽屉与终端会话都必须活着;**折叠**(箭头或双击顶边)把抽屉
|
|
1180
|
-
压成一条标题栏、会话继续跑(展开即回原样);**拖拽顶边**能调高度且不超过面板
|
|
1181
|
-
的 75%;此时点 backdrop 空白处或面板 ✕ 应弹「结束容器终端会话」确认,取消后
|
|
1182
|
-
会话仍在,确认才结束(tty 侧收到 kill)。
|
|
1183
|
-
离线回归:`node packages/tty/scripts/preview.mjs docker-exec-logs`
|
|
1184
|
-
(无头 Chrome 跑真实客户端:抽屉 + 日志页 + 折叠展开,断言会话未被结束)。
|
|
1185
|
-
14. **从连接栏进容器面板(dock 模式,需 tty ≥ 0.16)**:tty 面板里开一个 SSH 标签 →
|
|
1186
|
-
连接栏「容器」→ 容器面板应挂在**终端右侧**(不是全屏弹窗),终端仍可输入;
|
|
1187
|
-
拖左边缘可调宽(上限面板宽 72%)、标题栏箭头折叠成窄条(终端拿回全部宽度、
|
|
1188
|
-
容器面板不卸载)、✕ 收起面板且 SSH 标签不受影响;此时点卡片「终端」应在同一
|
|
1189
|
-
终端面板**新开一个 `<容器> · exec` 标签**,而不是再嵌一个终端抽屉。
|
|
1190
|
-
离线回归:`node packages/tty/scripts/preview.mjs docker-dock`
|
|
1191
|
-
(断言 docked / 无 backdrop / 折叠后终端变宽 / 容器面板存活)。
|
|
1192
|
-
15. **多选聚合日志**:容器列表点 `聚合选择` → 卡片左侧出现勾选框、卡片动作条收起;
|
|
1193
|
-
勾 2~3 个容器 → 操作条显示「已选 3 个容器」,点 `聚合日志` → 聚合视图标题为
|
|
1194
|
-
「聚合日志 · 3 个容器」,三条流按 `[service]` / 容器名前缀混流出现;只勾 1 个时
|
|
1195
|
-
按钮置灰并提示「至少选择 2 个容器」,勾到 9 个时置灰并提示最多 8 个;列表里
|
|
1196
|
-
停掉其中一个容器 → 该条流走原有 `end` 语义结束,其余流不受影响。
|
|
1197
|
-
16. **Esc 退出选择态**:选择态下按 Esc → 回到普通列表、勾选清空、动作条消失;
|
|
1198
|
-
再点一次 `聚合选择`(此时标签是 `退出选择`)效果相同;切目标 / 切分段 / 关面板
|
|
1199
|
-
也都会把选择态和勾选一起清掉。
|
|
1200
|
-
17. **事件活动条**:容器列表头部出现「活动」条,状态点先黄后绿、文案「实时接收中
|
|
1201
|
-
(docker events)」;执行 `docker restart <容器>` / `docker stop`+`start` → 1 秒内
|
|
1202
|
-
列表状态跟着变,活动条出现 `stop` / `start`(异常退出显示 `die(137)` 这种带码
|
|
1203
|
-
形式);点标题可折叠(事件流不中断,展开后仍是累积的最新 8 条);
|
|
1204
|
-
`docker exec` 连续几次**不产生任何事件**(`exec_*` 已被服务端白名单丢掉);
|
|
1205
|
-
断网 / 重启 `dsh web` 后恢复时状态点短暂黄色并自动补一次全量列表刷新。
|
|
1206
|
-
**不得出现假绿**(2026-10-11 现场):把某个目标的 SSH 地址改成黑洞地址(或在目标
|
|
1207
|
-
不可达时打开面板)→ 状态点必须是**黄色**的「正在连接事件流…」,只有宿主确认
|
|
1208
|
-
`docker events` 起来(`ready` 帧)才转绿;连不上时最终落到红色的「事件流已断开」+
|
|
1209
|
-
「重连」按钮。日志 / 统计 / 拉取三条流同理(建连期间各自停在「正在连接…」,不会
|
|
1210
|
-
先说「实时跟随中 / 实时接收中 / 正在拉取」)。
|
|
1211
|
-
另有一条**不许卡死**:宿主半体仍是旧代码(例如宿主进程还没重启、`/config` 里没有
|
|
1212
|
-
`streamProtocol`)而浏览器已加载新 `client.js` 时,活动条必须退回旧判据并点亮——
|
|
1213
|
-
不能永远停在「正在连接事件流…」。
|
|
1214
|
-
18. **网络 / 卷**:工具条分段出现「网络」「卷」。网络列表应含 `bridge` / `host` / `none`
|
|
1215
|
-
与 compose 创建的项目网络;点某条进详情 → 概览里子网 / 网关与 `docker network
|
|
1216
|
-
inspect` 一致、`internal` 网络带徽标,「接入的容器」页签列出容器名与 IPv4。
|
|
1217
|
-
卷列表的挂载点与 `docker volume ls` 一致(超长路径省略、悬停看全量)。
|
|
1218
|
-
未开「允许变更操作」时两个 prune 图标与详情里的删除键都是灰的;打开后:
|
|
1219
|
-
删除一个没有容器接入的网络成功、删一个还在用的网络报错并给出提示;
|
|
1220
|
-
卷清理会弹带「数据会一起删除」的确认框。**注意**:卷清理在 docker < 23 上会连
|
|
1221
|
-
命名卷一起删,先在测试目标上确认为妙。
|
|
1222
|
-
|
|
1223
|
-
19. **多目标总览**:配两个以上目标(本机 + 一台 SSH)→ 目标选择器旁出现 `总览` pill,
|
|
1224
|
-
点进去一屏出现两张计数卡(各带「本机」/「SSH」徽标)与异常区;卡片数字与该目标
|
|
1225
|
-
容器列表的口径一致。把其中一个目标的 SSH 地址改错(或停掉远端 docker)再刷新 →
|
|
1226
|
-
只有那张卡描红、顶部出现「1 个目标不可达」,另一张卡照常显示结果,**页面不会**
|
|
1227
|
-
等 SSH 超时才出内容(渐进落地)。点异常行 → 落到那台主机上该容器的详情,返回后是
|
|
1228
|
-
**该目标**的容器列表而不是总览;点计数卡 → 该目标的容器列表。全部正常时异常区显示
|
|
1229
|
-
「一切正常」;还有目标没答完时显示「读取中…」。打开「自动刷新」→ 按
|
|
1230
|
-
`pollIntervalSec` 轮询全部目标(关掉即停)。
|
|
1231
|
-
20. **切目标不串台(列表写入闸)**:让 目标1(不可达的 SSH)先失败出「操作失败」横幅,
|
|
1232
|
-
随即切到正常的目标2 → 横幅应**随切换立即消失**、列表是 目标2 的容器;再等 20s
|
|
1233
|
-
让 目标1 的超时响应**在其之后**才回来 → 横幅不得重新出现、列表也不得被换成
|
|
1234
|
-
目标1 的容器。从总览点 目标1 那张红色计数卡进去,同样不该看到上一个目标的失败。
|
|
1235
|
-
反向验证:切到目标2 后立刻切回目标1(它仍然连不上)→ 横幅应写的是目标1 自己的
|
|
1236
|
-
失败,而不是把目标2 的成功结果当成了目标1 的。
|
|
1237
|
-
21. **`docker_logs_grep` 服务端检索**(2026-10-09 真机实测的口径):起一个日志内容已知的容器
|
|
1238
|
-
(用本地已有镜像即可,不必联网):
|
|
1239
|
-
`docker run -d --rm --name dsh-grep-probe <本地镜像> sh -c 'echo "[boot] starting"; for i in 1 2 3 4 5; do echo "tick $i ok"; done; echo "FATAL request-id=REQ-7788"; echo "WARN disk 91%" >&2; sleep 3600'`
|
|
1240
|
-
然后在会话里让 agent 用 `docker_logs_grep` 检索它,**逐项看原始返回值**(别接受转述):
|
|
1241
|
-
- 找 `FATAL` → 命中 1 行、「扫描 8 行」、命中行标 `>` 且带 ±2 行上下文;
|
|
1242
|
-
- `tick [0-9]+` **不传** `regexp` → 0 命中;传 `regexp=true` → 5 命中(证明默认是**字面**匹配);
|
|
1243
|
-
- `fatal` 不传 `ignoreCase` → 0;传 `ignoreCase=true` → 1;
|
|
1244
|
-
- `tick` + `limit=1` → 「命中 5 行」但只返回 1 组窗口,并出现「命中超过上限」提示(`limited`);
|
|
1245
|
-
- `since=1h` 命中 / `until=1h` 与 `since=1s` 各 0 命中**且「扫描 0 行」**(证明 `--since`/`--until` 真透传);
|
|
1246
|
-
- 找 `WARN` → 命中行的 stream 是 `[err]`;
|
|
1247
|
-
- 检索不存在的容器 → 报 docker 原文错误,**不是**空结果。
|
|
1248
|
-
`truncated` 只能用 **>8MB** 日志逼出来(扫描侧 8MB 硬下限,调小「输出上限(KB)」没用):
|
|
1249
|
-
让容器打 ~30 万行、探针分别写在**最开头**与**最末尾** → 全量扫时前者 0 命中 + 「扫描输出超过
|
|
1250
|
-
字节上限」提示,后者命中 + 同一提示;加 `tail=5000` 后两者都不再出现该提示(证明它报的是
|
|
1251
|
-
扫描侧**真溢出**,不是「日志大就恒 true」)。另外**返回的每一行都必须是完整行**(带时间戳
|
|
1252
|
-
前缀):字节闸的切口会切在行中间,`lineAlign` 负责丢掉那个半行(见 ROADMAP 的真机验收补正)
|
|
1253
|
-
—— 看到 `g line 174798` 这种没时间戳的碎片,就是这条回归了。
|
|
1049
|
+
验证面分三层:**三套离线 smoke**(不需要 docker daemon)、**本包的 vitest 套件**、
|
|
1050
|
+
以及**真 daemon 的手工验收清单**。逐项的覆盖范围、浏览器侧性能门槛与手工步骤见
|
|
1051
|
+
[docs/verification.md](./docs/verification.md)——README 只留「怎么跑」,清单不在这里重复
|
|
1052
|
+
(同一事实两处全文是债,见 [conventions § 文档分层](../../docs/conventions.md#文档分层))。
|
|
1254
1053
|
|
|
1255
1054
|
## 版本 / 许可证
|
|
1256
1055
|
|