hyzyn/dsh-plugin-kit

docker

DSH Web GUI 的 Docker 容器面板:本机或 SSH 主机上的容器列表(可选含已停止)、docker inspect 详情、日志、stats 快照、镜像列表与一次性 exec——默认只读(启停删与 exec 均需显式开启权限),可经 tty 插件在面板底部就地打开容器终端。

UI 增强codegraphdeepseek-harnessdocker

@hyzyn/dsh-docker

中文 | English

DSH 侧边栏「容器」面板:本机与 SSH 主机的容器一屏巡检——读日志、看资源、进容器、启停删,默认只读。

特性

  • 常驻会话右侧栏:面板作为会话右侧栏标签(sidebar.right.pane.tab)与对话同屏——发完错误交给 Agent 后日志留在右边,不影响看它干活;折叠即退出视野、不占屏。面板级状态(目标 / 视图 / 过滤词 / 选中容器及它的页签)跨会话保留,而折叠会主动断流、把 SSH 通道还回去。拿不到右侧栏服务的老宿主自动退回原有的 dock / 模态,行为与旧版一致。
  • 日志右键交给 Agent:日志页拖选报错行 → 右键「直接发送到当前会话 / 填入输入框,我先改改」,把「选中的行 + 目标 / 容器 / 时间窗 + 前后各 20 行上下文」交给会话(内容会进模型上下文,菜单里明写了留意凭证)。投递成功有两处回执:视口级 toast(挂 body,盖过面板与终端弹窗),以及——终端面板正开着时——自动折起终端(ttyPanel 契约 v2 的 minimize();会话继续跑,恢复靠侧边栏「终端」入口的徽标),让会话直接露出来。老版本 tty 没有这个能力时退化成 toast 里「会话在面板后面」的指引。需要改稿就先「填入输入框」——编辑面只有会话输入框一个(它多行、能看到完整上下文,Agent 收到的就是它),不再另开一张更弱的卡片编辑器。目标会话的定位跨宿主版本:≤0.1.5 读 sessions.list 快照的 current,0.1.6 起该字段随「视图选中项」一起搬出了会话域,改看 retainedBy.mainView > 0(官方 dsh-client-ui-session 与本地 codegraph 的同一判据)——只认老字段会让菜单判成「当前没有打开的会话」而整组置灰;同一份取值也驱动「切会话把容器标签带过去」。
  • 显式一键连接本机:两个非常驻入口——一个目标都没有时是「还没有配置 Docker 目标」空态里的主按钮,已有别的目标时是目标下拉的末一项 + 连接本机(工具条上不挂:常驻时它既占位又招人点,纯噪音)——一次点击(或选中该项)就把宿主所在机器加成本机目标并选中(已有本机目标则直接复用,不重复创建;SSH / 自定义目标原样保留),agent 侧同一条操作是 docker_connect_local。它不改空 targets 的语义、不凭空造隐式默认目标、不自动装 Docker / 起 daemon / 切 docker context,也不需要任何能力授权(只加一条指向插件自身的本机目标)。
  • 多目标聚合取数:总览页对全部 targets[] 并行请求,单个目标不可达只污染自己那一格;agent 侧同一口径由 docker_ps target:"*" / docker_attention target:"*" 暴露,跨目标不互相阻塞。
  • 「需关注」读权威字段:不健康 / 反复重启 / OOM 被杀 / 非零退出 / 僵死;OOM 与真实退出码由一次 docker inspect 补齐——docker ps 摘要里的 137 分不出 OOM 与手动 kill,只按摘要筛必然误报。
  • 四条 SSE 长流共用一套基建:日志 FOLLOW、docker stats、docker events、docker pull 走同一个 openSseStream(心跳 / 活跃流登记 / 断开清理 / 管道起来后先发一帧 ready),差异只在收尾语义——日志与拉取自然结束,统计与事件由前端主动断。「连上了」与「在收数据」是两件事:客户端只认 ready,响应头到达不算(见「SSE 事件协议」)。多选聚合日志按 --timestamps 前缀还原跨容器真实时序,「暂停」只冻结渲染(流继续接收,恢复时一次性补齐)。
  • 默认只读,能力开关分三级:启停删 / exec / 镜像变更各自独立开关,未开启时 agent 工具不注册、HTTP 路由 403(能力不存在,而非调用后报错);开关的提权走两条带外通道(启动环境变量 / 卡片里就地确认,见下条);容器名与 ID 过白名单,命令一律 argv 构造 + 单引号转义,密码 / 口令以 env:NAME 凭据引用(官方凭据层解析,缺失时退回环境变量)且永不回传浏览器。
  • 与 dsh-tty 数据级复用、代码级不耦合:不 import 任何 tty 代码,tty 也无需改一行源码,两者可各自安装与升级;装了 tty 则消费三个可选扩展点——连接栏动作(ttyConnbar)、终端承载(ttyTerminal:标签 / dock 承载下经 open 新开标签,模态下经 mount 就地嵌入抽屉)、以及只在兜底路径用到的终端右侧 dock(ttyPanel.mountPane);未装或版本不足逐项静默降级。

