fix(bot): 기동 버그 3건 수정 + feat(vikunja): 프로젝트 관리 기능 추가
- fix: apscheduler 누락 의존성 설치 - fix(main): StreamHandler cp949 UnicodeEncodeError 수정 (UTF-8 강제) - fix: workspaces.json 경로 Certes→Variet-Worker 수정 - fix(gemini): MCP issues detected 노이즈 필터 추가 - fix(bot): on_command_error 핸들러 추가 (CommandNotFound 로그 오염 방지) - feat(vikunja): projects 커맨드 (전체 프로젝트 목록+태스크 통계) - feat(vikunja): report 커맨드 (태스크+git log+devlog 종합 현황) - docs(agent): Vikunja 도구 섹션 확장 (12개 커맨드+라벨 가이드) - docs: known-issues 2건 추가, devlog 세션 1 기록
This commit is contained in:
@@ -81,3 +81,15 @@
|
|||||||
- **원인**: Wiki.js `update` mutation이 `tags` 파라미터 생략 시 내부적으로 undefined 처리하여 crash
|
- **원인**: Wiki.js `update` mutation이 `tags` 파라미터 생략 시 내부적으로 undefined 처리하여 crash
|
||||||
- **해결**: `update_page()`에서 `tags`가 None이면 `get_page()`로 기존 tags를 먼저 조회하여 항상 전달
|
- **해결**: `update_page()`에서 `tags`가 None이면 `get_page()`로 기존 tags를 먼저 조회하여 항상 전달
|
||||||
- **주의**: Wiki.js GraphQL mutation은 optional로 보이는 필드도 생략 시 에러 가능. 항상 모든 필드를 명시적으로 전달
|
- **주의**: Wiki.js GraphQL mutation은 optional로 보이는 필드도 생략 시 에러 가능. 항상 모든 필드를 명시적으로 전달
|
||||||
|
|
||||||
|
### [2026-03-16] main.py StreamHandler — cp949 콘솔에서 한글/특수문자 UnicodeEncodeError
|
||||||
|
- **증상**: 봇 기동 시 `UnicodeEncodeError: 'cp949' codec can't encode character '\u2014'` 로 프로세스 비정상 종료
|
||||||
|
- **원인**: `logging.StreamHandler(sys.stdout)` 기본값이 시스템 인코딩(cp949) 사용. 로그 메시지의 em-dash(`—`) 등 유니코드 문자가 인코딩 불가
|
||||||
|
- **해결**: `io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")`로 StreamHandler를 UTF-8 고정
|
||||||
|
- **주의**: Windows 한글 환경에서 **모든 콘솔 출력**에 cp949 인코딩 문제 발생 가능. `subprocess.run`도 `encoding="utf-8", errors="replace"` 명시 필수
|
||||||
|
|
||||||
|
### [2026-03-16] workspaces.json — 다른 PC 사용자 경로로 봇 기동 실패
|
||||||
|
- **증상**: `[WinError 267] 디렉터리 이름이 올바르지 않습니다` — Gemini agent 호출 시 cwd 오류
|
||||||
|
- **원인**: `workspaces.json`의 path가 다른 PC의 사용자 경로(`c:\Users\Certes\...`)로 하드코딩
|
||||||
|
- **해결**: 현재 머신의 사용자 경로(`c:\Users\Variet-Worker\...`)로 수정
|
||||||
|
- **주의**: 멀티 환경 배포 시 workspaces.json의 절대 경로가 환경별로 다를 수 있음. 상대 경로 또는 환경변수 사용 고려
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ Usage:
|
|||||||
python vikunja_helper.py list # List all tasks
|
python vikunja_helper.py list # List all tasks
|
||||||
python vikunja_helper.py list todo # List TODO only
|
python vikunja_helper.py list todo # List TODO only
|
||||||
python vikunja_helper.py list done # List DONE only
|
python vikunja_helper.py list done # List DONE only
|
||||||
|
python vikunja_helper.py projects # List all Vikunja projects
|
||||||
|
python vikunja_helper.py report # Project status report (current)
|
||||||
|
python vikunja_helper.py report <project_id> # Project status report (specific)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -170,6 +173,109 @@ def create_task(title: str, description: str = "", done: bool = False, labels: l
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def list_projects():
|
||||||
|
"""Vikunja 전체 프로젝트 목록 + 태스크 통계."""
|
||||||
|
projects = api_get("/projects")
|
||||||
|
print("📂 프로젝트 목록:")
|
||||||
|
for p in projects:
|
||||||
|
pid = p["id"]
|
||||||
|
title = p["title"]
|
||||||
|
# 각 프로젝트의 태스크 수 조회
|
||||||
|
try:
|
||||||
|
tasks = api_get(f"/projects/{pid}/tasks?per_page=200")
|
||||||
|
todo = sum(1 for t in tasks if not t["done"])
|
||||||
|
done = sum(1 for t in tasks if t["done"])
|
||||||
|
print(f" #{pid:<3d} {title:<30s} TODO: {todo} DONE: {done}")
|
||||||
|
except Exception:
|
||||||
|
print(f" #{pid:<3d} {title:<30s} (조회 실패)")
|
||||||
|
print(f"\n Total: {len(projects)} projects")
|
||||||
|
|
||||||
|
|
||||||
|
def report(project_id: int = None):
|
||||||
|
"""프로젝트 종합 현황 보고 (태스크 + git log + devlog)."""
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
pid = project_id or PROJECT_ID
|
||||||
|
|
||||||
|
# 1) 프로젝트 이름 조회
|
||||||
|
try:
|
||||||
|
projects = api_get("/projects")
|
||||||
|
proj_name = next((p["title"] for p in projects if p["id"] == pid), f"Project #{pid}")
|
||||||
|
except Exception:
|
||||||
|
proj_name = f"Project #{pid}"
|
||||||
|
|
||||||
|
print(f"=== 프로젝트 현황: {proj_name} (#{pid}) ===")
|
||||||
|
|
||||||
|
# 2) 태스크 현황
|
||||||
|
try:
|
||||||
|
tasks = api_get(f"/projects/{pid}/tasks?per_page=200")
|
||||||
|
todo_tasks = [t for t in tasks if not t["done"]]
|
||||||
|
done_tasks = [t for t in tasks if t["done"]]
|
||||||
|
total = len(tasks)
|
||||||
|
rate = f"{len(done_tasks)/total*100:.0f}%" if total else "N/A"
|
||||||
|
|
||||||
|
print(f"\n[태스크]")
|
||||||
|
print(f" TODO: {len(todo_tasks)}건 | DONE: {len(done_tasks)}건 | 완료율: {rate}")
|
||||||
|
|
||||||
|
if todo_tasks:
|
||||||
|
print(f" 미완료:")
|
||||||
|
for t in todo_tasks:
|
||||||
|
labels = ", ".join(l["title"] for l in (t.get("labels") or []))
|
||||||
|
label_str = f" [{labels}]" if labels else ""
|
||||||
|
desc = (t.get("description") or "")[:40].replace("\n", " ")
|
||||||
|
desc_str = f" {desc}" if desc else ""
|
||||||
|
print(f" ⬜ #{t['id']} {t['title'][:50]}{label_str}{desc_str}")
|
||||||
|
|
||||||
|
if done_tasks:
|
||||||
|
# 최근 완료 5건만 표시
|
||||||
|
recent_done = sorted(done_tasks, key=lambda t: t.get("done_at", ""), reverse=True)[:5]
|
||||||
|
print(f" 최근 완료 (최대 5건):")
|
||||||
|
for t in recent_done:
|
||||||
|
print(f" ✅ #{t['id']} {t['title'][:50]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 태스크 조회 실패: {e}")
|
||||||
|
|
||||||
|
# 3) Git log (현재 디렉토리 기준)
|
||||||
|
print(f"\n[최근 커밋 5건]")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "log", "--oneline", "-5"],
|
||||||
|
capture_output=True, timeout=10,
|
||||||
|
encoding="utf-8", errors="replace",
|
||||||
|
)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
for line in result.stdout.strip().splitlines():
|
||||||
|
print(f" {line}")
|
||||||
|
else:
|
||||||
|
print(" (git log 없음)")
|
||||||
|
except Exception:
|
||||||
|
print(" (git 실행 불가)")
|
||||||
|
|
||||||
|
# 4) Devlog (오늘/어제)
|
||||||
|
print(f"\n[Devlog]")
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
devlog_found = False
|
||||||
|
for date_str in [today, yesterday]:
|
||||||
|
devlog_path = Path("docs/devlog") / f"{date_str}.md"
|
||||||
|
if devlog_path.exists():
|
||||||
|
content = devlog_path.read_text(encoding="utf-8").strip()
|
||||||
|
# 최대 500자
|
||||||
|
if len(content) > 500:
|
||||||
|
content = content[:500] + "\n ...(생략)"
|
||||||
|
print(f" [{date_str}]")
|
||||||
|
for line in content.splitlines():
|
||||||
|
print(f" {line}")
|
||||||
|
devlog_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not devlog_found:
|
||||||
|
print(" (최근 devlog 없음)")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print(__doc__)
|
print(__doc__)
|
||||||
@@ -208,6 +314,11 @@ def main():
|
|||||||
print("Error: title is required")
|
print("Error: title is required")
|
||||||
return
|
return
|
||||||
create_task(title, desc, done=is_done, labels=labels)
|
create_task(title, desc, done=is_done, labels=labels)
|
||||||
|
elif cmd == "projects":
|
||||||
|
list_projects()
|
||||||
|
elif cmd == "report":
|
||||||
|
pid = int(sys.argv[2]) if len(sys.argv) > 2 else None
|
||||||
|
report(pid)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown command: {cmd}")
|
print(f"Unknown command: {cmd}")
|
||||||
print(__doc__)
|
print(__doc__)
|
||||||
|
|||||||
@@ -230,6 +230,15 @@ async def on_guild_channel_update(before, after):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_command_error(ctx, error):
|
||||||
|
"""존재하지 않는 ! 명령어 무시."""
|
||||||
|
if isinstance(error, commands.CommandNotFound):
|
||||||
|
return # 무시 — 로그 오염 방지
|
||||||
|
raise error
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_message(message: discord.Message):
|
async def on_message(message: discord.Message):
|
||||||
"""메시지 수신 — 워크스페이스 채널이면 자동 응답."""
|
"""메시지 수신 — 워크스페이스 채널이면 자동 응답."""
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class GeminiCaller:
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
if any(noise in line for noise in [
|
if any(noise in line for noise in [
|
||||||
"YOLO mode", "Loaded cached", "Welcome to Gemini",
|
"YOLO mode", "Loaded cached", "Welcome to Gemini",
|
||||||
"Type /help", "Gemini CLI",
|
"Type /help", "Gemini CLI", "MCP issues detected",
|
||||||
]):
|
]):
|
||||||
continue
|
continue
|
||||||
cleaned.append(line)
|
cleaned.append(line)
|
||||||
|
|||||||
5
docs/devlog/2026-03-16.md
Normal file
5
docs/devlog/2026-03-16.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-03-16 Devlog
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 1 | 23:35~11:14 | 봇 기동 버그 3건 수정 (apscheduler, cp949 인코딩, workspaces 경로) + Vikunja 프로젝트 관리 기능 추가 (projects, report 커맨드) + agent 프롬프트 보강 | `` | ✅ |
|
||||||
30
docs/devlog/entries/20260316-001.md
Normal file
30
docs/devlog/entries/20260316-001.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 봇 기동 버그 수정 + Vikunja 프로젝트 관리 기능
|
||||||
|
|
||||||
|
- **시간**: 2026-03-16 23:35~11:14
|
||||||
|
- **Commit**: ``
|
||||||
|
- **Vikunja**: #375 done, #376 done
|
||||||
|
|
||||||
|
## 작업 내용
|
||||||
|
|
||||||
|
### 봇 기동 버그 3건 수정
|
||||||
|
1. **apscheduler 미설치** — `ModuleNotFoundError` → `pip install apscheduler` (3.11.2)
|
||||||
|
2. **cp949 인코딩 크래시** — `main.py` StreamHandler를 UTF-8 TextIOWrapper로 교체 + 로그 메시지 em-dash→plain dash
|
||||||
|
3. **workspaces.json 경로 불일치** — `c:\Users\Certes\...` → `c:\Users\Variet-Worker\...` 수정 (WinError 267 해결)
|
||||||
|
|
||||||
|
### 부수 수정
|
||||||
|
- `gemini_caller.py` — "MCP issues detected" 노이즈 필터 추가 (`_clean_output`)
|
||||||
|
- `discord_bot.py` — `on_command_error` 핸들러 추가 (CommandNotFound 로그 오염 방지)
|
||||||
|
|
||||||
|
### Vikunja 프로젝트 관리 기능 추가
|
||||||
|
- `vikunja_helper.py`에 `projects` 커맨드 추가 — 전체 Vikunja 프로젝트 목록 + TODO/DONE 통계
|
||||||
|
- `vikunja_helper.py`에 `report` 커맨드 추가 — 프로젝트별 종합 현황 (태스크 + git log + devlog)
|
||||||
|
- `agent.md` 프롬프트 보강 — Vikunja 도구 섹션 확장 (12개 커맨드 + 라벨 가이드)
|
||||||
|
|
||||||
|
## 변경 파일
|
||||||
|
- `.agent/workflows/helpers/vikunja_helper.py` — projects, report 함수 + main dispatcher
|
||||||
|
- `api/discord_bot.py` — on_command_error 핸들러
|
||||||
|
- `core/gemini_caller.py` — MCP 노이즈 필터
|
||||||
|
- `main.py` — UTF-8 StreamHandler, em-dash 제거
|
||||||
|
- `prompts/agent.md` — Vikunja 도구 확장
|
||||||
|
- `workspaces.json` — 경로 수정
|
||||||
|
- `.agent/references/known-issues.md` — 이슈 2건 추가
|
||||||
9
main.py
9
main.py
@@ -5,6 +5,7 @@ FastAPI 서버 + Discord Bot + APScheduler를 동시 실행합니다.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
@@ -21,8 +22,10 @@ import config
|
|||||||
LOG_DIR = Path(__file__).parent / "logs"
|
LOG_DIR = Path(__file__).parent / "logs"
|
||||||
LOG_DIR.mkdir(exist_ok=True)
|
LOG_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
_stdout_utf8 = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
handlers = [
|
handlers = [
|
||||||
logging.StreamHandler(sys.stdout),
|
logging.StreamHandler(_stdout_utf8),
|
||||||
RotatingFileHandler(
|
RotatingFileHandler(
|
||||||
LOG_DIR / "variet.log",
|
LOG_DIR / "variet.log",
|
||||||
maxBytes=10 * 1024 * 1024, # 10MB
|
maxBytes=10 * 1024 * 1024, # 10MB
|
||||||
@@ -75,7 +78,7 @@ async def run_scheduler():
|
|||||||
try:
|
try:
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("APScheduler 미설치 — 스케줄러 비활성화 (pip install apscheduler)")
|
logger.warning("APScheduler 미설치 - 스케줄러 비활성화 (pip install apscheduler)")
|
||||||
return
|
return
|
||||||
|
|
||||||
scheduler = AsyncIOScheduler()
|
scheduler = AsyncIOScheduler()
|
||||||
@@ -91,7 +94,7 @@ async def run_scheduler():
|
|||||||
# scheduler.add_job(check_schedule, "cron", hour=18, id="anime_schedule")
|
# scheduler.add_job(check_schedule, "cron", hour=18, id="anime_schedule")
|
||||||
|
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
logger.info(f"스케줄러 시작 — {len(scheduler.get_jobs())}개 작업 등록")
|
logger.info(f"스케줄러 시작 - {len(scheduler.get_jobs())}개 작업 등록")
|
||||||
|
|
||||||
# 스케줄러가 종료되지 않도록 대기
|
# 스케줄러가 종료되지 않도록 대기
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -62,15 +62,30 @@ C:\ProgramData\miniforge3\envs\variet-agent\python.exe tools/wiki_client.py dash
|
|||||||
|
|
||||||
> **위키 등록 필수**: 조사/리서치 결과는 **반드시** `wiki_client.py create`로 Wiki.js에 등록하세요. 로컬 파일에 쓰지 마세요.
|
> **위키 등록 필수**: 조사/리서치 결과는 **반드시** `wiki_client.py create`로 Wiki.js에 등록하세요. 로컬 파일에 쓰지 마세요.
|
||||||
|
|
||||||
### 🔧 인프라 (Git/태스크)
|
### 📋 프로젝트 관리 (Vikunja)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Vikunja 태스크
|
# 전체 프로젝트 목록 (어떤 프로젝트가 있는지, 각 프로젝트의 TODO/DONE 수)
|
||||||
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py projects
|
||||||
|
|
||||||
|
# 프로젝트 종합 현황 (태스크 + 최근 커밋 + devlog)
|
||||||
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py report
|
||||||
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py report <project_id>
|
||||||
|
|
||||||
|
# 태스크 목록
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py list
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py list
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py create "제목" "설명"
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py list todo
|
||||||
|
|
||||||
|
# 태스크 생성/완료/코멘트
|
||||||
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py done <ID>
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py done <ID>
|
||||||
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py comment <ID> "메모"
|
||||||
|
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent/workflows/helpers/vikunja_helper.py label <ID> Backend Priority:High
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **라벨 목록**: Backend, Frontend, Engine, Infra, Test, Priority:High, Priority:Mid, Priority:Low, Agent, Tool, AI/LLM
|
||||||
|
> 태스크 생성 시 영역 라벨(1개+)과 우선순위 라벨(1개)을 반드시 지정하세요.
|
||||||
|
|
||||||
## ⚡ 복수 작품 처리 — 반드시 전부 완료할 것
|
## ⚡ 복수 작품 처리 — 반드시 전부 완료할 것
|
||||||
|
|
||||||
사용자가 "이번 분기 애니 다운받아줘" 등 **복수 작업**을 요청하면:
|
사용자가 "이번 분기 애니 다운받아줘" 등 **복수 작업**을 요청하면:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"1480113683849023661": {
|
"1480113683849023661": {
|
||||||
"name": "variet-agent",
|
"name": "variet-agent",
|
||||||
"path": "c:\\Users\\Certes\\Desktop\\variet-agent",
|
"path": "c:\\Users\\Variet-Worker\\Desktop\\variet-agent",
|
||||||
"channel_id": 1480113683849023661,
|
"channel_id": 1480113683849023661,
|
||||||
"git": {
|
"git": {
|
||||||
"url": "",
|
"url": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user