Files
ozon-seller-kit/start.command
2026-09-27 22:41:33 +08:00

242 lines
9.2 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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=""
WORKER_SOURCE_WATCH_PID=""
COLLECTOR_WATCH_PID=""
COLLECTOR_STAMP=""
WORKER_SOURCE_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 "$WORKER_SOURCE_WATCH_PID"
stop_process_tree "$COLLECTOR_WATCH_PID"
[[ -n "$COLLECTOR_STAMP" ]] && rm -f "$COLLECTOR_STAMP" 2>/dev/null
[[ -n "$WORKER_SOURCE_STAMP" ]] && rm -f "$WORKER_SOURCE_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)"
# Uvicorn 的热更新只管理 Web 进程;Worker 独立运行,代码更新后必须主动重启,
# 否则它会继续用内存中已导入的旧 generator.py 提交模型任务。
WORKER_SOURCE_STAMP="$(mktemp /tmp/osk-worker-source.XXXXXX)"
watch_worker_source() {
while true; do
sleep 2
if [[ -n "$(find server -type f \( -name '*.py' -o -name '*.yaml' \) -newer "$WORKER_SOURCE_STAMP" -print -quit 2>/dev/null)" ]]; then
touch "$WORKER_SOURCE_STAMP"
echo "🔧 检测到 Worker 源码改动,正在重启 Worker 以应用新版本…"
for worker_pid in $(pgrep -P "$WORKER_SUPERVISOR_PID" 2>/dev/null); do
kill -TERM "$worker_pid" 2>/dev/null || true
done
fi
done
}
watch_worker_source &
WORKER_SOURCE_WATCH_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