DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

npm /

@hyzyn/dsh-docker

Verified

DSH Web GUI 的 Docker 容器面板插件:本机或 SSH 主机上的容器列表 / 启停删 / 日志 / 统计 / 镜像 / 一次性 exec,默认只读,配套只读优先的 agent 工具。

0.16.1 Versionnpm Package sourceN/A Community rating0 Confirmed installs
View on npmProject homepage
READMESource: npm@0.16.1

@hyzyn/dsh-docker

中文 | English

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

特性

  • 常驻会话右侧栏:面板作为右侧栏标签(sidebar.right.pane.tab)与对话同屏——发完错误交给 Agent 后日志留在右边;折叠即退出视野并主动断流、把 SSH 通道还回去。面板级状态(目标 / 视图 / 过滤词 / 选中容器及页签)跨会话保留;拿不到右侧栏服务的老宿主退回 dock / 模态。
  • 日志右键交给 Agent:日志页拖选报错行 → 右键「直接发送到当前会话 / 填入输入框,我先改改」,投递 「选中的行 + 目标 / 容器 / 时间窗 + 前后各 20 行上下文」(会进模型上下文,菜单里明写留意凭证);回执 两处:视口级 toast,以及终端面板开着时自动折起终端(ttyPanel 契约 v2)。改稿走「填入输入框」 ——编辑面只有会话输入框一个。
  • 一键连接本机 / 多目标聚合取数:两个非常驻入口(空态主按钮 / 目标下拉末项)一次点击就加上本机 目标(agent 侧 docker_connect_local);总览对全部 targets[] 并行取数,单个目标不可达只污染自己 那一格(docker_ps target:"*" 同一口径)。
  • 「需关注」读权威字段:不健康 / 反复重启 / OOM 被杀 / 非零退出 / 僵死;OOM 与真实退出码由一次 docker inspect 补齐——docker ps 摘要里的 137 分不出 OOM 与手动 kill(口径见 总览)。
  • 排障闭环:诊断页签把四路采集变成「结论 + 判据 + 证据 + 时间线 + 建议」,诊断历史再答「是不是在 变坏」;「进程」页签给 docker top 快照;「磁盘」视图摊开可回收账与「谁在用」;概览底部反推一条可复现 的 docker run。见 容器诊断 与 诊断历史。
  • 四条 SSE 长流共用一套基建:日志 FOLLOW、docker stats、docker events、docker pull 走同一个 openSseStream(心跳 / 活跃流登记 / 断开清理 / 管道起来后先发一帧 ready),差异只在收尾语义;聚合 日志把多个容器装进一条 /logs/stream?ids=…。「连上了」与「在收数据」是两件事:客户端只认 ready。 见 SSE 事件协议。
  • 默认只读,两个独立开关:allowMutations(容器启停删 + 镜像删除 / 清理 / 拉取)与 allowExec (一次性 docker exec);关闭时对应 agent 工具不注册、 HTTP 路由 403(能力不存在,而非调用后报错);提权走两条带外通道(启动环境变量 / 卡片里就地确认)。 容器名与 ID 过白名单,命令一律 argv 构造 + 单引号转义,密码 / 口令以 env:NAME 凭据引用且永不回传 浏览器。
  • 与 dsh-tty 数据级复用、代码级不耦合:不 import 任何 tty 代码,两者可各自安装与升级;装了 tty 则消费 三个可选扩展点(ttyConnbar / ttyTerminal / ttyPanel),未装或版本不足逐项静默降级。

与 dsh-tty 的关系

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

维度 说明
插件形态 独立包 @hyzyn/dsh-docker,可各自单独安装、各自升级
连接簿 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);tty 未装 / 版本过旧则静默跳过
面板承载 默认走会话右侧栏标签(sidebar.right.pane.tab),框架侧边栏入口只负责打开或聚焦它、页类型在同一栏内去重;终端连接栏的入口相反——它长在盖满视口的 tty 弹窗上,开标签会被挡住,所以走 ttyPanel.mountPane 停靠到终端右侧(入口决定承载)。拿不到右侧栏服务(老宿主)或把 localStorage['dsh-docker:carrier'] 置成 modal 时回退到 dock / 全屏模态。三种承载是同一个组件,只是外壳与几何不同
终端承载 交互式终端由 tty 承载(它才是 PTY 的所有者)。右侧栏标签:面板全屏(sidebar.fullscreen)时经 ttyTerminal.mount 就地嵌入面板底部,否则经 ttyTerminal.open 开新标签(同一容器重复点会聚焦已有标签——tty 契约 v3 的 reuse);dock 承载一律开标签;模态承载就地嵌入。命令标签(spawnSpec.command 非空)在 tty 侧不显示连接栏的扩展按钮区(SFTP / 隧道都作用于连接本身,挂在 docker exec 标签上会误导);本插件自己开的 exec 标签另不给「容器」入口。未装 tty 或版本不足时复制命令兜底。本插件不实现 PTY / xterm / 重连栈
分工 交互式排障(docker exec -it、容器内 shell、TUI)由 tty 承载;只读巡检与 agent 自动化用本插件自己的 exec 通道

安装

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。

使用

两个入口,同一个面板。默认承载是会话右侧栏标签——入口只负责打开或聚焦它,面板与对话同屏。

右侧栏标签承载:会话与容器面板同屏、面板全高;折叠即退出视野,不占屏

  • 侧边栏「容器」(总入口):任何目标都能用,包括本机 docker 与多目标切换。已打开则聚焦(页类型在 同一栏内去重),不会开出第二个「Docker 容器」标签。
  • SSH 连接栏「容器」按钮(上下文快捷方式,tty ≥ 0.13.0):在 tty 面板的 SSH 标签里、SFTP 旁出现, 注册即显示,点击直接用当前会话那台主机打开面板。目标在点击时解析:会话来自连接簿时按条目名 匹配,否则按 host:port;没配到目标也不藏按钮——面板会带一条提示告诉你该怎么配。这条路径走 终端右侧的 dock,不开右侧栏标签(按钮长在盖满视口的 tty 弹窗上,开标签会被挡住)。于是规则是 「入口决定承载」:框架侧边栏点 → 右侧栏标签;终端弹窗里点 → 终端右侧 dock。

承载形态

承载 何时用 行为
会话右侧栏标签(默认) 宿主提供 sidebarRight / sidebarRightTabs 与会话同屏;折叠=退出视野但不丢状态;支持右侧栏的 fullscreen 铺满
终端右侧 dock 从终端连接栏的「容器」按钮进来,或拿不到右侧栏服务 / 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 右侧栏的标签记录本身是会话作用域的,A 会话开的标签在 B 会话里并不存在;而面板 看的是主机,「切个会话它就没了」是纯损失。所以插件额外记一个「用户希望它开着」的意图:切会话时自动 在新会话里把标签重开,只有你点了标签的 ✕ 才停止。那个 ✕ 是宿主执行的,所以必须经 registerCloseHandler 登记才收得到——只靠面板自己的 onClose 会漏掉它。

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

docker 面板挂进终端面板右侧 dock:终端保持可见可用

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

面板内:

