feat(bot): 봇 명령어 14개 추가 (총 25개)

/oncall /incident /rca /escalate /sla
/assign /approve /reject /kb /wbs
/scouter /rollback /notify /topology /vuln

테스트: Python urllib UTF-8 직접 검증 - 25개 전원 통과

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-05-30 07:50:38 +09:00
parent 25d02183e3
commit a1b6f85917

View File

@ -325,6 +325,127 @@ async def handle_bot_command(
reply = await _cmd_open_issues(proj_code, db)
return BotReply(room=cmd.room, text=reply)
# ── /oncall ─── 현재 당직자 조회 ────────────────────────────────────────
elif keyword in ("/oncall", "!oncall"):
reply = await _cmd_oncall()
return BotReply(room=cmd.room, text=reply)
# ── /incident <제목> [P1|P2|P3|P4] ─── 인시던트 빠른 등록 ───────────────
elif keyword in ("/incident", "!incident", "/inc"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /incident <제목> [P1|P2|P3|P4]")
grade = parts[-1].upper() if parts[-1].upper() in ("P1","P2","P3","P4") else "P3"
title_parts = parts[1:-1] if grade in ("P1","P2","P3","P4") and len(parts) > 2 else parts[1:]
title = " ".join(title_parts)
bg.add_task(_cmd_create_incident, cmd.room, cmd.user, title, grade)
return BotReply(room=cmd.room, text=f"[{grade} 인시던트] '{title}' 등록 중...")
# ── /rca <인시던트ID> ─── AI 자동 RCA 분석 ───────────────────────────────
elif keyword in ("/rca", "!rca"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /rca <INC-ID>")
inc_id = parts[1]
bg.add_task(_cmd_auto_rca, cmd.room, cmd.user, inc_id)
return BotReply(room=cmd.room, text=f"[AI RCA] {inc_id} 자동 근본원인 분석 중...")
# ── /escalate <SR-ID> ─── SR 에스컬레이션 ───────────────────────────────
elif keyword in ("/escalate", "!escalate"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /escalate <SR-ID>")
sr_id = parts[1]
bg.add_task(_cmd_escalate, cmd.room, cmd.user, sr_id)
return BotReply(room=cmd.room, text=f"[에스컬레이션] {sr_id} 당직자에게 즉시 전달 중...")
# ── /sla ─── SLA 위반 현황 ───────────────────────────────────────────────
elif keyword in ("/sla", "!sla"):
reply = await _cmd_sla_violations()
return BotReply(room=cmd.room, text=reply)
# ── /assign <SR-ID> <담당자> ─── SR 담당자 배정 ──────────────────────────
elif keyword in ("/assign", "!assign"):
if len(parts) < 3:
return BotReply(room=cmd.room, text="사용법: /assign <SR-ID> <담당자>")
sr_id = parts[1]; assignee = parts[2]
reply = await _cmd_assign_sr(sr_id, assignee, cmd.user, db)
return BotReply(room=cmd.room, text=reply)
# ── /approve <SR-ID> ─── SR 즉시 승인 ───────────────────────────────────
elif keyword in ("/approve", "!approve"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /approve <SR-ID>")
sr_id = parts[1]
comment = " ".join(parts[2:]) if len(parts) > 2 else "봇 승인"
reply = await _cmd_approve_sr(sr_id, cmd.user, comment, db)
return BotReply(room=cmd.room, text=reply)
# ── /reject <SR-ID> [사유] ─── SR 반려 ───────────────────────────────────
elif keyword in ("/reject", "!reject"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /reject <SR-ID> [사유]")
sr_id = parts[1]
reason = " ".join(parts[2:]) if len(parts) > 2 else "봇 반려"
reply = await _cmd_reject_sr(sr_id, cmd.user, reason, db)
return BotReply(room=cmd.room, text=reply)
# ── /kb <검색어> ─── KB 문서 검색 ───────────────────────────────────────
elif keyword in ("/kb", "!kb"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /kb <검색어>")
query = " ".join(parts[1:])
reply = await _cmd_kb_search(query, db)
return BotReply(room=cmd.room, text=reply)
# ── /wbs <프로젝트코드> ─── WBS 지연 현황 ───────────────────────────────
elif keyword in ("/wbs", "!wbs"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /wbs <프로젝트코드>")
proj_code = parts[1]
reply = await _cmd_wbs_status(proj_code, db)
return BotReply(room=cmd.room, text=reply)
# ── /scouter <서버명> ─── Scouter APM 실시간 메트릭 ─────────────────────
elif keyword in ("/scouter", "!scouter"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /scouter <서버명>")
server_name = parts[1]
reply = await _cmd_scouter_metrics(server_name)
return BotReply(room=cmd.room, text=reply)
# ── /rollback <세션ID> ─── 배포 긴급 롤백 ───────────────────────────────
elif keyword in ("/rollback", "!rollback"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /rollback <세션ID>")
try:
sid = int(parts[1])
except ValueError:
return BotReply(room=cmd.room, text="세션 ID는 숫자여야 합니다.")
bg.add_task(_cmd_rollback, cmd.room, cmd.user, sid)
return BotReply(room=cmd.room, text=f"[긴급 롤백] Session #{sid} 롤백 시작...")
# ── /notify <메시지> ─── 운영팀 전체 공지 ───────────────────────────────
elif keyword in ("/notify", "!notify"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /notify <메시지>")
message = " ".join(parts[1:])
bg.add_task(_cmd_broadcast_notify, cmd.room, cmd.user, message)
return BotReply(room=cmd.room, text=f"[공지] 운영팀 전체에 메시지 발송 중...")
# ── /topology <서버명> ─── CI 의존관계 조회 ─────────────────────────────
elif keyword in ("/topology", "!topology"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /topology <서버명>")
server_name = parts[1]
reply = await _cmd_topology(server_name, db)
return BotReply(room=cmd.room, text=reply)
# ── /vuln <서버명> ─── 서버 취약점 스캔 ────────────────────────────────
elif keyword in ("/vuln", "!vuln"):
if len(parts) < 2:
return BotReply(room=cmd.room, text="사용법: /vuln <서버명|IP>")
target = parts[1]
bg.add_task(_cmd_vuln_scan, cmd.room, cmd.user, target)
return BotReply(room=cmd.room, text=f"[취약점 스캔] {target} 스캔 시작 (약 30초 소요)...")
# ── /help ─────────────────────────────────────────────────────────────────
elif keyword == "/help":
return BotReply(room=cmd.room, text=_help_text())
@ -515,6 +636,403 @@ async def _cmd_sm(room: str, actor: str, server: str,
)
# ══════════════════════════════════════════════════════════════════════════════
# 신규 봇 명령어 헬퍼 함수 (14개)
# ══════════════════════════════════════════════════════════════════════════════
async def _cmd_oncall() -> str:
"""/oncall — 현재 당직자 조회."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.get("http://localhost:8001/api/oncall/on-duty",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code == 200:
d = r.json()
if not d:
return "[당직자] 오늘 등록된 당직자가 없습니다."
lines = ["[현재 당직자]"]
for oc in (d if isinstance(d, list) else [d])[:3]:
eng = oc.get("engineer") or oc.get("username") or ""
phone = oc.get("phone") or oc.get("contact") or ""
lines.append(f" {eng}" + (f" ({phone})" if phone else ""))
return "\n".join(lines)
return f"당직자 조회 실패 ({r.status_code})"
except Exception as e:
return f"당직자 조회 오류: {str(e)[:80]}"
async def _cmd_create_incident(room: str, actor: str, title: str, grade: str):
"""/incident — 인시던트 빠른 등록."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.post("http://localhost:8001/api/incidents",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"title": title, "grade": grade,
"description": f"봇 등록 — 담당: {actor}",
"reported_by": actor})
if r.status_code == 201:
d = r.json()
inc_id = d.get("incident_id", "?")
grade_emoji = {"P1": "🚨", "P2": "🔴", "P3": "🟠", "P4": "🟡"}.get(grade, "⚠️")
msg = (f"{grade_emoji} [{grade}] 인시던트 등록 완료\n"
f"번호: {inc_id}\n제목: {title}\n담당: {actor}")
await _send_to_room(room, msg)
else:
await _send_to_room(room, f"[인시던트 등록 실패] {r.status_code}: {r.text[:100]}")
except Exception as e:
await _send_to_room(room, f"[인시던트 오류] {str(e)[:80]}")
async def _cmd_auto_rca(room: str, actor: str, inc_id: str):
"""/rca — AI 자동 RCA 분석."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=120.0) as c:
r = await c.post(f"http://localhost:8001/api/incidents/{inc_id}/auto-rca",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code == 200:
d = r.json()
rca = d.get("rca", {})
msg = (f"[AI RCA 완료] {inc_id}\n"
f"근본원인: {rca.get('root_cause','')[:100]}\n"
f"신뢰도: {int(rca.get('confidence',0)*100)}%\n"
f"재발방지: {', '.join(rca.get('prevention',[])[:2])}")
await _send_to_room(room, msg)
else:
# problem RCA 시도
async with _h.AsyncClient(timeout=120.0) as c:
r2 = await c.post(f"http://localhost:8001/api/problem/{inc_id}/auto-rca",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r2.status_code == 200:
d = r2.json()
rca = d.get("rca", {})
msg = (f"[AI RCA 완료] {inc_id}\n"
f"근본원인: {rca.get('root_cause','')[:100]}\n"
f"신뢰도: {int(rca.get('confidence',0)*100)}%")
await _send_to_room(room, msg)
else:
await _send_to_room(room, f"[RCA 실패] {inc_id} — 인시던트/Problem ID를 확인하세요")
except Exception as e:
await _send_to_room(room, f"[RCA 오류] {str(e)[:80]}")
async def _cmd_escalate(room: str, actor: str, sr_id: str):
"""/escalate — SR 에스컬레이션."""
import httpx as _h
try:
# 당직자 조회
async with _h.AsyncClient(timeout=10.0) as c:
oc_r = await c.get("http://localhost:8001/api/oncall/on-duty",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
oncall_info = oc_r.json() if oc_r.status_code == 200 else {}
oncall_eng = ""
if isinstance(oncall_info, list) and oncall_info:
oncall_eng = oncall_info[0].get("engineer", "")
elif isinstance(oncall_info, dict):
oncall_eng = oncall_info.get("engineer", "")
# SLA 에스컬레이션 API 호출
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.post("http://localhost:8001/api/oncall/escalate",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"sr_id": sr_id, "escalate_to": oncall_eng,
"reason": f"봇 에스컬레이션 — 요청자: {actor}"})
if r.status_code in (200, 201):
msg = (f"[에스컬레이션 완료] {sr_id}\n"
f"담당자: {oncall_eng or '당직자'}\n"
f"요청자: {actor}")
else:
msg = f"[에스컬레이션] {sr_id} → 당직자({oncall_eng or '미정'})에게 전달됨 (SR 상태 확인 필요)"
await _send_to_room(room, msg)
except Exception as e:
await _send_to_room(room, f"[에스컬레이션 오류] {str(e)[:80]}")
async def _cmd_sla_violations() -> str:
"""/sla — SLA 위반 현황."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.get("http://localhost:8001/api/tasks/sla/violations",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code == 200:
rows = r.json()
if not rows:
return "[SLA] 현재 위반 중인 SR이 없습니다. ✅"
lines = [f"[SLA 위반] {len(rows)}"]
for sr in rows[:5]:
overdue = sr.get("overdue_minutes", 0)
h, m = divmod(overdue, 60)
lines.append(f" {sr.get('sr_id','?')} [{sr.get('priority','?')}] "
f"{sr.get('title','')[:25]}{h}시간{m}분 초과")
if len(rows) > 5:
lines.append(f" ... 외 {len(rows)-5}")
return "\n".join(lines)
return f"SLA 조회 실패 ({r.status_code})"
except Exception as e:
return f"SLA 조회 오류: {str(e)[:80]}"
async def _cmd_assign_sr(sr_id: str, assignee: str, actor: str, db) -> str:
"""/assign — SR 담당자 즉시 배정."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.post(f"http://localhost:8001/api/assign/{sr_id}",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"engineer": assignee, "reason": f"봇 배정 by {actor}"})
if r.status_code in (200, 201):
return f"[배정 완료] {sr_id}{assignee}"
# 직접 상태 업데이트로 폴백
async with _h.AsyncClient(timeout=10.0) as c:
r2 = await c.patch(f"http://localhost:8001/api/tasks/{sr_id}/status",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"status": "IN_PROGRESS", "actor": actor,
"comment": f"담당자 배정: {assignee}"})
return (f"[배정] {sr_id}{assignee} (수동 확인 필요)"
if r2.status_code == 200 else f"배정 실패 ({r.status_code})")
except Exception as e:
return f"배정 오류: {str(e)[:80]}"
async def _cmd_approve_sr(sr_id: str, actor: str, comment: str, db) -> str:
"""/approve — SR 즉시 승인."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.post(f"http://localhost:8001/api/approvals/{sr_id}",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"approver": actor, "result": "APPROVED",
"comment": f"[봇승인] {comment}"})
if r.status_code == 201:
return f"[승인 완료] {sr_id} — 승인자: {actor}"
return f"승인 실패 ({r.status_code}) — SR이 승인 대기 상태인지 확인"
except Exception as e:
return f"승인 오류: {str(e)[:80]}"
async def _cmd_reject_sr(sr_id: str, actor: str, reason: str, db) -> str:
"""/reject — SR 반려."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.post(f"http://localhost:8001/api/approvals/{sr_id}",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"approver": actor, "result": "REJECTED",
"comment": f"[봇반려] {reason}"})
if r.status_code == 201:
return f"[반려 완료] {sr_id} — 사유: {reason}"
return f"반려 실패 ({r.status_code})"
except Exception as e:
return f"반려 오류: {str(e)[:80]}"
async def _cmd_kb_search(query: str, db) -> str:
"""/kb — KB 문서 검색."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.get("http://localhost:8001/api/kb",
params={"keyword": query, "limit": 5},
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code == 200:
docs = r.json()
if isinstance(docs, dict):
docs = docs.get("items", docs.get("results", []))
if not docs:
return f"[KB 검색] '{query}' 결과 없음 — 다른 키워드로 시도해 보세요."
lines = [f"[KB 검색] '{query}'{len(docs)}"]
for d in docs[:4]:
title = d.get("title", "")
doc_id = d.get("doc_id", d.get("id", ""))
lines.append(f" [{doc_id}] {title[:45]}")
return "\n".join(lines)
return f"KB 검색 실패 ({r.status_code})"
except Exception as e:
return f"KB 검색 오류: {str(e)[:80]}"
async def _cmd_wbs_status(proj_code: str, db) -> str:
"""/wbs — WBS 지연 현황."""
import httpx as _h
from datetime import date as _date
try:
from models import SiProject
from sqlalchemy import select as _sel
async with SessionLocal() as _db:
proj = (await _db.execute(
_sel(SiProject).where(SiProject.project_code == proj_code)
)).scalars().first()
if not proj:
return f"프로젝트를 찾을 수 없습니다: {proj_code}"
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.get(f"http://localhost:8001/api/si/projects/{proj.id}/wbs",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code == 200:
wbs_items = r.json()
today = str(_date.today())
delayed = [
w for w in wbs_items
if w.get("completion_pct", 0) < 100
and w.get("planned_end", "") and w.get("planned_end", "") < today
and w.get("is_leaf", True)
]
lines = [f"[WBS 현황] {proj_code}",
f"전체: {len(wbs_items)}건 | 지연: {len(delayed)}"]
for w in delayed[:5]:
lines.append(f" {w.get('wbs_code','?')} {w.get('title','')[:30]} "
f"({w.get('completion_pct',0)}%)")
return "\n".join(lines)
return f"WBS 조회 실패 ({r.status_code})"
except Exception as e:
return f"WBS 조회 오류: {str(e)[:80]}"
async def _cmd_scouter_metrics(server_name: str) -> str:
"""/scouter — Scouter APM 실시간 메트릭."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.get("http://localhost:8001/api/scouter/servers",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code != 200:
return "Scouter 서버 목록 조회 실패"
servers = r.json().get("servers", [])
target = next((s for s in servers
if server_name.lower() in s.get("objName","").lower()), None)
if not target:
return f"[Scouter] '{server_name}' 서버를 찾을 수 없습니다.\n모니터링 서버 목록을 확인하세요."
obj_hash = target.get("objHash")
async with _h.AsyncClient(timeout=10.0) as c:
r2 = await c.get(f"http://localhost:8001/api/scouter/servers/{obj_hash}/metrics",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r2.status_code == 200:
m = r2.json().get("metrics", {})
return (f"[Scouter] {target.get('objName')}\n"
f"CPU: {m.get('cpu',0):.1f}% | "
f"Heap: {m.get('heap_used',0)}MB/{m.get('heap_max',0)}MB\n"
f"TPS: {m.get('tps',0):.1f} | "
f"응답: {m.get('response_time',0):.0f}ms | "
f"에러: {m.get('error_rate',0):.1f}%")
return f"메트릭 조회 실패 ({r2.status_code})"
except Exception as e:
return f"Scouter 조회 오류: {str(e)[:80]}"
async def _cmd_rollback(room: str, actor: str, session_id: int):
"""/rollback — 배포 긴급 롤백."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=120.0) as c:
r = await c.post(f"http://localhost:8001/api/vibe/{session_id}/status",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"status": "FAILED", "actor": actor,
"note": "봇 긴급 롤백 요청"})
msg = (f"[긴급 롤백] Session #{session_id}\n"
+ ("롤백 처리 중 — Jenkins 파이프라인을 확인하세요."
if r.status_code == 200
else f"롤백 요청 전송 ({r.status_code}) — 수동 확인 필요"))
await _send_to_room(room, msg)
except Exception as e:
await _send_to_room(room, f"[롤백 오류] {str(e)[:80]}")
async def _cmd_broadcast_notify(room: str, actor: str, message: str):
"""/notify — 운영팀 전체 공지 발송."""
full_msg = f"[공지 — {actor}]\n{message}"
# 운영팀 채널로 발송
await _send_to_room(room, full_msg)
# ops 채널 추가 발송 (다른 채널이면)
if room != "ops":
await _send_to_room("ops", full_msg)
async def _cmd_topology(server_name: str, db) -> str:
"""/topology — 서버 CI 의존관계."""
import httpx as _h
from models import Server, ConfigItem
from sqlalchemy import select as _sel
try:
async with SessionLocal() as _db:
srv = (await _db.execute(
_sel(Server).where(Server.server_name.contains(server_name))
)).scalars().first()
if not srv:
return f"[토폴로지] 서버 '{server_name}'를 찾을 수 없습니다."
ci = (await _db.execute(
_sel(ConfigItem).where(ConfigItem.linked_server_id == srv.id)
)).scalars().first()
if not ci:
return f"[토폴로지] '{server_name}' — CMDB CI가 연결되지 않았습니다."
async with _h.AsyncClient(timeout=10.0) as c:
r = await c.get(f"http://localhost:8001/api/topology/graph/{ci.id}",
params={"depth": 2},
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r.status_code == 200:
d = r.json()
nodes = d.get("nodes", [])
links = d.get("links", [])
lines = [f"[토폴로지] {server_name} ({ci.ci_type})",
f"연결 노드: {len(nodes)}개 | 관계: {len(links)}"]
for n in nodes[:6]:
if not n.get("is_root"):
lines.append(f" └─ {n.get('name','')} [{n.get('type','')}]")
if len(nodes) > 7:
lines.append(f" ... 외 {len(nodes)-7}")
lines.append(f"상세: http://localhost:8001/api/topology/page?ci={ci.id}")
return "\n".join(lines)
return f"토폴로지 조회 실패 ({r.status_code})"
except Exception as e:
return f"토폴로지 오류: {str(e)[:80]}"
async def _cmd_vuln_scan(room: str, actor: str, target: str):
"""/vuln — 서버 취약점 스캔."""
import httpx as _h
try:
async with _h.AsyncClient(timeout=60.0) as c:
r = await c.post("http://localhost:8001/api/vuln/scan",
headers={"Authorization": f"Bearer {_get_internal_token()}",
"Content-Type": "application/json"},
json={"host": target, "include_llm": False, "timeout": 1.0})
if r.status_code == 202:
d = r.json()
scan_id = d.get("scan_id", "?")
# 결과 폴링 (최대 30초)
import asyncio as _aio
await _aio.sleep(30)
async with _h.AsyncClient(timeout=10.0) as c2:
r2 = await c2.get(f"http://localhost:8001/api/vuln/scans/{scan_id}",
headers={"Authorization": f"Bearer {_get_internal_token()}"})
if r2.status_code == 200:
res = r2.json()
msg = (f"[취약점 스캔 완료] {target}\n"
f"위험 수준: {res.get('risk_level','?')} | 점수: {res.get('risk_score',0)}\n"
f"취약점: {len(res.get('vulnerabilities',[]))}\n"
f"열린 포트: {', '.join(str(p) for p in res.get('open_ports',[])[:5])}")
else:
msg = f"[취약점 스캔] {target} 완료 — scan_id: {scan_id} (결과 조회: /api/vuln/scans/{scan_id})"
else:
msg = f"[취약점 스캔 실패] {target} ({r.status_code})"
await _send_to_room(room, msg)
except Exception as e:
await _send_to_room(room, f"[스캔 오류] {str(e)[:80]}")
async def _cmd_project_report(room: str, actor: str, proj_code: str, rtype: str, db):
"""/report — 프로젝트 보고서 생성 후 요약 발송."""
import httpx as _httpx
@ -855,38 +1373,55 @@ def _get_internal_token() -> str:
def _help_text() -> str:
return """GUARDiA ITSM 봇 명령어
[SR 관리]
/sr <제목> SR 빠른 접수
!status <sr_id> SR + 세션 상태 조회
/bulk <action> <SR-ID...> SR 대량 처리 (close/assign/status)
/bulk <action> <SR-ID...> SR 대량 처리
/assign <SR-ID> <담당자> SR 담당자 즉시 배정
/approve <SR-ID> [의견] SR 즉시 승인
/reject <SR-ID> [사유] SR 반려
/escalate <SR-ID> 당직자에게 에스컬레이션
/sla SLA 위반 현황
[PMS 프로젝트 관리]
/pms <프로젝트코드> 프로젝트 진척 현황
[인시던트 / 장애]
/incident <제목> [P1|P2|P3|P4] 인시던트 빠른 등록
/rca <INC-ID> AI 자동 RCA 분석
/oncall 현재 당직자 조회
[PMS 프로젝트]
/pms <코드> 프로젝트 진척 현황
/wbs <코드> WBS 지연 현황
/report <코드> [daily|weekly|monthly] 보고서 발송
/deliverables <프로젝트코드> 산출물 제출 현황
/issues <프로젝트코드> 미결 이슈 목록
/deliverables <코드> 산출물 제출 현황
/issues <코드> 미결 이슈 목록
[보안/품질]
/scan 시큐어코딩/웹접근성/개인정보 점검
/checklist 공공기관 필수 기능 현황
/perf [url] 성능 테스트 실행
[인프라 / 보안]
/topology <서버명> CI 의존관계 조회
/scouter <서버명> Scouter APM 실시간 메트릭
/vuln <서버명|IP> 취약점 스캔
/scan 소스 보안 점검
/checklist 공공기관 이행 현황
/perf [url] 성능 테스트
[배포]
!vibe <sr_id> [project_id] 바이브 코딩 세션 시작
[배포 제어]
!vibe <sr_id> [project_id] 바이브 코딩 세션
!build <session_id> 빌드 실행
!deploy <session_id> 배포 (SSH + WAS 재기동)
!deploy <session_id> 배포
/rollback <session_id> 긴급 롤백
!cancel <session_id> 세션 취소
[운영 정보]
/status 시스템 현황 요약
/license 라이선스 상태 조회
[정보 / 운영]
/status 시스템 현황
/license 라이선스 상태
/kb <검색어> KB 문서 검색
/notify <메시지> 운영팀 전체 공지
[SM 원격 제어]
!sm <server> <script> SM 스크립트 실행
!health <server> 시스템 헬스체크
!health <server> 헬스체크
!log <server> [logfile] 로그 분석
SM 스크립트 : system, tomcat, jboss, jeus, weblogic,
postgresql, oracle, mysql, tibero, esb, elasticsearch,
solr, pinpoint, scouter, ping, log_analysis"""
SM 스크립트 : system, tomcat, jboss, jeus,
weblogic, postgresql, oracle, mysql, tibero,
esb, elasticsearch, solr, pinpoint, scouter"""