Files
JX-BPOS/start.sh
T
ChaiHaoandClaude Opus 5 259192f736 人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
2026-08-02 13:41:44 +08:00

173 lines
6.5 KiB
Bash
Executable File

#!/bin/bash
# 一键重启开发服务 —— 起完**核实到 200 才算数**。
#
# ./start.sh 重启 api + web(默认)
# ./start.sh api 只重启 api(改了后端用这个;前端有 HMR,一般不必重启)
# ./start.sh web 只重启 web
# ./start.sh status 只看现在什么状态,一个进程都不动
# ./start.sh stop 停掉两个
#
# 为什么要有它 —— 手工重启踩过的坑,这里逐条堵上:
# ① `lsof -ti:5173` 会把**浏览器那条已建立的连接**也算成端口占用者,
# 按它 kill -9 就是杀用户的浏览器。所以一律 `-sTCP:LISTEN`,只认真属主。
# ② 只杀监听进程,**tsup 监视器留了下来** —— 它还活着,一秒后就重拉一个占回同一个端口。
# 所以顺进程链往上走,把本仓库的监视器与 pnpm 壳一起收;收完还要**再核端口是不是真空了**。
# ③ 拿旧 `ps` 快照里的 PID 去 kill,或者按名字 pkill —— 两样都误伤过。这里只从端口现查。
# ④ 起完不核实就说「好了」。实测有一次 macOS 没有 `setsid` 根本起不来,
# 而失败发生在后台里,不核实根本发现不了。
set -u
cd "$(dirname "$0")" || exit 1
REPO=$(pwd)
LOGDIR="$REPO/.dev-logs"
mkdir -p "$LOGDIR"
WHAT=${1:-both}
API_PORT=8787
WEB_PORT=5173
PG_NAME=jcai-pg
say() { printf '%s\n' "$*"; }
# 端口的**真属主** —— 只认 LISTEN。不加这个过滤会捞到浏览器(坑①)
owner_of() { lsof -ti:"$1" -sTCP:LISTEN 2>/dev/null | head -1; }
# 顺着进程链往上收:监听进程本身必收,再往上每一级**确认是本仓库的 dev 壳**才收。
# 链长这样:node dist/main.js ← node …/tsup/…/cli-default.js ← pnpm --filter @app/api dev
# 只收监听那一个的话,tsup 一秒后就把端口抢回去了(坑②)。
kill_chain() {
local pid=$1 depth=0 parent cmd
KILLED=''
while [ -n "$pid" ] && [ "$pid" != '1' ] && [ "$pid" != "$$" ] && [ "$depth" -lt 4 ]; do
parent=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
if [ "$depth" -gt 0 ]; then
cmd=$(ps -o command= -p "$pid" 2>/dev/null)
case "$cmd" in
*"$REPO"* | *'@app/'*) : ;; # 本仓库的监视器 / pnpm 壳,收
*) break ;; # 到终端或别人家的进程了,就此打住
esac
fi
kill -9 "$pid" 2>/dev/null && KILLED="$KILLED $pid"
pid=$parent
depth=$((depth + 1))
done
}
# 停到端口**真的空出来**为止 —— 收完还有人占,就是还有监视器在抢,再收一轮。
stop_service() {
local label=$1 port=$2 round owner all=''
for round in 1 2 3 4; do
owner=$(owner_of "$port")
if [ -z "$owner" ]; then
if [ -z "$all" ]; then
[ "$round" = '1' ] && say " $label 本来就没在跑"
return 0
fi
sleep 1
if [ -z "$(owner_of "$port")" ]; then
say " $label 已停 (pid$all)"
return 0
fi
continue
fi
kill_chain "$owner"
all="$all$KILLED"
sleep 1
done
say " ! $label :$port 还被占着,收了几轮都有人抢回来 —— 手工看看 lsof -i:$port"
return 1
}
start_one() {
local name=$1 port=$2 label=$3
if [ -n "$(owner_of "$port")" ]; then
say " $label 已在跑,不动它"
return 0
fi
#
# `exec` 不能省 —— 它让子 shell **被 pnpm 顶替掉**,进程链上不留 bash。
#
# 少了它(三种写法都试过:`(cmd & disown)`、双重 fork、`disown` 提到顶层),
# start.sh 自己会赖着不退出、一直攥着 stdout,于是 `./start.sh | tail` 这类管道用法
# **永远等不到结尾**(实测挂满 100 秒;服务其实早起好了,人只看见光标在闪)。
# 换成 `sleep` 当被起的命令就不复现 —— 是 pnpm 特有的,不是写法通病。
# `</dev/null` 断掉它跟终端的输入;`disown` 摘出作业表,免得停服务时刷一行 Killed: 9。
# (macOS 没有 setsid,别用 —— 试过,command not found,而失败发生在后台里,`||` 兜不住)
( cd "$REPO" && exec nohup pnpm --filter "@app/$name" dev < /dev/null > "$LOGDIR/$name.log" 2>&1 ) &
disown 2>/dev/null
say " $label 启动中… (日志 .dev-logs/$name.log)"
}
# 轮询到 200 才算就绪 —— 不核实的「好了」等于没说(坑④)
wait_ok() {
local url=$1 label=$2 i code
for i in $(seq 1 45); do
code=$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$url" 2>/dev/null)
if [ "$code" = '200' ]; then
say " ✓ $label 就绪 (${i}s)"
return 0
fi
sleep 1
done
say " ✗ $label 没有应答 —— 看 .dev-logs/$label.log"
return 1
}
http_code() { curl -s -o /dev/null -m 3 -w '%{http_code}' "$1" 2>/dev/null; }
check_db() {
if ! docker ps --filter "name=$PG_NAME" --filter status=running --format '{{.Names}}' 2>/dev/null | grep -q "$PG_NAME"; then
say " ! 数据库 $PG_NAME 没在跑 —— 先 pnpm db:up"
return 1
fi
say " ✓ 数据库 $PG_NAME 在跑"
return 0
}
case "$WHAT" in
status)
say '当前状态:'
say " api :$API_PORT $(http_code "http://127.0.0.1:$API_PORT/api/health")"
say " web :$WEB_PORT $(http_code "http://127.0.0.1:$WEB_PORT/")"
say " 反代 /api $(http_code "http://127.0.0.1:$WEB_PORT/api/health")"
check_db
exit 0
;;
stop)
say '停止服务:'
stop_service api "$API_PORT"
stop_service web "$WEB_PORT"
exit 0
;;
api) say '重启 api:'; stop_service api "$API_PORT" ;;
web) say '重启 web:'; stop_service web "$WEB_PORT" ;;
both) say '重启 api + web:'; stop_service api "$API_PORT"; stop_service web "$WEB_PORT" ;;
*) say "不认识的参数:$WHAT(可用:both / api / web / status / stop)"; exit 2 ;;
esac
check_db || say ' (数据库不在也照起,但一调接口就会报连不上)'
# 两个都过一遍:刚停掉的那个会起来,另一个在跑就不动它、碰巧是停的就顺手带起来。
# 「重启 api」不该把一个半残的环境留给人 —— 反代不通,浏览器里看到的还是坏的。
start_one api "$API_PORT" "api :$API_PORT"
start_one web "$WEB_PORT" "web :$WEB_PORT"
RC=0
wait_ok "http://127.0.0.1:$API_PORT/api/health" 'api' || RC=1
wait_ok "http://127.0.0.1:$WEB_PORT/" 'web' || RC=1
# 反代要单独核:api 活着而反代不通,人在浏览器里看到的还是坏的
PROXY=$(http_code "http://127.0.0.1:$WEB_PORT/api/health")
if [ "$PROXY" = '200' ]; then
say ' ✓ 反代 /api → 200'
else
say " ✗ 反代 /api → ${PROXY:-连不上}"
RC=1
fi
say ''
if [ "$RC" = '0' ]; then
say " 都好了 → http://localhost:$WEB_PORT/"
else
say ' 有起不来的,日志在 .dev-logs/'
fi
exit $RC