Files
planet/docs/technical/zh/ops-planet-sh-startup.md
linkong 899e3bce43
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.71.0
2026-06-11 16:47:24 +08:00

17 KiB
Raw Blame History

planet.sh 启动性能优化

背景

planet.sh 管理所有服务的启动/停止/重启。原有实现存在以下问题:

  1. AI Provider 每次都重新构建(即使代码未变)
  2. 杀端口速度极慢(最长等 45 秒)
  3. 端口绑定检测用 Python 子进程(每次 ~300ms
  4. 无参 restartrestart -b 行为不一致

问题一AI Provider 每次重建

根因

构建戳文件存放在 /tmp/WSL/Linux 重启后 /tmp 被清空,导致三个条件中的"戳文件非空"这一条始终不满足,进而判定需要重建:

# 三个条件必须同时成立才跳过重建
image_exists AND stamp_non_empty AND fingerprint_match

修复

将戳文件路径从临时目录改到持久缓存路径:

AI_PROVIDER_BUILD_STAMP_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/planet/aiprovider_build.sha256"

写入时确保目录存在:

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(只读文件元信息,不读内容):

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.tomluv.lockaiprovider/ 代码。仓库中还包含前端静态大图、PDF、历史数据和 Unreal 资料,如果 build context 使用整个仓库,transferring context 会浪费大量时间。

当前通过根目录 .dockerignore 收敛上下文:

**

!pyproject.toml
!uv.lock
!aiprovider/
!aiprovider/**

aiprovider/.env
aiprovider/.env.*
!aiprovider/.env.example

Dockerfile 也从全仓复制改为只复制 AI Provider 代码:

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 展开,可以显式启用:

PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a

如果排查时需要忽略个人 shell 配置:

PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a

跳过重建的原理

fingerprint 一致时不执行 docker compose build,而是:

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

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 增加可选参数,允许不同场景使用不同超时:

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 删除旧 portproxy、停止占用端口的服务或强制结束对应进程。若管理员请求被取消,或 iphlpsvc 这类系统服务拒绝停止,脚本会打印 Windows listener 详情和管理员 PowerShell 处理命令,然后立即停止启动,不再继续拉起服务碰同一个端口错误。前端 Vite 启动后才发现 Port 3000 is already in use 时,也会打印同一套 Windows listener 处理命令。非 WSL 环境不会尝试 Windows 清理路径。--allow-lan 直接开放 3000 / 8000 / 8010,不再启动额外的 Windows 端口转发进程;旧的持久 portproxy 规则应清理掉。

问题三:端口检测用 Python

原因

can_bind_portpython3 -c "import socket..." 检测端口,每次调用约 300ms。

修复

优先使用系统工具(~10msPython 作为兜底:

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 + startfingerprint 检查正常生效,行为与 restart -b 完全一致。无需额外代码变更。

状态文件、日志与失败清理

planet.sh 不再把 PID、日志和运行期 env-file 写入固定 /tmp/planet_* 路径。默认状态目录为:

${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 时,会优先检查上次启动端口;如果没有状态文件,则回退到默认端口 8000300080108765。这避免了用自定义端口启动后,健康检查仍只看默认端口的问题。

启动过程有轻量失败清理:如果 start 中途失败脚本只清理本轮已经拉起的本地进程backend、frontend、Motion Agent不会在正常启动完成后停止服务。AI Provider、PostgreSQL 和 Redis 容器仍按原有容器生命周期管理。

健康检查与安全加固

HTTP 健康检查统一使用 curl -fsS --max-time。因此 /health 返回 4xx/5xx 不再被视为在线。

进程终止路径现在会校验:

  • signal 只允许 TERMKILLINTHUP
  • 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 --formatsort -Vxargs -r 是 GNU 风格macOS 默认 BSD 工具不完全兼容。
  • hostname -Issfusersystemctl 在 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 的 DeploymentServiceIngress、readiness/liveness probe 管理端口、健康检查、重启和滚动发布。这样生产环境不需要脚本抢占宿主机端口,也不会依赖 Vite dev server。

前端生产形态是 vite build 生成静态资源,再由 nginx/Caddy 等 HTTP 服务器托管。不要在生产中使用 bun run devvite preview。当前不维护 Webpack 双构建链;如果未来需要评估更企业化的构建生态,优先做 Rsbuild/Rspack spike。Electron 仅在正式目标变成离线桌面软件时再单独评估。

Motion Agent 默认启动

planet.sh 现在默认随 start 和全量 restart 启动本地 Motion Agent。这样星球端、UE 或调试客户端可以直接连接 ws://127.0.0.1:8765/ws/gestures。如果当前机器没有可用摄像头,默认隐式启动会降级为 dry-run 协议服务,不会阻断后端/前端启动;只有显式传入 --motion-agent、摄像头 index、摄像头 URL 或 WSL USB 参数时live 模式缺摄像头才会硬失败。

如果本次不需要 Motion Agent

./planet.sh start --non-motion-agent
./planet.sh restart --non-motion-agent

常用参数:

  • --non-motion-agent:本次启动或全量重启不拉起 Motion Agent。
  • --motion-agent / -m:显式要求本次启动或重启拉起 Motion Agent此时 live 摄像头失败会作为错误反馈。
  • --motion-agent-port <端口>:覆盖默认 WebSocket 端口 8765
  • --motion-agent-mode <模式>:指定输入模式,可选 autosingledual_redundantsingle_fallbackcalibrated_3ddual 作为兼容别名会进入双路冗余。
  • --motion-agent-camera-indexes <indexes>:覆盖自动发现的摄像头 index例如 00,1。也可以用环境变量 MOTION_AGENT_CAMERA_INDEXES=0,1
  • --motion-agent-camera-urls <urls>:使用 RTSP/HTTP 摄像头流,适合 WSL、手机摄像头或网络摄像头。也可以用环境变量 MOTION_AGENT_CAMERA_URLS=...
  • --motion-agent-wsl-usbipd:在 WSL 中尝试通过 usbipd-win 自动把唯一的 Windows USB 摄像头透传到 Linux。
  • --motion-agent-wsl-usbipd-busid <BUSID>:在 WSL 中指定 usbipd list 里的摄像头 BUSID 后透传,适合多摄像头设备。
  • --motion-agent-dry-run:不打开摄像头、不加载 CV 依赖,只启动协议服务,适合调试 Web 端连接。

非 dry-run 的 live 模式会在启动前检查 mediapipeopencv-python。如果当前 .venv 缺包,脚本会自动执行:

uv add mediapipe opencv-python

如需禁止启动时自动安装 Python CV 依赖,可设置:

PLANET_MOTION_AGENT_AUTO_INSTALL=0 ./planet.sh start

./planet.sh init 会在 WSL 中预检查 usbipd-win。如果没有 usbipd.exe,脚本会先尝试 winget install -e --id dorssel.usbipd-win,失败后复用仓库内置的 dorssel.usbipd-win MSI fallback如果缓存缺失或需要其他架构版本再下载 MSI 并请求管理员 PowerShell 安装。该步骤是 best-effort失败会提示后续处理方式但不会阻断普通初始化。可通过 ./planet.sh init --non-motion-agent 跳过该预检。

live 模式会自动寻找 /dev/video*,并优先用 OpenCV 实测过滤出真正能打开并读帧的 index再传给 Motion Agent。在 WSL/USB 摄像头场景中,一个摄像头可能暴露多个 /dev/video* 节点,其中部分是 metadata 或非采集节点,脚本会跳过这类不可读 index。在 WSL 中Windows 摄像头通常不会自动出现在 /dev/video*。可先用下面命令看设备:

默认 live 采集使用低延迟参数:640x360 输入、约 15Hz 识别事件worker 内部用 latest-frame 读帧线程,只保留每路摄像头的最新帧,避免 MediaPipe 慢帧时继续排队识别旧画面。骨架调试流默认关闭,只在星球端打开动捕调试面板时按约 8Hz 推送,避免日常手势控制被调试数据拖慢。状态事件会同时上报采集 FPS 与识别 FPS方便区分摄像头掉帧和识别耗时。

ls /dev/video*

如需覆盖自动发现结果,可手动指定 index

./planet.sh start --motion-agent --motion-agent-camera-indexes 1,2

WSL 下更通用的方式是把手机摄像头或网络摄像头以 RTSP/HTTP 流接入:

./planet.sh start --motion-agent --motion-agent-camera-urls http://192.168.1.20:8080/video

如果希望直接使用 Windows USB 摄像头,可以让脚本调用 usbipd-win 透传。该能力是显式开启的,因为摄像头附加到 WSL 期间通常会从 Windows 应用中暂时断开。

只有一个摄像头时:

./planet.sh start --motion-agent --motion-agent-wsl-usbipd

多个摄像头时,先查看 BUSID再指定设备

usbipd.exe list
./planet.sh start --motion-agent --motion-agent-wsl-usbipd-busid 3-2

如果 usbipd attach 提示设备未共享或未绑定,脚本会尝试弹出 Windows 管理员 PowerShell 自动执行 usbipd bind,然后重试 attach。若 UAC 被取消或自动 bind 失败,可在 Windows 管理员 PowerShell 中手动执行:

usbipd bind --busid 3-2
usbipd attach --wsl --busid 3-2

如果 WSL 中没有发现 /dev/video*,且没有提供 --motion-agent-camera-urls,默认隐式启动会降级为 dry-run。显式 live 启动会停止并提示处理方式。可选处理:

./planet.sh start --motion-agent --motion-agent-camera-urls http://<手机IP>:8080/video
./planet.sh start --motion-agent --motion-agent-wsl-usbipd
./planet.sh start --motion-agent --motion-agent-dry-run

显式 live 启动时,只有设置 PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK=1WSL 无摄像头才会自动降级。

--non-motion-agent 是命令级跳过入口。环境变量仍可调整服务启动方式:

MOTION_AGENT_DRY_RUN=1 ./planet.sh start

日志入口:

./planet.sh log -m

和前端一起开放局域网时:

./planet.sh start --allow-lan --motion-agent

此时 Motion Agent 会绑定 0.0.0.0,启动输出会同时显示本机 WebSocket 地址和推荐局域网 WebSocket 地址。局域网浏览器访问智能星球时,需要把 motionAgent 参数指向这台大屏主机,例如:

http://<LAN_IP>:3000/earth?motion=1&motionAgent=ws://<LAN_IP>: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未改动