Files
ggx/deploy/install-service.sh
simon fb6608193b 功能:Web 前端与报告格式化(工作区中此前未提交的工作)
说明:本提交**不是本轮会话所做**,而是工作区里此前遗留的未提交改动。
为把历史分开,先单独提交它,再提交本轮会话的修改。

包含:
- Web 前端:web/index.html、web/app.js(统一 SPA,含回测/画像/Walk-forward 页面)
- 后端接口:web/server.py 路由、web/analysis.py(新增个股分析)
- 报告层:report/format.py(新增统一数字格式化 NumFmt)、
  report/{backtest,profile,sensitivity,universe,walkforward}_report.py 接入 NumFmt、
  report/renderer.py
- 股息率口径:factor/dividend_yield.py(毛刺消除 smooth_spikes)
- 筛选:universe/selector.py、universe/filters/dividend.py
- 绩效/敏感性:analysis/performance.py、analysis/sensitivity.py
- 部署:deploy/install-service.sh
- 测试:tests/test_format.py、tests/test_dividend_smoothing.py(新增)、
  tests/test_web.py、tests/test_universe.py

提交时全量测试 403 项通过。
2026-10-04 12:47:10 +08:00

111 lines
4.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.
#!/usr/bin/env bash
# ============================================================
# 把后端 API 服务注册为 macOS launchd 用户级服务(登录自启 + 崩溃自愈)
#
# 用法:
# ./deploy/install-service.sh install 安装并启动(幂等,可重复执行)
# ./deploy/install-service.sh uninstall 停止并移除
# ./deploy/install-service.sh status 查看状态与连通性
# ./deploy/install-service.sh reinstall 重新渲染 plist 并重启
# ./deploy/install-service.sh restart 只重启进程(改完 src/ 或 config/ 后执行)
#
# 为什么需要:nginx 由 brew services 托管、开机自启。若后端只是 nohup 进程,
# 机器重启后它就不在了 —— 页面能打开但会显示「API 不可用」。
#
# 为什么 restart 是刚需:Python 进程把 hdiv 模块与 config/*.yml 读进了内存
# (配置经 lru_cache 缓存),改完源码或配置**不会**自动生效。不重启就会出现
# 「一边读新配置、一边用旧模型校验」的假故障,例如给 datasource.yml 加了
# 新字段却报 ``Extra inputs are not permitted``。
#
# 若你不用 launchd,也可用 ./deploy/serve.sh start 手动启动(重启后需再次执行)。
# ============================================================
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="${PROJECT_ROOT}/deploy/com.hddiv.web.plist.example"
TARGET="${HOME}/Library/LaunchAgents/com.hddiv.web.plist"
LABEL="com.hddiv.web"
PYTHON="${PROJECT_ROOT}/.venv/bin/python"
PORT="${HDDIV_PORT:-8099}"
HOST="${HDDIV_HOST:-127.0.0.1}"
die() { echo "错误: $*" >&2; exit 1; }
is_loaded() { launchctl list 2>/dev/null | grep -q "^[0-9-]*[[:space:]]*[0-9-]*[[:space:]]*${LABEL}$"; }
render() {
[[ -f "${TEMPLATE}" ]] || die "缺少模板:${TEMPLATE}"
[[ -x "${PYTHON}" ]] || die "虚拟环境不可用:${PYTHON}\n请先创建 .venv 并安装依赖"
mkdir -p "${PROJECT_ROOT}/logs" "${HOME}/Library/LaunchAgents"
# 路径含中文与空格,用 sed 的 | 分隔符避免转义问题
sed -e "s|__PYTHON__|${PYTHON}|g" \
-e "s|__PROJECT_ROOT__|${PROJECT_ROOT}|g" \
"${TEMPLATE}" > "${TARGET}"
# 占位符必须全部被替换,否则 launchd 会静默失败
if grep -q "__" "${TARGET}"; then
die "plist 中仍有未替换的占位符:$(grep -o '__[A-Z_]*__' "${TARGET}" | sort -u | tr '\n' ' ')"
fi
plutil -lint "${TARGET}" >/dev/null || die "plist 格式非法:${TARGET}"
echo "已渲染 ${TARGET}"
}
cmd_install() {
render
if is_loaded; then
echo "服务已加载,重新加载以应用新配置…"
launchctl unload -w "${TARGET}" 2>/dev/null || true
fi
launchctl load -w "${TARGET}"
sleep 3
cmd_status
}
cmd_uninstall() {
if [[ -f "${TARGET}" ]]; then
launchctl unload -w "${TARGET}" 2>/dev/null || true
rm -f "${TARGET}"
echo "已停止并移除 ${TARGET}"
else
echo "${TARGET} 不存在,跳过"
fi
}
cmd_status() {
echo "=== launchd 服务 ==="
if is_loaded; then
launchctl list | grep "${LABEL}" | awk '{printf " PID=%s 上次退出码=%s %s\n", $1, $2, $3}'
echo " (PID 为 - 表示当前未运行)"
else
echo " 未加载"
fi
echo "=== 后端直连 ==="
if out="$(curl -sS --max-time 3 "http://${HOST}:${PORT}/api/health" 2>&1)"; then
echo " http://${HOST}:${PORT}/api/health -> ${out}"
else
echo " 无响应:${out}"
fi
echo "=== 日志 ==="
echo " ${PROJECT_ROOT}/logs/web.log"
echo " ${PROJECT_ROOT}/logs/web.err.log"
[[ -f "${PROJECT_ROOT}/logs/web.err.log" ]] && \
tail -3 "${PROJECT_ROOT}/logs/web.err.log" 2>/dev/null | sed 's/^/ /' || true
}
cmd_restart() {
is_loaded || die "服务未加载,先执行:./deploy/install-service.sh install"
# kickstart -k 先杀旧进程再拉新进程;PID 会变,所以顺手打印状态
launchctl kickstart -k "gui/$(id -u)/${LABEL}"
sleep 3
cmd_status
}
case "${1:-install}" in
install) cmd_install ;;
reinstall) render; launchctl unload -w "${TARGET}" 2>/dev/null || true
launchctl load -w "${TARGET}"; sleep 3; cmd_status ;;
restart) cmd_restart ;;
uninstall) cmd_uninstall ;;
status) cmd_status ;;
*) die "未知动作:$1(可用:install|reinstall|restart|uninstall|status)" ;;
esac