Files
exAichatbot_agent/local-dev/start-all.sh
T
2026-07-30 13:18:10 +09:00

405 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# 로컬 전체 스택 기동 (exdev AI SSH 터널 + 원격 Mongo/Qdrant + RAG + WAS + Skill + chatbotApp + Desktop)
# Mongo/Qdrant 는 stock.exdev.co.kr 를 사용 (로컬 컨테이너 미기동)
#
# ./local-dev/start-all.sh
# ./start-all.sh # 루트 래퍼
# ./local-dev/start-all.sh --no-tunnel
# ./local-dev/start-all.sh --skip-java
# ./local-dev/start-all.sh --no-build # RAG/웹/데스크톱 빌드 생략
# ./local-dev/start-all.sh --skip-desktop # Electron 재빌드만 생략
#
# 종료: ./local-dev/stop-all.sh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCAL_DEV_DIR="${ROOT_DIR}/local-dev"
RUN_DIR="${LOCAL_DEV_DIR}/run"
LOG_DIR="${LOCAL_DEV_DIR}/logs"
RAG_DIR="${ROOT_DIR}/exAiChatBot-chatbot2.0-agent"
API_DIR="${ROOT_DIR}/chatbotApi-chatbot2.0-agent"
SKILL_DIR="${ROOT_DIR}/kakaoChatbotSkill-chatbot2.0-agent"
APP_DIR="${ROOT_DIR}/chatbotApp-chatbot2.0-agent"
DESKTOP_DIR="${ROOT_DIR}/chatbotDesktopApp-chatbot2.0"
ENV_TEMPLATE="${LOCAL_DEV_DIR}/env.exdev-ai.template"
RAG_ENV="${RAG_DIR}/.env"
COMPOSE_LOCAL="${RAG_DIR}/docker-compose.local.yml"
EXDEV_DATA_HOST="${EXDEV_DATA_HOST:-stock.exdev.co.kr}"
INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY:-local-dev-key}"
MONGODB_URI="${MONGODB_URI:-mongodb://exDev:ehfhrhdtk!!@${EXDEV_DATA_HOST}:27017/chatbot?authSource=chatbot&heartbeatFrequencyMS=30000&minPoolSize=3&maxIdleTimeMS=1800000}"
LLM_PORT="${LLM_PORT:-16000}"
EMBED_PORT="${EMBED_PORT:-16001}"
RERANK_PORT="${RERANK_PORT:-16002}"
RAG_PORT="${RAG_API_PORT:-28012}"
ADMIN_PORT="${ADMIN_PORT:-28013}"
WAS_PORT="${CHATBOT_API_PORT:-8086}"
SKILL_PORT="${KAKAO_SKILL_PORT:-8083}"
APP_PORT="${CHATBOT_APP_PORT:-8089}"
MONGO_PORT="${MONGO_PORT:-27017}"
QDRANT_PORT="${QDRANT_PORT:-6333}"
SKIP_TUNNEL=false
SKIP_JAVA=false
NO_BUILD=false
SKIP_DESKTOP=false
for arg in "$@"; do
case "$arg" in
--no-tunnel) SKIP_TUNNEL=true ;;
--skip-java) SKIP_JAVA=true ;;
--no-build) NO_BUILD=true ;;
--skip-desktop) SKIP_DESKTOP=true ;;
--sync-qdrant)
echo "경고: 원격 Qdrant 사용 중이라 --sync-qdrant 는 무시됩니다."
;;
-h|--help)
sed -n '2,14p' "$0"
exit 0
;;
*)
echo "알 수 없는 옵션: $arg (--help)"
exit 1
;;
esac
done
export PATH="/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH}"
log() { echo "[$(date '+%H:%M:%S')] $*"; }
fail() { log "ERROR: $*"; exit 1; }
wait_http() {
local url="$1" label="$2" max="${3:-90}"
local i=0
while (( i < max )); do
if curl -sf --max-time 3 "$url" >/dev/null 2>&1; then
log "✅ ${label}"
return 0
fi
sleep 2
i=$((i + 2))
done
log "⚠️ ${label} 타임아웃 (${url})"
return 1
}
wait_port() {
local port="$1" label="$2" max="${3:-90}"
local i=0
while (( i < max )); do
if lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
log "✅ ${label} (:${port})"
return 0
fi
sleep 2
i=$((i + 2))
done
log "⚠️ ${label} 타임아웃 (:${port})"
return 1
}
ai_tunnel_ready() {
curl -sf --max-time 3 "http://127.0.0.1:${LLM_PORT}/v1/models" >/dev/null 2>&1 \
&& curl -sf --max-time 3 "http://127.0.0.1:${EMBED_PORT}/health" >/dev/null 2>&1 \
&& curl -sf --max-time 3 -X POST "http://127.0.0.1:${RERANK_PORT}/rerank" \
-H "Content-Type: application/json" \
-d '{"query":"ping","documents":["doc"]}' >/dev/null 2>&1
}
start_tunnel() {
if ai_tunnel_ready; then
log "exdev AI 터널 이미 연결됨 (:${LLM_PORT}~:${RERANK_PORT})"
return 0
fi
log "exdev AI SSH 터널 백그라운드 기동..."
ssh -f -N \
-o ServerAliveInterval=30 \
-o ExitOnForwardFailure=yes \
-L "${LLM_PORT}:127.0.0.1:${LLM_PORT}" \
-L "${EMBED_PORT}:127.0.0.1:${EMBED_PORT}" \
-L "${RERANK_PORT}:127.0.0.1:${RERANK_PORT}" \
"${EXDEV_USER:-aidev}@${EXDEV_HOST:-chatbot.exdev.co.kr}"
sleep 1
pgrep -f "ssh -f -N.*${LLM_PORT}:127.0.0.1:${LLM_PORT}" | head -1 > "${RUN_DIR}/tunnel.pid" || true
local i=0
while (( i < 60 )); do
if ai_tunnel_ready; then
log "✅ exdev AI 터널 (LLM/Embed/Rerank)"
return 0
fi
sleep 2
i=$((i + 2))
done
fail "exdev AI 터널 실패 — exdev에서 start-host-ai.sh 실행 여부 확인"
}
patch_env() {
local key="$1" value="$2"
local tmp="${RAG_ENV}.tmp.$$"
if [[ -f "${RAG_ENV}" ]]; then
grep -v "^${key}=" "${RAG_ENV}" > "${tmp}" || true
else
: > "${tmp}"
fi
printf '%s\n%s=%s\n' "$(cat "${tmp}")" "${key}" "${value}" > "${RAG_ENV}"
rm -f "${tmp}"
}
ensure_rag_env() {
if [[ ! -f "${RAG_ENV}" ]]; then
log ".env 없음 → env.exdev-ai.template 복사"
cp "${ENV_TEMPLATE}" "${RAG_ENV}"
fi
# Docker 컨테이너 → Mac 호스트(SSH 터널) 접근
patch_env "LLM_BASE_URL" "http://host.docker.internal:${LLM_PORT}"
patch_env "TEI_EMBED_URL" "http://host.docker.internal:${EMBED_PORT}"
patch_env "TEI_RERANK_URL" "http://host.docker.internal:${RERANK_PORT}"
patch_env "EMBED_API_STYLE" "tei"
patch_env "RERANK_API_STYLE" "legacy"
patch_env "EXDEV_LOCAL" "true"
patch_env "CHATBOT_API_BASE_URL" "http://host.docker.internal:${WAS_PORT}/api"
patch_env "INTERNAL_TOOL_API_KEY" "${INTERNAL_TOOL_API_KEY}"
# Mongo / Qdrant → stock.exdev.co.kr (원격)
patch_env "EXDEV_DATA_HOST" "${EXDEV_DATA_HOST}"
patch_env "MONGO_HOST" "${EXDEV_DATA_HOST}"
patch_env "MONGO_PORT" "${MONGO_PORT}"
patch_env "MONGO_USER" ""
patch_env "MONGO_PASSWORD" ""
patch_env "MONGO_DATABASE" "chat_history"
patch_env "QDRANT_HOST" "${EXDEV_DATA_HOST}"
patch_env "QDRANT_PORT" "${QDRANT_PORT}"
patch_env "QDRANT_COLLECTION" "qa_vectors_v2"
}
ensure_remote_data() {
log "원격 데이터 확인: ${EXDEV_DATA_HOST} (Mongo :${MONGO_PORT}, Qdrant :${QDRANT_PORT})"
if ! curl -sf --max-time 5 "http://${EXDEV_DATA_HOST}:${QDRANT_PORT}/collections" >/dev/null 2>&1; then
fail "원격 Qdrant 접속 실패 — http://${EXDEV_DATA_HOST}:${QDRANT_PORT}"
fi
log "✅ 원격 Qdrant (${EXDEV_DATA_HOST}:${QDRANT_PORT})"
if command -v nc >/dev/null 2>&1; then
if ! nc -z -w 5 "${EXDEV_DATA_HOST}" "${MONGO_PORT}" >/dev/null 2>&1; then
fail "원격 MongoDB 접속 실패 — ${EXDEV_DATA_HOST}:${MONGO_PORT}"
fi
fi
log "✅ 원격 MongoDB (${EXDEV_DATA_HOST}:${MONGO_PORT})"
}
ensure_rag_compose() {
local template="${LOCAL_DEV_DIR}/docker-compose.rag.local.yml"
[[ -f "${template}" ]] || fail "local-dev/docker-compose.rag.local.yml 없음"
cp "${template}" "${COMPOSE_LOCAL}"
}
start_rag() {
ensure_rag_env
ensure_rag_compose
[[ -f "${COMPOSE_LOCAL}" ]] || fail "docker-compose.local.yml 없음"
cd "${RAG_DIR}"
local -a up_flags=(-d)
if $NO_BUILD; then
up_flags+=(--no-build)
else
up_flags+=(--build)
fi
# 이전 slim/legacy 잔여 정리
docker compose -f docker-compose-slim.yml down --remove-orphans 2>/dev/null || true
# 로컬 Qdrant 잔여 컨테이너 정리 (원격 사용)
docker compose -f docker-compose.local.yml stop qdrant 2>/dev/null || true
docker compose -f docker-compose.local.yml rm -f qdrant 2>/dev/null || true
log "RAG 스택 기동 (docker-compose.local.yml, 원격 Mongo/Qdrant)..."
docker compose -f docker-compose.local.yml up "${up_flags[@]}"
wait_http "http://${EXDEV_DATA_HOST}:${QDRANT_PORT}/collections" "원격 Qdrant" 30 || true
wait_http "http://127.0.0.1:${RAG_PORT}/health" "RAG API" 120
wait_http "http://127.0.0.1:${ADMIN_PORT}/api/stats" "Vector Admin" 60 || \
wait_port "${ADMIN_PORT}" "Vector Admin" 30 || true
}
ensure_java() {
local home=""
local candidates=(
"/Users/macbook/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
"/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
)
for candidate in "${candidates[@]}"; do
if [[ -x "${candidate}/bin/java" ]]; then
home="${candidate}"
break
fi
done
if [[ -z "${home}" ]] && /usr/libexec/java_home -v 17 >/dev/null 2>&1; then
home="$(/usr/libexec/java_home -v 17)"
fi
if [[ -z "${home}" ]]; then
fail "Java 17 필요 — brew install openjdk@17"
fi
export JAVA_HOME="${home}"
export PATH="${JAVA_HOME}/bin:${PATH}"
log "Java 17: $("${JAVA_HOME}/bin/java" -version 2>&1 | head -1)"
}
start_java_service() {
local name="$1" dir="$2" pid_file="$3" log_file="$4"
shift 4
if [[ -f "${pid_file}" ]]; then
local old_pid
old_pid="$(cat "${pid_file}")"
if kill -0 "$old_pid" 2>/dev/null; then
log "${name} 이미 실행 중 (PID ${old_pid})"
return 0
fi
rm -f "${pid_file}"
fi
chmod +x "${dir}/gradlew"
log "${name} 기동 중..."
: > "${log_file}"
(
cd "${dir}"
export JAVA_HOME
for ev in "$@"; do
export "${ev?}"
done
nohup ./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" bootRun \
>> "${log_file}" 2>&1 &
echo $! > "${pid_file}"
)
log "${name} PID $(cat "${pid_file}") → ${log_file}"
}
verify_stack() {
local ok=true
ai_tunnel_ready || { log "❌ exdev AI 터널"; ok=false; }
curl -sf "http://127.0.0.1:${RAG_PORT}/health" | grep -q '"status"' || { log "❌ RAG health"; ok=false; }
wait_port "${WAS_PORT}" "chatbotApi" 5 || ok=false
wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 5 || ok=false
curl -sf "http://127.0.0.1:${APP_PORT}/chatbot/ai/" | grep -q 'data-chatbot-app' || { log "❌ chatbotApp UI"; ok=false; }
if $ok; then
log "✅ 전체 스택 정상"
return 0
fi
log "⚠️ 일부 서비스 미응답 — ${LOG_DIR}/ 로그 확인"
return 1
}
build_chatbot_app() {
if $NO_BUILD && [[ -f "${APP_DIR}/public-ai/index.html" ]]; then
log "chatbotApp 빌드 생략 (--no-build)"
return 0
fi
log "chatbotApp Next.js 빌드 (수정사항 반영)..."
(
cd "${APP_DIR}/web"
npm ci --prefer-offline 2>/dev/null || npm install
)
# 로컬에는 운영 업무 DB가 없으므로 미납/환불 조회만 2초·1건 목업으로 빌드한다.
NEXT_PUBLIC_FARE_QUERY_MOCK=true bash "${APP_DIR}/build.sh"
[[ -f "${APP_DIR}/public-ai/index.html" ]] || fail "chatbotApp public-ai/ 빌드 실패"
}
build_desktop_app() {
if $NO_BUILD || $SKIP_DESKTOP; then
log "데스크톱 앱 빌드 생략 (--no-build/--skip-desktop)"
return 0
fi
[[ -d "${DESKTOP_DIR}" ]] || fail "데스크톱 앱 디렉터리 없음: ${DESKTOP_DIR}"
chmod +x "${DESKTOP_DIR}/build-and-publish.sh"
# 로컬은 Mac 위주 — Windows 크로스빌드는 시간이 길어 SKIP_WIN 기본 1 (서버 배포는 Win 포함)
SKIP_WIN="${SKIP_WIN:-1}" bash "${DESKTOP_DIR}/build-and-publish.sh"
}
# ── main ──
mkdir -p "${RUN_DIR}" "${LOG_DIR}"
docker info >/dev/null 2>&1 || fail "Docker Engine 미기동"
log "=========================================="
log "exChatbot 로컬 전체 스택 기동"
log "=========================================="
if ! $SKIP_TUNNEL; then
start_tunnel
else
ai_tunnel_ready || fail "--no-tunnel 이지만 :${LLM_PORT}~:${RERANK_PORT} AI 미응답"
fi
ensure_remote_data
start_rag
if ! $SKIP_JAVA; then
ensure_java
start_java_service "chatbotApi" "${API_DIR}" \
"${RUN_DIR}/chatbot-api.pid" "${LOG_DIR}/chatbot-api.log" \
"INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \
"MONGODB_URI=${MONGODB_URI}" \
"SKILL_INTERNAL_BASE_URL=http://127.0.0.1:${SKILL_PORT}" \
"RAG_SERVER_HOST=127.0.0.1" \
"RAG_SERVER_PORT=${RAG_PORT}"
start_java_service "kakaoChatbotSkill" "${SKILL_DIR}" \
"${RUN_DIR}/kakao-skill.pid" "${LOG_DIR}/kakao-skill.log" \
"AGENT_MODE=agent" \
"RAG_AGENT_CHAT_URL=http://127.0.0.1:${RAG_PORT}/agent/chat" \
"CHATBOT_API_BASE_URL=http://127.0.0.1:${WAS_PORT}/api/" \
"INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \
"CHATBOT_APP_URL=http://localhost:${APP_PORT}/chatbot/ai/"
wait_port "${WAS_PORT}" "chatbotApi" 240 || true
wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 240 || true
fi
build_chatbot_app
build_desktop_app
log "chatbotApp 기동..."
docker rm -f ex-chatbot-app-web 2>/dev/null || true
(
cd "${APP_DIR}"
export CHATBOT_API_UPSTREAM="http://host.docker.internal:${SKILL_PORT}/web/"
export CHATBOT_API_HOST="host.docker.internal"
docker compose up -d --force-recreate
)
wait_http "http://127.0.0.1:${APP_PORT}/chatbot/ai/" "chatbotApp UI" 30 || true
log "=========================================="
log "기동 완료"
log " UI http://127.0.0.1:${APP_PORT}/chatbot/ai/"
log " RAG http://127.0.0.1:${RAG_PORT}/health"
log " Admin http://127.0.0.1:${ADMIN_PORT}/"
log " Mongo ${EXDEV_DATA_HOST}:${MONGO_PORT} (원격, RAG+WAS)"
log " Qdrant http://${EXDEV_DATA_HOST}:${QDRANT_PORT}/dashboard (원격)"
log " WAS http://127.0.0.1:${WAS_PORT}/"
log " Skill http://127.0.0.1:${SKILL_PORT}/web/ask"
log " exdev AI tunnel :${LLM_PORT} ~ :${RERANK_PORT}"
log " 로그 ${LOG_DIR}/"
log " 종료 ./local-dev/stop-all.sh"
log "=========================================="
HEALTH="$(curl -sf "http://127.0.0.1:${RAG_PORT}/health" 2>/dev/null || echo 'unavailable')"
log "RAG health: ${HEALTH}"
verify_stack || exit 1