与 dsh-tty 的关系

本插件自成一体:不 import 任何 tty 代码,tty 也无需改一行源码,两者可各自单独安装、 各自升级;装了 tty 就在下面这些可选扩展点上协作,没装则安静降级。

维度 · 说明 插件形态 · 独立包 @hyzyn/dsh-docker,不 import 任何 tty 代码,tty 也无需改一行源码;两者可各自单独安装、各自升级 连接簿 · SSH 目标可引用 tty 连接簿条目名(只读 ctx.settings.get('tty') 的 sshHosts);tty 未安装时退化为「内联 host/username」或本机目标 主机指纹 · 本插件自持一份 hostKeys(TOFU),并优先以 tty 已记录的指纹作种子——同一主机不必在两处各确认一次 执行通道 · 自持池化 SSH exec(src/ssh-exec.ts),与 tty 的 PTY 会话完全独立,互不占名额 上下文入口 · tty ≥ 0.13.0 时可选消费其客户端服务 ttyConnbar,在 SSH 连接栏(SFTP 旁)插入「容器」按钮(注册即显示),目标在点击时按当前会话解析;本插件自己开的 exec 标签除外——那种标签正是从容器面板点进来的,再给一个回去的入口等于绕回原地(判定走 spawnSpec.command,用户手敲的 docker exec -it 不算);tty 未装 / 版本过旧则静默跳过 面板承载 · 默认走会话右侧栏标签(sidebar.right.pane.tab):面板与对话同屏,折叠即退出视野、不占屏;框架侧边栏的入口只负责打开或聚焦它,页类型在同一栏内去重,反复点不会开出第二个。终端连接栏的入口相反——它长在盖满视口的 tty 弹窗上,开标签会被挡住,所以那条走 ttyPanel.mountPane 停靠到终端右侧(入口决定承载);投递日志给会话后经 ttyPanel.minimize()(契约 v2)折起终端,把舞台让给会话。拿不到右侧栏服务(老版本 DSH)、或把 localStorage['dsh-docker:carrier'] 置成 modal 时,侧边栏入口也回退到 dock(tty 开着时)或自带 backdrop 的全屏模态。三种承载是同一个组件,只是外壳与几何不同 终端承载 · 交互式终端由 tty 承载(它才是 PTY 的所有者)。右侧栏标签:面板全屏(sidebar.fullscreen)时经 ttyTerminal.mount 就地嵌入面板底部(宽度够、日志与 shell 同屏),否则经 ttyTerminal.open 在终端面板新开标签(同一容器重复点会聚焦已有标签,不再堆重复——tty 契约 v3 的 reuse)(栏太窄,硬塞两头难受);dock 承载(面板已经长在 tty 里)一律开标签;模态承载就地嵌入。那两类命令标签(spawnSpec.command 非空)在 tty 侧不显示连接栏的扩展按钮区(SFTP / 隧道 / 第三方面板都作用于连接本身,挂在 docker exec 标签上会误导——SFTP 浏览的是宿主机,不是用户以为的容器内);本插件另有一道版本无关的兜底:自己开的 exec 标签不给「容器」入口(按 spawnSpec.command 前缀判定)。未装 tty 或版本不足时复制命令兜底。本插件不实现 PTY / xterm / 重连栈 分工 · 交互式排障(docker exec -it、容器内 shell、TUI)由 tty 承载(抽屉内嵌或标签);只读巡检与 agent 自动化用本插件自己的 exec 通道

