Skip to content

套餐余量 SKILL

本文用于约束 OpenClaw 自动创建「ClawPilot 套餐余量查询 Skill」时的实现方式。

目标只有一个:创建一个只读 Skill,复用当前 openclaw.jsonmodels.providers.clawpilot-ai.apiKey 的值,请求 ClawPilot 套餐余量接口,并把结果稳定地展示给用户。

如果你正在自动创建这个 Skill,请按本文执行,不要自行改成别的接口或额外登录链路。

一、这个 Skill 必须完成什么

  • 查询当前 clawpilot-ai provider 对应账号的套餐余量、今日已用额度、今日剩余额度、请求数与 token 用量。
  • 复用当前 OpenClaw 已配置的 clawpilot-ai provider API key。
  • 不要求用户额外登录 App,也不要要求用户再粘贴一份 API key。
  • 不修改 openclaw.json、不写入新 secret、不过度缓存旧结果。
  • 当用户表达下面这些意图时,应优先使用这个 Skill:
    • 查询套餐余量
    • 查询套餐用量
    • 查询今日还剩多少额度
    • 查询今天用了多少额度
    • 查询套餐是否还有效

二、凭证来源规则

1. 只使用当前 clawpilot-ai provider 的 key

必须直接读取本地配置文件并解析:

bash
models.providers.clawpilot-ai.apiKey

不要使用下面这条命令来读取:

bash
openclaw config get models.providers.clawpilot-ai.apiKey

原因:

  • 这条命令返回的值会被脱敏。
  • 脱敏后的值不能拿来请求套餐余量接口。
  • 因此,创建 Skill 时必须直接读取本地配置文件,再解析 models.providers.clawpilot-ai.apiKey 的原始值。

推荐查找顺序:

  1. 如果存在 OPENCLAW_CONFIG_PATH,优先读取该路径指向的配置文件。
  2. 否则,如果存在 OPENCLAW_STATE_DIR,读取 OPENCLAW_STATE_DIR/openclaw.json
  3. 否则,读取默认路径 ~/.openclaw/openclaw.json

跨平台与部署约束:

  • 不要假设用户的 OpenClaw 一定运行在 macOS 或 Linux;也可能运行在 Windows、WSL 或 Docker 容器里。
  • 因此不要硬编码 /Users/.../home/...C:\\Users\\... 这类路径。
  • 读取配置时,优先依赖 OPENCLAW_CONFIG_PATHOPENCLAW_STATE_DIR;只有它们都不存在时,才回退到运行时用户目录下的默认配置文件。
  • 如果 OpenClaw 运行在 Docker 中,应读取容器内运行时可见的配置文件路径,而不是宿主机路径。
  • Skill 的实现应以“当前 OpenClaw 进程所在运行环境”作为文件系统基准,而不是以用户口头描述的宿主机操作系统作为基准。

处理规则:

  1. 读取配置文件原始内容,并解析 models.providers.clawpilot-ai.apiKey
  2. 对解析出的结果做 trim()
  3. 如果为空,直接返回“当前 OpenClaw 未配置 clawpilot-ai provider API key”,不要继续请求接口。
  4. 如果值形如 ${ENV_NAME},取 process.env.ENV_NAME 的实际值。
  5. 如果值本身像环境变量名(例如 CLAWPILOT_AI_API_KEY)且对应环境变量存在,优先取环境变量值。
  6. 其他情况按字面量 API key 使用。

额外约束:

  • 不要把这个 key 复制到 skills.entries.*.apiKey
  • 不要在回复中输出完整 key。
  • 不要把 key 写入日志、Markdown 或错误详情中。
  • 如果配置文件读取失败,应明确告诉用户是“本地配置读取失败”,而不是伪装成接口鉴权失败。

2. 推荐超时

  • HTTP 请求超时建议使用 12 秒。
  • 如果超时,直接告诉用户“查询超时,请稍后重试”,不要伪造旧结果。

三、接口契约

1. 请求

  • Method:GET
  • URL:https://api.clawpilot.me/api/v1/ai-api/me/summary
  • Headers:
    • Authorization: Bearer <clawpilot-ai provider apiKey>
    • Accept: application/json
  • Query:无
  • Body:无

