183 lines
5.2 KiB
Bash
Executable File
183 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# 一键启动本地 WebUI:
|
||
# - 后端 FastAPI: 127.0.0.1:8765
|
||
# - 前端 Next.js: 127.0.0.1:3000
|
||
# - PID 和日志统一写入 .port_sessions,重复执行会清理上一次由本脚本启动的进程。
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
RUN_DIR="${ROOT_DIR}/.port_sessions"
|
||
ENV_FILE="${ROOT_DIR}/.env.deploy"
|
||
if [[ -f "${ENV_FILE}" ]]; then
|
||
# shellcheck disable=SC1090
|
||
source "${ENV_FILE}"
|
||
fi
|
||
BACKEND_HOST="${CLAW_BACKEND_HOST:-0.0.0.0}"
|
||
BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}"
|
||
FRONTEND_HOST="${CLAW_FRONTEND_HOST:-0.0.0.0}"
|
||
FRONTEND_PORT="${CLAW_FRONTEND_PORT:-3000}"
|
||
BACKEND_URL="http://${BACKEND_HOST}:${BACKEND_PORT}"
|
||
FRONTEND_URL="http://${FRONTEND_HOST}:${FRONTEND_PORT}"
|
||
PYTHON_BIN="${CLAW_PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}"
|
||
if [[ ! -x "${PYTHON_BIN}" ]]; then
|
||
PYTHON_BIN="python3"
|
||
fi
|
||
LOCAL_IPV4_ORIGINS="$(ifconfig 2>/dev/null | awk '/inet / && $2 !~ /^127\\./ {print $2}' | awk '!seen[$0]++' | paste -sd, -)"
|
||
DEFAULT_ALLOWED_DEV_ORIGINS="localhost,127.0.0.1,0.0.0.0"
|
||
if [[ -n "${LOCAL_IPV4_ORIGINS}" ]]; then
|
||
DEFAULT_ALLOWED_DEV_ORIGINS="${DEFAULT_ALLOWED_DEV_ORIGINS},${LOCAL_IPV4_ORIGINS}"
|
||
while IFS= read -r local_ip; do
|
||
[[ -z "${local_ip}" ]] && continue
|
||
DEFAULT_ALLOWED_DEV_ORIGINS="${DEFAULT_ALLOWED_DEV_ORIGINS},${local_ip}:${FRONTEND_PORT}"
|
||
done < <(printf "%s\n" "${LOCAL_IPV4_ORIGINS}" | tr "," "\n")
|
||
fi
|
||
|
||
mkdir -p "${RUN_DIR}"
|
||
|
||
stop_pid_file() {
|
||
local pid_file="$1"
|
||
local name="$2"
|
||
if [[ ! -f "${pid_file}" ]]; then
|
||
return
|
||
fi
|
||
local pid
|
||
pid="$(cat "${pid_file}" 2>/dev/null || true)"
|
||
if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
|
||
echo "Stopping previous ${name} (${pid})..."
|
||
kill "${pid}" 2>/dev/null || true
|
||
for _ in {1..20}; do
|
||
if ! kill -0 "${pid}" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
if kill -0 "${pid}" 2>/dev/null; then
|
||
kill -9 "${pid}" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
rm -f "${pid_file}"
|
||
}
|
||
|
||
stop_port_listener() {
|
||
local port="$1"
|
||
local name="$2"
|
||
local pids
|
||
pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
|
||
if [[ -z "${pids}" ]]; then
|
||
return
|
||
fi
|
||
echo "Stopping ${name} listener(s) on port ${port}: ${pids//$'\n'/ }..."
|
||
while IFS= read -r pid; do
|
||
[[ -z "${pid}" ]] && continue
|
||
kill "${pid}" 2>/dev/null || true
|
||
done <<<"${pids}"
|
||
for _ in {1..20}; do
|
||
if ! lsof -tiTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||
return
|
||
fi
|
||
sleep 0.1
|
||
done
|
||
while IFS= read -r pid; do
|
||
[[ -z "${pid}" ]] && continue
|
||
kill -9 "${pid}" 2>/dev/null || true
|
||
done <<<"${pids}"
|
||
}
|
||
|
||
wait_for_url() {
|
||
local url="$1"
|
||
local name="$2"
|
||
local log_file="$3"
|
||
for _ in {1..80}; do
|
||
if curl -fsS "${url}" >/dev/null 2>&1; then
|
||
echo "${name} ready: ${url}"
|
||
return 0
|
||
fi
|
||
sleep 0.25
|
||
done
|
||
echo "${name} did not become ready: ${url}" >&2
|
||
echo "---- ${name} log tail ----" >&2
|
||
tail -80 "${log_file}" >&2 || true
|
||
return 1
|
||
}
|
||
|
||
shell_quote() {
|
||
printf "%q" "$1"
|
||
}
|
||
|
||
start_detached_process() {
|
||
local name="$1"
|
||
local pid_file="$2"
|
||
local log_file="$3"
|
||
local work_dir="$4"
|
||
shift 4
|
||
local command="cd $(shell_quote "${work_dir}") && echo \$\$ > $(shell_quote "${pid_file}") && exec"
|
||
local arg
|
||
for arg in "$@"; do
|
||
command+=" $(shell_quote "${arg}")"
|
||
done
|
||
command+=" >$(shell_quote "${log_file}") 2>&1"
|
||
|
||
if command -v screen >/dev/null 2>&1; then
|
||
screen -dmS "claw-${name}" bash -lc "${command}"
|
||
else
|
||
(
|
||
cd "${work_dir}"
|
||
nohup "$@" >"${log_file}" 2>&1 &
|
||
echo "$!" >"${pid_file}"
|
||
)
|
||
fi
|
||
}
|
||
|
||
stop_pid_file "${RUN_DIR}/webui-frontend.pid" "frontend"
|
||
stop_pid_file "${RUN_DIR}/webui-backend.pid" "backend"
|
||
stop_port_listener "${FRONTEND_PORT}" "frontend"
|
||
stop_port_listener "${BACKEND_PORT}" "backend"
|
||
|
||
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
||
echo "OPENAI_API_KEY is not configured." >&2
|
||
echo "Run scripts/deploy-ubuntu.sh once, or create ${ENV_FILE} from .env.deploy.example." >&2
|
||
fi
|
||
export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
|
||
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
|
||
export OPENAI_MODEL="${OPENAI_MODEL:-tongyi/deepseek-v4-pro}"
|
||
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
|
||
|
||
BACKEND_LOG="${RUN_DIR}/webui-backend.log"
|
||
FRONTEND_LOG="${RUN_DIR}/webui-frontend.log"
|
||
|
||
echo "Starting backend on ${BACKEND_URL}..."
|
||
start_detached_process "backend" "${RUN_DIR}/webui-backend.pid" "${BACKEND_LOG}" "${ROOT_DIR}" \
|
||
"${PYTHON_BIN}" -m src.gui \
|
||
--host "${BACKEND_HOST}" \
|
||
--port "${BACKEND_PORT}" \
|
||
--cwd "${ROOT_DIR}" \
|
||
--timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \
|
||
--session-dir "${RUN_DIR}/agent" \
|
||
--allow-write \
|
||
--allow-shell \
|
||
--no-browser
|
||
|
||
wait_for_url "${BACKEND_URL}/api/state" "backend" "${BACKEND_LOG}"
|
||
|
||
echo "Starting frontend on ${FRONTEND_URL}..."
|
||
export CLAW_API_URL="${BACKEND_URL}"
|
||
export CLAW_ALLOWED_DEV_ORIGINS="${CLAW_ALLOWED_DEV_ORIGINS:-${DEFAULT_ALLOWED_DEV_ORIGINS}}"
|
||
start_detached_process "frontend" "${RUN_DIR}/webui-frontend.pid" "${FRONTEND_LOG}" "${ROOT_DIR}/frontend/app" \
|
||
npm run dev -- --hostname "${FRONTEND_HOST}" --port "${FRONTEND_PORT}"
|
||
|
||
wait_for_url "${FRONTEND_URL}" "frontend" "${FRONTEND_LOG}"
|
||
|
||
cat <<EOF
|
||
|
||
WebUI started.
|
||
Frontend: ${FRONTEND_URL}
|
||
Backend : ${BACKEND_URL}
|
||
Logs : ${RUN_DIR}/webui-frontend.log
|
||
${RUN_DIR}/webui-backend.log
|
||
|
||
Stop:
|
||
kill \$(cat "${RUN_DIR}/webui-frontend.pid") \$(cat "${RUN_DIR}/webui-backend.pid")
|
||
|
||
EOF
|