数据级复用、代码级不耦合:连接簿与指纹种子是「读同一份 settings」,连接栏按钮是 「消费一个通用扩展点」,都不是「依赖 tty 的模块」,因此 tty 升级或卸载都不会连带 弄坏本插件。

安装

dsh plugin --profile web add @hyzyn/dsh-docker              # npm 安装(发布后)
dsh plugin --profile web add link:$(pwd)/packages/docker    # 仓库开发调试

聚合包 @hyzyn/dsh-all(或仓库根 bundle)已包含本插件,一次装齐时不必单独 add。装完重启 dsh web,侧边栏出现「容器」入口;设置 → 插件 → 「Docker 容器面板」卡片维护目标与开关,保存即热生效(settings/updated 触发重解析,无需重启)。

已在 web profile 里装过 @hyzyn/dsh-all 或根 bundle 时不要再 add 本包,
否则插件行重复挂载,启动报 duplicate loader entry id。

使用

两个入口,同一个面板。默认承载是会话右侧栏标签——入口只负责打开或聚焦它, 面板与对话同屏:发完错误交给 Agent 后,日志留在右边、不影响看它干活。

  • 侧边栏「容器」(总入口):任何目标都能用,包括本机 docker 与多目标切换。

已打开则聚焦(页类型在同一栏内去重),不会开出第二个「Docker 容器」标签。

  • 投递后去哪儿看:右侧栏标签承载下会话就在旁边;docked / 模态下发送成功后**终端会自动

折起**(会话照旧跑着,点侧边栏「终端」入口的徽标恢复),于是你直接落在会话里——不必对着 一个盖住会话的弹窗猜「点了没有反应」。

  • SSH 连接栏「容器」按钮(上下文快捷方式,tty ≥ 0.13.0):在 tty 终端面板的

SSH 标签里,连接栏 SFTP 按钮旁会出现「容器」——注册即显示,点击直接用 当前会话那台主机打开面板,不用再选目标。目标解析发生在点击时:会话来自 连接簿时按条目名匹配,否则按 host:port 匹配已解析的目标;没配到目标也不会 藏按钮——面板会带一条提示告诉你会话主机(含连接簿名)该去设置卡片怎么配。 这条路径走终端右侧的 dock,不开右侧栏标签——按钮长在 tty 弹窗上,而弹窗盖满 视口,开标签会被整个挡住、看着像「点了没反应」。目标照旧带进面板并重挂到那台主机。 于是规则是「入口决定承载」:框架侧边栏点 → 右侧栏标签(与对话同屏); 终端弹窗里点 → 终端右侧 dock(与终端同屏)。 dock 面板归属点它的那个标签(tty ≥ 0.18.4):切到别的 SSH 标签时整块收起 (React 树与轮询都保活,切回来恢复原样),关掉该标签则一并收掉;别的标签上再点 「容器」会用那台主机的目标重开一块。收起 ≠ 关掉:tty 不重挂,面板状态不丢。

承载形态

承载 · 何时用 · 行为 会话右侧栏标签(默认) · 宿主提供 sidebarRight / sidebarRightTabs · 与会话同屏;折叠=退出视野但不丢状态(面板级状态按模块保留,见下);支持右侧栏的 fullscreen 铺满 终端右侧 dock · 从终端连接栏的「容器」按钮进来(按钮就在 tty 弹窗上,标签会被它挡住),或拿不到右侧栏服务 / localStorage['dsh-docker:carrier'] = 'modal' 且 tty ≥ 0.16 面板开着 · 挂在终端面板右侧(拖宽 / 折叠 / ✕ 由 tty 提供,终端继续可用);同一时刻只挂一个,挂上去会收掉前一个(例如 tty 自己的 SFTP);tty ≥ 0.18.4 起归属点它的那个标签:切走收起、切回恢复、标签关掉一并收掉(收起期间 React 树与轮询保活) 全屏模态(兜底) · 上面两条都不成立 · 自带 backdrop、点击外部关闭;面板 z-index 高于 tty 弹窗

回滚到旧形态只要在控制台执行 localStorage.setItem('dsh-docker:carrier', 'modal') (removeItem 恢复默认)。这个开关是临时灰度用的,所以刻意没进 settings——不值得为 它连带改宿主配置结构、设置卡片与文档。

