- wiki_client.py: find_page()를 singleByPath 직접 조회로 교체 (O(1)) - debate-agent/: gemini/opus AG 프로젝트 생성 (GEMINI.md + wiki_debate.py + /start) - debate_handler.py: 로컬 파일 I/O → Wiki.js API 전환, _sync_wiki() 삭제 - response 페이지 매 턴 초기화 (누적 방지)
173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
"""Wiki.js Debate Tool — AG용 Wiki.js 읽기/쓰기 CLI.
|
|
|
|
사용법:
|
|
python tools/wiki_debate.py read <page-path>
|
|
python tools/wiki_debate.py write <page-path> <content>
|
|
python tools/wiki_debate.py write-file <page-path> <file-path>
|
|
|
|
예시:
|
|
python tools/wiki_debate.py read debates/frtb/input-gemini
|
|
python tools/wiki_debate.py write debates/frtb/response-gemini "SA 방식을 제안합니다..."
|
|
python tools/wiki_debate.py write-file debates/frtb/response-gemini response_draft.md
|
|
"""
|
|
|
|
import asyncio
|
|
import httpx
|
|
import io
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# .env 로드 (여러 후보 경로에서 탐색)
|
|
_candidates = [
|
|
Path(__file__).parent.parent / ".env", # tools/../.env (배포 위치)
|
|
Path.cwd() / ".env", # CWD/.env
|
|
Path.cwd().parent / ".env", # CWD/../.env
|
|
]
|
|
for _env_path in _candidates:
|
|
if _env_path.exists():
|
|
for line in _env_path.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if "=" in line:
|
|
key, _, value = line.partition("=")
|
|
value = value.strip()
|
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
|
value = value[1:-1]
|
|
os.environ.setdefault(key.strip(), value)
|
|
break # 첫 번째 .env만 사용
|
|
|
|
WIKI_URL = os.getenv("WIKI_URL", "https://wiki.variet.net")
|
|
WIKI_API_KEY = os.getenv("WIKI_API_KEY", "")
|
|
GRAPHQL_ENDPOINT = f"{WIKI_URL}/graphql"
|
|
|
|
|
|
async def _query(query: str, variables: dict = None) -> dict:
|
|
"""GraphQL 쿼리 실행."""
|
|
headers = {
|
|
"Authorization": f"Bearer {WIKI_API_KEY}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
payload = {"query": query}
|
|
if variables:
|
|
payload["variables"] = variables
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
resp = await client.post(GRAPHQL_ENDPOINT, json=payload, headers=headers)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
if "errors" in data and data["errors"]:
|
|
raise RuntimeError(f"Wiki.js 오류: {data['errors'][0].get('message', data['errors'])}")
|
|
return data.get("data", {})
|
|
|
|
|
|
async def read_page(path: str) -> str:
|
|
"""경로로 페이지 읽기 (singleByPath)."""
|
|
query = """
|
|
query ($path: String!) {
|
|
pages { singleByPath(path: $path, locale: "ko") {
|
|
id, path, title, content
|
|
}}
|
|
}
|
|
"""
|
|
data = await _query(query, {"path": path})
|
|
page = data.get("pages", {}).get("singleByPath")
|
|
if not page:
|
|
return f"[오류] 페이지를 찾을 수 없습니다: {path}"
|
|
return page.get("content", "")
|
|
|
|
|
|
async def write_page(path: str, content: str) -> str:
|
|
"""경로에 페이지 쓰기 (upsert)."""
|
|
# 기존 페이지 확인
|
|
find_query = """
|
|
query ($path: String!) {
|
|
pages { singleByPath(path: $path, locale: "ko") {
|
|
id, tags { tag }
|
|
}}
|
|
}
|
|
"""
|
|
data = await _query(find_query, {"path": path})
|
|
existing = data.get("pages", {}).get("singleByPath")
|
|
|
|
if existing:
|
|
# 수정
|
|
tags = [t["tag"] for t in existing.get("tags", [])]
|
|
update_query = """
|
|
mutation ($id: Int!, $content: String!, $tags: [String!]) {
|
|
pages { update(id: $id, content: $content, tags: $tags) {
|
|
responseResult { succeeded, message }
|
|
}}
|
|
}
|
|
"""
|
|
result = await _query(update_query, {
|
|
"id": existing["id"], "content": content, "tags": tags,
|
|
})
|
|
ok = result["pages"]["update"]["responseResult"]["succeeded"]
|
|
msg = result["pages"]["update"]["responseResult"]["message"]
|
|
return f"✅ 수정 완료 (id: {existing['id']})" if ok else f"❌ 수정 실패: {msg}"
|
|
else:
|
|
# 생성
|
|
title = path.split("/")[-1].replace("-", " ").title()
|
|
create_query = """
|
|
mutation ($content: String!, $path: String!, $title: String!,
|
|
$description: String!, $tags: [String!]!) {
|
|
pages { create(
|
|
content: $content, description: $description,
|
|
editor: "markdown", isPublished: true, isPrivate: false,
|
|
locale: "ko", path: $path, tags: $tags, title: $title
|
|
) {
|
|
responseResult { succeeded, message }
|
|
page { id }
|
|
}}
|
|
}
|
|
"""
|
|
result = await _query(create_query, {
|
|
"content": content, "path": path, "title": title,
|
|
"description": title, "tags": ["debate"],
|
|
})
|
|
ok = result["pages"]["create"]["responseResult"]["succeeded"]
|
|
if ok:
|
|
pid = result["pages"]["create"]["page"]["id"]
|
|
return f"✅ 생성 완료 (id: {pid})"
|
|
return f"❌ 생성 실패: {result['pages']['create']['responseResult']['message']}"
|
|
|
|
|
|
async def main():
|
|
if sys.stdout.encoding != "utf-8":
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
|
|
args = sys.argv[1:]
|
|
if len(args) < 2:
|
|
print(__doc__)
|
|
return
|
|
|
|
cmd, path = args[0], args[1]
|
|
|
|
if cmd == "read":
|
|
content = await read_page(path)
|
|
print(content)
|
|
|
|
elif cmd == "write" and len(args) > 2:
|
|
content = " ".join(args[2:])
|
|
result = await write_page(path, content)
|
|
print(result)
|
|
|
|
elif cmd == "write-file" and len(args) > 2:
|
|
file_path = Path(args[2])
|
|
if not file_path.exists():
|
|
print(f"❌ 파일을 찾을 수 없습니다: {file_path}")
|
|
return
|
|
content = file_path.read_text(encoding="utf-8")
|
|
result = await write_page(path, content)
|
|
print(result)
|
|
|
|
else:
|
|
print(__doc__)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|