Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: task frontend:install

- name: Run formatting checks
run: task fmt
run: task fmt:check

- name: Run lint checks
run: task lint
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
run: task frontend:install

- name: Run frontend format checks
run: task frontend:fmt
run: task frontend:fmt:check

- name: Run frontend lint checks
run: task frontend:lint
Expand Down
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 文档目录

[API](docs/api/contracts.md)

## 规范

[目录规范](docs/standards/directory.md)
[后端规范](docs/standards/backend.md)
[前端规范](docs/standards/frontend.md)
[运维规范](docs/standards/ops.md)

## 设计

[容器实例设计](docs/design-docs/instance_lifecycle.md)

## 执行计划

[执行计划目录说明](docs/exec-plans/README.md)
15 changes: 4 additions & 11 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,34 @@

## 运行指南

### Task 命令
```bash
task dev # 一键启动前后端开发服务
task fmt # 执行全局格式检查(当前依赖 backend:fmt)
task vet # 执行全局静态检查(当前依赖 backend:vet)
task test # 执行全局测试(当前依赖 backend:test)
task build # 统一编译前后端
task clean # 清理构建产物
task frontend:install # 安装前端依赖
```

## 待办清单

### 实例生命周期与资源调度

- [x] 开服与停服
- [x] 实例的创建与删除
- [ ] 实例重启与强制结束进程
- [ ] 基于 Cgroups 的 CPU 与内存资源配额可视化配置

### 实时交互与性能监控

- [ ] Web 控制台标准输出日志实时推流
- [ ] 网页端在线交互指令无缝下发
- [ ] 容器 CPU、内存、网络 I/O 运行态数据实时图表展示

### 数据灾备与持久化

- [ ] 游戏容器 Volume 持久化目录挂载映射
- [ ] 游戏存档/数据一键手动快照备份
- [ ] 基于 Cron 表达式的自动化定时备份任务
- [ ] 一键回档功能

### 在线文件管理与差异化配置

- [ ] 可视化 Web 文件浏览器
- [ ] 游戏基础配置文件的在线文本编辑器
- [ ] 大文件 (Mod/插件) 上传及在线解压缩

## 注意事项
默认 SQLite 数据库路径为 `backend/data/minedock.db`(在 `backend` 目录启动时对应 `data/minedock.db`)。
可通过环境变量 `MINEDOCK_DB_PATH` 覆盖。
79 changes: 74 additions & 5 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tasks:
cmds:
- go run main.go

backend:fmt:
backend:fmt:check:
desc: Verify backend Go formatting
dir: backend
cmds:
Expand All @@ -21,12 +21,24 @@ tasks:
exit 1
fi

backend:fmt:fix:
desc: Format backend Go files with gofmt
dir: backend
cmds:
- gofmt -w .

backend:vet:
desc: Run backend go vet checks
dir: backend
cmds:
- go vet ./...

backend:lint:
desc: Run backend golangci-lint checks
dir: backend
cmds:
- go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 run --config .golangci.yml ./...

backend:test:
desc: Run backend tests
dir: backend
Expand Down Expand Up @@ -64,12 +76,59 @@ tasks:
cmds:
- npm run lint

frontend:fmt:
frontend:fmt:check:
desc: Run frontend Prettier format checks
dir: frontend
cmds:
- npm run format:check

frontend:fmt:fix:
desc: Format frontend files with Prettier
dir: frontend
cmds:
- npm run format

docs:todo:
desc: Generate docs/exec-plans/TODO.md from TODO comments
cmds:
- go run ./scripts/todo-gen/main.go -root . -out ./docs/exec-plans/TODO.md

docs:lint:rules:
desc: Run Markdown rule checks for the knowledge base
cmds:
- npx --yes markdownlint-cli2 "AGENTS.md" "Readme.md" "docs/**/*.md"

docs:lint:rules:fix:
desc: Auto-fix Markdown rule violations where possible
cmds:
- npx --yes markdownlint-cli2 --fix "AGENTS.md" "Readme.md" "docs/**/*.md"

docs:links:check:
desc: Check Markdown links for the knowledge base
cmds:
- npx --yes markdown-link-check -q "AGENTS.md" "Readme.md" "docs"

docs:lint:
desc: Run documentation lint checks
deps:
- docs:lint:rules
- docs:links:check

docs:lint:fix:
desc: Auto-fix documentation lint issues where possible
deps:
- docs:lint:rules:fix

docs:fmt:check:
desc: Run Markdown formatting checks
cmds:
- npx --yes prettier --check "AGENTS.md" "Readme.md" "docs/**/*.md"

docs:fmt:fix:
desc: Format Markdown documents with Prettier
cmds:
- npx --yes prettier --write "AGENTS.md" "Readme.md" "docs/**/*.md"

dev:
desc: Run backend and frontend dev servers in parallel
deps:
Expand All @@ -87,16 +146,26 @@ tasks:
cmds:
- rm -rf backend/bin frontend/dist

fmt:
fmt:check:
desc: Run global formatting checks
deps:
- backend:fmt
- frontend:fmt
- backend:fmt:check
- frontend:fmt:check
- docs:fmt:check

fmt:fix:
desc: Run global formatting fixes
deps:
- backend:fmt:fix
- frontend:fmt:fix
- docs:fmt:fix

lint:
desc: Run global lint checks
deps:
- backend:lint
- frontend:lint
- docs:lint

vet:
desc: Run global vet checks
Expand Down
17 changes: 17 additions & 0 deletions backend/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
run:
timeout: 5m
tests: true
modules-download-mode: readonly

linters:
disable-all: true
enable:
- govet
- staticcheck
- ineffassign
- unused
- gosimple