状态保留:面板看的是主机、不是工作区,所以 view / 过滤词 / 选中容器(含概览- 日志-统计页签)/ 目标都是跨会话保留的;容器详情内部的日志过滤与 LINES 开关跟具体 容器绑定,不保留。折叠标签会断开全部实时流(把 SSH 通道还回去),展开时重连—— 单容器与聚合建流本来就带 tail,历史会自己补回来。

跨会话粘性:DSH 右侧栏的标签记录本身是会话作用域的(sidebar.right.pane.tab 与 rightbar.session 都声明 scope: 'session'),A 会话开的标签在 B 会话里并不存在。可 容器面板看的是主机,「切个会话它就没了」是纯损失,所以插件额外记了一个「用户希望它开着」 的意图:切会话时自动在新会话里把标签重开,只有你点了标签的 ✕ 才停止——也就是 「面板跟人走,而不是跟会话走」。那个 ✕ 是宿主执行的(sidebarRight 自己摘标签), 所以「用户关掉了」这件事必须经 registerCloseHandler 登记才收得到:只靠面板自己的 onClose 会漏掉它,于是刚关掉的标签会在每次切会话时自己回来。

dock 兜底形态(终端右侧栏):

这条路径打开的面板不会盖住终端:tty ≥ 0.16 时它挂在终端面板右侧的 dock 里 (可拖宽、可折叠成窄条、✕ 收起),终端照常敲命令。dock 模式下卡片「终端」按钮改为 在同一终端面板新开标签执行 docker exec -it(面板已经在一个终端里了,再嵌一层 没有意义);同时不再渲染面板自己的头部——标题与 ✕ 由侧栏标题栏承担,刷新与只读 徽标并到工具条末尾并靠右(刷新中图标自己转,不再另挂 spinner):左端留给目标 / 视图 / 搜索 / 筛选这些「过滤类」控件,刷新不会被当成第一个筛选项,位置也与非 dock 模式头部里一致;520px 窄栏里不会白留一条空行。

面板内:

  • 切目标有过渡与保护:选择器一变,下方仍是上一个目标的卡片(远端一次往返最长 20s)。

过渡层的三条原则——不推版、有方向感、少灰:

  • 面板正文顶部一条 2px 不定长流光进度条(绝对定位)给「正在取数」的全局信号;
  • 正文顶部居中浮出一枚蓝色胶囊(与 dsh-rss 的加载胶囊同一套位置与配色语言):

底色 color-mix(accent 12%, surface)、描边 accent 42%、文字与旋转图标走 --dk-accent; 可见文案只两段 ⟳ 正在切换到 目标2 · 当前显示:目标1(不写成一句话、不用引号裹目标名), 完整的「为什么点不动」放 title,需要时 hover 就有;

  • 旧数据保持 82% + 轻度去色(不是压暗到看不清)并锁住指针:读起来是「另一批

内容」而不是「坏了」,同时避免照着旧列表操作——那会拿新目标当目标、用旧列表的容器 ID 发命令,真能停掉对端同名容器;

  • 新数据 8px 上滑 + 淡入 200ms 落地,让「换了一批内容」看得见;动效尊重

prefers-reduced-motion。

以上全部绝对定位:切换过程中首卡位置与滚动高度实测完全不变(横幅方案会把内容整块 推下去)。切换失败则清空旧列表并归到新目标的错误态(不继续展示别的目标的数据), 空态文案也会区分「读取失败」与「筛选过窄」。

  • 目标选择:面板先选目标(来自配置 targets,本机 / SSH);只配置了一个

目标时默认选中它,agent 工具也可以省略 target 参数。 会记住上次选的目标(存浏览器 localStorage 的 dsh-docker:last-target,不落 配置、不进 settings):下次打开面板自动选中它。优先级是连接栏指定 > 上次记住的 (且仍存在)> 列表第一个——记住的目标被删掉/改名后会自动退回第一个,不会停在 「未知目标」上;从终端连接栏的「容器」按钮进来时,当前会话主机优先于记忆值。换目标是整段上下文切换: 上一个目标还在飞的请求一律作废(列表写入闸),不会出现「选择器已经是目标2、卡片 还是目标1 的容器」这种串台,也不会让旧目标的超时横幅停在新目标的页面上。

  • 多目标总览(只读):目标选择器旁的 总览 pill(配了 ≥2 个目标才出现)——不选

