release: bump version to 0.71.0
This commit is contained in:
718
planet.sh
718
planet.sh
@@ -116,6 +116,10 @@ FRONTEND_RUNTIME_SOURCE="${FRONTEND_RUNTIME_SOURCE:-}"
|
||||
PLANET_STATE_DIR="${PLANET_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/planet}"
|
||||
PLANET_CACHE_DIR="${PLANET_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/planet}"
|
||||
PLANET_UV_TUNA_INDEX_URL="${PLANET_UV_TUNA_INDEX_URL:-https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/}"
|
||||
USBIPD_WIN_FALLBACK_VERSION="${USBIPD_WIN_FALLBACK_VERSION:-5.3.0}"
|
||||
USBIPD_WIN_FALLBACK_URL_X64="${USBIPD_WIN_FALLBACK_URL_X64:-https://github.com/dorssel/usbipd-win/releases/download/v${USBIPD_WIN_FALLBACK_VERSION}/usbipd-win_${USBIPD_WIN_FALLBACK_VERSION}_x64.msi}"
|
||||
USBIPD_WIN_FALLBACK_URL_ARM64="${USBIPD_WIN_FALLBACK_URL_ARM64:-https://github.com/dorssel/usbipd-win/releases/download/v${USBIPD_WIN_FALLBACK_VERSION}/usbipd-win_${USBIPD_WIN_FALLBACK_VERSION}_arm64.msi}"
|
||||
USBIPD_WIN_DOWNLOAD_DIR="${USBIPD_WIN_DOWNLOAD_DIR:-$SCRIPT_DIR/downloads/usbipd-win}"
|
||||
BACKEND_PID_FILE="$PLANET_STATE_DIR/backend.pid"
|
||||
BACKEND_LOG_FILE="$PLANET_STATE_DIR/backend.log"
|
||||
FRONTEND_PID_FILE="$PLANET_STATE_DIR/frontend.pid"
|
||||
@@ -132,6 +136,7 @@ PLANET_PORT_STATE_FILE="$PLANET_STATE_DIR/ports.env"
|
||||
FRONTEND_VITE_ENTRY="$SCRIPT_DIR/frontend/node_modules/vite/bin/vite.js"
|
||||
MOTION_AGENT_PID_FILE="$PLANET_STATE_DIR/motion_agent.pid"
|
||||
MOTION_AGENT_LOG_FILE="$PLANET_STATE_DIR/motion_agent.log"
|
||||
MOTION_AGENT_SKIPPED_FILE="$PLANET_STATE_DIR/motion_agent.skipped"
|
||||
AI_PROVIDER_BUILD_STAMP_FILE="$PLANET_CACHE_DIR/aiprovider_build.sha256"
|
||||
AI_PROVIDER_BUILD_LOG_FILE="$PLANET_STATE_DIR/aiprovider_build.log"
|
||||
AI_PROVIDER_IMAGE_NAME="${AI_PROVIDER_IMAGE_NAME:-planet-aiprovider:latest}"
|
||||
@@ -144,6 +149,7 @@ AI_PROVIDER_RECREATE_REQUIRED=0
|
||||
START_RUN_ACTIVE=0
|
||||
START_RUN_COMPLETED=0
|
||||
STARTED_BACKEND_THIS_RUN=0
|
||||
MOTION_AGENT_SKIPPED_THIS_RUN=0
|
||||
STARTED_FRONTEND_THIS_RUN=0
|
||||
STARTED_MOTION_AGENT_THIS_RUN=0
|
||||
AI_PROVIDER_RUNTIME_ENV_NAMES=(
|
||||
@@ -1666,6 +1672,13 @@ detect_motion_agent_camera_indexes() {
|
||||
local devices=""
|
||||
local device=""
|
||||
local index=""
|
||||
local cv2_devices=""
|
||||
|
||||
cv2_devices="$(detect_motion_agent_camera_indexes_with_cv2 || true)"
|
||||
if [ -n "$cv2_devices" ]; then
|
||||
printf "%s" "$cv2_devices"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v v4l2-ctl >/dev/null 2>&1; then
|
||||
devices="$(v4l2-ctl --list-devices 2>/dev/null | sed -nE 's/^[[:space:]]*\\/dev\\/video([0-9]+).*/\\1/p' || true)"
|
||||
@@ -1684,10 +1697,348 @@ detect_motion_agent_camera_indexes() {
|
||||
printf "%s\n" "$devices" | sort -n | awk '!seen[$0]++' | head -n 2 | paste -sd, -
|
||||
}
|
||||
|
||||
detect_motion_agent_camera_indexes_with_cv2() {
|
||||
local python_bin="$SCRIPT_DIR/.venv/bin/python"
|
||||
|
||||
[ -x "$python_bin" ] || return 1
|
||||
|
||||
"$python_bin" - <<'PY' 2>/dev/null
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import re
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
indexes: list[int] = []
|
||||
for path in glob.glob("/dev/video*"):
|
||||
match = re.search(r"/dev/video(\d+)$", path)
|
||||
if match:
|
||||
indexes.append(int(match.group(1)))
|
||||
|
||||
usable: list[str] = []
|
||||
for index in sorted(set(indexes)):
|
||||
capture = cv2.VideoCapture(index, cv2.CAP_V4L2)
|
||||
try:
|
||||
if not capture or not capture.isOpened():
|
||||
continue
|
||||
ok, _frame = capture.read()
|
||||
if ok:
|
||||
usable.append(str(index))
|
||||
finally:
|
||||
if capture:
|
||||
capture.release()
|
||||
|
||||
print(",".join(usable[:2]))
|
||||
PY
|
||||
}
|
||||
|
||||
is_wsl_environment() {
|
||||
grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null
|
||||
}
|
||||
|
||||
detect_windows_usbipd_camera_busids() {
|
||||
local line=""
|
||||
local busid=""
|
||||
|
||||
command -v usbipd.exe >/dev/null 2>&1 || return 0
|
||||
|
||||
usbipd.exe list 2>/dev/null | tr -d '\r' | while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[[:space:]]*([0-9]+-[0-9]+)[[:space:]] ]] || continue
|
||||
busid="${match[1]}"
|
||||
case "$line" in
|
||||
*[Cc]amera*|*[Ww]ebcam*|*"USB Video"*|*UVC*|*摄像头*|*相机*)
|
||||
printf "%s|%s\n" "$busid" "$line"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
download_usbipd_win_msi() {
|
||||
local output_path="$1"
|
||||
local fallback_url="$2"
|
||||
|
||||
mkdir -p "$(dirname "$output_path")"
|
||||
|
||||
"$SCRIPT_DIR/.venv/bin/python" - "$output_path" "$fallback_url" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
output = Path(sys.argv[1])
|
||||
fallback_url = sys.argv[2]
|
||||
arch = "arm64" if "aarch64" in __import__("platform").machine().lower() or "arm64" in __import__("platform").machine().lower() else "x64"
|
||||
latest_api = "https://api.github.com/repos/dorssel/usbipd-win/releases/latest"
|
||||
|
||||
|
||||
def download(url: str) -> None:
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "planet-bootstrap"})
|
||||
with urllib.request.urlopen(request, timeout=25) as response:
|
||||
output.write_bytes(response.read())
|
||||
|
||||
|
||||
try:
|
||||
request = urllib.request.Request(latest_api, headers={"User-Agent": "planet-bootstrap"})
|
||||
with urllib.request.urlopen(request, timeout=12) as response:
|
||||
release = json.loads(response.read().decode("utf-8"))
|
||||
assets = release.get("assets") or []
|
||||
url = next(
|
||||
(
|
||||
asset.get("browser_download_url")
|
||||
for asset in assets
|
||||
if isinstance(asset, dict)
|
||||
and str(asset.get("name", "")).endswith(".msi")
|
||||
and arch in str(asset.get("name", "")).lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not url:
|
||||
raise RuntimeError("latest release has no matching MSI asset")
|
||||
download(url)
|
||||
except Exception:
|
||||
download(fallback_url)
|
||||
PY
|
||||
}
|
||||
|
||||
install_usbipd_win_from_msi() {
|
||||
local msi_path="$1"
|
||||
local msi_win_path=""
|
||||
local script_path=""
|
||||
|
||||
msi_win_path="$(wslpath -w "$msi_path" 2>/dev/null || true)"
|
||||
if [ -z "$msi_win_path" ]; then
|
||||
log_warn "usbipd-win MSI 已下载,但无法转换 Windows 路径: ${msi_path}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
script_path="$(mktemp "${TMPDIR:-/tmp}/planet-usbipd-install.XXXXXX.ps1")"
|
||||
cat > "$script_path" <<EOF
|
||||
\$ErrorActionPreference = 'Stop'
|
||||
Start-Process msiexec.exe -Wait -ArgumentList '/i', '${msi_win_path}', '/qn', '/norestart'
|
||||
EOF
|
||||
|
||||
log_warn "准备请求管理员 PowerShell 安装 usbipd-win"
|
||||
if run_windows_admin_powershell_script "$script_path" "无法请求管理员 PowerShell 安装 usbipd-win"; then
|
||||
rm -f "$script_path" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$script_path" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_usbipd_win_for_wsl() {
|
||||
local auto_install="${PLANET_MOTION_AGENT_USBIPD_AUTO_INSTALL:-1}"
|
||||
local fallback_url="$USBIPD_WIN_FALLBACK_URL_X64"
|
||||
local msi_path=""
|
||||
local winget_status=0
|
||||
|
||||
command -v usbipd.exe >/dev/null 2>&1 && return 0
|
||||
is_wsl_environment || return 1
|
||||
|
||||
case "$auto_install" in
|
||||
0|false|no|off)
|
||||
log_warn "未找到 usbipd.exe,且 PLANET_MOTION_AGENT_USBIPD_AUTO_INSTALL=0"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! command -v powershell.exe >/dev/null 2>&1; then
|
||||
log_warn "未找到 powershell.exe,无法从 WSL 自动安装 usbipd-win"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_warn "未找到 usbipd.exe,准备尝试自动安装 usbipd-win"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "\
|
||||
\$ProgressPreference = 'SilentlyContinue'; \
|
||||
if (Get-Command usbipd -ErrorAction SilentlyContinue) { exit 0 }; \
|
||||
if (Get-Command winget -ErrorAction SilentlyContinue) { \
|
||||
winget install -e --id dorssel.usbipd-win --accept-package-agreements --accept-source-agreements --silent; \
|
||||
exit \$LASTEXITCODE \
|
||||
}; \
|
||||
exit 2" >/dev/null 2>&1 || winget_status=$?
|
||||
|
||||
if [ "$winget_status" -eq 0 ]; then
|
||||
hash -r 2>/dev/null || true
|
||||
if command -v usbipd.exe >/dev/null 2>&1; then
|
||||
log_success "usbipd-win 已通过 winget 安装"
|
||||
return 0
|
||||
fi
|
||||
log_warn "winget 已完成,但当前 WSL shell 暂未发现 usbipd.exe;继续尝试 MSI fallback"
|
||||
else
|
||||
log_warn "winget 安装 usbipd-win 未完成,准备下载 MSI fallback"
|
||||
fi
|
||||
|
||||
case "$(uname -m 2>/dev/null | tr '[:upper:]' '[:lower:]')" in
|
||||
aarch64|arm64) fallback_url="$USBIPD_WIN_FALLBACK_URL_ARM64" ;;
|
||||
esac
|
||||
msi_path="$USBIPD_WIN_DOWNLOAD_DIR/usbipd-win-${USBIPD_WIN_FALLBACK_VERSION}.msi"
|
||||
if [ ! -s "$msi_path" ]; then
|
||||
log_note "下载 usbipd-win MSI 到: ${msi_path}"
|
||||
if ! download_usbipd_win_msi "$msi_path" "$fallback_url"; then
|
||||
log_warn "usbipd-win MSI 下载失败"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if install_usbipd_win_from_msi "$msi_path"; then
|
||||
hash -r 2>/dev/null || true
|
||||
if command -v usbipd.exe >/dev/null 2>&1; then
|
||||
log_success "usbipd-win 已安装"
|
||||
return 0
|
||||
fi
|
||||
log_warn "usbipd-win 已安装,但当前 WSL shell 仍未发现 usbipd.exe;请重新打开终端后重试"
|
||||
else
|
||||
log_warn "usbipd-win 自动安装未完成;MSI 已保留在 ${msi_path}"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_motion_agent_host_dependencies() {
|
||||
[ "${MOTION_AGENT_REQUESTED:-1}" -eq 1 ] || return 0
|
||||
is_wsl_environment || return 0
|
||||
|
||||
log_step "检查 Motion Agent WSL 宿主依赖"
|
||||
if command -v usbipd.exe >/dev/null 2>&1; then
|
||||
log_success "usbipd-win 已可用"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ensure_usbipd_win_for_wsl; then
|
||||
log_success "usbipd-win 已就绪"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "usbipd-win 自动准备未完成;普通前后端初始化继续"
|
||||
log_note "如果要使用 Windows USB 摄像头,请稍后重开终端或执行:"
|
||||
log_note " ./planet.sh restart --motion-agent-wsl-usbipd"
|
||||
return 0
|
||||
}
|
||||
|
||||
print_motion_agent_wsl_usbipd_manual_steps() {
|
||||
local busid_hint="${1:-<BUSID>}"
|
||||
|
||||
log_note "可在 Windows PowerShell 中查看并透传摄像头:"
|
||||
log_note " usbipd list"
|
||||
log_note " usbipd bind --busid ${busid_hint} # 需要管理员 PowerShell,仅首次共享时需要"
|
||||
log_note " usbipd attach --wsl --busid ${busid_hint}"
|
||||
log_note "透传后回到 WSL 执行:ls /dev/video*"
|
||||
}
|
||||
|
||||
request_motion_agent_wsl_usbipd_bind() {
|
||||
local busid="$1"
|
||||
local script_path=""
|
||||
|
||||
if ! [[ "$busid" =~ ^[0-9]+-[0-9]+$ ]]; then
|
||||
log_warn "usbipd BUSID 格式异常,跳过自动 bind: ${busid}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
script_path="$(mktemp "${TMPDIR:-/tmp}/planet-usbipd-bind.XXXXXX.ps1")"
|
||||
cat > "$script_path" <<EOF
|
||||
\$ErrorActionPreference = 'Stop'
|
||||
usbipd bind --busid '${busid}'
|
||||
EOF
|
||||
|
||||
log_warn "摄像头 ${busid} 尚未共享,准备弹出管理员 PowerShell 执行 usbipd bind"
|
||||
if run_windows_admin_powershell_script "$script_path" "无法请求管理员 PowerShell 执行 usbipd bind"; then
|
||||
rm -f "$script_path" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$script_path" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
try_motion_agent_wsl_usbipd_camera() {
|
||||
local enabled="${MOTION_AGENT_WSL_USBIPD:-${PLANET_MOTION_AGENT_WSL_USBIPD:-0}}"
|
||||
local busid="${MOTION_AGENT_WSL_USBIPD_BUSID:-}"
|
||||
local candidates=""
|
||||
local candidate_count=0
|
||||
local candidate_line=""
|
||||
local attach_output=""
|
||||
local attach_status=0
|
||||
|
||||
case "$enabled" in
|
||||
1|true|yes|on) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
is_wsl_environment || return 1
|
||||
|
||||
if ! ensure_usbipd_win_for_wsl; then
|
||||
log_warn "未找到可用 usbipd.exe,无法自动把 Windows USB 摄像头透传到 WSL"
|
||||
log_note "也可在 Windows 手动安装:winget install -e --id dorssel.usbipd-win"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$busid" ]; then
|
||||
candidates="$(detect_windows_usbipd_camera_busids || true)"
|
||||
candidate_count="$(printf "%s\n" "$candidates" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')"
|
||||
if [ "$candidate_count" -eq 1 ]; then
|
||||
candidate_line="$(printf "%s\n" "$candidates" | sed -n '1p')"
|
||||
busid="${candidate_line%%|*}"
|
||||
log_note "Motion Agent 在 Windows USB 设备中发现摄像头 BUSID: ${busid}"
|
||||
elif [ "$candidate_count" -gt 1 ]; then
|
||||
log_warn "发现多个疑似摄像头 USB 设备,脚本不会猜测使用哪一个"
|
||||
printf "%s\n" "$candidates" | sed 's/^/ /'
|
||||
log_note "请指定:./planet.sh restart -m --motion-agent-wsl-usbipd-busid <BUSID>"
|
||||
return 1
|
||||
else
|
||||
log_warn "usbipd-win 未列出明确的摄像头设备"
|
||||
print_motion_agent_wsl_usbipd_manual_steps
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! [[ "$busid" =~ ^[0-9]+-[0-9]+$ ]]; then
|
||||
log_warn "usbipd BUSID 格式异常,无法自动透传摄像头: ${busid}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_warn "准备通过 usbipd-win 将 Windows USB 摄像头 ${busid} 附加到 WSL"
|
||||
log_note "附加期间该摄像头通常会从 Windows 应用中暂时断开。"
|
||||
|
||||
attach_output="$(usbipd.exe attach --wsl --busid "$busid" 2>&1 | tr -d '\r')" || attach_status=$?
|
||||
if [ "$attach_status" -ne 0 ]; then
|
||||
case "$attach_output" in
|
||||
*"Device is not shared"*|*"not shared"*|*"usbipd bind"*)
|
||||
if request_motion_agent_wsl_usbipd_bind "$busid"; then
|
||||
log_note "usbipd bind 已完成,重试 attach 摄像头 ${busid}"
|
||||
attach_status=0
|
||||
attach_output="$(usbipd.exe attach --wsl --busid "$busid" 2>&1 | tr -d '\r')" || attach_status=$?
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$attach_status" -ne 0 ]; then
|
||||
log_warn "usbipd attach 未完成"
|
||||
[ -z "$attach_output" ] || printf "%s\n" "$attach_output" | sed 's/^/ /'
|
||||
print_motion_agent_wsl_usbipd_manual_steps "$busid"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
MOTION_AGENT_CAMERA_INDEXES="$(detect_motion_agent_camera_indexes)"
|
||||
if [ -n "$MOTION_AGENT_CAMERA_INDEXES" ]; then
|
||||
log_success "Motion Agent 已通过 usbipd-win 在 WSL 中发现摄像头 index: ${MOTION_AGENT_CAMERA_INDEXES}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "usbipd attach 已执行,但 WSL 仍未发现 /dev/video*"
|
||||
print_motion_agent_wsl_usbipd_manual_steps "$busid"
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_motion_agent_live_deps() {
|
||||
local auto_install="${PLANET_MOTION_AGENT_AUTO_INSTALL:-1}"
|
||||
|
||||
@@ -2419,8 +2770,66 @@ collect_port_pids() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
pids="$(python3 - "$port" <<'PY' 2>/dev/null || true
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
target_port = int(sys.argv[1])
|
||||
target_hex = f"{target_port:04X}"
|
||||
inodes: set[str] = set()
|
||||
|
||||
for table in ("/proc/net/tcp", "/proc/net/tcp6"):
|
||||
try:
|
||||
lines = open(table, "r", encoding="utf-8").read().splitlines()[1:]
|
||||
except OSError:
|
||||
continue
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) < 10:
|
||||
continue
|
||||
local_address = parts[1]
|
||||
state = parts[3]
|
||||
inode = parts[9]
|
||||
if state != "0A":
|
||||
continue
|
||||
try:
|
||||
_address_hex, port_hex = local_address.rsplit(":", 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if port_hex.upper() == target_hex:
|
||||
inodes.add(inode)
|
||||
|
||||
if not inodes:
|
||||
raise SystemExit(0)
|
||||
|
||||
for pid in filter(str.isdigit, os.listdir("/proc")):
|
||||
fd_dir = f"/proc/{pid}/fd"
|
||||
try:
|
||||
fds = os.listdir(fd_dir)
|
||||
except OSError:
|
||||
continue
|
||||
for fd in fds:
|
||||
try:
|
||||
target = os.readlink(f"{fd_dir}/{fd}")
|
||||
except OSError:
|
||||
continue
|
||||
if target.startswith("socket:[") and target[8:-1] in inodes:
|
||||
print(pid)
|
||||
break
|
||||
PY
|
||||
)"
|
||||
if [ -n "$pids" ]; then
|
||||
printf "%s\n" "$pids" | awk '!seen[$0]++'
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
pids="$(ss -ltnp "( sport = :${port} )" 2>/dev/null | sed -nE 's/.*pid=([0-9]+).*/\1/p' || true)"
|
||||
pids="$(ss -ltnpH "( sport = :${port} )" 2>/dev/null | sed -nE 's/.*pid=([0-9]+).*/\1/p; s/.*pid=([0-9]+),.*/\1/p' || true)"
|
||||
if [ -n "$pids" ]; then
|
||||
printf "%s\n" "$pids" | awk '!seen[$0]++'
|
||||
return 0
|
||||
@@ -2525,6 +2934,43 @@ PY
|
||||
fi
|
||||
}
|
||||
|
||||
print_port_bind_probe_details() {
|
||||
local port="$1"
|
||||
|
||||
command -v python3 >/dev/null 2>&1 || return 1
|
||||
|
||||
python3 - "$port" <<'PY' 2>/dev/null | while IFS= read -r line; do
|
||||
import socket
|
||||
import sys
|
||||
|
||||
port = int(sys.argv[1])
|
||||
for family, host, label in (
|
||||
(socket.AF_INET, "0.0.0.0", "IPv4 0.0.0.0"),
|
||||
(socket.AF_INET, "127.0.0.1", "IPv4 127.0.0.1"),
|
||||
(socket.AF_INET6, "::", "IPv6 ::"),
|
||||
(socket.AF_INET6, "::1", "IPv6 ::1"),
|
||||
):
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(family)
|
||||
if family == socket.AF_INET6 and hasattr(socket, "IPV6_V6ONLY"):
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
sock.bind((host, port))
|
||||
print(f"bind probe: {label}:{port} ok")
|
||||
except OSError as exc:
|
||||
print(f"bind probe: {label}:{port} failed errno={getattr(exc, 'errno', 'unknown')} {exc}")
|
||||
finally:
|
||||
if sock is not None:
|
||||
try:
|
||||
sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
PY
|
||||
[ -n "$line" ] || continue
|
||||
printf "${DIM} %s${NC}\n" "$line"
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_port_release() {
|
||||
local port="$1"
|
||||
local max_attempts="${2:-$PORT_RELEASE_ATTEMPTS}"
|
||||
@@ -2746,6 +3192,7 @@ EOF
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
log_note "未能在当前环境内定位端口 ${port} 的监听进程,可能被宿主机或外部网络命名空间占用。"
|
||||
print_port_bind_probe_details "$port" || true
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -2971,20 +3418,20 @@ parse_service_args() {
|
||||
MOTION_AGENT_PORT="$DEFAULT_MOTION_AGENT_PORT"
|
||||
MOTION_AGENT_CAMERA_INDEXES="${MOTION_AGENT_CAMERA_INDEXES:-}"
|
||||
MOTION_AGENT_CAMERA_URLS="${MOTION_AGENT_CAMERA_URLS:-}"
|
||||
MOTION_AGENT_MODE="${MOTION_AGENT_MODE:-auto}"
|
||||
MOTION_AGENT_WSL_USBIPD="${MOTION_AGENT_WSL_USBIPD:-${PLANET_MOTION_AGENT_WSL_USBIPD:-0}}"
|
||||
MOTION_AGENT_WSL_USBIPD_BUSID="${MOTION_AGENT_WSL_USBIPD_BUSID:-}"
|
||||
BACKEND_PORT_REQUESTED=0
|
||||
FRONTEND_PORT_REQUESTED=0
|
||||
AI_PROVIDER_REQUESTED=0
|
||||
MOTION_AGENT_REQUESTED=0
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=0
|
||||
MOTION_AGENT_DISABLED=0
|
||||
MOTION_AGENT_DRY_RUN=0
|
||||
DATABASE_REQUESTED=0
|
||||
FRONTEND_LAN_ENABLED=0
|
||||
FRONTEND_LAN_HTTPS_ENABLED=0
|
||||
|
||||
case "${PLANET_START_MOTION_AGENT:-0}" in
|
||||
1|true|yes|on)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
;;
|
||||
esac
|
||||
case "${MOTION_AGENT_DRY_RUN:-0}" in
|
||||
1|true|yes|on)
|
||||
MOTION_AGENT_DRY_RUN=1
|
||||
@@ -3022,10 +3469,19 @@ parse_service_args() {
|
||||
;;
|
||||
-m|--motion-agent)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
shift 1
|
||||
;;
|
||||
--non-motion-agent)
|
||||
MOTION_AGENT_REQUESTED=0
|
||||
MOTION_AGENT_DISABLED=1
|
||||
shift 1
|
||||
;;
|
||||
--motion-agent-port)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
||||
MOTION_AGENT_PORT="$2"
|
||||
shift 2
|
||||
@@ -3036,11 +3492,15 @@ parse_service_args() {
|
||||
;;
|
||||
--motion-agent-dry-run)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
MOTION_AGENT_DRY_RUN=1
|
||||
shift 1
|
||||
;;
|
||||
--motion-agent-camera-indexes)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
if [ -n "$2" ]; then
|
||||
MOTION_AGENT_CAMERA_INDEXES="$2"
|
||||
shift 2
|
||||
@@ -3051,6 +3511,8 @@ parse_service_args() {
|
||||
;;
|
||||
--motion-agent-camera-urls)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
if [ -n "$2" ]; then
|
||||
MOTION_AGENT_CAMERA_URLS="$2"
|
||||
shift 2
|
||||
@@ -3059,6 +3521,38 @@ parse_service_args() {
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--motion-agent-mode)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
if [ -n "$2" ] && [[ "$2" =~ ^(auto|single|dual|dual_redundant|single_fallback|calibrated_3d)$ ]]; then
|
||||
MOTION_AGENT_MODE="$2"
|
||||
shift 2
|
||||
else
|
||||
log_error "--motion-agent-mode 需要 auto/single/dual/dual_redundant/single_fallback/calibrated_3d"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--motion-agent-wsl-usbipd)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
MOTION_AGENT_WSL_USBIPD=1
|
||||
shift 1
|
||||
;;
|
||||
--motion-agent-wsl-usbipd-busid)
|
||||
MOTION_AGENT_REQUESTED=1
|
||||
MOTION_AGENT_EXPLICIT_REQUESTED=1
|
||||
MOTION_AGENT_DISABLED=0
|
||||
MOTION_AGENT_WSL_USBIPD=1
|
||||
if [ -n "$2" ]; then
|
||||
MOTION_AGENT_WSL_USBIPD_BUSID="$2"
|
||||
shift 2
|
||||
else
|
||||
log_error "--motion-agent-wsl-usbipd-busid 需要 usbipd list 中的 BUSID,例如 3-2"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-d|--database)
|
||||
DATABASE_REQUESTED=1
|
||||
shift 1
|
||||
@@ -3188,6 +3682,33 @@ print_process_health_status() {
|
||||
fi
|
||||
}
|
||||
|
||||
print_motion_agent_health_status() {
|
||||
local detail=""
|
||||
local tracked_pid=""
|
||||
|
||||
if pgrep -f "python.*-m motion_agent" >/dev/null 2>&1 || {
|
||||
[ -f "$MOTION_AGENT_PID_FILE" ] &&
|
||||
tracked_pid="$(read_pid_file "$MOTION_AGENT_PID_FILE" || true)" &&
|
||||
[ -n "$tracked_pid" ] &&
|
||||
kill -0 "$tracked_pid" 2>/dev/null
|
||||
}; then
|
||||
echo -e "${DIM} Motion Agent:${NC} ${GREEN}online${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -f "$MOTION_AGENT_SKIPPED_FILE" ]; then
|
||||
detail="$(sed -n 's/^reason=//p' "$MOTION_AGENT_SKIPPED_FILE" 2>/dev/null | head -n 1)"
|
||||
if [ -n "$detail" ]; then
|
||||
echo -e "${DIM} Motion Agent:${NC} ${YELLOW}skipped${NC} ${DIM}(${detail})${NC}"
|
||||
else
|
||||
echo -e "${DIM} Motion Agent:${NC} ${YELLOW}skipped${NC}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${DIM} Motion Agent:${NC} ${RED}offline${NC}"
|
||||
}
|
||||
|
||||
motion_agent_pids() {
|
||||
local pids=""
|
||||
|
||||
@@ -3214,6 +3735,73 @@ cleanup_motion_agent_processes() {
|
||||
remove_pid_file "$MOTION_AGENT_PID_FILE"
|
||||
}
|
||||
|
||||
mark_motion_agent_skipped() {
|
||||
local reason="$1"
|
||||
local detail="${2:-}"
|
||||
|
||||
mkdir -p "$PLANET_STATE_DIR"
|
||||
{
|
||||
printf "status=skipped\n"
|
||||
printf "time=%s\n" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
printf "reason=%s\n" "$reason"
|
||||
if [ -n "$detail" ]; then
|
||||
printf "detail=%s\n" "$detail"
|
||||
fi
|
||||
} > "$MOTION_AGENT_SKIPPED_FILE"
|
||||
typeset -g MOTION_AGENT_SKIPPED_THIS_RUN=1
|
||||
remove_pid_file "$MOTION_AGENT_PID_FILE"
|
||||
log_warn "Motion Agent skipped: ${reason}"
|
||||
if [ -n "$detail" ]; then
|
||||
log_note "$detail"
|
||||
fi
|
||||
}
|
||||
|
||||
clear_motion_agent_skipped_state() {
|
||||
typeset -g MOTION_AGENT_SKIPPED_THIS_RUN=0
|
||||
rm -f "$MOTION_AGENT_SKIPPED_FILE"
|
||||
}
|
||||
|
||||
release_motion_agent_port_for_start() {
|
||||
local port="$1"
|
||||
local pids=""
|
||||
local pid=""
|
||||
|
||||
cleanup_motion_agent_processes TERM
|
||||
if wait_for_port_release "$port" 25 0.2; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Motion Agent 端口 ${port} 清理后仍不可绑定,尝试强制清理"
|
||||
cleanup_motion_agent_processes KILL
|
||||
if wait_for_port_release "$port" 10 0.2; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
pids="$(collect_port_pids "$port" || true)"
|
||||
if [ -n "$pids" ]; then
|
||||
log_warn "发现 Motion Agent 端口 ${port} 仍有监听进程,正在强制终止"
|
||||
for pid in $pids; do
|
||||
terminate_process_group KILL "$pid"
|
||||
terminate_process_tree KILL "$pid"
|
||||
done
|
||||
if wait_for_port_release "$port" 10 0.2; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if force_cleanup_external_port_listener "$port" "Motion Agent"; then
|
||||
if wait_for_port_release "$port" 10 0.2; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
print_port_listener_details "$port"
|
||||
mark_motion_agent_skipped \
|
||||
"端口 ${port} 被外部环境占用,已跳过 Motion Agent 启动" \
|
||||
"其他服务会继续启动;需要动捕时请释放端口或改用 --motion-agent-port <端口>。"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_motion_agent_ready() {
|
||||
local port="$1"
|
||||
local pid="$2"
|
||||
@@ -3238,51 +3826,95 @@ start_motion_agent_service() {
|
||||
local dry_run="$2"
|
||||
local lan_enabled="${3:-0}"
|
||||
local bind_host="127.0.0.1"
|
||||
local auto_detected_camera_indexes=0
|
||||
local validated_camera_indexes=""
|
||||
local -a motion_args
|
||||
|
||||
clear_motion_agent_skipped_state
|
||||
ensure_uv_backend_deps
|
||||
if [ "$dry_run" -eq 0 ] && [ -z "$MOTION_AGENT_CAMERA_URLS" ] && [ -z "$MOTION_AGENT_CAMERA_INDEXES" ]; then
|
||||
MOTION_AGENT_CAMERA_INDEXES="$(detect_motion_agent_camera_indexes)"
|
||||
if [ -n "$MOTION_AGENT_CAMERA_INDEXES" ]; then
|
||||
auto_detected_camera_indexes=1
|
||||
log_note "Motion Agent 自动发现摄像头 index: ${MOTION_AGENT_CAMERA_INDEXES}"
|
||||
else
|
||||
if is_wsl_environment; then
|
||||
log_warn "Motion Agent 未自动发现 /dev/video* 摄像头"
|
||||
log_note "当前看起来是 WSL;Windows 摄像头通常不会自动出现在 /dev/video*。"
|
||||
log_note "真实识别请选择一种方式:"
|
||||
log_note " 1. 用 --motion-agent-camera-urls 接手机/RTSP/HTTP 摄像头流"
|
||||
log_note " 2. 用 usbipd-win 把 USB 摄像头透传到 WSL,再重试"
|
||||
log_note " 3. 仅验证 WebSocket/调试面板时,显式加 --motion-agent-dry-run"
|
||||
case "${PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK:-0}" in
|
||||
1|true|yes|on)
|
||||
log_warn "已设置 PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK=1,降级为 dry-run"
|
||||
dry_run=1
|
||||
;;
|
||||
*)
|
||||
MOTION_AGENT_CAMERA_MISSING=1
|
||||
;;
|
||||
esac
|
||||
if try_motion_agent_wsl_usbipd_camera; then
|
||||
auto_detected_camera_indexes=1
|
||||
:
|
||||
else
|
||||
log_warn "Motion Agent 未自动发现 /dev/video* 摄像头"
|
||||
log_note "当前看起来是 WSL;Windows 摄像头通常不会自动出现在 /dev/video*。"
|
||||
log_note "真实识别请选择一种方式:"
|
||||
log_note " 1. 用 --motion-agent-camera-urls 接手机/RTSP/HTTP 摄像头流"
|
||||
log_note " 2. 用 --motion-agent-wsl-usbipd 尝试通过 usbipd-win 自动透传唯一 USB 摄像头"
|
||||
log_note " 3. 用 --motion-agent-wsl-usbipd-busid <BUSID> 指定 usbipd list 中的摄像头"
|
||||
log_note " 4. 仅验证 WebSocket/调试面板时,显式加 --motion-agent-dry-run"
|
||||
case "${PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK:-0}" in
|
||||
1|true|yes|on)
|
||||
log_warn "已设置 PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK=1,降级为 dry-run"
|
||||
dry_run=1
|
||||
;;
|
||||
*)
|
||||
if [ "${MOTION_AGENT_EXPLICIT_REQUESTED:-0}" -eq 0 ]; then
|
||||
log_warn "默认启动 Motion Agent 未找到摄像头,降级为 dry-run 协议服务"
|
||||
dry_run=1
|
||||
else
|
||||
MOTION_AGENT_CAMERA_MISSING=1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
log_warn "Motion Agent 未自动发现 /dev/video* 摄像头,默认尝试 index 0"
|
||||
if [ "${MOTION_AGENT_EXPLICIT_REQUESTED:-0}" -eq 0 ]; then
|
||||
log_warn "默认启动 Motion Agent 未自动发现摄像头,降级为 dry-run 协议服务"
|
||||
dry_run=1
|
||||
else
|
||||
log_warn "Motion Agent 未自动发现 /dev/video* 摄像头,默认尝试 index 0"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "${MOTION_AGENT_CAMERA_MISSING:-0}" -eq 1 ]; then
|
||||
log_error "Motion Agent live 模式缺少可用摄像头"
|
||||
log_note "WSL 示例:./planet.sh restart -m --motion-agent-camera-urls http://<手机IP>:8080/video"
|
||||
log_note "WSL USB 示例:./planet.sh restart -m --motion-agent-wsl-usbipd"
|
||||
log_note "或仅调试协议:./planet.sh restart -m --motion-agent-dry-run"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$dry_run" -eq 0 ]; then
|
||||
ensure_motion_agent_live_deps
|
||||
if [ "$auto_detected_camera_indexes" -eq 1 ] && [ -z "$MOTION_AGENT_CAMERA_URLS" ]; then
|
||||
validated_camera_indexes="$(detect_motion_agent_camera_indexes_with_cv2 || true)"
|
||||
if [ -n "$validated_camera_indexes" ]; then
|
||||
if [ "$validated_camera_indexes" != "$MOTION_AGENT_CAMERA_INDEXES" ]; then
|
||||
log_warn "Motion Agent 过滤不可打开的摄像头 index: ${MOTION_AGENT_CAMERA_INDEXES} -> ${validated_camera_indexes}"
|
||||
fi
|
||||
MOTION_AGENT_CAMERA_INDEXES="$validated_camera_indexes"
|
||||
else
|
||||
if [ "${MOTION_AGENT_EXPLICIT_REQUESTED:-0}" -eq 0 ]; then
|
||||
log_warn "默认启动 Motion Agent 未找到可读帧摄像头,降级为 dry-run 协议服务"
|
||||
dry_run=1
|
||||
MOTION_AGENT_CAMERA_INDEXES=""
|
||||
else
|
||||
log_error "Motion Agent 未找到可打开并能读帧的摄像头 index"
|
||||
log_note "可手动指定:--motion-agent-camera-indexes <index>"
|
||||
log_note "WSL 下可改用:--motion-agent-camera-urls http://<手机IP>:8080/video"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if ! release_motion_agent_port_for_start "$motion_agent_port"; then
|
||||
return 0
|
||||
fi
|
||||
cleanup_motion_agent_processes TERM
|
||||
wait_for_port_release "$motion_agent_port" 10 0.2 || true
|
||||
|
||||
if ! can_bind_port "$motion_agent_port"; then
|
||||
log_error "Motion Agent 端口已被占用: ${motion_agent_port}"
|
||||
print_port_listener_details "$motion_agent_port"
|
||||
exit 1
|
||||
mark_motion_agent_skipped \
|
||||
"端口 ${motion_agent_port} 已被占用" \
|
||||
"未能释放 Motion Agent 端口,已跳过动捕服务;其他服务继续启动。"
|
||||
return 0
|
||||
fi
|
||||
|
||||
: > "$MOTION_AGENT_LOG_FILE"
|
||||
@@ -3290,7 +3922,7 @@ start_motion_agent_service() {
|
||||
bind_host="0.0.0.0"
|
||||
fi
|
||||
|
||||
motion_args=(-m motion_agent --host "$bind_host" --port "$motion_agent_port")
|
||||
motion_args=(-m motion_agent --host "$bind_host" --port "$motion_agent_port" --mode "$MOTION_AGENT_MODE")
|
||||
if [ -n "$MOTION_AGENT_CAMERA_URLS" ]; then
|
||||
motion_args+=(--camera-urls "$MOTION_AGENT_CAMERA_URLS")
|
||||
elif [ -n "$MOTION_AGENT_CAMERA_INDEXES" ]; then
|
||||
@@ -3315,6 +3947,7 @@ start_motion_agent_service() {
|
||||
print_port_listener_details "$motion_agent_port"
|
||||
log_note "如只需验证 Web 端连接,可加 --motion-agent-dry-run。"
|
||||
log_note "如需真实摄像头识别,请确认系统存在 /dev/video*;脚本会自动发现,也可用 --motion-agent-camera-indexes 1,2 覆盖。"
|
||||
log_note "输入模式可用 --motion-agent-mode auto/single/dual_redundant/single_fallback 指定。"
|
||||
log_note "WSL 下也可用 --motion-agent-camera-urls rtsp://... 或 http://... 接入手机/网络摄像头。"
|
||||
log_note "依赖缺失时脚本会自动执行 uv add mediapipe opencv-python。"
|
||||
cleanup_motion_agent_processes TERM
|
||||
@@ -3326,6 +3959,7 @@ stop_motion_agent_service() {
|
||||
cleanup_motion_agent_processes TERM
|
||||
log_halt "Motion Agent 已停止"
|
||||
fi
|
||||
clear_motion_agent_skipped_state
|
||||
}
|
||||
|
||||
cleanup_failed_start() {
|
||||
@@ -3467,6 +4101,8 @@ guard_init_when_services_running() {
|
||||
}
|
||||
|
||||
init() {
|
||||
parse_service_args "$@"
|
||||
|
||||
if ! guard_init_when_services_running; then
|
||||
return 0
|
||||
fi
|
||||
@@ -3492,6 +4128,8 @@ init() {
|
||||
ensure_planet_env_files
|
||||
log_success "环境变量文件已就绪"
|
||||
|
||||
prepare_motion_agent_host_dependencies
|
||||
|
||||
start_wait_session "启动数据库服务"
|
||||
ensure_database_services_healthy
|
||||
stop_wait_session
|
||||
@@ -3704,7 +4342,9 @@ start() {
|
||||
typeset -g STARTED_FRONTEND_THIS_RUN=1
|
||||
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
start_motion_agent_service "$MOTION_AGENT_PORT" "$MOTION_AGENT_DRY_RUN" "$FRONTEND_LAN_ENABLED"
|
||||
typeset -g STARTED_MOTION_AGENT_THIS_RUN=1
|
||||
if [ "${MOTION_AGENT_SKIPPED_THIS_RUN:-0}" -eq 0 ]; then
|
||||
typeset -g STARTED_MOTION_AGENT_THIS_RUN=1
|
||||
fi
|
||||
fi
|
||||
|
||||
local frontend_scheme=""
|
||||
@@ -3719,7 +4359,7 @@ start() {
|
||||
log_note "智能星球仪表盘: ${frontend_scheme}://localhost:${FRONTEND_PORT}/admin"
|
||||
log_note "AI Playground: ${frontend_scheme}://localhost:${FRONTEND_PORT}/playground"
|
||||
log_note "智能星球文档: ${frontend_scheme}://localhost:${FRONTEND_PORT}/docs"
|
||||
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ] && [ "${MOTION_AGENT_SKIPPED_THIS_RUN:-0}" -eq 0 ]; then
|
||||
log_motion_agent_access_notes "$MOTION_AGENT_PORT" "$FRONTEND_LAN_ENABLED"
|
||||
fi
|
||||
if [ "$FRONTEND_LAN_ENABLED" -eq 1 ]; then
|
||||
@@ -3878,7 +4518,7 @@ restart() {
|
||||
local state_ai_provider_port=""
|
||||
local state_motion_agent_port=""
|
||||
|
||||
if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ] && [ "$AI_PROVIDER_REQUESTED" -eq 0 ] && [ "$MOTION_AGENT_REQUESTED" -eq 0 ] && [ "$DATABASE_REQUESTED" -eq 0 ]; then
|
||||
if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ] && [ "$AI_PROVIDER_REQUESTED" -eq 0 ] && [ "$MOTION_AGENT_EXPLICIT_REQUESTED" -eq 0 ] && [ "$DATABASE_REQUESTED" -eq 0 ]; then
|
||||
stop_local_services_for_restart
|
||||
sleep 1
|
||||
start "$@"
|
||||
@@ -3912,9 +4552,10 @@ restart() {
|
||||
start_frontend_service "$FRONTEND_PORT" 1 "$FRONTEND_LAN_ENABLED" "$BACKEND_PORT"
|
||||
fi
|
||||
|
||||
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
if [ "$MOTION_AGENT_EXPLICIT_REQUESTED" -eq 1 ] && [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
stop_motion_agent_service
|
||||
sleep 1
|
||||
typeset -g MOTION_AGENT_SKIPPED_THIS_RUN=0
|
||||
start_motion_agent_service "$MOTION_AGENT_PORT" "$MOTION_AGENT_DRY_RUN" "$FRONTEND_LAN_ENABLED"
|
||||
fi
|
||||
|
||||
@@ -3933,7 +4574,7 @@ restart() {
|
||||
if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then
|
||||
state_ai_provider_port="$AI_PROVIDER_PORT"
|
||||
fi
|
||||
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
if [ "$MOTION_AGENT_EXPLICIT_REQUESTED" -eq 1 ] && [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
state_motion_agent_port="$MOTION_AGENT_PORT"
|
||||
fi
|
||||
local frontend_scheme=""
|
||||
@@ -3957,7 +4598,7 @@ restart() {
|
||||
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" "$frontend_scheme"
|
||||
fi
|
||||
fi
|
||||
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
|
||||
if [ "$MOTION_AGENT_EXPLICIT_REQUESTED" -eq 1 ] && [ "$MOTION_AGENT_REQUESTED" -eq 1 ] && [ "${MOTION_AGENT_SKIPPED_THIS_RUN:-0}" -eq 0 ]; then
|
||||
log_motion_agent_access_notes "$MOTION_AGENT_PORT" "$FRONTEND_LAN_ENABLED"
|
||||
fi
|
||||
}
|
||||
@@ -3979,7 +4620,7 @@ health() {
|
||||
print_http_health_status "后端" "http://localhost:${backend_port}/health"
|
||||
print_http_health_status "AI Provider" "http://localhost:${ai_provider_port}/health"
|
||||
print_frontend_health_status "$frontend_port"
|
||||
print_process_health_status "Motion Agent" "python.*-m motion_agent" "$MOTION_AGENT_PID_FILE"
|
||||
print_motion_agent_health_status
|
||||
}
|
||||
|
||||
log() {
|
||||
@@ -4041,7 +4682,8 @@ set -- "${GLOBAL_ARG_REMAINDER[@]}"
|
||||
|
||||
case "$1" in
|
||||
init)
|
||||
init
|
||||
shift
|
||||
init "$@"
|
||||
;;
|
||||
start)
|
||||
shift
|
||||
@@ -4070,10 +4712,10 @@ case "$1" in
|
||||
log_error "用法: ./planet.sh {init|start|stop|destroy|restart|createuser|health|log}"
|
||||
log_note "全局参数: -v, --verbose 在状态提示之间增量输出命令日志"
|
||||
log_note "init 首次初始化空项目: 同步 uv/bun 依赖、生成缺失 env、启动数据库并写入默认数据"
|
||||
log_note "start 启动服务,可选: -b <后端端口> -f <前端端口> -a <AI Provider 端口> -m/--motion-agent --motion-agent-port <端口> --motion-agent-camera-indexes 0,1 --motion-agent-camera-urls rtsp://... --motion-agent-dry-run --allow-lan --verbose"
|
||||
log_note "start 启动服务,默认包含 Motion Agent;可选: -b <后端端口> -f <前端端口> -a <AI Provider 端口> --non-motion-agent -m/--motion-agent --motion-agent-port <端口> --motion-agent-mode auto|single|dual_redundant|single_fallback --motion-agent-camera-indexes 0,1 --motion-agent-camera-urls rtsp://... --motion-agent-wsl-usbipd --motion-agent-wsl-usbipd-busid <BUSID> --motion-agent-dry-run --allow-lan --verbose"
|
||||
log_note "stop 停止服务"
|
||||
log_note "destroy 删除 Planet 容器、卷、镜像和本地编译状态,执行前需要输入 Y 确认"
|
||||
log_note "restart 重启服务,可选: -b [后端端口] -f [前端端口] -a [AI Provider 端口] -m [Motion Agent] -d --allow-lan --verbose"
|
||||
log_note "restart 重启服务,默认包含 Motion Agent;可选: -b [后端端口] -f [前端端口] -a [AI Provider 端口] --non-motion-agent -m [Motion Agent] -d --allow-lan --verbose"
|
||||
log_note "createuser 交互创建用户"
|
||||
log_note "health 检查健康状态"
|
||||
log_note "log 查看日志"
|
||||
|
||||
Reference in New Issue
Block a user