issues:
max-issues-per-linter: 0
max-same-issues: 0
59 changes: 44 additions & 15 deletions backend/internal/api/handlers.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
package api

import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"

"minedock/backend/internal/service"
"minedock/backend/internal/store"
"minedock/backend/internal/model"
)

// Handler exposes HTTP handlers.
// InstanceService 定义 API Handler 依赖的业务操作。
type InstanceService interface {
ListInstances(ctx context.Context) ([]model.Instance, error)
CreateInstance(ctx context.Context, name string) (string, error)
StartInstance(ctx context.Context, containerID string) error
StopInstance(ctx context.Context, containerID string) error
DeleteInstance(ctx context.Context, containerID string) error
}

// Handler 暴露 HTTP 处理器。
type Handler struct {
svc *service.DockerService
svc InstanceService
}

func NewHandler(svc *service.DockerService) *Handler {
// NewHandler 使用给定的实例服务创建 Handler。
func NewHandler(svc InstanceService) *Handler {
return &Handler{svc: svc}
}

// createRequest 定义创建实例请求体。
type createRequest struct {
Name string `json:"name"`
}

// statusResponse 定义通用状态响应体。
type statusResponse struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
}

// createResponse 定义创建成功时的响应体。
type createResponse struct {
Status string `json:"status"`
ContainerID string `json:"container_id"`
}

// GetInstances 处理 GET /api/instances 并返回所有托管实例。
func (h *Handler) GetInstances(w http.ResponseWriter, r *http.Request) {
instances, err := h.svc.ListInstances(r.Context())
if err != nil {
Expand All @@ -41,6 +56,7 @@ func (h *Handler) GetInstances(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, instances)
}

// CreateInstance 处理 POST /api/instances 并创建新实例。
func (h *Handler) CreateInstance(w http.ResponseWriter, r *http.Request) {
var req createRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Expand All @@ -56,17 +72,16 @@ func (h *Handler) CreateInstance(w http.ResponseWriter, r *http.Request) {

id, err := h.svc.CreateInstance(r.Context(), req.Name)
if err != nil {
code := http.StatusInternalServerError
if err == store.ErrNameExists {
code = http.StatusConflict
}
writeJSON(w, code, statusResponse{Status: "error", Error: err.Error()})
writeJSON(w, mapErrorCode(err), statusResponse{Status: "error", Error: err.Error()})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handlers return err.Error() directly in the JSON body even when the status code is derived from errors.Is(...) (e.g., ErrNameExists/ErrInstanceRunning). This makes the client-facing error field unstable when upstream layers wrap errors, and may leak internal context strings. Prefer emitting a stable, canonical message for known domain errors (e.g., use the sentinel’s Error() value or a fixed string) while still keeping wrapped errors for server-side logging.

Suggested change
writeJSON(w, mapErrorCode(err), statusResponse{Status: "error", Error: err.Error()})
status := mapErrorCode(err)
msg := http.StatusText(status)
if msg == "" {
msg = "error"
}
writeJSON(w, status, statusResponse{Status: "error", Error: msg})

Copilot uses AI. Check for mistakes.
return
}

writeJSON(w, http.StatusOK, createResponse{Status: "success", ContainerID: id})
}

// TODO: 抽取 start/stop/delete 的公共处理流程。
// 这三个处理器都重复了:解析容器 ID、调用 Service、写回状态 JSON。
// StartInstance 处理 POST /api/instances/{id}/start 并启动实例。
func (h *Handler) StartInstance(w http.ResponseWriter, r *http.Request) {
id, ok := pathContainerID(r)
if !ok {
Expand All @@ -82,6 +97,7 @@ func (h *Handler) StartInstance(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, statusResponse{Status: "success"})
}

// StopInstance 处理 POST /api/instances/{id}/stop 并停止实例。
func (h *Handler) StopInstance(w http.ResponseWriter, r *http.Request) {
id, ok := pathContainerID(r)
if !ok {
Expand All @@ -97,6 +113,7 @@ func (h *Handler) StopInstance(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, statusResponse{Status: "success"})
}

// DeleteInstance 处理 DELETE /api/instances/{id} 并删除实例。
func (h *Handler) DeleteInstance(w http.ResponseWriter, r *http.Request) {
id, ok := pathContainerID(r)
if !ok {
Expand All @@ -105,17 +122,14 @@ func (h *Handler) DeleteInstance(w http.ResponseWriter, r *http.Request) {
}

if err := h.svc.DeleteInstance(r.Context(), id); err != nil {
code := http.StatusInternalServerError
if err == service.ErrInstanceRunning {
code = http.StatusConflict
}
writeJSON(w, code, statusResponse{Status: "error", Error: err.Error()})
writeJSON(w, mapErrorCode(err), statusResponse{Status: "error", Error: err.Error()})
return
}

writeJSON(w, http.StatusOK, statusResponse{Status: "success"})
}

// pathContainerID 解析并校验 URL 路径中的容器 ID。
func pathContainerID(r *http.Request) (string, bool) {
id := strings.TrimSpace(r.PathValue("id"))
if id == "" || strings.Contains(id, "/") {
Expand All @@ -124,7 +138,22 @@ func pathContainerID(r *http.Request) (string, bool) {
return id, true
}

// mapErrorCode 将领域错误统一映射为 HTTP 状态码。
func mapErrorCode(err error) int {
switch {
case errors.Is(err, model.ErrNameExists):
return http.StatusConflict
case errors.Is(err, model.ErrInstanceRunning):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

// writeJSON 按指定状态码写入 JSON 响应。
func writeJSON(w http.ResponseWriter, code int, v any) {
// 说明:当前会忽略编码错误,因为状态码可能已写入。
// TODO: 增加统一的编码错误日志,提升可观测性。
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
Expand Down
Loading
Loading