# planet.sh 启动性能优化 ## 背景 `planet.sh` 管理所有服务的启动/停止/重启。原有实现存在以下问题: 1. AI Provider 每次都重新构建(即使代码未变) 2. 杀端口速度极慢(最长等 45 秒) 3. 端口绑定检测用 Python 子进程(每次 ~300ms) 4. 无参 `restart` 与 `restart -b` 行为不一致 ## 问题一:AI Provider 每次重建 ### 根因 构建戳文件存放在 `/tmp/`,WSL/Linux 重启后 `/tmp` 被清空,导致三个条件中的"戳文件非空"这一条始终不满足,进而判定需要重建: ```bash # 三个条件必须同时成立才跳过重建 image_exists AND stamp_non_empty AND fingerprint_match ``` ### 修复 将戳文件路径从临时目录改到持久缓存路径: ```bash AI_PROVIDER_BUILD_STAMP_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/planet/aiprovider_build.sha256" ``` 写入时确保目录存在: ```bash write_ai_provider_build_stamp() { mkdir -p "$(dirname "$AI_PROVIDER_BUILD_STAMP_FILE")" compute_ai_provider_build_fingerprint > "$AI_PROVIDER_BUILD_STAMP_FILE" } ``` ### fingerprint 计算提速 原实现对整个 `aiprovider/` 打 tar 包再算 SHA,大目录下耗时可达数秒。改为 `find + stat`(只读文件元信息,不读内容): ```bash compute_ai_provider_build_fingerprint() { find aiprovider \ -type f \ ! -path '*/__pycache__/*' \ ! -name '.env' \ ! -name '.env.*' \ ! -name '*.pyc' \ ! -name '*.pyo' \ | LC_ALL=C sort \ | xargs -r stat --format="%Y %s %n" 2>/dev/null sha256sum docker-compose.yml docker-compose.simple.yml 2>/dev/null python3 "$SCRIPT_DIR/scripts/compute_aiprovider_dependency_fingerprint.py" 2>/dev/null } ``` 速度提升约 10 倍(大量小文件场景),误报率相同(mtime+size 变化 ≡ 文件被修改)。 `.env` 和 `.env.*` 被排除在 fingerprint 外。它们属于运行期配置,不应该因为修改模型、密钥或 Base URL 触发镜像重建。 ### Docker build context 收敛 AI Provider 镜像只需要根目录的 `pyproject.toml`、`uv.lock` 和 `aiprovider/` 代码。仓库中还包含前端静态大图、PDF、历史数据和 Unreal 资料,如果 build context 使用整个仓库,`transferring context` 会浪费大量时间。 当前通过根目录 `.dockerignore` 收敛上下文: ```dockerignore ** !pyproject.toml !uv.lock !aiprovider/ !aiprovider/** aiprovider/.env aiprovider/.env.* !aiprovider/.env.example ``` Dockerfile 也从全仓复制改为只复制 AI Provider 代码: ```dockerfile COPY pyproject.toml uv.lock /app/ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev COPY aiprovider /app/aiprovider ``` `uv sync` 使用 BuildKit cache mount 后,首次构建仍可能受网络影响;后续构建会复用 `/root/.cache/uv`,依赖下载不再重复从零开始。 ### 运行期配置来源 `planet.sh` 启动 AI Provider 前会生成受当前用户保护的运行期 env-file,并把它传给 Compose 或手动 `docker run` fallback。默认路径位于 `${XDG_STATE_HOME:-$HOME/.local/state}/planet/aiprovider_runtime.env`。配置优先来自: 1. `aiprovider/.env` 2. `~/.zshrc` 中简单的 `export AI_...=...` 或 `AI_...=...` 行 默认解析是静态的,只覆盖 AI Provider、镜像、代理相关变量,避免执行交互 shell 初始化。如果确实需要复杂 shell 展开,可以显式启用: ```bash PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a ``` 如果排查时需要忽略个人 shell 配置: ```bash PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a ``` ### 跳过重建的原理 fingerprint 一致时不执行 `docker compose build`,而是: ```bash docker start planet_aiprovider # 启动已存在的容器,几秒内完成 ``` `docker stop` 停容器,不删镜像;`cleanup_exit_containers` 删已退出容器,不删镜像。下次 `docker start` 会从现有镜像直接创建并启动容器。 ## 问题二:杀端口速度慢 ### 原因 `wait_for_port_release` 默认最多等 45 秒(15 次 × 3 秒)。 ### 修复 将后台进程清理场景的超时缩短至 3 秒(TERM→1.5s→KILL→1.5s): ```bash PORT_RELEASE_ATTEMPTS=15 PORT_RELEASE_INTERVAL=0.2 # 每次等 0.2s,总计 3s # cleanup_backend_processes / kill_port_if_requested wait_for_port_release "$port" 15 0.2 ``` `wait_for_port_release` 增加可选参数,允许不同场景使用不同超时: ```bash wait_for_port_release() { local port="$1" local max_attempts="${2:-$PORT_RELEASE_ATTEMPTS}" local interval="${3:-$PORT_RELEASE_INTERVAL}" ... } ``` 当前启动前端时还有一层预清理重试: - `PORT_PRESTART_RETRIES`:默认 3 次。 - `PORT_PRESTART_RETRY_INTERVAL`:默认 2 秒。 `kill_port_if_requested()` 优先清理当前环境能找到的监听 PID;只有检测到当前运行在 WSL 且没有可杀 PID、但端口仍不可绑定时,才会检查 Windows 侧 listener,并尝试通过 PowerShell 停止对应服务或强制结束对应进程。若没有权限,或 `iphlpsvc` 这类系统服务拒绝停止,脚本会打印 Windows listener 详情并立即停止启动,不再继续拉起服务碰同一个端口错误。非 WSL 环境不会尝试 Windows 清理路径。此时需要用管理员 PowerShell 清理 portproxy/服务占用,或改用其他端口。 ## 问题三:端口检测用 Python ### 原因 `can_bind_port` 用 `python3 -c "import socket..."` 检测端口,每次调用约 300ms。 ### 修复 优先使用系统工具(~10ms),Python 作为兜底: ```bash can_bind_port() { local port="$1" if command -v ss >/dev/null 2>&1; then ! ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE ":${port}$" return fi if command -v lsof >/dev/null 2>&1; then [ -z "$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null)" ] return fi python3 - "$port" <<'PY' import sys, socket p = int(sys.argv[1]) s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: s.bind(("", p)); s.close(); sys.exit(0) except OSError: sys.exit(1) PY } ``` ## 问题四:restart 行为不一致 ### 现象 - `restart -b`:停全部服务 → 检查 AI Provider fingerprint → 按需重建 → 启动 - `restart`(无参):停全部服务 → AI Provider 总是判定需要重建(因戳文件在 /tmp) ### 修复 修复戳文件路径后,无参 `restart` 同样使用 `stop + start`,fingerprint 检查正常生效,行为与 `restart -b` 完全一致。无需额外代码变更。 ## 状态文件、日志与失败清理 `planet.sh` 不再把 PID、日志和运行期 env-file 写入固定 `/tmp/planet_*` 路径。默认状态目录为: ```bash ${XDG_STATE_HOME:-$HOME/.local/state}/planet ``` 脚本启动时会创建该目录并尽量设置为 `700`。当前使用的文件包括: - `backend.pid` / `frontend.pid` / `motion_agent.pid` - `backend.log` / `frontend.log` / `motion_agent.log` - `aiprovider_build.log` - `aiprovider_runtime.env` - `ports.env` PID 文件写入前会校验 PID 为正整数,写入时带换行并尽量设置为 `600`。读取 PID 文件时,如果内容不是数字,脚本会忽略该文件,不会把垃圾内容传给 `kill`。 `start` 成功后会把本次端口写入 `ports.env`。后续执行 `./planet.sh health` 时,会优先检查上次启动端口;如果没有状态文件,则回退到默认端口 `8000`、`3000`、`8010`、`8765`。这避免了用自定义端口启动后,健康检查仍只看默认端口的问题。 启动过程有轻量失败清理:如果 `start` 中途失败,脚本只清理本轮已经拉起的本地进程(backend、frontend、Motion Agent),不会在正常启动完成后停止服务。AI Provider、PostgreSQL 和 Redis 容器仍按原有容器生命周期管理。 ## 健康检查与安全加固 HTTP 健康检查统一使用 `curl -fsS --max-time`。因此 `/health` 返回 4xx/5xx 不再被视为在线。 进程终止路径现在会校验: - signal 只允许 `TERM`、`KILL`、`INT`、`HUP`; - PID 必须是正整数; - 进程组 PGID 必须是正整数。 这可以避免坏 PID 文件或错误 signal 造成不可预期的 `kill` 行为。 前端和 Motion Agent 启动失败时,现在也会调用 `print_port_listener_details()`,输出与后端一致的端口监听诊断。WSL 下如果端口看起来被 Windows 侧占用,脚本仍只在检测到 WSL 时才调用 PowerShell 诊断或清理路径。 ## 跨平台注意事项 当前脚本是 Linux-first,并带有 WSL 增强。普通 Linux 不会执行 WSL PowerShell 逻辑;WSL 下会额外提供 Windows listener、portproxy 和摄像头提示。 如果要把同一份脚本扩展为 Linux、macOS、WSL 三平台通用,还需要继续封装这些命令差异: - `stat --format`、`sort -V`、`xargs -r` 是 GNU 风格,macOS 默认 BSD 工具不完全兼容。 - `hostname -I`、`ss`、`fuser`、`systemctl` 在 macOS 上通常不可用。 - `tac` 在 macOS 上不一定存在,可用 `awk` 或 Python 兜底。 - Docker Desktop on macOS 不适用 `systemctl` daemon 诊断。 - 摄像头自动发现依赖 `/dev/video*` / `v4l2-ctl`,这是 Linux 路线;macOS 应显式使用 camera URL 或另做 AVFoundation 检测。 维护方向是增加一个小的 platform compatibility 层,把端口监听检测、版本比较、文件元信息、反向 tail、LAN IP 获取和 Docker daemon 诊断集中处理,而不是在业务启动流程里继续散落平台判断。 ## 正式交付边界 `planet.sh` 是本地开发便利脚本,不作为正式生产启动入口。正式交付应通过 Kubernetes 的 `Deployment`、`Service`、`Ingress`、readiness/liveness probe 管理端口、健康检查、重启和滚动发布。这样生产环境不需要脚本抢占宿主机端口,也不会依赖 Vite dev server。 前端生产形态是 `vite build` 生成静态资源,再由 nginx/Caddy 等 HTTP 服务器托管。不要在生产中使用 `bun run dev` 或 `vite preview`。当前不维护 Webpack 双构建链;如果未来需要评估更企业化的构建生态,优先做 Rsbuild/Rspack spike。Electron 仅在正式目标变成离线桌面软件时再单独评估。 ## Motion Agent 可选启动 `planet.sh` 现在可以管理本地动作捕捉 Agent,但默认不会启动它,避免普通开发机因为没有摄像头、OpenCV 或 MediaPipe 而影响后端/前端启动。 启动方式: ```bash ./planet.sh start --motion-agent ``` 常用参数: - `--motion-agent` / `-m`:随本次启动或重启拉起 Motion Agent。 - `--motion-agent-port <端口>`:覆盖默认 WebSocket 端口 `8765`。 - `--motion-agent-camera-indexes `:覆盖自动发现的摄像头 index,例如 `0` 或 `0,1`。也可以用环境变量 `MOTION_AGENT_CAMERA_INDEXES=0,1`。 - `--motion-agent-camera-urls `:使用 RTSP/HTTP 摄像头流,适合 WSL、手机摄像头或网络摄像头。也可以用环境变量 `MOTION_AGENT_CAMERA_URLS=...`。 - `--motion-agent-dry-run`:不打开摄像头、不加载 CV 依赖,只启动协议服务,适合调试 Web 端连接。 非 dry-run 的 live 模式会在启动前检查 `mediapipe` 和 `opencv-python`。如果当前 `.venv` 缺包,脚本会自动执行: ```bash uv add mediapipe opencv-python ``` 如需禁止启动时自动安装,可设置: ```bash PLANET_MOTION_AGENT_AUTO_INSTALL=0 ./planet.sh start --motion-agent ``` live 模式会自动寻找 `/dev/video*`,优先取前两个 index 传给 Motion Agent。在 WSL 中,Windows 摄像头通常不会自动出现在 `/dev/video*`。可先用下面命令看设备: ```bash ls /dev/video* ``` 如需覆盖自动发现结果,可手动指定 index: ```bash ./planet.sh start --motion-agent --motion-agent-camera-indexes 1,2 ``` WSL 下更通用的方式是把手机摄像头或网络摄像头以 RTSP/HTTP 流接入: ```bash ./planet.sh start --motion-agent --motion-agent-camera-urls http://192.168.1.20:8080/video ``` 如果 WSL 中没有发现 `/dev/video*`,且没有提供 `--motion-agent-camera-urls`,脚本会停止 live 启动并提示处理方式,不会自动降级为 dry-run。可选处理: ```bash ./planet.sh start --motion-agent --motion-agent-camera-urls http://<手机IP>:8080/video ./planet.sh start --motion-agent --motion-agent-dry-run ``` 只有显式设置 `PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK=1` 时,WSL 无摄像头才会自动降级。 也可以用环境变量启用: ```bash PLANET_START_MOTION_AGENT=1 ./planet.sh start MOTION_AGENT_DRY_RUN=1 PLANET_START_MOTION_AGENT=1 ./planet.sh start ``` 日志入口: ```bash ./planet.sh log -m ``` 和前端一起开放局域网时: ```bash ./planet.sh start --allow-lan --motion-agent ``` 此时 Motion Agent 会绑定 `0.0.0.0`,启动输出会同时显示本机 WebSocket 地址和推荐局域网 WebSocket 地址。局域网浏览器访问 Earth 时,需要把 `motionAgent` 参数指向这台大屏主机,例如: ```text http://:3000/earth?motion=1&motionAgent=ws://:8765/ws/gestures ``` 停止时 `./planet.sh stop` 会一并停止已由脚本启动的 Motion Agent。健康检查会显示 `Motion Agent` 的 online/offline 状态。Earth 页面仍需用 `?motion=1` 或浏览器本地存储显式启用 Web 端连接。 如果只是普通网页/WSL/无安装演示场景,可以不启动 Motion Agent,直接在 Earth 设置里选择 `浏览器摄像头` 输入源并打开动捕调试模式;该路线使用浏览器 `getUserMedia`,需要 HTTPS 或 localhost 和摄像头权限。 ## 其他:移除不必要的 sleep 启动链路中两处 `sleep 3` 在实际已有健康检查覆盖的情况下多余,已移除: - `start_backend_service`:数据库健康检查通过后的 `sleep 3` - `restart_database_service`:重启后的等待 `sleep 3` ## 相关文件 - `planet.sh` — 全量修改 - `.dockerignore` — 收敛 AI Provider Docker build context - `aiprovider/Dockerfile` — 只复制 AI Provider 代码,并为 `uv sync` 启用 BuildKit cache mount - `docker-compose.yml` / `docker-compose.simple.yml` — 读取 `planet.sh` 生成的运行期 env-file - `scripts/compute_aiprovider_dependency_fingerprint.py` — 依赖 fingerprint(未改动)