目标,一屏看全部主机:上面一行计数卡(目标名 + 本机 / SSH 标识 + 运行中 / 已停止 / 不健康),下面异常容器置顶表(容器名 / 目标 / 状态 / 镜像;不健康排在 重启中之前,同级按目标在配置里的顺序,跨轮询不跳行)。取数是并行 + 渐进落地: 每个目标独立发一次 POST /containers(all:true,裸 docker ps 不返回 exited, 「已停止」会永远是 0),一个目标不可达只影响它自己那一格——卡片描红 + 顶部一条 「N 个目标不可达」横幅,其余目标的结果照常显示(刻意不用 Promise.all 汇总: SSH 不可达要等 readyTimeout 20s,汇总等于让整页静默 20s)。点计数卡回到该目标的 常规容器列表,点异常行进该容器详情(返回落到该目标的容器列表);总览不做任何 跨目标操作,异常表的行上没有动作按钮。进总览时会清掉「当前这个目标」遗留的 失败横幅:总览按目标归因(红卡 + 不可达横幅),不再叠一条单目标的「操作失败」—— 同一个 SSH 超时被讲两遍,看着像两台机器都挂了。轮询复用工具条的「自动刷新」开关(默认关): 这一页每轮是全 N 个目标各一次 docker ps,比任何单目标页都贵。没有异常显示 「一切正常」;还有目标没答完时,异常区显示「读取中…」、那张计数卡也进入读取态 (转圈 + 压暗,不出 0/0/0——「0 个容器」会被读成「这台机器没容器」,而它只是还没答) (「还不知道」不等于「没问题」)。

  • 容器列表:名称 / 状态 / 健康态 / 镜像 / 端口映射 / compose 项目与服务 /

短 ID;支持按名称或镜像搜索、按状态筛选(运行中 / 已停止 / 全部), 以及自动刷新(按 pollIntervalSec 轮询,切到镜像页会暂停且不显示该开关—— 镜像变化慢,没必要每 5s 跑一次 docker images;切回容器页原设置照旧生效)。 工具条里「含已停止」与「自动刷新」是成组的两个开关,搜索框是其中唯一可伸缩的项: 宽面板下整条工具条收成一行,窄栏(dock 520px)里才按组换行——不会出现一行只有 一个复选框的孤行。

  • 「活动」条(docker events 事件流):容器列表头部的可折叠窄栏(默认展开),

跟着一条 SSE(GET /api/dsh-docker/events/stream,服务端跑 docker events --filter type=container)显示最近 8 条事件(时间 + 容器名 + 动作, die 带退出码如 die(137))。它是事件驱动刷新的入口:收到事件后 500ms 防抖触发一次列表重取(不是每帧一次请求),与原有 AUTO REFRESH 叠加而不互斥—— 轮询负责兜底,事件负责「刚发生」。事件只留在内存(环形缓冲 50 条),切页即关流, 换目标清空缓冲。白名单只留九类生命周期动作(start / die / stop / kill / oom / health_status / destroy / rename / update):exec_*、archive-path(docker cp) 这类噪音在服务端就丢掉了——实测一台跑批机器 24 小时 47 条事件全是 exec,白名单 命中 0,所以只被 exec 的机器上活动条是空的,这是刻意的。 断开后要手动重连:这条流进了 closed 就是终止态(EventSource 只在连接层抖动 时自愈;docker events 进程退出、服务端发 end 帧都会走 close),活动条上会出现 一个 重连 按钮(只在 closed 时出现;unsupported 环境不给——重连必然再失败), 工具条的 ⟳ 也一并重连。AUTO REFRESH 轮询刻意不重连:塞进轮询会让一条已终止的流 被不断重开,既掩盖「它断了」,又白建 SSE。重连成功后补一次全量列表刷新(断线期间 列表可能已经落后),daemon 全程没起、流从未连上过的情形同样补——否则活动条绿了而 列表还停在错误态,看着像好了其实没好。 重连被拒时要说原因(D164):服务端主动拒绝这条流时会发一帧 event: error 带 message(最典型的是长流配额满,见「已知限制」——那句话里写着该怎么办); 客户端现在把它显示在活动条上(「重连被拒:…」),而不是丢掉。原先只置 closed,于是「拒绝 → 回到原位」与「点了没反应」在界面上完全不可区分,用户只能反复点。 重连成功(或重新建流)时这条原因清除。 断开时右侧那句提示是折叠头里的单行小字,所以刻意写短(2026-10-08 用户反馈 「啰嗦又生硬」):状态行左边已经在说「事件流已断开」,这句只说「不会自动恢复,点 「重连」」——成因、上限与 MaxSessions 那一串写在本页,不塞进界面。 「在收事件」只认宿主那帧 `ready`(2026-10-11 用户现场):SSE 的 onopen 只等于 「响应头到了」——宿主是先把响应头 flush 出去、再去起 docker events,而目标不可达 / SSH 正在重连时建连可以卡十几秒(D150 的「槽位满 → 重建连接」还会再来一轮)。那段时间 活动条原本已经亮成绿色的「实时接收中(docker events)」,一个事件都还没收到。现在绿点 只认 ready(= 管道真的起来了);此前停在「正在连接事件流…」/「连接中断,正在自动 重连…」——两种都是实话。日志 / 统计 / 拉取三条流同一口径(见「SSE 事件协议」)。

  • 聚合选择(临时多选聚合日志):工具条的 聚合选择 进入选择态——每张卡片

