diff --git a/.zcode/plans/plan-sess_cf2ae9b7-2221-409b-b804-66d894f1b069.md b/.zcode/plans/plan-sess_cf2ae9b7-2221-409b-b804-66d894f1b069.md new file mode 100644 index 0000000..8767882 --- /dev/null +++ b/.zcode/plans/plan-sess_cf2ae9b7-2221-409b-b804-66d894f1b069.md @@ -0,0 +1,88 @@ +# Phase C — ModelRouter 风格重设计 + 模拟 frps 转发下发测试 + +## 决策(用户未选择,依最佳判断) +- **视觉方向 = 忠实采用 sakura×frost 玻璃拟态**(ModelRouter 主色 #FF7FAC + 动态光斑背景 + KPI 卡 + cardIn 入场 + 渐变按钮)。在线/成功状态仍用绿色 #17a964(与 ModelRouter 一致,不破坏 webui4frpc "绿=在线" 语义)。不加强调色切换器/深色模式(控范围,可后续加)。 +- **模拟 frps = 加 2 个真实 frps 容器**(frps2/frps3)+ 一个死地址 remote,写 `test-forward.sh` 验证 下发→claim→起 worker→连通 全链路。 + +--- + +## Part 1 — 主题令牌:theme.css + variables.scss(换成 sakura/frost) + +**web/src/styles/theme.css**(重写 `:root` + 增全局组件类): +- 调色:`--w4f-primary:#FF7FAC; -h:#F33B7C; -50:#FFF0F5; -100:#FFE4E9; -200:#FFCDD9; -300:#FF9EB5`;`--w4f-secondary:#88C0D0; -h:#4C8DAE; -50:#F0F9FC`;`--w4f-danger:#DB3694; -50:#FEEAF6`;`--w4f-ok:#17a964; --w4f-warn:#b7791f; --w4f-info:#3f6ef5`。 +- 表面/文字:`--w4f-bg-1:#eef2ff; -2:#fff; -3:#ffe9f0`;`--w4f-card:rgba(255,255,255,.60); -2:rgba(255,255,255,.42)`;`--w4f-line:rgba(255,127,172,.18); -strong:rgba(120,90,150,.22)`;`--w4f-fg:#3b3350; --w4f-muted:#7c7a95`。 +- 光斑:`--w4f-blob1/2/3`(sakura/frost/pink 透明色)。 +- 阴影:`--w4f-sh-sm/md/lg`(紫灰调 `rgba(120,90,160,..)`);`--w4f-glass:18px; --w4f-ease-spring:cubic-bezier(.34,1.56,.64,1)`。 +- EP 覆盖:`--el-color-primary:#FF7FAC` + light-3/5/7/8/9 + dark-2;`--el-color-success:#17a964; danger:#DB3694;` 圆角/字体。 +- body:14px/1.55 `Quicksand,Nunito,Inter,...` 字号栈(不联网加载 web 字体,用系统栈接近),letter-spacing .02em。 +- **动态光斑背景**:`#bgfx` fixed + 3 `.blob` `filter:blur(64px)` + `@keyframes drift1/2/3`,`prefers-reduced-motion` 关动画。 +- 全局类:`.w4f-card`(玻璃 + `cardIn` 入场动画)、`.w4f-kpi`(KPI 卡基座 k-lab+blink dot/k-val/k-sub/k-chart 位)、`.w4f-bar`(5px 渐变条)、`.w4f-tag` 变体、`.w4f-btn`(渐变主按钮 + ghost/danger/small 变体)、sakura 滚动条。 + +**web/src/styles/variables.scss**(SCSS 变量重映射,保持视图内 `$color-*` 自动跟随): +- `$color-primary:#FF7FAC; -h:#F33B7C; -50:#FFF0F5; -100:#FFE4E9`;`$color-secondary:#88C0D0; -50:#F0F9FC`;`$color-success:#17a964`(保绿);`$color-danger:#DB3694; -50:#FEEAF6`;`$color-info:#3f6ef5`。 +- 文字色换紫灰调(primary #3b3350 / secondary #5a5470 / muted #7c7a95 / light #9c98b0);glass/shadow/radius/ease 对齐 theme.css。 + +## Part 2 — App.vue:玻璃侧栏 + 光斑背景层 + 面包屑 + +- 模板加 `
` 作固定背景(z-index -10)。 +- 侧栏 246px 玻璃(`backdrop-filter:blur(glass*.7) saturate(1.3)`),品牌区渐变 logo 方块 "W"(渐变 primary→secondary + 阴影),nav item active = ModelRouter 渐变瓦片(`linear-gradient(120deg,primary-50,secondary-50)` + primary-h 文字 + sh-sm,而非当前整块填色);footer 活跃点 + "token-ring · M6" 药丸。 +- 内容区顶部加 `.breadcrumb` 显示当前视图名(状态/连接配置/设置/集群),对齐 ModelRouter 布局。 +- `.content` 滚动区,各视图自带内边距。 + +## Part 3 — StatusView.vue 重设计 + +- 顶栏玻璃卡:面包屑标题 + "每 5 秒自动刷新" + 刷新(ghost) + 添加远程节点(渐变 primary)。 +- **KPI 网格**(auto-fit minmax 168px):远程节点数(sub: 在线 X)、在线 worker 数、本地服务数、活跃转发数。每卡 k-lab+blink dot、k-val 27px tabular-nums、k-sub。 +- 远程节点卡:玻璃 + cardIn,左边框色按健康(绿/红),名称 + 连接药丸(ok 绿/pending 青/err 红点),操作按钮(启=渐变 / 停=ghost / 编辑=ghost / 删=danger small),meta(pid/err/vhost 药丸),forwards 药丸 chips,per-proxy 行带 5px bar 指示。 +- 本地服务卡:玻璃,proto 药丸,targets 带 → + 状态药丸。 +- el-dialog 沿用(EP 覆盖自动 sakura 化)。 + +## Part 4 — ClusterView.vue 重设计 + +- Hero 玻璃卡:渐变标题 + sub,状态药丸(运行中=绿点 blink / 待加入=青 / 已脱离=红)+ 成员数 + 周期 + leader ♔。 +- **KPI 行**:成员数、活跃转发、待办命令、周期。 +- 操作工具条:创建集群(渐变 primary) / 加入…(ghost) / 退出(danger) / 刷新(ghost small),保留既 `!isMember`/`isMember` 安全门。 +- 环拓扑:横向玻璃节点卡链,leader=青调 + 边框、self=sakura 高亮发光、dead=红暗;负载 bar;addr mono;移除按钮(small danger);→ 箭头。 +- 待办命令/活跃拓扑/集群日志:玻璃卡 + sticky 表头 + 药丸标签(新增 sakura / 撤销·移除 danger / owner self=sakura)+ 彩色 kind 徽章 + self 高亮,全部 cardIn 入场。 +- 保留加入 el-dialog。 + +## Part 5 — 模拟 frps 节点 + 转发下发测试 + +**新增 frps 配置 + 容器**: +- `deploy/frps2.toml`:bindPort 7001, vhostHTTPPort 7081, auth.token=demo-token。 +- `deploy/frps3.toml`:bindPort 7002, vhostHTTPPort 7082, auth.token=demo-token。 +- `deploy/run-cluster.sh` 增启动 `w4f-frps2`(--network-alias frps2, -p 7001:7001 -p 7081:7081) 与 `w4f-frps3`(alias frps3, -p 7002:7002 -p 7082:7082)。 +- `deploy/compose.yaml` 增 frps2/frps3 服务(compose 用户同步)。 +- `deploy/start-mock-frps.sh`:仅启 frps2/frps3(先 `docker rm -f` 这两个再 run),供集群已起时补启。 + +**`deploy/test-forward.sh`**(核心验证脚本,Basic Auth admin:admin123): +1. 确认 ring:`GET node-a:7501 /cluster/ring` 断言 3 节点。 +2. 快照原 canvas:`GET node-a /canvas` 存原始 `{locals,remotes,links}`。 +3. 创建 4 条转发(`PUT node-a /canvas`,localOnly=false,local.ip=`backend` docker DNS,port 8080): + - `fwd-frps1` → remote frps:7000, remotePort 18091 + - `fwd-frps2` → remote frps2:7001, remotePort 18092 + - `fwd-frps3` → remote frps3:7002, remotePort 18093 + - `fwd-dead` → remote 192.0.2.1:7000, remotePort 18094(死地址:验下发+起 worker,conn 失败) +4. 轮询 `GET /cluster/ring`(1s×~25):断言 4 条 pending→0 且 topology 出现 4 条带 ownerId;打印每条由谁 claim(最低负载)。 +5. 轮询各 owner `GET /status`:断言对应 remote process.state=running。 +6. 轮询 owner `GET /profiles/{remote}/logs`:真实 frps 断言含 `login to server success`;dead 断言 worker running 但 connState=failed。 +7. 撤销测试:`PUT /canvas` 还原为步骤2快照(移除我们的 locals)→ 轮询 topology 4 条消失 + owner /status worker 已停。 +8. 打印结果表(task / target / claimed-by / worker / conn / 结论)。 +9. 清理:canvas 已在步骤7还原;不删容器。 + +**验证连通可选项**:真实 frps 的 vhost/remotePort 不映射到宿主(dispatch 验证不需打隧道);如需端到端打隧道验证,可 `docker exec` 在 net 内 curl `frps2:18092` 命中 backend——纳入脚本 step 6.5。 + +## Part 6 — 构建 + 验证 + +1. `cd web && npm run build` +2. 同步前端:`rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist` +3. `go build -o deploy/artifacts/webui4frpc ./cmd/webui4frpc` +4. 起/刷集群:`bash deploy/run-cluster.sh`(或既有集群:`bash deploy/start-mock-frps.sh && docker restart w4f-node-a w4f-node-b w4f-node-c`) +5. `bash deploy/test-forward.sh` —— 验证转发下发链路 +6. **视觉验证**(直击"太丑"反馈):用浏览器打开 `http://localhost:7501`(admin/admin123),截图 状态页 + 集群页,对照 ModelRouter 确认 光斑背景/玻璃 KPI 卡/渐变按钮/入场动画 到位。若不符则迭代。 + +## 影响文件 +- 主题:`web/src/styles/theme.css`(重写)、`web/src/styles/variables.scss`(重映射) +- 壳:`web/src/App.vue`(侧栏+光斑+面包屑) +- 视图:`web/src/views/StatusView.vue`、`web/src/views/ClusterView.vue`(重设计) +- 部署:`deploy/frps2.toml`、`deploy/frps3.toml`(新)、`deploy/run-cluster.sh`、`deploy/compose.yaml`、`deploy/start-mock-frps.sh`(新)、`deploy/test-forward.sh`(新) \ No newline at end of file diff --git a/deploy/compose.yaml b/deploy/compose.yaml index df026a4..232f965 100644 --- a/deploy/compose.yaml +++ b/deploy/compose.yaml @@ -12,6 +12,32 @@ services: networks: - w4f + frps2: + image: w4f-demo:latest + container_name: w4f-frps2 + command: ["/app/frp/frps", "-c", "/app/frps.toml"] + volumes: + - ./frps2.toml:/app/frps.toml:ro + - /home/jianf/Program/frp/bin:/app/frp:ro + ports: + - "7001:7001" # frps control + - "7081:7081" # vhostHTTPPort + networks: + - w4f + + frps3: + image: w4f-demo:latest + container_name: w4f-frps3 + command: ["/app/frp/frps", "-c", "/app/frps.toml"] + volumes: + - ./frps3.toml:/app/frps.toml:ro + - /home/jianf/Program/frp/bin:/app/frp:ro + ports: + - "7002:7002" # frps control + - "7082:7082" # vhostHTTPPort + networks: + - w4f + backend: image: w4f-demo:latest container_name: w4f-backend diff --git a/deploy/frps2.toml b/deploy/frps2.toml new file mode 100644 index 0000000..fbf4431 --- /dev/null +++ b/deploy/frps2.toml @@ -0,0 +1,16 @@ +# Mock frps #2 for forwarding dispatch/enablement testing. +# Same auth token as the primary frps so a single frpc worker config works +# against any of them; distinct bindPort / vhostHTTPPort so multiple can run +# on the same docker network without clashing. +bindAddr = "0.0.0.0" +bindPort = 7001 + +auth.method = "token" +auth.token = "demo-token" + +# HTTP virtual host port (for http proxies) +vhostHTTPPort = 7081 + +log.to = "/app/frps.log" +log.level = "info" +log.maxDays = 3 diff --git a/deploy/frps3.toml b/deploy/frps3.toml new file mode 100644 index 0000000..75c47ee --- /dev/null +++ b/deploy/frps3.toml @@ -0,0 +1,16 @@ +# Mock frps #3 for forwarding dispatch/enablement testing. +# Same auth token as the primary frps so a single frpc worker config works +# against any of them; distinct bindPort / vhostHTTPPort so multiple can run +# on the same docker network without clashing. +bindAddr = "0.0.0.0" +bindPort = 7002 + +auth.method = "token" +auth.token = "demo-token" + +# HTTP virtual host port (for http proxies) +vhostHTTPPort = 7082 + +log.to = "/app/frps.log" +log.level = "info" +log.maxDays = 3 diff --git a/deploy/run-cluster.sh b/deploy/run-cluster.sh index f2f1ae7..7e60a34 100755 --- a/deploy/run-cluster.sh +++ b/deploy/run-cluster.sh @@ -21,14 +21,20 @@ docker run -d --name w4f-backend --user $(id -u):$(id -g) --network $NET --netwo echo "== starting frps ==" docker run -d --name w4f-frps --user $(id -u):$(id -g) --network $NET --network-alias frps -p 7000:7000 -p 7080:7080 -p 18081:18081 -v $FRP/frps:/app/frps:ro -v $PWD/frps.toml:/app/frps.toml:ro --restart unless-stopped --entrypoint /app/frps debian:bookworm-slim -c /app/frps.toml +echo "== starting mock frps2 (bindPort 7001) ==" +docker run -d --name w4f-frps2 --user $(id -u):$(id -g) --network $NET --network-alias frps2 -p 7001:7001 -p 7081:7081 -v $FRP/frps:/app/frps:ro -v $PWD/frps2.toml:/app/frps.toml:ro --restart unless-stopped --entrypoint /app/frps debian:bookworm-slim -c /app/frps.toml + +echo "== starting mock frps3 (bindPort 7002) ==" +docker run -d --name w4f-frps3 --user $(id -u):$(id -g) --network $NET --network-alias frps3 -p 7002:7002 -p 7082:7082 -v $FRP/frps:/app/frps:ro -v $PWD/frps3.toml:/app/frps.toml:ro --restart unless-stopped --entrypoint /app/frps debian:bookworm-slim -c /app/frps.toml + echo "== starting node-a (seed, has frpc cache) ==" -docker run -d --name w4f-node-a --user $(id -u):$(id -g) --network $NET --network-alias node-a -e W4F_HOST=node-a -p 7501:7500 -p 17401:7400 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-a:/data --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data +docker run -d --name w4f-node-a --user $(id -u):$(id -g) --network $NET --network-alias node-a -e W4F_HOST=node-a -p 7501:7500 -p 17401:7400 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-a:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc echo "== starting node-b (no frpc; peer node-a) ==" -docker run -d --name w4f-node-b --user $(id -u):$(id -g) --network $NET --network-alias node-b -e W4F_HOST=node-b -p 7502:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-b:/data --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -peer=node-a:7500 +docker run -d --name w4f-node-b --user $(id -u):$(id -g) --network $NET --network-alias node-b -e W4F_HOST=node-b -p 7502:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-b:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc -peer=node-a:7500 echo "== starting node-c (no frpc; peer node-b) ==" -docker run -d --name w4f-node-c --user $(id -u):$(id -g) --network $NET --network-alias node-c -e W4F_HOST=node-c -p 7503:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-c:/data --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -peer=node-b:7500 +docker run -d --name w4f-node-c --user $(id -u):$(id -g) --network $NET --network-alias node-c -e W4F_HOST=node-c -p 7503:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-c:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc -peer=node-b:7500 echo "== cluster up ==" docker ps --filter "name=w4f-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \ No newline at end of file diff --git a/deploy/start-mock-frps.sh b/deploy/start-mock-frps.sh new file mode 100755 index 0000000..5ba2a02 --- /dev/null +++ b/deploy/start-mock-frps.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Start (or restart) just the mock frps2/frps3 containers on the demo network, +# leaving the rest of the cluster untouched. Used by test-forward.sh and when +# the cluster is already running but the mock frps nodes are not. +set -euo pipefail +cd "$(dirname "$0")" + +FRP=${FRP:-/home/jianf/Program/frp/bin} +NET=${W4F_NET:-w4f-demo} + +# detect which network the existing nodes live on (compose uses "w4f") +if docker inspect w4f-node-a >/dev/null 2>&1; then + NET=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}' w4f-node-a 2>/dev/null || echo "$NET") +fi + +start_one() { + local name="$1" alias="$2" cfg="$3" bp="$4" vp="$5" + docker rm -f "$name" >/dev/null 2>&1 || true + docker run -d --name "$name" --user "$(id -u):$(id -g)" \ + --network "$NET" --network-alias "$alias" \ + -p "$bp:$bp" -p "$vp:$vp" \ + -v "$FRP/frps:/app/frps:ro" -v "$PWD/$cfg:/app/frps.toml:ro" \ + --restart unless-stopped --entrypoint /app/frps \ + debian:bookworm-slim -c /app/frps.toml >/dev/null + echo " $name up ($alias, bindPort $bp, vhost $vp) on network $NET" +} + +echo "== starting mock frps nodes on network $NET ==" +start_one w4f-frps2 frps2 frps2.toml 7001 7081 +start_one w4f-frps3 frps3 frps3.toml 7002 7082 + +echo "== mock frps nodes up ==" +docker ps --filter "name=w4f-frps" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" diff --git a/deploy/test-forward.sh b/deploy/test-forward.sh new file mode 100755 index 0000000..dccc2ff --- /dev/null +++ b/deploy/test-forward.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# test-forward.sh — exercise the token-ring forwarding dispatch pipeline. +# +# Submits 4 forwards via PUT /canvas (localOnly=false → ring tasks): +# r-frps → frps:7000 (real, expect connected) +# r-frps2 → frps2:7001 (real mock, expect connected) +# r-frps3 → frps3:7002 (real mock, expect connected) +# r-dead → 192.0.2.1 (dead addr, expect dispatch+worker-start, conn failed) +# +# Verifies: pending→0, topology claimed (with ownerId), worker running on the +# claiming node, frpc "login to server success" for real targets, dead target +# worker running but conn failed. Then revokes (restore canvas) and verifies +# topology cleared + worker stopped. +# +# Run after the cluster is up: bash deploy/test-forward.sh +set -euo pipefail +cd "$(dirname "$0")" + +AUTH=admin:admin123 +A=http://localhost:7501 +B=http://localhost:7502 +C=http://localhost:7503 +NODE_PORTS=("$A" "$B" "$C") +RESULTS=$(mktemp -t w4f-results.XXXX) +trap 'rm -f "$RESULTS"' EXIT + +# ---- helpers ---- +cget() { curl -s -u "$AUTH" "$1/api/manager$2"; } +cput() { curl -s -X PUT -u "$AUTH" -H 'Content-Type: application/json' --data "$3" "$1/api/manager$2"; } +cpost() { curl -s -X POST -u "$AUTH" -H 'Content-Type: application/json' --data "$3" "$1/api/manager$2"; } +# py '