Conversation
There was a problem hiding this comment.
Pull request overview
该 PR 将 MineDock 扩展为“模板驱动创建 + SQLite 持久化 + 实时状态同步 + 在线控制台 + 配置读取/重建”的完整闭环,并同步补齐前端页面、i18n 与文档/API 合约。
Changes:
- 后端新增 GameService(games.json + YAML 模板)与实例元数据(game_id)持久化,并补齐创建/配置读写/重建相关 API。
- 后端新增基于 Docker Events 的 EventHub + WebSocket 推送,以及容器控制台 WebSocket 桥接。
- 前端新增模板市场/创建/详情(控制台+配置)页面,接入实例 WebSocket 同步、xterm 控制台,并更新路由与多语言文案。
Reviewed changes
Copilot reviewed 61 out of 63 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| Readme.md | 更新待办结构,反映模板/实例/交互/运维规划拆分 |
| frontend/vite.config.js | 调整 dev server host/HMR 与代理 WS 配置 |
| frontend/src/views/InstanceDetail.vue | 新增实例详情页(控制台/配置 Tab)与控制台挂载 |
| frontend/src/views/InstanceConfig.vue | 新增实例配置读取与重建提交 UI(params/ports) |
| frontend/src/views/ImageRegistry.vue | 新增模板市场页(分类筛选、卡片入口) |
| frontend/src/views/CreateInstance.vue | 新增模板驱动的实例创建表单(params/ports) |
| frontend/src/views/ContainerList.vue | 接入实时同步、卡片跳转详情、创建入口改为市场页 |
| frontend/src/stores/games.ts | 新增 games store:列表缓存与模板加载 |
| frontend/src/stores/containers.ts | 扩展 create 入参、WS 状态标记、快照覆盖与稳定排序 |
| frontend/src/router/index.ts | 新增 /registry、/registry/:gameId/create、/instances/:id 路由 |
| frontend/src/locales/zh-CN.json | 新增控制台/配置/市场/同步状态等中文文案 |
| frontend/src/locales/en-US.json | 新增控制台/配置/市场/同步状态等英文文案 |
| frontend/src/composables/useInstanceSync.ts | 新增 WS 实时同步 + 断线轮询降级 + 退避重连 |
| frontend/src/composables/useConsole.ts | 新增 xterm 控制台:WS 双向收发、Fit/ResizeObserver |
| frontend/src/components/TopBar.vue | 新增实时同步连接指示点并保留语言切换 |
| frontend/src/components/Sidebar.vue | 新增“游戏模板/市场”导航入口与移动端样式调整 |
| frontend/src/api/index.ts | 新增 games/template/config API、WS URL 构造、类型扩展 |
| frontend/package.json | 增加 xterm 相关依赖 |
| frontend/package-lock.json | 锁定新增依赖版本 |
| docs/standards/ops.md | 更新后端环境变量:games/模板目录 |
| docs/standards/frontend.md | 更新路由说明(新增市场/详情) |
| docs/standards/directory.md | 更新目录结构说明(games.json/templates) |
| docs/exec-plans/completed/20260401-realtime-status-sync.md | 记录实时状态同步方案与落地步骤(完成) |
| docs/exec-plans/completed/20260331-static-image-registry.md | 历史计划文档归档(完成) |
| docs/exec-plans/completed/20260331-image-marketplace-page.md | 历史计划文档归档(完成) |
| docs/exec-plans/active/20260402-container-ports-volumes.md | 在途计划文档:端口/卷挂载实现路线 |
| docs/exec-plans/active/20260402-container-console.md | 在途计划文档:控制台实现路线 |
| docs/exec-plans/.markdownlint.json | exec-plans 子目录 markdownlint 配置 |
| docs/design-docs/instance_lifecycle.md | 补齐 game_id、配置重建、WS 同步、控制台交互策略 |
| docs/api/contracts.md | 新增 games/template/ws/config 合约与更新 POST/PUT 语义 |
| backend/templates/terraria.yaml | 新增 Terraria 模板(ports/volumes/resources/params) |
| backend/templates/minecraft-java.yaml | 新增 Minecraft Java 模板(env/health_check/params) |
| backend/templates/minecraft-bedrock.yaml | 新增 Minecraft Bedrock 模板(udp port/params) |
| backend/main.go | 注入 GameService/EventHub/WS/Console/Config handler 并启动 hub |
| backend/internal/store/sqlite.go | instances 表新增 game_id 列并迁移;Save/Get/List/Upsert 扩展 |
| backend/internal/store/sqlite_test.go | SQLite store 测试补齐 game_id 断言与 upsert 覆盖 |
| backend/internal/service/game_service.go | 新增 games.json 加载 + YAML 模板读取与结构校验 |
| backend/internal/service/game_service_test.go | 新增 GameService 单测(加载/缺模板/模板校验) |
| backend/internal/service/event_hub.go | 新增 Docker Events 监听、快照去重、WS 广播、退避重连 |
| backend/internal/service/event_hub_test.go | EventHub 单测:增删 client、快照去重广播 |
| backend/internal/service/console_service.go | 新增容器 attach 服务(运行态校验 + attach) |
| backend/internal/service/console_service_test.go | ConsoleService 单测:运行/停止/不存在容器 |
| backend/internal/model/template.go | 新增模板领域模型(ports/volumes/resources/params 等) |
| backend/internal/model/instance.go | Instance 增加 game_id 字段 |
| backend/internal/model/game.go | 新增 Game 领域模型 |
| backend/internal/model/errors.go | 新增 game/template/params/config 相关领域错误 |
| backend/internal/api/ws_handler.go | 新增 /api/ws/events handler(同源 WS) |
| backend/internal/api/ws_handler_test.go | WsHandler 单测:hub 不可用、Add/Remove 调用 |
| backend/internal/api/router.go | 路由注册扩展(games/ws/console/config)+ CORS 方法更新 |
| backend/internal/api/handlers.go | CreateInstance 请求体扩展(game_id/params/ports)+ 错误映射 |
| backend/internal/api/handlers_test.go | Handler 测试适配新创建签名 + games/template 场景覆盖 |
| backend/internal/api/game_handlers.go | 新增 games 列表与模板获取 API handler |
| backend/internal/api/console_handler.go | 新增 /api/ws/console/:id handler(WS ↔ Docker attach 双向桥接) |
| backend/internal/api/config_handler.go | 新增 GET/PUT /instances/:id/config handler |
| backend/internal/api/config_handler_test.go | ConfigHandler 单测:get/update/非法 JSON/冲突状态码 |
| backend/go.sum | 引入 coder/websocket 等依赖校验和 |
| backend/go.mod | 新增 coder/websocket 依赖与 yaml 依赖记录 |
| backend/games.json | 新增游戏目录索引数据(market 列表来源) |
| .markdownlint.json | 根目录 markdownlint 配置 |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| socket.onclose = (event: CloseEvent) => { | ||
| if (disposed) { | ||
| return; | ||
| } | ||
| connected.value = false; | ||
| if (event.reason) { | ||
| error.value = event.reason; | ||
| return; | ||
| } | ||
| if (event.code !== 1000) { | ||
| error.value = "console.disconnected"; | ||
| } | ||
| }; |
There was a problem hiding this comment.
useConsole sets error.value = "console.disconnected" on abnormal WebSocket close, but the i18n dictionaries only define console.disconnectedLabel (no console.disconnected). This will surface a raw key or fallback text in the UI. Consider reusing the existing console.disconnectedLabel key, or add a dedicated console.disconnected translation in both locales.
| func pipeDockerToWebSocket( | ||
| ctx context.Context, | ||
| conn *websocket.Conn, | ||
| hijacked types.HijackedResponse, | ||
| ) error { | ||
| writer := &wsBinaryWriter{ctx: ctx, conn: conn} | ||
| _, err := io.Copy(writer, hijacked.Reader) | ||
| if errors.Is(err, io.EOF) { | ||
| return nil | ||
| } | ||
| return err | ||
| } |
There was a problem hiding this comment.
Console WebSocket 输出目前直接 io.Copy 了 ContainerAttach 的 hijacked.Reader 到前端。但当容器未开启 TTY 时,Docker attach 输出是 stdcopy 多路复用格式(stdout/stderr 带 8-byte header),前端 xterm 会出现乱码。建议在 Attach 前读取容器 Config.Tty:TTY=true 时可直传;TTY=false 时应使用 stdcopy.StdCopy(或等价解复用)后再写入 WebSocket。
| for i := range tpl.Container.Volumes { | ||
| tpl.Container.Volumes[i].Name = strings.TrimSpace(tpl.Container.Volumes[i].Name) | ||
| tpl.Container.Volumes[i].ContainerPath = strings.TrimSpace(tpl.Container.Volumes[i].ContainerPath) | ||
| if tpl.Container.Volumes[i].ContainerPath == "" { | ||
| return fmt.Errorf("template %q volume index %d container_path is required: %w", gameID, i, model.ErrTemplateInvalid) | ||
| } | ||
| } |
There was a problem hiding this comment.
normalizeTemplate 对 container.volumes 仅校验了 container_path,但没有强制要求 volumes[i].name 非空。后续卷命名/挂载通常依赖该字段(例如 minedock-{instanceName}-{volumeName}),name 为空会导致不可读/冲突的卷名。建议在此处将空 name 视为 ErrTemplateInvalid 并返回明确错误。
| function buildPortsPayload(): PortMapping[] { | ||
| return ports.value.map((port) => ({ | ||
| host: Number(port.host), | ||
| container: Number(port.container), | ||
| protocol: port.protocol, | ||
| })); | ||
| } |
There was a problem hiding this comment.
buildPortsPayload() 使用 Number(port.host/container) 直接转换并提交端口;当输入框被清空或包含非数字时会转换成 0/NaN(JSON.stringify(NaN) 会变成 null),从而向后端发送不可用端口值。建议在提交前对每个端口做显式校验(整数且在有效范围内),并在不合法时阻止保存/给出错误提示。
| function getCreatePortsPayload(): PortMapping[] { | ||
| return ports.value.map((port) => ({ | ||
| host: Number(port.host), | ||
| container: Number(port.container), | ||
| protocol: port.protocol, | ||
| })); | ||
| } |
There was a problem hiding this comment.
getCreatePortsPayload() 使用 Number(port.host/container) 直接生成端口 payload;当用户清空输入或输入非法字符时会得到 0/NaN(序列化后可能变成 null),导致创建请求携带无效端口。建议在创建前校验端口为有效整数范围,并在不合法时阻止提交/提示用户修正。
| "@xterm/addon-attach": "^0.11.0", | ||
| "@xterm/addon-fit": "^0.10.0", | ||
| "@xterm/xterm": "^5.5.0", |
There was a problem hiding this comment.
新增了 @xterm/addon-attach 依赖,但当前代码中没有任何引用(控制台实现也未使用 AttachAddon)。这会增加包体积与依赖维护成本。若短期内不会用到,建议移除该依赖;若计划使用,请在实现中落地并补充对应引用。
| - 当前路由: | ||
| - `/` 映射容器列表页(`ContainerList.vue`) | ||
| - `/registry` 映射游戏模板市场页(`ImageRegistry.vue`) | ||
| - `/instances/:id` 映射容器详情页(`InstanceDetail.vue`) |
There was a problem hiding this comment.
docs/standards/frontend.md 的“当前路由”列表遗漏了本 PR 新增的创建页路由 /registry/:gameId/create(CreateInstance.vue)。建议补充该路由项,避免文档与实际 router 配置不一致。
| <div | ||
| v-for="item in store.instances" | ||
| :key="item.container_id" | ||
| class="card" | ||
| @click="openInstanceDetail(item.container_id)" | ||
| > |
There was a problem hiding this comment.
容器卡片使用 <div class="card" @click=...> 作为可点击入口,但没有 role="button" / tabindex="0" 以及键盘触发(Enter/Space)支持,键盘用户无法打开详情页。建议将卡片改为 <button>/<RouterLink>,或补齐可访问性属性与键盘事件处理。
| <header class="page-header"> | ||
| <div class="header-left"> | ||
| <button class="back-btn" @click="backToList"><</button> | ||
| </div> |
There was a problem hiding this comment.
返回按钮仅显示符号“<”,缺少可读的可访问性名称(screen reader 会读成“less-than”)。建议为按钮添加 aria-label(可复用已有 i18n key 如 console.back),并可考虑用图标 + 文案提升可用性。
变更概览
本 PR 将 MineDock 从“基础容器增删启停”扩展为“模板驱动创建 + 实时状态同步 + 在线控制台 + 配置重建”的完整链路,同时补齐文档体系与任务流水线。
主要改动
API 变化
行为说明与注意事项
影响范围