2. 成功响应

成功时返回 200,结构如下:

json
{
  "account": {
    "id": 123,
    "email": "[email protected]",
    "status": "active",
    "created_at": "2026-03-01T09:00:00.000Z",
    "updated_at": "2026-03-14T06:21:00.000Z",
    "last_login_at": "2026-03-14T05:10:00.000Z"
  },
  "summary": {
    "has_any_entitlement": true,
    "has_active_entitlement": true,
    "current_total_daily_allowance_usd_micros": 100000000,
    "today_used_amount_usd_micros": 12345678,
    "current_daily_remaining_usd_micros": 87654322,
    "today_used_input_tokens": 123456,
    "today_used_output_tokens": 78901,
    "today_used_total_tokens": 202357,
    "today_request_count": 42,
    "last_request_at": "2026-03-14T06:20:15.000Z",
    "last_entitlement_ends_at": "2026-04-10T16:00:00.000Z",
    "quota_date": "2026-03-14",
    "quota_reset_timezone": "Asia/Shanghai"
  },
  "server_time": "2026-03-14T06:21:02.000Z"
}

兼容性要求:

  • 可以忽略未知字段。
  • 不要依赖未在上面列出的字段。

3. 字段解释

字段含义展示建议
has_any_entitlement是否有过套餐记录用于区分“从未开通过”和“当前暂无生效套餐”
has_active_entitlement当前是否有生效套餐作为主状态判断
current_total_daily_allowance_usd_micros当前每日总额度(美元 micros)除以 1_000_000 后显示
today_used_amount_usd_micros今日已用额度(美元 micros)除以 1_000_000 后显示
current_daily_remaining_usd_micros当前今日剩余额度(美元 micros)除以 1_000_000 后显示
today_used_input_tokens今日输入 token展示时建议使用紧凑单位,如 1.2K3.4M
today_used_output_tokens今日输出 token展示时建议使用紧凑单位,如 1.2K3.4M
today_used_total_tokens今日总 token展示时建议使用紧凑单位,如 1.2K3.4M
today_request_count今日请求数直接显示整数
last_request_at最近一次请求时间若为空则显示“暂无”;若展示则转换为用户本地时区的绝对时间
last_entitlement_ends_at当前或最近套餐的最晚结束时间若为空则显示“暂无”;若展示则转换为用户本地时区的绝对时间
quota_date当前统计口径所属日期与时区一起显示
quota_reset_timezone每日额度重置时区必须保留原值展示
server_time接口返回时间建议作为“查询时间”显示,并转换为用户本地时区的绝对时间

4. 金额换算规则

所有 *_usd_micros 字段都按下面公式换算:

text
usd = micros / 1_000_000

展示规则:

  • 大于等于 0.01 美元时,默认保留最多 2 位小数。
  • 大于 0 且小于 0.01 美元时,保留最多 4 位小数。
  • 去掉无意义的末尾 0
  • 输出前缀统一使用 $

5. Token 展示规则

  • today_used_input_tokenstoday_used_output_tokenstoday_used_total_tokens 这三个字段,展示时应优先使用紧凑单位。
  • 推荐单位:
    • K = 千
    • M = 百万
    • B = 十亿
  • 推荐格式:
    • 950 显示为 950
    • 1_200 显示为 1.2K
    • 15_300 显示为 15.3K
    • 2_450_000 显示为 2.45M
  • 默认保留最多 2 位小数,并去掉无意义的末尾 0
  • today_request_count 不需要使用 K / M,保持普通整数显示即可。

6. 时间展示规则

  • server_timelast_request_atlast_entitlement_ends_at 这三个字段,展示时都要转换为用户本地时区
  • 展示时使用绝对时间,不要只说“今天”“刚刚”“昨天”。
  • 如果可以获取用户本地时区名称,建议同时显示时区缩写或时区名。
  • quota_reset_timezone 是套餐每日额度的重置时区,必须保留原值,不要替换成用户本地时区。
  • 如果某个时间字段为空,明确显示“暂无”。