容器列表:目标选择 / 搜索与状态筛选 / 卡片动作条(查看 · 变更两组)

  • 切目标有过渡与保护:选择器一变,下方仍是上一个目标的卡片(远端一次往返最长 20s)。过渡期间旧 卡片去色并锁住指针(读起来是「另一批内容」而不是「坏了」,也避免照着旧列表操作——那会拿新目标 当目标、用旧容器的 ID 发命令,真能停掉对端同名容器),新数据淡入落地;整块绝对定位,首卡位置与 滚动高度不变。切换失败则清空旧列表并归到新目标的错误态,空态文案区分「读取失败」与「筛选过窄」。

  • 目标选择:面板先选目标(来自配置 targets,本机 / SSH);只配了一个时默认选中它,agent 工具也 可以省略 target。会记住上次选的目标(localStorage 的 dsh-docker:last-target,不落配置): 优先级是连接栏指定 > 上次记住的(且仍存在)> 列表第一个。换目标是整段上下文切换:上一个目标 还在飞的请求一律作废(列表写入闸),不会出现「选择器已是目标2、卡片还是目标1 的容器」这种串台。

  • 容器列表:名称 / 状态 / 健康态 / 镜像 / 端口映射 / compose 项目与服务 / 短 ID;支持按名称或镜像 搜索、按状态筛选、自动刷新(按 pollIntervalSec 轮询,切到镜像页会暂停且不显示该开关)。 「含已停止」与「自动刷新」是成组的两个开关,搜索框是其中唯一可伸缩的项:宽面板下一行,窄栏 (dock 520px)里才按组换行。

  • 「活动」条(docker events 事件流):容器列表头部的可折叠窄栏(默认展开),跟着一条 SSE (GET /api/dsh-docker/events/stream,服务端跑 docker events --filter type=container)显示最近 8 条 事件(时间 + 容器名 + 动作,die 带退出码)。它是事件驱动刷新的入口:收到事件后 500ms 防抖 触发一次列表重取,与 AUTO REFRESH 叠加而不互斥——轮询兜底,事件负责「刚发生」。事件只留在内存 (环形缓冲 50 条),切页即关流,换目标清空。白名单只留九类生命周期动作(start / die / stop / kill / oom / health_status / destroy / rename / update):exec_*、archive-path 这类噪音在服务端就丢掉。 断开后要手动重连:进了 closed 就是终止态,活动条上出现重连按钮(unsupported 环境不给), 工具条的 ⟳ 也一并重连;AUTO REFRESH 刻意不重连(塞进轮询会让已终止的流被不断重开),重连成功后 补一次全量列表刷新。重连被拒要说原因(D164):服务端主动拒绝时发一帧 event: error 带 message,客户端把它显示在活动条上——只置 closed 会让「拒绝 → 回到原位」与「点了没反应」不 可区分。「在收事件」只认宿主那帧 ready(响应头到达不算)。

  • 聚合选择(临时多选聚合日志):工具条的 聚合选择 进入选择态——点卡片本体变成勾选 / 取消 (动作条暂时收起,免得一边多选一边误点启停删),出现操作条:「已选 N 个容器」+ 聚合日志 + 取消。 聚合日志 至少需要 2 个容器、最多 3 个(超了按钮置灰并提示上限来由)。进入后复用 Compose 项目 视图那套聚合日志,内容控制与单容器日志完全对齐,另有按时间 / 按到达排序与暂停。勾选是临时 的:不持久化、不进 settings;切目标 / 切分段 / 关面板即失效,列表刷新后已消失的容器按 id 自动剔除。

  • 按条件一键选中(多选态):操作条第二行给出条件 chip(全部可见 / 不健康 / 需关注 / 已停止,以及 有勾选后的 同镜像 / 同项目),计数按剩余名额截断——chip 上写 3 就真的会选中 3 个。条件只在 当前筛选结果里生效。

  • 容器卡片:与参考布局一致的「标签 + 值」行(镜像 / ID / 端口 / 创建 / compose)+ 一排图标按钮, 按「查看 / 变更」两组用竖线分隔:

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

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

  • 容器详情(整栏视图):顶部为「返回 + 容器名 + 状态徽标 + 目标主机」,下方五个标签页——概览 (docker inspect 权威数据 + 一次性 exec + 等价 docker run)、日志、统计、诊断、进程。从卡片的 日志 / 统计图标可直接落到对应页签;诊断与进程只在该页签被打开时才取数(切走即不再请求)。

  • 日志视图(紧凑两行):第一行 = 返回 + 容器名 + 状态 + 目标主机 + LINES / TIMESTAMPS / FOLLOW / AUTO REFRESH(开关 + 2/3/5/10s 间隔,仅日志页轮询)+ 刷新 / 关闭;第二行 = 标签页 + 常驻的「过滤日志」输入框(有内容时浮出 ✕,Esc 也能清空)+ 级别门槛(全部级别 / INFO+ / WARN+ / ERROR+)+ 导出(⬇ .log / ⬇ .md,导出当前显示内容)+ 右侧固定槽的行数统计。 两行的分工是刻意的:第一行管传输与显示,第二行管内容——而第二行与聚合日志完全同一套(同一个 级别内核、导出构建器、计数文案)。级别门槛的语义是「无级别前缀的行是上一条的续行,跟随其级别」, 否则 ERROR+ 会把堆栈拦腰截断。日志正文按级别着色,时间戳压暗,过滤命中高亮。

  • FOLLOW 实时日志流:打开后从「定时拉快照」切为 SSE 推送(GET /api/dsh-docker/logs/stream, 服务端跑 docker logs --follow),与 AUTO REFRESH 互斥(开流自动停轮询、开关置灰),关闭即回到 快照并立即刷新。流式日志保留最近 5000 行 / 4MB(环形缓冲,超限丢最旧并提示一次;无换行的超长 输出强制分片,内存恒有界),分片到达按 150ms 合帧渲染。正文按窗口化挂载(D152):DOM 里只 留「可见 + 上下各 24 行 overscan」,其余用上下垫高占位;行高是实测的、贴底时窗口锚在尾部,所以 「自动贴底 / 上滚暂停 / 回到底部」的几何判定依旧精确;换流 / 换容器 / 快照刷新会整代作废这批实测 高度与锚点(D155:快照行 id 是位置寻址,不换代就会拿另一代的高度算垫高)。显示层不截断: LINES 选多少就能滚到、能导出(真正的闸是缓冲与宿主的「输出上限(KB)」)。断线重连由插件自己 管:首连带 tail 补历史,重连一律 tail=0——只补新行、不重放历史。宿主侧背压(D153): 单条流的暂存队列上限 8MB;日志与拉取这类文本尾部流溢出时丢最旧的帧、留最新,并推一条 skip{frames,bytes} 让界面说明「中间断了一截,跟随继续」——不再掐流;skip 的账目是精确的(排队 期间再次丢弃原地更新计数,D156;客户端把多批 skip 累加显示,重连成功即清零,D157)。统计 / 事件这类结构化流仍走收尾重连。

  • 概览:docker inspect 的权威数据——状态与健康、退出码、重启次数与策略、端口映射、挂载(含只读 标记)、网络与 IP、entrypoint 与命令、最近一次健康检查输出;下方可执行一次性 docker exec(需 allowExec)。

  • 日志:docker logs --tail 的尾部快照(LINES 默认取设置卡片的「日志默认行数」),可切时间戳与 --since。两个闸要分清:LINES 是行数上限,设置卡片的「输出上限(KB)」是字节上限—— 两者取小,字节先超就截断并给出横幅。要持续观察新日志就打开 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 / 内存迷你趋势图。这条流不会自然结束,关闭语义是前端主动 断;docker stats 自己退出时服务端发 end(reason=stats-exit)。
  • Compose 项目视图:工具条第三段。把 composeProject / composeService 聚合成「项目 → 服务 → 容器」,每个项目一行看全运行数 / 不健康数 / 服务数;点进项目可看服务表,或打开项目级聚合日志 ——一条 /logs/stream?ids=… 装下项目内所有容器(帧里带 id),客户端按 [service] 前缀混流。某个 容器的子流失败只标注「部分出错」,其余容器的日志照常收。
  • 镜像:docker images 列表(reference / 大小 / 创建时间 / 短 ID),<none>:<none> 的 dangling 带标记;搜索框与计数固定在工具条里,表头列名在表体内吸顶。每行「详情 / 删除」:详情打开整栏 视图(概览 + docker history 构建历史);删除(需 allowMutations)二次确认后执行 docker image rm(不带 -f,被引用时会失败并给出「先删相关容器」的提示)。
  • 网络 / 卷:工具条分段扩到「容器 / 镜像 / Compose / 网络 / 卷 / 磁盘」(窄栏里分段自己横向滚动, 不做二级菜单)。两页都是「表格 + 整栏详情」的同构布局:
    • 网络:名称 / 驱动 / 范围 / internal 徽标 / ID,行点击进详情——概览与「接入的容器」页签。 容器数刻意不进列表行:只有 docker network inspect 才拿得到接入列表,列表逐行 inspect 就是 N 次 docker 调用。
    • 卷:名称 / 驱动 / 范围 / 挂载点(超长路径限宽省略,title 给全量),详情只有概览一页(卷没有 反向索引)。
    • 详情页头部有删除按钮,两页工具条各有 prune 图标,都受 allowMutations 门控(未开启时置灰
      • title 说明)并二次确认;删除失败就地弹横幅,不会静默。这两页不轮询(清单变化慢)。
  • 拉取镜像(SSE 进度流):镜像页工具条的拉取图标(需 allowMutations)打开拉取视图,输入引用后走 GET /api/dsh-docker/images/pull/stream——逐层进度实时出现;进度行按「层键」原地更新,TTY 下的 \r 刷新也不会让缓冲区越滚越长。工具条的「清理 dangling」执行 docker image prune -f,只删无标签 镜像(刻意不加 --all)。

镜像页:搜索框固定在工具条,表头吸顶

  • 一次性 exec:allowExec 开启后可输入命令,等价 docker exec <容器> sh -c "<命令>",返回退出码 与 stdout/stderr(无 TTY)。

  • 交互式终端(卡片第一个图标):跑 docker exec -it '<容器>' sh,按 tty 能力三级降级—— exec 终端抽屉 + 日志页共存:折叠只藏起来、会话保持运行

    1. 就地嵌入(tty ≥ 0.15,推荐):面板底部开一个终端抽屉,由 ttyTerminal.mount 把终端挂 进来,面板不收起;折叠(标题栏箭头或双击顶边)只把抽屉压成一条标题栏、会话照旧跑着, 拖拽顶边可调高度(上限 75%)。真正结束会话的只有抽屉的 ✕ 与关闭面板——有活动会话时关面板 会先弹确认,避免点 backdrop 误杀一个正在排障的容器 shell。
    2. 借 tty 弹窗开标签(tty ≥ 0.14):tty 新开一个标签执行同一命令,随后收起本面板(本面板 z-index 更高,不收起用户只会觉得「点了没反应」)。
    3. 复制命令:未装 tty / 版本过旧 / 内联目标用了 key·password 认证(浏览器端没有凭证)时,退化 为复制该命令并提示到终端面板粘贴。

    本地目标开本地会话,SSH 目标按连接簿条目名(或内联字段)走 SSH,标题为 <容器> · exec。能力判断走 服务契约版本(ttyTerminal.version >= 2 才有 mount),不是猜函数存不存在。

目标(本机 / SSH)

kind 说明
local 宿主所在机器上的 docker CLI(spawn 直接执行,不经 shell)
ssh 经 ssh2 连到远程主机,在远程执行 docker CLI(argv 经单引号转义)

kind=ssh 有两种填法:① 引用 tty 连接簿条目——book 填条目名(在「终端面板」的连接簿里维护), 主机 / 端口 / 用户名 / 认证方式随之生效;设置卡片的下拉只列出 tty 已保存的条目,tty 未安装或条目不 存在时该目标解析失败,面板与 agent 工具都会给出明确错误。② 内联字段——host + username 必填, 其余按需(port / auth / keyPath / password / passphrase / agentForward)。

目标解析是每次操作现算的:在 tty 卡片里改了连接簿条目,下一次操作立即用新值,无需重启。远端需要 装了 docker CLI,且当前账号免 sudo 可用 docker(通常在 docker 组),否则 probe 会原样透出 permission denied while trying to connect to the Docker daemon socket 之类的错误。

连接本机(一键)

不想手填目标时,UI 里有两个入口,都不做常驻:

何时 入口在哪
一个目标都没有 面板正文的空态里一个 连接本机 主按钮(那时整块是空的,一键就是最短路径)
已经有别的目标 目标下拉的末一项 + 连接本机——就在你找机器的地方,点开才出现