左侧出现勾选框,点卡片本体变成勾选 / 取消(不再进详情;动作条暂时收起, 免得一边多选一边误点启停删),工具条与列表之间出现操作条:「已选 N 个容器」+ 聚合日志 + 取消。聚合日志 至少需要 2 个容器;选到第 3 个时给一条软提示, 超过 3 个按钮置灰并提示上限(上限怎么来的见「已知限制」的浏览器同源连接预算, 操作条上也写着那句说明)。点 聚合日志 进入聚合视图:直接复用 Compose 项目视图那套聚合日志(每容器一条 /logs/stream, 按 [service] / 容器名前缀混流,带过滤与自动滚动),返回即退出选择态并清空。 聚合视图的内容控制与单容器日志完全对齐(文本过滤 + 级别门槛 + ⬇ .log / ⬇ .md 导出当前显示内容 + 行数统计),另有聚合独有的两项:按时间 / 按到达排序、暂停冻结 (暂停时流继续接收、恢复一次性补齐)。 再点 聚合选择 或按 Esc 同样退出并清空。勾选是临时的:不持久化、不命名 组合、不进 settings;切目标 / 切「容器 · 镜像 · Compose」分段 / 关面板即失效, 列表刷新后已消失的容器按 id 自动剔除。

  • 按条件一键选中(多选态):操作条第二行给出一排条件 chip(`全部可见 / 不健康 /

需关注 / 已停止,以及有勾选后的 同镜像 / 同项目`),计数按剩余名额截断—— chip 上写 3 就真的会选中 3 个;超出上限的数量在 title 与结果提示里如实说明。 条件只在当前筛选结果里生效(先搜索/筛状态再一键选中)。

  • 容器卡片:与参考布局一致的「标签 + 值」行(镜像 / ID / 端口 / 创建 /

compose,值等宽、可省略)+ 一排图标操作按钮,按「查看 / 变更」两组用竖线分隔:

组 · 按钮 · 说明 查看 / 进入 · 终端、日志、资源占用 · 不改容器状态,只读模式下也永远可用。终端按钮跑 `docker exec -it` 交互式 shell,不受 `allowExec` 门控(那条开关只管本插件的一次性 exec 通道,见「已知限制」) 变更 · 启停、重启、删除 · 按破坏性递增排列;只在 allowMutations 开启后可用,未开启时整组置灰

删除额外做了两点防护:破坏性配色 + 与「重启」之间留出间距,点击后仍需二次确认。 命令飞行期间整组锁住:stop / rm 要等容器真的退出(最长十几秒),这段时间 确认框保持打开并显示「执行中…」(两个按钮都禁用),卡片的其它变更按钮压暗置灰、 正在跑的那个图标换成转圈,等列表刷新落地后才恢复——避免中途连点叠出 stop + restart + remove 这类互相打断的命令。镜像的删除 / 清理走同一套确认框, 同样有执行中态。点卡片本体进入概览。

  • 容器详情(整栏视图):顶部为「返回 + 容器名 + 状态徽标 + 目标主机」,