四、失败响应与处理方式

2xx 时,接口会返回统一错误结构:

json
{
  "code": 401,
  "status": 401,
  "error_code": "UNAUTHORIZED",
  "message": "AI API Key 无效",
  "request_id": "req_xxx"
}

处理规则:

HTTP 状态处理方式
401告诉用户当前 clawpilot-ai provider API key 无效、缺失或不可用于查询套餐余量;附带 request_id(如果有)
403告诉用户当前 AI API 账号不可用;附带 request_id(如果有)
429告诉用户请求过于频繁,请稍后再试;若响应里有 retry_after_seconds,可以一起展示
5xx告诉用户服务端暂时异常,请稍后重试;附带 request_id(如果有)

额外约束:

  • 失败时不要伪造套餐信息。
  • 失败时不要继续调用其他 ClawPilot 私有接口做猜测。
  • 失败时可以建议用户检查 clawpilot-ai provider 配置是否仍有效。

五、给用户的展示格式约束

这个 Skill 返回成功后,推荐按下面顺序给用户展示,保持稳定、简洁、可读:

text
ClawPilot 套餐用量
- 查询时间:<server_time 转换为用户本地时区后的绝对时间>
- 账号:<account.email>
- 当前状态:<状态文案>
- 当前每日额度:<$ 金额>
- 今日已用:<$ 金额>
- 当前剩余:<$ 金额>
- 今日请求数:<整数>
- 今日 Token:输入 <紧凑单位> / 输出 <紧凑单位> / 合计 <紧凑单位>
- 最近一次请求:<last_request_at 转换为用户本地时区后的绝对时间,或 暂无>
- 当前套餐最晚到期:<last_entitlement_ends_at 转换为用户本地时区后的绝对时间,或 暂无>
- 统计口径:<quota_date>(<quota_reset_timezone>)

状态文案约束:

  • has_active_entitlement === true 显示:套餐生效中
  • has_active_entitlement === false && has_any_entitlement === true 显示:暂无生效套餐
  • has_active_entitlement === false && has_any_entitlement === false 显示:未检测到套餐

展示时必须遵守:

  • server_timelast_request_atlast_entitlement_ends_at 使用用户本地时区的绝对时间,不要只说“今天”“刚刚”“昨天”。
  • 不要猜测套餐名称,因为接口没有返回套餐名称。
  • 不要猜测剩余天数,因为接口没有直接返回剩余天数。
  • 不要输出原始 micros 数值,除非用户明确要求查看原始字段。
  • 若某些时间字段为空,明确显示“暂无”。

六、推荐创建的 Skill 目录

推荐目录:

bash
mkdir -p ~/.openclaw/workspace/skills/clawpilot-plan-summary/scripts

目录结构:

text
clawpilot-plan-summary/
├── SKILL.md
└── scripts/
    └── fetch_plan_summary.mjs

七、参考 SKILL.md

如果你要自动创建这个 Skill,推荐写成下面这样:

markdown
---
name: clawpilot-plan-summary
description: Query ClawPilot plan usage and remaining daily credits with the existing clawpilot-ai provider API key.
metadata:
  {
    "openclaw":
      {
        "emoji": "📊",
        "requires": { "bins": ["node", "openclaw"], "config": ["models.providers.clawpilot-ai.apiKey"] },
      },
  }
---

# ClawPilot plan summary

Use this skill when the user asks about ClawPilot plan balance, plan usage, daily credits, remaining quota, or whether the current plan is still active.

## Steps

