Skip to content

Commit a275d38

Browse files
committed
docs(quick-260504-414): 完成宿主机路径目录联想 quick task
1 parent 2006cd2 commit a275d38

2 files changed

Lines changed: 272 additions & 1 deletion

File tree

.planning/STATE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Milestone: v3.1 映射语义补齐与懒加载 — ✅ SHIPPED (2026-04-24)
2828
Phase: 37 (e2e-uat) — COMPLETE
2929
Plan: 5 of 5 (全部完成)
3030
Status: Phase complete — ready for verification
31-
Last activity: 2026-05-03 - 完成 quick task 260504-2n4: 修复挂载路径在长路径下撑爆容器(create-host-dialog flex min-w-0 + mount-manager truncate)
31+
Last activity: 2026-05-03 - 完成 quick task 260504-414: 为新建主机对话框的挂载路径添加宿主机目录联想功能
3232

3333
Progress: [██████████] 100% (11/11 plans across v3.1 phases)
3434

@@ -117,6 +117,7 @@ v3.1 milestone 已启动;等待 ROADMAP.md 写入后进入 Phase 36 执行:
117117
| 260425 | 修复 AuthResponse.Status 字段无法解析 JSON 数字类型的问题 | 2026-04-30 | 7b49af3 | [260425-authresponse-status-json-json-cannot-unm](./quick/260425-authresponse-status-json-json-cannot-unm/) |
118118
| 260502-ni0 | 用户中心化凭据重构(DB/后端/前端)+ 容器自启动 + 删除 claude wrapper + 修复 IP 探测镜像 | 2026-05-02 | 6ed9cdc, 9a5db98, b0e6791, f24e3d1, 1b23a18, f744ec0 | [260502-ni0-user-centric-creds](./quick/260502-ni0-user-centric-creds/) |
119119
| 260504-2n4 | 修复挂载路径在长路径下撑爆创建主机对话框与挂载管理卡片 | 2026-05-03 | e208501, bf1c526 | [260504-2n4-fix-mount-path-overflow](./quick/260504-2n4-fix-mount-path-overflow/) |
120+
| 260504-414 | 为新建主机对话框的挂载路径添加宿主机目录联想功能 | 2026-05-03 | 9754c3e, da36a2b, 4c482a1 | [260504-414-1-get-v1-admin-host-files-path-xxx-api-2](./quick/260504-414-1-get-v1-admin-host-files-path-xxx-api-2/) |
120121

121122
### Roadmap Evolution
122123

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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

Comments
 (0)