按获得时间计算 可以调接口查,脚本参考一下
#!/usr/bin/env python3
"""
Fetch Codex/WHAM usage-related ChatGPT backend endpoints with the local Codex auth.
The script reads ~/.codex/auth.json by default, extracts the bearer token, then calls:
- https://chatgpt.com/backend-api/wham/usage
- https://chatgpt.com/backend-api/wham/rate-limit-reset-credits
- https://chatgpt.com/backend-api/wham/referrals/eligibility_rules?referral_key=codex_referral_persistent_invite
It never prints the token. By default it prints a Chinese human-readable summary.
Use --json to print raw data.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
DEFAULT_ENDPOINTS = [
"https://chatgpt.com/backend-api/wham/usage",
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
"https://chatgpt.com/backend-api/wham/referrals/eligibility_rules?referral_key=codex_referral_persistent_invite",
]
ENDPOINT_TITLES = {
"https://chatgpt.com/backend-api/wham/usage": "Codex 用量",
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits": "重置次数 / 额度",
"https://chatgpt.com/backend-api/wham/referrals/eligibility_rules?referral_key=codex_referral_persistent_invite": "邀请资格规则",
}
FIELD_LABELS = {
"account_id": "账户 ID",
"available": "可用",
"available_credits": "可用额度",
"balance": "余额",
"code": "错误代码",
"count": "数量",
"created_at": "创建时间",
"credits": "额度",
"daily_limit": "每日限制",
"eligible": "是否符合资格",
"eligibility": "资格",
"enabled": "是否启用",
"error": "错误",
"expires_at": "过期时间",
"granted": "已发放",
"hard_limit": "硬限制",
"is_eligible": "是否符合资格",
"limit": "限制",
"limit_remaining": "剩余额度",
"message": "消息",
"model": "模型",
"next_reset_at": "下次重置时间",
"period": "周期",
"plan": "套餐",
"quota": "配额",
"rate_limit": "速率限制",
"remaining": "剩余",
"remaining_credits": "剩余次数",
"reset": "重置",
"reset_at": "重置时间",
"reset_credits": "重置次数",
"reset_time": "重置时间",
"soft_limit": "软限制",
"status": "状态",
"total": "总计",
"total_credits": "总次数",
"type": "类型",
"updated_at": "更新时间",
"usage": "用量",
"used": "已用",
"used_credits": "已用次数",
}
@dataclass(frozen=True)
class CodexAuth:
token: str
token_source: str
account_id: str | None = None
def load_json(path: Path) -> dict[str, Any]:
try:
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
raise SystemExit(f"auth file not found: {path}") from None
except json.JSONDecodeError as exc:
raise SystemExit(f"auth file is not valid JSON: {path}: {exc}") from None
if not isinstance(data, dict):
raise SystemExit(f"auth file root must be a JSON object: {path}")
return data
def first_string(data: dict[str, Any], paths: list[tuple[str, ...]]) -> tuple[str, str] | tuple[None, None]:
for path in paths:
current: Any = data
for key in path:
if not isinstance(current, dict) or key not in current:
current = None
break
current = current[key]
if isinstance(current, str) and current.strip():
return current.strip(), ".".join(path)
return None, None
def extract_auth(data: dict[str, Any], token_kind: str) -> CodexAuth:
access_paths = [
("tokens", "access_token"),
("tokens", "accessToken"),
("access_token",),
("accessToken",),
]
id_paths = [
("tokens", "id_token"),
("tokens", "idToken"),
("id_token",),
("idToken",),
]
if token_kind == "access":
token, source = first_string(data, access_paths)
elif token_kind == "id":
token, source = first_string(data, id_paths)
else:
token, source = first_string(data, access_paths + id_paths)
if not token or not source:
raise SystemExit(
"could not find a bearer token in auth.json. "
"Tried tokens.access_token/accessToken and tokens.id_token/idToken. "
"Run `codex login` again, or pass a different file with --auth."
)
account_id, _ = first_string(
data,
[
("tokens", "account_id"),
("tokens", "accountId"),
("account_id",),
("accountId",),
],
)
return CodexAuth(token=token, token_source=source, account_id=account_id)
def decode_body(raw: bytes, content_type: str | None) -> Any:
text = raw.decode("utf-8", errors="replace")
if content_type and "json" in content_type.lower():
try:
return json.loads(text)
except json.JSONDecodeError:
return text
stripped = text.strip()
if stripped.startswith("{") or stripped.startswith("["):
try:
return json.loads(stripped)
except json.JSONDecodeError:
pass
return text
def request_json(url: str, auth: CodexAuth, timeout: float) -> dict[str, Any]:
headers = {
"Authorization": f"Bearer {auth.token}",
"Accept": "application/json",
"User-Agent": "codex-wham-usage-fetcher/1.0",
"Origin": "https://chatgpt.com",
"Referer": "https://chatgpt.com/codex",
}
if auth.account_id:
headers["ChatGPT-Account-ID"] = auth.account_id
req = urllib.request.Request(url=url, headers=headers, method="GET")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
return {
"ok": 200 <= resp.status < 300,
"status": resp.status,
"url": url,
"body": decode_body(raw, resp.headers.get("Content-Type")),
}
except urllib.error.HTTPError as exc:
raw = exc.read()
return {
"ok": False,
"status": exc.code,
"url": url,
"reason": exc.reason,
"body": decode_body(raw, exc.headers.get("Content-Type")),
}
except urllib.error.URLError as exc:
return {
"ok": False,
"status": None,
"url": url,
"reason": str(exc.reason),
"body": None,
}
def label_for_key(key: str) -> str:
if key in FIELD_LABELS:
return FIELD_LABELS[key]
return key.replace("_", " ")
def format_scalar(value: Any) -> str:
if value is True:
return "是"
if value is False:
return "否"
if value is None:
return "无"
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, str):
return value if value else "空字符串"
return str(value)
def format_body(body: Any, indent: int = 0) -> list[str]:
prefix = " " * indent
lines: list[str] = []
if isinstance(body, dict):
if not body:
return [f"{prefix}- 无返回字段"]
for key, value in body.items():
label = label_for_key(str(key))
if isinstance(value, dict):
lines.append(f"{prefix}- {label}:")
lines.extend(format_body(value, indent + 1))
elif isinstance(value, list):
lines.append(f"{prefix}- {label}:")
lines.extend(format_body(value, indent + 1))
else:
lines.append(f"{prefix}- {label}: {format_scalar(value)}")
return lines
if isinstance(body, list):
if not body:
return [f"{prefix}- 空列表"]
for index, item in enumerate(body, start=1):
if isinstance(item, (dict, list)):
lines.append(f"{prefix}- 第 {index} 项:")
lines.extend(format_body(item, indent + 1))
else:
lines.append(f"{prefix}- 第 {index} 项: {format_scalar(item)}")
return lines
return [f"{prefix}- 内容: {format_scalar(body)}"]
def format_text_report(results: dict[str, Any]) -> str:
lines = [
"Codex 用量与额度查询结果",
"",
f"认证文件: {results['auth_path']}",
f"Token 来源: {results['token_source']}",
f"是否包含账户 ID: {'是' if results['account_id_present'] else '否'}",
"",
]
for index, item in enumerate(results["results"], start=1):
title = ENDPOINT_TITLES.get(item["url"], "自定义接口")
ok_text = "成功" if item["ok"] else "失败"
status = item["status"] if item["status"] is not None else "无状态码"
lines.extend(
[
f"{index}. {title}",
f"接口: {item['url']}",
f"请求结果: {ok_text}",
f"HTTP 状态: {status}",
]
)
if item.get("reason"):
lines.append(f"失败原因: {item['reason']}")
lines.append("返回信息:")
lines.extend(format_body(item.get("body"), indent=1))
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Fetch Codex WHAM usage/rate-limit/referral endpoint data using ~/.codex/auth.json."
)
parser.add_argument(
"--auth",
type=Path,
default=Path.home() / ".codex" / "auth.json",
help="Path to Codex auth.json. Default: ~/.codex/auth.json",
)
parser.add_argument(
"--token-kind",
choices=["auto", "access", "id"],
default="auto",
help="Which bearer token to use from auth.json. Default: auto",
)
parser.add_argument(
"--timeout",
type=float,
default=30.0,
help="HTTP timeout in seconds. Default: 30",
)
parser.add_argument(
"--endpoint",
action="append",
dest="endpoints",
help="Additional endpoint to fetch. Can be provided multiple times.",
)
parser.add_argument(
"--only-extra-endpoints",
action="store_true",
help="Fetch only endpoints passed with --endpoint, skipping the built-in list.",
)
parser.add_argument(
"--json",
action="store_true",
help="Print raw JSON instead of the default Chinese text report.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
auth_path = args.auth.expanduser()
auth_data = load_json(auth_path)
auth = extract_auth(auth_data, args.token_kind)
endpoints: list[str] = []
if not args.only_extra_endpoints:
endpoints.extend(DEFAULT_ENDPOINTS)
if args.endpoints:
endpoints.extend(args.endpoints)
if not endpoints:
raise SystemExit("no endpoints to fetch")
results = {
"auth_path": os.fspath(auth_path),
"token_source": auth.token_source,
"account_id_present": bool(auth.account_id),
"results": [request_json(url, auth, args.timeout) for url in endpoints],
}
if args.json:
json.dump(results, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
else:
sys.stdout.write(format_text_report(results))
return 0
if __name__ == "__main__":
raise SystemExit(main())