|
| 1 | +--- |
| 2 | +phase: quick |
| 3 | +plan: 260504-414 |
| 4 | +type: execute |
| 5 | +wave: 1 |
| 6 | +depends_on: [] |
| 7 | +files_modified: |
| 8 | + - internal/controlplane/http/admin_host_files.go |
| 9 | + - internal/controlplane/http/router.go |
| 10 | + - internal/controlplane/http/admin_host_files_test.go |
| 11 | + - web/admin/src/hooks/use-host-files.ts |
| 12 | + - web/admin/src/components/hosts/path-autocomplete.tsx |
| 13 | + - web/admin/src/components/hosts/create-host-dialog.tsx |
| 14 | + - web/admin/src/components/hosts/mount-manager.tsx |
| 15 | +autonomous: true |
| 16 | +requirements: |
| 17 | + - QUICK-260504-414 |
| 18 | +must_haves: |
| 19 | + truths: |
| 20 | + - 管理员在新建主机对话框输入宿主机路径时能看到目录联想下拉列表 |
| 21 | + - 管理员在挂载管理器添加挂载时输入宿主机路径时能看到目录联想下拉列表 |
| 22 | + - API 拒绝路径遍历攻击(如 ../etc/passwd) |
| 23 | + - API 拒绝访问敏感系统目录(如 /proc、/sys、/dev) |
| 24 | + artifacts: |
| 25 | + - path: internal/controlplane/http/admin_host_files.go |
| 26 | + provides: GET /v1/admin/host-files?path=/xxx 端点实现 |
| 27 | + exports: ["NewAdminHostFilesHandler"] |
| 28 | + - path: internal/controlplane/http/router.go |
| 29 | + provides: 路由注册 |
| 30 | + contains: 'GET /v1/admin/host-files' |
| 31 | + - path: internal/controlplane/http/admin_host_files_test.go |
| 32 | + provides: handler 单测 |
| 33 | + min_lines: 80 |
| 34 | + - path: web/admin/src/hooks/use-host-files.ts |
| 35 | + provides: React Query hook 封装 API 调用 |
| 36 | + exports: ["useHostFiles"] |
| 37 | + - path: web/admin/src/components/hosts/path-autocomplete.tsx |
| 38 | + provides: 可复用的路径自动补全输入组件 |
| 39 | + exports: ["PathAutocomplete"] |
| 40 | + - path: web/admin/src/components/hosts/create-host-dialog.tsx |
| 41 | + provides: 新建主机对话框使用 PathAutocomplete |
| 42 | + - path: web/admin/src/components/hosts/mount-manager.tsx |
| 43 | + provides: 挂载管理器使用 PathAutocomplete |
| 44 | + key_links: |
| 45 | + - from: web/admin/src/components/hosts/path-autocomplete.tsx |
| 46 | + to: web/admin/src/hooks/use-host-files.ts |
| 47 | + via: useHostFiles hook |
| 48 | + - from: web/admin/src/hooks/use-host-files.ts |
| 49 | + to: GET /v1/admin/host-files |
| 50 | + via: apiFetch |
| 51 | + - from: internal/controlplane/http/router.go |
| 52 | + to: internal/controlplane/http/admin_host_files.go |
| 53 | + via: NewAdminHostFilesHandler |
| 54 | +--- |
| 55 | + |
| 56 | +<objective> |
| 57 | +为新建主机对话框和挂载管理器的宿主机路径输入框添加目录联想功能。 |
| 58 | + |
| 59 | +Purpose: 减少管理员手动输入宿主机绝对路径时的出错概率,提升挂载配置体验。 |
| 60 | +Output: |
| 61 | + - 后端 GET /v1/admin/host-files?path=/xxx API(带安全过滤) |
| 62 | + - 前端 PathAutocomplete 可复用组件 |
| 63 | + - create-host-dialog.tsx 和 mount-manager.tsx 两处替换 |
| 64 | +</objective> |
| 65 | + |
| 66 | +<execution_context> |
| 67 | +@/workspace/Desktop/cloud-cli-proxy/.claude/get-shit-done/workflows/execute-plan.md |
| 68 | +@/workspace/Desktop/cloud-cli-proxy/.claude/get-shit-done/templates/summary.md |
| 69 | +</execution_context> |
| 70 | + |
| 71 | +<context> |
| 72 | +@internal/controlplane/http/router.go |
| 73 | +@internal/controlplane/http/admin_hosts.go |
| 74 | +@web/admin/src/components/hosts/create-host-dialog.tsx |
| 75 | +@web/admin/src/components/hosts/mount-manager.tsx |
| 76 | +@web/admin/src/hooks/use-hosts.ts |
| 77 | +@web/admin/src/lib/api.ts |
| 78 | + |
| 79 | +<interfaces> |
| 80 | +现有 API 模式(来自 router.go / admin_hosts.go): |
| 81 | +- 使用 `nethttp.NewServeMux()` + `mux.Handle("METHOD /path", handler)` |
| 82 | +- admin 端点统一用 `adminGuard(handler)` 包裹 |
| 83 | +- handler 返回 `nethttp.Handler`,内部用 `writeJSON(w, status, payload)` 写响应 |
| 84 | +- 敏感操作记录 audit event(通过 `EventRecorder`) |
| 85 | + |
| 86 | +现有前端模式(来自 use-hosts.ts / api.ts): |
| 87 | +- API 基地址 `/v1/admin` |
| 88 | +- `apiFetch<T>(path, init?)` 封装 fetch,自动带 Bearer token |
| 89 | +- React Query hook 模式:`useQuery({ queryKey: [...], queryFn: () => apiFetch<T>(path) })` |
| 90 | +- 组件使用 shadcn/ui:Input、Button、Label、Dialog、Select 等 |
| 91 | + |
| 92 | +前端挂载数据结构: |
| 93 | +```typescript |
| 94 | +interface HostMount { |
| 95 | + source: string; // 宿主机路径 |
| 96 | + target: string; // 容器路径 |
| 97 | + read_only?: boolean; |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +create-host-dialog.tsx 中宿主机路径输入框当前实现: |
| 102 | +- 第 263-272 行:纯 `<Input>` 组件,placeholder="宿主机路径 (例: /data/shared)" |
| 103 | +- 状态:`newMountSource` / `setNewMountSource` |
| 104 | +- 联动逻辑:输入 source 时若 target 为空或与上次 source 相同,自动同步 target |
| 105 | + |
| 106 | +mount-manager.tsx 中宿主机路径输入框当前实现: |
| 107 | +- 第 119-128 行:纯 `<Input>` 组件,placeholder="宿主机路径 (例: /data/shared)" |
| 108 | +- 状态:`newSource` / `setNewSource` |
| 109 | +- 联动逻辑同上 |
| 110 | +</interfaces> |
| 111 | +</context> |
| 112 | + |
| 113 | +<tasks> |
| 114 | + |
| 115 | +<task type="auto" tdd="true"> |
| 116 | + <name>Task 1: 后端实现 GET /v1/admin/host-files API</name> |
| 117 | + <files>internal/controlplane/http/admin_host_files.go, internal/controlplane/http/admin_host_files_test.go, internal/controlplane/http/router.go</files> |
| 118 | + <behavior> |
| 119 | + - Test 1: 正常请求 path=/tmp 返回目录列表(子目录名数组),不含文件 |
| 120 | + - Test 2: 路径遍历攻击 ../etc/passwd → 400 Bad Request |
| 121 | + - Test 3: 敏感目录 /proc → 403 Forbidden |
| 122 | + - Test 4: 不存在的路径 → 404 Not Found |
| 123 | + - Test 5: 非目录路径(如普通文件)→ 400 Bad Request |
| 124 | + - Test 6: 空 path 参数 → 400 Bad Request |
| 125 | + - Test 7: 相对路径 foo/bar → 400 Bad Request(必须以 / 开头) |
| 126 | + </behavior> |
| 127 | + <action> |
| 128 | + 1. 创建 `internal/controlplane/http/admin_host_files.go`: |
| 129 | + - 定义敏感目录黑名单:`/proc`, `/sys`, `/dev`, `/boot`, `/etc/ssh`, `/root/.ssh` |
| 130 | + - 实现 `AdminHostFilesHandler` 结构体,含 `logger *slog.Logger` |
| 131 | + - 实现 `List() nethttp.Handler` 方法: |
| 132 | + - 从 query 参数读取 `path` |
| 133 | + - 校验 path 非空、以 `/` 开头、不包含 `..` 路径遍历片段(用 `strings.Contains(path, "..")` 或 `filepath.Clean` 比对) |
| 134 | + - 校验 path 不在敏感目录黑名单中(用 `strings.HasPrefix` 匹配) |
| 135 | + - 用 `os.Stat(path)` 确认路径存在且是目录 |
| 136 | + - 用 `os.ReadDir(path)` 读取目录项,仅返回子目录名(过滤掉文件、symlink) |
| 137 | + - 返回 JSON:`{ "entries": ["dir1", "dir2"] }` |
| 138 | + - 错误时返回适当 HTTP 状态码 + `{ "error": "..." }` |
| 139 | + - 实现 `NewAdminHostFilesHandler(logger) *AdminHostFilesHandler` |
| 140 | + |
| 141 | + 2. 修改 `internal/controlplane/http/router.go`: |
| 142 | + - 在 `Dependencies` 结构体中无需新增字段(handler 无外部依赖) |
| 143 | + - 在 adminGuard 区域内新增路由:`mux.Handle("GET /v1/admin/host-files", adminGuard(NewAdminHostFilesHandler(deps.Logger).List()))` |
| 144 | + |
| 145 | + 3. 创建 `internal/controlplane/http/admin_host_files_test.go`: |
| 146 | + - 使用标准 `net/http/httptest` 测试 handler |
| 147 | + - 覆盖上述 7 个行为测试用例 |
| 148 | + - 正常用例在临时目录下创建子目录结构,确保可移植 |
| 149 | + </action> |
| 150 | + <verify> |
| 151 | + <automated>cd /Users/zaneliu/Projects/open-source/cloud-cli-proxy && go test ./internal/controlplane/http/ -run TestAdminHostFiles -v -count=1</automated> |
| 152 | + </verify> |
| 153 | + <done> |
| 154 | + - `go test` 7 条测试全部 PASS |
| 155 | + - `go build ./internal/controlplane/http/...` 无编译错误 |
| 156 | + - 路由注册正确,API 可通过 `curl` 访问 |
| 157 | + </done> |
| 158 | +</task> |
| 159 | + |
| 160 | +<task type="auto"> |
| 161 | + <name>Task 2: 前端实现 PathAutocomplete 组件和 useHostFiles hook</name> |
| 162 | + <files>web/admin/src/hooks/use-host-files.ts, web/admin/src/components/hosts/path-autocomplete.tsx</files> |
| 163 | + <action> |
| 164 | + 1. 创建 `web/admin/src/hooks/use-host-files.ts`: |
| 165 | + - 导出 `useHostFiles(path: string)` hook |
| 166 | + - 使用 `useQuery`: |
| 167 | + - `queryKey: ["host-files", path]` |
| 168 | + - `queryFn: () => apiFetch<{ entries: string[] }>(\`/host-files?path=${encodeURIComponent(path)}\`)` |
| 169 | + - `enabled: path.length > 0 && path.startsWith("/")` |
| 170 | + - `staleTime: 30_000`(目录结构变化不频繁,30 秒缓存) |
| 171 | + |
| 172 | + 2. 创建 `web/admin/src/components/hosts/path-autocomplete.tsx`: |
| 173 | + - Props 接口: |
| 174 | + ```typescript |
| 175 | + interface PathAutocompleteProps { |
| 176 | + value: string; |
| 177 | + onChange: (value: string) => void; |
| 178 | + placeholder?: string; |
| 179 | + disabled?: boolean; |
| 180 | + className?: string; |
| 181 | + } |
| 182 | + ``` |
| 183 | + - 组件内部状态: |
| 184 | + - `open: boolean` — 下拉是否展开 |
| 185 | + - `highlightedIndex: number` — 键盘高亮索引 |
| 186 | + - 行为: |
| 187 | + - 使用 `useHostFiles(value)` 获取联想数据 |
| 188 | + - 输入框获得焦点且 value 以 `/` 开头时展开下拉 |
| 189 | + - 输入框失去焦点时延迟 150ms 关闭(允许点击选项) |
| 190 | + - 下拉显示 `entries` 列表,每个选项点击后 `onChange(value + "/" + entry)`(若 value 不以 `/` 结尾则补 `/`) |
| 191 | + - 键盘支持:↓/↑ 移动高亮,Enter 选中,Esc 关闭 |
| 192 | + - 加载状态显示 `Loader2` 旋转图标 |
| 193 | + - 空结果或无权限时显示 "无可用子目录" 提示 |
| 194 | + - 若 value 不以 `/` 开头,不触发请求,显示提示文字 |
| 195 | + - 样式:使用现有 shadcn/ui `Input` + 自定义下拉面板(绝对定位,z-50,白色背景,阴影边框,与现有设计一致) |
| 196 | + - 注意:组件高度不能撑破父容器,使用 `min-w-0` 和 `truncate` 处理长路径(继承 260504-2n4 修复) |
| 197 | + </action> |
| 198 | + <verify> |
| 199 | + <automated>cd /Users/zaneliu/Projects/open-source/cloud-cli-proxy/web/admin && npx tsc --noEmit --pretty 2>&1 | grep -c "path-autocomplete\|use-host-files" || echo "0 type errors in new files"</automated> |
| 200 | + </verify> |
| 201 | + <done> |
| 202 | + - TypeScript 编译无错误 |
| 203 | + - `useHostFiles` 导出正确,类型完整 |
| 204 | + - `PathAutocomplete` 导出正确,Props 接口完整 |
| 205 | + - 组件支持键盘导航、点击选择、加载状态、空状态 |
| 206 | + </done> |
| 207 | +</task> |
| 208 | + |
| 209 | +<task type="auto"> |
| 210 | + <name>Task 3: 替换两处宿主机路径输入框为 PathAutocomplete</name> |
| 211 | + <files>web/admin/src/components/hosts/create-host-dialog.tsx, web/admin/src/components/hosts/mount-manager.tsx</files> |
| 212 | + <action> |
| 213 | + 1. 修改 `create-host-dialog.tsx`: |
| 214 | + - 导入 `PathAutocomplete` |
| 215 | + - 将第 263-272 行的宿主机路径 `<Input>` 替换为 `<PathAutocomplete>`: |
| 216 | + - `value={newMountSource}` |
| 217 | + - `onChange={(v) => { setNewMountSource(v); if (!newMountTarget || newMountTarget === prevMountSource) { setNewMountTarget(v); } setPrevMountSource(v); }}` |
| 218 | + - `placeholder="宿主机路径 (例: /data/shared)"` |
| 219 | + - 保留原有的联动逻辑(target 自动同步) |
| 220 | + - 保留原有的验证提示(`!newMountSource.startsWith("/")` 时显示错误) |
| 221 | + - 确保 `min-w-0` 和 `truncate` 行为不变(PathAutocomplete 内部已处理) |
| 222 | + |
| 223 | + 2. 修改 `mount-manager.tsx`: |
| 224 | + - 导入 `PathAutocomplete` |
| 225 | + - 将第 119-128 行的宿主机路径 `<Input>` 替换为 `<PathAutocomplete>`: |
| 226 | + - `value={newSource}` |
| 227 | + - `onChange={(v) => { setNewSource(v); if (!newTarget || newTarget === prevSource) { setNewTarget(v); } setPrevSource(v); }}` |
| 228 | + - `placeholder="宿主机路径 (例: /data/shared)"` |
| 229 | + - 保留原有的联动逻辑和验证提示 |
| 230 | + - 保留 `RESERVED_PATHS` 警告逻辑(与 PathAutocomplete 不冲突) |
| 231 | + |
| 232 | + 3. 两处修改的注意事项: |
| 233 | + - PathAutocomplete 的 `onChange` 回调在选中目录时会追加子目录名(如 `/data` → `/data/shared`),这与原有手动输入行为一致 |
| 234 | + - 管理员可以继续手动输入完整路径,联想只是辅助 |
| 235 | + - 确保添加按钮的 `disabled` 逻辑不受影响(仍检查 `startsWith("/")`) |
| 236 | + </action> |
| 237 | + <verify> |
| 238 | + <automated>cd /Users/zaneliu/Projects/open-source/cloud-cli-proxy/web/admin && npx tsc --noEmit --pretty 2>&1 | grep -c "create-host-dialog\|mount-manager" || echo "0 type errors in modified files"</automated> |
| 239 | + </verify> |
| 240 | + <done> |
| 241 | + - TypeScript 编译无错误 |
| 242 | + - create-host-dialog.tsx 中宿主机路径输入已替换为 PathAutocomplete |
| 243 | + - mount-manager.tsx 中宿主机路径输入已替换为 PathAutocomplete |
| 244 | + - 原有联动逻辑(target 自动同步)和验证行为保持不变 |
| 245 | + - 添加按钮的启用/禁用逻辑未被破坏 |
| 246 | + </done> |
| 247 | +</task> |
| 248 | + |
| 249 | +</tasks> |
| 250 | + |
| 251 | +<verification> |
| 252 | +1. 后端:`go test ./internal/controlplane/http/ -run TestAdminHostFiles -v` 全部 PASS |
| 253 | +2. 后端:`go build ./...` 无编译错误 |
| 254 | +3. 前端:`cd web/admin && npx tsc --noEmit` 无类型错误 |
| 255 | +4. 前端:`cd web/admin && npm run build` 构建成功 |
| 256 | +5. 端到端:启动后端后 `curl -H "Authorization: Bearer $TOKEN" "http://localhost:8080/v1/admin/host-files?path=/tmp"` 返回目录列表 |
| 257 | +6. 端到端:相同请求带 `path=../etc/passwd` 返回 400,`path=/proc` 返回 403 |
| 258 | +</verification> |
| 259 | + |
| 260 | +<success_criteria> |
| 261 | +- GET /v1/admin/host-files?path=/xxx 端点可用,返回目录名数组 |
| 262 | +- 路径遍历和敏感目录访问被正确拦截 |
| 263 | +- create-host-dialog.tsx 和 mount-manager.tsx 的宿主机路径输入框支持目录联想 |
| 264 | +- 管理员可以键盘或鼠标选择子目录,也可以继续手动输入 |
| 265 | +- 所有既有行为(target 联动、路径验证、添加按钮逻辑)保持完整 |
| 266 | +</success_criteria> |
| 267 | + |
| 268 | +<output> |
| 269 | +After completion, create `.planning/quick/260504-414-1-get-v1-admin-host-files-path-xxx-api-2/260504-414-SUMMARY.md` |
| 270 | +</output> |
0 commit comments