两处互斥(不会同一个入口出现两次),已经有本机目标时两处都不出现。做成常驻的工具条按钮不划算: 已经有可用目标时它既占位又招人点,是纯噪音;而只留空态那一个入口又会漏掉「手上有几台 SSH、想再加 一台本机」这种常见情况。agent 侧同一件事是 docker_connect_local,HTTP 侧是 POST /connect-local ——所有入口共用宿主里同一份实现。

情况 行为
已有一条 kind=local 目标(无论叫什么名字) 复用它(created:false),只切过去;不新建、不重写、不改名
没有本机目标 用稳定名 local 追加到列表末尾;local 已被占用时依次试 local-2、local-3…
已有 SSH / 自定义目标 原样保留(含顺序与凭据);只在末尾追加本机那条
重复点 / 并发点 幂等:最终只有一条本机目标
设置存储不可用 / 保存失败 明确报错(503 / 500 + 原因),不改内存配置——不会出现「界面上加上了、刷新就没了」

写盘成功之后顺带做一次只读探测(docker version),结果放进响应的 message:daemon 可达时给出 服务端版本;不可达时说清成因——docker CLI 缺失(去装 Docker,或把「docker CLI」改成正确路径)/ 探测超时(daemon 卡死,或 docker context 指向了连不上的远端)/ daemon 未启动(Linux systemctl start docker,macOS / Windows 启动 Docker Desktop)/ socket 无权访问(账号加入 docker 组)。探测失败不算这次操作失败(目标确实加上了,reachable:false 单独表示)。

探测用的二进制就是设置卡片里的「docker CLI」(dockerBin,默认 docker)。CLI 缺失时会只读地 在 PATH 里找一下别的容器 CLI(docker → podman → nerdctl),找到就在那句话里点名——只提示, 绝不替你改配置(podman 与本插件输出格式的兼容性并未逐项验证,见「已知限制」)。比对候选时按工具 名(basename)而不是配置里的原字符串。超时是独立一档:回 timedOut:true + 一句可执行的话,并且 真的按时返回——到点先杀、再给 500ms 收敛期,仍不 close 就自行放掉管道收尾(SIGKILL 只作用于直接 子进程,dockerBin 指向包装脚本时孙进程会把手上的上限拖成分钟级)。

刻意不做的事:不自动安装 Docker、不启动 daemon、不切换 docker context、不改任何环境变量;空 targets(= 用户没配)的语义不变,不产生隐式默认目标;clearTargets 的防误清空逻辑一字未动。这条 路径也与能力开关无关——它只写插件自己的配置,不涉及任何需要授权的目标侧写操作。

聚合日志(多选 / Compose 项目)

多选容器或打开一个 Compose 项目,都能把多个容器的日志聚合成一条流:一条 /logs/stream?ids=… 装下 全部容器(帧里带 id;宿主侧仍为每个容器各跑一条 docker logs -f),客户端按到达顺序混流。工具栏提供:

控件 语义
实时 / 已暂停 真暂停(见下)
时间戳 显示每行时间戳。时间戳始终随流接收(timestamps=1),只影响显示
按到达 / 按时间 按到达:零延迟跟随;按时间:用每行的容器时间戳合并成一条真时间线
级别过滤 全部级别 / WARN+ / ERROR+。无级别前缀的行(堆栈等续行)继承上一条日志的级别,所以 ERROR+ 会连它的堆栈一起保留;窗口开头的孤儿续行(记录头在窗口外)保留
⬇ .log / ⬇ .md 导出当前显示内容:.log 是纯行文本([服务] ISO时间 正文),.md 带来源容器 / 行数 / 导出时间表头

按时间合并怎么做的:SSE 各容器建连有先后,A 的首屏历史可能整批先到、B 随后才到,只对单批排序修 不了跨批逆序。实现是回填最近 400 行——每批新行到达时把「最近 400 行 + 新行」整体按时间戳重排 (RFC3339 前缀为排序依据,解析后从正文里剥掉)。代价是「按时间」模式下已在屏上的最近若干行可能轻微 重排(正在跟随实时输出时建议用「按到达」)。

聚合日志的「暂停」(真暂停)

多选容器 / Compose 项目的聚合日志有一个 实时 / 已暂停 开关。暂停是内容冻结,不只是停止自动滚动: 暂停期间新到的日志进入客户端缓冲、DOM 不再追加(读屏不会被顶走,也不会因显示上限 2000 行裁前部 而跳屏);按钮上直接显示攒了多少行(已暂停 +348);恢复时把缓冲一次性并入(沿用 5000 行环形上限) 并回到底部。只停「自动滚动」是不够的:标签写着「已暂停」而内容还在长,用户会以为开关坏了。

总览(跨目标)与「需关注」口径

目标选择器旁的 总览 pill(配了 ≥2 个目标才出现)一屏铺开全部目标:每个目标一张计数卡(运行中 / 已停止 / 不健康 / 需关注),下方是跨目标的「需关注容器」表(容器名 / 目标 / 状态 / 原因 / 镜像,点行进详情、点卡切到该目标的列表)。三条设计约束:

  • 渐进落地 + 失败隔离:每个目标独立请求、独立落格;一台 SSH 不可达不会让整页静默(刻意不用 Promise.all 汇总:SSH 不可达要等 readyTimeout 20s,汇总等于让整页静默 20s)。
  • 需关注口径以宿主为准:容器列表与 /attention 并行请求;后者额外做一次 docker inspect,因此能 识别 OOM(OOMKilled) 与真实退出码——ps 摘要里 Exited (137) 分不出是被 OOM 杀还是手动 kill。 拿不到 /attention(老版本宿主 / 该目标失败)时退回摘要口径,并在计数上标注「需关注(粗判)」。
  • 排序:OOM > 僵死 > 不健康 > 反复重启 > 非零退出;同权重按「最近一次结束时间」倒序(行 hover 显示 结束/启动时间、重启次数、退出码)。

总览的轮询复用工具条的「自动刷新」开关(默认关):这一页每轮是全 N 个目标各一次 docker ps,比任何 单目标页都贵。没有异常显示「一切正常」;还有目标没答完时显示「读取中…」,计数卡也进入读取态(不出 0/0/0——「0 个容器」会被读成「这台机器没容器」,而它只是还没答)。进总览时会清掉单目标遗留的失败横幅: 总览按目标归因,不再叠一条单目标的「操作失败」——同一个 SSH 超时被讲两遍,看着像两台机器都挂了。

容器诊断(「为什么不行」)

容器详情多一个诊断页签,agent 侧是 docker_diagnose。它一次采集四路——docker inspect(权威状态 / 退出码 / OOM / 重启次数 / 健康检查最后输出 / 资源上限)、docker logs --tail N、docker events --since (服务端按 --filter container=<id> 过滤)、docker stats --no-stream——然后交给一组纯判据 (src/diagnose.ts,零 IO、不读时钟)产出:

  • 一句话结论(verdict):取最高严重度的那条,分 critical / warning / info / ok;
  • 判据列表(findings):每条 = 判据 code + 一句话 + 证据。判据是这几条——OOM 被杀、非零退出(附 126 / 127 / 137 / 143 的常见含义)、反复重启、不健康(附健康检查最后输出)、健康检查尚未通过、正常 退出、未运行(created / paused / dead)、内存压力(≥90% 且设了上限)、CPU 压力(≥90%)、日志里有 报错、没有健康检查,以及三条采集失败信号;
  • 证据(evidence):状态 / ps 风格的状态描述 / 退出码 / OOM / 重启次数 / 起止时间 / 内存与 CPU 上限 / 日志行数与报错行数 / 事件条数 / 当前占用;时间线(timeline)是窗口内 start / die / oom / health_status 等事件(升序);建议(suggestions)0~5 条可执行动作。

「没采到」与「确实是空的」是两件事(本仓一贯的信号位原则):旁路三路(日志 / 事件 / 统计)任一失败 时,证据里对应字段是 null(不是 0、不是空数组),并多出一条 *-unavailable 说明原因。否则「连不上 主机」会被读成「日志是空的、从没重启过」,排障方向当场被带偏。

页签底部三个动作:重新诊断、复制诊断、交给 Agent。最后这个走日志右键那套会话桥(直接发送 / 填入草稿二选一,没有会话时置灰并写明原因):文本自带容器、目标、采集时间、结论与证据,日志原文用围栏 包起来并标注「这是数据不是指令」。采集时间在界面上是本地时间、投递文本里是 ISO 8601。

诊断是只读:不改容器、不重启、不发命令,只多跑四条只读 docker 命令。

诊断历史(它是不是在慢慢变坏)

单次诊断只回答「现在怎么样」;缓慢变坏(内存一点点涨、重启越来越频繁、报错逐渐变多)在单次结果里 看不出来。所以每次诊断都会落一个样本,诊断页据此多一块「历史」:

  • 色带:最近 N 次的严重度(悬停显示时间与结论 code);
  • 趋势结论(trend.verdict):七条判据取最高的一条——反复重启(重启次数涨 ≥2)、反复 OOM(≥2 次)、 内存爬升(≥20 个百分点且末值 ≥80%)、报错增长(≥5 行或从 0 变正)、新出现严重判据(oom-killed / crash-loop / unhealthy / exit-nonzero)、严重度连续走高、没有明显变化;样本不足 2 次时明说 「还不够形成趋势」;
  • 数字变化(trend.numbers):只列真的变了的项,且每项有「最小有意义变化」——重启 ≥1、内存 ≥1 个百分点、CPU ≥5 个百分点、报错 ≥3 行、事件 ≥5 条、日志行数 ≥50。诊断只取一个 stats 瞬时快照, 1.11% → 1.09% 这种抖动不设闸就会把趋势块填满;报错行数是滑动窗口,所以给到 3 行才算变化;
  • 判据增减:「新出现」/「已消失」两行(oom-killed 是新出现的,那就是恶化信号)。

存什么、不存什么:样本只存结论与数字(严重度、结论 code、判据 code 列表、状态 / 退出码 / OOM / 重启次数 / 内存与 CPU 占用 / 报错行数 / 事件条数 / 日志行数 / 时间)。绝不落盘:日志原文(logTail) 与每条判据的 detail(里面会嵌日志行,可能带凭证)——所以这份文件可以放心 cat,但它也因此不能用来 回放某次排障现场。