下方三个标签页——概览(docker inspect 权威数据 + 一次性 exec)、日志、 统计。从卡片的日志 / 统计图标可直接落到对应标签页。

  • 日志视图(紧凑两行):第一行 = 返回 + 容器名 + 状态 + 目标主机 +

LINES(尾部行数)/ TIMESTAMPS / `FOLLOW`(实时跟随,见下) / AUTO REFRESH(开关 + 2/3/5/10s 间隔,仅日志页轮询)+ 刷新 / 关闭; 第二行 = 标签页 + 常驻的「过滤日志」 输入框(有内容时框内浮出 ✕ 清空,Esc 也能清空)+ 级别门槛 (全部级别 / INFO+ / WARN+ / ERROR+(INFO+ 就是「清静但别丢关键信息」那档:噪音几乎都在 DEBUG 及以下,而只有 WARN+ 会把 INFO 一起滤掉))+ 导出(⬇ .log / ⬇ .md,导出的是当前显示内容)

  • 右侧固定槽的行数统计。两行的分工是刻意的:第一行管传输与显示(快照 / 流 / 轮询),

第二行管内容——而第二行与聚合日志完全同一套:同一个级别内核、同一个导出构建器、 同一份计数文案(N 行,有过滤时 N / M 行)。级别门槛的语义是「无级别前缀的行是上一条的 续行,跟随其级别」,否则 ERROR+ 会把堆栈拦腰截断。输入框宽度与位置恒定,输入 / 清空都不会挤动这一行。日志正文按级别着色([INFO] 与 |INFO 两种常见前缀 都能识别),时间戳压暗,过滤命中高亮;显示与导出取同一集合(LINES 选多少就多少),不再有「只显示最近 N 行」的截断。 详情视图下不再叠加列表工具条与面板头,每屏只有一个刷新入口。

  • FOLLOW 实时日志流:日志页 FOLLOW 开关打开后,界面从「定时拉快照」切换

为 SSE 推送(GET /api/dsh-docker/logs/stream,服务端跑 docker logs --follow)——新日志到达即追加,不再轮询;FOLLOW 与 AUTO REFRESH 互斥(开流自动停轮询、开关置灰),关闭即回到快照并立即刷新。 流式日志保留最近 5000 行 / 4MB(环形缓冲,行数或字节超限都丢最旧并提示 一次;没有换行符的超长输出会被强制分片,内存恒有界);分片到达按 150ms 合帧 渲染(话痨容器也不再逐条重绘),行 key 用单调 id。 正文按窗口化挂载(D152):DOM 里只留「可见 + 上下各 24 行 overscan」的几十行 (实测 5000 行时约 230 个节点,此前是全量 ~10000),其余用上下垫高占位——持续洪泛 20k 行/秒时事件循环最大延迟从 70~97ms 降到 6ms、>50ms 长帧从 25~26 个降到 0。 行高是实测的(不是估算),贴底时窗口锚在列表尾部,所以「自动贴底」「上滚暂停」 「回到底部」的几何判定依旧精确;往上滚历史时按可见区顶行做跨帧记锚的修正—— 洪泛里缓冲持续淘汰旧行、或聚合视图按时间插行,都不会跳。换流 / 换容器 / 快照刷新 会整代作废这批实测高度与锚点(D155):快照行 id 是位置寻址('s'+index),刷新后 同一个 id 就是另一行内容——不换代就会拿另一代的高度算垫高(实测滚动高虚高 35644px), 滚动条、跳转、「滚到顶看到第几行」全部按错的一代算。 显示层不截断:LINES 选多少就是多少——全部行都能滚到、都能导出(真正的闸是 缓冲与宿主的「输出上限(KB)」),只是不再同时挂在 DOM 里。自动滚动到底部,用户向上滚动时暂停并浮出 「回到底部」按钮;右上状态行显示连接状态,容器退出导致流自然结束时自动切回快照刷新。断线重连由插件自己管(不依赖 EventSource 的自动重连):首连带 tail 补历史,重连一律 tail=0——只补新行、不 重放历史(自动重连会复用带 tail 的 URL,服务端就会把最后 tail 行当新行重推一遍, 日志里凭空多出一段重复)。宿主侧背压(D153):单条流的暂存队列上限 8MB;日志与拉取 这类文本尾部流溢出时丢最旧的帧、留最新,并推一条 skip{frames,bytes} 让界面说明 「中间断了一截,跟随继续」——不再掐流(老策略是补一条 end{reason:output-limit} 收尾, 客户端只能重连,积压期间面板就是「连接中断」在打转)。skip 的账目是精确的:skip 帧还在 队列里没落地时又发生丢弃,服务端原地更新它的计数(D156,一段缺口都不吞);客户端把 多批 skip 累加显示成「本次连接共跳过 N 批」,重连成功即清零(D157)。统计 / 事件这类结构化流仍走收尾重连 (少一个采样/事件在语义上是错的,宁可让客户端补一次)。 连接 / 切页 / 关面板都会关闭 EventSource。

  • 概览:docker inspect 的权威数据——状态与健康、退出码、重启次数与策略、

