@hyzyn/dsh-docker 0.14.0 → 0.15.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/README.en.md +596 -899
- package/README.md +538 -913
- package/lib/index.js +18 -5
- package/lib/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -6,57 +6,48 @@
|
|
|
6
6
|
|
|
7
7
|
## 特性
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
内存爬升 / 报错增长 / 新出现严重判据)、只列「真的变了」的数字变化与判据增减。agent 侧同一份
|
|
36
|
-
口径是 `docker_diagnose` 的 `history` 参数。
|
|
37
|
-
- **四条 SSE 长流共用一套基建**:日志 FOLLOW、`docker stats`、`docker events`、`docker pull` 走同一个 `openSseStream`(心跳 / 活跃流登记 / 断开清理 / 管道起来后先发一帧 `ready`),差异只在收尾语义——日志与拉取自然结束,统计与事件由前端主动断。「连上了」与「在收数据」是两件事:客户端只认 `ready`,响应头到达不算(见「SSE 事件协议」)。多选 / Compose 项目的聚合日志把多个容器装进**一条** `/logs/stream?ids=…`(D168),按 `--timestamps` 前缀还原跨容器真实时序,「暂停」只冻结渲染(流继续接收,恢复时一次性补齐)。
|
|
38
|
-
- **默认只读,能力开关分三级**:启停删 / exec / 镜像变更各自独立开关,未开启时 agent 工具**不注册**、HTTP 路由 403(能力不存在,而非调用后报错);开关的**提权走两条带外通道**(启动环境变量 / 卡片里就地确认,见下条);容器名与 ID 过白名单,命令一律 argv 构造 + 单引号转义,密码 / 口令以 `env:NAME` **凭据引用**(官方凭据层解析,缺失时退回环境变量)且永不回传浏览器。
|
|
39
|
-
- **与 dsh-tty 数据级复用、代码级不耦合**:不 import 任何 tty 代码,tty 也无需改一行源码,两者可各自安装与升级;装了 tty 则消费三个可选扩展点——连接栏动作(`ttyConnbar`)、终端承载(`ttyTerminal`:标签 / dock 承载下经 `open` 新开标签,模态下经 `mount` 就地嵌入抽屉)、以及只在兜底路径用到的终端右侧 dock(`ttyPanel.mountPane`);未装或版本不足逐项静默降级。
|
|
9
|
+
- **常驻会话右侧栏**:面板作为右侧栏标签(`sidebar.right.pane.tab`)与对话**同屏**——发完错误交给 Agent
|
|
10
|
+
后日志留在右边;折叠即退出视野并主动断流、把 SSH 通道还回去。面板级状态(目标 / 视图 / 过滤词 /
|
|
11
|
+
选中容器及页签)**跨会话保留**;拿不到右侧栏服务的老宿主退回 dock / 模态。
|
|
12
|
+
- **日志右键交给 Agent**:日志页拖选报错行 → 右键「直接发送到当前会话 / 填入输入框,我先改改」,投递
|
|
13
|
+
「选中的行 + 目标 / 容器 / 时间窗 + 前后各 20 行上下文」(会进模型上下文,菜单里明写留意凭证);回执
|
|
14
|
+
两处:视口级 toast,以及终端面板开着时**自动折起终端**(`ttyPanel` 契约 v2)。改稿走「填入输入框」
|
|
15
|
+
——**编辑面只有会话输入框一个**。
|
|
16
|
+
- **一键连接本机 / 多目标聚合取数**:两个**非常驻**入口(空态主按钮 / 目标下拉末项)一次点击就加上本机
|
|
17
|
+
目标(agent 侧 `docker_connect_local`);总览对全部 `targets[]` 并行取数,单个目标不可达只污染自己
|
|
18
|
+
那一格(`docker_ps target:"*"` 同一口径)。
|
|
19
|
+
- **「需关注」读权威字段**:不健康 / 反复重启 / OOM 被杀 / 非零退出 / 僵死;OOM 与真实退出码由一次
|
|
20
|
+
`docker inspect` 补齐——`docker ps` 摘要里的 137 分不出 OOM 与手动 kill(口径见 [总览](#总览跨目标与需关注口径))。
|
|
21
|
+
- **排障闭环**:诊断页签把四路采集变成「结论 + 判据 + 证据 + 时间线 + 建议」,诊断历史再答「是不是在
|
|
22
|
+
变坏」;「进程」页签给 `docker top` 快照;「磁盘」视图摊开可回收账与「谁在用」;概览底部反推一条可复现
|
|
23
|
+
的 `docker run`。见 [容器诊断](#容器诊断为什么不行) 与 [诊断历史](#诊断历史它是不是在慢慢变坏)。
|
|
24
|
+
- **四条 SSE 长流共用一套基建**:日志 FOLLOW、`docker stats`、`docker events`、`docker pull` 走同一个
|
|
25
|
+
`openSseStream`(心跳 / 活跃流登记 / 断开清理 / 管道起来后先发一帧 `ready`),差异只在收尾语义;聚合
|
|
26
|
+
日志把多个容器装进**一条** `/logs/stream?ids=…`。「连上了」与「在收数据」是两件事:客户端只认 `ready`。
|
|
27
|
+
见 [SSE 事件协议](#sse-事件协议logsstreamstatsstreameventsstreamimagespullstream)。
|
|
28
|
+
- **默认只读,两个独立开关**:`allowMutations`(容器启停删 + 镜像删除 / 清理 / 拉取)与 `allowExec`
|
|
29
|
+
(一次性 `docker exec`);关闭时对应 agent 工具**不注册**、
|
|
30
|
+
HTTP 路由 403(能力不存在,而非调用后报错);提权走两条带外通道(启动环境变量 / 卡片里就地确认)。
|
|
31
|
+
容器名与 ID 过白名单,命令一律 argv 构造 + 单引号转义,密码 / 口令以 `env:NAME` **凭据引用**且永不回传
|
|
32
|
+
浏览器。
|
|
33
|
+
- **与 dsh-tty 数据级复用、代码级不耦合**:不 import 任何 tty 代码,两者可各自安装与升级;装了 tty 则消费
|
|
34
|
+
三个可选扩展点(`ttyConnbar` / `ttyTerminal` / `ttyPanel`),未装或版本不足逐项静默降级。
|
|
40
35
|
|
|
41
36
|
## 与 dsh-tty 的关系
|
|
42
37
|
|
|
43
|
-
本插件自成一体:不 import 任何 tty 代码,tty
|
|
44
|
-
|
|
38
|
+
本插件自成一体:不 import 任何 tty 代码,tty 也无需改一行源码;装了 tty 就在下面这些可选扩展点上
|
|
39
|
+
协作,没装则安静降级。
|
|
45
40
|
|
|
46
41
|
| 维度 | 说明 |
|
|
47
42
|
| --- | --- |
|
|
48
|
-
| 插件形态 | 独立包 `@hyzyn/dsh-docker
|
|
43
|
+
| 插件形态 | 独立包 `@hyzyn/dsh-docker`,可各自单独安装、各自升级 |
|
|
49
44
|
| 连接簿 | SSH 目标可**引用 tty 连接簿条目名**(只读 `ctx.settings.get('tty')` 的 `sshHosts`);tty 未安装时退化为「内联 host/username」或本机目标 |
|
|
50
45
|
| 主机指纹 | 本插件自持一份 `hostKeys`(TOFU),并**优先以 tty 已记录的指纹作种子**——同一主机不必在两处各确认一次 |
|
|
51
46
|
| 执行通道 | 自持池化 SSH exec(`src/ssh-exec.ts`),与 tty 的 PTY 会话完全独立,互不占名额 |
|
|
52
|
-
| 上下文入口 | tty ≥ 0.13.0
|
|
53
|
-
| 面板承载 | **默认走会话右侧栏标签**(`sidebar.right.pane.tab
|
|
54
|
-
| 终端承载 | 交互式终端由 tty 承载(它才是 PTY 的所有者)。**右侧栏标签**:面板全屏(`sidebar.fullscreen`)时经 `ttyTerminal.mount`
|
|
55
|
-
| 分工 | **交互式排障**(`docker exec -it`、容器内 shell、TUI)由 tty
|
|
56
|
-
|
|
57
|
-
数据级复用、代码级不耦合:连接簿与指纹种子是「读同一份 settings」,连接栏按钮是
|
|
58
|
-
「消费一个通用扩展点」,都不是「依赖 tty 的模块」,因此 tty 升级或卸载都不会连带
|
|
59
|
-
弄坏本插件。
|
|
47
|
+
| 上下文入口 | tty ≥ 0.13.0 时可选消费 `ttyConnbar`,在 SSH 连接栏(SFTP 旁)插入「容器」按钮(**注册即显示**),目标在点击时按当前会话解析;**本插件自己开的 exec 标签除外**(判定走 `spawnSpec.command`);tty 未装 / 版本过旧则静默跳过 |
|
|
48
|
+
| 面板承载 | **默认走会话右侧栏标签**(`sidebar.right.pane.tab`),框架侧边栏入口只负责打开或聚焦它、**页类型在同一栏内去重**;**终端连接栏**的入口相反——它长在盖满视口的 tty 弹窗上,开标签会被挡住,所以走 `ttyPanel.mountPane` 停靠到终端右侧(**入口决定承载**)。拿不到右侧栏服务(老宿主)或把 `localStorage['dsh-docker:carrier']` 置成 `modal` 时回退到 dock / 全屏模态。三种承载是**同一个组件**,只是外壳与几何不同 |
|
|
49
|
+
| 终端承载 | 交互式终端由 tty 承载(它才是 PTY 的所有者)。**右侧栏标签**:面板全屏(`sidebar.fullscreen`)时经 `ttyTerminal.mount` **就地嵌入**面板底部,否则经 `ttyTerminal.open` **开新标签**(同一容器重复点会**聚焦已有标签**——tty 契约 v3 的 `reuse`);**dock** 承载一律开标签;**模态**承载就地嵌入。命令标签(`spawnSpec.command` 非空)在 tty 侧**不显示连接栏的扩展按钮区**(SFTP / 隧道都作用于连接本身,挂在 `docker exec` 标签上会误导);本插件自己开的 exec 标签另不给「容器」入口。未装 tty 或版本不足时复制命令兜底。本插件不实现 PTY / xterm / 重连栈 |
|
|
50
|
+
| 分工 | **交互式排障**(`docker exec -it`、容器内 shell、TUI)由 tty 承载;**只读巡检与 agent 自动化**用本插件自己的 exec 通道 |
|
|
60
51
|
|
|
61
52
|
## 安装
|
|
62
53
|
|
|
@@ -65,313 +56,181 @@ dsh plugin --profile web add @hyzyn/dsh-docker # npm 安装(发
|
|
|
65
56
|
dsh plugin --profile web add link:$(pwd)/packages/docker # 仓库开发调试
|
|
66
57
|
```
|
|
67
58
|
|
|
68
|
-
聚合包 `@hyzyn/dsh-all`(或仓库根 bundle)已包含本插件,一次装齐时不必单独
|
|
69
|
-
|
|
70
|
-
**保存即热生效**(`settings/updated`
|
|
59
|
+
聚合包 `@hyzyn/dsh-all`(或仓库根 bundle)已包含本插件,一次装齐时不必单独 add。装完重启 `dsh web`,
|
|
60
|
+
侧边栏出现「容器」入口;「Docker 容器面板」卡片维护目标与开关,**保存即热生效**(`settings/updated`
|
|
71
61
|
触发重解析,无需重启)。
|
|
72
62
|
|
|
73
|
-
> 已在 web profile 里装过 `@hyzyn/dsh-all` 或根 bundle 时**不要**再 add
|
|
74
|
-
>
|
|
63
|
+
> 已在 web profile 里装过 `@hyzyn/dsh-all` 或根 bundle 时**不要**再 add 本包,否则插件行重复挂载,
|
|
64
|
+
> 启动报 `duplicate loader entry id`。
|
|
75
65
|
|
|
76
66
|
## 使用
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
面板与对话同屏:发完错误交给 Agent 后,日志留在右边、不影响看它干活。
|
|
68
|
+
两个入口,同一个面板。**默认承载是会话右侧栏标签**——入口只负责打开或聚焦它,面板与对话同屏。
|
|
80
69
|
|
|
81
70
|

|
|
82
71
|
|
|
83
|
-
- **侧边栏「容器」**(总入口):任何目标都能用,包括本机 docker
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
**当前会话那台主机**打开面板,不用再选目标。目标解析发生在点击时:会话来自
|
|
91
|
-
连接簿时按条目名匹配,否则按 `host:port` 匹配已解析的目标;**没配到目标也不会
|
|
92
|
-
藏按钮**——面板会带一条提示告诉你会话主机(含连接簿名)该去设置卡片怎么配。
|
|
93
|
-
这条路径走**终端右侧的 dock**,不开右侧栏标签——按钮长在 tty 弹窗上,而弹窗盖满
|
|
94
|
-
视口,开标签会被整个挡住、看着像「点了没反应」。目标照旧带进面板并重挂到那台主机。
|
|
95
|
-
于是规则是「**入口决定承载**」:框架侧边栏点 → 右侧栏标签(与对话同屏);
|
|
96
|
-
终端弹窗里点 → 终端右侧 dock(与终端同屏)。
|
|
97
|
-
dock 面板**归属点它的那个标签**(tty ≥ 0.18.4):切到别的 SSH 标签时整块收起
|
|
98
|
-
(React 树与轮询都保活,切回来恢复原样),关掉该标签则一并收掉;别的标签上再点
|
|
99
|
-
「容器」会用那台主机的目标重开一块。收起 ≠ 关掉:tty 不重挂,面板状态不丢。
|
|
72
|
+
- **侧边栏「容器」**(总入口):任何目标都能用,包括本机 docker 与多目标切换。已打开则聚焦(页类型在
|
|
73
|
+
同一栏内去重),不会开出第二个「Docker 容器」标签。
|
|
74
|
+
- **SSH 连接栏「容器」按钮**(上下文快捷方式,tty ≥ 0.13.0):在 tty 面板的 SSH 标签里、SFTP 旁出现,
|
|
75
|
+
**注册即显示**,点击直接用**当前会话那台主机**打开面板。目标在点击时解析:会话来自连接簿时按条目名
|
|
76
|
+
匹配,否则按 `host:port`;**没配到目标也不藏按钮**——面板会带一条提示告诉你该怎么配。这条路径走
|
|
77
|
+
**终端右侧的 dock**,不开右侧栏标签(按钮长在盖满视口的 tty 弹窗上,开标签会被挡住)。于是规则是
|
|
78
|
+
「**入口决定承载**」:框架侧边栏点 → 右侧栏标签;终端弹窗里点 → 终端右侧 dock。
|
|
100
79
|
|
|
101
80
|
### 承载形态
|
|
102
81
|
|
|
103
82
|
| 承载 | 何时用 | 行为 |
|
|
104
83
|
| --- | --- | --- |
|
|
105
|
-
| **会话右侧栏标签**(默认) | 宿主提供 `sidebarRight` / `sidebarRightTabs` |
|
|
106
|
-
| **终端右侧 dock** |
|
|
84
|
+
| **会话右侧栏标签**(默认) | 宿主提供 `sidebarRight` / `sidebarRightTabs` | 与会话同屏;折叠=退出视野但不丢状态;支持右侧栏的 fullscreen 铺满 |
|
|
85
|
+
| **终端右侧 dock** | 从**终端连接栏**的「容器」按钮进来,或拿不到右侧栏服务 / `localStorage['dsh-docker:carrier'] = 'modal'` 且 tty ≥ 0.16 面板开着 | 挂在终端面板右侧(拖宽 / 折叠 / ✕ 由 tty 提供,终端继续可用);同一时刻只挂一个,挂上去会收掉前一个(例如 tty 自己的 SFTP);tty ≥ 0.18.4 起**归属点它的那个标签**:切走收起、切回恢复、标签关掉一并收掉(收起期间 React 树与轮询保活) |
|
|
107
86
|
| **全屏模态**(兜底) | 上面两条都不成立 | 自带 backdrop、点击外部关闭;面板 z-index 高于 tty 弹窗 |
|
|
108
87
|
|
|
109
|
-
回滚到旧形态只要在控制台执行 `localStorage.setItem('dsh-docker:carrier', 'modal')`
|
|
110
|
-
|
|
111
|
-
它连带改宿主配置结构、设置卡片与文档。
|
|
88
|
+
回滚到旧形态只要在控制台执行 `localStorage.setItem('dsh-docker:carrier', 'modal')`(`removeItem`
|
|
89
|
+
恢复默认)。这个开关是临时灰度用的,所以刻意没进 settings。
|
|
112
90
|
|
|
113
|
-
**状态保留**:面板看的是主机、不是工作区,所以 view / 过滤词 /
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
单容器与聚合建流本来就带 `tail`,历史会自己补回来。
|
|
91
|
+
**状态保留**:面板看的是主机、不是工作区,所以 view / 过滤词 / 选中容器(含概览-日志-统计页签)/ 目标
|
|
92
|
+
都是**跨会话保留**的;容器详情内部的日志过滤与 LINES 开关跟具体容器绑定,不保留。**折叠标签会断开全部
|
|
93
|
+
实时流**(把 SSH 通道还回去),展开时重连——建流本来就带 `tail`,历史会自己补回来。
|
|
117
94
|
|
|
118
|
-
**跨会话粘性**:DSH
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
「面板跟人走,而不是跟会话走」。那个 ✕ 是**宿主**执行的(`sidebarRight` 自己摘标签),
|
|
123
|
-
所以「用户关掉了」这件事必须经 `registerCloseHandler` 登记才收得到:只靠面板自己的
|
|
124
|
-
`onClose` 会漏掉它,于是刚关掉的标签会在每次切会话时自己回来。
|
|
95
|
+
**跨会话粘性**:DSH 右侧栏的标签记录本身是**会话作用域**的,A 会话开的标签在 B 会话里并不存在;而面板
|
|
96
|
+
看的是主机,「切个会话它就没了」是纯损失。所以插件额外记一个「用户希望它开着」的意图:**切会话时自动
|
|
97
|
+
在新会话里把标签重开**,只有你点了标签的 ✕ 才停止。那个 ✕ 是**宿主**执行的,所以必须经
|
|
98
|
+
`registerCloseHandler` 登记才收得到——只靠面板自己的 `onClose` 会漏掉它。
|
|
125
99
|
|
|
126
100
|
**dock 兜底形态**(终端右侧栏):
|
|
127
101
|
|
|
128
102
|

|
|
129
103
|
|
|
130
|
-
这条路径打开的面板**不会盖住终端**:tty ≥ 0.16 时它挂在终端面板右侧的 dock
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
徽标并到工具条**末尾并靠右**(刷新中图标自己转,不再另挂 spinner):左端留给目标 /
|
|
135
|
-
视图 / 搜索 / 筛选这些「过滤类」控件,刷新不会被当成第一个筛选项,位置也与非 dock
|
|
136
|
-
模式头部里一致;520px 窄栏里不会白留一条空行。
|
|
104
|
+
这条路径打开的面板**不会盖住终端**:tty ≥ 0.16 时它挂在终端面板右侧的 dock 里(可拖宽、可折叠成窄条、
|
|
105
|
+
✕ 收起),终端照常敲命令。dock 模式下卡片「终端」按钮改为**在同一终端面板新开标签**执行
|
|
106
|
+
`docker exec -it`;同时**不再渲染面板自己的头部**——标题与 ✕ 由侧栏标题栏承担,刷新与只读徽标并到
|
|
107
|
+
工具条**末尾并靠右**:左端留给目标 / 视图 / 搜索 / 筛选这些「过滤类」控件。
|
|
137
108
|
|
|
138
109
|
面板内:
|
|
139
110
|
|
|
140
111
|

|
|
141
112
|
|
|
142
|
-
- **切目标有过渡与保护**:选择器一变,下方仍是上一个目标的卡片(远端一次往返最长 20s
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
跨目标操作**,异常表的行上没有动作按钮。进总览时会清掉「当前这个目标」遗留的
|
|
176
|
-
失败横幅:总览按目标归因(红卡 + 不可达横幅),不再叠一条单目标的「操作失败」——
|
|
177
|
-
同一个 SSH 超时被讲两遍,看着像两台机器都挂了。轮询复用工具条的「自动刷新」开关(默认关):
|
|
178
|
-
这一页每轮是全 N 个目标各一次 `docker ps`,比任何单目标页都贵。没有异常显示
|
|
179
|
-
「一切正常」;还有目标没答完时,异常区显示「读取中…」、**那张计数卡也进入读取态**
|
|
180
|
-
(转圈 + 压暗,不出 0/0/0——「0 个容器」会被读成「这台机器没容器」,而它只是还没答)
|
|
181
|
-
(「还不知道」不等于「没问题」)。
|
|
182
|
-
- **容器列表**:名称 / 状态 / 健康态 / 镜像 / 端口映射 / compose 项目与服务 /
|
|
183
|
-
短 ID;支持按名称或镜像**搜索**、按状态**筛选**(运行中 / 已停止 / 全部),
|
|
184
|
-
以及**自动刷新**(按 `pollIntervalSec` 轮询,切到镜像页会暂停且不显示该开关——
|
|
185
|
-
镜像变化慢,没必要每 5s 跑一次 `docker images`;切回容器页原设置照旧生效)。
|
|
186
|
-
工具条里「含已停止」与「自动刷新」是**成组**的两个开关,搜索框是其中唯一可伸缩的项:
|
|
187
|
-
宽面板下整条工具条收成一行,窄栏(dock 520px)里才按组换行——不会出现一行只有
|
|
188
|
-
一个复选框的孤行。
|
|
189
|
-
- **「活动」条(docker events 事件流)**:容器列表头部的可折叠窄栏(默认展开),
|
|
190
|
-
跟着一条 SSE(`GET /api/dsh-docker/events/stream`,服务端跑
|
|
191
|
-
`docker events --filter type=container`)显示最近 8 条事件(时间 + 容器名 + 动作,
|
|
192
|
-
`die` 带退出码如 `die(137)`)。它是**事件驱动刷新**的入口:收到事件后 500ms
|
|
193
|
-
**防抖**触发一次列表重取(不是每帧一次请求),与原有 `AUTO REFRESH` 叠加而不互斥——
|
|
194
|
-
轮询负责兜底,事件负责「刚发生」。事件只留在内存(环形缓冲 50 条),切页即关流,
|
|
195
|
-
换目标清空缓冲。白名单只留九类生命周期动作(start / die / stop / kill / oom /
|
|
196
|
-
health_status / destroy / rename / update):`exec_*`、`archive-path`(`docker cp`)
|
|
197
|
-
这类噪音在服务端就丢掉了——实测一台跑批机器 24 小时 47 条事件全是 exec,白名单
|
|
198
|
-
命中 0,所以只被 exec 的机器上活动条是空的,这是刻意的。
|
|
199
|
-
断开后**要手动重连**:这条流进了 `closed` 就是终止态(EventSource 只在连接层抖动
|
|
200
|
-
时自愈;`docker events` 进程退出、服务端发 `end` 帧都会走 close),活动条上会出现
|
|
201
|
-
一个 **重连** 按钮(只在 `closed` 时出现;`unsupported` 环境不给——重连必然再失败),
|
|
202
|
-
工具条的 ⟳ 也一并重连。**AUTO REFRESH 轮询刻意不重连**:塞进轮询会让一条已终止的流
|
|
203
|
-
被不断重开,既掩盖「它断了」,又白建 SSE。重连成功后补一次全量列表刷新(断线期间
|
|
204
|
-
列表可能已经落后),daemon 全程没起、流从未连上过的情形同样补——否则活动条绿了而
|
|
205
|
-
列表还停在错误态,看着像好了其实没好。
|
|
206
|
-
**重连被拒时要说原因**(`D164`):服务端主动拒绝这条流时会发一帧 `event: error`
|
|
207
|
-
带 `message`(最典型的是长流配额满,见「已知限制」——那句话里写着该怎么办);
|
|
208
|
-
客户端现在把它**显示在活动条上**(「重连被拒:…」),而不是丢掉。原先只置
|
|
209
|
-
`closed`,于是「拒绝 → 回到原位」与「点了没反应」在界面上完全不可区分,用户只能反复点。
|
|
210
|
-
重连成功(或重新建流)时这条原因清除。
|
|
211
|
-
断开时右侧那句提示是**折叠头里的单行小字**,所以刻意写短(2026-10-08 用户反馈
|
|
212
|
-
「啰嗦又生硬」):状态行左边已经在说「事件流已断开」,这句只说「不会自动恢复,点
|
|
213
|
-
「重连」」——成因、上限与 `MaxSessions` 那一串写在本页,不塞进界面。
|
|
214
|
-
**「在收事件」只认宿主那帧 `ready`**(2026-10-11 用户现场):SSE 的 `onopen` 只等于
|
|
215
|
-
「响应头到了」——宿主是先把响应头 flush 出去、**再去起** `docker events`,而目标不可达 /
|
|
216
|
-
SSH 正在重连时建连可以卡十几秒(D150 的「槽位满 → 重建连接」还会再来一轮)。那段时间
|
|
217
|
-
活动条原本已经亮成绿色的「实时接收中(docker events)」,一个事件都还没收到。现在绿点
|
|
218
|
-
只认 `ready`(= 管道真的起来了);此前停在「正在连接事件流…」/「连接中断,正在自动
|
|
219
|
-
重连…」——两种都是实话。日志 / 统计 / 拉取三条流同一口径(见「SSE 事件协议」)。
|
|
220
|
-
- **聚合选择(临时多选聚合日志)**:工具条的 `聚合选择` 进入选择态——每张卡片
|
|
221
|
-
左侧出现勾选框,点卡片本体变成**勾选 / 取消**(不再进详情;动作条暂时收起,
|
|
222
|
-
免得一边多选一边误点启停删),工具条与列表之间出现操作条:「已选 N 个容器」+
|
|
223
|
-
`聚合日志` + `取消`。`聚合日志` 至少需要 2 个容器;选到第 3 个时给一条软提示,
|
|
224
|
-
超过 **3 个**按钮置灰并提示上限(**上限怎么来的见「已知限制」的聚合上限**,
|
|
225
|
-
操作条上也写着那句说明)。点 `聚合日志`
|
|
226
|
-
进入聚合视图:直接复用 Compose 项目视图那套聚合日志(**一条** `/logs/stream?ids=…`
|
|
227
|
-
装下所有选中容器,帧里带 `id`,按 `[service]` / 容器名前缀混流,带过滤与自动滚动),
|
|
228
|
-
返回即退出选择态并清空。
|
|
229
|
-
聚合视图的内容控制与**单容器日志完全对齐**(文本过滤 + 级别门槛 + `⬇ .log` / `⬇ .md`
|
|
230
|
-
导出当前显示内容 + 行数统计),另有聚合独有的两项:**按时间 / 按到达**排序、**暂停**冻结
|
|
231
|
-
(暂停时流继续接收、恢复一次性补齐)。
|
|
232
|
-
再点 `聚合选择` 或按 **Esc** 同样退出并清空。勾选是**临时的**:不持久化、不命名
|
|
233
|
-
组合、不进 settings;切目标 / 切「容器 · 镜像 · Compose」分段 / 关面板即失效,
|
|
234
|
-
列表刷新后已消失的容器按 id 自动剔除。
|
|
235
|
-
- **按条件一键选中**(多选态):操作条第二行给出一排条件 chip(`全部可见 / 不健康 /
|
|
236
|
-
需关注 / 已停止`,以及有勾选后的 `同镜像 / 同项目`),计数**按剩余名额截断**——
|
|
237
|
-
chip 上写 3 就真的会选中 3 个;超出上限的数量在 title 与结果提示里如实说明。
|
|
238
|
-
条件只在**当前筛选结果**里生效(先搜索/筛状态再一键选中)。
|
|
239
|
-
- **容器卡片**:与参考布局一致的「标签 + 值」行(镜像 / ID / 端口 / 创建 /
|
|
240
|
-
compose,值等宽、可省略)+ 一排图标操作按钮,**按「查看 / 变更」两组用竖线分隔**:
|
|
113
|
+
- **切目标有过渡与保护**:选择器一变,下方仍是上一个目标的卡片(远端一次往返最长 20s)。过渡期间旧
|
|
114
|
+
卡片去色并**锁住指针**(读起来是「另一批内容」而不是「坏了」,也避免照着旧列表操作——那会拿新目标
|
|
115
|
+
当目标、用旧容器的 ID 发命令,真能停掉对端同名容器),新数据淡入落地;整块**绝对定位**,首卡位置与
|
|
116
|
+
滚动高度不变。**切换失败则清空旧列表**并归到新目标的错误态,空态文案区分「读取失败」与「筛选过窄」。
|
|
117
|
+
- **目标选择**:面板先选目标(来自配置 `targets`,本机 / SSH);只配了一个时默认选中它,agent 工具也
|
|
118
|
+
可以省略 `target`。**会记住上次选的目标**(`localStorage` 的 `dsh-docker:last-target`,不落配置):
|
|
119
|
+
优先级是**连接栏指定 > 上次记住的(且仍存在)> 列表第一个**。换目标是**整段上下文切换**:上一个目标
|
|
120
|
+
还在飞的请求一律作废(列表写入闸),不会出现「选择器已是目标2、卡片还是目标1 的容器」这种串台。
|
|
121
|
+
- **容器列表**:名称 / 状态 / 健康态 / 镜像 / 端口映射 / compose 项目与服务 / 短 ID;支持按名称或镜像
|
|
122
|
+
**搜索**、按状态**筛选**、**自动刷新**(按 `pollIntervalSec` 轮询,切到镜像页会暂停且不显示该开关)。
|
|
123
|
+
「含已停止」与「自动刷新」是**成组**的两个开关,搜索框是其中唯一可伸缩的项:宽面板下一行,窄栏
|
|
124
|
+
(dock 520px)里才按组换行。
|
|
125
|
+
- **「活动」条(docker events 事件流)**:容器列表头部的可折叠窄栏(默认展开),跟着一条 SSE
|
|
126
|
+
(`GET /api/dsh-docker/events/stream`,服务端跑 `docker events --filter type=container`)显示最近 8 条
|
|
127
|
+
事件(时间 + 容器名 + 动作,`die` 带退出码)。它是**事件驱动刷新**的入口:收到事件后 500ms **防抖**
|
|
128
|
+
触发一次列表重取,与 `AUTO REFRESH` 叠加而不互斥——轮询兜底,事件负责「刚发生」。事件只留在内存
|
|
129
|
+
(环形缓冲 50 条),切页即关流,换目标清空。白名单只留九类生命周期动作(start / die / stop / kill /
|
|
130
|
+
oom / health_status / destroy / rename / update):`exec_*`、`archive-path` 这类噪音在服务端就丢掉。
|
|
131
|
+
断开后**要手动重连**:进了 `closed` 就是终止态,活动条上出现**重连**按钮(`unsupported` 环境不给),
|
|
132
|
+
工具条的 ⟳ 也一并重连;**AUTO REFRESH 刻意不重连**(塞进轮询会让已终止的流被不断重开),重连成功后
|
|
133
|
+
补一次全量列表刷新。**重连被拒要说原因**(`D164`):服务端主动拒绝时发一帧 `event: error` 带
|
|
134
|
+
`message`,客户端把它**显示在活动条上**——只置 `closed` 会让「拒绝 → 回到原位」与「点了没反应」不
|
|
135
|
+
可区分。**「在收事件」只认宿主那帧 `ready`**(响应头到达不算)。
|
|
136
|
+
- **聚合选择(临时多选聚合日志)**:工具条的 `聚合选择` 进入选择态——点卡片本体变成**勾选 / 取消**
|
|
137
|
+
(动作条暂时收起,免得一边多选一边误点启停删),出现操作条:「已选 N 个容器」+ `聚合日志` + `取消`。
|
|
138
|
+
`聚合日志` 至少需要 2 个容器、最多 **3 个**(超了按钮置灰并提示上限来由)。进入后复用 Compose 项目
|
|
139
|
+
视图那套聚合日志,内容控制与单容器日志完全对齐,另有**按时间 / 按到达**排序与**暂停**。勾选是**临时
|
|
140
|
+
的**:不持久化、不进 settings;切目标 / 切分段 / 关面板即失效,列表刷新后已消失的容器按 id 自动剔除。
|
|
141
|
+
- **按条件一键选中**(多选态):操作条第二行给出条件 chip(`全部可见 / 不健康 / 需关注 / 已停止`,以及
|
|
142
|
+
有勾选后的 `同镜像 / 同项目`),计数**按剩余名额截断**——chip 上写 3 就真的会选中 3 个。条件只在
|
|
143
|
+
**当前筛选结果**里生效。
|
|
144
|
+
- **容器卡片**:与参考布局一致的「标签 + 值」行(镜像 / ID / 端口 / 创建 / compose)+ 一排图标按钮,
|
|
145
|
+
**按「查看 / 变更」两组用竖线分隔**:
|
|
241
146
|
|
|
242
147
|
| 组 | 按钮 | 说明 |
|
|
243
148
|
| --- | --- | --- |
|
|
244
149
|
| 查看 / 进入 | 终端、日志、资源占用 | 不改容器状态,只读模式下也永远可用。**终端按钮跑 `docker exec -it` 交互式 shell,不受 `allowExec` 门控**(那条开关只管本插件的一次性 exec 通道,见「已知限制」) |
|
|
245
150
|
| 变更 | 启停、重启、删除 | 按破坏性递增排列;只在 `allowMutations` 开启后可用,未开启时整组置灰 |
|
|
246
151
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
`
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
洪泛里缓冲持续淘汰旧行、或聚合视图按时间插行,都不会跳。**换流 / 换容器 / 快照刷新
|
|
282
|
-
会整代作废这批实测高度与锚点(D155)**:快照行 id 是位置寻址(`'s'+index`),刷新后
|
|
283
|
-
同一个 id 就是另一行内容——不换代就会拿另一代的高度算垫高(实测滚动高虚高 35644px),
|
|
284
|
-
滚动条、跳转、「滚到顶看到第几行」全部按错的一代算。
|
|
285
|
-
显示层**不截断**:`LINES` 选多少就是多少——全部行都能滚到、都能导出(真正的闸是
|
|
286
|
-
缓冲与宿主的「输出上限(KB)」),只是不再同时挂在 DOM 里。自动滚动到底部,用户向上滚动时暂停并浮出
|
|
287
|
-
「回到底部」按钮;右上状态行显示连接状态,容器退出导致流自然结束时自动切回快照刷新。**断线重连由插件自己管**(不依赖
|
|
288
|
-
EventSource 的自动重连):首连带 tail 补历史,重连一律 tail=0——只补新行、**不
|
|
289
|
-
重放历史**(自动重连会复用带 tail 的 URL,服务端就会把最后 tail 行当新行重推一遍,
|
|
290
|
-
日志里凭空多出一段重复)。**宿主侧背压(D153)**:单条流的暂存队列上限 8MB;日志与拉取
|
|
291
|
-
这类**文本尾部流**溢出时丢最旧的帧、留最新,并推一条 `skip{frames,bytes}` 让界面说明
|
|
292
|
-
「中间断了一截,跟随继续」——**不再掐流**(老策略是补一条 `end{reason:output-limit}` 收尾,
|
|
293
|
-
客户端只能重连,积压期间面板就是「连接中断」在打转)。skip 的账目是**精确**的:skip 帧还在
|
|
294
|
-
队列里没落地时又发生丢弃,服务端**原地更新**它的计数(D156,一段缺口都不吞);客户端把
|
|
295
|
-
多批 skip **累加**显示成「本次连接共跳过 N 批」,重连成功即清零(D157)。统计 / 事件这类结构化流仍走收尾重连
|
|
296
|
-
(少一个采样/事件在语义上是错的,宁可让客户端补一次)。
|
|
297
|
-
连接 / 切页 / 关面板都会关闭 `EventSource`。
|
|
298
|
-
- **概览**:`docker inspect` 的权威数据——状态与健康、退出码、重启次数与策略、
|
|
299
|
-
端口映射、挂载(含只读标记)、网络与 IP、entrypoint 与命令、最近一次健康
|
|
300
|
-
检查输出;下方可执行一次性 `docker exec`(需 `allowExec`)。
|
|
301
|
-
- **日志**:`docker logs --tail` 的尾部快照(`LINES` 默认取设置卡片的「日志
|
|
302
|
-
默认行数」),可切时间戳与 `--since`。**两个闸要分清**:`LINES` 是**行数**上限,
|
|
303
|
-
设置卡片的「输出上限(KB)」是**字节**上限——两者取小,字节先超就截断并给出横幅
|
|
304
|
-
(所以 `LINES` 选了 5000 也可能只回来一部分)。要持续观察新日志就打开上面的
|
|
305
|
-
`FOLLOW`(同一份 argv 加 `--follow`,无总超时、不受该字节上限约束,靠连接
|
|
306
|
-
生命周期收尾)。
|
|
152
|
+
删除额外两点防护:破坏性配色 + 与「重启」之间留出间距,点击后仍需二次确认。**命令飞行期间整组
|
|
153
|
+
锁住**:`stop` / `rm` 要等容器真的退出(最长十几秒),确认框保持打开并显示「执行中…」,其它变更按钮
|
|
154
|
+
压暗置灰、正在跑的那个换成转圈,等列表刷新落地后才恢复。镜像的删除 / 清理走同一套确认框。点卡片本体
|
|
155
|
+
进入概览。
|
|
156
|
+
- **容器详情(整栏视图)**:顶部为「返回 + 容器名 + 状态徽标 + 目标主机」,下方五个标签页——概览
|
|
157
|
+
(`docker inspect` 权威数据 + 一次性 exec + 等价 `docker run`)、日志、统计、诊断、进程。从卡片的
|
|
158
|
+
日志 / 统计图标可直接落到对应页签;诊断与进程**只在该页签被打开时才取数**(切走即不再请求)。
|
|
159
|
+
- **日志视图(紧凑两行)**:第一行 = 返回 + 容器名 + 状态 + 目标主机 + `LINES` / `TIMESTAMPS` /
|
|
160
|
+
**`FOLLOW`** / `AUTO REFRESH`(开关 + 2/3/5/10s 间隔,仅日志页轮询)+ 刷新 / 关闭;第二行 = 标签页 +
|
|
161
|
+
**常驻**的「过滤日志」输入框(有内容时浮出 ✕,Esc 也能清空)+ **级别门槛**(`全部级别 / INFO+ /
|
|
162
|
+
WARN+ / ERROR+`)+ **导出**(`⬇ .log` / `⬇ .md`,导出**当前显示内容**)+ 右侧固定槽的行数统计。
|
|
163
|
+
**两行的分工是刻意的**:第一行管传输与显示,第二行管内容——而第二行与聚合日志**完全同一套**(同一个
|
|
164
|
+
级别内核、导出构建器、计数文案)。级别门槛的语义是「无级别前缀的行是上一条的续行,跟随其级别」,
|
|
165
|
+
否则 `ERROR+` 会把堆栈拦腰截断。日志正文按级别着色,时间戳压暗,过滤命中高亮。
|
|
166
|
+
- **FOLLOW 实时日志流**:打开后从「定时拉快照」切为 **SSE 推送**(`GET /api/dsh-docker/logs/stream`,
|
|
167
|
+
服务端跑 `docker logs --follow`),与 `AUTO REFRESH` 互斥(开流自动停轮询、开关置灰),关闭即回到
|
|
168
|
+
快照并立即刷新。流式日志保留最近 **5000 行 / 4MB**(环形缓冲,超限丢最旧并提示一次;无换行的超长
|
|
169
|
+
输出强制分片,内存恒有界),分片到达按 **150ms 合帧**渲染。**正文按窗口化挂载**(`D152`):DOM 里只
|
|
170
|
+
留「可见 + 上下各 24 行 overscan」,其余用上下垫高占位;行高是实测的、贴底时窗口锚在尾部,所以
|
|
171
|
+
「自动贴底 / 上滚暂停 / 回到底部」的几何判定依旧精确;**换流 / 换容器 / 快照刷新会整代作废这批实测
|
|
172
|
+
高度与锚点**(`D155`:快照行 id 是位置寻址,不换代就会拿另一代的高度算垫高)。显示层**不截断**:
|
|
173
|
+
`LINES` 选多少就能滚到、能导出(真正的闸是缓冲与宿主的「输出上限(KB)」)。**断线重连由插件自己
|
|
174
|
+
管**:首连带 tail 补历史,重连一律 `tail=0`——只补新行、**不重放历史**。**宿主侧背压**(`D153`):
|
|
175
|
+
单条流的暂存队列上限 8MB;日志与拉取这类**文本尾部流**溢出时丢最旧的帧、留最新,并推一条
|
|
176
|
+
`skip{frames,bytes}` 让界面说明「中间断了一截,跟随继续」——**不再掐流**;skip 的账目是精确的(排队
|
|
177
|
+
期间再次丢弃**原地更新**计数,`D156`;客户端把多批 skip 累加显示,重连成功即清零,`D157`)。统计 /
|
|
178
|
+
事件这类结构化流仍走收尾重连。
|
|
179
|
+
- **概览**:`docker inspect` 的权威数据——状态与健康、退出码、重启次数与策略、端口映射、挂载(含只读
|
|
180
|
+
标记)、网络与 IP、entrypoint 与命令、最近一次健康检查输出;下方可执行一次性 `docker exec`(需
|
|
181
|
+
`allowExec`)。
|
|
182
|
+
- **日志**:`docker logs --tail` 的尾部快照(`LINES` 默认取设置卡片的「日志默认行数」),可切时间戳与
|
|
183
|
+
`--since`。**两个闸要分清**:`LINES` 是**行数**上限,设置卡片的「输出上限(KB)」是**字节**上限——
|
|
184
|
+
两者取小,字节先超就截断并给出横幅。要持续观察新日志就打开 `FOLLOW`(同一份 argv 加 `--follow`,
|
|
185
|
+
无总超时、不受该字节上限约束,靠连接生命周期收尾)。
|
|
307
186
|
|
|
308
187
|

|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`--no-stream
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
`docker network inspect` 才能拿到接入列表,列表逐行 inspect 就是 N 次 docker 调用,
|
|
337
|
-
改成点进详情取一次。
|
|
338
|
-
- **卷**:名称 / 驱动 / 范围 / 挂载点(超长路径限宽省略,title 给全量),详情为
|
|
339
|
-
名称 / 驱动 / 范围 / 挂载点 / 创建 / 选项 / 标签(卷没有反向索引,只有概览一页)。
|
|
340
|
-
- 详情页头部有**删除**按钮,两页工具条各有 **prune 图标**,都受 `allowMutations`
|
|
341
|
-
门控(未开启时置灰 + title 说明),点击后二次确认;删除失败(网络还有容器接着 /
|
|
342
|
-
卷还被占用 / 403)在详情页就地弹横幅,不会静默。
|
|
343
|
-
- 自动刷新与镜像页一致:这两页**不轮询**(清单变化慢,5s 一次 docker CLI 是白烧),
|
|
344
|
-
切页 / 切目标时才刷。
|
|
345
|
-
- **拉取镜像(SSE 进度流)**:镜像页工具条的拉取图标(需 `allowMutations`)
|
|
346
|
-
打开拉取视图,输入引用后走 `GET /api/dsh-docker/images/pull/stream`
|
|
347
|
-
(服务端跑 `docker pull`)——逐层进度(Pulling fs layer / Downloading /
|
|
348
|
-
Extracting / Pull complete)实时出现;进度行按「层键」原地更新,TTY 下的 `\r`
|
|
349
|
-
刷新也不会让缓冲区越滚越长。工具条上的「清理 dangling」执行
|
|
350
|
-
`docker image prune -f`,**只删无标签镜像**(刻意不加 `--all`,避免误删未
|
|
351
|
-
使用的普通镜像)。
|
|
188
|
+
|
|
189
|
+
- **统计**:`docker stats --no-stream` 快照(CPU% / 内存用量与占比 / 网络 IO / 块 IO / PIDs),按
|
|
190
|
+
`pollIntervalSec` 轮询刷新。统计页还有 **`FOLLOW` 实时跟随**:切到
|
|
191
|
+
`GET /api/dsh-docker/stats/stream`(服务端跑**不带** `--no-stream` 的 `docker stats`,每秒一行),
|
|
192
|
+
浏览器侧留 **60 个点**的环形缓冲画 CPU / 内存迷你趋势图。这条流**不会自然结束**,关闭语义是前端主动
|
|
193
|
+
断;`docker stats` 自己退出时服务端发 `end`(reason=`stats-exit`)。
|
|
194
|
+
- **Compose 项目视图**:工具条第三段。把 `composeProject` / `composeService` 聚合成「项目 → 服务 →
|
|
195
|
+
容器」,每个项目一行看全运行数 / 不健康数 / 服务数;点进项目可看服务表,或打开**项目级聚合日志**
|
|
196
|
+
——一条 `/logs/stream?ids=…` 装下项目内所有容器(帧里带 `id`),客户端按 `[service]` 前缀混流。某个
|
|
197
|
+
容器的子流失败只标注「部分出错」,其余容器的日志照常收。
|
|
198
|
+
- **镜像**:`docker images` 列表(reference / 大小 / 创建时间 / 短 ID),`<none>:<none>` 的 dangling
|
|
199
|
+
带标记;搜索框与计数**固定在工具条里**,表头列名在表体内吸顶。每行「详情 / 删除」:**详情**打开整栏
|
|
200
|
+
视图(概览 + `docker history` 构建历史);**删除**(需 `allowMutations`)二次确认后执行
|
|
201
|
+
`docker image rm`(不带 `-f`,被引用时会失败并给出「先删相关容器」的提示)。
|
|
202
|
+
- **网络 / 卷**:工具条分段扩到「容器 / 镜像 / Compose / 网络 / 卷 / 磁盘」(窄栏里分段自己横向滚动,
|
|
203
|
+
不做二级菜单)。两页都是「表格 + 整栏详情」的同构布局:
|
|
204
|
+
- **网络**:名称 / 驱动 / 范围 / internal 徽标 / ID,行点击进详情——概览与「接入的容器」页签。
|
|
205
|
+
**容器数刻意不进列表行**:只有 `docker network inspect` 才拿得到接入列表,列表逐行 inspect 就是
|
|
206
|
+
N 次 docker 调用。
|
|
207
|
+
- **卷**:名称 / 驱动 / 范围 / 挂载点(超长路径限宽省略,title 给全量),详情只有概览一页(卷没有
|
|
208
|
+
反向索引)。
|
|
209
|
+
- 详情页头部有**删除**按钮,两页工具条各有 **prune 图标**,都受 `allowMutations` 门控(未开启时置灰
|
|
210
|
+
+ title 说明)并二次确认;删除失败就地弹横幅,不会静默。这两页**不轮询**(清单变化慢)。
|
|
211
|
+
- **拉取镜像(SSE 进度流)**:镜像页工具条的拉取图标(需 `allowMutations`)打开拉取视图,输入引用后走
|
|
212
|
+
`GET /api/dsh-docker/images/pull/stream`——逐层进度实时出现;进度行按「层键」原地更新,TTY 下的 `\r`
|
|
213
|
+
刷新也不会让缓冲区越滚越长。工具条的「清理 dangling」执行 `docker image prune -f`,**只删无标签
|
|
214
|
+
镜像**(刻意不加 `--all`)。
|
|
352
215
|
|
|
353
216
|

|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
217
|
+
|
|
218
|
+
- **一次性 exec**:`allowExec` 开启后可输入命令,等价 `docker exec <容器> sh -c "<命令>"`,返回退出码
|
|
219
|
+
与 stdout/stderr(无 TTY)。
|
|
220
|
+
- **交互式终端(卡片第一个图标)**:跑 `docker exec -it '<容器>' sh`,按 tty 能力三级降级——
|
|
358
221
|

|
|
359
222
|
|
|
360
|
-
1. **就地嵌入(tty ≥ 0.15
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
本地目标开本地会话,SSH 目标按连接簿条目名(或 agent 认证的内联字段)走 SSH,
|
|
373
|
-
标签/抽屉标题为 `<容器> · exec`。能力判断走服务契约版本
|
|
374
|
-
(`ttyTerminal.version >= 2` 才有 `mount`),不是猜函数存不存在。
|
|
223
|
+
1. **就地嵌入(tty ≥ 0.15,推荐)**:面板底部开一个**终端抽屉**,由 `ttyTerminal.mount` 把终端挂
|
|
224
|
+
进来,面板不收起;**折叠**(标题栏箭头或双击顶边)只把抽屉压成一条标题栏、会话照旧跑着,
|
|
225
|
+
**拖拽顶边**可调高度(上限 75%)。真正结束会话的只有抽屉的 ✕ 与关闭面板——有活动会话时关面板
|
|
226
|
+
会先弹确认,避免点 backdrop 误杀一个正在排障的容器 shell。
|
|
227
|
+
2. **借 tty 弹窗开标签(tty ≥ 0.14)**:tty 新开一个标签执行同一命令,随后收起本面板(本面板
|
|
228
|
+
z-index 更高,不收起用户只会觉得「点了没反应」)。
|
|
229
|
+
3. **复制命令**:未装 tty / 版本过旧 / 内联目标用了 key·password 认证(浏览器端没有凭证)时,退化
|
|
230
|
+
为复制该命令并提示到终端面板粘贴。
|
|
231
|
+
|
|
232
|
+
本地目标开本地会话,SSH 目标按连接簿条目名(或内联字段)走 SSH,标题为 `<容器> · exec`。能力判断走
|
|
233
|
+
服务契约版本(`ttyTerminal.version >= 2` 才有 `mount`),不是猜函数存不存在。
|
|
375
234
|
|
|
376
235
|
### 目标(本机 / SSH)
|
|
377
236
|
|
|
@@ -380,20 +239,14 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;「Docker 容
|
|
|
380
239
|
| `local` | 宿主所在机器上的 docker CLI(`spawn` 直接执行,不经 shell) |
|
|
381
240
|
| `ssh` | 经 `ssh2` 连到远程主机,在远程执行 docker CLI(argv 经单引号转义) |
|
|
382
241
|
|
|
383
|
-
`kind=ssh`
|
|
242
|
+
`kind=ssh` 有两种填法:① **引用 tty 连接簿条目**——`book` 填条目名(在「终端面板」的连接簿里维护),
|
|
243
|
+
主机 / 端口 / 用户名 / 认证方式随之生效;设置卡片的下拉只列出 tty 已保存的条目,tty 未安装或条目不
|
|
244
|
+
存在时该目标解析失败,面板与 agent 工具都会给出明确错误。② **内联字段**——`host` + `username` 必填,
|
|
245
|
+
其余按需(`port` / `auth` / `keyPath` / `password` / `passphrase` / `agentForward`)。
|
|
384
246
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
面板与 agent 工具都会给出明确错误。
|
|
389
|
-
2. **内联字段**:`host` + `username` 必填,其余按需(`port` / `auth` /
|
|
390
|
-
`keyPath` / `password` / `passphrase` / `agentForward`)。
|
|
391
|
-
|
|
392
|
-
目标解析是**每次操作现算**的:在 tty 卡片里改了连接簿条目(换端口、改密码),
|
|
393
|
-
下一次操作立即用新值,无需重启。远端需要满足:装了 docker CLI,且当前账号
|
|
394
|
-
**免 sudo** 可用 docker(通常在 `docker` 组),否则 `probe` 会原样透出
|
|
395
|
-
`permission denied while trying to connect to the Docker daemon socket`
|
|
396
|
-
之类的错误。
|
|
247
|
+
目标解析是**每次操作现算**的:在 tty 卡片里改了连接簿条目,下一次操作立即用新值,无需重启。远端需要
|
|
248
|
+
装了 docker CLI,且当前账号**免 sudo** 可用 docker(通常在 `docker` 组),否则 `probe` 会原样透出
|
|
249
|
+
`permission denied while trying to connect to the Docker daemon socket` 之类的错误。
|
|
397
250
|
|
|
398
251
|
#### 连接本机(一键)
|
|
399
252
|
|
|
@@ -404,257 +257,202 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;「Docker 容
|
|
|
404
257
|
| 一个目标都没有 | 面板正文的空态里一个 **连接本机** 主按钮(那时整块是空的,一键就是最短路径) |
|
|
405
258
|
| 已经有别的目标 | **目标下拉的末一项** `+ 连接本机`——就在你找机器的地方,点开才出现 |
|
|
406
259
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
所有入口共用宿主里同一份实现。
|
|
412
|
-
|
|
413
|
-
为什么不做成常驻的工具条按钮:常驻一个「连接本机」既占掉目标选择器右边的位置,又要靠
|
|
414
|
-
强调色招人点,而**已经有可用目标**时它是纯噪音(第一版就是常驻的,用户当场指出多余)。
|
|
415
|
-
但只留空态那一个入口又会漏掉「手上有几台 SSH、想再加一台本机」这种常见情况——所以
|
|
416
|
-
第二个入口放进下拉,只有在真的去找机器时才出现。
|
|
260
|
+
两处互斥(不会同一个入口出现两次),**已经有本机目标时两处都不出现**。做成常驻的工具条按钮不划算:
|
|
261
|
+
**已经有可用目标**时它既占位又招人点,是纯噪音;而只留空态那一个入口又会漏掉「手上有几台 SSH、想再加
|
|
262
|
+
一台本机」这种常见情况。agent 侧同一件事是 `docker_connect_local`,HTTP 侧是 `POST /connect-local`
|
|
263
|
+
——所有入口共用宿主里同一份实现。
|
|
417
264
|
|
|
418
265
|
| 情况 | 行为 |
|
|
419
266
|
| --- | --- |
|
|
420
267
|
| 已有一条 `kind=local` 目标(无论叫什么名字) | **复用它**(`created:false`),只切过去;不新建、不重写、不改名 |
|
|
421
|
-
| 没有本机目标 | 用稳定名 `local` 追加到列表末尾;`local`
|
|
268
|
+
| 没有本机目标 | 用稳定名 `local` 追加到列表末尾;`local` 已被占用时依次试 `local-2`、`local-3`… |
|
|
422
269
|
| 已有 SSH / 自定义目标 | **原样保留**(含顺序与凭据);只在末尾追加本机那条 |
|
|
423
270
|
| 重复点 / 并发点 | 幂等:最终只有一条本机目标 |
|
|
424
271
|
| 设置存储不可用 / 保存失败 | 明确报错(503 / 500 + 原因),**不改内存配置**——不会出现「界面上加上了、刷新就没了」 |
|
|
425
272
|
|
|
426
|
-
写盘成功之后**顺带做一次只读探测**(`docker version`),结果放进响应的 `message`:
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
`
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
超时是**独立一档**:`probe()` 不只报退出码(`code` 为 `null` 时那句「退出码 null」对用户
|
|
443
|
-
零信息量),而是回 `timedOut:true` + 一句可执行的话。超时还会**真的按时返回**——
|
|
444
|
-
SIGKILL 只作用于直接子进程,`dockerBin` 指向包装脚本时孙进程仍握着管道,会把手上的
|
|
445
|
-
15 秒上限拖成分钟级(真机实测 3 秒上限拖到 60 秒),所以到点先杀、再给 500ms 收敛期,
|
|
446
|
-
仍不 `close` 就自行放掉管道收尾。
|
|
447
|
-
|
|
448
|
-
刻意**不做**的事(保守边界):不自动安装 Docker、不启动 daemon、不切换 docker
|
|
449
|
-
context、不改任何环境变量;空 `targets`(= 用户没配)的语义不变,不产生隐式默认目标;
|
|
450
|
-
`clearTargets` 的防误清空逻辑一字未动。这条路径也**与能力开关无关**——它只写插件
|
|
451
|
-
自己的配置,不涉及任何需要授权的目标侧写操作。
|
|
273
|
+
写盘成功之后**顺带做一次只读探测**(`docker version`),结果放进响应的 `message`:daemon 可达时给出
|
|
274
|
+
服务端版本;不可达时说清成因——**docker CLI 缺失**(去装 Docker,或把「docker CLI」改成正确路径)/
|
|
275
|
+
**探测超时**(daemon 卡死,或 `docker context` 指向了连不上的远端)/ **daemon 未启动**(Linux
|
|
276
|
+
`systemctl start docker`,macOS / Windows 启动 Docker Desktop)/ **socket 无权访问**(账号加入 `docker`
|
|
277
|
+
组)。探测失败**不算这次操作失败**(目标确实加上了,`reachable:false` 单独表示)。
|
|
278
|
+
|
|
279
|
+
探测用的二进制**就是设置卡片里的「docker CLI」**(`dockerBin`,默认 `docker`)。CLI 缺失时会**只读地**
|
|
280
|
+
在 `PATH` 里找一下别的容器 CLI(`docker` → `podman` → `nerdctl`),找到就在那句话里点名——**只提示,
|
|
281
|
+
绝不替你改配置**(podman 与本插件输出格式的兼容性并未逐项验证,见「已知限制」)。比对候选时按**工具
|
|
282
|
+
名**(basename)而不是配置里的原字符串。超时是**独立一档**:回 `timedOut:true` + 一句可执行的话,并且
|
|
283
|
+
**真的按时返回**——到点先杀、再给 500ms 收敛期,仍不 `close` 就自行放掉管道收尾(SIGKILL 只作用于直接
|
|
284
|
+
子进程,`dockerBin` 指向包装脚本时孙进程会把手上的上限拖成分钟级)。
|
|
285
|
+
|
|
286
|
+
刻意**不做**的事:不自动安装 Docker、不启动 daemon、不切换 docker context、不改任何环境变量;空
|
|
287
|
+
`targets`(= 用户没配)的语义不变,不产生隐式默认目标;`clearTargets` 的防误清空逻辑一字未动。这条
|
|
288
|
+
路径也**与能力开关无关**——它只写插件自己的配置,不涉及任何需要授权的目标侧写操作。
|
|
452
289
|
|
|
453
290
|
### 聚合日志(多选 / Compose 项目)
|
|
454
291
|
|
|
455
|
-
多选容器或打开一个 Compose 项目,都能把多个容器的日志聚合成一条流:**一条**
|
|
456
|
-
|
|
457
|
-
`docker logs -f`),客户端按到达顺序混流。工具栏提供:
|
|
292
|
+
多选容器或打开一个 Compose 项目,都能把多个容器的日志聚合成一条流:**一条** `/logs/stream?ids=…` 装下
|
|
293
|
+
全部容器(帧里带 `id`;宿主侧仍为每个容器各跑一条 `docker logs -f`),客户端按到达顺序混流。工具栏提供:
|
|
458
294
|
|
|
459
295
|
| 控件 | 语义 |
|
|
460
296
|
| --- | --- |
|
|
461
297
|
| **实时 / 已暂停** | 真暂停(见下) |
|
|
462
298
|
| **时间戳** | 显示每行时间戳。时间戳**始终随流接收**(`timestamps=1`),只影响显示 |
|
|
463
299
|
| **按到达 / 按时间** | `按到达`:零延迟跟随;`按时间`:用每行的容器时间戳合并成一条真时间线 |
|
|
464
|
-
| **级别过滤** | `全部级别 / WARN+ / ERROR+`。**无级别前缀的行(堆栈等续行)继承上一条日志的级别**,所以 ERROR+
|
|
465
|
-
| **⬇ .log / ⬇ .md** | 导出当前显示内容:`.log` 是纯行文本(`[服务] ISO时间 正文`),`.md` 带来源容器 / 行数 /
|
|
300
|
+
| **级别过滤** | `全部级别 / WARN+ / ERROR+`。**无级别前缀的行(堆栈等续行)继承上一条日志的级别**,所以 ERROR+ 会连它的堆栈一起保留;窗口开头的孤儿续行(记录头在窗口外)保留 |
|
|
301
|
+
| **⬇ .log / ⬇ .md** | 导出当前显示内容:`.log` 是纯行文本(`[服务] ISO时间 正文`),`.md` 带来源容器 / 行数 / 导出时间表头 |
|
|
466
302
|
|
|
467
|
-
**按时间合并怎么做的**:SSE 各容器建连有先后,A 的首屏历史可能整批先到、B
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
历史错序;代价是「按时间」模式下已在屏上的最近若干行可能轻微重排(正在跟随实时输出时
|
|
472
|
-
建议用「按到达」)。
|
|
303
|
+
**按时间合并怎么做的**:SSE 各容器建连有先后,A 的首屏历史可能整批先到、B 随后才到,只对单批排序修
|
|
304
|
+
不了跨批逆序。实现是**回填最近 400 行**——每批新行到达时把「最近 400 行 + 新行」整体按时间戳重排
|
|
305
|
+
(RFC3339 前缀为排序依据,解析后从正文里剥掉)。代价是「按时间」模式下已在屏上的最近若干行可能轻微
|
|
306
|
+
重排(正在跟随实时输出时建议用「按到达」)。
|
|
473
307
|
|
|
474
308
|
### 聚合日志的「暂停」(真暂停)
|
|
475
309
|
|
|
476
310
|
多选容器 / Compose 项目的聚合日志有一个 `实时 / 已暂停` 开关。**暂停是内容冻结**,不只是停止自动滚动:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
- 按钮上直接显示攒了多少行(`已暂停 +348`);
|
|
481
|
-
- 恢复时把缓冲一次性并入(沿用 5000 行环形上限)并回到底部。
|
|
482
|
-
|
|
483
|
-
只停「自动滚动」是不够的:标签写着「已暂停」而内容还在长,用户会以为开关坏了;日志量大时
|
|
484
|
-
画面还会因裁前部而自己跳。
|
|
311
|
+
暂停期间新到的日志进入客户端缓冲、**DOM 不再追加**(读屏不会被顶走,也不会因显示上限 2000 行裁前部
|
|
312
|
+
而跳屏);按钮上直接显示攒了多少行(`已暂停 +348`);恢复时把缓冲一次性并入(沿用 5000 行环形上限)
|
|
313
|
+
并回到底部。只停「自动滚动」是不够的:标签写着「已暂停」而内容还在长,用户会以为开关坏了。
|
|
485
314
|
|
|
486
315
|
### 总览(跨目标)与「需关注」口径
|
|
487
316
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
317
|
+
目标选择器旁的 `总览` pill(配了 ≥2 个目标才出现)一屏铺开全部目标:每个目标一张计数卡(运行中 /
|
|
318
|
+
已停止 / 不健康 / **需关注**),下方是跨目标的「需关注容器」表(容器名 / 目标 / 状态 / **原因** /
|
|
319
|
+
镜像,点行进详情、点卡切到该目标的列表)。三条设计约束:
|
|
320
|
+
|
|
321
|
+
- **渐进落地 + 失败隔离**:每个目标独立请求、独立落格;一台 SSH 不可达不会让整页静默(刻意**不用**
|
|
322
|
+
`Promise.all` 汇总:SSH 不可达要等 readyTimeout 20s,汇总等于让整页静默 20s)。
|
|
323
|
+
- **需关注口径以宿主为准**:容器列表与 `/attention` 并行请求;后者额外做一次 `docker inspect`,因此能
|
|
324
|
+
识别 **OOM(OOMKilled)** 与**真实退出码**——ps 摘要里 `Exited (137)` 分不出是被 OOM 杀还是手动 kill。
|
|
325
|
+
拿不到 `/attention`(老版本宿主 / 该目标失败)时退回摘要口径,并在计数上标注「需关注(粗判)」。
|
|
326
|
+
- **排序**:OOM > 僵死 > 不健康 > 反复重启 > 非零退出;同权重按「最近一次结束时间」倒序(行 hover 显示
|
|
327
|
+
结束/启动时间、重启次数、退出码)。
|
|
491
328
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
杀还是手动 kill。拿不到 `/attention`(老版本宿主 / 该目标失败)时退回摘要口径,并在
|
|
497
|
-
计数上标注「需关注(粗判)」。
|
|
498
|
-
- **排序**:OOM > 僵死 > 不健康 > 反复重启 > 非零退出;同权重按「最近一次结束时间」倒序,
|
|
499
|
-
刚崩的排在最上面(行 hover 显示结束/启动时间、重启次数、退出码)。
|
|
329
|
+
总览的轮询复用工具条的「自动刷新」开关(默认关):这一页每轮是全 N 个目标各一次 `docker ps`,比任何
|
|
330
|
+
单目标页都贵。没有异常显示「一切正常」;还有目标没答完时显示「读取中…」,**计数卡也进入读取态**(不出
|
|
331
|
+
0/0/0——「0 个容器」会被读成「这台机器没容器」,而它只是还没答)。进总览时会清掉单目标遗留的失败横幅:
|
|
332
|
+
总览按目标归因,不再叠一条单目标的「操作失败」——同一个 SSH 超时被讲两遍,看着像两台机器都挂了。
|
|
500
333
|
|
|
501
334
|
### 容器诊断(「为什么不行」)
|
|
502
335
|
|
|
503
|
-
容器详情多一个**诊断**页签,agent 侧是 `docker_diagnose`。它一次采集四路——`docker inspect
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
336
|
+
容器详情多一个**诊断**页签,agent 侧是 `docker_diagnose`。它一次采集四路——`docker inspect`(权威状态 /
|
|
337
|
+
退出码 / OOM / 重启次数 / 健康检查最后输出 / 资源上限)、`docker logs --tail N`、`docker events --since`
|
|
338
|
+
(**服务端**按 `--filter container=<id>` 过滤)、`docker stats --no-stream`——然后交给一组**纯判据**
|
|
339
|
+
(`src/diagnose.ts`,零 IO、不读时钟)产出:
|
|
507
340
|
|
|
508
341
|
- **一句话结论**(`verdict`):取最高严重度的那条,分 `critical` / `warning` / `info` / `ok`;
|
|
509
|
-
- **判据列表**(`findings`):每条 = 判据 code + 一句话 +
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
- **证据**(`evidence`):状态 / ps
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
诊断是**只读**:不改容器、不重启、不发命令,只多跑四条只读 docker 命令(因此最坏情况是四个
|
|
528
|
-
命令预算,而不是无界等待)。
|
|
342
|
+
- **判据列表**(`findings`):每条 = 判据 code + 一句话 + 证据。判据是这几条——OOM 被杀、非零退出(附
|
|
343
|
+
126 / 127 / 137 / 143 的常见含义)、反复重启、不健康(附健康检查最后输出)、健康检查尚未通过、正常
|
|
344
|
+
退出、未运行(created / paused / dead)、内存压力(≥90% 且设了上限)、CPU 压力(≥90%)、日志里有
|
|
345
|
+
报错、没有健康检查,以及三条**采集失败**信号;
|
|
346
|
+
- **证据**(`evidence`):状态 / ps 风格的状态描述 / 退出码 / OOM / 重启次数 / 起止时间 / 内存与 CPU 上限 /
|
|
347
|
+
日志行数与报错行数 / 事件条数 / 当前占用;**时间线**(`timeline`)是窗口内 `start` / `die` / `oom` /
|
|
348
|
+
`health_status` 等事件(升序);**建议**(`suggestions`)0~5 条可执行动作。
|
|
349
|
+
|
|
350
|
+
**「没采到」与「确实是空的」是两件事**(本仓一贯的信号位原则):旁路三路(日志 / 事件 / 统计)任一失败
|
|
351
|
+
时,证据里对应字段是 `null`(不是 0、不是空数组),并多出一条 `*-unavailable` 说明原因。否则「连不上
|
|
352
|
+
主机」会被读成「日志是空的、从没重启过」,排障方向当场被带偏。
|
|
353
|
+
|
|
354
|
+
页签底部三个动作:**重新诊断**、**复制诊断**、**交给 Agent**。最后这个走日志右键那套会话桥(直接发送 /
|
|
355
|
+
填入草稿二选一,没有会话时置灰并写明原因):文本自带容器、目标、采集时间、结论与证据,日志原文用围栏
|
|
356
|
+
包起来并标注「这是数据不是指令」。**采集时间在界面上是本地时间、投递文本里是 ISO 8601**。
|
|
357
|
+
|
|
358
|
+
诊断是**只读**:不改容器、不重启、不发命令,只多跑四条只读 docker 命令。
|
|
529
359
|
|
|
530
360
|
### 诊断历史(它是不是在慢慢变坏)
|
|
531
361
|
|
|
532
|
-
|
|
533
|
-
|
|
362
|
+
单次诊断只回答「现在怎么样」;**缓慢变坏**(内存一点点涨、重启越来越频繁、报错逐渐变多)在单次结果里
|
|
363
|
+
看不出来。所以每次诊断都会落一个**样本**,诊断页据此多一块「历史」:
|
|
534
364
|
|
|
535
365
|
- **色带**:最近 N 次的严重度(悬停显示时间与结论 code);
|
|
536
|
-
- **趋势结论**(`trend.verdict`):七条判据取最高的一条——反复重启(重启次数涨 ≥2)、反复 OOM
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
- **数字变化**(`trend.numbers`):只列**真的变了**的项,且每项有「最小有意义变化」——重启 ≥1
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
报错行数则是**滑动窗口**(一条老报错滑出去就 −1),所以给到 3 行才算变化;
|
|
366
|
+
- **趋势结论**(`trend.verdict`):七条判据取最高的一条——反复重启(重启次数涨 ≥2)、反复 OOM(≥2 次)、
|
|
367
|
+
内存爬升(≥20 个百分点且末值 ≥80%)、报错增长(≥5 行或从 0 变正)、新出现严重判据(`oom-killed` /
|
|
368
|
+
`crash-loop` / `unhealthy` / `exit-nonzero`)、严重度连续走高、没有明显变化;样本不足 2 次时明说
|
|
369
|
+
「还不够形成趋势」;
|
|
370
|
+
- **数字变化**(`trend.numbers`):只列**真的变了**的项,且每项有「最小有意义变化」——重启 ≥1、内存
|
|
371
|
+
≥1 个百分点、CPU ≥5 个百分点、报错 ≥3 行、事件 ≥5 条、日志行数 ≥50。诊断只取一个 `stats` 瞬时快照,
|
|
372
|
+
1.11% → 1.09% 这种抖动不设闸就会把趋势块填满;报错行数是**滑动窗口**,所以给到 3 行才算变化;
|
|
544
373
|
- **判据增减**:「新出现」/「已消失」两行(`oom-killed` 是新出现的,那就是恶化信号)。
|
|
545
374
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
375
|
+
**存什么、不存什么**:样本**只存结论与数字**(严重度、结论 code、判据 code 列表、状态 / 退出码 / OOM /
|
|
376
|
+
重启次数 / 内存与 CPU 占用 / 报错行数 / 事件条数 / 日志行数 / 时间)。**绝不落盘**:日志原文(`logTail`)
|
|
377
|
+
与每条判据的 detail(里面会嵌日志行,可能带凭证)——所以这份文件可以放心 `cat`,但它也因此**不能**用来
|
|
378
|
+
回放某次排障现场。
|
|
550
379
|
|
|
551
|
-
落盘:`<DSH 主目录>/dsh-docker/diagnosis-history.jsonl`(目录 `0700`、文件 `0600
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
三条已知边界:① **容器改名 = 开一段新历史**(索引键是「目标 + 容器名」)——比让两段历史混在一起、
|
|
558
|
-
趋势说出没发生过的变化好;② **并发追加没加锁**(两个诊断同时写可能丢一次更新),历史是辅助信息,
|
|
559
|
-
为它加文件锁不值;③ **写盘失败只降级不报错**:诊断照常返回、`recorded = false`、宿主 stdout 留一行。
|
|
380
|
+
落盘:`<DSH 主目录>/dsh-docker/diagnosis-history.jsonl`(目录 `0700`、文件 `0600`),一行一条。上限:
|
|
381
|
+
**每个容器 50 条**、整文件 **2000 条**,超了丢最旧并**原子重写**(临时文件 + rename)。**30 秒去重**:同一
|
|
382
|
+
容器、30 秒内、且严重度 + 结论 + 判据集合 + 重启次数完全相同时**不追加**。`DSH_DOCKER_HISTORY_DIR` 可
|
|
383
|
+
覆写目录(测试 / 排障专用),空串视为未设。三条边界:① **容器改名 = 开一段新历史**(索引键是「目标 +
|
|
384
|
+
容器名」);② **并发追加没加锁**(可能丢一次更新);③ **写盘失败只降级不报错**:诊断照常返回、
|
|
385
|
+
`recorded = false`、宿主 stdout 留一行。
|
|
560
386
|
|
|
561
387
|
### 磁盘与可回收(存储治理)
|
|
562
388
|
|
|
563
|
-
**磁盘**是工具条分段的最后一段(容器 / 镜像 / Compose / 网络 / 卷 / 磁盘),agent 侧是
|
|
564
|
-
|
|
389
|
+
**磁盘**是工具条分段的最后一段(容器 / 镜像 / Compose / 网络 / 卷 / 磁盘),agent 侧是
|
|
390
|
+
`docker_disk_usage`。数据来自两条命令:`docker system df`(四类汇总)与 `docker system df -v`(逐项明细)。
|
|
565
391
|
|
|
566
|
-
- 四张汇总卡:镜像 / 容器 / 本地卷 /
|
|
567
|
-
|
|
392
|
+
- 四张汇总卡:镜像 / 容器 / 本地卷 / 构建缓存各自的**条目数、活跃数、占用、可回收**(分类名保留 docker
|
|
393
|
+
原文,界面负责翻译);
|
|
568
394
|
- **合计可回收**:四类 `reclaimable` 的字节和(任一行解析不出就给 `—`,宁可不说也不给错数);
|
|
569
|
-
- **镜像明细**(按独占大小降序):仓库:标签 / 大小 / 独占 / **容器数**——`containers === 0`
|
|
570
|
-
|
|
395
|
+
- **镜像明细**(按独占大小降序):仓库:标签 / 大小 / 独占 / **容器数**——`containers === 0` 打「未使用」
|
|
396
|
+
徽标,行尾删除图标(受 `allowMutations` 门、走既有确认框);
|
|
571
397
|
- **卷明细**(按大小降序):名称 / 驱动 / 大小 / **链接数**——`links === 0` 同样打「未使用」。
|
|
572
398
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
- **明细上限 100 行**,超了在表头写「明细已截断,只显示前 100 行」(工具侧另按 20 行截,
|
|
578
|
-
两个 `truncated` 取或)——截断要留痕,不静默少列;
|
|
579
|
-
- **老 docker 只降级不报错**:不支持 `-v --format '{{json .}}'`、或输出被截断/形状不对时,
|
|
580
|
-
`degraded: true` + 一句人话原因,汇总卡照常显示、明细表**不渲染**(渲染空表会让人以为「没有」)。
|
|
399
|
+
三条刻意的取舍:**先排序再截断**(真机实测 259 个卷里 258 个是 0B,先截断会把 100 个名额全给空卷);
|
|
400
|
+
**明细上限 100 行**并在表头留痕(工具侧另按 20 行截,两个 `truncated` 取或);**老 docker 只降级不报错**
|
|
401
|
+
(不支持 `-v --format '{{json .}}'` 或输出形状不对时 `degraded: true` + 人话原因,汇总卡照常显示、明细表
|
|
402
|
+
**不渲染**——渲染空表会让人以为「没有」)。
|
|
581
403
|
|
|
582
|
-
磁盘页**不轮询**(`system df -v` 比 `docker images` 贵),也**没有 prune 按钮**:逐行删除 +
|
|
583
|
-
|
|
404
|
+
磁盘页**不轮询**(`system df -v` 比 `docker images` 贵),也**没有 prune 按钮**:逐行删除 + 确认框比
|
|
405
|
+
「一键清空一类」安全,而那类入口已经在镜像 / 卷页的工具条上各有一个。
|
|
584
406
|
|
|
585
407
|
### 等价 `docker run`(现场还原)
|
|
586
408
|
|
|
587
|
-
概览页底部有一块可折叠的**等价 docker run**,agent 侧是 `docker_run_command`。它从 `docker inspect`
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
409
|
+
概览页底部有一块可折叠的**等价 docker run**,agent 侧是 `docker_run_command`。它从 `docker inspect` 的
|
|
410
|
+
**原始行**反推一条可复现命令——**不扩 `ContainerDetail`**,因为那会把 env 顺带灌进 `docker_inspect` 的
|
|
411
|
+
模型上下文。覆盖:名称、重启策略、端口映射(没绑 hostPort 的用 `--expose`)、bind / 命名卷 / tmpfs、
|
|
412
|
+
非默认网络、环境变量、用户 / 工作目录 / 主机名(等于默认值时省略)、内存与 CPU 上限、cap-add / cap-drop /
|
|
413
|
+
device,以及镜像与命令本身。**点开才请求**,一行一个参数、POSIX 单引号转义,可直接复制。
|
|
592
414
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
而它的用途正是「拿出去跑」;
|
|
601
|
-
- **故意没还原的必须点名**(`omitted`):`--privileged`、`--link`、非默认日志驱动、
|
|
602
|
-
`--pid/--ipc/--uts`、`--sysctl`、`--add-host`、`--security-opt`、特殊挂载类型、匿名卷、
|
|
603
|
-
ENTRYPOINT 覆盖、自定义健康检查——逐条给出「为什么没写」与「怎么自己补上」,**绝不静默丢**;
|
|
604
|
-
- **compose 管理的容器只是近似**:网络别名 / labels / depends_on 不在命令里,界面直说。
|
|
415
|
+
三条刻意的取舍:**环境变量原值保留**(改写过就不是等价命令),命中
|
|
416
|
+
`/(PASS|PWD|SECRET|TOKEN|KEY|CRED|AUTH|PRIVATE)/i` 的**键名**收进 `secretKeys`——界面在命令上方给一条
|
|
417
|
+
「含疑似凭证」的警告,**只点键名、不显示值**,命令本身不脱敏(脱敏后的命令跑不起来);**故意没还原的
|
|
418
|
+
必须点名**(`omitted`:`--privileged`、`--link`、非默认日志驱动、`--pid/--ipc/--uts`、`--sysctl`、
|
|
419
|
+
`--add-host`、`--security-opt`、特殊挂载类型、匿名卷、ENTRYPOINT 覆盖、自定义健康检查——逐条给出「为什么
|
|
420
|
+
没写」与「怎么补」,**绝不静默丢**);**compose 管理的容器只是近似**(网络别名 / labels / depends_on
|
|
421
|
+
不在命令里,界面直说)。
|
|
605
422
|
|
|
606
423
|
## 配置(保存即热生效)
|
|
607
424
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
撤销按钮右对齐——两个能力的授权是分开的,并排放会看不出哪个撤销管哪个能力。
|
|
614
|
-
|
|
615
|
-
**两个能力的确认也能一次粘贴搞定**:面板开着时再点另一个开关不会顶掉前一块面板(两个能力的
|
|
616
|
-
授权本来就互不影响),两块面板**纵向堆叠**,顶部多出一个「复制全部(N 条)」——它把仍 pending 的
|
|
617
|
-
命令按**发起顺序**用**换行符**连接后一次复制,用户去宿主终端**粘贴一次**即可:`bash` / `zsh` /
|
|
618
|
-
PowerShell / cmd 粘贴多行都逐行执行,所以刻意不用 `&&`(PowerShell 5.1 不认)或 `;`(cmd 不认)
|
|
619
|
-
连接。某条过期后,合并按钮只复制仍 pending 的那些;全部解锁(或全部收起)后它自己消失。两个
|
|
620
|
-
challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后全部开关自动解锁」、**不承诺同一时刻**
|
|
621
|
-
——倒计时仍是逐面板显示,每块面板的复制 / 重新生成 / 收起也照旧只管自己那一个能力。
|
|
622
|
-
|
|
623
|
-
为什么显示时刻:带外授权是**持久**的——宿主重启后它**直接生效、不再有任何一次确认**。也就是说
|
|
624
|
-
「上个月授权的能力今天一开机就开着」这件事不会有任何提示,除非界面与日志说出来。所以宿主启动时会
|
|
625
|
-
逐条打一行 `[dsh-docker] elevation: load capability=… via=file grantedAt=…`(不含 nonce 与路径),
|
|
626
|
-
卡片上也把它显示出来。启动环境变量授权**没有**时刻(它就是启动环境的一部分,编一个假时间更糟),
|
|
627
|
-
那一行显示的是「由启动环境变量授权;要撤销需在启动环境里去掉它并重启宿主」。
|
|
425
|
+
未获宿主授权时,设置卡片里那两个开关**点它会就地发起一次授权**:面板给出一条「在宿主终端执行」的命令,
|
|
426
|
+
执行完十秒内自动解锁并替你打开开关(免重启);面板里另有「另一种方式(最强)」说明启动环境变量的做法。
|
|
427
|
+
已授权时它们就是普通开关,**关掉**永远可用(宿主侧不拦降权)。「配置开着但没授权」会显示「未生效:未获
|
|
428
|
+
宿主授权」徽标——这个状态最容易被当成插件坏了,所以必须有个名字。**一个能力一行**:开关后面跟这个能力
|
|
429
|
+
自己的授权状态与**授权时刻**(`已授权 · 2026-09-26 22:31:08`),撤销按钮右对齐。
|
|
628
430
|
|
|
629
|
-
|
|
431
|
+
**两个能力的确认也能一次粘贴搞定**:再点另一个开关不会顶掉前一块面板,两块面板**纵向堆叠**,顶部多出
|
|
432
|
+
一个「复制全部(N 条)」。
|
|
630
433
|
|
|
631
|
-
|
|
632
|
-
|
|
434
|
+
为什么显示时刻:带外授权是**持久**的——宿主重启后它直接生效、不再有任何一次确认,所以宿主启动时会逐条
|
|
435
|
+
打一行 `[dsh-docker] elevation: load capability=… via=file grantedAt=…`(不含 nonce 与路径),卡片上
|
|
436
|
+
也显示出来。启动环境变量授权**没有**时刻,那一行写的是「由启动环境变量授权;要撤销需在启动环境里去掉它
|
|
437
|
+
并重启宿主」。
|
|
633
438
|
|
|
634
|
-
|
|
635
|
-
|---|---|
|
|
636
|
-
| `0.2.0-rc.1` 起 | **插件详情页「说明」正下方**(`plugins.bundle.config`,key = **本包自己的** bundle 包名)——设置就在详情页里,不必再点一次「>」进二级页 |
|
|
637
|
-
| `0.1.6` 线 | 侧边栏「插件」→ 该 bundle 的**行** → 「>」行详情(`plugins.row.config`) |
|
|
638
|
-
| 以上各版 | 设置 → **插件配置** 里的同一条目(`settings.kit.item`,由 `@hyzyn/dsh-kit-settings` 提供) |
|
|
439
|
+

|
|
639
440
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
安装下继续走 row 入口。
|
|
441
|
+
设置面在宿主里的**位置**随 DSH 版本变,但**始终是同一份表单**(同一个 settings 命名空间、同一条写入
|
|
442
|
+
通道,配置数据与行为完全相同)——各代际的落点与四条注册契约的唯一归宿在
|
|
443
|
+
[conventions.md § 客户端设置面](../../docs/conventions.md#客户端设置面内联优先)。
|
|
644
444
|
|
|
645
|
-
配置落在 settings 命名空间 `docker`,即 `~/.dsh/settings.yaml` 的 `docker:`
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
它;HTTP `POST /api/dsh-docker/config` 是卡片的写入通道,只接受下表这些键
|
|
649
|
-
(未知键返回 400)。
|
|
445
|
+
配置落在 settings 命名空间 `docker`,即 `~/.dsh/settings.yaml` 的 `docker:` 段
|
|
446
|
+
(`$DSH_HOME/settings.yaml`)。插件行里的 composition 配置作为 schema `base` 打底,settings 层覆盖它;
|
|
447
|
+
HTTP `POST /api/dsh-docker/config` 是卡片的写入通道,只接受下表这些键(未知键返回 400)。
|
|
650
448
|
|
|
651
449
|
| 项 | 默认 | 说明 |
|
|
652
450
|
| --- | --- | --- |
|
|
653
451
|
| `enabled` | true | 关闭整个插件(**保存即热生效**:工具立即注销、公告撤下、除 `/config` 外的数据路由 403;`/config` 始终可读写——设置卡片就是重新启用的入口。与 tty 不同:tty 是重启生效) |
|
|
654
452
|
| `announceToAgent` | true | 是否向 agent 注入能力公告(systemPrompt section `plugin:dsh-docker`) |
|
|
655
|
-
| `dockerBin` | `docker` | docker CLI 可执行名或路径(podman 可填 `podman`);只允许字母、数字与 `_ . / \ : -` 及内部空格,且不能以 `-` 开头(**Windows 盘符与 `\`
|
|
453
|
+
| `dockerBin` | `docker` | docker CLI 可执行名或路径(podman 可填 `podman`);只允许字母、数字与 `_ . / \ : -` 及内部空格,且不能以 `-` 开头(**Windows 盘符与 `\` 必须放行**)。**「连接本机」的探测用的就是它**;填错时那句话会点名 `PATH` 里找到的别的容器 CLI,但**不会**替你改这个值 |
|
|
656
454
|
| `allowMutations` | false | 允许**变更操作**:容器 start / stop / restart / remove、镜像删除 / dangling 清理 / 拉取(面板按钮与 `docker_action`、`docker_image_remove`、`docker_image_prune`、`docker_image_pull` 工具;关闭时 `/action`、`/images/remove`、`/images/prune`、`/images/pull/stream` 返回 403,对应工具不注册) |
|
|
657
|
-
| —(能力授权) | 未授权 | `allowMutations` / `allowExec` 有**两条提权通道**:① **启动环境变量**(`DSH_DOCKER_ALLOW_MUTATIONS` / `DSH_DOCKER_ALLOW_EXEC`,值为 `1` / `true` / `yes` / `on`)——判定源是宿主的**启动环境快照**,只认继承来的 `process` 层:写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权;② **就地提权**(免重启):在设置卡片点开关 → 面板给出一条「在宿主终端执行」的命令 →
|
|
455
|
+
| —(能力授权) | 未授权 | `allowMutations` / `allowExec` 有**两条提权通道**:① **启动环境变量**(`DSH_DOCKER_ALLOW_MUTATIONS` / `DSH_DOCKER_ALLOW_EXEC`,值为 `1` / `true` / `yes` / `on`)——判定源是宿主的**启动环境快照**,只认继承来的 `process` 层:写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权;② **就地提权**(免重启):在设置卡片点开关 → 面板给出一条「在宿主终端执行」的命令 → 执行后十秒内生效。HTTP 侧永远可以**关掉**它们(紧急刹车不能依赖重启),但给 `true` 而无授权会被 400 拒绝并说清两条路。配置里的 `true` **不算授权**(它与 HTTP 写进去的值存在同一个存储里,分不出来源)。**升级影响**:升级前靠界面打开的开关会变成关——设环境变量重启,或在卡片里就地确认。**为什么**:回环围栏与同源证明都拦不住跨站页面与页内脚本,而 docker socket 等价目标主机 root;细节(含拦不住谁)见 [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么) |
|
|
658
456
|
| `allowExec` | false | 允许**一次性** `docker exec`(**容器详情抽屉**「概览」页签里的一次性命令框,与 `docker_exec` 工具;关闭时 `/exec` 返回 403)。**不管**卡片「终端」按钮的 `docker exec -it` 交互式 shell——那条由 tty 承载(见「已知限制」) |
|
|
659
457
|
| `execTimeoutSec` | 30 | exec 默认超时秒数(1~120) |
|
|
660
458
|
| `pollIntervalSec` | 5 | 面板统计刷新间隔秒数(1~60) |
|
|
@@ -681,17 +479,12 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
681
479
|
| `passphrase` | `''` | 私钥口令;**建议填 `env:NAME`** 凭据引用 |
|
|
682
480
|
| `agentForward` | false | 是否转发本机 ssh-agent(`SSH_AUTH_SOCK` 存在时生效) |
|
|
683
481
|
|
|
684
|
-
`password` / `passphrase` 里的 `env:NAME`
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
2. 服务不可用(老宿主 / 未装该 bundle)或它没有这个引用时,退回 `process.env[NAME]`。
|
|
691
|
-
|
|
692
|
-
两边都没有会明确报错并**点名两个来源**(服务报错也一并带上——否则"凭据服务坏了"会伪装成
|
|
693
|
-
"你没配",那是最难查的一类)。这两个值**永不回传浏览器**:
|
|
694
|
-
配置快照里只给 `passwordSet` / `passphraseSet` 两个布尔位。
|
|
482
|
+
`password` / `passphrase` 里的 `env:NAME` 是**凭据引用**(配置只持引用、值归 provider),**连接时才
|
|
483
|
+
解析**:① **官方凭据层**(`ctx.credentials`)——叠 `file`(`$DSH_HOME/.credentials.yaml`)/ `env` /
|
|
484
|
+
`project-env` / `user-env` 各层,且「每次操作重新解析」,所以**改完下一个操作即生效、不必重启宿主**;
|
|
485
|
+
② 服务不可用(老宿主 / 未装该 bundle)或它没有这个引用时,退回 `process.env[NAME]`。两边都没有会明确
|
|
486
|
+
报错并**点名两个来源**(服务报错也一并带上——否则"凭据服务坏了"会伪装成"你没配")。这两个值**永不回传
|
|
487
|
+
浏览器**:配置快照里只给 `passwordSet` / `passphraseSet` 两个布尔位。
|
|
695
488
|
|
|
696
489
|
### `hostKeys[]`(SSH 主机指纹,TOFU)
|
|
697
490
|
|
|
@@ -702,108 +495,100 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
702
495
|
| `fingerprints` | **指纹集合**(必填,至少一条):`hostHash: 'sha256'` 回调收到的原样十六进制指纹。同一 host:port 可含多把主机密钥(rsa / ed25519 …),命中任意一条即视为匹配 |
|
|
703
496
|
| `fingerprint` | 旧版单指纹字段(字符串):只作迁移输入,清洗时并入 `fingerprints`,新配置不要再用 |
|
|
704
497
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
498
|
+
首次连接自动记录并落盘;之后每次连接必须匹配,**指纹变更直接拒绝连接**,错误信息带「删除该主机记录再
|
|
499
|
+
重连」的指引。记录列表在设置卡片里可删除重置(删除会以显式的 `hostKeysRemove: [{host, port}]` 提交,
|
|
500
|
+
不会被并集合并悄悄撤销)。
|
|
708
501
|
|
|
709
502
|
## agent 工具
|
|
710
503
|
|
|
711
504
|
| 工具 | 注册条件 | 参数 | 作用 / 典型用法 |
|
|
712
505
|
| --- | --- | --- | --- |
|
|
713
|
-
| `docker_targets` | 恒注册 | `probe?: boolean` | 列出目标(name / kind / label
|
|
714
|
-
| `docker_connect_local` | 恒注册 | —(无参数) | **连接本机**:确保存在一条 `kind=local` 目标并选中它(已存在则复用,不重复创建;SSH / 自定义目标原样保留),随后只读探测 daemon:返回 `{name, kind, created, saved, reachable, serverVersion?}
|
|
715
|
-
| `docker_ps` | 有目标即注册 | `target?`(**传 `*` = 全部目标**)、`all?: boolean`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 列容器(名称 / 状态 / 健康 / 镜像 / 端口 / compose 项目与服务 / 短 ID);默认只列运行中,`all:true` 含已停止。`target:'*'` 时按目标分组返回,**单个目标不可达不影响其他目标**(该组带 `error`)。端口是 IPv4/IPv6
|
|
716
|
-
| `docker_attention` | 有目标即注册 | `target?`(支持 `*`)、`limit?: number` | **需关注汇总**:不健康 / 反复重启 / 被 OOM 杀 / 非零退出 / 僵死;每条带 `reasons`、`exitCode`、`oomKilled`、`restartCount`。OOM 与真实退出码来自一次 `docker inspect
|
|
717
|
-
| `docker_diagnose` | 有目标即注册 | `target?`、`id`(必填)、`logTail?`(1~500,默认 80)、`since?`(docker 语法,默认 `1h`)、`history?`(0~50,默认 10,0 = 不要历史) | **容器「为什么不行」**:一次采集 inspect + 日志尾部 + 本容器事件(服务端 `--filter container`)+ `stats` 快照,产出 `verdict
|
|
506
|
+
| `docker_targets` | 恒注册 | `probe?: boolean` | 列出目标(name / kind / label),**SSH 目标还会带出它引用的 tty 连接簿条目名 `book`**(内联 host/username 的目标没有这个字段;用它去终端面板 / `sftp_*` 的 `book`,名字对不上时只能靠它);`probe:true` 逐个探测 docker 版本与 daemon 可达性(SSH 目标会建连接,较慢)。其他工具的 `target` 取自这里 |
|
|
507
|
+
| `docker_connect_local` | 恒注册 | —(无参数) | **连接本机**:确保存在一条 `kind=local` 目标并选中它(已存在则复用,不重复创建;SSH / 自定义目标原样保留),随后只读探测 daemon:返回 `{name, kind, created, saved, reachable, serverVersion?}`。没有任何目标时其余工具不注册(见下方「零目标」那条),用它一步补上。**不需要任何能力授权**;不会安装 Docker / 启动 daemon / 切 context / 改环境变量。daemon 不可达时说清成因(CLI 缺失 / 探测超时 / daemon 未启动 / socket 无权),不是只报空列表 |
|
|
508
|
+
| `docker_ps` | 有目标即注册 | `target?`(**传 `*` = 全部目标**)、`all?: boolean`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 列容器(名称 / 状态 / 健康 / 镜像 / 端口 / compose 项目与服务 / 短 ID);默认只列运行中,`all:true` 含已停止。`target:'*'` 时按目标分组返回,**单个目标不可达不影响其他目标**(该组带 `error`)。端口是 IPv4/IPv6 双栈归并后的映射(D130);`ports` 为空**不等于**没暴露端口——host 网络容器的端口即宿主机端口,这种情况会给 `net` 字段(D131)。排障第一步 |
|
|
509
|
+
| `docker_attention` | 有目标即注册 | `target?`(支持 `*`)、`limit?: number` | **需关注汇总**:不健康 / 反复重启 / 被 OOM 杀 / 非零退出 / 僵死;每条带 `reasons`、`exitCode`、`oomKilled`、`restartCount`。OOM 与真实退出码来自一次 `docker inspect`。排障入口:不确定从哪台/哪个容器看起时先调它 |
|
|
510
|
+
| `docker_diagnose` | 有目标即注册 | `target?`、`id`(必填)、`logTail?`(1~500,默认 80)、`since?`(docker 语法,默认 `1h`)、`history?`(0~50,默认 10,0 = 不要历史) | **容器「为什么不行」**:一次采集 inspect + 日志尾部 + 本容器事件(服务端 `--filter container`)+ `stats` 快照,产出 `verdict` / `findings`(每条带 code、severity、title、detail)/ `evidence` / `timeline` / `suggestions`。**只读**;「没采到」给 `null` 并附 `*-unavailable` 判据,绝不写成 0。要更多日志 / 找关键词用 `docker_logs_grep`。返回里还带 `history`(最近 N 个**只含结论与数字**的样本 + 趋势结论 `trend`)——`trend.verdict.summary` 就是「它是不是在慢慢变坏」那一句 |
|
|
718
511
|
| `docker_inspect` | 有目标即注册 | `target?`、`id`(必填) | `docker inspect` 的权威详情:状态 / 健康检查 / 退出码 / 重启次数 / 端口 / 挂载 / 网络 / 启动命令 |
|
|
719
|
-
| `docker_logs` | 有目标即注册 | `target?`、`id`、`tail?`(1~5000,默认 `logTailDefault`)、`timestamps?`、`since?`、`until?` | `docker logs --tail` 尾部;`since` / `until` 用 docker
|
|
720
|
-
| `docker_logs_grep` | 有目标即注册 | `target?`、`id`、`pattern`(必填,≤512 字符)、`regexp?`、`ignoreCase?`、`since?`、`until?`、`context?`(0~20,默认 2)、`limit?`(1~500,默认 200)、`tail?`(缺省 = 扫全部历史)、`timestamps?`(默认开) | **日志服务端检索**:宿主侧逐行过滤(不碰远端 shell、不拼 `\| grep`),返回命中行 + 前后上下文,并如实报告 `matched
|
|
512
|
+
| `docker_logs` | 有目标即注册 | `target?`、`id`、`tail?`(1~5000,默认 `logTailDefault`)、`timestamps?`、`since?`、`until?` | `docker logs --tail` 尾部;`since` / `until` 用 docker 语法圈时间窗;超上限标记 `truncated`;截断时结果里会说明可传更小的 `tail`、加 `since`/`until`,或改用 `docker_logs_grep` |
|
|
513
|
+
| `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`——「只扫到这些」与「扫完了没更多」可区分;扫描窗口**对齐到行边界**(按字节截断处的半行不当作一行返回);返回行数另有上限:最多 4000 行(`MAX_RESULT_LINES`),超了截到上限并给 `truncatedForSize` 与 `notes`,`matched` 仍报全量。与 `docker_logs` 的分工:报错就在最近尾部 → `docker_logs`(便宜);埋在深处 / 特定时间窗 / 找关键词、异常码、请求 ID → 本工具。默认字面子串(grep -F 语义,零 ReDoS),`regexp: true` 才按 JS 正则 |
|
|
721
514
|
| `docker_stats` | 有目标即注册 | `target?`、`ids?`(逗号分隔的容器名/ID) | `docker stats --no-stream` 快照:CPU% / 内存用量与占比 / 网络 IO / 块 IO / PIDs;`ids` 省略 = 全部运行中容器。实时跟随是面板能力(SSE),工具保持单值快照语义 |
|
|
722
515
|
| `docker_images` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 镜像列表(仓库:标签 / 大小 / 创建时间 / 短 ID) |
|
|
723
516
|
| `docker_top` | 有目标即注册 | `target?`、`id`(必填) | 容器内进程快照(`docker top <id> -eo pid,ppid,user,stat,etime,pcpu,pmem,cmd`):PID / PPID / 用户 / STAT / 已运行 / CPU / 内存 / 命令,字段全是 docker 打印的原文。容器「还在跑」与「进程还在」是两件事 |
|
|
724
|
-
| `docker_events` | 有目标即注册 | `target?`、`since?`(docker `--since` 语法,默认 `10m`) |
|
|
725
|
-
| `docker_networks` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 网络列表(名称 / 驱动 / 范围 / 是否 internal / 短 ID
|
|
517
|
+
| `docker_events` | 有目标即注册 | `target?`、`since?`(docker `--since` 语法,默认 `10m`) | 容器事件快照(过服务端白名单):start / die / stop / kill / oom / health_status / destroy / rename / update 九类,`exec_*` 等噪音已在服务端丢掉。要持续观察请让用户看面板容器列表的「活动」条 |
|
|
518
|
+
| `docker_networks` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 网络列表(名称 / 驱动 / 范围 / 是否 internal / 短 ID)。接入的容器列表不进列表行——列表逐行 inspect 就是 N 次 docker 调用 |
|
|
726
519
|
| `docker_volumes` | 有目标即注册 | `target?`、`limit?`(1~500,默认 100)、`offset?`(默认 0) | 卷列表(名称 / 驱动 / 范围 / 挂载点) |
|
|
727
|
-
| `docker_disk_usage` | 有目标即注册 | `target?` | 磁盘占用与**「谁在用」**:`docker system df`
|
|
728
|
-
| `docker_run_command` | 有目标即注册 | `target?`、`id`(必填) | **只推导、不执行**:从 `docker inspect` 原始数据反推等价 `docker run`
|
|
729
|
-
| `docker_image_inspect` | 有目标即注册 | `target?`、`ref`(必填) | `docker image inspect` + `docker history`:大小 / 含父层大小 / 创建时间 / 平台 / 层数与层列表 / 入口与命令 / 暴露端口 / digest /
|
|
520
|
+
| `docker_disk_usage` | 有目标即注册 | `target?` | 磁盘占用与**「谁在用」**:`docker system df` 四类汇总 + `-v` 明细(镜像的 `containers`、卷的 `links`,各截 20 行)。删之前先看 `containers` 是不是 0;汇总与明细是两条命令,明细失败只降级(`degraded`) |
|
|
521
|
+
| `docker_run_command` | 有目标即注册 | `target?`、`id`(必填) | **只推导、不执行**:从 `docker inspect` 原始数据反推等价 `docker run` 命令文本 + `warnings` / `secretKeys`(只给键名,值不脱敏)/ `omitted`(故意没还原的,逐条给原因)。compose 容器只是近似 |
|
|
522
|
+
| `docker_image_inspect` | 有目标即注册 | `target?`、`ref`(必填) | `docker image inspect` + `docker history`:大小 / 含父层大小 / 创建时间 / 平台 / 层数与层列表 / 入口与命令 / 暴露端口 / digest / 构建历史 |
|
|
730
523
|
| `docker_action` | `allowMutations` + 有目标 | `target?`、`action`(`start` \| `stop` \| `restart` \| `remove`)、`id` | 容器生命周期操作。`remove` 是破坏性的:删除容器配置与可写层(数据卷不在其中),执行前必须向用户确认目标容器 |
|
|
731
524
|
| `docker_image_remove` | `allowMutations` + 有目标 | `target?`、`ref`(必填) | 删除镜像(`docker image rm`,不带 `-f`)。**破坏性**:被容器或子镜像引用时会失败;执行前须确认目标镜像并复述后果 |
|
|
732
525
|
| `docker_image_prune` | `allowMutations` + 有目标 | `target?` | 清理 dangling(无标签)镜像(`docker image prune -f`)。刻意不加 `--all`,只删无标签镜像 |
|
|
733
526
|
| `docker_image_pull` | `allowMutations` + 有目标 | `target?`、`ref`(必填)、`timeoutSec?`(10~1800,默认 600) | `docker pull` 快照形态(**可能耗时数分钟**);交互式看逐层进度请让用户到面板镜像页的拉取图标看 SSE 进度流 |
|
|
734
527
|
| `docker_exec` | `allowExec` + 有目标 | `target?`、`id`、`command`(必填,经容器内 `sh -c` 执行)、`timeoutSec?`(1~120,默认 `execTimeoutSec`) | 一次性 `docker exec`,返回退出码 / stdout / stderr;无 TTY,交互式排障请让用户到 tty 面板跑 `docker exec -it <容器> sh` |
|
|
735
528
|
|
|
736
|
-
- `target`
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
`
|
|
759
|
-
`
|
|
760
|
-
`links` 谁在用),再决定删什么。
|
|
761
|
-
- `announceToAgent` 开启时,插件向 systemPrompt 注入一段精简公告:插件与面板位置、「默认
|
|
762
|
-
只读」与「docker socket ≈ 目标主机 root」的约束、「`allowExec` 只管一次性 exec 通道」的
|
|
763
|
-
边界(卡片「终端」按钮不受它门控)、`target` 的 `*` 适用范围,以及跨工具排障顺序;它不
|
|
764
|
-
逐条复述工具用法——那是各工具 description 的职责。
|
|
529
|
+
- `target` 省略时回落到**唯一**已配置目标;配置了多个目标则必填,错误信息会列出可用目标名。
|
|
530
|
+
- **零目标时只有 `docker_targets` / `docker_connect_local` 注册**:其余工具没有可寻址对象,注册了只会让
|
|
531
|
+
模型白试一次;配好目标后**立即补注册**,无需重启。
|
|
532
|
+
- **四条列表工具支持分页**:`docker_ps` / `docker_images` / `docker_networks` / `docker_volumes` 都接受
|
|
533
|
+
`limit`(1~500,默认 100)与 `offset`(默认 0),结果带 `total` / `truncated`,截断时给出下一页的
|
|
534
|
+
`offset`。分页只解决「结果长到读不下去」,不改变单条 docker 命令 512KB 的上限。
|
|
535
|
+
- 两个开关变化会**立即重注册**工具:关掉 `allowMutations` / `allowExec` 后,对应工具(含三个镜像变更
|
|
536
|
+
工具)从 agent 侧消失,无需重启。
|
|
537
|
+
- **已注册的调用还受会话权限档位闸(tier gate)约束**:`read` 类(上表标「有目标即注册」的读工具)全档
|
|
538
|
+
放行;`docker_action` / `docker_image_remove` / `docker_image_prune` / `docker_image_pull` 归 `write`
|
|
539
|
+
类,`docker_exec` 归 `exec` 类,这两类在受限档(仅可查看 / 工作区内修改)下**每次调用弹授权询问**(人
|
|
540
|
+
允许这一次才执行;无人值守即审批策略 `never` 时确定性拒绝),完全权限档零询问,Auto review 档不叠加。
|
|
541
|
+
宿主未组合 `sandboxPolicy` / `approval` 服务时不闸,启动日志打一行
|
|
542
|
+
`tier-gate: services absent (…), per-call gate disabled`。它与上面两个静态开关是**双层**:静态开关管
|
|
543
|
+
注册(宿主级、持久授权),档位闸管已注册调用的放行(逐调用、随档位即时变)。机制见
|
|
544
|
+
[docs/permission-tier-plan.md](../../docs/permission-tier-plan.md)。
|
|
545
|
+
- 推荐排障顺序:`docker_targets` → `docker_ps` → **`docker_diagnose`**(一次拿到结论 + 判据 + 证据,多数
|
|
546
|
+
「它为什么挂了」到这里就有方向了)→ `docker_logs`(尾部看不到成因就 `docker_logs_grep` 带 `pattern` /
|
|
547
|
+
`since` / `until` 检索深处)→ `docker_inspect`(原始字段)→ `docker_stats` / `docker_top`(活着但不对劲:
|
|
548
|
+
先看资源,再看进程还在不在);镜像排查 `docker_images` → `docker_image_inspect`;要清磁盘先
|
|
549
|
+
`docker_disk_usage`(看 `containers` / `links` 谁在用),再决定删什么。
|
|
550
|
+
- `announceToAgent` 开启时,插件向 systemPrompt 注入一段精简公告:插件与面板位置、「默认只读」与
|
|
551
|
+
「docker socket ≈ 目标主机 root」的约束、「`allowExec` 只管一次性 exec 通道」的边界(卡片「终端」按钮
|
|
552
|
+
不受它门控)、`target` 的 `*` 适用范围,以及跨工具排障顺序;它不逐条复述工具用法。
|
|
765
553
|
|
|
766
554
|
## HTTP 路由(`/api/dsh-docker` 前缀,全部 loopback 围栏)
|
|
767
555
|
|
|
768
|
-
围栏校验 `remoteAddress`(整个 127/8 + `::1` + `::ffff:` 映射)、`Host`(字面量环回、`localhost
|
|
769
|
-
|
|
556
|
+
围栏校验 `remoteAddress`(整个 127/8 + `::1` + `::ffff:` 映射)、`Host`(字面量环回、`localhost`,或
|
|
557
|
+
**解析到本机的主机名 / `/etc/hosts` 别名**,解析带 500ms 超时与 60s 缓存)、`Origin` 与 `sec-fetch-site`;
|
|
770
558
|
非本机请求一律 403 `forbidden: loopback-only`。请求体上限 1MB,响应统一 `application/json` +
|
|
771
559
|
`referrer-policy: no-referrer`。
|
|
772
560
|
|
|
773
561
|
**除 loopback 之外还有一道「同源证明」**:四条 SSE(`/logs/stream`、`/stats/stream`、`/events/stream`、
|
|
774
|
-
`/images/pull/stream
|
|
775
|
-
`/volumes/remove|prune`、`/exec`、`/connect-local`)要求请求带 `Origin: <同源>` 或
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
**例外:桌面版靠宿主 Cookie 过闸(D139
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
`sec-fetch-site: cross-site`(loopback 围栏先拒),所以拒绝分支没有被放松。修复前桌面版的这十二条
|
|
787
|
-
路由**全部** 403——症状是日志 / 统计 / 活动 / 拉取四条流无限「连接中断,正在自动重连…」,而只读
|
|
788
|
-
路由照常可用。
|
|
562
|
+
`/images/pull/stream`)与全部写子路由(`/action`、`/images/remove|prune`、`/networks/remove|prune`、
|
|
563
|
+
`/volumes/remove|prune`、`/exec`、`/elevate`(含 `/elevate/status`、`/elevate/revoke`)、`/connect-local`)要求请求带 `Origin: <同源>` 或
|
|
564
|
+
`Sec-Fetch-Site: same-origin`,否则 403 `缺少同源证明`。这是为了挡住「恶意页面用
|
|
565
|
+
`<img src=.../images/pull/stream>` 触发一次真实拉取」这类跨站副作用;curl / 老 Safari / 部分 WebView 不带
|
|
566
|
+
这两个头时会撞上它(浏览器正常使用不受影响)。只读路由不要求同源证明。
|
|
567
|
+
|
|
568
|
+
**例外:桌面版靠宿主 Cookie 过闸(D139)**。桌面版把页面发往 `dsh-app://app/api/…` 的请求转给真实宿主时
|
|
569
|
+
会**删掉 `Origin` 与 `Sec-Fetch-Site`**、只重写 `Cookie`(`forwardWebRequest`),所以「两条证明都缺省」
|
|
570
|
+
在桌面版是**常态**。这类转发的判定条件是**带着宿主会话 Cookie**(缺它整条转发直接 503):**没有
|
|
571
|
+
`Origin`、没有 `Sec-Fetch-Site`、但有 `Cookie` → 放行**;两条都没有且**无** Cookie(裸 curl / 老 Safari)
|
|
572
|
+
→ 仍 403。浏览器页面伪造不了 `Cookie` 头,而跨站请求一定带 `sec-fetch-site: cross-site`(loopback 围栏
|
|
573
|
+
先拒),所以拒绝分支没有被放松;症状与排查见 [docs/troubleshooting.md](../../docs/troubleshooting.md)。
|
|
789
574
|
|
|
790
575
|
| 路由 | 方法 | 请求体 | 返回 |
|
|
791
576
|
| --- | --- | --- | --- |
|
|
792
|
-
| `/config` | GET | — | `{ok:true, config}`:配置快照(targets 只给 `passwordSet` / `passphraseSet`,另附只读的 `ttyBooks` / `ttyAvailable` / `toolsRegistered` / `streamProtocol`——最后这个是 SSE
|
|
577
|
+
| `/config` | GET | — | `{ok:true, config}`:配置快照(targets 只给 `passwordSet` / `passphraseSet`,另附只读的 `ttyBooks` / `ttyAvailable` / `toolsRegistered` / `streamProtocol`——最后这个是 SSE 帧协议版本,客户端据此决定「怎样才算已就绪」) |
|
|
793
578
|
| `/config` | POST | 上表配置键的任意子集 | `{ok:true, config}`;未知键 400,非法 JSON 400 |
|
|
794
579
|
| `/targets` | GET / POST | — | `{ok:true, targets:[{name, kind, label?\|error?}]}` |
|
|
795
580
|
| `/connect-local` | POST | `{}`(请求体被忽略) | **一键连接本机**:复用或新建 `kind=local` 目标并返回 `{ok:true, result:{name, kind, created, saved, reachable, serverVersion?, message}, config}`;宿主没有 settings 服务时 503,保存失败时 500(都带原因且不改内存配置)。要求同源证明(它是写路径),但**不需要** `allowMutations` / `allowExec` |
|
|
796
581
|
| `/probe` | POST | `{target?}` | `{ok:true, probe:{ok, bin, serverVersion, error, target}}` |
|
|
797
582
|
| `/containers` | POST | `{target?, all?}` | `{ok:true, containers: ContainerSummary[]}`;`target:'*'` 时返回 `{ok:true, groups:[{target,label,ok,error?,data?}]}`(跨目标并发聚合) |
|
|
798
|
-
| `/attention` | POST | `{target?, limit?}` | 单目标 `{ok:true, items
|
|
583
|
+
| `/attention` | POST | `{target?, limit?}` | 单目标 `{ok:true, items, total, truncated, degraded}`(`limit` 在过滤 + 严重度排序**之后**生效,默认 100、上限 500);`target:'*'` 时按目标分组 |
|
|
799
584
|
| `/inspect` | POST | `{target?, id}` | `{ok:true, details: ContainerDetail[]}` |
|
|
800
585
|
| `/stats` | POST | `{target?, ids?: string[]}` | `{ok:true, stats: ContainerStats[]}` |
|
|
801
586
|
| `/logs` | POST | `{target?, id, tail?, timestamps?, since?}` | `{ok:true, logs:{id, text, truncated}}` |
|
|
802
587
|
| `/top` | POST | `{target?, id}`(必填) | `{ok:true, top:{id, name, rows:[{pid, ppid, user, stat, elapsed, cpu, mem, command}], truncated}}`——字段全是 `docker top` 打印的原文;未运行 / 不存在时走既有错误通道(不是 200 空列表) |
|
|
803
|
-
| `/diagnose` | POST | `{target?, id}`(必填)、`logTail?`(1~500,默认 80)、`since?`(默认 `1h`)、`history?`(0~50,默认 10) | `{ok:true, diagnosis:{verdict, findings, evidence, timeline, logTail, suggestions, history:{samples, trend, recorded}}}`。`history` 是本次记录**之后**的最近 N 条样本与趋势(`recorded`
|
|
588
|
+
| `/diagnose` | POST | `{target?, id}`(必填)、`logTail?`(1~500,默认 80)、`since?`(默认 `1h`)、`history?`(0~50,默认 10) | `{ok:true, diagnosis:{verdict, findings, evidence, timeline, logTail, suggestions, history:{samples, trend, recorded}}}`。`history` 是本次记录**之后**的最近 N 条样本与趋势(`recorded` 说明本次有没有真的追加)。四路采集,旁路失败只降级(evidence 对应字段 `null` + `*-unavailable` 判据);非法 `since` 在引用白名单段就 400 |
|
|
804
589
|
| `/logs/stream` | GET | query:`target?`、`id`(单容器,与 `ids` 二选一)或 `ids`(逗号分隔的多个容器,**一条连接装下全部**,最多 32 个)、`tail?`(**0**~5000;`0` = 不补历史只跟随,客户端重连用)、`timestamps?`(`1`/`true`)、`since?` | `200 text/event-stream` 长连接,事件协议见下;参数错误 / 未知目标 / 非 loopback 返回常规 JSON 错误 |
|
|
805
|
-
| `/stats/stream` | GET | query:`target?`、`ids?`(逗号分隔;省略 = 全部运行中) | `200 text/event-stream`:每秒一帧 `stats
|
|
806
|
-
| `/events/stream` | GET | query:`target?` | `200 text/event-stream`:一帧 `event`
|
|
590
|
+
| `/stats/stream` | GET | query:`target?`、`ids?`(逗号分隔;省略 = 全部运行中) | `200 text/event-stream`:每秒一帧 `stats`(形状与 /stats 快照一致);不会自然结束,靠客户端断连收尾 |
|
|
591
|
+
| `/events/stream` | GET | query:`target?` | `200 text/event-stream`:一帧 `event` 一个容器事件(服务端已过白名单);不会自然结束 |
|
|
807
592
|
| `/images` | POST | `{target?}` | `{ok:true, images: ImageSummary[]}` |
|
|
808
593
|
| `/images/inspect` | POST | `{target?, ref}`(必填) | `{ok:true, image:{ref, detail: ImageDetail, history: ImageHistoryEntry[], historyError}}`;history 优先 `--format '{{json .}}'`,老版本退回纯文本表格 |
|
|
809
594
|
| `/images/remove` | POST | `{target?, ref}`(必填) | 需 `allowMutations`(否则 403);`docker image rm`(不带 `-f`);`{ok:true, result:{ref, message}}` |
|
|
@@ -817,44 +602,37 @@ challenge 的 TTL 与探测周期各自独立,所以文案只说「执行后
|
|
|
817
602
|
| `/volumes/inspect` | POST | `{target?, name}`(必填) | `{ok:true, volume:{name, detail: VolumeDetail}}`(挂载点 / 选项 / 标签) |
|
|
818
603
|
| `/volumes/remove` | POST | `{target?, name}`(必填) | 需 `allowMutations`(否则 403);`docker volume rm`(**数据随卷一起没**);`{ok:true, result:{name, message}}` |
|
|
819
604
|
| `/volumes/prune` | POST | `{target?}` | 需 `allowMutations`(否则 403);`docker volume prune -f`(**删数据**,见已知限制);`{ok:true, result:{message}}` |
|
|
820
|
-
| `/system-df` | POST | `{target?}` | `{ok:true, disk:{summary, images, volumes, buildCache, truncated, degraded, degradedReason}}`:`summary`
|
|
821
|
-
| `/run-command` | POST | `{target?, id}`(必填) | `{ok:true, recreate:{id, name, command, warnings, secretKeys, omitted}}
|
|
605
|
+
| `/system-df` | POST | `{target?}` | `{ok:true, disk:{summary, images, volumes, buildCache, truncated, degraded, degradedReason}}`:`summary` 四类汇总,明细各上限 100 行(**先排序再截断**);老 docker 只降级不报错 |
|
|
606
|
+
| `/run-command` | POST | `{target?, id}`(必填) | `{ok:true, recreate:{id, name, command, warnings, secretKeys, omitted}}`——**只推导,不执行** |
|
|
822
607
|
| `/action` | POST | `{target?, action, id}` | 需 `allowMutations`(否则 403);`{ok:true, result:{id, action, message}}` |
|
|
823
608
|
| `/exec` | POST | `{target?, id, command, timeoutSec?}` | 需 `allowExec`(否则 403);`{ok:true, result:{id, command, code, stdout, stderr, truncated, durationMs}}` |
|
|
824
609
|
|
|
825
|
-
其余子路径 404(`unknown route: ...`);`/config`、`/targets
|
|
826
|
-
|
|
827
|
-
500 或 400 加 `{error}` 文本。
|
|
610
|
+
其余子路径 404(`unknown route: ...`);`/config`、`/targets`、上述四条 `*/stream` 之外的 GET 返回 405;
|
|
611
|
+
执行失败(docker 报错、目标解析失败等)返回 500 或 400 加 `{error}` 文本。
|
|
828
612
|
|
|
829
613
|
### SSE 事件协议(`/logs/stream`、`/stats/stream`、`/events/stream`、`/images/pull/stream`)
|
|
830
614
|
|
|
831
|
-
四条长流共用同一份基建(`openSseStream`):统一写头 + `flushHeaders()`、15s
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
每帧一行 `event:` + 一行 JSON `data:`(JSON 单行封装:换行 / 引号被转义,
|
|
837
|
-
多字节字符不会被 SSE 行边界截断),随后空行:
|
|
615
|
+
四条长流共用同一份基建(`openSseStream`):统一写头 + `flushHeaders()`、15s 一帧 `: ping` 心跳、活跃流
|
|
616
|
+
登记(插件禁用 / 配置热更新 / 卸载时统一 `end` + abort)、客户端断开静默中止,以及**管道起来后先发一帧
|
|
617
|
+
`ready`**。每帧一行 `event:` + 一行 JSON `data:`(单行封装:换行 / 引号被转义,多字节字符不会被 SSE 行
|
|
618
|
+
边界截断),随后空行:
|
|
838
619
|
|
|
839
620
|
| 事件 | data | 说明 |
|
|
840
621
|
| --- | --- | --- |
|
|
841
|
-
| `ready` | `{}` | **执行器真的起来了**才发一帧(本机子进程 `spawn` / 远端通道已开 = `StreamHandlers.onStart`):客户端据此才认为「在收数据」。**响应头到达(`onopen`)不算**——响应头由 `flushHeaders()` 立刻写出去,而建连(SSH `acquire` /
|
|
622
|
+
| `ready` | `{}` | **执行器真的起来了**才发一帧(本机子进程 `spawn` / 远端通道已开 = `StreamHandlers.onStart`):客户端据此才认为「在收数据」。**响应头到达(`onopen`)不算**——响应头由 `flushHeaders()` 立刻写出去,而建连(SSH `acquire` / 远端开通道)可能卡十几秒。起不来就**没有** `ready` |
|
|
842
623
|
| `line` | `{"d":"..."}` / `{"e":"..."}`;多源时 `{"id":"<容器 id>","d"\|"e":"..."}` | stdout / stderr 分片(不保证按行切,客户端自行拼行)。日志流与拉取流用。**`ids=` 多源时每帧带 `id`**,客户端按它把分片派回对应容器 |
|
|
843
624
|
| `source-end` | `{"id":"<容器 id>","code":N\|null}` | **仅多源**:该容器的 `docker logs -f` 自然退出(容器停止)。整条连接**不关**——其余容器照常推;`end` 要等所有源都结束 |
|
|
844
|
-
| `source-error` | `{"id":"<容器 id>","message":"..."}` |
|
|
845
|
-
| `stats` | `ContainerStats` | 统计流专用:每秒每个容器一帧,字段与 `/stats` 快照完全一致。服务端按**扁平 `{...}` 抽取**(`docker stats` 即便 stdout 是管道也走 TTY 渲染器,帧里混着 `ESC[H/ESC[K/ESC[J
|
|
625
|
+
| `source-error` | `{"id":"<容器 id>","message":"..."}` | **仅多源**:该容器这一条子流失败。整条连接也不关,其余容器继续;客户端标注「部分出错」 |
|
|
626
|
+
| `stats` | `ContainerStats` | 统计流专用:每秒每个容器一帧,字段与 `/stats` 快照完全一致。服务端按**扁平 `{...}` 抽取**(`docker stats` 即便 stdout 是管道也走 TTY 渲染器,帧里混着 `ESC[H/ESC[K/ESC[J`,按行解析会整行丢掉)并折叠同一采样的重复渲染 |
|
|
846
627
|
| `event` | `{action, name, image, composeProject?, time?, exitCode?}` | 事件流专用:一帧一个容器事件(白名单外的事件行在服务端就丢了;值为 null 的字段省略) |
|
|
847
628
|
| `end` | `{"reason":"container-exit"\|"stats-exit"\|"events-exit"\|"pull-exit","code":N, ...}` | 执行器自然退出。日志流附容器退出码(多源时取**按 `ids` 顺序最后一个非 null** 的退出码,各容器的真实退出码在各自的 `source-end` 里)、统计流 reason=`stats-exit`、事件流 reason=`events-exit`、拉取流附 `ref` |
|
|
848
629
|
| `error` | `{"message":"..."}` | 参数 / 执行失败,随后关闭;连接层断路不会发这个事件 |
|
|
849
630
|
|
|
850
|
-
**多容器单连接(`ids=`,D168
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
`streamProtocol` 因此升到 **3**:客户端据此决定走单连接还是退回「每容器一条」。
|
|
856
|
-
|
|
857
|
-
四条流的差异只有「执行器 + 结束原因」:
|
|
631
|
+
**多容器单连接(`ids=`,D168)**:聚合日志把 N 个容器塞进**一条** SSE——宿主为每个容器各起一条
|
|
632
|
+
`docker logs -f`,帧里带 `id` 分流。这样聚合视图无论选几个容器都只占浏览器**一条**同源连接(旧实现是
|
|
633
|
+
每容器一条 `EventSource`,叠加活动条的事件流会吃满 D165 实测出来的 4 条常驻流)。单源 `id=` 的形状
|
|
634
|
+
**逐字节未变**:帧不带 `id`、不发 `source-end`/`source-error`。`/config` 的 `streamProtocol` 因此升到
|
|
635
|
+
**3**:客户端据此决定走单连接还是退回「每容器一条」。四条流的差异只有「执行器 + 结束原因」:
|
|
858
636
|
|
|
859
637
|
| 流 | 执行器 | 自然结束条件 | 关闭语义 |
|
|
860
638
|
| --- | --- | --- | --- |
|
|
@@ -863,85 +641,63 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
863
641
|
| `/events/stream` | `docker events`(`--filter type=container`) | daemon 侧流结束(`events-exit`) | 切页 / 切目标 / 关面板 |
|
|
864
642
|
| `/images/pull/stream` | `docker pull` | 拉取完成 / 失败(`pull-exit`) | 前端离开拉取视图 |
|
|
865
643
|
|
|
866
|
-
- 响应头:`content-type: text/event-stream; charset=utf-8`、`cache-control:
|
|
867
|
-
|
|
868
|
-
对 `text/event-stream` 显式跳过,不会缓冲)。
|
|
644
|
+
- 响应头:`content-type: text/event-stream; charset=utf-8`、`cache-control: no-cache`、
|
|
645
|
+
`connection: keep-alive`,写头后立即 `flushHeaders()`(宿主 gzip 对 `text/event-stream` 显式跳过)。
|
|
869
646
|
- 心跳:每 15s 一帧 `: ping` 注释(SSE 规范里客户端忽略)。
|
|
870
|
-
- **背压与溢出(D153)**:客户端消费不过来时帧进内存队列(每条流 ≤ 8MB);日志 /
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
流结束后恢复回收。
|
|
885
|
-
- **「已就绪」的判据(客户端侧)**:绿点 /「实时跟随中」只认宿主那帧 `ready`——响应头到了
|
|
886
|
-
不算。此外每条流还有一条「更硬的证据」兜底(收到它就等于管道在产数据):事件流的第一条
|
|
887
|
-
真实事件、日志流的第一行、统计流的第一个采样、拉取流的第一帧进度。
|
|
888
|
-
**开发档错位**(宿主进程还跑着旧代码、浏览器却从磁盘读到新的 `client.js`)另有版本协商:
|
|
889
|
-
`/config` 的 `streamProtocol < 2`(或老到没有这个字段)说明该宿主从不发 `ready`,客户端
|
|
890
|
-
退回旧判据(响应头到了就算连上),并只重开一次那条流让回退判据生效——2026-10-11 实测:
|
|
891
|
-
旧宿主 + 新客户端原本会被钉死在「正在连接事件流…」(卡片能读到容器,活动条却在转)。
|
|
647
|
+
- **背压与溢出(D153)**:客户端消费不过来时帧进内存队列(每条流 ≤ 8MB);日志 / 拉取流溢出时丢最旧的
|
|
648
|
+
整帧、只留最新,并推一条 `skip{frames,bytes}`(`skip` 帧自身永不丢)——**流不断**;skip 排队期间再发生
|
|
649
|
+
丢弃时**原地更新**这条 skip 的账目(D156)。统计 / 事件流溢出时补 `end{reason:output-limit}` 收尾,由
|
|
650
|
+
客户端重连。另外 `write()` 返回 false 只代表「越过高水位」,帧**不会被重写**。
|
|
651
|
+
- **分片合帧(D151)**:日志流与拉取流把上游分片按 50ms 窗口合并后再推(同一通道严格保序、stdout 先于
|
|
652
|
+
stderr、攒满 256KB 立刻推),所以 `line` 帧的 `d` / `e` 里可能有**多行**——客户端本来就按「任意分片」
|
|
653
|
+
切行。`end` 帧一定排在窗口里最后一批 `line` 之后。
|
|
654
|
+
- 清理:客户端断开 → 立即中止执行器(本机 `SIGTERM`,2s 未退再 `SIGKILL`;SSH 关闭该 exec channel、
|
|
655
|
+
连接池连接保留复用),不写任何帧;插件禁用 / 配置热更新 / 卸载 → 服务端主动收尾(abort + `end`)。
|
|
656
|
+
- SSH 的长流会占用连接池里的连接(busy 计数),空闲回收(120s)不会误杀;流结束后恢复回收。
|
|
657
|
+
- **「已就绪」的判据(客户端侧)**:绿点 /「实时跟随中」只认宿主那帧 `ready`——响应头到了不算。此外每条
|
|
658
|
+
流还有一条「更硬的证据」兜底:事件流的第一条真实事件、日志流的第一行、统计流的第一个采样、拉取流的
|
|
659
|
+
第一帧进度。**开发档错位**(宿主进程还跑着旧代码、浏览器却读到新的 `client.js`)另有版本协商:
|
|
660
|
+
`/config` 的 `streamProtocol < 2` 说明该宿主从不发 `ready`,客户端退回旧判据并只重开一次那条流。
|
|
892
661
|
|
|
893
662
|
## 安全模型
|
|
894
663
|
|
|
895
|
-
**docker socket ≈ 目标主机的 root 权限。** 能访问 daemon
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
1. **默认只读**。`allowMutations` 未开启时,`/action`、`/images/remove`、
|
|
899
|
-
`/images/
|
|
900
|
-
|
|
901
|
-
`
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
以及按 host:port 只存一条(同主机多密钥类型可能误报变更,删除记录重连
|
|
920
|
-
即可重新校准)。
|
|
921
|
-
5. **命令一律 argv 构造,绝不做字符串拼接**。容器名 / ID 先过白名单
|
|
922
|
-
`assertRef`(`[A-Za-z0-9][A-Za-z0-9_.-]*`,≤128 字符,拒绝空格、`;`、
|
|
923
|
-
`$()`、反引号等),镜像与容器引用同理;远程经 `shJoin` 逐参数单引号
|
|
924
|
-
转义后交给远端 shell,本机 `spawn(bin, args)` 不经 shell。
|
|
925
|
-
6. **输出有上限**。`maxOutputKb` 限制单次命令的 stdout/stderr 字节数,超出
|
|
926
|
-
截断并标记,避免大日志撑爆内存或 agent 上下文。
|
|
664
|
+
**docker socket ≈ 目标主机的 root 权限。** 能访问 daemon 就能挂载宿主目录、以特权模式起容器、读容器里的
|
|
665
|
+
密钥——因此本插件按「只读优先」设计:
|
|
666
|
+
|
|
667
|
+
1. **默认只读**。`allowMutations` 未开启时,`/action`、`/images/remove`、`/images/prune`、
|
|
668
|
+
`/images/pull/stream` 一律返回 403,面板的启停删 / 镜像删除 / 清理 / 拉取不可用,`docker_action`、
|
|
669
|
+
`docker_image_remove`、`docker_image_prune`、`docker_image_pull` 工具**根本不注册**;`allowExec` 未开启
|
|
670
|
+
时,`/exec` 返回 403,`docker_exec` 工具同样不注册。两个开关互相独立,必须在设置卡片由用户显式打开。
|
|
671
|
+
读取类路由(`/logs/stream`、`/stats/stream`、`/events/stream`、`/images/inspect`)不受这两个开关影响。
|
|
672
|
+
**`allowExec` 只管本插件的「一次性」exec 通道**(`/exec` 路由、`docker_exec` 工具、容器详情抽屉
|
|
673
|
+
「概览」页签的一次性命令框)——卡片上的**「终端」按钮跑的是 `docker exec -it` 交互式 shell,由 tty
|
|
674
|
+
承载、不经过这两个开关**,只读模式下也可用(它不是漏了门,是刻意留的进入通道,见「已知限制」)。
|
|
675
|
+
2. **破坏性操作要复述后果**。`remove` 映射为 `docker rm`(**不带 `-f`**),agent 公告要求执行前向用户
|
|
676
|
+
确认目标容器;运行中容器会报错并附「容器仍在运行:先停止再删除」的提示,不会静默强删。
|
|
677
|
+
3. **凭证不落明文(建议)**。`password` / `passphrase` 支持 `env:NAME` 凭据引用,值存在**官方凭据存储**
|
|
678
|
+
(`$DSH_HOME/.credentials.yaml`)里,避免明文写进 `settings.yaml`;`agent` 认证(`SSH_AUTH_SOCK`)
|
|
679
|
+
完全不落盘。配置快照只回「是否已设置」。
|
|
680
|
+
4. **主机指纹 TOFU 钉扎**。首次连接记录 sha256 指纹,之后必须一致,变更即拒绝连接(防中间人);tty 已
|
|
681
|
+
确认过的主机会被当作种子直接信任并复制进本插件的记录。TOFU 的固有边界是「首次若已遭遇 MITM,记下的
|
|
682
|
+
就是伪指纹」,以及按 host:port 只存一条(同主机多密钥类型可能误报变更,删除记录重连即可重新校准)。
|
|
683
|
+
5. **命令一律 argv 构造,绝不做字符串拼接**。容器名 / ID 先过白名单 `assertRef`
|
|
684
|
+
(`[A-Za-z0-9][A-Za-z0-9_.-]*`,≤128 字符,拒绝空格、`;`、`$()`、反引号等),镜像与容器引用同理;
|
|
685
|
+
远程经 `shJoin` 逐参数单引号转义后交给远端 shell,本机 `spawn(bin, args)` 不经 shell。
|
|
686
|
+
6. **输出有上限**。`maxOutputKb` 限制单次命令的 stdout/stderr 字节数,超出截断并标记,避免大日志撑爆
|
|
687
|
+
内存或 agent 上下文。
|
|
927
688
|
7. **HTTP 只对本机开放**。全部路由走 loopback 围栏,远程浏览器无法调用。
|
|
928
|
-
8. **日志实时流是只读能力**。`GET /logs/stream` 与 `/logs` 一致:不受
|
|
929
|
-
`
|
|
930
|
-
`
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
结构化审计行(出口是宿主 stdout),面板路由与 agent 工具两条入口都覆盖。行格式与
|
|
936
|
-
不记什么(403 / tier-gate ask)、以及「exec 命令会随行出现」这条代价,见「已知限制」
|
|
937
|
-
的使用审计条。
|
|
689
|
+
8. **日志实时流是只读能力**。`GET /logs/stream` 与 `/logs` 一致:不受 `allowMutations` / `allowExec` 门控
|
|
690
|
+
(它不改容器状态),但同样只放行 loopback、`id` 过 `assertRef` 白名单、参数经同样的夹紧。与快照不同,
|
|
691
|
+
长流没有 `maxOutputKb` 上限(跟随被截断就失去意义),内存防护由客户端的 5000 行环形缓冲承担。
|
|
692
|
+
9. **变更操作有逐条使用审计**。kit elevation 的审计行回答「授权了」,这里回答「用了」:每一次**过了
|
|
693
|
+
能力闸**的变更 / exec 都落一行 `[dsh-docker] capability-use: …` 结构化审计行(出口是宿主 stdout),
|
|
694
|
+
面板路由与 agent 工具两条入口都覆盖。行格式、不记什么(403 / tier-gate ask)与「exec 命令会随行
|
|
695
|
+
出现」这条代价,见「已知限制」的使用审计条。
|
|
938
696
|
|
|
939
697
|
## 已知限制
|
|
940
698
|
|
|
941
|
-
- **没有交互式 TTY**:`exec` 是一次性命令(`docker exec <id> sh -c <cmd
|
|
942
|
-
|
|
943
|
-
对话。交互排障请到 tty 面板执行 `docker exec -it <容器> sh`(本机与 SSH
|
|
944
|
-
目标都可以)。
|
|
699
|
+
- **没有交互式 TTY**:`exec` 是一次性命令(`docker exec <id> sh -c <cmd>`,不带 `-i` / `-t`),不能跑
|
|
700
|
+
vim / top / 交互式 shell,也不能喂 stdin 做对话。交互排障请到 tty 面板执行 `docker exec -it <容器> sh`。
|
|
945
701
|
- **`allowExec` 不是「进不了容器」的闸**:卡片「终端」按钮(`docker exec -it
|
|
946
702
|
'<容器>' sh`)经 tty 的 `ttyTerminal` 承载,**不检查 `allowExec`**,只读模式下
|
|
947
703
|
也照常可用——exec 被关时面板给的提示原文就是这么说的(「打开『允许 exec』,或
|
|
@@ -952,232 +708,109 @@ abort)、客户端断开静默中止,以及**管道起来后先发一帧 `re
|
|
|
952
708
|
`allowExec` 对**页内脚本**(第三方插件的 client 半体 / XSS)本来就不是边界——它们
|
|
953
709
|
走 `ttyTerminal.open` 或 WS `spawn` 帧就能跑任意命令,比本插件的一次性 exec 还强。
|
|
954
710
|
这一层的威胁模型边界见 [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么)。
|
|
955
|
-
- **事件流有断线窗口**:`docker events`
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
`
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
`
|
|
992
|
-
|
|
993
|
-
- **
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
回话。原先只等远端的 `close` 事件:远端不确认(上面那种被拒的 KILL,或通道静默)时,
|
|
1013
|
-
那条流的 `busy` 计数**永远不会减**,累积到 8 之后该目标的每条长流都被配额拒绝,
|
|
1014
|
-
且没有任何自愈路径(现场与完整因果见 [DEFECTS.md](./DEFECTS.md) 的 D164)。同一类坑
|
|
1015
|
-
`run()` 在 `D112` / `D161` 已经踩过两次,现在三条收尾路径统一成「自己 settle」。
|
|
1016
|
-
**根治办法在 sshd 一侧**:把那台主机的
|
|
1017
|
-
`MaxSessions` 调大 —— 例如 `/etc/ssh/sshd_config.d/10-dsh-maxsessions.conf` 写一行
|
|
1018
|
-
`MaxSessions 50`,再 `sshd -t && systemctl reload ssh`(reload 只影响新连接)。
|
|
1019
|
-
- **docker CLI 版本差异**:解析走 `--format '{{json .}}'`,字段随版本增减,
|
|
1020
|
-
解析器一律降级而不抛异常(例如 `State` 缺失就从 `Status` 推导状态,健康态
|
|
1021
|
-
从 `(healthy)` / `(unhealthy)` 提取);缺字段时对应列可能为空,需要权威
|
|
1022
|
-
数据请用详情(`docker inspect`)。
|
|
1023
|
-
- **`rm` 不带 `-f`**:容器 `remove` 映射 `docker rm`、镜像 `remove` 映射
|
|
1024
|
-
`docker image rm`,都不加 `-f`——运行中的容器、被容器或子镜像引用的镜像会
|
|
1025
|
-
失败并给出提示;要强制删除得去 tty 面板手动执行。
|
|
1026
|
-
- **SSH 目标需要免 sudo 的 docker**:账号不在 docker 组时 docker 报权限错误,
|
|
1027
|
-
面板与工具原样透出,不做自动 sudo 提权。
|
|
1028
|
-
- **远端未安装 docker**:`probe` 失败(`command not found` / 退出码 127),
|
|
1029
|
-
面板显示错误;PATH 不一致时可把 `dockerBin` 填成绝对路径。
|
|
1030
|
-
- **`dockerBin` 的校验发生在落盘之前**:非法值直接 400 并带上原因,不会被写进
|
|
1031
|
-
`settings.yaml`(早先的实现在 `scope.update` 之后才校验,于是非法值照样写盘、
|
|
1032
|
-
用户只拿到一个没有正文的 400;下次启动整段 docker 配置会静默退回默认)。
|
|
1033
|
-
- **`dockerBin` 指向 `.cmd` / `.bat` 时本机通道起不来**:本机执行走
|
|
1034
|
-
`runLocal` / `runLocalStream` 的裸 `spawn`,Windows 上 Node 会拒绝对 `.cmd` 的
|
|
1035
|
-
直接执行(`EINVAL`)。docker 官方发行的是 `docker.exe`,日常不受影响;手写
|
|
1036
|
-
`.cmd` 包装脚本时请改用 `.exe`,或等待本机通道也切到 kit 的 `spawnPortable`。
|
|
1037
|
-
- **Compose 项目视图只认 `com.docker.compose.project`**:`docker stack deploy` 起的
|
|
1038
|
-
swarm 服务带的是 `com.docker.stack.namespace` / `com.docker.swarm.service.name`,
|
|
1039
|
-
于是它们的容器在面板里显示为「未分组」(真机实测:Ubuntu 24.04 + Docker 29.3.1 的
|
|
1040
|
-
三节点 swarm,stack 容器 `composeProject` 全为 `null`)。要支持得另立一套 stack
|
|
1041
|
-
分组语义,不能直接塞进 compose 口径。
|
|
1042
|
-
- **`volume prune` 在 Docker 29 只回收匿名未用卷**:命名卷即使在
|
|
1043
|
-
`docker volume ls -f dangling=true` 里,`docker volume prune -f` 也不会删它
|
|
1044
|
-
(真机实测:命名卷留着,回 `Total reclaimed space: 0B`;匿名卷被删并列出名字)。
|
|
1045
|
-
这与本插件「刻意不加 `--all`、避免误删」的取向一致 —— 要删命名卷请用
|
|
1046
|
-
`/volumes/remove`(面板的卷删除)。
|
|
1047
|
-
- **podman 兼容靠 `dockerBin`**:填 `podman` 即可跑,但 `stats` 与
|
|
1048
|
-
`--format '{{json .}}'` 的字段和输出格式与 docker 有差异,只能依赖解析器
|
|
1049
|
-
的降级路径,未逐项验证。
|
|
1050
|
-
- **没有镜像构建 / Compose 编排变更**:镜像支持拉取 / 删除 / 清理 dangling,
|
|
1051
|
-
但没有 `docker build`、`docker save` / `load`、`docker push`;Compose 是**只读**
|
|
1052
|
-
项目视图(按项目分组 + 项目级聚合日志),不提供 `compose up` / `down` / `restart`。
|
|
1053
|
-
- **诊断是判据,不是专家**:15 条判据覆盖的是「容器自报的客观证据」(退出码 / OOM /
|
|
1054
|
-
重启次数 / 健康检查 / 资源压力 / 日志里的报错行),**不理解业务**。它给出的结论是
|
|
1055
|
-
「证据指向什么」,不会替你判断「这个报错是否可忽略」——那一步交给「交给 Agent」之后的人或模型。
|
|
1056
|
-
代价也说清:诊断一次跑**四条**只读命令(inspect + logs + events + stats),在 SSH 目标上
|
|
1057
|
-
就是最多四条 channel(短命令闸会把并发压到 2,所以是两批),慢机器上会感觉到等。
|
|
1058
|
-
- **事件采集给的是独立预算(8MB + 留尾)**:单容器一小时的事件里,`exec_create` /
|
|
1059
|
-
`exec_start` / `exec_die` 这种解析阶段就被白名单丢掉的三连占绝大多数(真机实测 1h / 256 条 /
|
|
1060
|
-
588KB 里绝大多数是它们),按通用的 512KB 档会让时间线**永远为空**。所以诊断的事件采集单独
|
|
1061
|
-
放宽到 8MB 并保留尾部——窗口内事件特别多的容器,时间线只覆盖**最近的**一段。
|
|
711
|
+
- **事件流有断线窗口**:`docker events` 是「从现在开始」的推流,断线重连期间发生的事件服务端已经推过、不会
|
|
712
|
+
补发。客户端用「重连成功后先做一次全量列表刷新」来补偿(对齐状态,不是把事件补回来);要精确的完整事件
|
|
713
|
+
历史请用 `docker_events` 工具(带 `--since` 的快照)。
|
|
714
|
+
- **浏览器同源连接预算是硬约束,docker 面板最多只占 1 条常驻流(D165 实测 + D168)**:宿主是
|
|
715
|
+
**HTTP/1.1**(没有 h2 多路复用),而**每条 `EventSource` 独占一条连接、且不会主动释放**。吃满之后,同一
|
|
716
|
+
个页面里**所有**普通请求(列表刷新、`/api/*`、会话加载……)一起排队——用户看到的是**整个 DSH 页面
|
|
717
|
+
卡住**,不只是 docker 面板。docker 面板的常驻流**在任何视图下都只有 1 条**:列表 = 活动条事件流;详情 =
|
|
718
|
+
日志 / 统计 FOLLOW(同一时刻只有一个页签);聚合 / Compose 日志 = **一条** `ids=` 多容器流;拉取 = 一条
|
|
719
|
+
进度流(进入详情 / 聚合时事件流主动断开,回列表重连并补一次全量刷新)。超了不是「慢一点」,是同页面所有
|
|
720
|
+
请求一起失效;完整实测数据见 [DEFECTS.md](./DEFECTS.md) 的 D165,复核方式是在 devtools 里连开
|
|
721
|
+
`new EventSource('/api/dsh-docker/events/stream?target=<目标>')` 到第 5 条,再打一次普通请求。
|
|
722
|
+
(WebSocket **不**占这份预算;流式日志 / 统计在浏览器侧的保留量见「使用」。)
|
|
723
|
+
- **多标签页与聚合上限**:那些连接是整个页面共享的(官方 `dsh-client-hmr`、其它插件的 SSE 都算);治本要
|
|
724
|
+
宿主换 WebSocket / h2,或各插件共用一条多路复用连接。聚合上限 3 现在是「可读性 + SSH 8 条通道额度」的
|
|
725
|
+
上限,不再由浏览器预算推出(D168):一屏混三份以上日志会看不清,且每个被选容器在宿主侧仍各跑一条
|
|
726
|
+
`docker logs -f`(SSH 目标上就是一条 channel)。`PICK_MAX = 3` 即此值。
|
|
727
|
+
- **SSH 目标上的通道额度是共享的(`MaxSessions`)**:一个目标只维持**一条** TCP 连接,长流与短命令共用
|
|
728
|
+
这条连接上的通道,而 OpenSSH 的 `MaxSessions` 默认只有 10。长流会一直占到用户关掉面板为止,聚合日志里
|
|
729
|
+
每个被选容器仍各占**一条** channel——叠上刷新 / inspect 这类短命令就可能逼近额度,远端拒绝时看到的是
|
|
730
|
+
`(SSH) Channel open failure: open failed`。插件对每个 SSH 目标限制**同时最多 8 条长流**(源码常量 `MAX_STREAMS_PER_TARGET`,即 10 − 2,留两
|
|
731
|
+
条给短命令),聚合上限也定在 3;折叠右侧栏标签会主动断流、把通道还回去。(`D150`)两道补救:① **短命令
|
|
732
|
+
按目标排队**(每条连接同时最多 2 条并发,`DSH_DOCKER_SHORT_CHANNELS` 可覆盖,仅测试 / 排障);② **额度
|
|
733
|
+
满会自愈**——远端回 `no more sessions` 这类拒绝时,插件丢掉并**关闭**那条连接、重建一条再重试一次,宿主
|
|
734
|
+
日志留一行 `通道额度已满:已重建连接重试`(这条连接**不会自己恢复**)。(`D164`)③ **中止长流时插件自己
|
|
735
|
+
的账当场结清**—— `AbortSignal` 触发时除了向远端 `KILL` + 关通道,还会**直接结束这条流**(原先只等远端
|
|
736
|
+
`close`,远端不确认时那条流的 `busy` 计数**永远不会减**,累积到 8 之后该目标的每条长流都被拒绝且没有
|
|
737
|
+
自愈路径)。**根治办法在 sshd 一侧**:把那台主机的 `MaxSessions` 调大——例如
|
|
738
|
+
`/etc/ssh/sshd_config.d/10-dsh-maxsessions.conf` 写一行 `MaxSessions 50`,再 `sshd -t && systemctl
|
|
739
|
+
reload ssh`(reload 只影响新连接)。当前上限是编译期常量,需要跟着改就提 issue。
|
|
740
|
+
- **docker CLI 版本差异**:解析走 `--format '{{json .}}'`,字段随版本增减,解析器一律降级而不抛异常
|
|
741
|
+
(例如 `State` 缺失就从 `Status` 推导状态);缺字段时对应列可能为空,权威数据请用 `docker inspect`。
|
|
742
|
+
- **`rm` 不带 `-f`**:容器 `remove` 映射 `docker rm`、镜像 `remove` 映射 `docker image rm`,都不加 `-f`
|
|
743
|
+
——运行中的容器、被引用的镜像会失败并给出提示;要强制删除得去 tty 面板手动执行。
|
|
744
|
+
- **SSH 目标需要免 sudo 的 docker**:账号不在 docker 组时权限错误原样透出,不做自动 sudo 提权。
|
|
745
|
+
**远端未安装 docker** 时 `probe` 失败(退出码 127),PATH 不一致时可把 `dockerBin` 填成绝对路径。
|
|
746
|
+
- **`dockerBin` 的校验发生在落盘之前**:非法值直接 400 并带上原因,不会被写进 `settings.yaml`。
|
|
747
|
+
**指向 `.cmd` / `.bat` 时本机通道起不来**:本机执行走裸 `spawn`,Windows 上 Node 会拒绝直接执行 `.cmd`
|
|
748
|
+
(`EINVAL`)——手写包装脚本请改用 `.exe`,或等待本机通道也切到 kit 的 `spawnPortable`。
|
|
749
|
+
- **Compose 项目视图只认 `com.docker.compose.project`**:`docker stack deploy` 起的 swarm 服务带的是
|
|
750
|
+
`com.docker.stack.namespace` / `com.docker.swarm.service.name`,于是这些容器显示为「未分组」。要支持得
|
|
751
|
+
另立一套 stack 分组语义。
|
|
752
|
+
- **`volume prune` 的回收范围随版本变,且可能删数据**:Docker 29 上 `docker volume prune -f` **只回收匿名
|
|
753
|
+
未用卷**(命名卷留着);而 docker ≥ 23 有 `-a/--all`,docker < 23 没有这个开关、plain prune 会把未被
|
|
754
|
+
使用的**命名卷**一起删掉。本插件就不带 `--all`,确认文案写明了版本差异;要删命名卷请用
|
|
755
|
+
`/volumes/remove`。
|
|
756
|
+
- **podman 兼容靠 `dockerBin`**:填 `podman` 即可跑,但 `stats` 与 `--format '{{json .}}'` 的字段和输出
|
|
757
|
+
格式与 docker 有差异,只能依赖解析器的降级路径,未逐项验证。
|
|
758
|
+
- **没有镜像构建 / Compose 编排变更**:镜像支持拉取 / 删除 / 清理 dangling,但没有 `docker build`、
|
|
759
|
+
`docker save` / `load`、`docker push`;Compose 是**只读**项目视图。**网络 / 卷的变更只有面板按钮,
|
|
760
|
+
没有 agent 工具**——刻意不扩大 agent 侧的变更面。
|
|
761
|
+
- **诊断是判据,不是专家**:这些判据覆盖的是「容器自报的客观证据」(退出码 / OOM / 重启次数 / 健康检查 /
|
|
762
|
+
资源压力 / 日志里的报错行),**不理解业务**。「这个报错是否可忽略」那一步交给「交给 Agent」之后的人或
|
|
763
|
+
模型。代价:诊断一次跑**四条**只读命令,在 SSH 目标上就是最多四条 channel(短命令闸把并发压到 2,所以是
|
|
764
|
+
两批),慢机器上会感觉到等。
|
|
765
|
+
- **事件采集给的是独立预算(8MB + 留尾)**:单容器一小时的事件里 `exec_create` / `exec_start` /
|
|
766
|
+
`exec_die` 这类三连占绝大多数,按通用的 512KB 档会让时间线**永远为空**。所以诊断的事件采集单独放宽到
|
|
767
|
+
8MB 并保留尾部——窗口内事件特别多的容器,时间线只覆盖**最近的**一段。
|
|
1062
768
|
- **等价 `docker run` 是近似,不是备份**:compose 管理的容器不带网络别名 / labels / depends_on;
|
|
1063
769
|
`--privileged` / `--link` / ENTRYPOINT 覆盖 / 自定义健康检查 / 匿名卷等**故意不还原**,逐条列在
|
|
1064
|
-
`omitted` 里。**它不能替代 `docker commit` /
|
|
1065
|
-
- **磁盘明细的两处截断各自独立**:宿主侧每类 100 行(`truncated`)、工具侧 20
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
- **使用审计走宿主 stdout,exec 命令会随行出现**:每一次**过了能力闸**的变更 / exec
|
|
1091
|
-
(面板的变更 / exec 路由与 agent 的 `docker_action` / `docker_image_remove` /
|
|
1092
|
-
`docker_image_prune` / `docker_image_pull` / `docker_exec`;403 与参数校验 400 不记,
|
|
1093
|
-
「使用 = 过了闸」)落一行 `[dsh-docker] capability-use: capability=allowMutations|allowExec
|
|
1094
|
-
source=http|tool action=container.remove|… target=… ref=… ok=true|false durationMs=…`,
|
|
1095
|
-
key=value 风格与 kit elevation 的授权行一致,`capability` 与授权行直接对得上号;pull
|
|
1096
|
-
进度流拆 start / end 两行(宿主中途挂掉时至少 start 还在)。**出口是 `console.log`**
|
|
1097
|
-
(docker D162:真机实测 `ctx.logger` 的行在 0.2.x 宿主上既不进 stdout 也不落盘,而审计行的
|
|
1098
|
-
意义全在「出问题时翻得到」;tier-gate 的日志出口同此取向)——跑 `dsh web` 的那个终端里
|
|
1099
|
-
就能看到、可 grep。kit elevation 的授权行同走宿主
|
|
1100
|
-
stdout(**kit D14**,两类行同通道,可对照检索)。**代价**:exec 的命令会记进行尾 `detail`(连同退出码,
|
|
1101
|
-
如 `detail=code=0 cmd=ls -la /app`),整段截断 200 字符、控制字符转义成 `\n` 等字面量——
|
|
1102
|
-
行是给人看的,就得接受它出现在终端可见的日志流里,与 elevation「nonce 绝不进日志」是
|
|
1103
|
-
同一类取舍的正面。docker 报错也算一次使用(`ok=false` + `detail=` 截断后的错误文案);
|
|
1104
|
-
tier-gate 的 ask / deny 不记(没执行不算使用,放行与否由宿主 approval 日志覆盖)。
|
|
1105
|
-
没有查询界面、没有独立审计文件——要检索就翻宿主 stdout。
|
|
1106
|
-
|
|
1107
|
-
- **跨目标聚合的边界**:并发上限 4;**单目标预算 90s(兜底)**,而每条命令自己的超时
|
|
1108
|
-
(30s)才是被遵守的那个——到点时**底层的子进程 / SSH 通道会被真的取消**(不是只让这一格
|
|
1109
|
-
返回失败),否则超时那条命令会继续占着远端会话槽(D161)。单个目标失败/超时只影响它自己
|
|
1110
|
-
那一格(组里带 `error`)。目标很多时总览的请求量随目标数线性增长(每目标 2 个请求),
|
|
1111
|
-
自动刷新会放大这个量——目标多时建议关掉自动刷新。
|
|
770
|
+
`omitted` 里。**它不能替代 `docker commit` / 卷备份**。
|
|
771
|
+
- **磁盘明细的两处截断各自独立**:宿主侧每类 100 行(`truncated`)、工具侧 20 行;真机上卷多到几千个时,
|
|
772
|
+
`system df -v` 的输出本身也可能撞 8MB 预算(那时 `degraded: true`,汇总仍然可用)。「合计可回收」由
|
|
773
|
+
字符串解析成字节再相加——**解析不出就整条显示 `—`**。
|
|
774
|
+
- **总览只汇总「容器概况」,且完全只读**:只做计数卡与异常置顶表,**没有任何跨目标操作**,也不聚合日志 /
|
|
775
|
+
统计 / 事件流。另外「异常」只含 `unhealthy` 与 `restarting`:`ContainerSummary` 里没有 `exitCode`,
|
|
776
|
+
无法区分「崩溃退出」与「人工停掉」——死掉的容器请在计数卡的「已停止」里看。
|
|
777
|
+
- **连接栏按钮需要一条匹配的目标才有数据**:按钮在 SSH 标签上一律显示,但若会话主机没有对应的 `kind=ssh`
|
|
778
|
+
目标(连接簿名或 `host:port` 都匹配不上),点开只会看到「尚未配置为 Docker 目标」的提示;本地标签的
|
|
779
|
+
连接栏本身隐藏。目标增删后最多 30 秒内刷新(设置卡片保存会立即刷新)。
|
|
780
|
+
- **`enabled: false` 是热生效**:保存后工具立即注销、公告撤下、除 `/config` 外的数据路由 403(进行中的长流
|
|
781
|
+
也会被立即收尾)。路由对象本身不卸载、靠 403 拦截;`/config` 始终可读写,设置卡片就是重新启用的入口,
|
|
782
|
+
无需重启 `dsh web`。
|
|
783
|
+
- **使用审计走宿主 stdout,exec 命令会随行出现**:每一次**过了能力闸**的变更 / exec(面板路由与 agent 的
|
|
784
|
+
`docker_action` / `docker_image_remove` / `docker_image_prune` / `docker_image_pull` / `docker_exec`;
|
|
785
|
+
403 与参数校验 400 不记,「使用 = 过了闸」)落一行 `[dsh-docker] capability-use:
|
|
786
|
+
capability=allowMutations|allowExec source=http|tool action=… target=… ref=… ok=true|false
|
|
787
|
+
durationMs=…`,key=value 风格与 kit elevation 的授权行一致;pull 进度流拆 start / end 两行。**出口是
|
|
788
|
+
`console.log`**(`D162`:真机实测 `ctx.logger` 的行在 0.2.x 宿主上既不进 stdout 也不落盘)——跑
|
|
789
|
+
`dsh web` 的那个终端里就能看到、可 grep。**代价**:exec 的命令会记进行尾 `detail`(连同退出码),整段
|
|
790
|
+
截断 200 字符、控制字符转义成字面量——行是给人看的,就得接受它出现在终端可见的日志流里。docker 报错也
|
|
791
|
+
算一次使用(`ok=false`);tier-gate 的 ask / deny 不记。没有查询界面、没有独立审计文件。
|
|
792
|
+
- **跨目标聚合的边界**:并发上限 4;**单目标预算 90s(兜底)**,而每条命令自己的超时(30s)才是被遵守的
|
|
793
|
+
那个——到点时**底层的子进程 / SSH 通道会被真的取消**,否则超时那条命令会继续占着远端会话槽(D161)。
|
|
794
|
+
单个目标失败/超时只影响它自己那一格。目标很多时总览的请求量线性增长(每目标 2 个请求),自动刷新会放大
|
|
795
|
+
这个量。
|
|
1112
796
|
|
|
1113
797
|
## 工作原理
|
|
1114
798
|
|
|
1115
799
|
```
|
|
1116
800
|
浏览器半体 (client.js)
|
|
1117
|
-
├─ 侧边栏「容器」入口 →
|
|
1118
|
-
│
|
|
1119
|
-
|
|
1120
|
-
│
|
|
1121
|
-
|
|
1122
|
-
│ 镜像列表(行内详情 · 删除)+ 镜像详情(层 / 构建历史)+ 拉取进度 /
|
|
1123
|
-
│ 网络列表 + 网络详情(子网 / 接入的容器)+ 卷列表 + 卷详情 / 一次性 exec
|
|
1124
|
-
│ └─ 终端抽屉:tty ≥ 0.15 时经 ttyTerminal.mount 就地嵌入 tty 的终端
|
|
1125
|
-
│ (面板不收起;折叠/拖拽只改尺寸,会话不中断;✕ 或关面板才 dispose,
|
|
1126
|
-
│ 有活动会话时关面板先确认 → tty 那边结束会话并拆 DOM)
|
|
1127
|
-
│ ├─ fetch → /api/dsh-docker/*(loopback 围栏)
|
|
1128
|
-
│ ├─ FOLLOW → EventSource /logs/stream(SSE:5000 行 / 4MB 环形缓冲、150ms 合帧、
|
|
1129
|
-
│ │ 行 id 稳定 key、显示不截断 / 自动贴底 /
|
|
1130
|
-
│ │ 回到底部 / 断线自动重连 / 容器退出自动回快照)
|
|
1131
|
-
│ ├─ 统计 FOLLOW → EventSource /stats/stream(SSE:60 点环形缓冲画
|
|
1132
|
-
│ │ CPU / 内存 sparkline;**前端主动断**,docker stats 自己退出才收流)
|
|
1133
|
-
│ ├─ 事件 → EventSource /events/stream(SSE:活动条环形缓冲 50 条 +
|
|
1134
|
-
│ │ 500ms 防抖触发列表重取;重连成功后补一次全量刷新)
|
|
1135
|
-
│ ├─ 拉取 → EventSource /images/pull/stream(SSE:逐层进度按层键 upsert)
|
|
1136
|
-
│ └─ 聚合日志 → **一条** EventSource /logs/stream?ids=…(多容器共用;帧带 id,
|
|
1137
|
-
│ 客户端按 [service] 混流;老宿主退回每容器一条)
|
|
1138
|
-
├─ 可选消费 tty 的 ttyConnbar 服务 → SSH 连接栏「容器」按钮
|
|
1139
|
-
│ (按 book 名 / host:port 匹配已配置目标,命中才出现)
|
|
1140
|
-
├─ 可选消费 tty 的 ttyPanel 服务(0.16.0)→ 终端面板开着时挂右侧 dock
|
|
1141
|
-
└─ 可选消费 tty 的 ttyTerminal 服务 → 卡片「终端」按钮直接开 docker exec 标签
|
|
1142
|
-
(tty 不可用 / 凭证不在浏览器时退回复制命令)
|
|
801
|
+
├─ 侧边栏「容器」入口 → 面板(目标选择 / 容器列表 / 卡片动作 / 五页签详情 / 多目标总览 /
|
|
802
|
+
│ Compose · 镜像 · 网络 · 卷 · 磁盘各页)与终端抽屉(ttyTerminal.mount)
|
|
803
|
+
├─ 四条 SSE(/logs/stream 含 ids=、/stats/stream、/events/stream、/images/pull/stream)
|
|
804
|
+
│ + 普通 fetch → /api/dsh-docker/*(loopback 围栏;写路由另需同源证明)
|
|
805
|
+
└─ 可选消费 tty 的 ttyConnbar / ttyPanel / ttyTerminal(未装或版本不足逐项降级)
|
|
1143
806
|
|
|
1144
807
|
宿主半体 (src/index.ts)
|
|
1145
|
-
├─ settings 命名空间 docker
|
|
1146
|
-
│ DOCKER_SETTINGS_SCHEMA → normalizeConfig(夹紧 / 白名单键)
|
|
808
|
+
├─ settings 命名空间 docker → normalizeConfig(夹紧 / 白名单键)
|
|
1147
809
|
├─ 只读复用 tty settings 的 sshHosts(连接簿)与 hostKeys(指纹种子)
|
|
1148
|
-
├─ resolveTarget
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
├─ RemoteExec(src/ssh-exec.ts)
|
|
1153
|
-
│ ├─ 懒连接池:同 user@host:port 复用一条连接,空闲 120s 回收
|
|
1154
|
-
│ │ (每 30s 扫一次,连接超时 20s,keepalive 10s;busy>0 的长流跳过回收)
|
|
1155
|
-
│ ├─ 非 PTY exec channel:一命令一 channel,收完 stdout/stderr 即关;
|
|
1156
|
-
│ │ 长流(run()/stream())不设总超时与输出上限,靠 AbortSignal 停止
|
|
1157
|
-
│ ├─ shJoin 单引号转义(远端 shell 解析);env:NAME 凭据解析(provider 优先 → 退回 env)
|
|
1158
|
-
│ └─ hostVerifier TOFU 钉扎(首次记录、变更拒绝)
|
|
1159
|
-
├─ runLocal / runLocalStream:spawn(dockerBin, args)(不经 shell,本机目标)
|
|
1160
|
-
│ 停止阶梯:SIGTERM → 2s 未退出 SIGKILL
|
|
1161
|
-
├─ 通用 SSE 长连接 openSseStream(src/index.ts,四条流共用一份基建)
|
|
1162
|
-
│ ├─ loopback 围栏 + assertRef / assertImageRef + tail 夹紧(与快照路由一致)
|
|
1163
|
-
│ ├─ 写头 + flushHeaders / 15s ping 心跳 / 活跃流登记 / 前端断开静默 abort
|
|
1164
|
-
│ │ 管道起来后先发 ready{}(StreamHandlers.onStart:客户端只认它当「在收数据」)
|
|
1165
|
-
│ ├─ /logs/stream:docker logs -f → line{"d"|"e"} + end{container-exit,code};
|
|
1166
|
-
│ │ ids= 模式:一条连接 N 个执行器 → line{id,d|e} + 每容器 source-end/source-error
|
|
1167
|
-
│ ├─ /stats/stream(只读):docker stats 逐行归一 → stats{ContainerStats};
|
|
1168
|
-
│ │ 不会自然结束,前端断连即 abort,docker stats 自退则 end{stats-exit}
|
|
1169
|
-
│ ├─ /events/stream(只读):docker events --filter type=container →
|
|
1170
|
-
│ │ event{action,name,image,...}(白名单过滤);daemon 侧结束则 end{events-exit}
|
|
1171
|
-
│ ├─ /images/pull/stream(allowMutations):docker pull → line{d|e} +
|
|
1172
|
-
│ │ end{pull-exit,code,ref};未开启变更时 403 且不建流
|
|
1173
|
-
│ └─ 插件禁用 / 配置热更新 / 卸载 → 四条流统一 end + abort
|
|
1174
|
-
├─ 镜像路由:/images/inspect(只读)· /images/remove · /images/prune(allowMutations)
|
|
1175
|
-
├─ 网络 / 卷路由:/networks · /volumes 与各自的 inspect(只读)、remove / prune(allowMutations)
|
|
1176
|
-
└─ agent 工具:docker_targets / docker_ps / docker_inspect /
|
|
1177
|
-
docker_logs(快照语义不变)/ docker_logs_grep(服务端检索)/ docker_stats / docker_events / docker_images /
|
|
1178
|
-
docker_image_inspect / docker_networks / docker_volumes(有目标即注册)
|
|
1179
|
-
+ docker_action / docker_image_remove / docker_image_prune / docker_image_pull
|
|
1180
|
-
(allowMutations + 有目标)/ docker_exec(allowExec + 有目标)
|
|
810
|
+
├─ resolveTarget → 每目标一个 DockerApi(src/docker.ts)+ RemoteExec(src/ssh-exec.ts):
|
|
811
|
+
│ argv 构造 + assertRef + 输出上限;懒连接池 / 非 PTY exec channel / shJoin / TOFU
|
|
812
|
+
├─ openSseStream:四条流共用一份基建(ready / 心跳 / 活跃流登记 / 背压 / 统一收尾)
|
|
813
|
+
└─ 路由 + agent 工具(读类恒注册,写 / exec 类按开关注册)——清单见上面两张表
|
|
1181
814
|
```
|
|
1182
815
|
|
|
1183
816
|
## 开发与验收
|
|
@@ -1189,23 +822,15 @@ pnpm --filter @hyzyn/dsh-docker smoke # 三套离线回归,都不需要
|
|
|
1189
822
|
pnpm test # 仓库级 vitest(本包 test/ 下的用例都在里面;项数以运行输出为准)
|
|
1190
823
|
```
|
|
1191
824
|
|
|
1192
|
-
>
|
|
1193
|
-
>
|
|
1194
|
-
>
|
|
1195
|
-
>
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
> 忘了重启的症状很迷惑:**客户端是对的、宿主是旧的**,于是错误文案、重试、配额这类宿主侧
|
|
1201
|
-
> 逻辑全都不生效,看起来像「改了没用」。判断依据是**文案**——宿主侧新增的提示语如果没出现,
|
|
1202
|
-
> 那就是旧进程。
|
|
1203
|
-
|
|
1204
|
-
验证面分三层:**三套离线 smoke**(不需要 docker daemon)、**本包的 vitest 套件**、
|
|
1205
|
-
以及**真 daemon 的手工验收清单**。逐项的覆盖范围、浏览器侧性能门槛与手工步骤见
|
|
1206
|
-
[docs/verification.md](./docs/verification.md)——README 只留「怎么跑」,清单不在这里重复
|
|
1207
|
-
(同一事实两处全文是债,见 [conventions § 文档分层](../../docs/conventions.md#文档分层))。
|
|
825
|
+
> **改了哪一半、怎么才生效**:`client-src/*`(浏览器半体)→ esbuild 出 `client.js`,宿主有 client HMR
|
|
826
|
+
> 轮询,**热更**、刷新页面即见;`src/*.ts`(宿主半体)→ tsc 出 `lib/`,**必须是新进程才生效**(运行中的
|
|
827
|
+
> `dsh web` 在启动时就把 `lib/` 载进内存),改完 `pnpm build` 记得重启。忘了重启的症状是**客户端是新的、
|
|
828
|
+
> 宿主是旧的**:宿主侧新增的提示语没出现,那就是旧进程。
|
|
829
|
+
|
|
830
|
+
验证面分三层:**三套离线 smoke**(不需要 docker daemon)、**本包的 vitest 套件**、以及**真 daemon 的
|
|
831
|
+
手工验收清单**。逐项的覆盖范围、浏览器侧性能门槛与手工步骤见
|
|
832
|
+
[docs/verification.md](./docs/verification.md)——README 只留「怎么跑」,清单不在这里重复。
|
|
1208
833
|
|
|
1209
834
|
## 版本 / 许可证
|
|
1210
835
|
|
|
1211
|
-
`@hyzyn/dsh-docker`
|
|
836
|
+
`@hyzyn/dsh-docker`(版本以 `package.json` 为准) · [Apache License 2.0](../../LICENSE)
|