Repository navigation
Add 蹬王之王 / Expiry Sentinel: spend whichever credit expires first - #28
Hylouis233 wants to merge 2 commits into
Conversation
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
MyPrototypeWhat
left a comment
There was a problem hiding this comment.
感谢贡献!「先过期先用」这个思路很实用,代码里也看得出对重复扣卡做了不少防护:先记账再请求、按 out_biz_no 幂等、token 只放在内存里。我在本地用 Node 22 跑了 npm run check,结果通过;daemon 用 stable Rust 跑 cargo test 9/9 通过。另外我用假的 lease socket 和 mock 接口把 Node 入口跑了一遍(没有访问真实接口),发现下面几个问题。
需要修改
- 页面里的「使用建议的重置卡」和自动执行实际上不会扣卡。
useCard()用snapshot.cards.items判断目标卡是否还在列表里,但 items 已经去掉了 cardId(只剩 index 和 expireTimeMs),所以stillListed永远是 false。我用 mock 模拟「额度耗尽、卡比积分先过期」:决策是 use-card,但接口返回「这张卡已不在可用列表中,跳过」,自动执行首轮也是SKIPPED_DUPLICATE,reset_card/use一次都没有调用。 - 积分数据读取失败时,决策会变成用卡。Node 这边,
credit/details失败被.catch(() => null)吞掉以后,decide会认为「没有可用积分」,给出 use-card。我用 mock 放了一笔比卡早到期的 Code 积分,正常情况下结论是留卡;让这个接口返回 502 后,结论变成「没有可用积分,使用重置卡重置额度」。daemon 的earliest_credit()读取失败时也只是记日志「忽略」,decide随后返回 UseCard。扣卡是真实写操作,建议任何一路积分数据没拿到时都不扣卡,在页面上提示即可。第 1 条修好以后,这一条就会真的扣错卡。 - 后台服务目前没法从这个包装上。README 写开启「后台服务」会安装 launchd agent,plist「written only when you enable 后台服务」,但包里没有任何代码写这个 plist,
controlDaemon只检查它是否存在,然后执行launchctl load/unload,也没有说明 plist 里运行的是哪个二进制。麻烦补上安装和卸载流程(写什么 plist、二进制放在哪里、卸载插件时怎么清理;Host 只会停掉入口进程),或者在 README 里把它标成需要手动配置的功能,并写清楚步骤。 - README 写截图来自真实账号,用的是真实数字。CONTRIBUTING.md 要求截图使用合成数据或充分脱敏的数据,麻烦换成合成数据。
- 「Tested environment」请补上 MiniMax Code 的具体版本号。3.1.2 前后,宿主对 Mini App 端口的处理不一样。
另外,这个插件通过桌面端内部的 OAuth lease socket 拿用户的 access token,再调用私有接口扣卡。仓库目前的文档里没有这类用法的说明,我们需要内部确认一下是否接受,确认后再回复你。
建议(可选)
/api/use-card、/api/auto、/api/daemon这几个写接口没有同源校验。3.1.2 以后宿主会挡住跨站请求,但老版本宿主不会,而这些接口会真实扣卡,建议加一道Sec-Fetch-Site/Origin检查。- 「使用建议的重置卡」建议加一个确认步骤。
- README 写「No card IDs are shown anywhere in the UI」,但
/api/state返回的decision.target带有 cardId 和 outBizNo,页面也会显示「目标卡 …」。 restart只做一次 load 或 unload,所以服务运行时点「重启」会把它停掉。- daemon 默认会真实扣卡(只有
--dry-run才不扣),README 里的「默认只读」指的是页面,建议写清楚。 - README 提到的
verify-parity.mjs一致性校验不在 PR 里;~/.config/mcode-reset-card/token包里只读不写,和 README 的描述不一致。 Cargo.toml建议加上rust-version:我用 1.85 编译时#[derive(Default)]用在含裸指针字段的结构体上会报错,较新的 stable 可以编译。未使用的BASE_GLOBAL也可以删掉。- 根 README 的新行建议放到表格末尾。
| * 进程重启后 spent 清空也没关系,列表判定依然成立。 | ||
| */ | ||
| const stillListed = (snapshot.cards.items ?? []).some( | ||
| (c) => c.cardId === target.cardId && !c.expired, |
There was a problem hiding this comment.
这里的 snapshot.cards.items 已经映射成 { index, expireTimeMs },没有 cardId,所以这个判断永远是 false。我本地用 mock 测试,决策是 use-card 时也总是返回「这张卡已不在可用列表中」,reset_card/use 不会被调用。可以改用 buildSnapshot 内部的原始 cards 列表来判断。
| cached('codeCredits', () => | ||
| fetchCodeCredits(token) | ||
| .then((r) => r.payload) | ||
| .catch(() => null), |
There was a problem hiding this comment.
这里失败时返回 null,decide 会把 Code 积分当作不存在。我用 mock 让 credit/details 返回 502,结论就从「留卡」变成了「没有可用积分,使用重置卡」。扣卡是真实写操作,建议这一路失败时不执行写操作。
| } | ||
| } | ||
| } | ||
| Err(e) => log(&format!(" Code 积分读取失败(忽略): {e}")), |
There was a problem hiding this comment.
和 Node 这边一样,这里读取失败只记日志,decide 收到 None 后会直接返回 UseCard。建议读取失败时这一轮跳过扣卡。
## PR MiniMax-AI#28 review 的三条建议(都是真 bug,全部仍然存在) 1. **扣卡永远被跳过** `cards.items` 是给页面看的**脱敏视图**(只有序号与到期日,没有 card_id), 却拿它判断「这张卡还在不在可用列表里」⇒ `c.cardId === target.cardId` 恒为 false ⇒ **每次都跳过,`reset_card/use` 从未被调用**。 ⇒ 新增 `cards.raw`(带 cardId)供写路径判定,页面仍不展示卡号。 2. **积分接口失败被当成「没有积分」⇒ 误扣真卡** `credit/details` 失败被 `.catch(() => null)` 吞掉 ⇒ codeCredits 为空 ⇒ 结论从「留卡」翻转成「用卡」。而扣卡是**真实写操作**。 ⇒ 新增 `creditDataComplete` 闸门:数据不完整 ⇒ 本轮**不做任何写操作**。 3. **守护进程侧同样的问题** 积分读取失败只记日志(`best` 保持 None)⇒ 同样落到 use-card。 ⇒ `earliest_credit()` 现在返回「数据是否完整」,不完整 ⇒ wait。 ## 新功能:可重试错误无限重试 429(负载过高)、500/502/503/504(内部与网关错误)、连接重置/超时/DNS 失败 ⇒ 一直重试到成功。退避指数增长 + 抖动,**封顶 60 秒**(不会把接口打爆)。 401/403 重取 token 一次;其余 4xx 与业务错误快速失败。 扣卡也走重试 —— 请求带 out_biz_no 幂等键,重复提交只回放不重复扣。 重试**可中断**:SIGINT/SIGTERM 让在飞的请求立刻放弃,守护进程不会被卡死。 ## 顺带 - verify-parity.mjs 改为**插件内自包含**(原先硬编码 ~/Desktop/mmx, 违反 CONTRIBUTING 的「插件应能独立复制使用」约定) - 抽出 `decide_retry()` 纯函数,使重试决策可脱离网络单测 ## 测试 Rust 41/41 · Python 20/20 · 三方 parity 19/19(新增 2 条「数据不完整」用例) MiniApp 决策 58/58 · 重试真实行为 4/4(本地 mock server 实测: 500/429 会重试到成功、404 立即失败、持续 500 不提前放弃) npm run validate OK · npm test 69/69
中文说明
这个 Mini App 做什么
MiniMax Code 的积分和重置卡都会到期。放着不管,最快到期的那笔会悄悄作废,而你一直在用最耐放的那笔。
蹬王之王把每笔余额和到期日摊开给你看,然后「先过期先用」——不浪费,也不过期。
决策规则
5 小时或周限额耗尽时,请求开始扣积分——这就是该动手的时刻。应用会比两个日期:
Node 服务和后台守护进程实现同一套规则,并有逐字比对的一致性校验,防止两边漂移。
三种使用方式
三条路径共用同一套决策规则,服务端按幂等键去重——即使同时开着,卡也不会被扣两次。
截图
来自应用的实际运行:一张是看板全景,一张是重置卡列表与已用统计。
Review 说明
daemon/下,以 Rust 源码形式提供,不提交构建产物;用cargo build --release构建。它暴露一个--selftest模式,喂给它与 Node 服务相同的一组用例,一致性检查比对的就是这两者。~/.minimax/run/mcode-auth-lease-v1.sock—— 这是内部路径,已在## Data & access中说明。www.minimaxi.com与agent.minimaxi.com。无遥测、无外部服务。npm run check对本包报告OK,无警告。Need help on this PR? Tag
@codesmith-botwith what you need.