1. Run `node {baseDir}/scripts/fetch_plan_summary.mjs`.
2. Parse the returned JSON.
3. If `ok !== true`, explain the failure briefly and include `request_id` when present.
4. If `ok === true`, present the result in this exact order:
   - fetched_at
   - account_email
   - status_label
   - current_daily_allowance
   - used_today
   - remaining_today
   - today_request_count
   - today token usage(compact units like K / M)
   - last_request_at(show in the user's local timezone)
   - last_entitlement_ends_at(show in the user's local timezone)
   - quota_date + quota_reset_timezone

## Important rules

- Reuse the existing `clawpilot-ai` provider API key.
- Read the provider API key directly from the local OpenClaw config file. Do not use `openclaw config get ...apiKey` because that output is masked.
- Do not ask the user to paste a second API key if this skill is eligible.
- Do not call `/api/v1/client/ai-api/runtime-status`.
- Do not infer a plan name.
- Do not show raw micros unless the user explicitly asks for raw fields.
- Render displayed timestamps in the user's local timezone.
- Render token usage with compact units such as `K` and `M`.

八、参考脚本 scripts/fetch_plan_summary.mjs

下面这份脚本是一个可直接生成的参考实现。它会:

  • 读取当前 clawpilot-ai provider 的 API key
  • 调用套餐余量接口
  • 输出一个更适合 Skill 消费的标准化 JSON

注意:

  • 这个脚本保留原始 ISO 时间字符串。
  • 最终给用户展示时,应在 Skill 展示层把这些时间转换成用户本地时区。
  • 这个脚本使用 os.homedir()path.join(),就是为了兼容 Windows、macOS、Linux 以及容器内路径。
js
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const API_URL = "https://api.clawpilot.me/api/v1/ai-api/me/summary";
const TIMEOUT_MS = 12_000;

function trimOrEmpty(value) {
  return typeof value === "string" ? value.trim() : "";
}

function resolveConfigFilePath() {
  const explicitConfigPath = trimOrEmpty(process.env.OPENCLAW_CONFIG_PATH);
  if (explicitConfigPath) {
    return explicitConfigPath;
  }

  const stateDir = trimOrEmpty(process.env.OPENCLAW_STATE_DIR);
  if (stateDir) {
    return path.join(stateDir, "openclaw.json");
  }

  return path.join(os.homedir(), ".openclaw", "openclaw.json");
}

function readOpenClawConfig() {
  const configPath = resolveConfigFilePath();
  let rawText = "";

  try {
    rawText = fs.readFileSync(configPath, "utf8");
  } catch {
    throw new Error(`读取 OpenClaw 配置文件失败:${configPath}`);
  }

  try {
    return JSON.parse(rawText);
  } catch {
    throw new Error(`OpenClaw 配置文件不是合法 JSON:${configPath}`);
  }
}

function readProviderApiKeyRaw() {
  const config = readOpenClawConfig();
  return trimOrEmpty(config?.models?.providers?.["clawpilot-ai"]?.apiKey);
}

function resolveApiKey(rawValue) {
  const raw = trimOrEmpty(rawValue);
  if (!raw) {
    throw new Error("当前 OpenClaw 未配置 clawpilot-ai provider API key。");
  }

  const bracketEnvMatch = raw.match(/^\$\{([A-Z][A-Z0-9_]*)\}$/u);
  if (bracketEnvMatch) {
    const resolved = trimOrEmpty(process.env[bracketEnvMatch[1]]);
    if (!resolved) {
      throw new Error(`环境变量 ${bracketEnvMatch[1]} 未设置,无法继续查询套餐余量。`);
    }
    return resolved;
  }

  if (/^[A-Z][A-Z0-9_]*$/u.test(raw)) {
    const resolved = trimOrEmpty(process.env[raw]);
    if (resolved) {
      return resolved;
    }
  }

  return raw;
}

function toNumber(value) {
  return typeof value === "number" && Number.isFinite(value) ? value : Number(value || 0);
}

function microsToUsd(value) {
  return toNumber(value) / 1_000_000;
}

function formatUsd(value) {
  const amount = microsToUsd(value);
  if (!Number.isFinite(amount) || amount <= 0) {
    return "$0";
  }

  const digits = amount < 0.01 ? 4 : 2;
  return `$${amount.toFixed(digits).replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "")}`;
}

function formatInteger(value) {
  return Math.max(0, Math.trunc(toNumber(value))).toLocaleString("en-US");
}

function formatCompactTokenCount(value) {
  const safeValue = Math.max(0, Math.trunc(toNumber(value)));
  if (safeValue < 1000) {
    return String(safeValue);
  }

  const units = [
    { threshold: 1_000_000_000, suffix: "B" },
    { threshold: 1_000_000, suffix: "M" },
    { threshold: 1_000, suffix: "K" },
  ];

  for (const unit of units) {
    if (safeValue >= unit.threshold) {
      const amount = safeValue / unit.threshold;
      return `${amount.toFixed(2).replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "")}${unit.suffix}`;
    }
  }

  return String(safeValue);
}

function resolveStatusLabel(summary) {
  if (summary?.has_active_entitlement === true) {
    return "套餐生效中";
  }
  if (summary?.has_any_entitlement === true) {
    return "暂无生效套餐";
  }
  return "未检测到套餐";
}

async function requestSummary(apiKey) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);

  try {
    const response = await fetch(API_URL, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        Accept: "application/json",
      },
      signal: controller.signal,
    });

    const text = await response.text();
    let payload = null;

    try {
      payload = text ? JSON.parse(text) : null;
    } catch {
      payload = null;
    }

    if (!response.ok) {
      return {
        ok: false,
        status: response.status,
        error_code: payload?.error_code ?? "HTTP_ERROR",
        message: payload?.message ?? "请求失败",
        request_id: payload?.request_id ?? null,
      };
    }

    return {
      ok: true,
      payload,
    };
  } finally {
    clearTimeout(timer);
  }
}

