220 lines
8.3 KiB
Bash
Executable File
220 lines
8.3 KiB
Bash
Executable File
#!/bin/zsh
|
||
# 双击本文件,或在终端执行:./start.command
|
||
cd "$(dirname "$0")" || exit 1
|
||
PROJECT_DIR="$PWD"
|
||
|
||
BACKEND_HOST="127.0.0.1"
|
||
BACKEND_PORT="8800"
|
||
BACKEND_URL="http://${BACKEND_HOST}:${BACKEND_PORT}"
|
||
STUDIO_URL="http://localhost:8900"
|
||
RUNTIME_DIR="$PROJECT_DIR/data/runtime"
|
||
INSTANCE_PID_FILE="$RUNTIME_DIR/start-command.pid"
|
||
# 两个前端项目均锁定 pnpm 10.32.1。系统的 `pnpm` 是 Corepack shim,
|
||
# 其默认版本可能随 Node 更新而变化,因此显式指定项目所需的版本。
|
||
PNPM_VERSION="10.32.1"
|
||
|
||
project_pnpm() {
|
||
corepack "pnpm@${PNPM_VERSION}" "$@"
|
||
}
|
||
|
||
# 由当前 start.command 管理的子进程。它们都在清理函数里按 PID 树结束,
|
||
# 不扫描或误杀其他项目的 Python / Node 进程。
|
||
BACKEND_PID=""
|
||
STUDIO_PID=""
|
||
WORKER_SUPERVISOR_PID=""
|
||
COLLECTOR_WATCH_PID=""
|
||
COLLECTOR_STAMP=""
|
||
CLEANUP_DONE=false
|
||
|
||
stop_process_tree() {
|
||
local pid="$1"
|
||
local child
|
||
[[ "$pid" == <-> ]] || return 0
|
||
kill -0 "$pid" 2>/dev/null || return 0
|
||
# 先停止子进程,避免 Worker 守护循环在父进程退出前重新拉起 Worker。
|
||
for child in $(pgrep -P "$pid" 2>/dev/null); do
|
||
stop_process_tree "$child"
|
||
done
|
||
kill -TERM "$pid" 2>/dev/null || true
|
||
}
|
||
|
||
cleanup() {
|
||
[[ "$CLEANUP_DONE" == true ]] && return
|
||
CLEANUP_DONE=true
|
||
stop_process_tree "$BACKEND_PID"
|
||
stop_process_tree "$STUDIO_PID"
|
||
stop_process_tree "$WORKER_SUPERVISOR_PID"
|
||
stop_process_tree "$COLLECTOR_WATCH_PID"
|
||
[[ -n "$COLLECTOR_STAMP" ]] && rm -f "$COLLECTOR_STAMP" 2>/dev/null
|
||
if [[ -f "$INSTANCE_PID_FILE" ]] && [[ "$(<"$INSTANCE_PID_FILE")" == "$$" ]]; then
|
||
rm -f "$INSTANCE_PID_FILE"
|
||
fi
|
||
}
|
||
|
||
on_signal() {
|
||
exit 0
|
||
}
|
||
|
||
stop_previous_instance() {
|
||
[[ -f "$INSTANCE_PID_FILE" ]] || return 0
|
||
local previous_pid command cwd
|
||
previous_pid="$(<"$INSTANCE_PID_FILE")"
|
||
if [[ "$previous_pid" != <-> ]] || ! kill -0 "$previous_pid" 2>/dev/null; then
|
||
rm -f "$INSTANCE_PID_FILE"
|
||
return 0
|
||
fi
|
||
|
||
command="$(ps -p "$previous_pid" -o command= 2>/dev/null)"
|
||
cwd="$(lsof -a -p "$previous_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1)"
|
||
if [[ "$command" != *"start.command"* || "$cwd" != "$PROJECT_DIR" ]]; then
|
||
echo "⚠️ 发现过期的启动记录,未匹配到本项目进程,已忽略。"
|
||
rm -f "$INSTANCE_PID_FILE"
|
||
return 0
|
||
fi
|
||
|
||
echo "检测到已有工具台实例(PID: $previous_pid),正在停止后重新启动…"
|
||
stop_process_tree "$previous_pid"
|
||
for _ in {1..50}; do
|
||
kill -0 "$previous_pid" 2>/dev/null || break
|
||
sleep 0.1
|
||
done
|
||
if kill -0 "$previous_pid" 2>/dev/null; then
|
||
echo "⚠️ 旧实例未能停止;为避免启动重复 Worker,本次不启动新实例。"
|
||
exit 1
|
||
fi
|
||
rm -f "$INSTANCE_PID_FILE"
|
||
}
|
||
|
||
stop_unmanaged_project_workers() {
|
||
# PID 文件只能管理此前由 start.command 拉起的实例;这里额外收敛曾被手动
|
||
# 启动、但 cwd 明确属于当前项目的 Worker。绝不按 Python 名称做全局清理。
|
||
local pid parent_pid command cwd parent_command parent_cwd
|
||
for pid in $(pgrep -f 'server/worker\.py' 2>/dev/null); do
|
||
command="$(ps -p "$pid" -o command= 2>/dev/null)"
|
||
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1)"
|
||
[[ "$command" == *"server/worker.py"* && "$cwd" == "$PROJECT_DIR" ]] || continue
|
||
|
||
parent_pid="$(ps -p "$pid" -o ppid= 2>/dev/null | tr -d ' ')"
|
||
parent_command="$(ps -p "$parent_pid" -o command= 2>/dev/null)"
|
||
parent_cwd="$(lsof -a -p "$parent_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1)"
|
||
echo "检测到遗留 Worker(PID: $pid),正在停止…"
|
||
if [[ "$parent_command" == *"start.command"* && "$parent_cwd" == "$PROJECT_DIR" ]]; then
|
||
# 这是脱离 PID 文件的旧守护循环;连同父进程停掉,避免它 2 秒后重拉 Worker。
|
||
stop_process_tree "$parent_pid"
|
||
else
|
||
# 手动启动的 Worker 只停止自身,不触碰其可能属于其他工具的父终端。
|
||
kill -TERM "$pid" 2>/dev/null || true
|
||
fi
|
||
done
|
||
}
|
||
|
||
mkdir -p "$RUNTIME_DIR" || exit 1
|
||
stop_previous_instance
|
||
stop_unmanaged_project_workers
|
||
print -r -- "$$" > "$INSTANCE_PID_FILE"
|
||
trap cleanup EXIT
|
||
trap on_signal INT TERM HUP
|
||
|
||
if [[ ! -d .venv ]]; then
|
||
echo "未找到 .venv,正在创建…"
|
||
python3 -m venv .venv || exit 1
|
||
fi
|
||
|
||
source .venv/bin/activate
|
||
# 每次启动都同步依赖,requirements.txt 有更新时自动补装(已装满时耗时极短)
|
||
pip install -q -r server/requirements.txt || exit 1
|
||
|
||
if [[ ! -f .env ]]; then
|
||
echo "未找到 .env,已从 .env.example 复制,请填入 DEEPSEEK_API_KEY 后再启动。"
|
||
cp .env.example .env
|
||
echo "按回车关闭…"
|
||
read -r
|
||
exit 1
|
||
fi
|
||
|
||
# 数据结构变更由 Alembic 管理;必须先迁移,再启动会读取新字段的服务端。
|
||
if ! alembic upgrade head; then
|
||
echo "⚠️ 数据库迁移失败,停止启动以避免新旧代码与表结构不一致"
|
||
exit 1
|
||
fi
|
||
|
||
# ---- 采集插件(extensions/collector):启动时构建一次,之后监听代码改动自动重建 ----
|
||
# 产物 .output/chrome-mv3 由 Chrome 以解压方式加载;重建后仍需去 chrome://extensions 点刷新
|
||
COLLECTOR_STAMP="$(mktemp -u /tmp/osk-collector-stamp.XXXXXX)"
|
||
watch_collector() {
|
||
touch "$COLLECTOR_STAMP"
|
||
while true; do
|
||
sleep 3
|
||
if [[ -n "$(find extensions/collector/src extensions/collector/wxt.config.ts -newer "$COLLECTOR_STAMP" -print 2>/dev/null)" ]]; then
|
||
touch "$COLLECTOR_STAMP"
|
||
echo "🔧 检测到插件代码改动,重新构建…"
|
||
if project_pnpm -C extensions/collector build >/dev/null 2>&1; then
|
||
echo "✅ 采集插件已重建($(date +%H:%M:%S)),去 chrome://extensions 点刷新生效"
|
||
else
|
||
echo "⚠️ 采集插件重建失败($(date +%H:%M:%S)):可手动跑 pnpm -C extensions/collector build 看报错"
|
||
fi
|
||
fi
|
||
done
|
||
}
|
||
|
||
if [[ ! -d extensions/collector ]]; then
|
||
echo "⚠️ 未找到 extensions/collector,跳过采集插件构建"
|
||
elif ! command -v corepack >/dev/null 2>&1; then
|
||
echo "⚠️ 未找到 Corepack,本次跳过采集插件构建(可手动:corepack pnpm@${PNPM_VERSION} -C extensions/collector build)"
|
||
elif ! CI=true project_pnpm -C extensions/collector install --silent; then
|
||
echo "⚠️ 采集插件依赖安装失败,本次跳过构建"
|
||
else
|
||
echo "正在构建采集插件…"
|
||
if project_pnpm -C extensions/collector build; then
|
||
echo "采集插件已构建:extensions/collector/.output/chrome-mv3"
|
||
else
|
||
echo "⚠️ 采集插件构建失败,继续启动服务(可手动重试:pnpm -C extensions/collector build)"
|
||
fi
|
||
watch_collector &
|
||
COLLECTOR_WATCH_PID=$!
|
||
fi
|
||
echo
|
||
|
||
# ---- 持久化后台任务 Worker(与 uvicorn 独立,服务重启不会中断正在轮询的模型任务)----
|
||
# Worker 因配置/网络等未捕获错误退出时,2 秒后自动拉起;否则任务会长期停在「排队中」。
|
||
supervise_worker() {
|
||
while true; do
|
||
python server/worker.py
|
||
WORKER_EXIT_CODE=$?
|
||
echo "⚠️ 后台任务 Worker 已退出(code: $WORKER_EXIT_CODE),2 秒后自动重启…"
|
||
sleep 2
|
||
done
|
||
}
|
||
supervise_worker &
|
||
WORKER_SUPERVISOR_PID=$!
|
||
echo "后台任务 Worker 守护进程已启动(PID: $WORKER_SUPERVISOR_PID)"
|
||
|
||
if ! command -v corepack >/dev/null 2>&1; then
|
||
echo "⚠️ 未找到 Corepack,本次跳过 studio 前端(可手动:corepack pnpm@${PNPM_VERSION} -C studio dev)"
|
||
# CI=true:node_modules 与 lockfile 不一致时自动重建,不弹确认
|
||
elif CI=true project_pnpm -C studio install --silent; then
|
||
project_pnpm -C studio dev & # vite:8900,启动后会自动打开浏览器
|
||
STUDIO_PID=$!
|
||
else
|
||
echo "⚠️ studio 依赖安装失败,本次只启动后端"
|
||
fi
|
||
|
||
echo
|
||
echo "前端访问地址:"
|
||
echo " 工具台 v1(web/) $BACKEND_URL/ozonSeller.html"
|
||
echo " 发布工作台(studio/) $STUDIO_URL"
|
||
echo " 后端 API 文档 $BACKEND_URL/docs"
|
||
echo
|
||
echo "按 Ctrl+C 可停止全部服务"
|
||
echo
|
||
|
||
# --app-dir server 让 uvicorn 在 server/ 下找 main:app,工作目录仍是仓库根(.env 在此)。
|
||
# 后端改为后台子进程 + wait:start.command 本身始终存活,PID 文件和 EXIT 清理才能可靠生效。
|
||
uvicorn main:app --app-dir server --reload --reload-include '*.yaml' --host "$BACKEND_HOST" --port "$BACKEND_PORT" &
|
||
BACKEND_PID=$!
|
||
wait "$BACKEND_PID"
|
||
|
||
echo
|
||
echo "服务已停止。按回车关闭窗口…"
|
||
read -r
|