端口映射、挂载(含只读标记)、网络与 IP、entrypoint 与命令、最近一次健康 检查输出;下方可执行一次性 docker exec(需 allowExec)。

  • 日志:docker logs --tail 的尾部快照(LINES 默认取设置卡片的「日志

默认行数」),可切时间戳与 --since。两个闸要分清:LINES 是行数上限, 设置卡片的「输出上限(KB)」是字节上限——两者取小,字节先超就截断并给出横幅 (所以 LINES 选了 5000 也可能只回来一部分)。要持续观察新日志就打开上面的 FOLLOW(同一份 argv 加 --follow,无总超时、不受该字节上限约束,靠连接 生命周期收尾)。

  • 统计:docker stats --no-stream 快照(CPU% / 内存用量与占比 / 网络 IO /

块 IO / PIDs),面板按 pollIntervalSec 轮询刷新。统计页还有 `FOLLOW` 实时 跟随:打开后切到 GET /api/dsh-docker/stats/stream(服务端跑不带 `--no-stream` 的 docker stats,每秒一行),浏览器侧留 60 个点的环形 缓冲画 CPU / 内存迷你趋势图(sparkline)。与日志流不同,这条流不会 自然结束,关闭语义是前端主动断 EventSource;docker stats 自己退出时 服务端发 end(reason=stats-exit),界面提示并自动切回快照轮询。

  • Compose 项目视图:工具条第三段切换。把 composeProject / composeService

标签聚合成「项目 → 服务 → 容器」,每个项目一行看全运行数 / 不健康数 / 服务数与 各容器状态;点进项目可看服务表,或打开项目级聚合日志——对项目内每个容器各开 一条 /logs/stream,客户端按 [service] 前缀混流(宿主侧 logsStream 本 就支持任意容器,无需新接口),带自动滚动开关与过滤框。混流是到达序,不保证跨 容器严格时序。

  • 镜像:docker images 列表(reference / 大小 / 创建时间 / 短 ID);

: 的 dangling 镜像带 dangling 标记。搜索框与「N / M 个镜像」 计数固定在工具条里(不随列表滚走),表头列名在表体内吸顶。 每行有「详情 / 删除」两个动作:详情打开整栏视图(概览:大小 / 含父层 / 创建 / 平台 / 层数 / 入口与命令 / 暴露端口 / digest / 标签;构建历史: docker history 的逐层命令与大小)。删除(需 allowMutations)二次确认 后执行 docker image rm(不带 -f,镜像被引用时会失败并给出「先删相关 容器」的提示)。

  • 网络 / 卷(第五、六段):工具条分段扩到「容器 / 镜像 / Compose / 网络 / 卷」

(窄栏里放不下时分段容器自己横向滚动,不做二级菜单)。两页都是「表格 + 整栏详情」 的同构布局:

  • 网络:名称 / 驱动 / 范围 / internal 徽标 / ID,行点击进详情——概览(ID / 驱动 /

范围 / 创建 / 子网 / 网关 / internal·attachable·ingress·ipv6 / 选项 / 标签)与 「接入的容器」页签(容器 / IPv4 / IPv6 / MAC)。容器数刻意不进列表行: docker network inspect 才能拿到接入列表,列表逐行 inspect 就是 N 次 docker 调用, 改成点进详情取一次。

  • 卷:名称 / 驱动 / 范围 / 挂载点(超长路径限宽省略,title 给全量),详情为

名称 / 驱动 / 范围 / 挂载点 / 创建 / 选项 / 标签(卷没有反向索引,只有概览一页)。

  • 详情页头部有删除按钮,两页工具条各有 prune 图标,都受 allowMutations