async function main() {
  try {
    const apiKey = resolveApiKey(readProviderApiKeyRaw());
    const result = await requestSummary(apiKey);

    if (!result.ok) {
      console.log(JSON.stringify(result, null, 2));
      process.exit(result.status >= 500 ? 1 : 2);
    }

    const payload = result.payload ?? {};
    const account = payload.account ?? {};
    const summary = payload.summary ?? {};

    const normalized = {
      ok: true,
      fetched_at: payload.server_time ?? new Date().toISOString(),
      account_email: account.email ?? null,
      account_status: account.status ?? null,
      last_login_at: account.last_login_at ?? null,
      status_label: resolveStatusLabel(summary),
      has_any_entitlement: summary.has_any_entitlement === true,
      has_active_entitlement: summary.has_active_entitlement === true,
      current_daily_allowance: formatUsd(summary.current_total_daily_allowance_usd_micros),
      used_today: formatUsd(summary.today_used_amount_usd_micros),
      remaining_today: formatUsd(summary.current_daily_remaining_usd_micros),
      today_request_count: formatInteger(summary.today_request_count),
      today_used_input_tokens: formatCompactTokenCount(summary.today_used_input_tokens),
      today_used_output_tokens: formatCompactTokenCount(summary.today_used_output_tokens),
      today_used_total_tokens: formatCompactTokenCount(summary.today_used_total_tokens),
      last_request_at: summary.last_request_at ?? null,
      last_entitlement_ends_at: summary.last_entitlement_ends_at ?? null,
      quota_date: summary.quota_date ?? null,
      quota_reset_timezone: summary.quota_reset_timezone ?? null,
      raw: payload,
    };

    console.log(JSON.stringify(normalized, null, 2));
  } catch (error) {
    console.log(
      JSON.stringify(
        {
          ok: false,
          status: 0,
          error_code: "LOCAL_RUNTIME_ERROR",
          message: error instanceof Error ? error.message : String(error),
          request_id: null,
        },
        null,
        2,
      ),
    );
    process.exit(1);
  }
}

await main();

九、创建完成后的自检清单

自动创建这个 Skill 后,至少自检下面几项:

  1. clawpilot-ai provider API key 已配置时,Skill 能返回套餐余量。
  2. 当 provider key 缺失时,Skill 会明确提示配置缺失,而不是请求错误接口。
  3. 当账号暂无生效套餐时,Skill 会显示“暂无生效套餐”或“未检测到套餐”,而不是伪造套餐名。
  4. 当接口失败时,Skill 会把 request_id 带给用户(如果响应中存在)。
  5. Skill 不会在回复里泄露完整 API key。

Last updated:

ClawPilot