Agent 2.0 exdev 서버 배포 스택

- server-dev start/stop/deploy 및 Gitea push 자동 배포
- local-dev 로컬 개발 환경

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-07-21 22:57:30 +09:00
commit 4b86b2a660
344 changed files with 45787 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
run/
logs/
*.pid
# Qdrant / Mongo 데이터
exAiChatBot-chatbot2.0-agent/qdrant_data/
exAiChatBot-chatbot2.0-agent/.env
local-dev/run/
# Java / Node build
**/build/
**/.gradle/
**/node_modules/
chatbotApp-chatbot2.0-agent/public/
chatbotApp-chatbot2.0-agent/public-*/
# IDE / OS
.DS_Store
.idea/
*.log
# 스냅샷 (용량)
local-dev/run/qdrant-snapshots/
+79
View File
@@ -0,0 +1,79 @@
# exdev 서버 배포 — exAichatbot Agent 2.0
`chatbot.exdev.co.kr` Mac Studio 서버에 Agent 2.0 스택(RAG + WAS + Skill + chatbotApp)을 배포합니다.
## 디렉터리
| 경로 (서버) | 용도 |
|-------------|------|
| `/Users/aidev/dev/exAichatbot_agent` | Git work tree (배포 소스) |
| `/Volumes/ADATA/git/exAichatbot_agent.git` | Gitea bare repo |
| `/Volumes/ADATA/git/deploy-exAichatbot_agent.sh` | push → 배포 스크립트 |
## 1회 설정 (서버)
```bash
ssh aidev@chatbot.exdev.co.kr
bash /Users/aidev/dev/exAichatbot_agent/server-dev/setup-gitea.sh
```
Gitea(`https://github.exdev.co.kr`)에서 **`exAichatbot_agent`** 저장소 생성 (Initialize unchecked).
## 로컬 → Gitea push
```bash
cd exChatbot_agent
git init
git add .
git commit -m "Agent 2.0 server deploy"
# Gitea (이 서버는 git@ 가 아닌 aidev@ 사용)
git remote add origin ssh://aidev@github.exdev.co.kr:2222/aidev/exAichatbot_agent.git
git push -u origin main
# 잘못된 예 (Permission denied):
# git@github.exdev.co.kr:aidev/exAichatbot_agent.git
```
**main push 시 자동:** `post-receive` → `deploy-exAichatbot_agent.sh` → `stop-all-server.sh` → `start-all-server.sh`
배포 로그: `/Volumes/ADATA/logs/deploy/exAichatbot_agent.log`
## 수동 기동 / 중지 (서버)
```bash
cd /Users/aidev/dev/exAichatbot_agent
./server-dev/stop-all-server.sh
./server-dev/start-all-server.sh # 빌드 포함
./server-dev/start-all-server.sh --no-build
```
## 스택 구성
| 서비스 | 포트 | URL |
|--------|------|-----|
| HTTPS UI (Caddy) | 443 | https://chatbot.exdev.co.kr/chatbot/app/ |
| chatbotApp nginx | 8092 | 내부 |
| kakaoChatbotSkill | 8083 | /web/ask |
| chatbotApi WAS | 8086 | /api |
| RAG API | 28012 | /agent/chat |
| Vector Admin | 28013 | /chatbot/admin |
| Qdrant | 6333 | REST |
| 호스트 AI | 16000~16002 | MLX LLM/Embed/Rerank |
## Qdrant 데이터
- 최초 기동 시 `vector_count=0` 이면 레거시 `exAIChatbot2.0/exAiChatBot-chatbot2.0/qdrant_data` 를 자동 복사
- 이후 `qdrant_data/` 볼륨 영속 유지 (git 제외)
## 환경변수
```bash
cp server-dev/env.exdev-server.template exAiChatBot-chatbot2.0-agent/.env
```
## 레거시 스택
`start-all-server.sh`는 포트 충돌 방지를 위해 `exAIChatbot2.0/remote-stop-chatbot.sh` 로 레거시 스택을 중지합니다.
유지하려면 `--skip-legacy-stop` 옵션을 사용하세요.
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# 레거시 exAIChatbot2.0 Qdrant(qa_vectors) → Agent 스택 qdrant_data 복사
# 최초 배포 시 vector_count=0 일 때 start-all-server.sh 에서 자동 호출
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RAG_DIR="${ROOT_DIR}/exAiChatBot-chatbot2.0-agent"
LEGACY_STACK_DIR="${LEGACY_STACK_DIR:-/Users/aidev/dev/exAIChatbot2.0}"
LEGACY_QDRANT="${LEGACY_QDRANT_DATA:-${LEGACY_STACK_DIR}/exAiChatBot-chatbot2.0/qdrant_data}"
TARGET_QDRANT="${RAG_DIR}/qdrant_data"
log() { echo "[$(date '+%H:%M:%S')] $*"; }
if [[ ! -d "${LEGACY_QDRANT}/collections" ]]; then
log "레거시 qdrant_data 없음: ${LEGACY_QDRANT}"
exit 1
fi
if [[ -d "${TARGET_QDRANT}/collections/qa_vectors" ]]; then
local_count="$(curl -sf http://127.0.0.1:6333/collections/qa_vectors 2>/dev/null \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('points_count',0))" 2>/dev/null || echo 0)"
if [[ "${local_count}" -gt 0 ]]; then
log "이미 ${local_count} points — bootstrap 생략"
exit 0
fi
fi
log "Qdrant 중지 후 데이터 복사..."
export PATH="/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH}"
(cd "${RAG_DIR}" && docker compose -f docker-compose.server.yml stop qdrant api admin 2>/dev/null) || true
rm -rf "${TARGET_QDRANT}"
mkdir -p "${TARGET_QDRANT}"
cp -a "${LEGACY_QDRANT}/." "${TARGET_QDRANT}/"
log "✅ qdrant_data 복사 완료 → ${TARGET_QDRANT}"
+60
View File
@@ -0,0 +1,60 @@
# Agent 2.0 — chatbot.exdev.co.kr HTTPS 라우팅
# chatbotApp(:8092) + Skill(:8083) + RAG(:28012) + Admin(:28013)
{
email {$CADDY_ACME_EMAIL:admin@exdev.co.kr}
auto_https disable_redirects
}
(tls_certs) {
tls /etc/caddy/certs/fullchain.pem /etc/caddy/certs/privkey.pem
}
https://{$CHATBOT_DOMAIN:chatbot.exdev.co.kr} {
import tls_certs
# RAG Agent API (Skill 내부 호출·외부 디버그)
handle /agent/* {
reverse_proxy host.docker.internal:{$RAG_API_PORT:28012} {
transport http {
read_timeout 120s
write_timeout 120s
}
}
}
# Vector Admin
handle /chatbot/admin* {
uri strip_prefix /chatbot/admin
reverse_proxy host.docker.internal:{$ADMIN_PORT:28013} {
transport http {
read_timeout 120s
write_timeout 120s
}
}
}
# Skill 웹훅·API (/web/ask 등)
handle /web/* {
reverse_proxy host.docker.internal:{$SKILL_PORT:8083} {
transport http {
read_timeout 120s
write_timeout 120s
}
}
}
# UI + /chatbot/* API 프록시 (chatbotApp nginx)
handle {
reverse_proxy host.docker.internal:{$CHATBOT_APP_PORT:8092} {
flush_interval -1
transport http {
read_timeout 120s
write_timeout 120s
}
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
}
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# ==============================================================================
# exAichatbot_agent — Gitea push → exdev 서버 빌드·배포
# 설치: /Volumes/ADATA/git/deploy-exAichatbot_agent.sh
# 호출: post-receive (main 브랜치 push)
# ==============================================================================
set -euo pipefail
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git}"
WORK_TREE="${WORK_TREE:-/Users/aidev/dev/exAichatbot_agent}"
BRANCH="${BRANCH:-main}"
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/exAichatbot_agent.log}"
LOG_TAG="[exAichatbot_agent-deploy]"
# 레거시 bare repo (Gitea 미사용 시)
# GIT_DIR=/Volumes/ADATA/git/exAichatbot_agent.git
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH:-/usr/bin:/bin}"
mkdir -p "$(dirname "$DEPLOY_LOG")"
exec >> "$DEPLOY_LOG" 2>&1
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_TAG $*"; }
fail() { log "[ERROR] $*"; exit 1; }
LOCK_FILE="/tmp/exAichatbot_agent_deploy.lock"
if [[ -f "$LOCK_FILE" ]]; then
LOCK_PID="$(cat "$LOCK_FILE" 2>/dev/null || echo "")"
if [[ -n "$LOCK_PID" ]] && kill -0 "$LOCK_PID" 2>/dev/null; then
fail "다른 배포 진행 중 (PID: $LOCK_PID)"
fi
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"; log "락 해제"' EXIT
log "======================================================"
log " exAichatbot Agent 2.0 배포 시작"
log " GIT_DIR : $GIT_DIR"
log " WORK_TREE : $WORK_TREE"
log " BRANCH : $BRANCH"
log "======================================================"
[[ -d "$GIT_DIR" ]] || fail "GIT_DIR 없음: $GIT_DIR"
mkdir -p "$WORK_TREE"
log "[Phase 1] git checkout -f $BRANCH"
git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
COMMIT="$(git --git-dir="$GIT_DIR" rev-parse --short HEAD)"
log "체크아웃: $COMMIT"
chmod +x "$WORK_TREE/server-dev/"*.sh 2>/dev/null || true
chmod +x "$WORK_TREE/chatbotApp-chatbot2.0-agent/build.sh" 2>/dev/null || true
log "[Phase 2] stop-all-server.sh"
"$WORK_TREE/server-dev/stop-all-server.sh" --keep-caddy || true
log "[Phase 3] .env 준비"
ENV_FILE="$WORK_TREE/exAiChatBot-chatbot2.0-agent/.env"
if [[ ! -f "$ENV_FILE" ]]; then
cp "$WORK_TREE/server-dev/env.exdev-server.template" "$ENV_FILE"
log ".env 생성 (env.exdev-server.template)"
fi
log "[Phase 4] start-all-server.sh (빌드 포함)"
"$WORK_TREE/server-dev/start-all-server.sh"
log "======================================================"
log " 배포 완료 — $COMMIT"
log " UI: https://chatbot.exdev.co.kr/chatbot/app/"
log "======================================================"
+27
View File
@@ -0,0 +1,27 @@
# Agent 2.0 HTTPS — chatbot.exdev.co.kr (443)
# 인증서: exAIChatbot2.0/docker/nginx/certs/ (기존 obtain-cert-exdev.sh)
services:
caddy-agent:
image: caddy:2-alpine
container_name: exaichatbot-agent-caddy
restart: unless-stopped
ports:
- "${NGINX_HTTPS_PORT:-443}:443"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
CHATBOT_DOMAIN: "${CHATBOT_DOMAIN:-chatbot.exdev.co.kr}"
CHATBOT_APP_PORT: "${CHATBOT_APP_PORT:-8092}"
RAG_API_PORT: "${RAG_API_PORT:-28012}"
SKILL_PORT: "${SKILL_PORT:-8083}"
ADMIN_PORT: "${ADMIN_PORT:-28013}"
CADDY_ACME_EMAIL: "${CADDY_ACME_EMAIL:-admin@exdev.co.kr}"
volumes:
- ./caddy/Caddyfile.agent.exdev:/etc/caddy/Caddyfile:ro
- "${LEGACY_CERTS_DIR:-/Users/aidev/dev/exAIChatbot2.0/docker/nginx/certs}:/etc/caddy/certs:ro"
- exaichatbot-agent-caddy-data:/data
- exaichatbot-agent-caddy-config:/config
volumes:
exaichatbot-agent-caddy-data:
exaichatbot-agent-caddy-config:
+39
View File
@@ -0,0 +1,39 @@
# exdev 서버(chatbot.exdev.co.kr) — Agent 2.0 스택
# cp server-dev/env.exdev-server.template exAiChatBot-chatbot2.0-agent/.env
# ── 호스트 AI (Mac Studio MLX) ──
LLM_BASE_URL=http://127.0.0.1:16000
TEI_EMBED_URL=http://127.0.0.1:16001
TEI_RERANK_URL=http://127.0.0.1:16002
LLM_MODEL_NAME=mlx-community/Qwen2.5-7B-Instruct-4bit
EMBED_MODEL_NAME=Qwen/Qwen3-Embedding-8B
TEI_RERANK_MODEL=qwen3-reranker-4b
EMBED_API_STYLE=tei
RERANK_API_STYLE=legacy
MODEL_VERIFY_SSL=false
API_TIMEOUT=120
MODEL_API_KEY=
# ── MongoDB ──
MONGO_HOST=127.0.0.1
MONGO_PORT=27017
MONGO_USER=exlink
MONGO_PASSWORD=!wkcproqkf1
MONGO_DATABASE=chat_history
MONGO_COLLECTION=rag_conversations
# ── Qdrant ──
VECTOR_STORE=qdrant
QDRANT_HOST=127.0.0.1
QDRANT_PORT=6333
QDRANT_COLLECTION=qa_vectors
# ── Agent / WAS ──
AGENT_MODE=agent
AGENT_MAX_ROUNDS=6
CHATBOT_API_BASE_URL=http://127.0.0.1:8086/api
INTERNAL_TOOL_API_KEY=exdev-agent-tool-key
CHATBOT_TOOL_TIMEOUT=60
ADMIN_PORT=28013
UVICORN_WORKERS=2
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# exAichatbot_agent — Gitea post-receive (main push → deploy)
# 설치: setup-gitea.sh 가 server hooks/post-receive 로 복사
set -euo pipefail
GIT_DIR="/Volumes/ADATA/git/exAichatbot_agent.git"
WORK_TREE="/Users/aidev/dev/exAichatbot_agent"
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy-exAichatbot_agent.sh"
DEPLOY_LOG="/Volumes/ADATA/logs/deploy/exAichatbot_agent.log"
HOOK_TAG="[post-receive]"
mkdir -p "$(dirname "$DEPLOY_LOG")"
echo ""
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG exAichatbot_agent push 수신"
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
while read -r OLDREV NEWREV REFNAME; do
BRANCH_NAME="${REFNAME#refs/heads/}"
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 브랜치: $BRANCH_NAME ($OLDREV → $NEWREV)"
if [[ "$BRANCH_NAME" != "main" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG main 외 브랜치 — 배포 생략"
continue
fi
if [[ ! -x "$DEPLOY_SCRIPT" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] deploy 스크립트 없음: $DEPLOY_SCRIPT"
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG deploy 실행"
export GIT_DIR WORK_TREE
"$DEPLOY_SCRIPT" 2>&1 | tee -a "$DEPLOY_LOG"
EXIT_CODE=${PIPESTATUS[0]}
if [[ "$EXIT_CODE" -ne 0 ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 실패 (exit: $EXIT_CODE)"
exit "$EXIT_CODE"
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 배포 완료"
done
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# chatbot.exdev.co.kr — Gitea bare repo + post-receive 훅 1회 설정
#
# 서버(aidev@chatbot.exdev.co.kr)에서 실행:
# bash server-dev/setup-gitea.sh
#
# 또는 로컬에서 SSH로:
# ssh aidev@chatbot.exdev.co.kr 'bash -s' < server-dev/setup-gitea.sh
set -euo pipefail
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git}"
WORK_TREE="${WORK_TREE:-/Users/aidev/dev/exAichatbot_agent}"
DEPLOY_SCRIPT="${DEPLOY_SCRIPT:-/Volumes/ADATA/git/deploy-exAichatbot_agent.sh}"
GITEA_URL="${GITEA_URL:-https://github.exdev.co.kr}"
REPO_NAME="${REPO_NAME:-exAichatbot_agent}"
log() { echo "[setup] $*"; }
fail() { log "ERROR: $*"; exit 1; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log "bare repo: ${GIT_DIR}"
mkdir -p "$(dirname "${GIT_DIR}")" "${WORK_TREE}" /Volumes/ADATA/logs/deploy
if [[ ! -d "${GIT_DIR}" ]]; then
git init --bare "${GIT_DIR}"
log "bare repo 생성"
else
log "bare repo 이미 존재"
fi
log "deploy 스크립트 설치 → ${DEPLOY_SCRIPT}"
cp "${SCRIPT_DIR}/deploy.sh" "${DEPLOY_SCRIPT}"
chmod +x "${DEPLOY_SCRIPT}"
log "post-receive 훅 설치"
cp "${SCRIPT_DIR}/hooks/post-receive" "${GIT_DIR}/hooks/post-receive"
chmod +x "${GIT_DIR}/hooks/post-receive"
log "work tree 준비: ${WORK_TREE}"
if [[ ! -f "${WORK_TREE}/server-dev/start-all-server.sh" ]]; then
git --work-tree="${WORK_TREE}" --git-dir="${GIT_DIR}" checkout -f main 2>/dev/null || \
log "work tree 비어 있음 — 첫 push 후 checkout 됩니다"
fi
chmod +x "${WORK_TREE}/server-dev/"*.sh 2>/dev/null || true
cat <<EOF
============================================================
Gitea 저장소 등록 (웹 UI 또는 CLI)
1) ${GITEA_URL} → New Repository
- Name: ${REPO_NAME}
- Owner: (조직 또는 aidev)
- Uncheck "Initialize Repository"
2) 로컬 개발 Mac에서:
cd /path/to/exChatbot_agent
git init
git remote add origin ssh://aidev@github.exdev.co.kr:2222/aidev/${REPO_NAME}.git
git add .
git commit -m "Initial Agent 2.0 stack"
git push -u origin main
3) push 시 post-receive → deploy-exAichatbot_agent.sh 자동 실행
서버 경로:
GIT_DIR = ${GIT_DIR}
WORK_TREE = ${WORK_TREE}
Deploy log = /Volumes/ADATA/logs/deploy/exAichatbot_agent.log
수동 배포:
${DEPLOY_SCRIPT}
${WORK_TREE}/server-dev/start-all-server.sh
============================================================
EOF
+5
View File
@@ -0,0 +1,5 @@
Host github.exdev.co.kr
HostName github.exdev.co.kr
Port 2222
User aidev
IdentityFile ~/.ssh/id_ed25519
+322
View File
@@ -0,0 +1,322 @@
#!/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-java
# ./server-dev/start-all-server.sh --skip-legacy-stop
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"
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"
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
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 ;;
-h|--help)
sed -n '2,10p' "$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
}
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"
[[ -x "${ai_script}" ]] || [[ -f "${ai_script}" ]] || fail "호스트 AI 스크립트 없음: ${ai_script}"
log "호스트 AI 기동..."
bash "${ai_script}" || fail "start-host-ai.sh 실패"
wait_http "http://127.0.0.1:${LLM_PORT}/v1/models" "호스트 LLM" 120 || true
}
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() {
if [[ ! -f "${RAG_ENV}" ]]; then
log ".env 없음 → env.exdev-server.template 복사"
cp "${ENV_TEMPLATE}" "${RAG_ENV}"
fi
}
ensure_mongo() {
if docker ps --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 vc
vc="$(curl -sf "http://127.0.0.1:6333/collections/qa_vectors" 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
}
start_rag() {
ensure_rag_env
[[ -f "${COMPOSE_SERVER}" ]] || fail "docker-compose.server.yml 없음"
cd "${RAG_DIR}"
local -a up_flags=(-d)
if $NO_BUILD; then up_flags+=(--no-build); else up_flags+=(--build); fi
log "RAG 스택 기동..."
docker compose -f "${COMPOSE_SERVER}" up "${up_flags[@]}"
wait_http "http://127.0.0.1:6333/collections" "Qdrant" 30 || true
bootstrap_qdrant_if_empty
wait_http "http://127.0.0.1:${RAG_PORT}/health" "RAG API" 120
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
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}"
}
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
)
bash "${APP_DIR}/build.sh"
}
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/app/" "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/app/" "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/app/" | grep -q 'data-chatbot-app' || { log "❌ chatbotApp UI"; ok=false; }
if $ok; then log "✅ 전체 스택 정상"; return 0; fi
log "⚠️ 일부 서비스 미응답 — ${LOG_DIR}/ 확인"
return 1
}
mkdir -p "${RUN_DIR}" "${LOG_DIR}"
docker info >/dev/null 2>&1 || fail "Docker Engine 미기동"
log "=========================================="
log "exAichatbot Agent 2.0 — exdev 서버 기동"
log "=========================================="
stop_legacy_stack
ensure_host_ai
ensure_mongo
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}" \
"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=${PUBLIC_BASE_URL}/chatbot/app/"
wait_port "${WAS_PORT}" "chatbotApi" 240 || true
wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 240 || true
fi
start_chatbot_app
start_caddy
log "=========================================="
log "기동 완료"
log " HTTPS UI ${PUBLIC_BASE_URL}/chatbot/app/"
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 " 로그 ${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
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# chatbot.exdev.co.kr — Agent 2.0 전체 스택 중지
#
# ./server-dev/stop-all-server.sh
# ./server-dev/stop-all-server.sh --keep-caddy
# ./server-dev/stop-all-server.sh --purge-mongo
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SERVER_DEV_DIR="${ROOT_DIR}/server-dev"
RUN_DIR="${SERVER_DEV_DIR}/run"
RAG_DIR="${ROOT_DIR}/exAiChatBot-chatbot2.0-agent"
APP_DIR="${ROOT_DIR}/chatbotApp-chatbot2.0-agent"
COMPOSE_SERVER="${RAG_DIR}/docker-compose.server.yml"
COMPOSE_CADDY="${SERVER_DEV_DIR}/docker-compose.caddy.yml"
MONGO_CONTAINER="${MONGO_CONTAINER:-exaichatbot-agent-mongo}"
LEGACY_STACK_DIR="${LEGACY_STACK_DIR:-/Users/aidev/dev/exAIChatbot2.0}"
KEEP_CADDY=false
PURGE_MONGO=false
STOP_HOST_AI=false
for arg in "$@"; do
case "$arg" in
--keep-caddy) KEEP_CADDY=true ;;
--purge-mongo) PURGE_MONGO=true ;;
--stop-host-ai) STOP_HOST_AI=true ;;
-h|--help)
sed -n '2,8p' "$0"
exit 0
;;
esac
done
export PATH="/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH}"
log() { echo "[$(date '+%H:%M:%S')] $*"; }
stop_pid_file() {
local name="$1" pid_file="$2"
[[ -f "${pid_file}" ]] || return 0
local pid
pid="$(cat "${pid_file}")"
if kill -0 "$pid" 2>/dev/null; then
log "${name} 종료 (PID ${pid})"
kill "$pid" 2>/dev/null || true
sleep 2
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "${pid_file}"
}
stop_port() {
local port="$1"
local pids
pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
if [[ -n "${pids}" ]]; then
log ":${port} 점유 프로세스 종료"
kill ${pids} 2>/dev/null || true
sleep 1
kill -9 ${pids} 2>/dev/null || true
fi
}
log "Agent 2.0 스택 중지..."
stop_pid_file "chatbotApi" "${RUN_DIR}/chatbot-api.pid"
stop_pid_file "kakaoChatbotSkill" "${RUN_DIR}/kakao-skill.pid"
for port in 8086 8083; do stop_port "${port}"; done
if [[ -d "${APP_DIR}" ]]; then
docker rm -f ex-chatbot-app-web 2>/dev/null || true
(cd "${APP_DIR}" && docker compose down --remove-orphans 2>/dev/null) || true
log "chatbotApp 중지"
fi
if [[ -f "${COMPOSE_SERVER}" ]]; then
(cd "${RAG_DIR}" && docker compose -f "${COMPOSE_SERVER}" down --remove-orphans 2>/dev/null) || true
log "RAG/Qdrant/Admin 중지"
fi
if ! $KEEP_CADDY && [[ -f "${COMPOSE_CADDY}" ]]; then
(cd "${SERVER_DEV_DIR}" && docker compose -f docker-compose.caddy.yml down --remove-orphans 2>/dev/null) || true
docker rm -f exaichatbot-agent-caddy 2>/dev/null || true
log "Caddy(HTTPS) 중지"
fi
if $PURGE_MONGO; then
docker stop "${MONGO_CONTAINER}" 2>/dev/null || true
docker rm -f "${MONGO_CONTAINER}" 2>/dev/null || true
log "MongoDB 컨테이너 삭제 (${MONGO_CONTAINER})"
else
docker stop "${MONGO_CONTAINER}" 2>/dev/null || true
log "MongoDB 중지 (${MONGO_CONTAINER})"
fi
if $STOP_HOST_AI && [[ -f "${LEGACY_STACK_DIR}/scripts/exdev/stop-host-ai.sh" ]]; then
bash "${LEGACY_STACK_DIR}/scripts/exdev/stop-host-ai.sh" || true
log "호스트 AI 중지"
fi
log "완료"