落盘:<DSH 主目录>/dsh-docker/diagnosis-history.jsonl(目录 0700、文件 0600),一行一条。上限: 每个容器 50 条、整文件 2000 条,超了丢最旧并原子重写(临时文件 + rename)。30 秒去重:同一 容器、30 秒内、且严重度 + 结论 + 判据集合 + 重启次数完全相同时不追加。DSH_DOCKER_HISTORY_DIR 可 覆写目录(测试 / 排障专用),空串视为未设。三条边界:① 容器改名 = 开一段新历史(索引键是「目标 + 容器名」);② 并发追加没加锁(可能丢一次更新);③ 写盘失败只降级不报错:诊断照常返回、 recorded = false、宿主 stdout 留一行。

磁盘与可回收(存储治理)

磁盘是工具条分段的最后一段(容器 / 镜像 / Compose / 网络 / 卷 / 磁盘),agent 侧是 docker_disk_usage。数据来自两条命令:docker system df(四类汇总)与 docker system df -v(逐项明细)。

  • 四张汇总卡:镜像 / 容器 / 本地卷 / 构建缓存各自的条目数、活跃数、占用、可回收(分类名保留 docker 原文,界面负责翻译);
  • 合计可回收:四类 reclaimable 的字节和(任一行解析不出就给 —,宁可不说也不给错数);
  • 镜像明细(按独占大小降序):仓库:标签 / 大小 / 独占 / 容器数——containers === 0 打「未使用」 徽标,行尾删除图标(受 allowMutations 门、走既有确认框);
  • 卷明细(按大小降序):名称 / 驱动 / 大小 / 链接数——links === 0 同样打「未使用」。

三条刻意的取舍:先排序再截断(真机实测 259 个卷里 258 个是 0B,先截断会把 100 个名额全给空卷); 明细上限 100 行并在表头留痕(工具侧另按 20 行截,两个 truncated 取或);老 docker 只降级不报错 (不支持 -v --format '{{json .}}' 或输出形状不对时 degraded: true + 人话原因,汇总卡照常显示、明细表 不渲染——渲染空表会让人以为「没有」)。

磁盘页不轮询(system df -v 比 docker images 贵),也没有 prune 按钮:逐行删除 + 确认框比 「一键清空一类」安全,而那类入口已经在镜像 / 卷页的工具条上各有一个。

等价 docker run(现场还原)

概览页底部有一块可折叠的等价 docker run,agent 侧是 docker_run_command。它从 docker inspect 的 原始行反推一条可复现命令——不扩 ContainerDetail,因为那会把 env 顺带灌进 docker_inspect 的 模型上下文。覆盖:名称、重启策略、端口映射(没绑 hostPort 的用 --expose)、bind / 命名卷 / tmpfs、 非默认网络、环境变量、用户 / 工作目录 / 主机名(等于默认值时省略)、内存与 CPU 上限、cap-add / cap-drop / device,以及镜像与命令本身。点开才请求,一行一个参数、POSIX 单引号转义,可直接复制。

三条刻意的取舍:环境变量原值保留(改写过就不是等价命令),命中 /(PASS|PWD|SECRET|TOKEN|KEY|CRED|AUTH|PRIVATE)/i 的键名收进 secretKeys——界面在命令上方给一条 「含疑似凭证」的警告,只点键名、不显示值,命令本身不脱敏(脱敏后的命令跑不起来);故意没还原的 必须点名(omitted:--privileged、--link、非默认日志驱动、--pid/--ipc/--uts、--sysctl、 --add-host、--security-opt、特殊挂载类型、匿名卷、ENTRYPOINT 覆盖、自定义健康检查——逐条给出「为什么 没写」与「怎么补」,绝不静默丢);compose 管理的容器只是近似(网络别名 / labels / depends_on 不在命令里,界面直说)。

配置(保存即热生效)

未获宿主授权时,设置卡片里那两个开关点它会就地发起一次授权:面板给出一条「在宿主终端执行」的命令, 执行完十秒内自动解锁并替你打开开关(免重启);面板里另有「另一种方式(最强)」说明启动环境变量的做法。 已授权时它们就是普通开关,关掉永远可用(宿主侧不拦降权)。「配置开着但没授权」会显示「未生效:未获 宿主授权」徽标——这个状态最容易被当成插件坏了,所以必须有个名字。一个能力一行:开关后面跟这个能力 自己的授权状态与授权时刻(已授权 · 2026-09-26 22:31:08),撤销按钮右对齐。

两个能力的确认也能一次粘贴搞定:再点另一个开关不会顶掉前一块面板,两块面板纵向堆叠,顶部多出 一个「复制全部(N 条)」。

为什么显示时刻:带外授权是持久的——宿主重启后它直接生效、不再有任何一次确认,所以宿主启动时会逐条 打一行 [dsh-docker] elevation: load capability=… via=file grantedAt=…(不含 nonce 与路径),卡片上 也显示出来。启动环境变量授权没有时刻,那一行写的是「由启动环境变量授权;要撤销需在启动环境里去掉它 并重启宿主」。

设置卡片:目标 CRUD、能力开关与参数,保存即热生效

设置面在宿主里的位置随 DSH 版本变,但始终是同一份表单(同一个 settings 命名空间、同一条写入 通道,配置数据与行为完全相同)——各代际的落点与四条注册契约的唯一归宿在 conventions.md § 客户端设置面。

配置落在 settings 命名空间 docker,即 ~/.dsh/settings.yaml 的 docker: 段 ($DSH_HOME/settings.yaml)。插件行里的 composition 配置作为 schema base 打底,settings 层覆盖它; HTTP POST /api/dsh-docker/config 是卡片的写入通道,只接受下表这些键(未知键返回 400)。

项 默认 说明
enabled true 关闭整个插件(保存即热生效:工具立即注销、公告撤下、除 /config 外的数据路由 403;/config 始终可读写——设置卡片就是重新启用的入口。与 tty 不同:tty 是重启生效)
announceToAgent true 是否向 agent 注入能力公告(systemPrompt section plugin:dsh-docker)
dockerBin docker docker CLI 可执行名或路径(podman 可填 podman);只允许字母、数字与 _ . / \ : - 及内部空格,且不能以 - 开头(Windows 盘符与 \ 必须放行**)。**「连接本机」的探测用的就是它**;填错时那句话会点名 PATH 里找到的别的容器 CLI,但不会**替你改这个值
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,对应工具不注册)
—(能力授权) 未授权 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
allowExec false 允许一次性 docker exec(容器详情抽屉「概览」页签里的一次性命令框,与 docker_exec 工具;关闭时 /exec 返回 403)。不管卡片「终端」按钮的 docker exec -it 交互式 shell——那条由 tty 承载(见「已知限制」)
execTimeoutSec 30 exec 默认超时秒数(1~120)
pollIntervalSec 5 面板统计刷新间隔秒数(1~60)
logTailDefault 200 日志默认尾部行数(1~5000)
maxOutputKb 512 单次命令输出上限(KB,1~8192);超出截断并标记 truncated
targets [] 目标列表,见下
hostKeys [] SSH 主机指纹记录(TOFU,自动维护)

数值越界会被夹到边界内,类型不符则回落到默认值。

targets[] 字段

字段 默认 说明
name —(必填) 目标展示名,唯一;空名或重名条目在保存时被丢弃(≤64 字符)
kind local local 本机 / ssh 远程
book '' kind=ssh 时引用 tty 连接簿条目名(留空则用下面的内联字段)
host '' 内联主机名或 IP(无 book 时必填)
port 22 SSH 端口(1~65535,越界回落 22)
username '' 内联 SSH 用户名(无 book 时必填)
auth agent agent(走 SSH_AUTH_SOCK)/ key(用 keyPath)/ password(用 password,同时挂 keyboard-interactive)
keyPath '' auth=key 的私钥路径(~ 开头会展开为 home)
password '' auth=password 的密码;建议填 env:NAME 凭据引用
passphrase '' 私钥口令;建议填 env:NAME 凭据引用
agentForward false 是否转发本机 ssh-agent(SSH_AUTH_SOCK 存在时生效)

password / passphrase 里的 env:NAME 是凭据引用(配置只持引用、值归 provider),连接时才 解析:① 官方凭据层(ctx.credentials)——叠 file($DSH_HOME/.credentials.yaml)/ env / project-env / user-env 各层,且「每次操作重新解析」,所以改完下一个操作即生效、不必重启宿主; ② 服务不可用(老宿主 / 未装该 bundle)或它没有这个引用时,退回 process.env[NAME]。两边都没有会明确 报错并点名两个来源(服务报错也一并带上——否则"凭据服务坏了"会伪装成"你没配")。这两个值永不回传 浏览器:配置快照里只给 passwordSet / passphraseSet 两个布尔位。

hostKeys[](SSH 主机指纹,TOFU)

字段 说明
host 主机名或 IP(必填)
port 端口,默认 22(按 host:port 唯一)
fingerprints 指纹集合(必填,至少一条):hostHash: 'sha256' 回调收到的原样十六进制指纹。同一 host:port 可含多把主机密钥(rsa / ed25519 …),命中任意一条即视为匹配
fingerprint 旧版单指纹字段(字符串):只作迁移输入,清洗时并入 fingerprints,新配置不要再用

首次连接自动记录并落盘;之后每次连接必须匹配,指纹变更直接拒绝连接,错误信息带「删除该主机记录再 重连」的指引。记录列表在设置卡片里可删除重置(删除会以显式的 hostKeysRemove: [{host, port}] 提交, 不会被并集合并悄悄撤销)。

agent 工具

