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:
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""후잉 OpenAPI 잔액(bs.json) 조회.
|
||||
|
||||
Usage:
|
||||
whooing_balance.py # 모든 섹션의 현재 잔액
|
||||
whooing_balance.py --section-id 1 # 특정 섹션
|
||||
whooing_balance.py --as-of 2026-04-23 # 특정 날짜 기준
|
||||
whooing_balance.py --json # 원시 JSON 출력
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import secrets
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
CRED_PATH = Path("/Users/snowoyh/.openclaw/credentials/whooing.json")
|
||||
BASE = "https://whooing.com/api"
|
||||
|
||||
|
||||
def build_api_key(app_id, token, signature) -> str:
|
||||
nounce = secrets.token_hex(20)
|
||||
ts = int(time.time())
|
||||
return f"app_id={app_id},token={token},signiture={signature},nounce={nounce},timestamp={ts}"
|
||||
|
||||
|
||||
def api_get(endpoint: str, api_key: str, params: dict | None = None) -> dict:
|
||||
url = f"{BASE}/{endpoint}"
|
||||
if params:
|
||||
url += "?" + urllib.parse.urlencode(params)
|
||||
req = urllib.request.Request(url, headers={"X-API-KEY": api_key})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
data = json.loads(body)
|
||||
if data.get("code") != 200:
|
||||
raise RuntimeError(f"후잉 API error {data.get('code')}: {data.get('message')} (endpoint={endpoint})")
|
||||
return data["results"]
|
||||
|
||||
|
||||
def fmt_won(n: int) -> str:
|
||||
return f"{n:,}원"
|
||||
|
||||
|
||||
def fetch_asset_balances(account_names: list[str]) -> dict[str, int]:
|
||||
"""주어진 자산 계정명들의 후잉 잔액을 dict로 반환 (호환용 별칭)."""
|
||||
return fetch_balances(account_names, sides=("assets",))
|
||||
|
||||
|
||||
def fetch_balances(account_names: list[str], sides: tuple = ("assets", "liabilities")) -> dict[str, int]:
|
||||
"""주어진 계정명들의 후잉 잔액을 dict로 반환.
|
||||
|
||||
sides 에 'assets' / 'liabilities' / 'capital' 중 원하는 것만 포함.
|
||||
name -> money. 미발견 계정은 dict에 포함하지 않음.
|
||||
"""
|
||||
cred = json.loads(CRED_PATH.read_text())
|
||||
api_cfg = cred["api"]
|
||||
|
||||
def key() -> str:
|
||||
return build_api_key(api_cfg["app_id"], api_cfg["token"], api_cfg["signature"])
|
||||
|
||||
end_date = time.strftime("%Y%m%d")
|
||||
start_date = "19000101"
|
||||
|
||||
sections = api_get("sections.json", key())
|
||||
if isinstance(sections, dict):
|
||||
section_list = sections.get("sections") or sections.get("rows") or list(sections.values())
|
||||
else:
|
||||
section_list = sections
|
||||
|
||||
wanted = set(account_names)
|
||||
out: dict[str, int] = {}
|
||||
for sec in section_list:
|
||||
sid = sec.get("section_id") or sec.get("id")
|
||||
accounts_raw = api_get("accounts.json", key(), {"section_id": sid})
|
||||
id_to_name: dict[str, str] = {}
|
||||
for acc_list in accounts_raw.values():
|
||||
if not isinstance(acc_list, list):
|
||||
continue
|
||||
for a in acc_list:
|
||||
id_to_name[str(a.get("account_id"))] = a.get("title") or str(a.get("account_id"))
|
||||
bs = api_get("bs.json", key(), {
|
||||
"section_id": sid, "start_date": start_date, "end_date": end_date,
|
||||
})
|
||||
for side in sides:
|
||||
for row in (bs.get(side) or {}).get("accounts", []) or []:
|
||||
name = id_to_name.get(str(row.get("account_id")), "")
|
||||
if name in wanted:
|
||||
out[name] = row.get("money", 0)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--section-id", help="섹션 id. 미지정 시 모든 섹션 조회.")
|
||||
ap.add_argument("--as-of", help="기준일 YYYY-MM-DD. 기본값: 오늘.")
|
||||
ap.add_argument("--json", action="store_true", help="원시 JSON 출력")
|
||||
args = ap.parse_args()
|
||||
|
||||
cred = json.loads(CRED_PATH.read_text())
|
||||
api_cfg = cred.get("api")
|
||||
if not api_cfg:
|
||||
print("error: credentials/whooing.json 에 'api' 블록이 없습니다.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
def key() -> str:
|
||||
return build_api_key(api_cfg["app_id"], api_cfg["token"], api_cfg["signature"])
|
||||
|
||||
end_date = (args.as_of or time.strftime("%Y-%m-%d")).replace("-", "")
|
||||
start_date = "19000101"
|
||||
|
||||
sections = api_get("sections.json", key())
|
||||
if isinstance(sections, dict):
|
||||
section_list = sections.get("sections") or sections.get("rows") or list(sections.values())
|
||||
else:
|
||||
section_list = sections
|
||||
|
||||
if args.section_id:
|
||||
section_list = [s for s in section_list if str(s.get("section_id")) == str(args.section_id)]
|
||||
|
||||
output: dict = {"as_of": end_date, "sections": []}
|
||||
|
||||
for sec in section_list:
|
||||
sid = sec.get("section_id") or sec.get("id")
|
||||
title = sec.get("title") or sec.get("name") or str(sid)
|
||||
|
||||
accounts_raw = api_get("accounts.json", key(), {"section_id": sid})
|
||||
id_to_name: dict[str, str] = {}
|
||||
for acc_type, acc_list in accounts_raw.items():
|
||||
if not isinstance(acc_list, list):
|
||||
continue
|
||||
for a in acc_list:
|
||||
id_to_name[str(a.get("account_id"))] = a.get("title") or str(a.get("account_id"))
|
||||
|
||||
bs = api_get("bs.json", key(), {
|
||||
"section_id": sid,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
})
|
||||
|
||||
sec_out = {"section_id": sid, "title": title, "groups": {}}
|
||||
for key_name, ko in [("assets", "자산"), ("liabilities", "부채"), ("capital", "자본")]:
|
||||
group = bs.get(key_name) or {}
|
||||
total = group.get("total", 0)
|
||||
items = []
|
||||
for row in group.get("accounts", []) or []:
|
||||
money = row.get("money", 0)
|
||||
if money == 0:
|
||||
continue
|
||||
items.append({
|
||||
"account_id": row.get("account_id"),
|
||||
"name": id_to_name.get(str(row.get("account_id")), str(row.get("account_id"))),
|
||||
"money": money,
|
||||
})
|
||||
items.sort(key=lambda x: abs(x["money"]), reverse=True)
|
||||
sec_out["groups"][ko] = {"total": total, "items": items}
|
||||
output["sections"].append(sec_out)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(output, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
print(f"## 후잉 잔액 (기준일 {end_date[:4]}-{end_date[4:6]}-{end_date[6:]})\n")
|
||||
for sec_out in output["sections"]:
|
||||
print(f"### [{sec_out['section_id']}] {sec_out['title']}")
|
||||
for ko in ("자산", "부채", "자본"):
|
||||
g = sec_out["groups"][ko]
|
||||
print(f"- **{ko} 합계:** {fmt_won(g['total'])}")
|
||||
for it in g["items"]:
|
||||
print(f" - {it['name']}: {fmt_won(it['money'])}")
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user