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

636 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# chatbot.exdev.co.kr — Agent 2.0 전체 스택 기동
#
# ./server-dev/start-all-server.sh
# ./server-dev/start-all-server.sh --no-build
# ./server-dev/start-all-server.sh --skip-desktop
# ./server-dev/start-all-server.sh --skip-java
# ./server-dev/start-all-server.sh --skip-legacy-stop
# ./server-dev/start-all-server.sh --no-pull # git 동기화 생략
# ./server-dev/start-all-server.sh --no-push # Gitea push 생략
# ./server-dev/start-all-server.sh --local # 로컬 Mac 개발 모드 (local-dev 위임)
#
# 로컬 Mac 실행 시: 현재 소스 → 개발서버(Gitea/exdev)만 반영·배포.
# 공식 GitHub(KoreaExpressWayCorp) push 는 하지 않음 — ./git_fetch_all.sh 전용.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SERVER_DEV_DIR="${ROOT_DIR}/server-dev"
RUN_DIR="${SERVER_DEV_DIR}/run"
LOG_DIR="${SERVER_DEV_DIR}/logs"
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git}"
WORK_TREE="${WORK_TREE:-/Users/aidev/dev/exAichatbot_agent}"
GIT_BRANCH="${GIT_BRANCH:-main}"
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="${SERVER_DEV_DIR}/env.exdev-server.template"
RAG_ENV="${RAG_DIR}/.env"
COMPOSE_SERVER="${RAG_DIR}/docker-compose.server.yml"
COMPOSE_CADDY="${SERVER_DEV_DIR}/docker-compose.caddy.yml"
LEGACY_STACK_DIR="${LEGACY_STACK_DIR:-/Users/aidev/dev/exAIChatbot2.0}"
MONGO_CONTAINER="${MONGO_CONTAINER:-exaichatbot-agent-mongo}"
INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY:-exdev-agent-tool-key}"
PUBLIC_BASE_URL="${PUBLIC_BASE_URL:-https://chatbot.exdev.co.kr}"
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:-8092}"
NO_BUILD=false
SKIP_JAVA=false
SKIP_LEGACY_STOP=false
SKIP_CADDY=false
SKIP_DESKTOP=false
NO_PULL=false
NO_PUSH=false
LOCAL_DEV=false
EXDEV_SSH="${EXDEV_SSH:-aidev@chatbot.exdev.co.kr}"
for arg in "$@"; do
case "$arg" in
--no-build) NO_BUILD=true ;;
--skip-java) SKIP_JAVA=true ;;
--skip-legacy-stop) SKIP_LEGACY_STOP=true ;;
--skip-caddy) SKIP_CADDY=true ;;
--skip-desktop) SKIP_DESKTOP=true ;;
--no-pull) NO_PULL=true ;;
--no-push) NO_PUSH=true ;;
--local) LOCAL_DEV=true ;;
-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; }
warn() { log "⚠️ $*"; }
# 공식 GitHub 동기화/push 는 start-all-server 경로에서 절대 수행하지 않음.
# (과거: git_fetch_all.sh 호출 → 로컬 수정이 공식 GitHub 로 올라가던 문제 방지)
push_source_to_gitea() {
if $NO_PUSH; then
log "Gitea push 생략 (--no-push)"
return 0
fi
if is_exdev_server; then
log "exdev 서버 — Gitea push 생략 (post-receive/deploy 경로 사용)"
return 0
fi
local push_script="${SERVER_DEV_DIR}/push-to-gitea.sh"
[[ -x "${push_script}" ]] || fail "push-to-gitea.sh 없음: ${push_script}"
log "현재 소스 → Gitea (${GIT_BRANCH}) 반영..."
"${push_script}"
}
wait_remote_deploy() {
if $NO_PUSH; then
return 0
fi
local wait_script="${SERVER_DEV_DIR}/wait-remote-deploy.sh"
if [[ -x "${wait_script}" ]]; then
EXDEV_SSH="${EXDEV_SSH}" PUBLIC_BASE_URL="${PUBLIC_BASE_URL}" "${wait_script}" || \
warn "원격 배포 대기 실패 — ssh ${EXDEV_SSH} tail /Volumes/ADATA/logs/deploy/exAichatbot_agent.log"
else
warn "wait-remote-deploy.sh 없음 — https://chatbot.exdev.co.kr/chatbot/ai/ 수동 확인"
fi
}
sync_submodules_to_server() {
local rsync_script="${SERVER_DEV_DIR}/sync-submodules-rsync.sh"
[[ -x "${rsync_script}" ]] || fail "sync-submodules-rsync.sh 없음"
log "submodule 소스 → chatbot.exdev.co.kr rsync..."
EXDEV_SSH="${EXDEV_SSH}" "${rsync_script}"
}
trigger_remote_deploy() {
log "원격 monorepo checkout..."
ssh -o BatchMode=yes "${EXDEV_SSH}" \
"GIT_DIR=/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git; \
WT=/Users/aidev/dev/exAichatbot_agent; \
git --work-tree=\"\${WT}\" --git-dir=\"\${GIT_DIR}\" checkout -f main; \
rm -f \"\${WT}/.git\" /tmp/exAichatbot_agent_deploy.lock"
sync_submodules_to_server
log "원격 deploy (빌드 + 기동)..."
ssh -o BatchMode=yes "${EXDEV_SSH}" \
"cp /Users/aidev/dev/exAichatbot_agent/server-dev/deploy.sh /Volumes/ADATA/git/deploy-exAichatbot_agent.sh 2>/dev/null || true; \
chmod +x /Volumes/ADATA/git/deploy-exAichatbot_agent.sh; \
export PATH=\"/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:\$PATH\"; \
bash /Volumes/ADATA/git/deploy-exAichatbot_agent.sh --start-only" \
|| warn "원격 deploy 실패"
}
build_chatbot_app_for_deploy() {
log "chatbotApp 로컬 빌드 (서버 Next.js 대신 public/ 생성)..."
chmod -R u+w "${APP_DIR}/public-sample" 2>/dev/null || true
rm -rf "${APP_DIR}/public-sample" 2>/dev/null || true
(
cd "${APP_DIR}/web"
npm ci --prefer-offline 2>/dev/null || npm install
)
NEXT_PUBLIC_FARE_QUERY_MOCK=true bash "${APP_DIR}/build.sh"
[[ -f "${APP_DIR}/public/index.html" ]] || fail "chatbotApp public/ 빌드 실패"
[[ -f "${APP_DIR}/public-ai/index.html" ]] || fail "chatbotApp public-ai/ 빌드 실패"
build_desktop_app
}
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"
log "chatbotDesktopApp Electron 재빌드 + downloads 배포..."
# 서버/원격 배포용: Mac + Windows 모두 생성
SKIP_WIN="${SKIP_WIN:-0}" SKIP_MAC="${SKIP_MAC:-0}" \
bash "${DESKTOP_DIR}/build-and-publish.sh"
}
deploy_from_local_mac() {
log "로컬 Mac → Gitea(exdev) push → chatbot.exdev.co.kr 배포"
log "공식 GitHub push 생략 (필요 시 ./git_fetch_all.sh)"
build_chatbot_app_for_deploy
push_source_to_gitea
trigger_remote_deploy
wait_remote_deploy
log "=========================================="
log "원격 배포 요청 완료"
log " UI ${PUBLIC_BASE_URL}/chatbot/ai/"
log " 로그 ssh ${EXDEV_SSH} tail -f /Volumes/ADATA/logs/deploy/exAichatbot_agent.log"
log "=========================================="
exit 0
}
sync_source_from_git() {
if $NO_PULL; then
log "git 동기화 생략 (--no-pull)"
return 0
fi
local tree="${WORK_TREE}"
local branch="${GIT_BRANCH}"
if [[ -d "${GIT_DIR}" ]]; then
mkdir -p "${tree}"
log "Gitea 최신 소스 동기화 (${branch})..."
if ! git --git-dir="${GIT_DIR}" show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
fail "브랜치 없음: ${branch} (GIT_DIR=${GIT_DIR})"
fi
git --work-tree="${tree}" --git-dir="${GIT_DIR}" checkout -f "${branch}"
# Gitea work tree 만 사용 — 공식 GitHub fetch/push 없음
if [[ -f "${tree}/.gitmodules" ]]; then
log "Gitea submodule 포인터 checkout..."
git -C "${tree}" submodule update --init --recursive 2>/dev/null || \
warn "submodule update 실패 — rsync/배포 산출물로 계속"
fi
local commit
commit="$(git --git-dir="${GIT_DIR}" rev-parse --short HEAD)"
log "✅ 소스 동기화(Gitea): ${commit} (${branch})"
elif [[ -d "${ROOT_DIR}/.git" ]]; then
log "git pull origin ${branch} (Gitea/monorepo)..."
(
cd "${ROOT_DIR}"
git fetch origin "${branch}"
git checkout "${branch}"
git pull --ff-only origin "${branch}"
)
log "✅ git pull 완료: $(git -C "${ROOT_DIR}" rev-parse --short HEAD)"
tree="${ROOT_DIR}"
else
log "Git 저장소 없음 — 현재 디렉터리 소스 사용 (${ROOT_DIR})"
return 0
fi
chmod +x "${tree}/server-dev/"*.sh 2>/dev/null || true
chmod +x "${tree}/git_fetch_all.sh" 2>/dev/null || true
chmod +x "${tree}/chatbotApp-chatbot2.0-agent/build.sh" 2>/dev/null || true
chmod +x "${tree}/chatbotDesktopApp-chatbot2.0/build-and-publish.sh" 2>/dev/null || true
chmod +x "${tree}/start-all-server.sh" "${tree}/stop-all-server.sh" "${tree}/start-all.sh" 2>/dev/null || true
}
is_exdev_server() {
[[ -d "${GIT_DIR}" ]] \
&& [[ -f "${LEGACY_STACK_DIR}/scripts/exdev/start-host-ai.sh" ]]
}
run_local_dev() {
log "로컬 Mac — local-dev/start-all.sh 로 위임 (exdev AI SSH 터널 사용)"
local -a args=()
$NO_BUILD && args+=(--no-build)
$SKIP_JAVA && args+=(--skip-java)
$SKIP_DESKTOP && args+=(--skip-desktop)
if ((${#args[@]} > 0)); then
exec "${ROOT_DIR}/local-dev/start-all.sh" "${args[@]}"
else
exec "${ROOT_DIR}/local-dev/start-all.sh"
fi
}
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
}
host_ai_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
}
ensure_host_ai() {
if host_ai_ready; then
log "호스트 AI 이미 실행 중 (:${LLM_PORT}~:${RERANK_PORT})"
return 0
fi
local ai_script="${LEGACY_STACK_DIR}/scripts/exdev/start-host-ai.sh"
if [[ -x "${ai_script}" ]] || [[ -f "${ai_script}" ]]; then
log "호스트 AI 기동..."
bash "${ai_script}" || log "⚠️ start-host-ai.sh 일부 실패 — 기존 프로세스 확인"
fi
if host_ai_ready; then
return 0
fi
log "호스트 AI 기동 대기 (최대 120s)..."
local i=0
while (( i < 120 )); do
if host_ai_ready; then
log "✅ 호스트 AI 준비 완료"
return 0
fi
sleep 5
i=$((i + 5))
done
if curl -sf --max-time 3 "http://127.0.0.1:${LLM_PORT}/v1/models" >/dev/null 2>&1; then
log "⚠️ LLM만 응답 (Embed/Rerank 미기동) — degraded 모드로 계속"
return 0
fi
fail "호스트 AI 미응답 (:${LLM_PORT})"
}
stop_legacy_stack() {
if $SKIP_LEGACY_STOP; then return 0; fi
local stop_script="${LEGACY_STACK_DIR}/remote-stop-chatbot.sh"
if [[ -f "${stop_script}" ]]; then
log "레거시 exAIChatbot2.0 스택 중지 (포트 충돌 방지)..."
bash "${stop_script}" --destroy-stack 2>/dev/null || bash "${stop_script}" 2>/dev/null || true
docker rm -f ex-chatbot-caddy 2>/dev/null || true
fi
}
ensure_rag_env() {
# .env 키=값 갱신 (없으면 append)
patch_env() {
local key="$1" value="$2" tmp
tmp="$(mktemp)"
if [[ -f "${RAG_ENV}" ]]; then
grep -vE "^${key}=" "${RAG_ENV}" > "${tmp}" || true
else
: > "${tmp}"
fi
printf '%s\n%s=%s\n' "$(cat "${tmp}")" "${key}" "${value}" > "${RAG_ENV}"
rm -f "${tmp}"
}
if [[ ! -f "${RAG_ENV}" ]]; then
log ".env 없음 → env.exdev-server.template 복사"
cp "${ENV_TEMPLATE}" "${RAG_ENV}"
fi
# RAG(Docker) → 호스트 chatbotApi (exdev 로컬 WAS). 운영 IP(172.16.180.130) 사용 금지.
# GitHub 커밋 기본값은 운영 IP 유지 — git_fetch_all.sh 가 push 전 복원.
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}"
patch_env "CHATBOT_TOOL_TIMEOUT" "60"
if grep -qE 'CHATBOT_API_BASE_URL=.*172\.16\.180\.130' "${RAG_ENV}" 2>/dev/null; then
fail "exdev RAG .env 에 운영 WAS(172.16.180.130) 가 남아 있습니다"
fi
log "RAG .env WAS: host.docker.internal:${WAS_PORT} (로컬 chatbotApi, EXDEV_LOCAL=true)"
}
ensure_mongo() {
if docker ps -a --format '{{.Names}}' | grep -qx "${MONGO_CONTAINER}"; then
docker start "${MONGO_CONTAINER}" >/dev/null 2>&1 || true
log "MongoDB 재사용: ${MONGO_CONTAINER}"
wait_port 27017 "MongoDB" 20 || true
return 0
fi
if lsof -iTCP:27017 -sTCP:LISTEN >/dev/null 2>&1; then
log "MongoDB :27017 이미 사용 중"
return 0
fi
log "MongoDB 컨테이너 생성: ${MONGO_CONTAINER}"
docker run -d --name "${MONGO_CONTAINER}" -p 27017:27017 mongo:7 >/dev/null
wait_port 27017 "MongoDB" 30 || true
}
bootstrap_qdrant_if_empty() {
local collection="${QDRANT_COLLECTION:-qa_vectors_v2}"
if [[ -f "${RAG_ENV}" ]]; then
local from_env
from_env="$(grep -E '^QDRANT_COLLECTION=' "${RAG_ENV}" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d ' "\r' || true)"
[[ -n "${from_env}" ]] && collection="${from_env}"
fi
local vc
vc="$(curl -sf "http://127.0.0.1:6333/collections/${collection}" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('points_count',0))" 2>/dev/null || echo 0)"
if [[ "${vc}" -gt 0 ]]; then
log "Qdrant 기존 데이터 ${vc} points"
return 0
fi
local bootstrap="${SERVER_DEV_DIR}/bootstrap-qdrant-from-legacy.sh"
if [[ -x "${bootstrap}" ]]; then
log "Qdrant 비어 있음 → 레거시 데이터 이전"
"${bootstrap}" || log "⚠️ Qdrant bootstrap 실패"
fi
}
ensure_rag_compose() {
local template="${SERVER_DEV_DIR}/docker-compose.rag.server.yml"
[[ -f "${template}" ]] || fail "server-dev/docker-compose.rag.server.yml 없음"
cp "${template}" "${COMPOSE_SERVER}"
}
start_rag() {
ensure_rag_env
ensure_rag_compose
[[ -f "${COMPOSE_SERVER}" ]] || fail "docker-compose.server.yml 없음"
# compose environment 보간 + 컨테이너 env (로컬 WAS)
export EXDEV_LOCAL=true
export CHATBOT_API_BASE_URL="http://host.docker.internal:${WAS_PORT}/api"
export INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY}"
export CHATBOT_TOOL_TIMEOUT="${CHATBOT_TOOL_TIMEOUT:-60}"
cd "${RAG_DIR}"
docker compose -f "${COMPOSE_SERVER}" down --remove-orphans 2>/dev/null || true
local -a up_flags=(-d)
if $NO_BUILD; then up_flags+=(--no-build); else up_flags+=(--build); fi
log "RAG 스택 기동 (docker-compose.server.yml)..."
log " EXDEV_LOCAL=${EXDEV_LOCAL} CHATBOT_API_BASE_URL=${CHATBOT_API_BASE_URL}"
docker compose -f "${COMPOSE_SERVER}" up "${up_flags[@]}"
wait_http "http://127.0.0.1:6333/collections" "Qdrant" 30 || true
bootstrap_qdrant_if_empty
(cd "${RAG_DIR}" && docker compose -f "${COMPOSE_SERVER}" up -d) || true
wait_http "http://127.0.0.1:${RAG_PORT}/health" "RAG API" 180 || true
wait_port "${ADMIN_PORT}" "Vector Admin" 30 || true
}
ensure_java() {
local home=""
for candidate in \
"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" \
"/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"; 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
[[ -n "${home}" ]] || fail "Java 17 필요 — brew install openjdk@17"
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
local port=""
case "${name}" in
chatbotApi) port="${WAS_PORT}" ;;
kakaoChatbotSkill) port="${SKILL_PORT}" ;;
esac
if [[ -f "${pid_file}" ]]; then
local old_pid
old_pid="$(cat "${pid_file}")"
if kill -0 "$old_pid" 2>/dev/null; then
if [[ -n "${port}" ]] && lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
log "${name} 이미 실행 중 (PID ${old_pid}, :${port})"
return 0
fi
# pid 는 있으나 포트 미개방 → 좀비/실패 프로세스 정리
kill "$old_pid" 2>/dev/null || true
fi
rm -f "${pid_file}"
fi
[[ -n "${JAVA_HOME}" ]] || ensure_java
chmod +x "${dir}/gradlew"
# release=8 프로젝트는 Java 8 API만 허용. 기동 전 컴파일로 실패를 즉시 드러낸다.
log "${name} 컴파일 확인 (Java ${JAVA_HOME})..."
if ! (
cd "${dir}"
export JAVA_HOME
./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" compileJava --quiet
); then
fail "${name} 컴파일 실패 — ${dir} 소스/JDK 확인 (Java 8 API만 사용, isBlank/List.of 금지)"
fi
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}"
if [[ -n "${port}" ]]; then
if ! wait_port "${port}" "${name}" 240; then
warn "${name} :${port} 미개방 — 최근 로그:"
tail -40 "${log_file}" 2>/dev/null | sed 's/^/ /' || true
return 1
fi
fi
return 0
}
build_chatbot_app() {
if $NO_BUILD && [[ -f "${APP_DIR}/public/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
)
NEXT_PUBLIC_FARE_QUERY_MOCK=true bash "${APP_DIR}/build.sh"
build_desktop_app
}
start_chatbot_app() {
build_chatbot_app
docker rm -f ex-chatbot-app-web 2>/dev/null || true
(
cd "${APP_DIR}"
export CHATBOT_APP_PORT="${APP_PORT}"
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" 60 || true
}
start_caddy() {
if $SKIP_CADDY; then return 0; fi
[[ -f "${COMPOSE_CADDY}" ]] || return 0
local cert_dir="${LEGACY_STACK_DIR}/docker/nginx/certs"
[[ -f "${cert_dir}/fullchain.pem" ]] || fail "TLS 인증서 없음: ${cert_dir}/fullchain.pem"
log "Caddy HTTPS 기동 (443 → chatbotApp:${APP_PORT})..."
(
cd "${SERVER_DEV_DIR}"
export CHATBOT_APP_PORT="${APP_PORT}"
export RAG_API_PORT="${RAG_PORT}"
export SKILL_PORT="${SKILL_PORT}"
export ADMIN_PORT="${ADMIN_PORT}"
export LEGACY_CERTS_DIR="${cert_dir}"
docker compose -f docker-compose.caddy.yml up -d --force-recreate
)
wait_http "https://chatbot.exdev.co.kr/chatbot/ai/" "HTTPS UI" 30 || true
}
verify_stack() {
local ok=true
host_ai_ready || { log "❌ 호스트 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="ai-mode"' || { log "❌ chatbotApp UI"; ok=false; }
# exdev 는 운영 WAS IP 를 절대 참조하지 않아야 한다
local rag_was
rag_was="$(docker exec exaichatbot-agent-api-1 printenv CHATBOT_API_BASE_URL 2>/dev/null || true)"
if [[ "${rag_was}" == *172.16.180.130* ]]; then
log "❌ RAG 가 운영 WAS(172.16.180.130) 를 참조 중: ${rag_was}"
ok=false
fi
# 로컬 WAS tool 정의 로드 (음식메뉴 등 WEB tool 전제)
local tools_json
tools_json="$(curl -sf -m 8 -H "X-Internal-Tool-Key: ${INTERNAL_TOOL_API_KEY}" \
"http://127.0.0.1:${WAS_PORT}/api/v1/tools/definitions" 2>/dev/null || true)"
if ! echo "${tools_json}" | grep -q 'REST_AREA_FOOD_LIST'; then
log "❌ chatbotApi tool 정의에 REST_AREA_FOOD_LIST 없음 (WAS 미기동/연동 실패)"
ok=false
else
log "✅ chatbotApi domain tools OK"
fi
if $ok; then log "✅ 전체 스택 정상"; return 0; fi
log "⚠️ 일부 서비스 미응답 — ${LOG_DIR}/ 확인"
return 1
}
mkdir -p "${RUN_DIR}" "${LOG_DIR}"
log "=========================================="
log "exAichatbot Agent 2.0 기동"
log "=========================================="
if ! is_exdev_server; then
if $LOCAL_DEV; then
sync_source_from_git
run_local_dev
else
deploy_from_local_mac
fi
fi
sync_source_from_git
docker info >/dev/null 2>&1 || fail "Docker Engine 미기동"
log "exdev 서버 모드"
stop_legacy_stack
ensure_host_ai
ensure_mongo
start_rag
if ! $SKIP_JAVA; then
ensure_java
# Skill 먼저 (WAS WEB tool 이 Skill data-portal 을 역호출)
start_java_service "kakaoChatbotSkill" "${SKILL_DIR}" \
"${RUN_DIR}/kakao-skill.pid" "${LOG_DIR}/kakao-skill.log" \
"EXDEV_LOCAL=true" \
"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=${PUBLIC_BASE_URL}/chatbot/ai/" \
|| warn "kakaoChatbotSkill 기동 실패"
start_java_service "chatbotApi" "${API_DIR}" \
"${RUN_DIR}/chatbot-api.pid" "${LOG_DIR}/chatbot-api.log" \
"INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \
"SKILL_INTERNAL_BASE_URL=http://127.0.0.1:${SKILL_PORT}" \
"RAG_SERVER_HOST=127.0.0.1" \
"RAG_SERVER_PORT=${RAG_PORT}" \
|| warn "chatbotApi 기동 실패"
fi
start_chatbot_app
start_caddy
log "=========================================="
log "기동 완료"
log " HTTPS UI ${PUBLIC_BASE_URL}/chatbot/ai/"
log " RAG http://127.0.0.1:${RAG_PORT}/health"
log " Admin http://127.0.0.1:${ADMIN_PORT}/"
log " WAS http://127.0.0.1:${WAS_PORT}/"
log " Skill http://127.0.0.1:${SKILL_PORT}/web/ask"
log " Kakao ${PUBLIC_BASE_URL}/kakao/skill"
log " 로그 ${LOG_DIR}/"
log " 종료 ./server-dev/stop-all-server.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