G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.4 KiB
6.4 KiB
| name | description |
|---|---|
| guardia-messenger | GUARDiA 프로젝트에서 메신저 연동 및 sLLM 파서 코드를 작성하거나 수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라: - FastAPI Webhook 엔드포인트 구현 - WebSocket 실시간 중계 서버 코드 - sLLM 자연어 파서 프롬프트 엔지니어링 - 메신저 응답 포맷 및 인터랙티브 버튼 구현 - Proactive 챗봇 대화 맥락 분석 |
GUARDiA 메신저 연동 스킬
sLLM 파서 원칙
출력 포맷 강제 (절대 준수)
- JSON 외 텍스트 출력 금지
- 부연 설명, 인삿말 포함 금지
- temperature: 0.1 (결정론적 출력)
- response_format: {"type": "json_object"}
시스템 프롬프트 기본 구조
SYSTEM_PROMPT = """
당신은 인프라 자동화 시스템의 핵심 파서입니다.
1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다.
2. 부연 설명, 인삿말, 안내 텍스트는 절대 포함하지 않습니다.
3. CMDB에 정의된 정확한 서버 ID와 매핑되지 않는 정보는 "UNKNOWN"으로 처리합니다.
4. 배포 명령 시 반드시 'requires_approval'(boolean) 값을 판단합니다.
출력 포맷 (반드시 이 구조만 사용):
{
"intent_type": "DEPLOY_UPGRADE|LOG_ANALYSIS|SERVICE_RESTART|SSL_CHECK|DB_QUERY|CRON_MANAGE",
"institution": "기관명 또는 UNKNOWN",
"system_name": "시스템명 또는 UNKNOWN",
"infrastructure_layer": "WEB|WAS|DB|ESB",
"target_nodes": [],
"action_sequence": [],
"deploy_artifacts": [],
"requires_approval": false,
"priority": "HIGH|MEDIUM|LOW"
}
"""
Webhook 수신 표준
@app.post("/messenger/webhook")
async def receive_message(request: Request):
# 1. 시크릿 키 검증
signature = request.headers.get("X-Messenger-Signature")
if not verify_signature(signature, await request.body()):
raise HTTPException(status_code=403)
payload = await request.json()
user_id = payload.get("user_id")
text = payload.get("text", "")
files = payload.get("files", [])
# 2. 봇 자신의 메시지는 무시 (무한 루프 방지)
if payload.get("bot") or not text.strip():
return {"status": "IGNORED"}
# 3. 파일 스테이징
staged = await stage_uploaded_files(files, user_id)
# 4. sLLM 파싱
parsed = parse_natural_language(text)
# 5. SR 생성 및 라우팅
sr_id = create_ops_task(parsed, user_id, staged)
route_to_engine(sr_id, parsed)
return {"status": "ACCEPTED", "sr_id": sr_id}
메신저 응답 포맷
텍스트 응답
def send_text_response(room_id: str, text: str):
messenger.post_message(room_id, {
"sender": "GUARDiA-Bot",
"text": text,
"msg_type": "CHAT"
})
인터랙티브 버튼 응답
def send_interactive_response(room_id: str, text: str, action_code: str, label: str):
messenger.post_message(room_id, {
"sender": "GUARDiA-Bot",
"text": text,
"is_widget": True,
"interactive_action": {
"type": "BUTTON",
"label": label,
"command_code": action_code
}
})
# 사용 예
send_interactive_response(
room_id,
"🤖 DB 지연 징후 감지. 대기 쿼리를 확인할까요?",
"FETCH_DB_LOCKS",
"대기 쿼리 확인"
)
승인 요청 응답
def request_approval(approver_id: str, sr_id: str, summary: str):
messenger.post_direct_message(approver_id, {
"sender": "GUARDiA-Bot",
"text": f"[{sr_id}] 배포 승인 요청\n{summary}",
"is_widget": True,
"interactive_action": {
"type": "APPROVAL_BUTTONS",
"approve_url": f"/api/sr/approve/{sr_id}",
"reject_url": f"/api/sr/reject/{sr_id}"
}
})
금지 사항
- 메신저 응답에 서버 IP, 비밀번호, SSH 계정 노출 금지
- 봇 응답 메시지에 다시 파서 실행 금지 (무한 루프)
- 에러 메시지에 스택트레이스 전체 노출 금지 → SR ID와 요약만 전달
- 파일 스테이징 없이 바로 원격 서버 전송 금지 → 반드시 임시 디렉토리 거침
파일 스테이징 표준
import aiofiles
from pathlib import Path
STAGING_BASE = Path("/app/guardia/staging")
async def stage_uploaded_files(files: list, user_id: str) -> list:
"""메신저 첨부 파일을 임시 디렉토리에 다운로드"""
staged = []
sr_dir = STAGING_BASE / user_id / datetime.now().strftime("%Y%m%d_%H%M%S")
sr_dir.mkdir(parents=True, exist_ok=True)
for f in files:
filename = f["name"]
url = f["url"]
dest = sr_dir / filename
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
async with aiofiles.open(dest, "wb") as fp:
await fp.write(await resp.read())
staged.append({"name": filename, "path": str(dest), "type": classify_artifact(filename)})
return staged
def classify_artifact(filename: str) -> str:
ext = Path(filename).suffix.lower()
if ext in {".class", ".jar", ".war"}: return "DYNAMIC"
if ext in {".html", ".js", ".css"}: return "STATIC"
if ext in {".png", ".jpg", ".gif"}: return "STATIC"
return "UNKNOWN"
Nginx 업스트림 설정 (WebSocket)
location /ws {
proxy_pass http://was_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400s;
proxy_buffering off;
client_max_body_size 100M;
}
라이선스 주의사항
메신저 봇이 SR 생성 또는 기관·서버 등록 명령을 처리할 때 라이선스 제한을 확인해야 한다.
| 기능 | 필요 에디션 | 한도 초과 시 봇 응답 |
|---|---|---|
| SR 생성 | 모든 에디션 | 정상 처리 |
| 기관 등록 명령 | STANDARD+(50개), ENTERPRISE(무제한) | "라이선스 한도 초과: 업그레이드가 필요합니다." |
| AI 에이전트 연동 | STANDARD 이상 | "AI_AGENTS 기능은 STANDARD 이상 에디션에서만 사용 가능합니다." |
봇이 ITSM API로부터 HTTP 403을 받으면 해당 오류 메시지를 사용자에게 전달한다:
if resp.status_code == 403:
detail = resp.json().get("detail", "라이선스 제한으로 요청이 거부되었습니다.")
messenger.reply(room_id, f"⚠️ {detail}")