Initial commit: OpenClaw 워크스페이스 버전관리 시작

설정·스크립트·스킬·문서·큐레이션 메모리 추적.
시크릿(credentials/identity)·런타임 상태(state/logs/sessions/sqlite)·
백업(clobbered/bak)·dream 캐시는 .gitignore로 제외.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-04 15:39:41 +09:00
commit fed3526b20
199 changed files with 49671 additions and 0 deletions
@@ -0,0 +1,115 @@
"""Telegram notifier for 골디(budget) — 후잉 동기화 실패 알림 전송.
openclaw.json 의 channels.telegram.accounts.budget.botToken 과 allowFrom[0] (관리자 chat_id)
를 사용해 직접 sendMessage 호출. 외부 의존성 없음.
"""
import json
import re
import urllib.parse
import urllib.request
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
CONFIG_PATH = Path("/Users/snowoyh/.openclaw/openclaw.json")
KST = ZoneInfo("Asia/Seoul")
def _load_budget_account():
cfg = json.loads(CONFIG_PATH.read_text())
acct = cfg["channels"]["telegram"]["accounts"]["budget"]
token = acct["botToken"]
chat_ids = acct.get("allowFrom") or []
return token, chat_ids
def send(text: str, parse_mode: str = "HTML") -> bool:
"""관리자에게 텔레그램 메시지 전송. 성공 시 True. 실패해도 예외 안 던짐 (알림 자체로 흐름 막지 않음)."""
try:
token, chat_ids = _load_budget_account()
except Exception as e:
print(f"⚠️ notify: config 로드 실패: {e}")
return False
if not chat_ids:
print("⚠️ notify: allowFrom 비어 있어 전송 대상 없음")
return False
ok = True
url = f"https://api.telegram.org/bot{token}/sendMessage"
for chat_id in chat_ids:
payload = {
"chat_id": chat_id,
"text": text[:4000],
"parse_mode": parse_mode,
"disable_web_page_preview": "true",
}
data = urllib.parse.urlencode(payload).encode()
req = urllib.request.Request(url, data=data, method="POST")
try:
with urllib.request.urlopen(req, timeout=10) as r:
body = r.read().decode("utf-8", errors="replace")
if r.status != 200:
print(f"⚠️ notify: telegram {r.status}: {body[:200]}")
ok = False
except Exception as e:
print(f"⚠️ notify: telegram 전송 실패: {e}")
ok = False
return ok
def escape_html(s: str) -> str:
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
def format_kst(iso: str) -> str:
"""imsg created_at(UTC ISO) -> 'MM-DD HH:MM' KST. 파싱 실패 시 원문."""
if not iso:
return ""
try:
dt = datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(KST)
return dt.strftime("%m-%d %H:%M")
except Exception:
return iso[:16]
def clean_reason(body: str, status: int) -> str:
"""후잉 응답 본문 -> 한국어 한 줄 사유. 모르는 패턴은 원문 그대로."""
body = (body or "").strip()
if status == 0:
return f"네트워크 오류 — {body[:200]}"
if not (200 <= status < 300):
return f"HTTP {status}{body[:200]}"
if body.lower() == "fail":
return "후잉이 형식 거절함 (사유 미제공). 보통 memo 누락 또는 인코딩 문제."
# "Error :" / "Error:" 접두어 제거
m = re.match(r"^Error\s*:?\s*(.+)$", body, flags=re.S)
inner = (m.group(1).strip() if m else body).strip()
# 알려진 패턴 → 한국어 번역
# 1) "left/right account does not exist at entry_date. ... [계정명]"
m = re.search(r"(left|right)\s+account\s+does\s+not\s+exist.*?\[(.+?)\]", inner, flags=re.I | re.S)
if m:
side = "차변" if m.group(1).lower() == "left" else "대변"
name = m.group(2).strip()
return f"{side} 계정 {name}’ 이(가) 후잉 차트에 없습니다. 계정명 오타이거나 거래일에 닫혀있는 계정입니다."
# 2) "[필드명] 필드가 없습니다"
m = re.search(r"\[(\w+)\]\s*필드가\s*없습니다", inner)
if m:
return f"필수 필드 {m.group(1)} 이(가) 빠졌습니다."
# 3) entry_date 관련
if re.search(r"entry_date.*(format|invalid|올바)", inner, flags=re.I):
return "거래일(entry_date) 형식 오류. YYYYMMDD 8자리여야 합니다."
# 4) money 관련
if re.search(r"money.*(invalid|format|숫자|negative)", inner, flags=re.I):
return "금액(money) 값이 잘못됐습니다. 양의 정수만 허용됩니다."
# 5) section / webhook URL 만료
if re.search(r"section|webhook|invalid\s*url|expired", inner, flags=re.I):
return f"웹훅/섹션 문제 — {inner[:200]}"
# 알려진 패턴 없음: 원문 (영문 그대로)
return inner[:300]