工具 注册条件 参数 作用 / 典型用法
docker_targets 恒注册 probe?: boolean 列出目标(name / kind / label),SSH 目标还会带出它引用的 tty 连接簿条目名 book(内联 host/username 的目标没有这个字段;用它去终端面板 / sftp_* 的 book,名字对不上时只能靠它);probe:true 逐个探测 docker 版本与 daemon 可达性(SSH 目标会建连接,较慢)。其他工具的 target 取自这里
docker_connect_local 恒注册 —(无参数) 连接本机:确保存在一条 kind=local 目标并选中它(已存在则复用,不重复创建;SSH / 自定义目标原样保留),随后只读探测 daemon:返回 {name, kind, created, saved, reachable, serverVersion?}。没有任何目标时其余工具不注册(见下方「零目标」那条),用它一步补上。不需要任何能力授权;不会安装 Docker / 启动 daemon / 切 context / 改环境变量。daemon 不可达时说清成因(CLI 缺失 / 探测超时 / daemon 未启动 / socket 无权),不是只报空列表
docker_ps 有目标即注册 target?(传 * = 全部目标)、all?: boolean、name?(容器名子串过滤)、limit?(1~500,默认 100)、offset?(默认 0) 列容器(名称 / 状态 / 健康 / 镜像 / 端口 / compose 项目与服务 / 短 ID);默认只列运行中,all:true 含已停止。name 是服务端过滤(docker ps --filter name=,子串匹配),过滤先于分页,翻页时总数不会漂。target:'*' 时按目标分组返回,单个目标不可达不影响其他目标(该组带 error)。端口是 IPv4/IPv6 双栈归并后的映射(D130);ports 为空不等于没暴露端口——host 网络容器的端口即宿主机端口,这种情况会给 net 字段(D131)。排障第一步
docker_attention 有目标即注册 target?(支持 *)、limit?: number 需关注汇总:不健康 / 反复重启 / 被 OOM 杀 / 非零退出 / 僵死;每条带 reasons、exitCode、oomKilled、restartCount。OOM 与真实退出码来自一次 docker inspect。排障入口:不确定从哪台/哪个容器看起时先调它
docker_diagnose 有目标即注册 target?、id(必填)、logTail?(1500,默认 80)、since?(docker 语法,默认 1h)、history?(050,默认 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 就是「它是不是在慢慢变坏」那一句
docker_inspect 有目标即注册 target?、id(必填) docker inspect 的权威详情:状态 / 健康检查 / 退出码 / 重启次数 / 端口 / 挂载 / 网络 / 启动命令
docker_logs 有目标即注册 target?、id、tail?(1~5000,默认 logTailDefault)、timestamps?、since?、until? docker logs --tail 尾部;since / until 用 docker 语法圈时间窗;超上限标记 truncated;截断时结果里会说明可传更小的 tail、加 since/until,或改用 docker_logs_grep
docker_logs_grep 有目标即注册 target?、id、pattern(必填,≤512 字符)、regexp?、ignoreCase?、since?、until?、context?(020,默认 2)、limit?(1500,默认 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 正则
docker_stats 有目标即注册 target?、ids?(逗号分隔的容器名/ID) docker stats --no-stream 快照:CPU% / 内存用量与占比 / 网络 IO / 块 IO / PIDs;ids 省略 = 全部运行中容器。实时跟随是面板能力(SSE),工具保持单值快照语义
docker_images 有目标即注册 target?、limit?(1~500,默认 100)、offset?(默认 0) 镜像列表(仓库:标签 / 大小 / 创建时间 / 短 ID)
docker_top 有目标即注册 target?、id(必填) 容器内进程快照(docker top <id> -eo pid,ppid,user,stat,etime,pcpu,pmem,cmd):PID / PPID / 用户 / STAT / 已运行 / CPU / 内存 / 命令,字段全是 docker 打印的原文。容器「还在跑」与「进程还在」是两件事
docker_events 有目标即注册 target?、since?(docker --since 语法,默认 10m) 容器事件快照(过服务端白名单):start / die / stop / kill / oom / health_status / destroy / rename / update 九类,exec_* 等噪音已在服务端丢掉。要持续观察请让用户看面板容器列表的「活动」条
docker_networks 有目标即注册 target?、limit?(1~500,默认 100)、offset?(默认 0) 网络列表(名称 / 驱动 / 范围 / 是否 internal / 短 ID)。接入的容器列表不进列表行——列表逐行 inspect 就是 N 次 docker 调用
docker_volumes 有目标即注册 target?、limit?(1~500,默认 100)、offset?(默认 0) 卷列表(名称 / 驱动 / 范围 / 挂载点)
docker_disk_usage 有目标即注册 target? 磁盘占用与**「谁在用」**:docker system df 四类汇总 + -v 明细(镜像的 containers、卷的 links,各截 20 行)。删之前先看 containers 是不是 0;汇总与明细是两条命令,明细失败只降级(degraded)
docker_run_command 有目标即注册 target?、id(必填) 只推导、不执行:从 docker inspect 原始数据反推等价 docker run 命令文本 + warnings / secretKeys(只给键名,值不脱敏)/ omitted(故意没还原的,逐条给原因)。compose 容器只是近似
docker_image_inspect 有目标即注册 target?、ref(必填) docker image inspect + docker history:大小 / 含父层大小 / 创建时间 / 平台 / 层数与层列表 / 入口与命令 / 暴露端口 / digest / 构建历史
docker_action allowMutations + 有目标 target?、action(start | stop | restart | remove)、id 容器生命周期操作。remove 是破坏性的:删除容器配置与可写层(数据卷不在其中),执行前必须向用户确认目标容器
docker_image_remove allowMutations + 有目标 target?、ref(必填) 删除镜像(docker image rm,不带 -f)。破坏性:被容器或子镜像引用时会失败;执行前须确认目标镜像并复述后果
docker_image_prune allowMutations + 有目标 target? 清理 dangling(无标签)镜像(docker image prune -f)。刻意不加 --all,只删无标签镜像
docker_image_pull allowMutations + 有目标 target?、ref(必填)、timeoutSec?(10~1800,默认 600) docker pull 快照形态(可能耗时数分钟);交互式看逐层进度请让用户到面板镜像页的拉取图标看 SSE 进度流
docker_exec allowExec + 有目标 target?、id、command / argv(二选一:command 经容器内 sh -c 执行;argv 是精确参数数组,宿主与容器都不做 shell 展开)、timeoutSec?(1~120,默认 execTimeoutSec) 一次性 docker exec,返回退出码 / stdout / stderr。简单命令用 command;需要精确保留空格、引号、空参数时用 argv(如 ["printf","%s","a b"]),需要管道 / 变量时显式写 ["sh","-c","…"]。无 TTY,交互式排障请让用户到 tty 面板跑 docker exec -it <容器> sh
  • target 省略时回落到唯一已配置目标;配置了多个目标则必填,错误信息会列出可用目标名。
  • 零目标时只有 docker_targets / docker_connect_local 注册:其余工具没有可寻址对象,注册了只会让 模型白试一次;配好目标后立即补注册,无需重启。
  • 四条列表工具支持分页:docker_ps / docker_images / docker_networks / docker_volumes 都接受 limit(1~500,默认 100)与 offset(默认 0),结果带 total / truncated,截断时给出下一页的 offset。分页只解决「结果长到读不下去」,不改变单条 docker 命令 512KB 的上限。
  • 两个开关变化会立即重注册工具:关掉 allowMutations / allowExec 后,对应工具(含三个镜像变更 工具)从 agent 侧消失,无需重启。
  • 已注册的调用还受会话权限档位闸(tier gate)约束:read 类(上表标「有目标即注册」的读工具)全档 放行;docker_action / docker_image_remove / docker_image_prune / docker_image_pull 归 write 类,docker_exec 归 exec 类,这两类在受限档(仅可查看 / 工作区内修改)下每次调用弹授权询问(人 允许这一次才执行;无人值守即审批策略 never 时确定性拒绝),完全权限档零询问,Auto review 档不叠加。 宿主未组合 sandboxPolicy / approval 服务时不闸,启动日志打一行 tier-gate: services absent (…), per-call gate disabled。它与上面两个静态开关是双层:静态开关管 注册(宿主级、持久授权),档位闸管已注册调用的放行(逐调用、随档位即时变)。机制见 docs/permission-tier-plan.md。
  • 推荐排障顺序:docker_targets → docker_ps → docker_diagnose(一次拿到结论 + 判据 + 证据,多数 「它为什么挂了」到这里就有方向了)→ docker_logs(尾部看不到成因就 docker_logs_grep 带 pattern / since / until 检索深处)→ docker_inspect(原始字段)→ docker_stats / docker_top(活着但不对劲: 先看资源,再看进程还在不在);镜像排查 docker_images → docker_image_inspect;要清磁盘先 docker_disk_usage(看 containers / links 谁在用),再决定删什么。
  • announceToAgent 开启时,插件向 systemPrompt 注入一段精简公告:插件与面板位置、「默认只读」与 「docker socket ≈ 目标主机 root」的约束、「allowExec 只管一次性 exec 通道」的边界(卡片「终端」按钮 不受它门控)、target 的 * 适用范围,以及跨工具排障顺序;它不逐条复述工具用法。

HTTP 路由(/api/dsh-docker 前缀,全部 loopback 围栏)

围栏校验 remoteAddress(整个 127/8 + ::1 + ::ffff: 映射)、Host(字面量环回、localhost,或 解析到本机的主机名 / /etc/hosts 别名,解析带 500ms 超时与 60s 缓存)、Origin 与 sec-fetch-site; 非本机请求一律 403 forbidden: loopback-only。请求体上限 1MB,响应统一 application/json + referrer-policy: no-referrer。

除 loopback 之外还有一道「同源证明」:四条 SSE(/logs/stream、/stats/stream、/events/stream、 /images/pull/stream)与全部写子路由(/action、/images/remove|prune、/networks/remove|prune、 /volumes/remove|prune、/exec、/elevate(含 /elevate/status、/elevate/revoke)、/connect-local)要求请求带 Origin: <同源> 或 Sec-Fetch-Site: same-origin,否则 403 缺少同源证明。这是为了挡住「恶意页面用 <img src=.../images/pull/stream> 触发一次真实拉取」这类跨站副作用;curl / 老 Safari / 部分 WebView 不带 这两个头时会撞上它(浏览器正常使用不受影响)。只读路由不要求同源证明。

例外:桌面版靠宿主 Cookie 过闸(D139)。桌面版把页面发往 dsh-app://app/api/… 的请求转给真实宿主时 会删掉 Origin 与 Sec-Fetch-Site、只重写 Cookie(forwardWebRequest),所以「两条证明都缺省」 在桌面版是常态。这类转发的判定条件是带着宿主会话 Cookie(缺它整条转发直接 503):没有 Origin、没有 Sec-Fetch-Site、但有 Cookie → 放行;两条都没有且无 Cookie(裸 curl / 老 Safari) → 仍 403。浏览器页面伪造不了 Cookie 头,而跨站请求一定带 sec-fetch-site: cross-site(loopback 围栏 先拒),所以拒绝分支没有被放松;症状与排查见 docs/troubleshooting.md。

路由 方法 请求体 返回
/config GET — {ok:true, config}:配置快照(targets 只给 passwordSet / passphraseSet,另附只读的 ttyBooks / ttyAvailable / toolsRegistered / streamProtocol——最后这个是 SSE 帧协议版本,客户端据此决定「怎样才算已就绪」)
/config POST 上表配置键的任意子集 {ok:true, config};未知键 400,非法 JSON 400
/targets GET / POST — {ok:true, targets:[{name, kind, label?|error?}]}
/connect-local POST {}(请求体被忽略) 一键连接本机:复用或新建 kind=local 目标并返回 {ok:true, result:{name, kind, created, saved, reachable, serverVersion?, message}, config};宿主没有 settings 服务时 503,保存失败时 500(都带原因且不改内存配置)。要求同源证明(它是写路径),但不需要 allowMutations / allowExec
/probe POST {target?} {ok:true, probe:{ok, bin, serverVersion, error, target}}
/containers POST {target?, all?} {ok:true, containers: ContainerSummary[]};target:'*' 时返回 {ok:true, groups:[{target,label,ok,error?,data?}]}(跨目标并发聚合)
/attention POST {target?, limit?} 单目标 {ok:true, items, total, truncated, degraded}(limit 在过滤 + 严重度排序之后生效,默认 100、上限 500);target:'*' 时按目标分组
/inspect POST {target?, id} {ok:true, details: ContainerDetail[]}
/stats POST {target?, ids?: string[]} {ok:true, stats: ContainerStats[]}
/logs POST {target?, id, tail?, timestamps?, since?} {ok:true, logs:{id, text, truncated}}
/top POST {target?, id}(必填) {ok:true, top:{id, name, rows:[{pid, ppid, user, stat, elapsed, cpu, mem, command}], truncated}}——字段全是 docker top 打印的原文;未运行 / 不存在时走既有错误通道(不是 200 空列表)
/diagnose POST {target?, id}(必填)、logTail?(1500,默认 80)、since?(默认 1h)、history?(050,默认 10) {ok:true, diagnosis:{verdict, findings, evidence, timeline, logTail, suggestions, history:{samples, trend, recorded}}}。history 是本次记录之后的最近 N 条样本与趋势(recorded 说明本次有没有真的追加)。四路采集,旁路失败只降级(evidence 对应字段 null + *-unavailable 判据);非法 since 在引用白名单段就 400
/logs/stream GET query:target?、id(单容器,与 ids 二选一)或 ids(逗号分隔的多个容器,一条连接装下全部,最多 32 个)、tail?(0~5000;0 = 不补历史只跟随,客户端重连用)、timestamps?(1/true)、since? 200 text/event-stream 长连接,事件协议见下;参数错误 / 未知目标 / 非 loopback 返回常规 JSON 错误
/stats/stream GET query:target?、ids?(逗号分隔;省略 = 全部运行中) 200 text/event-stream:每秒一帧 stats(形状与 /stats 快照一致);不会自然结束,靠客户端断连收尾
/events/stream GET query:target? 200 text/event-stream:一帧 event 一个容器事件(服务端已过白名单);不会自然结束
/images POST {target?} {ok:true, images: ImageSummary[]}
/images/inspect POST {target?, ref}(必填) {ok:true, image:{ref, detail: ImageDetail, history: ImageHistoryEntry[], historyError}};history 优先 --format '{{json .}}',老版本退回纯文本表格
/images/remove POST {target?, ref}(必填) 需 allowMutations(否则 403);docker image rm(不带 -f);{ok:true, result:{ref, message}}
/images/prune POST {target?} 需 allowMutations(否则 403);docker image prune -f(只删 dangling);{ok:true, result:{message}}
/images/pull/stream GET query:target?、ref(必填) 需 allowMutations(否则 403 且不建流);200 text/event-stream:line 逐层进度 + end{reason:'pull-exit',code,ref}
/networks POST {target?} {ok:true, networks: NetworkSummary[]}
/networks/inspect POST {target?, name}(必填) {ok:true, network:{name, detail: NetworkDetail}}(含子网 / 网关 / 选项 / 标签 / 接入的容器)
/networks/remove POST {target?, name}(必填) 需 allowMutations(否则 403);docker network rm;{ok:true, result:{name, message}}
/networks/prune POST {target?} 需 allowMutations(否则 403);docker network prune -f;{ok:true, result:{message}}
/volumes POST {target?} {ok:true, volumes: VolumeSummary[]}
/volumes/inspect POST {target?, name}(必填) {ok:true, volume:{name, detail: VolumeDetail}}(挂载点 / 选项 / 标签)
/volumes/remove POST {target?, name}(必填) 需 allowMutations(否则 403);docker volume rm(数据随卷一起没);{ok:true, result:{name, message}}
/volumes/prune POST {target?} 需 allowMutations(否则 403);docker volume prune -f(删数据,见已知限制);{ok:true, result:{message}}
/system-df POST {target?} {ok:true, disk:{summary, images, volumes, buildCache, truncated, degraded, degradedReason}}:summary 四类汇总,明细各上限 100 行(先排序再截断);老 docker 只降级不报错
/run-command POST {target?, id}(必填) {ok:true, recreate:{id, name, command, warnings, secretKeys, omitted}}——只推导,不执行
/action POST {target?, action, id} 需 allowMutations(否则 403);{ok:true, result:{id, action, message}}
/exec POST {target?, id, command, timeoutSec?} 需 allowExec(否则 403);{ok:true, result:{id, command, code, stdout, stderr, truncated, durationMs}}

其余子路径 404(unknown route: ...);/config、/targets、上述四条 */stream 之外的 GET 返回 405; 执行失败(docker 报错、目标解析失败等)返回 500 或 400 加 {error} 文本。

SSE 事件协议(/logs/stream、/stats/stream、/events/stream、/images/pull/stream)

四条长流共用同一份基建(openSseStream):统一写头 + flushHeaders()、15s 一帧 : ping 心跳、活跃流 登记(插件禁用 / 配置热更新 / 卸载时统一 end + abort)、客户端断开静默中止,以及管道起来后先发一帧 ready。每帧一行 event: + 一行 JSON data:(单行封装:换行 / 引号被转义,多字节字符不会被 SSE 行 边界截断),随后空行:

事件 data 说明
ready {} 执行器真的起来了才发一帧(本机子进程 spawn / 远端通道已开 = StreamHandlers.onStart):客户端据此才认为「在收数据」。响应头到达(onopen)不算——响应头由 flushHeaders() 立刻写出去,而建连(SSH acquire / 远端开通道)可能卡十几秒。起不来就没有 ready
line {"d":"..."} / {"e":"..."};多源时 {"id":"<容器 id>","d"|"e":"..."} stdout / stderr 分片(不保证按行切,客户端自行拼行)。日志流与拉取流用。ids= 多源时每帧带 id,客户端按它把分片派回对应容器
source-end {"id":"<容器 id>","code":N|null} 仅多源:该容器的 docker logs -f 自然退出(容器停止)。整条连接不关——其余容器照常推;end 要等所有源都结束
source-error {"id":"<容器 id>","message":"..."} 仅多源:该容器这一条子流失败。整条连接也不关,其余容器继续;客户端标注「部分出错」
stats ContainerStats 统计流专用:每秒每个容器一帧,字段与 /stats 快照完全一致。服务端按扁平 {...} 抽取(docker stats 即便 stdout 是管道也走 TTY 渲染器,帧里混着 ESC[H/ESC[K/ESC[J,按行解析会整行丢掉)并折叠同一采样的重复渲染
event {action, name, image, composeProject?, time?, exitCode?} 事件流专用:一帧一个容器事件(白名单外的事件行在服务端就丢了;值为 null 的字段省略)
end {"reason":"container-exit"|"stats-exit"|"events-exit"|"pull-exit","code":N, ...} 执行器自然退出。日志流附容器退出码(多源时取按 ids 顺序最后一个非 null 的退出码,各容器的真实退出码在各自的 source-end 里)、统计流 reason=stats-exit、事件流 reason=events-exit、拉取流附 ref
error {"message":"..."} 参数 / 执行失败,随后关闭;连接层断路不会发这个事件

多容器单连接(ids=,D168):聚合日志把 N 个容器塞进一条 SSE——宿主为每个容器各起一条 docker logs -f,帧里带 id 分流。这样聚合视图无论选几个容器都只占浏览器一条同源连接(旧实现是 每容器一条 EventSource,叠加活动条的事件流会吃满 D165 实测出来的 4 条常驻流)。单源 id= 的形状 逐字节未变:帧不带 id、不发 source-end/source-error。/config 的 streamProtocol 因此升到 3:客户端据此决定走单连接还是退回「每容器一条」。四条流的差异只有「执行器 + 结束原因」:

流 执行器 自然结束条件 关闭语义
/logs/stream docker logs --follow(ids= 时每个容器各一条) 容器停止(container-exit;多源时全部容器的流都结束) 前端关 FOLLOW / 切页 / 关面板
/stats/stream docker stats(无 --no-stream) 全部被统计容器退出(stats-exit) 前端主动断(这条流不会自己停)
/events/stream docker events(--filter type=container) daemon 侧流结束(events-exit) 切页 / 切目标 / 关面板
/images/pull/stream docker pull 拉取完成 / 失败(pull-exit) 前端离开拉取视图
  • 响应头:content-type: text/event-stream; charset=utf-8、cache-control: no-cache、 connection: keep-alive,写头后立即 flushHeaders()(宿主 gzip 对 text/event-stream 显式跳过)。
  • 心跳:每 15s 一帧 : ping 注释(SSE 规范里客户端忽略)。
  • 背压与溢出(D153):客户端消费不过来时帧进内存队列(每条流 ≤ 8MB);日志 / 拉取流溢出时丢最旧的 整帧、只留最新,并推一条 skip{frames,bytes}(skip 帧自身永不丢)——流不断;skip 排队期间再发生 丢弃时原地更新这条 skip 的账目(D156)。统计 / 事件流溢出时补 end{reason:output-limit} 收尾,由 客户端重连。另外 write() 返回 false 只代表「越过高水位」,帧不会被重写。
  • 分片合帧(D151):日志流与拉取流把上游分片按 50ms 窗口合并后再推(同一通道严格保序、stdout 先于 stderr、攒满 256KB 立刻推),所以 line 帧的 d / e 里可能有多行——客户端本来就按「任意分片」 切行。end 帧一定排在窗口里最后一批 line 之后。
  • 清理:客户端断开 → 立即中止执行器(本机 SIGTERM,2s 未退再 SIGKILL;SSH 关闭该 exec channel、 连接池连接保留复用),不写任何帧;插件禁用 / 配置热更新 / 卸载 → 服务端主动收尾(abort + end)。
  • SSH 的长流会占用连接池里的连接(busy 计数),空闲回收(120s)不会误杀;流结束后恢复回收。
  • 「已就绪」的判据(客户端侧):绿点 /「实时跟随中」只认宿主那帧 ready——响应头到了不算。此外每条 流还有一条「更硬的证据」兜底:事件流的第一条真实事件、日志流的第一行、统计流的第一个采样、拉取流的 第一帧进度。开发档错位(宿主进程还跑着旧代码、浏览器却读到新的 client.js)另有版本协商: /config 的 streamProtocol < 2 说明该宿主从不发 ready,客户端退回旧判据并只重开一次那条流。

安全模型

docker socket ≈ 目标主机的 root 权限。 能访问 daemon 就能挂载宿主目录、以特权模式起容器、读容器里的 密钥——因此本插件按「只读优先」设计:

  1. 默认只读。allowMutations 未开启时,/action、/images/remove、/images/prune、 /images/pull/stream 一律返回 403,面板的启停删 / 镜像删除 / 清理 / 拉取不可用,docker_action、 docker_image_remove、docker_image_prune、docker_image_pull 工具根本不注册;allowExec 未开启 时,/exec 返回 403,docker_exec 工具同样不注册。两个开关互相独立,必须在设置卡片由用户显式打开。 读取类路由(/logs/stream、/stats/stream、/events/stream、/images/inspect)不受这两个开关影响。 allowExec 只管本插件的「一次性」exec 通道(/exec 路由、docker_exec 工具、容器详情抽屉 「概览」页签的一次性命令框)——卡片上的**「终端」按钮跑的是 docker exec -it 交互式 shell,由 tty 承载、不经过这两个开关**,只读模式下也可用(它不是漏了门,是刻意留的进入通道,见「已知限制」)。
  2. 破坏性操作要复述后果。remove 映射为 docker rm(不带 -f),agent 公告要求执行前向用户 确认目标容器;运行中容器会报错并附「容器仍在运行:先停止再删除」的提示,不会静默强删。
  3. 凭证不落明文(建议)。password / passphrase 支持 env:NAME 凭据引用,值存在官方凭据存储 ($DSH_HOME/.credentials.yaml)里,避免明文写进 settings.yaml;agent 认证(SSH_AUTH_SOCK) 完全不落盘。配置快照只回「是否已设置」。
  4. 主机指纹 TOFU 钉扎。首次连接记录 sha256 指纹,之后必须一致,变更即拒绝连接(防中间人);tty 已 确认过的主机会被当作种子直接信任并复制进本插件的记录。TOFU 的固有边界是「首次若已遭遇 MITM,记下的 就是伪指纹」,以及按 host:port 只存一条(同主机多密钥类型可能误报变更,删除记录重连即可重新校准)。
  5. 命令一律 argv 构造,绝不做字符串拼接。容器名 / ID 先过白名单 assertRef ([A-Za-z0-9][A-Za-z0-9_.-]*,≤128 字符,拒绝空格、;、$()、反引号等),镜像与容器引用同理; 远程经 shJoin 逐参数单引号转义后交给远端 shell,本机 spawn(bin, args) 不经 shell。
  6. 输出有上限。maxOutputKb 限制单次命令的 stdout/stderr 字节数,超出截断并标记,避免大日志撑爆 内存或 agent 上下文。
  7. HTTP 只对本机开放。全部路由走 loopback 围栏,远程浏览器无法调用。
  8. 日志实时流是只读能力。GET /logs/stream 与 /logs 一致:不受 allowMutations / allowExec 门控 (它不改容器状态),但同样只放行 loopback、id 过 assertRef 白名单、参数经同样的夹紧。与快照不同, 长流没有 maxOutputKb 上限(跟随被截断就失去意义),内存防护由客户端的 5000 行环形缓冲承担。
  9. 变更操作有逐条使用审计。kit elevation 的审计行回答「授权了」,这里回答「用了」:每一次过了 能力闸的变更 / exec 都落一行 [dsh-docker] capability-use: … 结构化审计行(出口是宿主 stdout), 面板路由与 agent 工具两条入口都覆盖。行格式、不记什么(403 / tier-gate ask)与「exec 命令会随行 出现」这条代价,见「已知限制」的使用审计条。

已知限制

  • 没有交互式 TTY:exec 是一次性命令(docker exec <id> sh -c <cmd>,不带 -i / -t),不能跑 vim / top / 交互式 shell,也不能喂 stdin 做对话。交互排障请到 tty 面板执行 docker exec -it <容器> sh。
  • allowExec 不是「进不了容器」的闸:卡片「终端」按钮(docker exec -it '<容器>' sh)经 tty 的 ttyTerminal 承载,不检查 allowExec,只读模式下 也照常可用——exec 被关时面板给的提示原文就是这么说的(「打开『允许 exec』,或 直接复制卡片上的 exec 命令到终端面板交互式进入容器」)。这是刻意的:只读巡检时 进容器看现场是最常用的一条路径,闸门管的是本插件自己那条一次性通道 (/exec 与 docker_exec:改本地状态、可被 agent 工具批量调用),交互式 shell 归 tty(用户主动操作、它自己才是 PTY 的所有者)。代价要说清:装上 tty 之后, allowExec 对页内脚本(第三方插件的 client 半体 / XSS)本来就不是边界——它们 走 ttyTerminal.open 或 WS spawn 帧就能跑任意命令,比本插件的一次性 exec 还强。 这一层的威胁模型边界见 architecture.md § 7。
  • 事件流有断线窗口:docker events 是「从现在开始」的推流,断线重连期间发生的事件服务端已经推过、不会 补发。客户端用「重连成功后先做一次全量列表刷新」来补偿(对齐状态,不是把事件补回来);要精确的完整事件 历史请用 docker_events 工具(带 --since 的快照)。
  • 浏览器同源连接预算是硬约束,docker 面板最多只占 1 条常驻流(D165 实测 + D168):宿主是 HTTP/1.1(没有 h2 多路复用),而每条 EventSource 独占一条连接、且不会主动释放。吃满之后,同一 个页面里所有普通请求(列表刷新、/api/*、会话加载……)一起排队——用户看到的是整个 DSH 页面 卡住,不只是 docker 面板。docker 面板的常驻流在任何视图下都只有 1 条:列表 = 活动条事件流;详情 = 日志 / 统计 FOLLOW(同一时刻只有一个页签);聚合 / Compose 日志 = 一条 ids= 多容器流;拉取 = 一条 进度流(进入详情 / 聚合时事件流主动断开,回列表重连并补一次全量刷新)。超了不是「慢一点」,是同页面所有 请求一起失效;完整实测数据见 DEFECTS.md 的 D165,复核方式是在 devtools 里连开 new EventSource('/api/dsh-docker/events/stream?target=<目标>') 到第 5 条,再打一次普通请求。 (WebSocket 不占这份预算;流式日志 / 统计在浏览器侧的保留量见「使用」。)
  • 多标签页与聚合上限:那些连接是整个页面共享的(官方 dsh-client-hmr、其它插件的 SSE 都算);治本要 宿主换 WebSocket / h2,或各插件共用一条多路复用连接。聚合上限 3 现在是「可读性 + SSH 8 条通道额度」的 上限,不再由浏览器预算推出(D168):一屏混三份以上日志会看不清,且每个被选容器在宿主侧仍各跑一条 docker logs -f(SSH 目标上就是一条 channel)。PICK_MAX = 3 即此值。
  • SSH 目标上的通道额度是共享的(MaxSessions):一个目标只维持一条 TCP 连接,长流与短命令共用 这条连接上的通道,而 OpenSSH 的 MaxSessions 默认只有 10。长流会一直占到用户关掉面板为止,聚合日志里 每个被选容器仍各占一条 channel——叠上刷新 / inspect 这类短命令就可能逼近额度,远端拒绝时看到的是 (SSH) Channel open failure: open failed。插件对每个 SSH 目标限制同时最多 8 条长流(源码常量 MAX_STREAMS_PER_TARGET,即 10 − 2,留两 条给短命令),聚合上限也定在 3;折叠右侧栏标签会主动断流、把通道还回去。(D150)两道补救:① 短命令 按目标排队(每条连接同时最多 2 条并发,DSH_DOCKER_SHORT_CHANNELS 可覆盖,仅测试 / 排障);② 额度 满会自愈——远端回 no more sessions 这类拒绝时,插件丢掉并关闭那条连接、重建一条再重试一次,宿主 日志留一行 通道额度已满:已重建连接重试(这条连接不会自己恢复)。(D164)③ 中止长流时插件自己 的账当场结清—— AbortSignal 触发时除了向远端 KILL + 关通道,还会直接结束这条流(原先只等远端 close,远端不确认时那条流的 busy 计数永远不会减,累积到 8 之后该目标的每条长流都被拒绝且没有 自愈路径)。根治办法在 sshd 一侧:把那台主机的 MaxSessions 调大——例如 /etc/ssh/sshd_config.d/10-dsh-maxsessions.conf 写一行 MaxSessions 50,再 sshd -t && systemctl reload ssh(reload 只影响新连接)。当前上限是编译期常量,需要跟着改就提 issue。
  • docker CLI 版本差异:解析走 --format '{{json .}}',字段随版本增减,解析器一律降级而不抛异常 (例如 State 缺失就从 Status 推导状态);缺字段时对应列可能为空,权威数据请用 docker inspect。
  • rm 不带 -f:容器 remove 映射 docker rm、镜像 remove 映射 docker image rm,都不加 -f ——运行中的容器、被引用的镜像会失败并给出提示;要强制删除得去 tty 面板手动执行。
  • SSH 目标需要免 sudo 的 docker:账号不在 docker 组时权限错误原样透出,不做自动 sudo 提权。 远端未安装 docker 时 probe 失败(退出码 127),PATH 不一致时可把 dockerBin 填成绝对路径。
  • dockerBin 的校验发生在落盘之前:非法值直接 400 并带上原因,不会被写进 settings.yaml。 指向 .cmd / .bat 时本机通道起不来:本机执行走裸 spawn,Windows 上 Node 会拒绝直接执行 .cmd (EINVAL)——手写包装脚本请改用 .exe,或等待本机通道也切到 kit 的 spawnPortable。
  • Compose 项目视图只认 com.docker.compose.project:docker stack deploy 起的 swarm 服务带的是 com.docker.stack.namespace / com.docker.swarm.service.name,于是这些容器显示为「未分组」。要支持得 另立一套 stack 分组语义。
  • volume prune 的回收范围随版本变,且可能删数据:Docker 29 上 docker volume prune -f 只回收匿名 未用卷(命名卷留着);而 docker ≥ 23 有 -a/--all,docker < 23 没有这个开关、plain prune 会把未被 使用的命名卷一起删掉。本插件就不带 --all,确认文案写明了版本差异;要删命名卷请用 /volumes/remove。
  • podman 兼容靠 dockerBin:填 podman 即可跑,但 stats 与 --format '{{json .}}' 的字段和输出 格式与 docker 有差异,只能依赖解析器的降级路径,未逐项验证。
  • 没有镜像构建 / Compose 编排变更:镜像支持拉取 / 删除 / 清理 dangling,但没有 docker build、 docker save / load、docker push;Compose 是只读项目视图。网络 / 卷的变更只有面板按钮, 没有 agent 工具——刻意不扩大 agent 侧的变更面。
  • 诊断是判据,不是专家:这些判据覆盖的是「容器自报的客观证据」(退出码 / OOM / 重启次数 / 健康检查 / 资源压力 / 日志里的报错行),不理解业务。「这个报错是否可忽略」那一步交给「交给 Agent」之后的人或 模型。代价:诊断一次跑四条只读命令,在 SSH 目标上就是最多四条 channel(短命令闸把并发压到 2,所以是 两批),慢机器上会感觉到等。
  • 事件采集给的是独立预算(8MB + 留尾):单容器一小时的事件里 exec_create / exec_start / exec_die 这类三连占绝大多数,按通用的 512KB 档会让时间线永远为空。所以诊断的事件采集单独放宽到 8MB 并保留尾部——窗口内事件特别多的容器,时间线只覆盖最近的一段。
  • 等价 docker run 是近似,不是备份:compose 管理的容器不带网络别名 / labels / depends_on; --privileged / --link / ENTRYPOINT 覆盖 / 自定义健康检查 / 匿名卷等故意不还原,逐条列在 omitted 里。它不能替代 docker commit / 卷备份。
  • 磁盘明细的两处截断各自独立:宿主侧每类 100 行(truncated)、工具侧 20 行;真机上卷多到几千个时, system df -v 的输出本身也可能撞 8MB 预算(那时 degraded: true,汇总仍然可用)。「合计可回收」由 字符串解析成字节再相加——解析不出就整条显示 —。
  • 总览只汇总「容器概况」,且完全只读:只做计数卡与异常置顶表,没有任何跨目标操作,也不聚合日志 / 统计 / 事件流。另外「异常」只含 unhealthy 与 restarting:ContainerSummary 里没有 exitCode, 无法区分「崩溃退出」与「人工停掉」——死掉的容器请在计数卡的「已停止」里看。
  • 连接栏按钮需要一条匹配的目标才有数据:按钮在 SSH 标签上一律显示,但若会话主机没有对应的 kind=ssh 目标(连接簿名或 host:port 都匹配不上),点开只会看到「尚未配置为 Docker 目标」的提示;本地标签的 连接栏本身隐藏。目标增删后最多 30 秒内刷新(设置卡片保存会立即刷新)。
  • enabled: false 是热生效:保存后工具立即注销、公告撤下、除 /config 外的数据路由 403(进行中的长流 也会被立即收尾)。路由对象本身不卸载、靠 403 拦截;/config 始终可读写,设置卡片就是重新启用的入口, 无需重启 dsh web。
  • 使用审计走宿主 stdout,exec 命令会随行出现:每一次过了能力闸的变更 / exec(面板路由与 agent 的 docker_action / docker_image_remove / docker_image_prune / docker_image_pull / docker_exec; 403 与参数校验 400 不记,「使用 = 过了闸」)落一行 [dsh-docker] capability-use: capability=allowMutations|allowExec source=http|tool action=… target=… ref=… ok=true|false durationMs=…,key=value 风格与 kit elevation 的授权行一致;pull 进度流拆 start / end 两行。出口是 console.log(D162:真机实测 ctx.logger 的行在 0.2.x 宿主上既不进 stdout 也不落盘)——跑 dsh web 的那个终端里就能看到、可 grep。代价:exec 的命令会记进行尾 detail(连同退出码),整段 截断 200 字符、控制字符转义成字面量——行是给人看的,就得接受它出现在终端可见的日志流里。docker 报错也 算一次使用(ok=false);tier-gate 的 ask / deny 不记。没有查询界面、没有独立审计文件。
  • 跨目标聚合的边界:并发上限 4;单目标预算 90s(兜底),而每条命令自己的超时(30s)才是被遵守的 那个——到点时底层的子进程 / SSH 通道会被真的取消,否则超时那条命令会继续占着远端会话槽(D161)。 单个目标失败/超时只影响它自己那一格。目标很多时总览的请求量线性增长(每目标 2 个请求),自动刷新会放大 这个量。

工作原理

浏览器半体 (client.js)
  ├─ 侧边栏「容器」入口 → 面板(目标选择 / 容器列表 / 卡片动作 / 五页签详情 / 多目标总览 /
  │   Compose · 镜像 · 网络 · 卷 · 磁盘各页)与终端抽屉(ttyTerminal.mount)
  ├─ 四条 SSE(/logs/stream 含 ids=、/stats/stream、/events/stream、/images/pull/stream)
  │   + 普通 fetch → /api/dsh-docker/*(loopback 围栏;写路由另需同源证明)
  └─ 可选消费 tty 的 ttyConnbar / ttyPanel / ttyTerminal(未装或版本不足逐项降级)

宿主半体 (src/index.ts)
  ├─ settings 命名空间 docker → normalizeConfig(夹紧 / 白名单键)
  ├─ 只读复用 tty settings 的 sshHosts(连接簿)与 hostKeys(指纹种子)
  ├─ resolveTarget → 每目标一个 DockerApi(src/docker.ts)+ RemoteExec(src/ssh-exec.ts):
  │   argv 构造 + assertRef + 输出上限;懒连接池 / 非 PTY exec channel / shJoin / TOFU
  ├─ openSseStream:四条流共用一份基建(ready / 心跳 / 活跃流登记 / 背压 / 统一收尾)
  └─ 路由 + agent 工具(读类恒注册,写 / exec 类按开关注册)——清单见上面两张表

开发与验收

pnpm --filter @hyzyn/dsh-docker build       # tsc → lib/(宿主半体)+ esbuild → client.js(浏览器半体)
pnpm --filter @hyzyn/dsh-docker typecheck
pnpm --filter @hyzyn/dsh-docker smoke       # 三套离线回归,都不需要 docker daemon(项数以脚本尾部自报为准)
pnpm test                                    # 仓库级 vitest(本包 test/ 下的用例都在里面;项数以运行输出为准)

改了哪一半、怎么才生效:client-src/*(浏览器半体)→ esbuild 出 client.js,宿主有 client HMR 轮询,热更、刷新页面即见;src/*.ts(宿主半体)→ tsc 出 lib/,必须是新进程才生效(运行中的 dsh web 在启动时就把 lib/ 载进内存),改完 pnpm build 记得重启。忘了重启的症状是客户端是新的、 宿主是旧的:宿主侧新增的提示语没出现,那就是旧进程。

验证面分三层:三套离线 smoke(不需要 docker daemon)、本包的 vitest 套件、以及真 daemon 的 手工验收清单。逐项的覆盖范围、浏览器侧性能门槛与手工步骤见 docs/verification.md——README 只留「怎么跑」,清单不在这里重复。

版本 / 许可证

@hyzyn/dsh-docker(版本以 package.json 为准) · Apache License 2.0

—/ 5

No ratings yet

Verified DSH bundle

npm 0.16.1

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout