Skill: alex-shen1121/gmncode-usage (version 0.1.3)

--- name: gmncode-usage description: 通过 HTTP 接口查询 GMNCODE / gmncode.cn 中转站的大模型用量，包括 dashboard 汇总、每日趋势、按模型拆分的 token 与费用数据。当用户要求查看 GMNCODE token 用量、每日模型消耗、API/中转站花费、dashboard 用量，或希望用脚本/HTTP 接口直接获取 GMNCODE 使用数据而不是手动打开网页时使用。 --- # GMNCODE Usage 使用本技能自带脚本，通过可用的 dashboard HTTP 接口查询 GMNCODE 用量数据。 ## 快速开始 1. 先确保凭据已经通过环境变量或 `~/.openclaw/.env` 提供。 2. 优先运行 `scripts/gmncode_usage.py`，不要每次都重新手写登录、token 缓存和重试逻辑。 3. 常用命令： - `python3 scripts/gmncode_usage.py brief` 查询你最常要的那组指标：**账户每日额度 + 今日模型用量** - `python3 scripts/gmncode_usage.py quota` 只看账户每日额度 - `python3 scripts/gmncode_usage.py report --date today` - `python3 scripts/gmncode_usage.py report --date 2026-03-25` - `python3 scripts/gmncode_usage.py report --start 2026-03-01 --end 2026-03-26` - `python3 scripts/gmncode_usage.py models --date 2026-03-25 --json` - `python3 scripts/gmncode_usage.py trend --start 2026-03-01 --end 2026-03-26 --json` ## 工作流 ### 1. 安全加载凭据 不要把邮箱和密码硬编码进临时脚本、回复内容或日志里。 优先使用： - `GMNCODE_EMAIL` - `GMNCODE_PASSWORD` `GMNCODE_BASE_URL` 已固定写死为 `https://gmncode.cn`，因为这不是敏感信息，不需要放进环境变量。 如果缺少凭据，就停止执行，并提示用户补充到 `~/.openclaw/.env`。 ### 2. 复用内置客户端 脚本已经处理好了： - 通过 `/api/v1/auth/login` 登录 - access token 本地缓存 - 遇到 `401` / `INVALID_TOKEN` 时自动重新登录并重试一次 - dashboard 所需的 referer / headers 除非 HTTP 接口失效，否则不要退回浏览器自动化方案。 ### 3. 使用正确接口 使用以下接口： - `/api/v1/subscriptions?status=active` - `/api/v1/usage/dashboard/stats` - `/api/v1/usage/dashboard/trend` - `/api/v1/usage/dashboard/models` 请求参数统一传： - `start_date=YYYY-MM-DD` - `end_date=YYYY-MM-DD` - `timezone=Asia/Shanghai` 不要用 `/api/v1/admin/dashboard/*`，普通用户 token 会返回 `403 FORBIDDEN`。 ### 4. 按指标口径取值 如果用户要的是这组固定指标，按下面口径取： #### A. 账户每日使用额度 从 `/api/v1/subscriptions?status=active` 读取所有活跃订阅： - **每日使用额度** = `sum(group.daily_limit_usd)` - **今日已用** = `sum(daily_usage_usd)` - **今日剩余** = 每日使用额度 - 今日已用 #### B. 今日模型用量 从 `/api/v1/usage/dashboard/models` 读取指定日期： - 模型名：`model` - token 用量：`total_tokens` - 实际金额：`actual_cost` 默认把 token 格式化成 `x.x M` / `x.xx B` 这种紧凑写法。 ## 输出建议 如果用户只想看固定口径，优先给两块： 1. **账户额度**：每日使用额度 / 今日已用 / 今日剩余 2. **今日模型使用**：模型名 / token 用量 / 实际金额 如果用户要更完整的汇报，再补： - `stats` 的汇总数据 - `trend` 的每日趋势 - 当查询区间为单日时，补 `models` 的按模型拆分 默认使用紧凑表格或短列表，避免冗长描述。 ## 资源 ### scripts/ - `scripts/gmncode_usage.py`：安全的 HTTP 客户端与 CLI，负责登录、stats、trend、models 查询。 ### references/ - `references/api.md`：接口说明、安全约定、凭据存储方式与字段解释。

path: references/api.md
content: # GMNCODE Usage API 参考

## 凭据存储

把账号密码放在 `~/.openclaw/.env`，不要硬编码进脚本。

```bash
GMNCODE_EMAIL=[REDACTED_SECRET]
GMNCODE_PASSWORD=your_password
```

建议权限：

```bash
chmod 600 ~/.openclaw/.env
```

脚本读取顺序：
1. 当前进程环境变量
2. `~/.openclaw/.env`

`GMNCODE_BASE_URL` 在 skill 里固定为 `https://gmncode.cn`，不需要额外配置。

Token 缓存路径：
- `~/.cache/openclaw/gmncode-usage/token.json`
- 脚本会用仅当前用户可读写的权限写入缓存。
- 遇到 `401` / `INVALID_TOKEN` 时，脚本会删除缓存、重新登录并自动重试一次。

## 登录接口

`POST /api/v1/auth/login`

请求体：

```json
{
  "email": "...",
  "password": "..."
}
```

关键返回字段：
- `data.access_token`
- `data.refresh_token`
- `data.expires_in`

## 可用的接口

### 0. 活跃订阅 / 额度口径

`GET /api/v1/subscriptions?status=active`

常用字段：
- `daily_usage_usd`
- `weekly_usage_usd`
- `monthly_usage_usd`
- `group.daily_limit_usd`
- `group.weekly_limit_usd`
- `group.monthly_limit_usd`

推荐口径：
- 账户**每日使用额度** = 所有活跃订阅的 `group.daily_limit_usd` 求和
- 账户**今日已用** = 所有活跃订阅的 `daily_usage_usd` 求和
- 账户**今日剩余** = 每日使用额度 - 今日已用

### 1. 汇总统计

`GET /api/v1/usage/dashboard/stats?timezone=Asia/Shanghai`

常用字段：
- `today_requests`
- `today_tokens`
- `today_cost`
- `total_requests`
- `total_tokens`
- `total_cost`
- `total_actual_cost`

### 2. 每日趋势

`GET /api/v1/usage/dashboard/trend?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD&timezone=Asia/Shanghai`

返回 `data.trend[]`，字段包括：
- `date`
- `requests`
- `total_tokens`
- `cost`
- `actual_cost`

### 3. 按模型拆分

`GET /api/v1/usage/dashboard/models?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD&timezone=Asia/Shanghai`

返回 `data.models[]`，字段包括：
- `model`
- `requests`
- `input_tokens`
- `output_tokens`
- `cache_read_tokens`
- `total_tokens`
- `cost`
- `actual_cost`

## 备注

- 旧的 `/api/v1/usage/stats` 不适合这里的按日期 dashboard 统计。
- 上面这组 dashboard 接口使用普通用户 Bearer token 即可，不需要 admin 权限。
- `/api/v1/admin/dashboard/*` 这类 admin 路由对普通用户 token 会返回 `403 FORBIDDEN`，不要在这个技能里使用。


path: scripts/gmncode_usage.py
content: #!/usr/bin/env python3
"""Query GMNCODE usage data via HTTP APIs.

Secure credential loading order:
1. Environment variables: GMNCODE_EMAIL / GMNCODE_PASSWORD
2. ~/.openclaw/.env entries with the same names

Base URL is hardcoded to https://gmncode.cn because it is not sensitive.

Features:
- Login on demand and cache token locally with restrictive permissions
- Retry automatically on 401/INVALID_TOKEN by re-login
- Query dashboard stats / trend / per-model usage
- Query active subscriptions and derive daily account quota metrics

Examples:
  python3 gmncode_usage.py report --date today
  python3 gmncode_usage.py quota
  python3 gmncode_usage.py brief
  python3 gmncode_usage.py report --date 2026-03-25
  python3 gmncode_usage.py report --start 2026-03-01 --end 2026-03-26
  python3 gmncode_usage.py models --date 2026-03-25 --json
  python3 gmncode_usage.py stats --json
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import pathlib
import stat
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

TIMEZONE = "Asia/Shanghai"
DEFAULT_BASE_URL = "https://gmncode.cn"
ENV_FILE = pathlib.Path.home() / ".openclaw" / ".env"
CACHE_DIR = pathlib.Path.home() / ".cache" / "openclaw" / "gmncode-usage"
TOKEN_CACHE = CACHE_DIR / "token.json"


def load_dotenv(path: pathlib.Path) -> dict[str, str]:
    data: dict[str, str] = {}
    if not path.exists():
        return data
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip().strip('"').strip("'")
        data[key] = value
    return data


def env_value(key: str, dotenv: dict[str, str]) -> str | None:
    return os.environ.get(key) or dotenv.get(key)


def ensure_file_mode(path: pathlib.Path, mode: int) -> None:
    try:
        path.chmod(mode)
    except OSError:
        pass


def secure_write_json(path: pathlib.Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
    ensure_file_mode(path, stat.S_IRUSR | stat.S_IWUSR)


def load_credentials() -> tuple[str, str, str]:
    dotenv = load_dotenv(ENV_FILE)
    email = env_value("GMNCODE_EMAIL", dotenv)
    [REDACTED_SECRET]"GMNCODE_PASSWORD", dotenv)
    base_url = DEFAULT_BASE_URL
    if not email or not password:
        raise SystemExit(
            "Missing GMNCODE credentials. Set GMNCODE_EMAIL and GMNCODE_PASSWORD "
            f"in environment variables or {ENV_FILE}."
        )
    return email, password, base_url.rstrip("/")


class GMNCodeClient:
    def __init__(self, email: str, password: str, base_url: str):
        self.email = email
        self.[REDACTED_SECRET]
        self.base_url = base_url.rstrip("/")
        self.api_base = f"{self.base_url}/api/v1"
        self.token: str | None = None
        self._load_cached_token()

    def _load_cached_token(self) -> None:
        if not TOKEN_CACHE.exists():
            return
        try:
            payload = json.loads(TOKEN_CACHE.read_text(encoding="utf-8"))
            [REDACTED_SECRET]"access_token")
            expires_at = payload.get("expires_at")
            if token and isinstance(expires_at, int):
                if int(dt.datetime.now(dt.timezone.utc).timestamp()) < expires_at - 60:
                    self.token = token
        except Exception:
            self.token = None

    def _save_cached_token(self, access_token: str, expires_in: int) -> None:
        expires_at = int(dt.datetime.now(dt.timezone.utc).timestamp()) + int(expires_in)
        secure_write_json(TOKEN_CACHE, {
            "access_token": access_token,
            "expires_at": expires_at,
            "base_url": self.base_url,
        })

    def clear_cached_token(self) -> None:
        self.token = None
        try:
            TOKEN_CACHE.unlink()
        except FileNotFoundError:
            pass

    def login(self) -> str:
        body = json.dumps({"email": self.email, "password": self.password}).encode("utf-8")
        req = urllib.request.Request(
            f"{self.api_base}/auth/login",
            data=body,
            headers={"Content-Type": "application/json", "Accept": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=30) as resp:
            payload = json.loads(resp.read())
        data = payload.get("data") or {}
        [REDACTED_SECRET]"access_token")
        expires_in = int(data.get("expires_in") or 86400)
        if not token:
            raise RuntimeError(f"Login succeeded but no token returned: {payload}")
        self.token = token
        self._save_cached_token(token, expires_in)
        return token

    def ensure_token(self) -> str:
        if self.[REDACTED_SECRET] self.token
        return self.login()

    def _request(self, path: str, params: dict[str, Any] | None = None, retry: bool = True) -> Any:
        query = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v is not None})
        url = f"{self.api_base}{path}"
        if query:
            url = f"{url}?{query}"
        req = urllib.request.Request(
            url,
            headers={
                "Authorization": f"Bearer {self.ensure_token()}",
                "Accept": "application/json, text/plain, */*",
                "Accept-Language": "zh",
                "Referer": f"{self.base_url}/dashboard",
                "User-Agent": "Mozilla/5.0 OpenClaw GMNCode Usage Skill",
            },
            method="GET",
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                payload = json.loads(resp.read())
        except urllib.error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="ignore")
            if retry and exc.code == 401:
                self.clear_cached_token()
                self.login()
                return self._request(path, params=params, retry=False)
            raise RuntimeError(f"HTTP {exc.code} calling {path}: {body}") from exc

        if payload.get("code") in ("INVALID_TOKEN", 401) and retry:
            self.clear_cached_token()
            self.login()
            return self._request(path, params=params, retry=False)
        if payload.get("code") not in (0, "0", None):
            raise RuntimeError(f"API error calling {path}: {payload}")
        return payload.get("data")

    def dashboard_stats(self) -> dict[str, Any]:
        return self._request("/usage/dashboard/stats", params={"timezone": TIMEZONE})

    def dashboard_trend(self, start_date: str, end_date: str) -> dict[str, Any]:
        return self._request(
            "/usage/dashboard/trend",
            params={"start_date": start_date, "end_date": end_date, "timezone": TIMEZONE},
        )

    def dashboard_models(self, start_date: str, end_date: str) -> dict[str, Any]:
        return self._request(
            "/usage/dashboard/models",
            params={"start_date": start_date, "end_date": end_date, "timezone": TIMEZONE},
        )

    def active_subscriptions(self) -> list[dict[str, Any]]:
        return self._request("/subscriptions", params={"status": "active"}) or []


def parse_date_spec(value: str | None) -> tuple[str, str]:
    today = dt.datetime.now(dt.timezone(dt.timedelta(hours=8))).date()
    if not value or value == "today":
        return today.isoformat(), today.isoformat()
    if value == "yesterday":
        d = today - dt.timedelta(days=1)
        return d.isoformat(), d.isoformat()
    if value == "this-month":
        return today.replace(day=1).isoformat(), today.isoformat()
    if "," in value:
        start, end = [part.strip() for part in value.split(",", 1)]
        return start, end
    return value, value


def fmt_tokens(value: int | float) -> str:
    n = float(value)
    if n >= 1_000_000_000:
        return f"{n/1_000_000_000:.2f}B"
    if n >= 1_000_000:
        return f"{n/1_000_000:.1f}M"
    if n >= 1_000:
        return f"{n/1_000:.0f}K"
    return str(int(n))


def derive_daily_quota(subscriptions: list[dict[str, Any]]) -> dict[str, float]:
    daily_limit = 0.0
    daily_used = 0.0
    for sub in subscriptions:
        group = sub.get("group") or {}
        daily_limit += float(group.get("daily_limit_usd") or 0)
        daily_used += float(sub.get("daily_usage_usd") or 0)
    return {
        "daily_limit_usd": daily_limit,
        "daily_used_usd": daily_used,
        "daily_remaining_usd": daily_limit - daily_used,
    }


def print_quota(client: GMNCodeClient) -> None:
    subs = client.active_subscriptions()
    quota = derive_daily_quota(subs)
    print("📊 账户额度")
    print(f"  每日使用额度: ${quota['daily_limit_usd']:.6f}")
    print(f"  今日已用:     ${quota['daily_used_usd']:.6f}")
    print(f"  今日剩余:     ${quota['daily_remaining_usd']:.6f}")


def print_brief(client: GMNCodeClient, date: str) -> None:
    subs = client.active_subscriptions()
    quota = derive_daily_quota(subs)
    models = client.dashboard_models(date, date).get("models", [])
    print("📊 账户额度")
    print(f"  每日使用额度: ${quota['daily_limit_usd']:.6f}")
    print(f"  今日已用:     ${quota['daily_used_usd']:.6f}")
    print(f"  今日剩余:     ${quota['daily_remaining_usd']:.6f}")
    print("\n🤖 今日模型使用")
    for model in models:
        print(
            f"  {model['model']}: {fmt_tokens(model['total_tokens'])} tokens | "
            f"actual ${float(model.get('actual_cost') or 0):.6f}"
        )


def print_report(client: GMNCodeClient, start_date: str, end_date: str) -> None:
    stats = client.dashboard_stats()
    trend = client.dashboard_trend(start_date, end_date)
    rows = trend.get("trend", [])
    range_requests = sum(int(day.get("requests") or 0) for day in rows)
    range_tokens = sum(int(day.get("total_tokens") or 0) for day in rows)
    range_cost = sum(float(day.get("cost") or 0) for day in rows)
    range_actual_cost = sum(float(day.get("actual_cost") or 0) for day in rows)

    print(f"📅 区间: {start_date} → {end_date}\n")
    print("📊 区间汇总")
    print(f"  区间请求:   {range_requests:,}")
    print(f"  区间 Tokens: {fmt_tokens(range_tokens)}")
    print(f"  区间费用:   ${range_cost:.4f} (actual ${range_actual_cost:.4f})")
    print(f"  当前今日:   {stats['today_requests']:,} 请求 | {fmt_tokens(stats['today_tokens'])} | ${stats['today_cost']:.4f}")
    print(f"  当前累计:   {stats['total_requests']:,} 请求 | {fmt_tokens(stats['total_tokens'])} | ${stats['total_cost']:.4f} (actual ${stats['total_actual_cost']:.4f})\n")

    print("📈 每日趋势")
    print(f"{'日期':<12} {'请求数':>8} {'Tokens':>10} {'费用':>10}")
    print("-" * 46)
    for day in rows:
        print(f"{day['date']:<12} {day['requests']:>8,} {fmt_tokens(day['total_tokens']):>10} ${day['cost']:>9.4f}")

    if start_date == end_date:
        models = client.dashboard_models(start_date, end_date).get("models", [])
        print("\n🤖 模型分布")
        for model in models:
            print(
                f"  {model['model']}: {model['requests']:,} 请求 | "
                f"{fmt_tokens(model['total_tokens'])} tokens | ${model['cost']:.4f}"
            )


def main() -> None:
    parser = argparse.ArgumentParser(description="Query GMNCODE dashboard usage APIs")
    sub = parser.add_subparsers(dest="command", required=False)

    p_stats = sub.add_parser("stats", help="Fetch dashboard aggregate stats")
    p_stats.add_argument("--json", action="store_true")

    p_trend = sub.add_parser("trend", help="Fetch daily trend")
    p_trend.add_argument("--date", default=None, help="today | yesterday | this-month | YYYY-MM-DD | start,end")
    p_trend.add_argument("--start")
    p_trend.add_argument("--end")
    p_trend.add_argument("--json", action="store_true")

    p_models = sub.add_parser("models", help="Fetch model usage for a single day or range")
    p_models.add_argument("--date", default="today", help="today | YYYY-MM-DD | start,end")
    p_models.add_argument("--start")
    p_models.add_argument("--end")
    p_models.add_argument("--json", action="store_true")

    p_report = sub.add_parser("report", help="Print combined report")
    p_report.add_argument("--date", default="today", help="today | yesterday | this-month | YYYY-MM-DD | start,end")
    p_report.add_argument("--start")
    p_report.add_argument("--end")

    p_quota = sub.add_parser("quota", help="Print account daily quota derived from active subscriptions")

    p_brief = sub.add_parser("brief", help="Print the concise metrics set: account daily quota + today model usage")
    p_brief.add_argument("--date", default="today", help="today | YYYY-MM-DD")

    args = parser.parse_args()
    email, password, base_url = load_credentials()
    client = GMNCodeClient(email=email, [REDACTED_SECRET], base_url=base_url)

    cmd = args.command or "report"
    if cmd == "stats":
        data = client.dashboard_stats()
        print(json.dumps(data, ensure_ascii=False, indent=2) if args.json else data)
        return

    if cmd == "quota":
        print_quota(client)
        return

    if cmd == "brief":
        start_date, _ = parse_date_spec(getattr(args, "date", None))
        print_brief(client, start_date)
        return

    if cmd in {"trend", "models", "report"}:
        if getattr(args, "start", None) and getattr(args, "end", None):
            start_date, end_date = args.start, args.end
        else:
            start_date, end_date = parse_date_spec(getattr(args, "date", None))

        if cmd == "trend":
            data = client.dashboard_trend(start_date, end_date)
            if args.json:
                print(json.dumps(data, ensure_ascii=False, indent=2))
            else:
                for day in data.get("trend", []):
                    print(
                        f"{day['date']}\t{day['requests']}\t{day['total_tokens']}\t"
                        f"${day['cost']:.4f}"
                    )
            return

        if cmd == "models":
            data = client.dashboard_models(start_date, end_date)
            if args.json:
                print(json.dumps(data, ensure_ascii=False, indent=2))
            else:
                for model in data.get("models", []):
                    print(
                        f"{model['model']}\t{model['requests']}\t{model['total_tokens']}\t"
                        f"${model['cost']:.4f}"
                    )
            return

        print_report(client, start_date, end_date)
        return


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)