From 162f94597d6f23451b39d1fc690a5c0d7f0ae50c Mon Sep 17 00:00:00 2001 From: Jovan <3071058281@qq.com> Date: Mon, 21 Sep 2026 01:46:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E9=BD=90=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E5=B8=82=E5=9C=BA=E6=8A=95=E7=A8=BF=E8=A6=81=E6=B1=82=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=20icon=20=E4=B8=8E=E4=B8=AD=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为投稿 zai-org/zcode-plugins(ZCode 官方市场)做准备: - 新增 assets/command-code-usage/icon.png(256x256)+ scripts/make-icon.mjs: 图标由代码确定性生成(4x 超采样 + 距离场抗锯齿),可复现、无外部依赖 - 新增 command-code-usage/README_CN.md:官方要求插件目录内必须有内容对等的中英文 README,且需写明网络访问、凭证、文件写入与命令执行等副作用 - 插件清单加 description_i18n(en + zh-CN):官方 validate.py 要求,且必须与市场 条目里的完全一致 - 市场条目对齐官方字段格式:i18n 用 en/zh-CN 双键,补 keywords / icon / requiresPaidPlan / license / repository - check.mjs 白名单补 keywords / license / repository(官方也在用的字段) 门禁 48 项通过;claude plugin validate --strict 通过;官方 validate.py 与 build_dist.py 在投稿分支上均通过。 --- .claude-plugin/marketplace.json | 2 +- assets/command-code-usage/icon.png | Bin 0 -> 5181 bytes command-code-usage/.claude-plugin/plugin.json | 2 +- command-code-usage/.zcode-plugin/plugin.json | 8 +- command-code-usage/README.md | 165 +++++++++++++-- command-code-usage/README_CN.md | 144 +++++++++++++ marketplace.json | 43 ++-- scripts/check.mjs | 2 + scripts/make-icon.mjs | 193 ++++++++++++++++++ 9 files changed, 522 insertions(+), 37 deletions(-) create mode 100644 assets/command-code-usage/icon.png create mode 100644 command-code-usage/README_CN.md create mode 100644 scripts/make-icon.mjs diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5efa19b..e47a68c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "command-code-usage", "source": "./command-code-usage", "version": "1.1.0", - "description": "Check your Command Code plan usage from inside your coding agent: 5-hour and weekly rolling windows, monthly credits or balance, plus a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", + "description": "See your Command Code plan usage — 5-hour and weekly rolling windows, monthly credits or balance — right inside the conversation, with a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", "displayName": "Command Code Usage", "category": "utilities", "homepage": "https://github.com/Jovan1666/zcode-command-code-usage", diff --git a/assets/command-code-usage/icon.png b/assets/command-code-usage/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cab3f94aa95abdcc65d76a8ecff58e67f9fcab8e GIT binary patch literal 5181 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 + +5-hour window ██░░░░░░░░░░░░░░░░░░░░░░ 6.3% $0.89 / $14.00 + resets 04:29 · in 3h 43m +weekly window █░░░░░░░░░░░░░░░░░░░░░░░ 2.5% $0.89 / $35.00 + resets 09-27 23:29 · in 6d 22h +monthly ░░░░░░░░░░░░░░░░░░░░░░░░ 1.3% $0.89 / $70.00 + $69.11 remaining + +this period 330 requests · 100% success · 96.3M in / 306.9K out tokens +estimate at your $0.0026 average, room for ≈ 5,064 more requests in the 5-hour window + (based on your actual model mix this period; pricier models go much shorter) +``` -```bash -# user-scope install (no marketplace needed) -node scripts/install-user-scope.mjs +When you are burning fast enough that a window will run out before it resets, it says so: -# the panel, standalone -node scripts/cc-usage.mjs ``` +⚠ at the current $4.30/h, the 5-hour window runs out before it resets — exhausted in ~15m 20s +``` + +## Components + +| Type | Name | What it does | +|---|---|---| +| Command | `/quota` | Renders the usage panel in the conversation | +| Command | `/usage` | Alias for `/quota` | +| Skill | `command-code-usage` | Teaches the agent to fetch the panel and to answer "is it enough to finish this task?" from the remaining-requests estimate rather than the monthly balance | + +No hooks, no MCP servers, no agents, no background processes. + +## Requirements + +- **A Command Code plan.** Without one there is nothing to show. On pay-as-you-go rather than a + subscription it still works, but shows a balance instead of windows. +- **Node.js** on `PATH`. The bundled scripts use only Node built-ins (`node:fs`, `node:http`, + `node:os`, `node:path`, `node:child_process`, `node:zlib`) and the global `fetch`, so there is + nothing to install. + +## Usage + +| Command | Result | +|---|---| +| `/quota` | The panel above | +| `/quota --md` | Markdown table, easier to copy | +| `/quota --compact` | One line, e.g. `CC GOAT · 5h 6% · weekly 2% · monthly 1.2% · $69.16 left` | +| `/quota --json` | Normalised fields plus the raw API responses | +| `/quota --demo hot` | Sample data — previews the warning state without touching the network | + +Asking in plain language works too: *"How much Command Code quota do I have left? Is it enough to +finish what we are doing?"* + +## Credentials and network access + +**Network:** the panel calls four read-only endpoints on `https://api.commandcode.ai` — +`/alpha/whoami`, `/alpha/billing/credits`, `/alpha/billing/subscriptions`, and +`/alpha/usage/summary`. No other host is contacted. Nothing is sent anywhere else, and there is no +telemetry. + +**The API key is resolved at runtime, in this order** (first hit wins): + +1. the environment variable `COMMAND_CODE_API_KEY`, `CMD_API_KEY` or `COMMANDCODE_API_KEY`; +2. `~/.commandcode/auth.json`, written by logging into the Command Code CLI; +3. `~/.zcode/v2/provider_config.json` — a provider whose `api.baseUrl` points at `commandcode.ai`, + i.e. the key you already configured in ZCode. + +The key is only ever placed in an `Authorization: Bearer` header. **The plugin does not write, +print, log or transmit the key**, and it contains no credentials. `--verbose` reports which source +was used (never the key itself). + +## Side effects + +- **Reads** the credential files listed above, and the API endpoints listed above. +- **Writes nothing** by default. No cache, no state, no config changes. +- `--html` writes one HTML file, only when you explicitly pass that flag, to the path you choose + (default `./command-code-usage.html`). +- `--serve` starts a local HTTP server on `127.0.0.1` (default port 8787) so a browser can poll the + panel. It is off unless you pass the flag, binds to loopback only, and stops with Ctrl+C. +- The panel shells out to `node /scripts/cc-usage.mjs`. That is the only process it starts. + +## How the two useful numbers are derived + +**"Room for ≈ N more requests"** = remaining allowance ÷ your average cost per request *this +period*. The average comes from your own usage, so the estimate adapts to any plan and any model +mix without hard-coding per-model rates. Because the baseline is your own average, **it stops +holding the moment you switch models** — the panel says so. Command Code's `/provider/v1/models` +returns a model list with no allowance factors or prices, so "how many requests of model X +specifically" cannot be computed from the API. + +**The warning** extrapolates your current burn rate. That path has a trap: an hour after a window +opens, extrapolating one hour of activity across seven days will always claim the weekly cap is +about to blow — pure noise. So the script enforces a minimum sample: **under 5% of the window +elapsed it draws no conclusion at all**. No warning therefore means "not enough data yet", not +"you are safe". + +## Account shapes it handles + +| Situation | What is shown | +|---|---| +| Subscription, plan in the known table | monthly allowance bar plus both windows | +| Subscription, plan not in the table (new or enterprise) | "allowance", with an explicit note that the total is inferred from spent + remaining | +| No active subscription (pay-as-you-go, enterprise pool) | balance only, no meaningless percentage | +| Organisation spend caps configured | extra limit rows (shapes it cannot recognise are skipped, never guessed) | +| No requests yet this period | no request-count estimate, and it says why | + +## Bundled scripts + +Besides the command the agent runs (`scripts/cc-usage.mjs`), the plugin ships two standalone tools. +Neither runs on its own; they exist for the cases described here. + +- `scripts/cc-usage.mjs` — the panel itself. Runnable directly: + `node scripts/cc-usage.mjs --compact`. The panel adds no state and no cache. +- `scripts/install-user-scope.mjs` — installs the commands and skill into your user-scope agent + directories (`~/.zcode/commands`, `~/.zcode/skills`) for users who prefer not to go through the + marketplace. It writes those files and keeps a small manifest of what it wrote so it can update + or remove them later. **Do not run it on top of a marketplace installation**: user-scope copies + are discovered first and would shadow the installed plugin. `--uninstall` removes them. +- `scripts/verify-discoverable.cjs` — a read-only diagnostic that re-implements ZCode's own command + parser, so a "my command does not show up" report can come with evidence. It reads files and + prints a report; it writes nothing. + +## Troubleshooting + +| Symptom | Cause and fix | +|---|---| +| `/quota` missing from the `/` menu | The catalogue is snapshotted when a session starts. Fully quit the app and reopen it. | +| Typing `/quota` sends it as a normal message | The command was not discovered. Run `node scripts/verify-discoverable.cjs .` — it re-implements ZCode's own parser and reports diagnostics. | +| "No Command Code credentials found" | Provide one of the three sources above. | +| HTTP 401 on every endpoint | The key is invalid or expired. Re-login, or re-enter it in the ZCode provider settings. | +| Numbers look stale | Window reset times move. Re-run the command rather than reusing an older reading. | +| Requests being rate-limited (429) | Check which window reports `exceeded`, then wait for the reset, buy extra credits, or upgrade. | + +## Contributing + +Issues and pull requests: . + +Not affiliated with Command Code. The plugin reads your own account's usage through the same +endpoints the official CLI uses. + +## License -This plugin contains no credentials. The Command Code API key is resolved at runtime from -`COMMAND_CODE_API_KEY`, `~/.commandcode/auth.json`, or the provider already configured in ZCode. +[MIT](https://github.com/Jovan1666/zcode-command-code-usage/blob/main/LICENSE) diff --git a/command-code-usage/README_CN.md b/command-code-usage/README_CN.md new file mode 100644 index 0000000..4f9375b --- /dev/null +++ b/command-code-usage/README_CN.md @@ -0,0 +1,144 @@ +# Command Code Usage + +在对话里直接看 **Command Code** 套餐还剩多少用量,不用离开对话。 + +Command Code 的套餐(Go / GOAT / Pro / Max / Teams)除了月度额度,还压着两个**滚动窗口**: +5 小时上限和每周上限。窗口从你第一次请求开始计时,到点重置,**不跟自然日/周走**,用量也不跨 +窗口结转。所以「这个任务还能不能跑完」光看月度余额是答不出来的——要看**当前窗口**还剩多少、 +什么时候重置。 + +这个插件把这三个数读出来,渲染在你本来就在看的地方。 + +``` +Command Code · GOAT 09-21 00:45 · 周期剩 30 天 +────────────────────────────────────────────────────────────────────────── +账号 your-name + +5 小时窗口 ██░░░░░░░░░░░░░░░░░░░░░░ 6.3% $0.89 / $14.00 + 重置 04:29 · 3h 43m 后 +每周窗口 █░░░░░░░░░░░░░░░░░░░░░░░ 2.5% $0.89 / $35.00 + 重置 09-27 23:29 · 6d 22h 后 +月度额度 ░░░░░░░░░░░░░░░░░░░░░░░░ 1.3% $0.89 / $70.00 + 剩 $69.11 + +本周期 330 次请求 · 成功率 100% · 入 96.3M / 出 306.9K tokens +预估 按本周期均单价 $0.0026 估算还能跑:5 小时窗口 ≈ 5,064 次 + (基于你本周期实际的模型组合;换更贵的模型次数会明显变少) +``` + +当消耗速度足以在重置前撞上限时,它会直说: + +``` +⚠ 按当前速度($4.30/小时),5 小时窗口会在重置前用完,约 15m 20s 后耗尽 +``` + +## 组件 + +| 类型 | 名称 | 作用 | +|---|---|---| +| 命令 | `/quota` | 在对话里渲染额度面板 | +| 命令 | `/usage` | `/quota` 的别名 | +| 技能 | `command-code-usage` | 教会 agent 取面板,并用剩余次数估算(而不是月度余额)回答「够不够跑完」 | + +没有 hook、没有 MCP server、没有 agent、没有后台常驻进程。 + +## 环境要求 + +- **一个 Command Code 套餐。** 没有套餐就没数可读。按量计费(非订阅)也能用,只是显示余额 + 而不是窗口。 +- **`PATH` 里有 Node.js。** 脚本只用 Node 内置模块(`node:fs`、`node:http`、`node:os`、 + `node:path`、`node:child_process`、`node:zlib`)和全局 `fetch`,不需要安装任何依赖。 + +## 用法 + +| 命令 | 结果 | +|---|---| +| `/quota` | 上面那个面板 | +| `/quota --md` | Markdown 表格,方便复制 | +| `/quota --compact` | 一行,例如 `CC GOAT · 5h 6% · 周 2% · 月 1.2% · 剩 $69.16` | +| `/quota --json` | 归一化字段,外加原始接口响应 | +| `/quota --demo hot` | 样例数据,不联网也能预览告警长什么样 | + +也可以直接问:*「我 Command Code 额度还剩多少?够不够把手上这个做完?」* + +## 凭证与网络访问 + +**网络:** 面板只调用 `https://api.commandcode.ai` 上四个只读端点——`/alpha/whoami`、 +`/alpha/billing/credits`、`/alpha/billing/subscriptions`、`/alpha/usage/summary`。 +不访问任何其他主机,不向任何其他位置发送数据,也没有遥测。 + +**API key 在运行时解析,按以下顺序,命中即用:** + +1. 环境变量 `COMMAND_CODE_API_KEY`、`CMD_API_KEY` 或 `COMMANDCODE_API_KEY`; +2. `~/.commandcode/auth.json`——登录 Command Code CLI 后生成; +3. `~/.zcode/v2/provider_config.json`——其中 `api.baseUrl` 指向 `commandcode.ai` 的 provider, + 也就是你已经在 ZCode 里配好的那把 key。 + +key 只会出现在 `Authorization: Bearer` 请求头里。**本插件不写入、不打印、不记录、不外传 key**, +插件内也不含任何凭证。`--verbose` 会报告用了哪个来源(但不显示 key 本身)。 + +## 副作用 + +- **读取**上面列出的凭证文件,以及上面列出的接口端点。 +- **默认不写任何东西。** 没有缓存、没有状态文件、不改配置。 +- `--html` 会写一个 HTML 文件——只在你显式传该参数时,写入你指定的路径 + (默认 `./command-code-usage.html`)。 +- `--serve` 会在 `127.0.0.1` 起一个本地 HTTP 服务(默认端口 8787)供浏览器轮询面板。不传参数 + 就不会启动;只绑定回环地址;Ctrl+C 停止。 +- 面板会调用 `node <插件目录>/scripts/cc-usage.mjs`。这是它唯一启动的进程。 + +## 两个有用的数是怎么来的 + +**「还能跑约 N 次」** = 剩余额度 ÷ 本周期均单价。均单价取自**你自己**这个周期的实际用量, +所以它自动适配任何套餐、任何模型组合,不需要把每个模型的费率硬编码进来。正因为基准是你自己的 +均值,**一换模型它就不再成立**——面板里写明了这一点。Command Code 的 `/provider/v1/models` +只返回模型清单,不含额度系数或单价,所以「某个特定模型还能跑几次」从接口层就算不出来。 + +**告警**按当前消耗速度外推。这条路有个陷阱:窗口刚开一小时,拿这一小时的速度去推七天,必然 +天天喊「周窗口要超限了」——纯噪音。所以脚本设了最小采样门槛:**不足窗口时长的 5% 就不出结论**。 +因此「没有告警」的意思是「样本还不够判断」,而不是「你安全」。 + +## 它怎么适配不同账号形态 + +| 情况 | 显示 | +|---|---| +| 有订阅、套餐在已知表里 | 月度额度进度条 + 两个窗口 | +| 有订阅、套餐不在表里(新套餐/企业套餐) | 显示「额度」,并明说总额是按「已花 + 剩余」推算 | +| 无有效订阅(按量计费 / 企业池) | 只显示余额,不套没有意义的百分比 | +| 组织配置了消费上限 | 追加限额行(认不出的字段形状直接跳过,不猜) | +| 本周期还没有请求 | 不给次数估算,并说明原因 | + +## 附带脚本 + +除了 agent 执行的那条命令(`scripts/cc-usage.mjs`),插件还带两个独立工具。它们不会自动运行, +只在下列场景下由你手动使用。 + +- `scripts/cc-usage.mjs` —— 面板本体。可直接运行:`node scripts/cc-usage.mjs --compact`。 + 面板不写状态、不建缓存。 +- `scripts/install-user-scope.mjs` —— 把命令与技能装进你的用户级 agent 目录 + (`~/.zcode/commands`、`~/.zcode/skills`),供不想走市场的用户使用。它会写入这些文件,并保留 + 一份「我写过什么」的小清单,以便日后更新或卸载。**不要在市场安装之上再跑它**:用户级副本的 + 发现优先级更高,会遮蔽已安装的插件。`--uninstall` 可移除。 +- `scripts/verify-discoverable.cjs` —— 只读诊断工具,复刻了 ZCode 自己的命令解析器, + 这样「我的命令不出现」这类问题可以带着证据来报。它只读文件、打印报告,不写任何东西。 + +## 排查 + +| 现象 | 原因与处理 | +|---|---| +| `/` 菜单里找不到 `/quota` | 命令清单在会话启动时快照。完全退出应用再打开。 | +| 输入 `/quota` 被当成普通消息发出去 | 命令没被发现。跑 `node scripts/verify-discoverable.cjs .`——它复刻了 ZCode 自己的解析器,会报出诊断。 | +| 提示找不到凭证 | 按上面三种来源提供其一。 | +| 每个端点都 HTTP 401 | key 无效或已过期。重新登录,或在 ZCode provider 设置里重新填。 | +| 数字看着不新 | 窗口重置时间一直在走。重新跑一次命令,别复用旧读数。 | +| 请求被限流(429) | 看哪个窗口报告 `exceeded`,然后等重置、买额外额度、或升级套餐。 | + +## 参与贡献 + +问题与 PR:。 + +与 Command Code 官方无关。本插件通过官方 CLI 使用的同一批端点读取你自己账号的用量。 + +## 许可证 + +[MIT](https://github.com/Jovan1666/zcode-command-code-usage/blob/main/LICENSE) diff --git a/marketplace.json b/marketplace.json index 347ea0d..0db4377 100644 --- a/marketplace.json +++ b/marketplace.json @@ -3,33 +3,44 @@ "plugins": [ { "name": "command-code-usage", - "source": "./command-code-usage", - "version": "1.1.0", - "description": "Check your Command Code plan usage from inside your coding agent: 5-hour and weekly rolling windows, monthly credits or balance, plus a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", "displayName": "Command Code Usage", "displayName_i18n": { + "en": "Command Code Usage", "zh-CN": "Command Code 额度面板" }, + "source": "./command-code-usage", + "description": "See your Command Code plan usage — 5-hour and weekly rolling windows, monthly credits or balance — right inside the conversation, with a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", "description_i18n": { - "zh-CN": "在 ZCode 对话里直接查看 Command Code 套餐用量(5 小时与每周滚动窗口、月度额度或余额),含剩余次数估算与超限预警。需要 Command Code 套餐。" + "en": "See your Command Code plan usage — 5-hour and weekly rolling windows, monthly credits or balance — right inside the conversation, with a remaining-requests estimate and a burn-rate warning. Requires a Command Code plan.", + "zh-CN": "在对话里直接查看 Command Code 套餐用量——5 小时与每周滚动窗口、月度额度或余额,含剩余次数估算与超限预警。需要 Command Code 套餐。" }, - "category": "utilities", - "homepage": "https://github.com/Jovan1666/zcode-command-code-usage", + "version": "1.1.0", "author": { "name": "Jovan1666", "url": "https://github.com/Jovan1666" }, - "examplePrompts": [ - "/quota", - "How much Command Code quota do I have left, and is it enough to finish this task?" + "homepage": "https://github.com/Jovan1666/zcode-command-code-usage", + "repository": "https://github.com/Jovan1666/zcode-command-code-usage", + "license": "MIT", + "keywords": [ + "quota", + "usage", + "command-code", + "billing", + "credits", + "rate-limit", + "limits", + "monitoring", + "tokens" ], - "examplePrompts_i18n": { - "zh-CN": [ - "/quota", - "我 Command Code 额度还剩多少?够不够把这个任务跑完?" - ] - } + "category": "utilities", + "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/command-code-usage/icon.png", + "requiresPaidPlan": true } ], - "description": "See your Command Code plan usage (5-hour and weekly rolling windows, monthly credits or balance) from inside your coding agent, with a remaining-requests estimate. Requires a Command Code plan." + "description": "Command Code plan usage panel for ZCode: rolling windows, credits, estimates.", + "description_i18n": { + "en": "Command Code plan usage panel for ZCode: rolling windows, credits, estimates.", + "zh-CN": "ZCode 用的 Command Code 套餐额度面板:滚动窗口、额度与次数估算。" + } } diff --git a/scripts/check.mjs b/scripts/check.mjs index 432ebe4..496bfe7 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -117,6 +117,8 @@ const zcodeEntryAllowed = new Set([ 'name', 'source', 'version', 'description', 'displayName', 'displayName_i18n', 'description_i18n', 'icon', 'category', 'homepage', 'privacyPolicy', 'termsOfService', 'heroImage', 'author', 'examplePrompts', 'examplePrompts_i18n', 'requiresPaidPlan', + // 官方 zai-org/zcode-plugins 也在用的字段 + 'keywords', 'license', 'repository', ]); const unknownZ = Object.keys(zMarket.plugins[0]).filter((k) => !zcodeEntryAllowed.has(k)); ok(unknownZ.length === 0, 'ZCode 市场条目字段均受支持', unknownZ.join(', ')); diff --git a/scripts/make-icon.mjs b/scripts/make-icon.mjs new file mode 100644 index 0000000..b77a308 --- /dev/null +++ b/scripts/make-icon.mjs @@ -0,0 +1,193 @@ +#!/usr/bin/env node +/** + * 生成插件图标 —— assets/command-code-usage/icon.png + * + * node scripts/make-icon.mjs + * + * 为什么不用图像生成模型:这就是一块纯几何图形(深色圆角底 + 三条渐变进度条), + * 代码画出来的结果确定、可复现、随仓库走,也不必依赖外部服务或 API key。 + * + * 画面内容对应该插件本身的形态:深色底 + 三条不同长度的进度条, + * 即面板里的 5 小时窗口 / 每周窗口 / 月度额度。 + * + * 实现:4 倍超采样 + 有符号距离场求覆盖率,再盒式降采样,得到抗锯齿边缘。 + * 只用 Node 内置模块(zlib 做 PNG 的 deflate)。 + */ + +import zlib from 'node:zlib'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const OUT = path.join(ROOT, 'assets', 'command-code-usage', 'icon.png'); + +const SIZE = 256; +const SS = 4; // 超采样倍数 +const W = SIZE * SS; + +/* ------------------------------------------------------------------ 绘图参数 */ + +const BG = [0x0b, 0x0d, 0x12]; // #0b0d12 +const BORDER = [0x2a, 0x30, 0x40]; // 细边,避免深色图标在深色界面上糊成一片 +const TRACK = [0x23, 0x28, 0x37]; // #232837 +const GRAD_FROM = [0x34, 0xd3, 0x99]; // #34d399 低用量 +const GRAD_TO = [0xa7, 0x8b, 0xfa]; // #a78bfa 高用量 +const LEVELS = [0.72, 0.44, 0.24]; // 与面板的三个进度条对应 + +const TILE = { x: 6, y: 6, w: 244, h: 244, r: 56 }; +const BAR = { x: 40, w: 176, h: 28, r: 14, gap: 18 }; + +/* ------------------------------------------------------------ 距离场与采样 */ + +// 圆角矩形有符号距离:<0 在内部,>0 在外部 +function sdRoundRect(px, py, cx, cy, hw, hh, r) { + const qx = Math.abs(px - cx) - (hw - r); + const qy = Math.abs(py - cy) - (hh - r); + const ax = Math.max(qx, 0); + const ay = Math.max(qy, 0); + return Math.hypot(ax, ay) + Math.min(Math.max(qx, qy), 0) - r; +} + +// 把距离换算成覆盖率(1px 过渡带),用于超采样下的平滑边缘 +function coverage(d) { + return Math.min(Math.max(0.5 - d, 0), 1); +} + +const lerp = (a, b, t) => a + (b - a) * t; +const mix = (c1, c2, t) => [lerp(c1[0], c2[0], t), lerp(c1[1], c2[1], t), lerp(c1[2], c2[2], t)]; + +/* -------------------------------------------------------------- 渲染(超采样) */ + +const buf = new Float32Array(W * W * 4); // RGBA,线性 0..1 + +function setPx(x, y, color, alpha) { + if (alpha <= 0) return; + const i = (y * W + x) * 4; + const a = Math.min(alpha, 1); + buf[i] = buf[i] * (1 - a) + (color[0] / 255) * a; + buf[i + 1] = buf[i + 1] * (1 - a) + (color[1] / 255) * a; + buf[i + 2] = buf[i + 2] * (1 - a) + (color[2] / 255) * a; + buf[i + 3] = buf[i + 3] * (1 - a) + a; +} + +// 三条进度条的几何位置(居中) +const bars = LEVELS.map((level, i) => { + const totalH = BAR.h * LEVELS.length + BAR.gap * (LEVELS.length - 1); + const top = (SIZE - totalH) / 2; + const y = top + i * (BAR.h + BAR.gap); + return { level, cy: y + BAR.h / 2 }; +}); + +for (let py = 0; py < W; py++) { + for (let px = 0; px < W; px++) { + const x = (px + 0.5) / SS; + const y = (py + 0.5) / SS; + + // 底:圆角方块 + const dTile = sdRoundRect(x, y, TILE.x + TILE.w / 2, TILE.y + TILE.h / 2, TILE.w / 2, TILE.h / 2, TILE.r); + setPx(px, py, BG, coverage(dTile)); + + // 描边:外缘附近一圈 + const borderBand = coverage(dTile) * (1 - coverage(dTile - 1.2)); + setPx(px, py, BORDER, borderBand * 0.85); + + // 三条进度条 + for (const bar of bars) { + const cx = BAR.x + BAR.w / 2; + const dTrack = sdRoundRect(x, y, cx, bar.cy, BAR.w / 2, BAR.h / 2, BAR.r); + const covTrack = coverage(dTrack); + if (covTrack <= 0) continue; + setPx(px, py, TRACK, covTrack); + + const fillW = Math.max(BAR.h, BAR.w * bar.level); + const dFill = sdRoundRect(x, y, BAR.x + fillW / 2, bar.cy, fillW / 2, BAR.h / 2, BAR.r); + const covFill = coverage(dFill); + if (covFill <= 0) continue; + + // 渐变按轨道整体位置取样:短条停在绿色端,长条延伸到紫色端 + const t = Math.min(Math.max((x - BAR.x) / BAR.w, 0), 1); + setPx(px, py, mix(GRAD_FROM, GRAD_TO, t), covFill); + } + } +} + +/* ------------------------------------------------------------- 降采样 + 编码 */ + +const out = Buffer.alloc(SIZE * SIZE * 4); +for (let y = 0; y < SIZE; y++) { + for (let x = 0; x < SIZE; x++) { + let r = 0, g = 0, b = 0, a = 0; + for (let dy = 0; dy < SS; dy++) { + for (let dx = 0; dx < SS; dx++) { + const i = ((y * SS + dy) * W + (x * SS + dx)) * 4; + const sa = buf[i + 3]; + // 按 alpha 加权,避免边缘出现暗边 + r += buf[i] * sa; + g += buf[i + 1] * sa; + b += buf[i + 2] * sa; + a += sa; + } + } + const n = SS * SS; + const o = (y * SIZE + x) * 4; + if (a > 0) { + out[o] = Math.round(Math.min(r / a, 1) * 255); + out[o + 1] = Math.round(Math.min(g / a, 1) * 255); + out[o + 2] = Math.round(Math.min(b / a, 1) * 255); + } + out[o + 3] = Math.round((a / n) * 255); + } +} + +function crc32(buf) { + let c; + const table = crc32.table ?? (crc32.table = (() => { + const t = new Int32Array(256); + for (let n = 0; n < 256; n++) { + c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + t[n] = c; + } + return t; + })()); + let crc = -1; + for (let i = 0; i < buf.length; i++) crc = (crc >>> 8) ^ table[(crc ^ buf[i]) & 0xff]; + return (crc ^ -1) >>> 0; +} + +function chunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length); + const body = Buffer.concat([Buffer.from(type, 'ascii'), data]); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(body)); + return Buffer.concat([len, body, crc]); +} + +const ihdr = Buffer.alloc(13); +ihdr.writeUInt32BE(SIZE, 0); +ihdr.writeUInt32BE(SIZE, 4); +ihdr[8] = 8; // bit depth +ihdr[9] = 6; // colour type: RGBA +ihdr[10] = 0; // deflate +ihdr[11] = 0; // adaptive filtering +ihdr[12] = 0; // no interlace + +// 每行前加一个 filter 字节 0 +const raw = Buffer.alloc(SIZE * (SIZE * 4 + 1)); +for (let y = 0; y < SIZE; y++) { + raw[y * (SIZE * 4 + 1)] = 0; + out.copy(raw, y * (SIZE * 4 + 1) + 1, y * SIZE * 4, (y + 1) * SIZE * 4); +} + +const png = Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + chunk('IHDR', ihdr), + chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), + chunk('IEND', Buffer.alloc(0)), +]); + +fs.mkdirSync(path.dirname(OUT), { recursive: true }); +fs.writeFileSync(OUT, png); +console.log(`已写入 ${path.relative(ROOT, OUT)} ${SIZE}x${SIZE} ${(png.length / 1024).toFixed(1)} KB`);