按用户指示做**一包提交**,不按工作流拆分。本提交刻意混合了多条并行线:
· 本地 ASR 接管:audio 成为与 chat/embed/image/video 同等的路由类别
(IsLocalRoute 单一判据、audio 健康探测、default_audio_route、
auto 占位、GET /api/ai/routes/audio、回退云端时界面明示「音频已出网」)
· LLM 调用层:ctx 贯穿、ToolCall/ToolSchema、EmptyCompletionError /
TransientUpstreamError(按错误类型而非文案判重试)
· 编排 Agent:general_assistant orchestrate/persistence/spec_driver
· 联网搜索:internal/search(playwright)
· 网盘:backend + 前端
· 前端 UI:导航/路由/工作台若干页
· 交付文档:DELIVERY.md / AR04 / 部署文档的「无 Python」表述据实改写,
新增 eai_agentplatform-asr.service、asr.env、clonezilla-cleanup 清 ~/asr-poc
不分拆的原因:dev 早期,粒度不该打断工作节奏。且实测过——这些改动
**在编译上是同一个单元**(llm.go 的 ctx 签名变更牵动 12 个调用点,
chat_message.go 的 ctx 改动又与编排重写同处一个 hunk),拆出来的中间态编不过。
详见 TOP_CODING_RULES.md G14.5 与 bugs_and_errors.md E09。
Co-Authored-By: Claude Code <noreply@anthropic.com>
409 lines
12 KiB
Bash
Executable File
409 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
if [ -z "${BASH_VERSION:-}" ]; then
|
||
if command -v bash >/dev/null 2>&1; then
|
||
echo "[INFO] start_dev_10231_10232.sh requires bash. Re-running with bash ..."
|
||
exec bash "$0" "$@"
|
||
fi
|
||
echo "[FATAL] bash is required but was not found on PATH." >&2
|
||
echo " Please run: bash ./start_dev_10231_10232.sh" >&2
|
||
exit 1
|
||
fi
|
||
|
||
set -euo pipefail
|
||
|
||
FORCE=0
|
||
SKIP_PREFLIGHT=0
|
||
SKIP_SMOKE=0
|
||
SKIP_BUILD=0
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--force|-f)
|
||
FORCE=1
|
||
;;
|
||
--skip-preflight)
|
||
SKIP_PREFLIGHT=1
|
||
;;
|
||
--skip-smoke)
|
||
SKIP_SMOKE=1
|
||
;;
|
||
--skip-build)
|
||
SKIP_BUILD=1
|
||
;;
|
||
--help|-h)
|
||
cat <<'EOF'
|
||
Usage: ./start_dev_10231_10232.sh [options]
|
||
|
||
Options:
|
||
--force Kill whatever holds the ports, even non-project processes
|
||
(a previous run of this script is always stopped and
|
||
replaced automatically)
|
||
--skip-preflight Skip preflight checks
|
||
--skip-smoke Skip admin account smoke login
|
||
--skip-build Do not rebuild the backend, even if sources are newer
|
||
--help Show this help
|
||
EOF
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo "[FATAL] Unknown option: $1" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
# ================================================================
|
||
# 端口说明
|
||
# 10231 - 前端 Vite 开发服务器 (dev only)
|
||
# 10232 - 后端 Go API 服务 (dev only)
|
||
# 8080 - 外部 LLM 服务 (llamacpp) — AI 对话 API 入口
|
||
# 8081 - 外部 Embed 服务 (llamacpp) — 向量嵌入服务
|
||
# 11434 - 外部 Ollama 服务 (如使用 Ollama 路由)
|
||
# ================================================================
|
||
BACKEND_PORT=10232
|
||
FRONTEND_PORT=10231
|
||
|
||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
APP_DIR="$REPO_DIR/eai_agentplatform"
|
||
BACKEND_DIR="$APP_DIR/backend-go"
|
||
BACKEND_BIN="$BACKEND_DIR/bin/eai_agentplatform-server"
|
||
FRONTEND_DIR="$APP_DIR/frontend"
|
||
DEV_BIND_HOST="${EAI_DEV_BIND_HOST:-0.0.0.0}"
|
||
DEBUG_LOG_DIR="$REPO_DIR/debuglog"
|
||
mkdir -p "$DEBUG_LOG_DIR"
|
||
|
||
RUN_ID="$(date +%Y%m%d_%H%M%S)"
|
||
BACKEND_LOG="$DEBUG_LOG_DIR/backend_${RUN_ID}.log"
|
||
FRONTEND_LOG="$DEBUG_LOG_DIR/frontend_${RUN_ID}.log"
|
||
BACKEND_PID_FILE="$DEBUG_LOG_DIR/backend.pid"
|
||
FRONTEND_PID_FILE="$DEBUG_LOG_DIR/frontend.pid"
|
||
|
||
# 加载本地运行 secrets(.env 已被 .gitignore 忽略,不进版本库)。
|
||
# 例如 SERPAPI_API_KEY 等环境变量,供后端进程继承(backed 用 `env PORT=... $BIN` 启动,
|
||
# 会继承本 shell 导出的所有环境变量)。文件不存在时静默跳过。
|
||
if [ -f "$APP_DIR/.env" ]; then
|
||
set -a
|
||
# shellcheck disable=SC1091
|
||
source "$APP_DIR/.env"
|
||
set +a
|
||
fi
|
||
|
||
check() {
|
||
local label="$1"
|
||
local fix_hint="$2"
|
||
shift 2
|
||
printf ' [.] %s ...' "$label"
|
||
if "$@" >/dev/null 2>&1; then
|
||
printf ' OK\n'
|
||
return 0
|
||
fi
|
||
printf ' FAIL\n'
|
||
if [[ -n "$fix_hint" ]]; then
|
||
printf ' fix: %s\n' "$fix_hint"
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
wait_health() {
|
||
local url="$1"
|
||
local label="$2"
|
||
local timeout_sec="${3:-60}"
|
||
local elapsed=0
|
||
printf ' [.] waiting on %s (%s) ...' "$label" "$url"
|
||
while (( elapsed < timeout_sec )); do
|
||
if curl -fsS --max-time 2 "$url" >/dev/null 2>&1; then
|
||
printf ' READY\n'
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
printf '.'
|
||
done
|
||
printf ' TIMEOUT\n'
|
||
return 1
|
||
}
|
||
|
||
detect_public_host() {
|
||
local candidate
|
||
for candidate in $(hostname -I 2>/dev/null || true); do
|
||
case "$candidate" in
|
||
127.*|172.17.*)
|
||
continue
|
||
;;
|
||
*)
|
||
echo "$candidate"
|
||
return 0
|
||
;;
|
||
esac
|
||
done
|
||
echo "127.0.0.1"
|
||
}
|
||
|
||
port_pids() {
|
||
local port="$1"
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | sort -u
|
||
return 0
|
||
fi
|
||
if command -v ss >/dev/null 2>&1; then
|
||
ss -ltnp "( sport = :$port )" 2>/dev/null | awk -F'pid=' 'NR>1 && NF>1 {split($2,a,","); print a[1]}' | sort -u
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# Decide whether a process belongs to this project.
|
||
#
|
||
# This must look at the full command line, not `ps -o comm=`: comm is capped at
|
||
# 15 characters (the kernel's TASK_COMM_LEN), so the backend reports itself as
|
||
# "eai_agentplatfo" and could never match its own name. That made the launcher
|
||
# treat its own backend as a foreign process and refuse to restart.
|
||
process_is_ours() {
|
||
local pid="$1"
|
||
local port="$2"
|
||
local args
|
||
args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
|
||
[[ -z "$args" ]] && return 1
|
||
[[ "$args" == *"$BACKEND_BIN"* ]] && return 0
|
||
[[ "$args" == *vite* && "$args" == *"--port $port"* ]] && return 0
|
||
return 1
|
||
}
|
||
|
||
process_label() {
|
||
local pid="$1"
|
||
local args
|
||
args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
|
||
if [[ -z "$args" ]]; then
|
||
printf 'unknown'
|
||
return 0
|
||
fi
|
||
args="${args%% *}"
|
||
printf '%s' "${args##*/}"
|
||
}
|
||
|
||
# Stop the run recorded by a previous invocation. Uses the pid files so a
|
||
# previous instance still gets cleaned up even if it is not holding its port.
|
||
kill_previous_run() {
|
||
local pid_file="$1"
|
||
local port="$2"
|
||
local label="$3"
|
||
local pid
|
||
[[ -f "$pid_file" ]] || return 0
|
||
pid="$(cat "$pid_file" 2>/dev/null || true)"
|
||
[[ "$pid" =~ ^[0-9]+$ ]] || return 0
|
||
kill -0 "$pid" 2>/dev/null || return 0
|
||
process_is_ours "$pid" "$port" || return 0
|
||
printf ' [.] stopping previous %s (PID %s)\n' "$label" "$pid"
|
||
kill "$pid" 2>/dev/null || true
|
||
sleep 1
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
return 0
|
||
}
|
||
|
||
resolve_port() {
|
||
local port="$1"
|
||
local label="$2"
|
||
local pids
|
||
mapfile -t pids < <(port_pids "$port" || true)
|
||
if [[ ${#pids[@]} -eq 0 ]]; then
|
||
printf ' [.] %s :%s free\n' "$label" "$port"
|
||
return 0
|
||
fi
|
||
|
||
printf ' [!] %s :%s in use by ' "$label" "$port"
|
||
for pid in "${pids[@]}"; do
|
||
printf '%s(PID %s) ' "$(process_label "$pid")" "$pid"
|
||
done
|
||
printf '\n'
|
||
|
||
for pid in "${pids[@]}"; do
|
||
if [[ "$FORCE" -eq 1 ]] || process_is_ours "$pid" "$port"; then
|
||
printf ' killing previous %s(PID %s)\n' "$(process_label "$pid")" "$pid"
|
||
kill "$pid" 2>/dev/null || true
|
||
sleep 1
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
else
|
||
printf ' keeping non-project process: %s(PID %s)\n' "$(process_label "$pid")" "$pid"
|
||
printf ' fix: stop that process manually, or rerun with --force\n'
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
mapfile -t pids < <(port_pids "$port" || true)
|
||
if [[ ${#pids[@]} -gt 0 ]]; then
|
||
printf ' [FAIL] %s :%s still in use\n' "$label" "$port"
|
||
return 1
|
||
fi
|
||
|
||
printf ' [OK] %s :%s released\n' "$label" "$port"
|
||
return 0
|
||
}
|
||
|
||
# True when the backend binary is missing or older than its sources.
|
||
#
|
||
# Preflight only proves the binary exists, which happily launches stale code
|
||
# after a source edit — the failure is silent, so it needs an explicit check.
|
||
backend_needs_build() {
|
||
[[ -f "$BACKEND_BIN" ]] || return 0
|
||
local newer
|
||
newer="$(find "$BACKEND_DIR" -name '*.go' -newer "$BACKEND_BIN" -print -quit 2>/dev/null || true)"
|
||
[[ -n "$newer" ]] && return 0
|
||
local f
|
||
for f in "$BACKEND_DIR/go.mod" "$BACKEND_DIR/go.sum"; do
|
||
[[ -f "$f" && "$f" -nt "$BACKEND_BIN" ]] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
build_backend() {
|
||
local build_log="$DEBUG_LOG_DIR/backend_build.log"
|
||
if ! command -v go >/dev/null 2>&1; then
|
||
printf ' [FAIL] backend sources changed, but go is not on PATH\n'
|
||
printf ' fix: install go, or rebuild manually then rerun with --skip-build\n'
|
||
return 1
|
||
fi
|
||
printf ' [.] backend sources newer than binary, rebuilding ...'
|
||
if ( cd "$BACKEND_DIR" && go build -o bin/eai_agentplatform-server ./cmd/server ) >"$build_log" 2>&1; then
|
||
printf ' OK\n'
|
||
return 0
|
||
fi
|
||
printf ' FAIL\n'
|
||
printf ' log : %s\n' "$build_log"
|
||
tail -n 20 "$build_log" 2>/dev/null | sed 's/^/ /' || true
|
||
return 1
|
||
}
|
||
|
||
cleanup_started() {
|
||
if [[ -f "$BACKEND_PID_FILE" ]]; then
|
||
kill "$(cat "$BACKEND_PID_FILE")" 2>/dev/null || true
|
||
fi
|
||
if [[ -f "$FRONTEND_PID_FILE" ]]; then
|
||
kill "$(cat "$FRONTEND_PID_FILE")" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
echo "==> eai_agentplatform dev launcher"
|
||
echo " repo : $REPO_DIR"
|
||
echo " backend : $BACKEND_DIR (port $BACKEND_PORT)"
|
||
echo " frontend : $FRONTEND_DIR (port $FRONTEND_PORT)"
|
||
echo " bind host: $DEV_BIND_HOST"
|
||
echo
|
||
|
||
if [[ "${EAI_DEV_PUBLIC_HOST:-}" != "" ]]; then
|
||
DEV_PUBLIC_HOST="$EAI_DEV_PUBLIC_HOST"
|
||
elif [[ "$DEV_BIND_HOST" == "0.0.0.0" ]]; then
|
||
DEV_PUBLIC_HOST="$(detect_public_host)"
|
||
else
|
||
DEV_PUBLIC_HOST="$DEV_BIND_HOST"
|
||
fi
|
||
|
||
if [[ "$SKIP_PREFLIGHT" -eq 0 ]]; then
|
||
echo "[1/4] preflight checks"
|
||
all_ok=1
|
||
check "backend dir exists" "run from repo root" test -d "$BACKEND_DIR" || all_ok=0
|
||
check "frontend dir exists" "run from repo root" test -d "$FRONTEND_DIR" || all_ok=0
|
||
check "frontend node_modules" "run npm install in frontend/" test -e "$FRONTEND_DIR/node_modules" || all_ok=0
|
||
check "node on PATH" "install nodejs" command -v node || all_ok=0
|
||
check "npm on PATH" "install npm" command -v npm || all_ok=0
|
||
check "curl on PATH" "install curl" command -v curl || all_ok=0
|
||
if [[ "$all_ok" -eq 0 ]]; then
|
||
echo
|
||
echo "[ABORT] preflight failed."
|
||
exit 1
|
||
fi
|
||
echo
|
||
else
|
||
echo "[1/4] preflight skipped (--skip-preflight)"
|
||
echo
|
||
fi
|
||
|
||
# Runs whether or not preflight did: a stale binary is a silent failure, and
|
||
# building before the old services are stopped means a compile error leaves the
|
||
# currently running stack untouched.
|
||
if [[ "$SKIP_BUILD" -eq 1 ]]; then
|
||
echo "[1/4] backend build check skipped (--skip-build)"
|
||
echo
|
||
elif backend_needs_build; then
|
||
if ! build_backend; then
|
||
echo
|
||
echo "[ABORT] backend build failed; services left as they were."
|
||
exit 1
|
||
fi
|
||
echo
|
||
else
|
||
echo "[1/4] backend binary up to date"
|
||
echo
|
||
fi
|
||
|
||
echo "[2/4] port conflict check"
|
||
kill_previous_run "$BACKEND_PID_FILE" "$BACKEND_PORT" "backend"
|
||
kill_previous_run "$FRONTEND_PID_FILE" "$FRONTEND_PORT" "frontend"
|
||
resolve_port "$BACKEND_PORT" "backend" || exit 1
|
||
resolve_port "$FRONTEND_PORT" "frontend" || exit 1
|
||
echo
|
||
|
||
trap cleanup_started EXIT
|
||
|
||
echo "[3/4] launch services"
|
||
echo " [.] starting backend (eai_agentplatform-server :$BACKEND_PORT)"
|
||
echo " [.] log : $BACKEND_LOG"
|
||
nohup env PORT="$BACKEND_PORT" "$BACKEND_BIN" >>"$BACKEND_LOG" 2>&1 </dev/null &
|
||
BACKEND_PID=$!
|
||
echo "$BACKEND_PID" > "$BACKEND_PID_FILE"
|
||
|
||
if ! wait_health "http://127.0.0.1:$BACKEND_PORT/api/health" "backend"; then
|
||
echo
|
||
echo "[ABORT] backend did not become healthy, frontend not started."
|
||
echo " check: $BACKEND_LOG"
|
||
exit 1
|
||
fi
|
||
|
||
echo " [.] starting frontend (vite :$FRONTEND_PORT)"
|
||
echo " [.] log : $FRONTEND_LOG"
|
||
nohup bash -lc "cd \"$FRONTEND_DIR\" && exec node ./node_modules/vite/bin/vite.js --host \"$DEV_BIND_HOST\" --port \"$FRONTEND_PORT\"" >>"$FRONTEND_LOG" 2>&1 </dev/null &
|
||
FRONTEND_PID=$!
|
||
echo "$FRONTEND_PID" > "$FRONTEND_PID_FILE"
|
||
echo
|
||
|
||
echo "[4/4] health check + smoke login"
|
||
f_ok=1
|
||
if ! wait_health "http://127.0.0.1:$FRONTEND_PORT/" "frontend"; then
|
||
f_ok=0
|
||
fi
|
||
|
||
if [[ "$SKIP_SMOKE" -eq 0 && "$f_ok" -eq 1 ]]; then
|
||
echo
|
||
echo " [.] smoke login admin account ..."
|
||
payload='{"username":"admin","password":"admin123"}'
|
||
status_code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
||
-H 'Content-Type: application/json' \
|
||
-d "$payload" \
|
||
"http://127.0.0.1:$BACKEND_PORT/api/auth/login" || true)"
|
||
if [[ "$status_code" == "200" ]]; then
|
||
printf ' [OK] admin login success\n'
|
||
else
|
||
printf ' [FAIL] admin login (HTTP %s)\n' "$status_code"
|
||
fi
|
||
else
|
||
echo " [.] smoke login skipped"
|
||
fi
|
||
|
||
echo
|
||
echo "================================================================="
|
||
echo " Service status"
|
||
echo "================================================================="
|
||
echo " backend : http://$DEV_PUBLIC_HOST:$BACKEND_PORT"
|
||
echo " frontend : http://$DEV_PUBLIC_HOST:$FRONTEND_PORT"
|
||
echo " backend log : $BACKEND_LOG"
|
||
echo " frontend log: $FRONTEND_LOG"
|
||
echo " PIDs: backend=$BACKEND_PID frontend=$FRONTEND_PID"
|
||
echo "================================================================="
|
||
echo
|
||
|
||
trap - EXIT
|
||
echo "Usage: ./start_dev_10231_10232.sh [--force] [--skip-preflight] [--skip-build]"
|
||
echo " env: EAI_DEV_BIND_HOST=0.0.0.0 (listen all)"
|
||
echo " env: EAI_DEV_PUBLIC_HOST=x.x.x.x (public url)"
|
||
echo
|