From b34e80ebf7b9bb623e103389fe1bf82c5ee7b863 Mon Sep 17 00:00:00 2001
From: Hylouis233 <>
Date: Sat, 17 Oct 2026 03:43:16 +0800
Subject: [PATCH 1/2] =?UTF-8?q?Add=20=E8=B9=AC=E7=8E=8B=E4=B9=8B=E7=8E=8B?=
=?UTF-8?q?=20/=20Expiry=20Sentinel:=20spend=20whichever=20credit=20expire?=
=?UTF-8?q?s=20first?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A Mini App that tracks MiniMax Code credit and reset card expiry, then
automatically spends whichever lapses first so nothing expires unused.
- Dashboard shows expiry alerts, 5-hour quota, credit balance, next expiry
- Decision rule compares the earliest-expiring credit against the
earliest-expiring reset card; a parity test keeps the Node service and
the Rust daemon from drifting apart
- Optional Rust daemon keeps checking via launchd after the page is closed
- README in English and Simplified Chinese, with screenshots from a live run
---
README.md | 1 +
README.zh-CN.md | 1 +
.../.minimax-plugin/plugin.json | 18 +
plugins/hylouis233/expiry-sentinel/LICENSE | 21 +
plugins/hylouis233/expiry-sentinel/README.md | 143 ++
.../expiry-sentinel/README.zh-CN.md | 125 ++
.../expiry-sentinel/daemon/Cargo.lock | 833 +++++++++++
.../expiry-sentinel/daemon/Cargo.toml | 17 +
.../expiry-sentinel/daemon/src/api.rs | 313 ++++
.../expiry-sentinel/daemon/src/decision.rs | 408 ++++++
.../expiry-sentinel/daemon/src/main.rs | 491 +++++++
.../expiry-sentinel/daemon/src/state.rs | 95 ++
.../expiry-sentinel/docs/preview-cards.png | Bin 0 -> 126629 bytes
.../expiry-sentinel/docs/preview.png | Bin 0 -> 173638 bytes
plugins/hylouis233/expiry-sentinel/icon.png | Bin 0 -> 283847 bytes
.../expiry-sentinel/miniapp/client/index.html | 934 ++++++++++++
.../expiry-sentinel/miniapp/miniapp.json | 20 +
.../expiry-sentinel/miniapp/node/server.mjs | 1256 +++++++++++++++++
.../hylouis233/expiry-sentinel/package.json | 6 +
19 files changed, 4682 insertions(+)
create mode 100644 plugins/hylouis233/expiry-sentinel/.minimax-plugin/plugin.json
create mode 100644 plugins/hylouis233/expiry-sentinel/LICENSE
create mode 100644 plugins/hylouis233/expiry-sentinel/README.md
create mode 100644 plugins/hylouis233/expiry-sentinel/README.zh-CN.md
create mode 100644 plugins/hylouis233/expiry-sentinel/daemon/Cargo.lock
create mode 100644 plugins/hylouis233/expiry-sentinel/daemon/Cargo.toml
create mode 100644 plugins/hylouis233/expiry-sentinel/daemon/src/api.rs
create mode 100644 plugins/hylouis233/expiry-sentinel/daemon/src/decision.rs
create mode 100644 plugins/hylouis233/expiry-sentinel/daemon/src/main.rs
create mode 100644 plugins/hylouis233/expiry-sentinel/daemon/src/state.rs
create mode 100644 plugins/hylouis233/expiry-sentinel/docs/preview-cards.png
create mode 100644 plugins/hylouis233/expiry-sentinel/docs/preview.png
create mode 100644 plugins/hylouis233/expiry-sentinel/icon.png
create mode 100644 plugins/hylouis233/expiry-sentinel/miniapp/client/index.html
create mode 100644 plugins/hylouis233/expiry-sentinel/miniapp/miniapp.json
create mode 100644 plugins/hylouis233/expiry-sentinel/miniapp/node/server.mjs
create mode 100644 plugins/hylouis233/expiry-sentinel/package.json
diff --git a/README.md b/README.md
index bb3c574..75bd8a9 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,7 @@ The MiniMax Code Agent communicates through the host MCP client. Business reques
| MiniApp | What it does | Author |
| --- | --- | --- |
+| [蹬王之王 / Expiry Sentinel](plugins/hylouis233/expiry-sentinel/) | Track MiniMax Code credit and reset card expiry, then automatically spend whichever lapses first | [hylouis233](https://github.com/hylouis233) |
| [Token Usage Board](plugins/amszuidas/mcode-token-usage-board/) | Explore local Token usage by date, model, and session, including input, output, and cache usage | [amszuidas](https://github.com/amszuidas) |
| [Token Usage Board](plugins/yanhy2000/mcode-usage-monitor/) | Watch local Token usage, output speed, and cache hit rate in near real time; filter by time range, model, and session | [yanhy2000](https://github.com/yanhy2000) |
| [Model Manager](plugins/ocoomber/openrouter-model-manager/) | Browse, search, and enable/disable models in your `~/.minimax/config.yaml` with instant save, bulk actions, one-click undo, and automatic backups | [ocoomber](https://github.com/ocoomber) |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 241b298..c0f493a 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -60,6 +60,7 @@ MiniMax Code Agent 通过宿主 MCP 客户端与 MiniApp 协作。业务请求
| MiniApp | 功能 | 作者 |
| --- | --- | --- |
+| [蹬王之王](plugins/hylouis233/expiry-sentinel/README.zh-CN.md) | 追踪 MiniMax Code 积分与重置卡的到期时间,自动按「先过期先用」消耗 | [hylouis233](https://github.com/hylouis233) |
| [Token 用量看板](plugins/amszuidas/mcode-token-usage-board/README.zh-CN.md) | 按时间、模型和会话查看本机 Token 用量,包含输入、输出和缓存用量 | [amszuidas](https://github.com/amszuidas) |
| [Token 用量看板](plugins/yanhy2000/mcode-usage-monitor/README.zh-CN.md) | 近实时查看本机 Token 用量、输出速度与缓存命中率,可按时间范围、模型和会话筛选 | [yanhy2000](https://github.com/yanhy2000) |
| [模型管理器](plugins/ocoomber/openrouter-model-manager/README.zh-CN.md) | 浏览、搜索并启用/停用 `~/.minimax/config.yaml` 中的模型,支持即时保存、批量操作、一键撤销和自动备份 | [ocoomber](https://github.com/ocoomber) |
diff --git a/plugins/hylouis233/expiry-sentinel/.minimax-plugin/plugin.json b/plugins/hylouis233/expiry-sentinel/.minimax-plugin/plugin.json
new file mode 100644
index 0000000..d11db59
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/.minimax-plugin/plugin.json
@@ -0,0 +1,18 @@
+{
+ "schemaVersion": 1,
+ "name": "expiry-sentinel",
+ "displayName": "蹬王之王",
+ "version": "1.0.0",
+ "description": "实时追踪 MiniMax Code 积分与重置卡的到期时间,自动按「先过期先用」决定用卡还是留积分,榨干最后一点算力。",
+ "author": "hylouis233",
+ "icon": "icon.png",
+ "category": "Productivity",
+ "exampleQueries": [
+ "打开蹬王之王",
+ "看看我的积分和重置卡什么时候过期",
+ "我的额度快用完了,该用重置卡还是留着?"
+ ],
+ "apps": [],
+ "mcpServers": [],
+ "skills": []
+}
diff --git a/plugins/hylouis233/expiry-sentinel/LICENSE b/plugins/hylouis233/expiry-sentinel/LICENSE
new file mode 100644
index 0000000..1c08e39
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Hylouis233
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/hylouis233/expiry-sentinel/README.md b/plugins/hylouis233/expiry-sentinel/README.md
new file mode 100644
index 0000000..28dfb89
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/README.md
@@ -0,0 +1,143 @@
+
+
+

+
+# 蹬王之王 · Expiry Sentinel
+
+**Track what expires first, then spend exactly that.**
+
+English | [简体中文](README.zh-CN.md)
+
+[](LICENSE)
+
+
+
+---
+
+MiniMax Code credits and reset cards both expire on a timer. Left alone, the
+shortest-dated balance quietly evaporates while you keep using the one that
+lasts longest. **蹬王之王** shows every balance with its expiry date, then spends
+whichever runs out first — so nothing is wasted and nothing expires unused.
+
+## Screenshot
+
+
+

+
+
+The header answers the only question that matters: *how much is about to expire?*
+Below it, the 5-hour quota, total credit balance, the next expiry, and how many
+reset cards are left.
+
+
+

+
+
+Credit detail, reset cards by expiry, and platform credit pools each get their own
+table, sorted by the date they lapse.
+
+## How the decision works
+
+When the 5-hour or weekly quota runs out, requests start spending credits. That is
+the moment to act, and the app compares two dates:
+
+| Situation | Decision | Why |
+| --- | --- | --- |
+| Reset card expires **before** the oldest credit | **Spend the card** | Restores the quota, so the longer-lived credit is preserved |
+| Credit expires **before** the card | **Keep the card** | Let the credit lapse first; the card still has value later |
+| Quota not exhausted | **Do nothing** | No credits are being burned yet |
+
+Both the Node service and the background daemon implement this rule, and a
+parity test compares them character by character so the two cannot drift apart.
+
+## Install
+
+Copy this entire directory — including the hidden `.minimax-plugin` directory —
+into your MiniMax Code data directory. By default that is `.minimax` in your home
+folder, so the default path is:
+
+```text
+/plugins/expiry-sentinel/
+```
+
+If you configured a different data directory, use that instead. Restart MiniMax
+Code, confirm the plugin is recognized and enabled, then open **蹬王之王**, or ask
+the Agent to open it.
+
+## Use it
+
+**Watch only (default).** Open the board and read it. Nothing is spent.
+
+**Auto-spend inside the page.** Turn on 自动执行 and pick an interval (seconds or
+minutes). The page decides and spends on that interval while it stays open.
+「立即判定一次」 always triggers one check immediately, with no throttle.
+
+**Auto-spend around the clock.** Turn on 后台服务. This installs a launchd agent
+that keeps running with the page closed, and starts at login on macOS. The page
+shows its PID and lets you stop it.
+
+Both paths share one decision rule, and the server deduplicates by idempotency
+key, so a card is never spent twice even if both are active.
+
+## Background daemon (optional)
+
+The `daemon/` directory contains a Rust rewrite of the same decision logic,
+built for low overhead while it runs unattended:
+
+```bash
+cd daemon
+cargo build --release
+```
+
+It needs a token. It obtains one through the desktop app's OAuth lease socket
+(`~/.minimax/run/mcode-auth-lease-v1.sock`), so it must run while MiniMax Code is
+running. Set `MCODE_TOKEN` to use a fixed token instead.
+
+Typical resident memory is under 1 MB, and it polls one endpoint per cycle and
+caches the rest by how often each changes.
+
+## Tested environment
+
+- MiniMax Code desktop app on macOS (Apple Silicon)
+- Verified against a live account: quota polling, credit detail, card listing,
+ card spending, and the auto-spend path that fired on quota exhaustion
+- Cross-platform behaviour and other client versions are **unconfirmed** — the
+ daemon path uses launchd and has only been exercised on macOS
+- The endpoints are private APIs. They may change without notice, and the app
+ will report a parse or read error rather than invent data when they do
+
+## Data & access
+
+**Files read.** The desktop app's OAuth lease capability file
+(`~/.minimax/run/mcode-auth-lease-v1.cap`) and the lease socket beside it, to
+obtain the same session the desktop app uses. These paths are an internal
+implementation detail of the client, not a documented interface; the background
+daemon will stop working if they change.
+
+**Files written.**
+
+- `~/.config/mcode-reset-card/token` — only when you provide `MCODE_TOKEN`
+- `~/.config/mcode-reset-card/bg-state.json` — the daemon's record of which cards
+ it already attempted, so a restart cannot spend the same card twice
+- `~/Library/LaunchAgents/com.local.mcode-reset-card.plist` — written only when you
+ enable 后台服务
+
+**Network hosts.** Two, both MiniMax:
+
+- `https://www.minimaxi.com` — quota, platform credit pools, reset cards
+- `https://agent.minimaxi.com` — MiniMax Code credit detail, membership summary
+
+**Processes spawned.** `launchctl load` / `unload` for the background service,
+and `launchctl list` to read its status. Nothing else.
+
+**Agent turns.** None. The app never invokes the Agent.
+
+**Secrets.** The session token lives only in the Node process and the daemon.
+It is never placed in HTML, client JavaScript, logs, or API responses.
+
+**Spending is a real write.** Enabling auto-spend spends real reset cards on your
+account. Start with the dashboard in watch-only mode.
+
+## License
+
+MIT — see [LICENSE](LICENSE).
diff --git a/plugins/hylouis233/expiry-sentinel/README.zh-CN.md b/plugins/hylouis233/expiry-sentinel/README.zh-CN.md
new file mode 100644
index 0000000..5149232
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/README.zh-CN.md
@@ -0,0 +1,125 @@
+
+
+

+
+# 蹬王之王 · Expiry Sentinel
+
+**看清谁先过期,就先用哪一个。**
+
+[English](README.md) | 简体中文
+
+[](LICENSE)
+
+
+
+---
+
+MiniMax Code 的积分和重置卡都会到期。放着不管,最快到期的那笔会悄悄作废,
+而你一直在用最耐放的那笔。**蹬王之王**把每笔余额和到期日摊开给你看,然后
+「先过期先用」——不浪费,也不过期。
+
+## 界面
+
+
+

+
+
+顶部只回答一个最要紧的问题:**有多少快要过期了**。下面是 5 小时限额、积分总余额、
+最早到期日和剩余重置卡数量。
+
+
+

+
+
+积分明细、重置卡、平台积分池各一张表,全部按到期日排序。
+
+## 决策规则
+
+5 小时或周限额耗尽时,请求开始扣积分——这就是该动手的时刻。应用会比两个日期:
+
+| 情况 | 决策 | 原因 |
+| --- | --- | --- |
+| 重置卡比最早到期的积分**更早**过期 | **用卡** | 重置额度,保住更耐放的积分 |
+| 积分比重置卡**更早**过期 | **留卡** | 让积分先消耗掉,卡留到以后更有价值 |
+| 额度未耗尽 | **不动** | 还没在烧积分 |
+
+Node 服务和后台守护进程实现同一套规则,并有逐字比对的一致性校验,防止两边漂移。
+
+## 安装
+
+把整个目录(含隐藏的 `.minimax-plugin` 目录)复制到 MiniMax Code 的数据目录。
+默认为用户目录下的 `.minimax`,即:
+
+```text
+/plugins/expiry-sentinel/
+```
+
+如果你配置了其他数据目录,请用那个目录。重启 MiniMax Code,确认插件已被识别并启用,
+然后打开 **蹬王之王**,也可以直接让 Agent 帮你打开。
+
+## 使用
+
+**只看不动(默认)。** 打开看板读数据,不消耗任何东西。
+
+**页面内自动执行。** 打开「自动执行」并设置间隔(秒或分钟)。页面开着时按间隔
+判定并执行。「立即判定一次」可随时手动触发,无节流。
+
+**全天候后台执行。** 打开「后台服务」。它会安装一个 launchd 代理,关掉页面也在跑,
+macOS 上开机自启。页面上能看到它的 PID,也可以随时停掉。
+
+两条路径共用同一套决策规则,服务端按幂等键去重——即使同时开着,卡也不会被扣两次。
+
+## 后台守护进程(可选)
+
+`daemon/` 里是同一套决策逻辑的 Rust 版本,为长时间无人值守而写:
+
+```bash
+cd daemon
+cargo build --release
+```
+
+它需要 token:通过桌面端的 OAuth lease socket
+(`~/.minimax/run/mcode-auth-lease-v1.sock`)获取,也就是说 MiniMax Code 需要在运行。
+也可以设置 `MCODE_TOKEN` 使用固定凭据。
+
+实测常驻内存不到 1 MB,每轮只请求一个接口,其余按各自变化频率缓存。
+
+## 测试环境
+
+- macOS(Apple Silicon)上的 MiniMax Code 桌面端
+- 已在真实账号验证:额度轮询、积分明细、卡列表、扣卡,以及额度耗尽时触发的自动执行
+- **未验证**:跨平台表现与其他客户端版本——守护进程走 launchd,目前只在 macOS 上跑过
+- 所用接口是私有接口,可能随时变化;接口变化时应用会如实报错,不会编造数据
+
+## 数据与访问
+
+**读取的文件。** 桌面端的 OAuth lease capability 文件
+(`~/.minimax/run/mcode-auth-lease-v1.cap`)及其同目录的 lease socket,
+用于获取与桌面端相同的登录态。这些路径是客户端内部实现细节,并非公开接口;
+一旦变化,后台守护进程会失效。
+
+**写入的文件。**
+
+- `~/.config/mcode-reset-card/token` —— 仅当你提供 `MCODE_TOKEN` 时
+- `~/.config/mcode-reset-card/bg-state.json` —— 守护进程记录已尝试过的卡,
+ 避免重启后重复扣同一张
+- `~/Library/LaunchAgents/com.local.mcode-reset-card.plist` —— 仅在启用「后台服务」时写入
+
+**网络地址。** 两个,均为 MiniMax:
+
+- `https://www.minimaxi.com` —— 额度、平台积分池、重置卡
+- `https://agent.minimaxi.com` —— MiniMax Code 积分明细、会员汇总
+
+**启动的进程。** 仅 `launchctl load` / `unload`(后台服务)与 `launchctl list`(读状态)。
+
+**Agent 调用。** 无。本应用从不调用 Agent。
+
+**凭据。** 会话 token 只存在于 Node 进程和守护进程内存中,绝不进入 HTML、
+客户端 JavaScript、日志或 API 响应。
+
+**扣卡是真实写操作。** 开启自动执行会在你的账号上真实消耗重置卡。
+建议先用只读模式熟悉一下。
+
+## 许可证
+
+MIT —— 见 [LICENSE](LICENSE)。
diff --git a/plugins/hylouis233/expiry-sentinel/daemon/Cargo.lock b/plugins/hylouis233/expiry-sentinel/daemon/Cargo.lock
new file mode 100644
index 0000000..5c0dc4d
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/daemon/Cargo.lock
@@ -0,0 +1,833 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "cookie"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "libc"
+version = "0.2.190"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "litrs"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "reset-card-daemon"
+version = "1.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "ureq",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "untrusted",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "smallvec"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
+dependencies = [
+ "base64",
+ "cookie_store",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/plugins/hylouis233/expiry-sentinel/daemon/Cargo.toml b/plugins/hylouis233/expiry-sentinel/daemon/Cargo.toml
new file mode 100644
index 0000000..73b8afd
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/daemon/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "reset-card-daemon"
+version = "1.0.0"
+edition = "2024"
+
+[dependencies]
+# HTTP 客户端:手写 TLS 容易出错且难维护,用成熟实现更可靠
+ureq = { version = "3", default-features = false, features = ["rustls", "json", "gzip"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+[profile.release]
+opt-level = "z" # 体积优先:后台常驻服务不需要极致速度
+lto = true
+codegen-units = 1
+panic = "abort"
+strip = true
diff --git a/plugins/hylouis233/expiry-sentinel/daemon/src/api.rs b/plugins/hylouis233/expiry-sentinel/daemon/src/api.rs
new file mode 100644
index 0000000..b9e7921
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/daemon/src/api.rs
@@ -0,0 +1,313 @@
+//! 账号 API 客户端 + OAuth lease token。
+//!
+//! 域名划分是实测结论,不是猜的:
+//! 开放平台 / 额度 / 重置卡 → www.minimaxi.com(agent 域对这些路径 404 或返回 HTML)
+//! Code 积分 / 会员信息 → agent.minimaxi.com(platform 域返回非 JSON)
+
+use std::io::{Read, Write};
+use std::os::unix::net::UnixStream;
+use std::path::PathBuf;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+pub const BASE_CN: &str = "https://www.minimaxi.com";
+pub const BASE_GLOBAL: &str = "https://platform.minimax.io";
+pub const CODE_BASE: &str = "https://agent.minimaxi.com";
+
+const LEASE_SOCK: &str = ".minimax/run/mcode-auth-lease-v1.sock";
+const LEASE_CAP: &str = ".minimax/run/mcode-auth-lease-v1.cap";
+
+const TOKEN_TTL_MS: u64 = 30 * 60 * 1000;
+const TOKEN_REFRESH_MARGIN_MS: u64 = 5 * 60 * 1000;
+
+pub fn now_ms() -> i64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|d| d.as_millis() as i64)
+ .unwrap_or(0)
+}
+
+#[derive(Debug)]
+pub enum ApiError {
+ Unauthorized(String),
+ Network(String),
+ Business(String),
+}
+
+impl ApiError {
+ pub fn message(&self) -> &str {
+ match self {
+ ApiError::Unauthorized(m) | ApiError::Network(m) | ApiError::Business(m) => m,
+ }
+ }
+ pub fn is_unauthorized(&self) -> bool {
+ matches!(self, ApiError::Unauthorized(_))
+ }
+}
+
+impl std::fmt::Display for ApiError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.message())
+ }
+}
+
+fn home() -> PathBuf {
+ PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/".into()))
+}
+
+fn random_hex(nbytes: usize) -> String {
+ let mut buf = vec![0u8; nbytes];
+ if std::fs::File::open("/dev/urandom")
+ .and_then(|mut f| f.read_exact(&mut buf))
+ .is_err()
+ {
+ buf[..8].copy_from_slice(&now_ms().to_be_bytes());
+ }
+ buf.iter().map(|b| format!("{b:02x}")).collect()
+}
+
+/// 向桌面端的 lease socket 取一个短期 access token。
+/// 协议:4 字节大端长度前缀 + JSON。详见 packages/oauth-lease-protocol。
+pub fn acquire_lease_token() -> Option {
+ let capability = std::fs::read_to_string(home().join(LEASE_CAP))
+ .ok()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())?;
+ let sock_path = home().join(LEASE_SOCK);
+ if !sock_path.exists() {
+ return None;
+ }
+
+ let payload = serde_json::json!({
+ "version": 1,
+ "requestId": random_hex(16),
+ "capability": capability,
+ "method": "lease",
+ "minValidityMs": 120_000i64,
+ })
+ .to_string();
+
+ let body = payload.as_bytes();
+ let mut frame = Vec::with_capacity(4 + body.len());
+ frame.extend_from_slice(&(body.len() as u32).to_be_bytes());
+ frame.extend_from_slice(body);
+
+ let mut stream = UnixStream::connect(&sock_path).ok()?;
+ stream.set_read_timeout(Some(Duration::from_secs(5))).ok()?;
+ stream.set_write_timeout(Some(Duration::from_secs(5))).ok()?;
+ stream.write_all(&frame).ok()?;
+
+ let mut len_buf = [0u8; 4];
+ stream.read_exact(&mut len_buf).ok()?;
+ let size = u32::from_be_bytes(len_buf) as usize;
+ if size > 1 << 20 {
+ return None;
+ }
+ let mut buf = vec![0u8; size];
+ stream.read_exact(&mut buf).ok()?;
+
+ let resp: serde_json::Value = serde_json::from_slice(&buf).ok()?;
+ if resp.get("ok").and_then(|v| v.as_bool()) != Some(true) {
+ return None;
+ }
+ let result = resp.get("result")?;
+ if result.get("method").and_then(|v| v.as_str()) != Some("lease") {
+ return None;
+ }
+ result
+ .get("accessToken")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+}
+
+/// token 来源:环境变量 → 本地文件 → lease socket
+pub fn resolve_token() -> Option {
+ for env in ["MCODE_TOKEN", "MINIMAX_TOKEN"] {
+ if let Ok(v) = std::env::var(env) {
+ let t = v.trim().to_string();
+ if !t.is_empty() {
+ return Some(t);
+ }
+ }
+ }
+ if let Ok(t) = std::fs::read_to_string(home().join(".config/mcode-reset-card/token")) {
+ let t = t.trim().to_string();
+ if !t.is_empty() {
+ return Some(t);
+ }
+ }
+ acquire_lease_token()
+}
+
+/// ureq Agent 自带 keep-alive 连接池:后台每轮打多个 API,
+/// 复用连接省掉每轮的 TLS 握手 —— 这是常驻进程最大的一笔固定开销。
+pub struct Client {
+ agent: ureq::Agent,
+ token: String,
+ cached_at: u64,
+}
+
+impl Client {
+ pub fn new(base: &str) -> Result {
+ let token = resolve_token().ok_or_else(|| {
+ ApiError::Unauthorized(
+ "未获取到登录态,请确认 MiniMax Code 桌面端正在运行且已登录".into(),
+ )
+ })?;
+
+ let config = ureq::Agent::config_builder()
+ .timeout_global(Some(Duration::from_secs(20)))
+ .timeout_connect(Some(Duration::from_secs(5)))
+ .max_idle_connections(8)
+ .build();
+ let agent: ureq::Agent = config.into();
+
+ // 用一个明确的 base 做首连校验,确保 base 可用
+ let c = Client { agent, token, cached_at: now_ms() as u64 };
+ let _ = base;
+ Ok(c)
+ }
+
+ fn token(&mut self) -> String {
+ let age = (now_ms() as u64).saturating_sub(self.cached_at);
+ if age >= TOKEN_TTL_MS - TOKEN_REFRESH_MARGIN_MS {
+ if let Some(t) = acquire_lease_token() {
+ self.token = t;
+ self.cached_at = now_ms() as u64;
+ }
+ }
+ self.token.clone()
+ }
+
+ fn base_for(&self, code_domain: bool) -> &'static str {
+ if code_domain { CODE_BASE } else { BASE_CN }
+ }
+
+ pub fn get(&mut self, path: &str, code_domain: bool) -> Result {
+ self.do_request("GET", path, None, code_domain)
+ }
+
+ pub fn post(
+ &mut self,
+ path: &str,
+ body: &serde_json::Value,
+ code_domain: bool,
+ ) -> Result {
+ self.do_request("POST", path, Some(body), code_domain)
+ }
+
+ fn do_request(
+ &mut self,
+ method: &str,
+ path: &str,
+ body: Option<&serde_json::Value>,
+ code_domain: bool,
+ ) -> Result {
+ let base = self.base_for(code_domain);
+ let url = format!("{base}{path}");
+
+ // 鉴权失败重取 token 再试一次
+ for attempt in 0..2 {
+ let token = self.token();
+ let auth = format!("Bearer {token}");
+ let accept = "application/json";
+ let ua = "MiniMaxCode";
+
+ let result = match (method, body) {
+ ("GET", None) => self
+ .agent
+ .get(&url)
+ .header("Authorization", &auth)
+ .header("Accept", accept)
+ .header("User-Agent", ua)
+ .call(),
+ ("POST", Some(b)) => self
+ .agent
+ .post(&url)
+ .header("Authorization", &auth)
+ .header("Accept", accept)
+ .header("User-Agent", ua)
+ .send_json(b.clone()),
+ _ => return Err(ApiError::Network(format!("不支持的请求 {method}"))),
+ };
+
+ match result {
+ Ok(resp) => {
+ let text = resp
+ .into_body()
+ .read_to_string()
+ .map_err(|e| ApiError::Network(format!("读取响应体失败: {e}")))?;
+ let value: serde_json::Value =
+ serde_json::from_str(&text).map_err(json_err)?;
+ return check_business(value);
+ }
+ Err(ureq::Error::StatusCode(401)) | Err(ureq::Error::StatusCode(403))
+ if attempt == 0 =>
+ {
+ self.cached_at = 0;
+ continue;
+ }
+ Err(ureq::Error::StatusCode(code)) => {
+ return Err(ApiError::Network(format!("HTTP {code}")));
+ }
+ Err(e) => return Err(ApiError::Network(format!("{e}"))),
+ }
+ }
+ Err(ApiError::Unauthorized("登录态失效".into()))
+ }
+}
+
+fn json_err(e: serde_json::Error) -> ApiError {
+ ApiError::Network(format!("JSON 解析失败: {e}"))
+}
+
+/// 检查 `base_resp.status_code`。
+///
+/// 这是踩过的坑:`{"total":0,"base_resp":{"status_code":2013,"status_msg":"invalid params"}}`
+/// 是**错误**,不是空结果。必须在这里拦下,否则会把接口错误读成「没有记录」。
+fn check_business(value: serde_json::Value) -> Result {
+ if let Some(code) = value
+ .get("base_resp")
+ .and_then(|b| b.get("status_code"))
+ .and_then(|v| v.as_i64())
+ {
+ if code != 0 {
+ let msg = value
+ .get("base_resp")
+ .and_then(|b| b.get("status_msg"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("unknown")
+ .to_string();
+ if code == 1004 {
+ return Err(ApiError::Unauthorized(format!("{msg} (code={code})")));
+ }
+ return Err(ApiError::Business(format!("{msg} (code={code})")));
+ }
+ }
+ Ok(value)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn business_error_detected() {
+ let v = serde_json::json!({"total":0,"base_resp":{"status_code":2013,"status_msg":"invalid params"}});
+ match check_business(v) {
+ Err(ApiError::Business(m)) => assert!(m.contains("invalid params")),
+ other => panic!("应当识别为业务错误,实际 {other:?}"),
+ }
+ }
+
+ #[test]
+ fn success_passes_through() {
+ let v = serde_json::json!({"base_resp":{"status_code":0},"data":[1,2]});
+ assert!(check_business(v).is_ok());
+ }
+
+ #[test]
+ fn unauthorized_code() {
+ let v = serde_json::json!({"base_resp":{"status_code":1004,"status_msg":"not login"}});
+ assert!(matches!(check_business(v), Err(ApiError::Unauthorized(_))));
+ }
+}
diff --git a/plugins/hylouis233/expiry-sentinel/daemon/src/decision.rs b/plugins/hylouis233/expiry-sentinel/daemon/src/decision.rs
new file mode 100644
index 0000000..6c3861b
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/daemon/src/decision.rs
@@ -0,0 +1,408 @@
+//! 额度与到期判定。
+//!
+//! 这些分支与 Mini App「蹬王之王」的 `decide()` 一一对应,
+//! 由 `verify-parity.mjs` 逐字比对「动作 + 理由文本」来防止漂移。
+//! **改动任何一条理由文案,两侧都要同步改,并重跑校验器。**
+
+use serde::Deserialize;
+
+pub const QUOTA_FIVE_HOUR: i64 = 1;
+pub const QUOTA_WEEKLY: i64 = 2;
+pub const QUOTA_VIDEO: i64 = 3;
+
+/// 一个限额维度的用量。
+#[derive(Debug, Clone, Copy)]
+pub struct QuotaState {
+ pub quota_type: i64,
+ pub total: f64,
+ pub used: f64,
+ pub is_unlimited: bool,
+}
+
+impl QuotaState {
+ /// 是否已耗尽。不限量或总量为 0 都不算耗尽。
+ pub fn exhausted(&self) -> bool {
+ if self.is_unlimited || self.total <= 0.0 {
+ return false;
+ }
+ self.used >= self.total
+ }
+
+ pub fn remaining_percent(&self) -> f64 {
+ if self.is_unlimited || self.total <= 0.0 {
+ return 100.0;
+ }
+ ((self.total - self.used) / self.total * 100.0).max(0.0)
+ }
+
+ pub fn label(&self) -> &'static str {
+ match self.quota_type {
+ QUOTA_FIVE_HOUR => "5小时限额",
+ QUOTA_WEEKLY => "周限额",
+ QUOTA_VIDEO => "视频限额",
+ _ => "未知限额",
+ }
+ }
+
+ pub fn describe(&self) -> String {
+ if self.is_unlimited {
+ return format!("{}=不限量", self.label());
+ }
+ format!(
+ "{}=已用{:.0}% / 剩余{:.0}%",
+ self.label(),
+ self.used,
+ self.remaining_percent()
+ )
+ }
+}
+
+/// 金额字符串(可能带千分位)转数字。
+pub fn amount(value: &serde_json::Value) -> f64 {
+ match value {
+ serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
+ serde_json::Value::String(s) => {
+ let cleaned: String = s.chars().filter(|c| *c != ',').collect();
+ cleaned.trim().parse::().unwrap_or(0.0)
+ }
+ _ => 0.0,
+ }
+}
+
+/// 百分数字符串(如 "71%")转数字。
+///
+/// 接口返回的就是 `"71%"` 这种带百分号的字符串,直接 `parse::()` 会失败并
+/// 静默退回 0,把「已用 71%」误判成「已用 0%」。这里必须先剥掉 `%`。
+pub fn percent(value: &serde_json::Value) -> f64 {
+ match value {
+ serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
+ serde_json::Value::String(s) => s.trim().trim_end_matches('%').parse::().unwrap_or(0.0),
+ _ => 0.0,
+ }
+}
+
+#[derive(Deserialize)]
+pub struct ModelRemains {
+ pub model_name: String,
+ #[serde(default)]
+ pub current_interval_total_percent: serde_json::Value,
+ #[serde(default)]
+ pub current_interval_used_percent: serde_json::Value,
+ #[serde(default)]
+ pub current_interval_status: i64,
+ #[serde(default)]
+ pub current_interval_total_count: f64,
+ #[serde(default)]
+ pub current_interval_usage_count: f64,
+ #[serde(default)]
+ pub current_weekly_total_percent: serde_json::Value,
+ #[serde(default)]
+ pub current_weekly_used_percent: serde_json::Value,
+ #[serde(default)]
+ pub current_weekly_status: i64,
+ #[serde(default)]
+ pub end_time: Option,
+}
+
+#[derive(Deserialize)]
+pub struct RemainsPayload {
+ #[serde(default)]
+ pub model_remains: Vec,
+}
+
+pub fn parse_quotas(payload: &RemainsPayload) -> Vec {
+ let mut out = Vec::with_capacity(3);
+
+ let general = payload
+ .model_remains
+ .iter()
+ .find(|m| m.model_name == "general")
+ .or_else(|| payload.model_remains.first());
+
+ if let Some(g) = general {
+ let total = {
+ let t = percent(&g.current_interval_total_percent);
+ if t > 0.0 { t } else { 100.0 }
+ };
+ out.push(QuotaState {
+ quota_type: QUOTA_FIVE_HOUR,
+ total,
+ used: percent(&g.current_interval_used_percent),
+ is_unlimited: g.current_interval_status == 3,
+ });
+ out.push(QuotaState {
+ quota_type: QUOTA_WEEKLY,
+ total: {
+ let t = percent(&g.current_weekly_total_percent);
+ if t > 0.0 { t } else { 100.0 }
+ },
+ used: percent(&g.current_weekly_used_percent),
+ is_unlimited: g.current_weekly_status == 3,
+ });
+ }
+
+ // 视频额度:只有非 general 且计数 > 0 的模型
+ if let Some(v) = payload
+ .model_remains
+ .iter()
+ .find(|m| m.model_name != "general" && m.current_interval_total_count > 0.0)
+ {
+ out.push(QuotaState {
+ quota_type: QUOTA_VIDEO,
+ total: v.current_interval_total_count,
+ used: v.current_interval_usage_count,
+ is_unlimited: v.current_interval_status == 3,
+ });
+ }
+
+ out
+}
+
+/// 一张可用于扣卡的重置卡。
+#[derive(Debug, Clone)]
+pub struct UsableCard {
+ pub card_id: String,
+ pub source_type: String,
+ pub out_biz_no: String,
+ pub expire_time: i64,
+}
+
+/// 扣卡决策的动作。
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Action {
+ /// 用重置卡重置额度
+ UseCard,
+ /// 不用卡,让积分先消耗
+ KeepCredit,
+ /// 额度充足,不动手
+ Idle,
+ /// 既没积分也没卡,等着
+ Wait,
+}
+
+impl Action {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Action::UseCard => "use-card",
+ Action::KeepCredit => "keep-credit",
+ Action::Idle => "idle",
+ Action::Wait => "wait",
+ }
+ }
+}
+
+pub struct Decision {
+ pub action: Action,
+ pub reason: String,
+}
+
+/// 毫秒时间戳 → "10月25日"。与 Python 的 `_fmt_day` 同一时区口径。
+pub fn fmt_day(ms: Option) -> String {
+ let Some(ms) = ms else {
+ return "未知".to_string();
+ };
+ if ms == 0 {
+ return "未知".to_string();
+ }
+ // 本地时区;固定 UTC 会导致跨日错位,必须借 localtime_r
+ let (m, d) = local_md(ms.div_euclid(1000));
+ if m == 0 {
+ return "未知".to_string();
+ }
+ format!("{m}月{d}日")
+}
+
+/// 把 Unix 秒换算成本地时区的 (月, 日)。
+///
+/// 桌面端与 Python 版都用本地时区展示;这里手算避免引入 chrono 依赖。
+/// libc 的 `struct tm`,只需本地时区换算用到的字段。
+#[repr(C)]
+#[derive(Default)]
+struct Tm {
+ tm_sec: i32, tm_min: i32, tm_hour: i32, tm_mday: i32,
+ tm_mon: i32, tm_year: i32, tm_wday: i32, tm_yday: i32, tm_isdst: i32,
+ tm_gmtoff: i64, tm_zone: *const u8,
+}
+
+#[repr(C)]
+struct TimeT(i64);
+
+unsafe extern "C" {
+ fn localtime_r(timep: *const TimeT, result: *mut Tm) -> *mut Tm;
+}
+
+/// 把 Unix 秒换算成本地时区结构。
+fn secs_to_local(secs: i64) -> Option {
+ let t = TimeT(secs);
+ let mut tm = Tm::default();
+ unsafe {
+ if localtime_r(&t, &mut tm).is_null() {
+ return None;
+ }
+ }
+ Some(tm)
+}
+
+/// 本地时区的 (月, 日),供日志使用。
+pub fn local_md(secs: i64) -> (u32, u32) {
+ secs_to_local(secs)
+ .map(|t| (t.tm_mon as u32 + 1, t.tm_mday as u32))
+ .unwrap_or((0, 0))
+}
+
+/// 本地时区的 (时, 分)。
+pub fn local_hm(secs: i64) -> (u32, u32) {
+ secs_to_local(secs)
+ .map(|t| (t.tm_hour as u32, t.tm_min as u32))
+ .unwrap_or((0, 0))
+}
+
+/// 后台扣卡决策(纯函数,不触碰网络)。
+///
+/// 动作 ∈ {use-card, keep-credit, idle, wait}。
+///
+/// 分支与 Mini App 的 `decide()` 刻意同构:
+/// 未耗尽 → idle
+/// 耗尽 + 无积分 + 有卡 → use-card
+/// 耗尽 + 无积分 + 无卡 → wait
+/// 耗尽 + 有积分 + 无卡 → keep-credit
+/// 耗尽 + 都有,卡先过期 → use-card
+/// 耗尽 + 都有,积分先过期 → keep-credit
+pub fn decide(
+ quotas: &[QuotaState],
+ usable_cards: &[UsableCard],
+ credit_expiry_ms: Option,
+ credit_desc: &str,
+) -> Decision {
+ let five = quotas.iter().find(|q| q.quota_type == QUOTA_FIVE_HOUR);
+ let weekly = quotas.iter().find(|q| q.quota_type == QUOTA_WEEKLY);
+
+ let five_exhausted = five.is_some_and(|q| q.exhausted());
+ let weekly_exhausted = weekly.is_some_and(|q| q.exhausted());
+
+ // ── 未耗尽 ──
+ if !five_exhausted && !weekly_exhausted {
+ return Decision {
+ action: Action::Idle,
+ reason: "额度充足,暂不需要处理".to_string(),
+ };
+ }
+
+ // ── 无可用积分:只能靠卡 ──
+ let Some(credit_expiry) = credit_expiry_ms else {
+ if !usable_cards.is_empty() {
+ return Decision {
+ action: Action::UseCard,
+ reason: "没有可用积分,使用重置卡重置额度".to_string(),
+ };
+ }
+ return Decision {
+ action: Action::Wait,
+ reason: "额度已耗尽,既没有积分也没有可用的重置卡".to_string(),
+ };
+ };
+
+ // ── 有积分但没卡:让积分先消耗 ──
+ // 注意这一句带「到期」二字,与 Mini App / Python 的文案保持逐字一致。
+ // 改文案前务必跑 `node verify-parity.mjs`。
+ if usable_cards.is_empty() {
+ return Decision {
+ action: Action::KeepCredit,
+ reason: format!(
+ "没有可用的重置卡,让{credit_desc}({}到期)先消耗",
+ fmt_day(Some(credit_expiry))
+ ),
+ };
+ }
+
+ let card_expiry = usable_cards[0].expire_time;
+
+ // ── 两者都有:比「先过期先用」 ──
+ if card_expiry < credit_expiry {
+ return Decision {
+ action: Action::UseCard,
+ reason: format!(
+ "重置卡({})比{credit_desc}({})先过期,先用卡保住积分",
+ fmt_day(Some(card_expiry)),
+ fmt_day(Some(credit_expiry))
+ ),
+ };
+ }
+ Decision {
+ action: Action::KeepCredit,
+ reason: format!(
+ "{credit_desc}({})比重置卡({})先过期,不用卡,让积分先消耗",
+ fmt_day(Some(credit_expiry)),
+ fmt_day(Some(card_expiry))
+ ),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn q(t: i64, total: f64, used: f64, unlimited: bool) -> QuotaState {
+ QuotaState { quota_type: t, total, used, is_unlimited: unlimited }
+ }
+
+ fn c(days: i64) -> UsableCard {
+ UsableCard {
+ card_id: "C1".into(),
+ source_type: "mcode_signin".into(),
+ out_biz_no: "b1".into(),
+ expire_time: 1_700_000_000_000 + days * 86_400_000,
+ }
+ }
+
+ #[test]
+ fn percent_strings() {
+ assert_eq!(percent(&serde_json::json!("71%")), 71.0);
+ assert_eq!(percent(&serde_json::json!("100%")), 100.0);
+ assert_eq!(percent(&serde_json::json!(0)), 0.0);
+ assert_eq!(percent(&serde_json::json!("")), 0.0);
+ }
+
+ #[test]
+ fn amount_strings() {
+ assert_eq!(amount(&serde_json::json!("17400.00")), 17400.0);
+ assert_eq!(amount(&serde_json::json!("1,200.50")), 1200.5);
+ }
+
+ #[test]
+ fn exhausted_rules() {
+ assert!(!q(1, 100.0, 99.0, false).exhausted());
+ assert!(q(1, 100.0, 100.0, false).exhausted());
+ assert!(!q(1, 100.0, 100.0, true).exhausted(), "不限量不算耗尽");
+ assert!(!q(1, 0.0, 0.0, false).exhausted(), "总量为 0 不算耗尽");
+ }
+
+ #[test]
+ fn decide_branches() {
+ let five_ok = vec![q(1, 100.0, 50.0, false), q(2, 100.0, 0.0, true)];
+ let five_dead = vec![q(1, 100.0, 100.0, false), q(2, 100.0, 0.0, true)];
+ let week_dead = vec![q(1, 100.0, 50.0, false), q(2, 100.0, 100.0, false)];
+ let credit = Some(1_700_000_000_000 + 13 * 86_400_000);
+
+ assert_eq!(decide(&five_ok, &[c(7)], credit, "平台积分").action, Action::Idle);
+ assert_eq!(decide(&five_dead, &[c(7)], credit, "平台积分").action, Action::UseCard);
+ assert_eq!(decide(&five_dead, &[], credit, "平台积分").action, Action::KeepCredit);
+ assert_eq!(decide(&five_dead, &[], None, "").action, Action::Wait);
+ assert_eq!(decide(&five_dead, &[c(7)], None, "").action, Action::UseCard);
+ assert_eq!(decide(&week_dead, &[c(7)], credit, "平台积分").action, Action::UseCard);
+ // 积分更早过期 → 不用卡
+ let credit_soon = Some(1_700_000_000_000 + 2 * 86_400_000);
+ assert_eq!(
+ decide(&five_dead, &[c(20)], credit_soon, "平台积分").action,
+ Action::KeepCredit
+ );
+ }
+
+ #[test]
+ fn reasons_are_filled() {
+ let five_dead = vec![q(1, 100.0, 100.0, false), q(2, 100.0, 0.0, true)];
+ let d = decide(&five_dead, &[], Some(1_700_000_000_000 + 13 * 86_400_000), "平台积分");
+ assert!(d.reason.contains("没有可用的重置卡"), "{}", d.reason);
+ assert!(d.reason.contains("平台积分"), "{}", d.reason);
+ }
+}
diff --git a/plugins/hylouis233/expiry-sentinel/daemon/src/main.rs b/plugins/hylouis233/expiry-sentinel/daemon/src/main.rs
new file mode 100644
index 0000000..8a4050f
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/daemon/src/main.rs
@@ -0,0 +1,491 @@
+//! 后台守护进程:额度探测 + 先过期先用决策 + 自动扣卡。
+//!
+//! 与 Mini App「蹬王之王」的判定逻辑严格同构,由 `verify-parity.mjs`
+//! 逐字比对「动作 + 理由文本」防止漂移。
+//!
+//! 设计要点:
+//! - 分层缓存:额度每轮取,积分/卡/汇总按各自变化速度缓存
+//! - 先记账再请求:崩溃或重启都不会重复扣同一张卡
+//! - 幂等键:用服务端下发的 out_biz_no,重试安全
+
+mod api;
+mod decision;
+mod state;
+
+use std::collections::{BTreeMap, HashSet};
+use std::time::Duration;
+
+use api::{Client, ApiError, BASE_CN};
+use decision::{
+ amount, decide, fmt_day, parse_quotas, Action, QuotaState, RemainsPayload, UsableCard,
+};
+use serde_json::json;
+
+/// 日志:带本地时间戳,便于 tail 追踪。
+fn log(msg: &str) {
+ use std::io::Write;
+ let now = api::now_ms();
+ let secs = now.div_euclid(1000);
+ let ms = now.rem_euclid(1000);
+ // 复用 decision 里的本地时区换算
+ let (month, day) = decision::local_md(secs);
+ let (hh, mm) = decision::local_hm(secs);
+ let _ = std::io::stdout().write_all(
+ format!("[{month:02}-{day:02} {hh:02}:{mm:02}.{ms:03}] {msg}\n").as_bytes(),
+ );
+}
+
+/// 各数据源的缓存 TTL(毫秒)。按变化速度分配,不是所有数据都值得每轮取。
+struct Ttl {
+ quota: i64,
+ code_credits: i64,
+ cards: i64,
+ platform: i64,
+}
+
+impl Default for Ttl {
+ fn default() -> Self {
+ // quota TTL=0:额度是所有联动的触发源,必须每轮看。
+ // 其余按「多久会变」给:积分随消耗变、卡按天签到变、平台池极少变。
+ Ttl { quota: 0, code_credits: 60_000, cards: 300_000, platform: 300_000 }
+ }
+}
+
+struct Cache {
+ slots: BTreeMap<&'static str, (Option, i64)>,
+}
+
+impl Cache {
+ fn new() -> Self {
+ Cache { slots: BTreeMap::new() }
+ }
+ fn get(&mut self, key: &'static str, ttl: i64) -> Option {
+ let entry = self.slots.get(key).cloned();
+ if let Some((Some(v), at)) = entry {
+ if api::now_ms() - at < ttl {
+ return Some(v);
+ }
+ }
+ None
+ }
+ fn put(&mut self, key: &'static str, value: T) {
+ self.slots.insert(key, (Some(value), api::now_ms()));
+ }
+ fn invalidate(&mut self, key: &str) {
+ self.slots.remove(key);
+ }
+}
+
+struct Caches {
+ quota: Cache,
+ code: Cache,
+ platform: Cache,
+ cards: Cache>,
+}
+
+impl Caches {
+ fn new() -> Self {
+ Caches {
+ quota: Cache::new(),
+ code: Cache::new(),
+ platform: Cache::new(),
+ cards: Cache::new(),
+ }
+ }
+}
+
+struct Daemon {
+ client: Client,
+ ttl: Ttl,
+ cache: Caches,
+}
+
+/// 取缓存:命中则零网络,未命中才请求。
+///
+/// 做成自由函数(而非 `&mut self` 方法):这样 `cache` 与 `client`
+/// 可以分别借用,不会触发「同一个 self 被可变借两次」。
+fn cached(
+ client: &mut Client,
+ cache: &mut Cache,
+ key: &'static str,
+ ttl: i64,
+ load: impl FnOnce(&mut Client) -> Result,
+) -> Result {
+ if let Some(v) = cache.get(key, ttl) {
+ return Ok(v);
+ }
+ let v = load(client)?;
+ cache.put(key, v.clone());
+ Ok(v)
+}
+
+impl Daemon {
+ fn new(client: Client) -> Self {
+ Daemon { client, ttl: Ttl::default(), cache: Caches::new() }
+ }
+
+ fn quotas(&mut self) -> Vec {
+ let ttl = self.ttl.quota;
+ let Daemon { client, cache, .. } = self;
+ match cached(client, &mut cache.quota, "quota", ttl, |c| {
+ c.get("/backend/account/token_plan/remains_percent", false)
+ }) {
+ Ok(v) => {
+ let payload: RemainsPayload = serde_json::from_value(v).unwrap_or(RemainsPayload {
+ model_remains: vec![],
+ });
+ parse_quotas(&payload)
+ }
+ Err(e) => {
+ log(&format!(" 额度读取失败(沿用上次): {e}"));
+ vec![]
+ }
+ }
+ }
+
+ /// 全部分页拉取重置卡
+ fn all_cards(&mut self) -> Vec {
+ let ttl = self.ttl.cards;
+ if let Some(v) = self.cache.cards.get("cards", ttl) {
+ return v;
+ }
+ let mut seen: HashSet = HashSet::new();
+ let mut all = Vec::new();
+ for page in 1..=10 {
+ match self
+ .client
+ .get(&format!("/backend/account/reset_card/list?page={page}&size=20"), false)
+ {
+ Ok(v) => {
+ let cards = v.get("cards").and_then(|c| c.as_array()).cloned().unwrap_or_default();
+ if cards.is_empty() {
+ break;
+ }
+ let n = cards.len();
+ for c in cards {
+ if let Some(id) = c.get("card_id").and_then(|x| x.as_str()) {
+ if seen.insert(id.to_string()) {
+ all.push(c);
+ }
+ }
+ }
+ if n < 20 {
+ break;
+ }
+ }
+ Err(e) => {
+ log(&format!(" 重置卡列表读取失败(忽略): {e}"));
+ break;
+ }
+ }
+ }
+ self.cache.cards.put("cards", all.clone());
+ all
+ }
+
+ /// 可用于扣卡的卡:未过期 + 有 out_biz_no + 有 source_type,按到期排序
+ fn usable_cards(&mut self) -> Vec {
+ let now = api::now_ms();
+ let mut out: Vec = self
+ .all_cards()
+ .iter()
+ .filter_map(|c| {
+ let expire = c.get("expire_time").and_then(|v| v.as_i64())?;
+ if expire <= now {
+ return None;
+ }
+ let out_biz_no = c.get("out_biz_no").and_then(|v| v.as_str()).unwrap_or("");
+ let source_type = c.get("source_type").and_then(|v| v.as_str()).unwrap_or("");
+ if out_biz_no.is_empty() || source_type.is_empty() {
+ return None;
+ }
+ Some(UsableCard {
+ card_id: c.get("card_id")?.as_str()?.to_string(),
+ source_type: source_type.to_string(),
+ out_biz_no: out_biz_no.to_string(),
+ expire_time: expire,
+ })
+ })
+ .collect();
+ out.sort_by_key(|c| c.expire_time);
+ out
+ }
+
+ /// 全账户里最早到期的一笔「仍有效且有余额」的积分(两个来源取更早的)
+ fn earliest_credit(&mut self) -> (Option, String) {
+ let now = api::now_ms();
+ let mut best: Option = None;
+ let mut desc = String::new();
+
+ let mut consider = |exp: Option, balance: f64, label: &str| {
+ let (Some(e), true) = (exp, balance > 0.0) else { return };
+ if e <= now {
+ return;
+ }
+ if best.is_none_or(|b| e < b) {
+ best = Some(e);
+ desc = label.to_string();
+ }
+ };
+
+ // 平台积分池
+ let pttl = self.ttl.platform;
+ let platform_result = {
+ let Daemon { client, cache, .. } = self;
+ cached(client, &mut cache.platform, "platform", pttl, |c| {
+ c.get("/backend/account/token_plan_credit", false)
+ })
+ };
+ match platform_result {
+ Ok(v) => {
+ if let Some(buckets) = v
+ .get("balance_breakdown")
+ .and_then(|b| b.get("buckets"))
+ .and_then(|b| b.as_array())
+ {
+ for b in buckets {
+ let label = match b.get("source").and_then(|s| s.as_i64()) {
+ Some(2) => "平台积分·充值".to_string(),
+ Some(3) => "平台积分·赠送".to_string(),
+ _ => "平台积分·未知".to_string(),
+ };
+ consider(
+ b.get("expire_time_ms").and_then(|v| v.as_i64()),
+ amount(b.get("balance").unwrap_or(&json!(0))),
+ &label,
+ );
+ }
+ }
+ }
+ Err(e) => log(&format!(" 平台积分读取失败(忽略): {e}")),
+ }
+
+ // Code 积分明细
+ let cttl = self.ttl.code_credits;
+ let raw = {
+ let Daemon { client, cache, .. } = self;
+ cached(client, &mut cache.code, "code", cttl, |c| {
+ c.get(
+ "/minimax-cloud/api/v1/credit/details?page_number=1&page_size=100&include_expired=1",
+ true,
+ )
+ })
+ };
+ match raw {
+ Ok(v) => {
+ if let Some(details) = v.get("details").and_then(|d| d.as_array()) {
+ for item in details {
+ let label = if item.get("credit_type").and_then(|c| c.as_i64()) == Some(2) {
+ "Code 积分·赠送"
+ } else {
+ "Code 积分·购买"
+ };
+ consider(
+ item.get("expire_at_ms").and_then(|v| v.as_i64()),
+ amount(item.get("remaining_amount").unwrap_or(&json!(0))),
+ label,
+ );
+ }
+ }
+ }
+ Err(e) => log(&format!(" Code 积分读取失败(忽略): {e}")),
+ }
+
+ (best, desc)
+ }
+
+ /// 一轮:判定 + 必要时扣卡
+ fn tick(&mut self, dry_run: bool) -> Result<(Action, String, bool), ApiError> {
+ let quotas = self.quotas();
+ let usable = self.usable_cards();
+ let (credit_expiry, credit_desc) = self.earliest_credit();
+
+ let d = decide("as, &usable, credit_expiry, &credit_desc);
+ log(&format!(
+ "额度: {}",
+ quotas.iter().map(|q| q.describe()).collect::>().join(" | ")
+ ));
+ log(&format!("决策[{}]: {}", d.action.as_str(), d.reason));
+
+ if d.action != Action::UseCard || dry_run {
+ return Ok((d.action, d.reason, false));
+ }
+ let Some(card) = usable.first() else {
+ return Ok((d.action, d.reason, false));
+ };
+
+ let fingerprint = format!("{}:{}", card.card_id, card.out_biz_no);
+ if state::was_spent(&fingerprint) {
+ log("这张卡本进程已尝试过,跳过");
+ return Ok((d.action, d.reason, false));
+ }
+
+ // 先记账再请求:崩溃/重启都不会重复扣
+ state::mark_spent(&fingerprint, &card.card_id, &card.source_type);
+ self.cache.cards.invalidate("cards");
+
+ match self
+ .client
+ .post(
+ "/backend/account/reset_card/use",
+ &json!({ "source_type": card.source_type, "out_biz_no": card.out_biz_no }),
+ false,
+ ) {
+ Ok(v) => {
+ let outcome = v.get("outcome").and_then(|x| x.as_str()).unwrap_or("UNKNOWN");
+ let ok = outcome == "SUCCESS" || outcome == "REPLAYED";
+ if ok {
+ log(&format!("✓ 已使用重置卡 {},额度已重置", card.card_id));
+ } else {
+ log(&format!("✗ 重置卡不可用({outcome})"));
+ }
+ Ok((d.action, d.reason, ok))
+ }
+ Err(e) => {
+ log(&format!("✗ 扣卡失败:{e}"));
+ Ok((d.action, d.reason, false))
+ }
+ }
+ }
+}
+
+/// 校验模式:读入与 Node/Python 同一份用例 JSON,打印同样的结果结构。
+///
+/// 供 `verify-parity.mjs` 调用,让三方(Mini App / Python / Rust)跑同一组输入。
+/// 不联网、不落盘,纯函数比对。
+fn selftest() {
+ use std::io::Read as _;
+ let mut input = String::new();
+ if std::io::stdin().read_to_string(&mut input).is_err() {
+ eprintln!("selftest: 无法读取 stdin");
+ std::process::exit(2);
+ }
+ let payload: serde_json::Value = match serde_json::from_str(&input) {
+ Ok(v) => v,
+ Err(e) => {
+ eprintln!("selftest: JSON 解析失败 {e}");
+ std::process::exit(2);
+ }
+ };
+ let now = payload["now"].as_i64().unwrap_or(0);
+ let day = payload["day"].as_i64().unwrap_or(86_400_000);
+ let mut out = Vec::new();
+
+ for c in payload["cases"].as_array().cloned().unwrap_or_default() {
+ let mk = |t: i64, used: f64, total: f64, unlimited: bool| QuotaState {
+ quota_type: t,
+ total,
+ used,
+ is_unlimited: unlimited,
+ };
+ let five_used = c["five"].as_f64().unwrap_or(0.0);
+ let week_used = c["week"].as_i64();
+ let quotas = vec![
+ mk(1, five_used, 100.0, false),
+ match week_used {
+ None => mk(2, 0.0, 100.0, true),
+ Some(w) => mk(2, w as f64, 100.0, false),
+ },
+ ];
+ let cards: Vec = c["cards"]
+ .as_array()
+ .cloned()
+ .unwrap_or_default()
+ .iter()
+ .filter_map(|v| {
+ let d = v.as_i64()?;
+ Some(UsableCard {
+ card_id: format!("C{d}"),
+ source_type: "mcode_signin".into(),
+ out_biz_no: format!("b{d}"),
+ expire_time: now + d * day,
+ })
+ })
+ .collect();
+ let credit = c["credit"].as_i64().map(|d| now + d * day);
+
+ let d = decide("as, &cards, credit, "平台积分");
+ out.push(serde_json::json!({
+ "n": c["n"].as_str().unwrap_or(""),
+ "action": d.action.as_str(),
+ "reason": d.reason,
+ }));
+ }
+ println!("{}", serde_json::to_string(&out).unwrap_or_default());
+}
+
+fn main() {
+ let args: Vec = std::env::args().collect();
+ if args.iter().any(|a| a == "--selftest") {
+ selftest();
+ return;
+ }
+
+ let mut interval: f64 = 60.0;
+ let mut dry_run = false;
+ let mut cmd = "watch".to_string();
+
+ let mut i = 1;
+ while i < args.len() {
+ match args[i].as_str() {
+ "--interval" if i + 1 < args.len() => {
+ interval = args[i + 1].parse().unwrap_or(60.0);
+ i += 2;
+ }
+ "--base" if i + 1 < args.len() => i += 2,
+ "--dry-run" => {
+ dry_run = true;
+ i += 1;
+ }
+ "watch" | "once" | "status" | "forget" => {
+ cmd = args[i].clone();
+ i += 1;
+ }
+ _ => i += 1,
+ }
+ }
+
+ if cmd == "forget" {
+ state::clear_spent();
+ println!("已清空本地去重记录。");
+ return;
+ }
+
+ let client = match Client::new(BASE_CN) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("初始化失败:{e}");
+ std::process::exit(1);
+ }
+ };
+ let mut daemon = Daemon::new(client);
+
+ if cmd == "once" {
+ match daemon.tick(dry_run) {
+ Ok(_) => {}
+ Err(e) => {
+ eprintln!("失败:{e}");
+ std::process::exit(1);
+ }
+ }
+ return;
+ }
+
+ log(&format!("启动后台监控,间隔 {interval}s(Ctrl+C 退出)"));
+ log("扣卡策略:与 Mini App「蹬王之王」一致 —— 额度真正耗尽才动手,并按先过期先用");
+ loop {
+ match daemon.tick(dry_run) {
+ Ok(_) => {}
+ Err(e) if e.is_unauthorized() => {
+ log(&format!(
+ "✗ 鉴权失败:{e}。请确认 MiniMax Code 桌面端已启动并处于登录状态。"
+ ));
+ std::process::exit(1);
+ }
+ Err(e) => log(&format!("⚠️ 本轮失败:{e},下一轮继续")),
+ }
+ // 抖动,避免与整点/整 5 小时重置撞在一起
+ let jitter = (api::now_ms() % 1000) as f64 / 1000.0 * 4.0 - 2.0;
+ std::thread::sleep(Duration::from_millis(((interval + jitter) * 1000.0) as u64));
+ }
+}
diff --git a/plugins/hylouis233/expiry-sentinel/daemon/src/state.rs b/plugins/hylouis233/expiry-sentinel/daemon/src/state.rs
new file mode 100644
index 0000000..7d4dd6b
--- /dev/null
+++ b/plugins/hylouis233/expiry-sentinel/daemon/src/state.rs
@@ -0,0 +1,95 @@
+//! 去重记录的持久化。
+//!
+//! 落盘而非只放内存:后台进程会被 launchd 重启,只靠内存的话
+//! 重启后会把同一张卡再扣一遍。先写盘再请求,崩溃也不重复。
+
+use std::collections::BTreeSet;
+use std::path::PathBuf;
+
+fn state_dir() -> Option {
+ let home = std::env::var("HOME").ok()?;
+ let dir = PathBuf::from(home).join(".config/mcode-reset-card");
+ Some(dir)
+}
+
+fn state_file() -> Option {
+ Some(state_dir()?.join("bg-state.json"))
+}
+
+fn read_set() -> BTreeSet {
+ let Some(path) = state_file() else {
+ return BTreeSet::new();
+ };
+ let Ok(text) = std::fs::read_to_string(&path) else {
+ return BTreeSet::new();
+ };
+ serde_json::from_str::(&text)
+ .ok()
+ .and_then(|v| v.get("spent").cloned())
+ .and_then(|s| serde_json::from_value::>(s).ok())
+ .map(|v| v.into_iter().collect())
+ .unwrap_or_default()
+}
+
+fn write_set(set: &BTreeSet) {
+ let Some(dir) = state_dir() else { return };
+ let Some(path) = state_file() else { return };
+ if std::fs::create_dir_all(&dir).is_err() {
+ return;
+ }
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
+ }
+ let json = serde_json::json!({
+ "spent": set.iter().collect::>(),
+ "updatedAtMs": crate::api::now_ms(),
+ });
+ // 原子替换:先写临时文件再 rename,避免半截文件
+ let tmp = path.with_extension("json.tmp");
+ if std::fs::write(&tmp, serde_json::to_string_pretty(&json).unwrap_or_default()).is_err() {
+ return;
+ }
+ if std::fs::rename(&tmp, &path).is_err() {
+ let _ = std::fs::remove_file(&tmp);
+ return;
+ }
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
+ }
+}
+
+pub fn was_spent(fingerprint: &str) -> bool {
+ read_set().contains(fingerprint)
+}
+
+pub fn mark_spent(fingerprint: &str, card_id: &str, source_type: &str) {
+ let mut set = read_set();
+ if set.insert(fingerprint.to_string()) {
+ write_set(&set);
+ }
+ crate::log(&format!("已记账:{card_id} ({source_type})"));
+}
+
+pub fn clear_spent() {
+ if let Some(path) = state_file() {
+ let _ = std::fs::remove_file(path);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn set_roundtrip() {
+ let mut s = BTreeSet::new();
+ s.insert("a:1".to_string());
+ s.insert("b:2".to_string());
+ assert!(s.contains("a:1"));
+ assert!(!s.contains("c:3"));
+ }
+}
diff --git a/plugins/hylouis233/expiry-sentinel/docs/preview-cards.png b/plugins/hylouis233/expiry-sentinel/docs/preview-cards.png
new file mode 100644
index 0000000000000000000000000000000000000000..99299d564889daf6d19e561d00056c1cf3d02a34
GIT binary patch
literal 126629
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