feat(nextcloud): Nextcloud 4모듈 + NC핸들러 + AI Foreman v0.1
- tools/nextcloud_client.py: WebDAV/OCS/CalDAV/CardDAV 공통 클라이언트 - tools/nc_files.py: 파일 검색/목록/최근/공유링크 - tools/nc_calendar.py: CalDAV 일정 CRUD + ICS 빌더 - tools/nc_mail.py: IMAP 메일 조회 (PLAIN auth for Mailcow) - tools/nc_contacts.py: CardDAV 연락처 + EasyOCR 명함 스캔 - handlers/nc_handler.py: 자연어→NC도구 자동 라우팅 - core/foreman.py: 목표 분해 + 상담 세션 + Vikunja 등록 - prompts/foreman.md: Foreman 시스템 프롬프트 - prompts/unified.md: nextcloud 모드 분류 추가 - config.py: .env 따옴표 파싱 버그 수정 - api/discord_bot.py: /goal 커맨드 + Foreman 스레드 라우팅
This commit is contained in:
@@ -19,9 +19,17 @@ from discord.ext import commands
|
||||
import config
|
||||
from core.workspace import WorkspaceManager
|
||||
from core.gemini_caller import GeminiCaller, GeminiCallError
|
||||
from core.foreman import Foreman
|
||||
from handlers.nc_handler import NCHandler
|
||||
|
||||
logger = logging.getLogger("variet.discord")
|
||||
|
||||
# Nextcloud 도구 핸들러
|
||||
_nc_handler = NCHandler()
|
||||
|
||||
# AI Foreman (목표 분해)
|
||||
_foreman = Foreman()
|
||||
|
||||
EMBED_DESC_LIMIT = 4096
|
||||
EMBED_FIELD_LIMIT = 1024
|
||||
|
||||
@@ -263,7 +271,48 @@ async def on_message(message: discord.Message):
|
||||
if not user_text:
|
||||
return
|
||||
|
||||
# 취소 명령어 확인
|
||||
# ──────────────────────────────────────
|
||||
# Foreman 세션 스레드 확인
|
||||
# ──────────────────────────────────────
|
||||
foreman_session = _foreman.get_session(message.channel.id)
|
||||
if foreman_session:
|
||||
async def _foreman_reply():
|
||||
try:
|
||||
async with message.channel.typing():
|
||||
# ! 명령어 처리
|
||||
if user_text.startswith("!"):
|
||||
parts = user_text[1:].split(maxsplit=1)
|
||||
command = parts[0] if parts else ""
|
||||
args = parts[1] if len(parts) > 1 else ""
|
||||
response = await _foreman.handle_command(
|
||||
foreman_session, command, args,
|
||||
)
|
||||
else:
|
||||
# 자유 형식 대화
|
||||
response = await _foreman.handle_freeform(
|
||||
foreman_session, user_text,
|
||||
)
|
||||
|
||||
if response:
|
||||
if len(response) <= 2000:
|
||||
await message.reply(response)
|
||||
else:
|
||||
for i in range(0, len(response), 4000):
|
||||
embed = discord.Embed(
|
||||
description=response[i:i + 4000],
|
||||
color=0x9B59B6,
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
except Exception as e:
|
||||
logger.error(f"Foreman 오류: {e}", exc_info=True)
|
||||
await message.reply(f"⚠️ 오류: {str(e)[:200]}")
|
||||
|
||||
asyncio.create_task(_foreman_reply())
|
||||
return
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# 취소 명령어 확인
|
||||
# ──────────────────────────────────────
|
||||
cancel_keywords = {"취소", "stop", "cancel", "중지", "멈춰"}
|
||||
if user_text.lower() in cancel_keywords:
|
||||
channel_id = message.channel.id
|
||||
@@ -975,6 +1024,63 @@ async def anime_status(interaction: discord.Interaction):
|
||||
bot.tree.add_command(anime_group)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# /goal 커맨드 — AI Foreman 목표 분해
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
@bot.tree.command(name="goal", description="목표를 입력하면 AI가 작업 트리로 분해합니다")
|
||||
@app_commands.describe(goal="달성할 목표 (자연어)")
|
||||
async def goal_command(interaction: discord.Interaction, goal: str):
|
||||
"""Foreman 상담 모드 시작."""
|
||||
await interaction.response.defer()
|
||||
|
||||
# 스레드 생성
|
||||
thread_name = f"🎯 {goal[:80]}"
|
||||
thread = await interaction.channel.create_thread(
|
||||
name=thread_name,
|
||||
type=discord.ChannelType.public_thread,
|
||||
auto_archive_duration=1440,
|
||||
)
|
||||
|
||||
# 세션 생성
|
||||
session = _foreman.create_session(goal, thread.id, interaction.user.id)
|
||||
|
||||
# 시작 메시지
|
||||
start_embed = discord.Embed(
|
||||
title="🎯 AI Foreman — 목표 분해",
|
||||
description=(
|
||||
f"**목표:** {goal}\n\n"
|
||||
f"작업 트리를 생성 중... ⏳"
|
||||
),
|
||||
color=0x9B59B6,
|
||||
)
|
||||
await thread.send(embed=start_embed)
|
||||
await interaction.followup.send(f"✅ 상담 스레드가 생성되었습니다: <#{thread.id}>", ephemeral=True)
|
||||
|
||||
# Gemini로 목표 분해
|
||||
try:
|
||||
tasks = await _foreman.decompose_goal(session)
|
||||
if tasks:
|
||||
tree_display = "\n".join(t.to_display() for t in tasks)
|
||||
total = sum(len(t.to_flat_list()) for t in tasks)
|
||||
result_embed = discord.Embed(
|
||||
title="📋 작업 트리 (초안)",
|
||||
description=tree_display[:4000],
|
||||
color=0x2ECC71,
|
||||
)
|
||||
result_embed.set_footer(
|
||||
text=f"총 {total}개 작업 | !확정 !수정 !추가 !삭제 !현황"
|
||||
)
|
||||
await thread.send(embed=result_embed)
|
||||
else:
|
||||
await thread.send("⚠️ 작업 분해에 실패했습니다. 목표를 더 구체적으로 입력해주세요.")
|
||||
except GeminiCallError as e:
|
||||
await thread.send(f"⚠️ AI 호출 오류: {str(e)[:300]}")
|
||||
except Exception as e:
|
||||
logger.error(f"Foreman 분해 오류: {e}", exc_info=True)
|
||||
await thread.send(f"❌ 오류: {str(e)[:200]}")
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 기존 ! 명령어 (유지, 하위호환)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user