waygate-mmcode 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhanghaojie513
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,197 @@
1
- # Temporary Holding Version
1
+ # waygate-mmcode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <div align="center">
4
+
5
+ **途关 · MiniMax Code 反代**
6
+
7
+ 把 MiniMax Code 客户端的订阅,变成你自己的本地 API
8
+
9
+ ![Node](https://img.shields.io/badge/Node-%E2%89%A520-3C873A?style=flat-square&logo=node.js&logoColor=white)
10
+ ![Platform](https://img.shields.io/badge/Platform-Windows-0078D4?style=flat-square&logo=windows&logoColor=white)
11
+ ![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)
12
+
13
+ [快速开始](#快速开始) · [凭证互斥](#凭证互斥必读) · [客户端配置](#客户端怎么填) · [工作原理](#工作原理) · [常见问题](#常见问题) · [相关项目](#相关项目)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ > [!CAUTION]
20
+ > **凭证互斥(必读)**:MiniMax 的 refresh token 是**一次性**的,且**同一账号同时只有一个有效会话**。
21
+ > 本工具与官方客户端登录**同一个账号**会**互相踢下线**(你一登录客户端,本工具就失效;反之亦然)。
22
+ >
23
+ > **解决**:给本工具配一个**专用账号**(与日常使用的客户端分开注册一个即可)。
24
+ > 登录专用账号 → 跑 `mmcode login` → 此后**不再在那个账号上登录官方客户端**。
25
+
26
+ **读取本机 MiniMax Code 客户端的登录态,在本机起一个 Anthropic / OpenAI 兼容的聊天端点** ——
27
+ Cursor、Claude Code、Cherry Studio、任何能改 base_url 的工具,都能直接用你的订阅额度。
28
+
29
+ - **零配置接入** —— 一条命令起服务,打印出「代理地址 + 密钥 + 模型 ID」
30
+ - **两个协议都认** —— `POST /v1/chat/completions`(OpenAI)与 `POST /v1/messages`(Anthropic)
31
+ - **只在本机转** —— 读本机客户端文件、写 `~/.waygate/`、本机转发;不向任何第三方上传数据
32
+
33
+ ## 快速开始
34
+
35
+ ```bash
36
+ # 全局安装
37
+ npm i -g waygate-mmcode
38
+
39
+ # 导入客户端登录态(在 MiniMax Code 客户端里登录专用账号后执行)
40
+ mmcode login
41
+
42
+ # 启动
43
+ mmcode
44
+ ```
45
+
46
+ ```
47
+ 途关 · MiniMax Code 反代 v0.2.0
48
+
49
+ 凭证 ✓ access 有效约 187 分钟(下次请求自动刷新)
50
+ 上游 https://agent.minimax.cn (Anthropic 兼容)
51
+ 代理 http://127.0.0.1:8788
52
+ 密钥 sk-mmcode-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
53
+
54
+ 模型 ID(填进你的客户端):
55
+ MiniMax-M3
56
+ MiniMax-M3.1-Flash-Preview
57
+ MiniMax-M2.7
58
+ MiniMax-M2.7-highspeed
59
+
60
+ 把上面的「代理地址 + 密钥」填进 Cursor / Claude Code / Cherry Studio 等客户端即可。
61
+ 按 Ctrl+C 停止。
62
+ ```
63
+
64
+
65
+ **要求**:Node.js ≥ 20;本机装有 MiniMax Code 桌面客户端**且已登录**(建议专用账号)。
66
+
67
+ ## 凭证互斥(必读)
68
+
69
+ MiniMax 的会话机制决定了**同一个账号同时只有一个有效会话**。这意味着:
70
+
71
+ | 你做的事 | 后果 |
72
+ | --- | --- |
73
+ | 本工具在跑 → 你登录官方客户端(同一账号) | **本工具失效**,需重跑 `mmcode login` |
74
+ | 官方客户端在用 → 你启动本工具(同一账号) | 官方客户端被踢下线 |
75
+
76
+ **正确姿势**:注册一个专用账号,**只在客户端登录一次**(为了导入凭证),之后**永远不在那个账号上登录客户端**。日常客户端用你的主账号,两边互不干扰。
77
+
78
+ ## 命令
79
+
80
+ ```bash
81
+ mmcode # 启动反代(同 mmcode serve),打印模型 ID 与代理地址
82
+ mmcode models # 只列出模型 ID(不起服务)
83
+ mmcode doctor # 自检:端口 / 凭证 / 令牌刷新 / 上游连通
84
+ mmcode login # 重新读取客户端登录态(令牌被顶掉/过期时用)
85
+ mmcode checkin # 每日签到领积分(用本工具令牌代签,无需开客户端)
86
+ mmcode credits # 查积分账本(剩余 / 已消耗 / 累计,精确到 0.01)
87
+ ```
88
+
89
+ **签到与积分**(MiniMax Code 专属):
90
+
91
+ ```bash
92
+ $ mmcode checkin
93
+ ✓ 签到成功 +400 积分(连续第 4 天)
94
+
95
+ $ mmcode credits
96
+ MiniMax Code 积分账本
97
+
98
+ 剩余 4188.65
99
+ 已消耗 6611.35
100
+ 累计发放 10800
101
+ 最近到期 2026-10-14
102
+
103
+ 批次明细(17 笔):
104
+ +400 剩 400 用 0 到期 2026-11-12
105
+ +800 剩 188.65 用 611.35 到期 2026-11-05
106
+ …(还有 7 笔)
107
+
108
+ 注:上游不回报单次请求消耗,以上为账户级批次累计(精确到 0.01)。
109
+ ```
110
+
111
+ > **为什么能代签**:签到接口用的就是本工具已在维护的 access token。所以你**不需要为了签到去登录客户端**
112
+ > ——(登录客户端反而会踢掉本工具)。加 `--json` 可得到机器可读输出,便于脚本/定时任务。
113
+
114
+ | 参数 | 说明 |
115
+ | --- | --- |
116
+ | `--port <n>` | 监听端口(默认 8788) |
117
+ | `--host <addr>` | 监听地址(默认 `127.0.0.1`) |
118
+ | `--key <sk-…>` | 指定入站密钥(默认自动生成并缓存) |
119
+ | `--json` | 机器可读输出 |
120
+ | `--verbose` | 打印每个请求的路由日志 |
121
+
122
+ ## 客户端怎么填
123
+
124
+ **Cursor / Cherry Studio / OpenAI SDK**
125
+
126
+ - Base URL:`http://127.0.0.1:8788/v1`
127
+ - API Key:启动时打印的 `sk-mmcode-…`
128
+ - 模型:模型 ID(如 `MiniMax-M3`)
129
+
130
+ **Claude Code**
131
+
132
+ ```bash
133
+ ANTHROPIC_BASE_URL=http://127.0.0.1:8788
134
+ ANTHROPIC_API_KEY=sk-mmcode-…
135
+ ```
136
+
137
+ 暴露的端点:`POST /v1/chat/completions` · `POST /v1/messages` · `GET /v1/models` · `GET /healthz`
138
+
139
+ 支持流式(SSE)与非流式(`stream: false` 时自动聚合为单个 JSON)两种响应模式。
140
+
141
+ ## 工作原理
142
+
143
+ ```mermaid
144
+ flowchart LR
145
+ C[你的客户端<br/>Cursor / Claude Code]
146
+ M[mmcode<br/>本机 8788]
147
+ G[MiniMax mavis 网关]
148
+
149
+ C -->|OpenAI / Anthropic 协议| M
150
+ M -->|Anthropic 兼容协议| G
151
+
152
+ subgraph local [本机]
153
+ M
154
+ F[~/.waygate/mmcode/credential.json]
155
+ end
156
+ F -.->|mmcode login 写入| M
157
+
158
+ style M fill:#EDE9FE,stroke:#8B5CF6,color:#1F2328
159
+ style F fill:#FFFFFF,stroke:#D0D7DE,color:#1F2328
160
+ style C fill:#FFFFFF,stroke:#D0D7DE,color:#1F2328
161
+ style G fill:#FFFFFF,stroke:#D0D7DE,color:#1F2328
162
+ style local fill:#F6F8FA,stroke:#D0D7DE
163
+ ```
164
+
165
+ - **凭证**:MiniMax Code 客户端把登录态存在 `~/.minimax/auth/prod/cn/mcode-public/auth.json`(**明文 JSON**)。`mmcode login` 读出令牌写到 `~/.waygate/mmcode/credential.json`(0600)。
166
+ - **令牌生命周期**:access token(`mmoat_`)寿命短(约 1–4 小时),到期前 2 分钟自动刷新;refresh token(`mmort_`)刷新后会被上游**轮换**,本工具自动写回本地。
167
+ - **模型清单**:上游**没有**清单端点(实测 `/models` 恒 503)—— 用内置清单(4 个模型),这是「目录 = 接口」原则的唯一例外。
168
+
169
+ > [!NOTE]
170
+ > **单次请求的积分消耗上游不回报** —— MiniMax 的聊天响应只有 token 数,没有单次费用字段。
171
+ > 积分余额以[官方客户端](https://www.minimax.io)的账本页为准(精确到 0.01)。
172
+
173
+ ## 常见问题
174
+
175
+ **要装 MiniMax Code 客户端吗?** 要。本工具不提供账号,只是把你**已有**的客户端登录态转成本地 API。
176
+
177
+ **令牌被顶掉了怎么办?** 你(或别人)用同一账号登录了客户端 —— 本工具被踢。在客户端**重新登录**(用专用账号),然后跑一次 `mmcode login`。
178
+
179
+ **会不会把我的令牌上传?** 不会。全程只在本机读写:读客户端文件 → 写 `~/.waygate/` → 本机转发。代码开源可查。
180
+
181
+ **端口被占用?** `mmcode --port 9790`,或 `mmcode doctor` 看是哪一项没过。
182
+
183
+ **支持 macOS / Linux 吗?** 是。凭证是明文 JSON,无平台专有加密。
184
+
185
+ ## 免责声明
186
+
187
+ 仅供**个人学习与研究**使用,请遵守 MiniMax 的服务条款。上游接口随时可能变更,届时本工具可能失效(`mmcode doctor` 是排查入口)。作者不对使用后果负责。
188
+
189
+ ## 相关项目
190
+
191
+ - **[waygate-qoder](https://github.com/zhanghaojie513/waygate-qoder)** —— 同款工具,对应 Qoder CN(无凭证互斥问题)
192
+ - **[waygate-core](https://github.com/zhanghaojie513/waygate-core)** —— 两个工具共用的骨架(`npm i waygate-core`)
193
+ - **[relay-station](https://github.com/zhanghaojie513/relay-station)** —— 同一作者的完整多提供商反代(含控制台、用量统计)
194
+
195
+ ## License
196
+
197
+ MIT
@@ -0,0 +1,79 @@
1
+ /**
2
+ * MiniMax Code 签到与积分账本(命令:`mmcode checkin` / `mmcode credits`)。
3
+ *
4
+ * ## 端点(自官方客户端逆向 + 生产实测)
5
+ * GET /minimax-cloud/api/v1/signin/status?timezone_id=Asia/Shanghai —— 签到面板
6
+ * POST /minimax-cloud/api/v1/signin/claim?timezone_id=Asia/Shanghai —— 领取(body {})
7
+ * GET /minimax-cloud/api/v1/credit/details —— 积分账本明细
8
+ *
9
+ * ## 为什么能代签(这是本工具相对其他方案的独特价值)
10
+ * Qoder / MiniMax 的「登录客户端签到」和「反代挂着」是互斥的(客户端登录会踢掉反代)。
11
+ * 但签到接口用的就是**本工具已在维护的 access token** —— 于是不必登客户端,直接代签。
12
+ *
13
+ * ## 积分口径(铁律:不编造)
14
+ * 账本是**批次级累计**(每笔发放的 授予/剩余/已消耗 + 到期时间);上游**不回报单次请求消耗**
15
+ * (实测 usage 里无 credit 字段)。所以 `credits` 只给账户级余额,不做单次估算。
16
+ */
17
+ /** 签面板里的一天。 */
18
+ export interface SigninDay {
19
+ day_no: number;
20
+ points: number;
21
+ /** 1 = 未到,2 = 可签,3 = 已签。 */
22
+ status: number;
23
+ is_today: boolean;
24
+ }
25
+ export interface SigninPanel {
26
+ days: SigninDay[];
27
+ /** 今天是否可签。 */
28
+ claimable: boolean;
29
+ /** 连续天数(今天在周期里的 day_no)。 */
30
+ streak: number | null;
31
+ /** 今日应得积分(可签时)。 */
32
+ todayPoints: number | null;
33
+ }
34
+ export interface SigninResult {
35
+ outcome: 'claimed' | 'already' | 'unavailable' | 'error';
36
+ points: number | null;
37
+ streak: number | null;
38
+ message: string;
39
+ }
40
+ /** 一笔积分发放。 */
41
+ export interface CreditGrant {
42
+ creditType: number;
43
+ granted: number;
44
+ remaining: number;
45
+ consumed: number;
46
+ grantedAt: number;
47
+ expireAt: number;
48
+ }
49
+ export interface CreditLedger {
50
+ /** 账户累计:授予 / 剩余 / 已消耗(各批次求和,精确到 0.01)。 */
51
+ granted: number;
52
+ remaining: number;
53
+ consumed: number;
54
+ /** 最近一笔到期时刻(epoch ms);null = 无。 */
55
+ nextExpireAt: number | null;
56
+ grants: CreditGrant[];
57
+ }
58
+ export declare class MinimaxActivity {
59
+ private readonly token;
60
+ constructor(token: () => Promise<string>);
61
+ /** 打一次签到接口,返回 data 段。 */
62
+ private callSignin;
63
+ /** 查签到面板(不领取)。 */
64
+ panel(): Promise<SigninPanel>;
65
+ /**
66
+ * 签到(幂等:上游对重复 claim 会拒,这里先查面板再决定)。
67
+ * 领取成功后返回本次积分与连续天数。
68
+ */
69
+ checkin(): Promise<SigninResult>;
70
+ /**
71
+ * 拉积分账本。
72
+ * 三个金额任一缺失的批次**跳过**(宁可少算也不编);全批次数不出来就抛错(不返回假 0)。
73
+ */
74
+ credits(): Promise<CreditLedger>;
75
+ }
76
+ /** 人读渲染:签到结果。 */
77
+ export declare function renderCheckin(r: SigninResult): string;
78
+ /** 人读渲染:积分账本。 */
79
+ export declare function renderCredits(l: CreditLedger): string;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * MiniMax Code 签到与积分账本(命令:`mmcode checkin` / `mmcode credits`)。
3
+ *
4
+ * ## 端点(自官方客户端逆向 + 生产实测)
5
+ * GET /minimax-cloud/api/v1/signin/status?timezone_id=Asia/Shanghai —— 签到面板
6
+ * POST /minimax-cloud/api/v1/signin/claim?timezone_id=Asia/Shanghai —— 领取(body {})
7
+ * GET /minimax-cloud/api/v1/credit/details —— 积分账本明细
8
+ *
9
+ * ## 为什么能代签(这是本工具相对其他方案的独特价值)
10
+ * Qoder / MiniMax 的「登录客户端签到」和「反代挂着」是互斥的(客户端登录会踢掉反代)。
11
+ * 但签到接口用的就是**本工具已在维护的 access token** —— 于是不必登客户端,直接代签。
12
+ *
13
+ * ## 积分口径(铁律:不编造)
14
+ * 账本是**批次级累计**(每笔发放的 授予/剩余/已消耗 + 到期时间);上游**不回报单次请求消耗**
15
+ * (实测 usage 里无 credit 字段)。所以 `credits` 只给账户级余额,不做单次估算。
16
+ */
17
+ import { UpstreamError } from './errors.js';
18
+ const MINIMAX_API = 'https://agent.minimax.cn/minimax-cloud/api/v1';
19
+ /** 签到「今天」的时区边界(与官方客户端一致;缺参数上游报 invalid timezone_id)。 */
20
+ const SIGNIN_TZ = 'Asia/Shanghai';
21
+ const REQUEST_TIMEOUT_MS = 20_000;
22
+ /** 上游信封里的业务码闸(非 0 抛错,带上游 msg)。 */
23
+ function assertOk(body) {
24
+ const code = body.base_resp?.status_code;
25
+ if (typeof code === 'number' && code !== 0) {
26
+ throw new UpstreamError(body.base_resp?.status_msg ?? `上游业务码 ${code}`);
27
+ }
28
+ }
29
+ /** 取数字(数字或数字字符串);取不到 null。 */
30
+ function toAmount(v) {
31
+ if (typeof v === 'number' && Number.isFinite(v))
32
+ return v;
33
+ if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
34
+ return Number(v);
35
+ return null;
36
+ }
37
+ const round2 = (n) => Math.round(n * 100) / 100;
38
+ export class MinimaxActivity {
39
+ token;
40
+ constructor(token) {
41
+ this.token = token;
42
+ }
43
+ /** 打一次签到接口,返回 data 段。 */
44
+ async callSignin(action) {
45
+ const token = await this.token();
46
+ const url = `${MINIMAX_API}/signin/${action}?timezone_id=${encodeURIComponent(SIGNIN_TZ)}`;
47
+ const res = await fetch(url, {
48
+ method: action === 'claim' ? 'POST' : 'GET',
49
+ headers: {
50
+ authorization: `Bearer ${token}`,
51
+ accept: 'application/json',
52
+ ...(action === 'claim' ? { 'content-type': 'application/json' } : {}),
53
+ },
54
+ ...(action === 'claim' ? { body: '{}' } : {}),
55
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
56
+ });
57
+ if (!res.ok)
58
+ throw new UpstreamError(`签到接口 HTTP ${res.status}`);
59
+ const body = (await res.json());
60
+ assertOk(body);
61
+ if (!body.data || typeof body.data !== 'object')
62
+ throw new UpstreamError('签到响应缺少 data 段');
63
+ return body.data;
64
+ }
65
+ /** 查签到面板(不领取)。 */
66
+ async panel() {
67
+ const data = await this.callSignin('status');
68
+ const days = Array.isArray(data.days) ? data.days : [];
69
+ const today = days.find((d) => d.is_today) ?? null;
70
+ return {
71
+ days,
72
+ claimable: today?.status === 2,
73
+ streak: today?.day_no ?? null,
74
+ todayPoints: today?.status === 2 ? today.points : null,
75
+ };
76
+ }
77
+ /**
78
+ * 签到(幂等:上游对重复 claim 会拒,这里先查面板再决定)。
79
+ * 领取成功后返回本次积分与连续天数。
80
+ */
81
+ async checkin() {
82
+ let panel;
83
+ try {
84
+ panel = await this.panel();
85
+ }
86
+ catch (err) {
87
+ return { outcome: 'error', points: null, streak: null, message: err.message };
88
+ }
89
+ if (!panel.claimable) {
90
+ const already = panel.streak !== null;
91
+ return {
92
+ outcome: 'already',
93
+ points: null,
94
+ streak: panel.streak,
95
+ message: already ? `今天已签到(连续第 ${panel.streak} 天)` : '今日不可签到(活动未开始或已结束)',
96
+ };
97
+ }
98
+ try {
99
+ const data = await this.callSignin('claim');
100
+ const points = toAmount(data.points);
101
+ const streak = typeof data.day_no === 'number' ? data.day_no : panel.streak;
102
+ return { outcome: 'claimed', points, streak, message: `签到成功 +${points ?? '?'} 积分(连续第 ${streak ?? '?'} 天)` };
103
+ }
104
+ catch (err) {
105
+ return { outcome: 'error', points: null, streak: panel.streak, message: err.message };
106
+ }
107
+ }
108
+ /**
109
+ * 拉积分账本。
110
+ * 三个金额任一缺失的批次**跳过**(宁可少算也不编);全批次数不出来就抛错(不返回假 0)。
111
+ */
112
+ async credits() {
113
+ const token = await this.token();
114
+ const res = await fetch(`${MINIMAX_API}/credit/details`, {
115
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
116
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
117
+ });
118
+ if (!res.ok)
119
+ throw new UpstreamError(`账本接口 HTTP ${res.status}`);
120
+ const body = (await res.json());
121
+ assertOk(body);
122
+ if (!Array.isArray(body.details))
123
+ throw new UpstreamError('账本响应缺少 details 数组');
124
+ const grants = [];
125
+ for (const raw of body.details) {
126
+ if (!raw || typeof raw !== 'object')
127
+ continue;
128
+ const d = raw;
129
+ const granted = toAmount(d.granted_amount);
130
+ const remaining = toAmount(d.remaining_amount);
131
+ const consumed = toAmount(d.consumed_amount);
132
+ if (granted === null || remaining === null || consumed === null)
133
+ continue;
134
+ grants.push({
135
+ creditType: typeof d.credit_type === 'number' ? d.credit_type : 0,
136
+ granted,
137
+ remaining,
138
+ consumed,
139
+ grantedAt: typeof d.granted_at_ms === 'number' ? d.granted_at_ms : 0,
140
+ expireAt: typeof d.expire_at_ms === 'number' ? d.expire_at_ms : 0,
141
+ });
142
+ }
143
+ if (grants.length === 0) {
144
+ throw new UpstreamError('账本里没有可识别的积分批次(响应形状变化?)');
145
+ }
146
+ const sum = (pick) => round2(grants.reduce((s, g) => s + pick(g), 0));
147
+ const future = grants.filter((g) => g.expireAt > Date.now()).map((g) => g.expireAt);
148
+ return {
149
+ granted: sum((g) => g.granted),
150
+ remaining: sum((g) => g.remaining),
151
+ consumed: sum((g) => g.consumed),
152
+ nextExpireAt: future.length > 0 ? Math.min(...future) : null,
153
+ grants,
154
+ };
155
+ }
156
+ }
157
+ /** 人读渲染:签到结果。 */
158
+ export function renderCheckin(r) {
159
+ const icon = r.outcome === 'claimed' ? '✓' : r.outcome === 'already' ? '·' : '✗';
160
+ return `${icon} ${r.message}`;
161
+ }
162
+ /** 人读渲染:积分账本。 */
163
+ export function renderCredits(l) {
164
+ const lines = [];
165
+ lines.push('MiniMax Code 积分账本');
166
+ lines.push('');
167
+ lines.push(`剩余 ${l.remaining}`);
168
+ lines.push(`已消耗 ${l.consumed}`);
169
+ lines.push(`累计发放 ${l.granted}`);
170
+ if (l.nextExpireAt)
171
+ lines.push(`最近到期 ${new Date(l.nextExpireAt).toISOString().slice(0, 10)}`);
172
+ lines.push('');
173
+ lines.push(`批次明细(${l.grants.length} 笔):`);
174
+ for (const g of l.grants.slice(0, 10)) {
175
+ const exp = g.expireAt ? new Date(g.expireAt).toISOString().slice(0, 10) : '—';
176
+ lines.push(` +${g.granted} 剩 ${g.remaining} 用 ${g.consumed} 到期 ${exp}`);
177
+ }
178
+ if (l.grants.length > 10)
179
+ lines.push(` …(还有 ${l.grants.length - 10} 笔)`);
180
+ lines.push('');
181
+ lines.push('注:上游不回报单次请求消耗,以上为账户级批次累计(精确到 0.01)。');
182
+ return lines.join('\n');
183
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * MiniMax Code 适配器:把「本机客户端登录态」变成「本地 Anthropic/OpenAI 兼容反代」。
3
+ *
4
+ * 与 waygate-qoder 同构(同一个 core 骨架),差异只有三处:
5
+ * · 凭证:读客户端**明文** auth.json(无需解密,比 Qoder 简单);
6
+ * · 协议:上游是 **Anthropic 兼容**(`/mavis/api/v1/llm/v1/messages`),不是自研签名;
7
+ * · ⚠️ 有**凭证互斥**问题(见 README),必须给用户明确告警。
8
+ */
9
+ import { type Adapter, type ModelEntry } from 'waygate-core';
10
+ export declare const DEFAULT_MMCODE_BASE_URL = "https://agent.minimax.cn";
11
+ export declare const DEFAULT_MMCODE_CHAT_PATH = "/mavis/api/v1/llm/v1/messages";
12
+ /**
13
+ * MiniMax 模型清单:上游**没有**清单端点(实测 `/mavis/api/v1/llm/v1/models` 恒 503),
14
+ * 故用内置清单(与 relay 的种子一致)。这是「目录 = 接口清单」原则的**唯一例外** ——
15
+ * 上游确实不提供,不编造其他来源,只列已知可用的这几个。
16
+ */
17
+ export declare const MINIMAX_MODELS: ModelEntry[];
18
+ /** 追加一条 OAuth 行(`~/.waygate/mmcode/oauth.json`),让 token 刷新可独立记录过期时间。 */
19
+ export declare function statePath(): string;
20
+ export declare function createMmcodeAdapter(opts?: {
21
+ baseUrl?: string;
22
+ chatPath?: string;
23
+ }): Adapter;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * MiniMax Code 适配器:把「本机客户端登录态」变成「本地 Anthropic/OpenAI 兼容反代」。
3
+ *
4
+ * 与 waygate-qoder 同构(同一个 core 骨架),差异只有三处:
5
+ * · 凭证:读客户端**明文** auth.json(无需解密,比 Qoder 简单);
6
+ * · 协议:上游是 **Anthropic 兼容**(`/mavis/api/v1/llm/v1/messages`),不是自研签名;
7
+ * · ⚠️ 有**凭证互斥**问题(见 README),必须给用户明确告警。
8
+ */
9
+ import { chmodSync, writeFileSync } from 'node:fs';
10
+ import { commandDir, } from 'waygate-core';
11
+ import { join } from 'node:path';
12
+ import { MinimaxTokenManager, clientAuthFile, credentialPath, readClientCredential, readSavedCredential, saveCredential, } from './credential.js';
13
+ import { CredentialError, UpstreamError } from './errors.js';
14
+ import { MinimaxActivity, renderCheckin, renderCredits } from './activity.js';
15
+ import { anthropicToOpenAiSse } from './protocol/stream-decode.js';
16
+ import { toAnthropicRequest } from './protocol/translate.js';
17
+ export const DEFAULT_MMCODE_BASE_URL = 'https://agent.minimax.cn';
18
+ export const DEFAULT_MMCODE_CHAT_PATH = '/mavis/api/v1/llm/v1/messages';
19
+ /**
20
+ * MiniMax 模型清单:上游**没有**清单端点(实测 `/mavis/api/v1/llm/v1/models` 恒 503),
21
+ * 故用内置清单(与 relay 的种子一致)。这是「目录 = 接口清单」原则的**唯一例外** ——
22
+ * 上游确实不提供,不编造其他来源,只列已知可用的这几个。
23
+ */
24
+ export const MINIMAX_MODELS = [
25
+ { id: 'MiniMax-M3', displayName: 'MiniMax M3' },
26
+ { id: 'MiniMax-M3.1-Flash-Preview', displayName: 'MiniMax M3.1 Flash Preview' },
27
+ { id: 'MiniMax-M2.7', displayName: 'MiniMax M2.7' },
28
+ { id: 'MiniMax-M2.7-highspeed', displayName: 'MiniMax M2.7 Highspeed' },
29
+ ];
30
+ /** 追加一条 OAuth 行(`~/.waygate/mmcode/oauth.json`),让 token 刷新可独立记录过期时间。 */
31
+ export function statePath() {
32
+ return join(commandDir('mmcode'), 'oauth.json');
33
+ }
34
+ export function createMmcodeAdapter(opts = {}) {
35
+ const baseUrl = (opts.baseUrl ?? DEFAULT_MMCODE_BASE_URL).replace(/\/+$/, '');
36
+ const chatPath = opts.chatPath ?? DEFAULT_MMCODE_CHAT_PATH;
37
+ const tokens = new MinimaxTokenManager((line) => process.stdout.write(`[mmcode] ${line}\n`));
38
+ const activity = new MinimaxActivity(() => tokens.accessToken());
39
+ return {
40
+ command: 'mmcode',
41
+ displayName: 'MiniMax Code',
42
+ defaultPort: 8788,
43
+ upstreamLabel: `${baseUrl} (Anthropic 兼容)`,
44
+ // 注意:MiniMax 无需登录态就能列模型 —— 用内置清单(上游无清单端点)。
45
+ /** [专属子命令] MiniMax 的签到与积分 —— 用本工具维护的令牌代签,无需开客户端。 */
46
+ extraCommands: [
47
+ {
48
+ name: 'checkin',
49
+ summary: '每日签到领积分(用本工具令牌代签,无需开客户端)',
50
+ usage: 'checkin',
51
+ async run({ json }) {
52
+ const r = await activity.checkin();
53
+ return json ? JSON.stringify(r, null, 2) : renderCheckin(r);
54
+ },
55
+ },
56
+ {
57
+ name: 'credits',
58
+ summary: '查询积分账本(剩余/已消耗/累计,精确到 0.01)',
59
+ usage: 'credits',
60
+ async run({ json }) {
61
+ const l = await activity.credits();
62
+ return json ? JSON.stringify(l, null, 2) : renderCredits(l);
63
+ },
64
+ },
65
+ ],
66
+ async credentialStatus() {
67
+ const saved = readSavedCredential();
68
+ if (saved) {
69
+ const left = saved.expiresAt ? Math.round((saved.expiresAt - Date.now()) / 60000) : null;
70
+ const state = left !== null && left > 0 ? `access 有效约 ${left} 分钟` : 'access 可能已过期(下次请求会自动刷新)';
71
+ return { ok: true, detail: `${state}${saved.account ? `(账号 ${saved.account})` : ''}`, source: credentialPath() };
72
+ }
73
+ try {
74
+ const c = readClientCredential();
75
+ return { ok: true, detail: `已从客户端读取登录态${c.account ? `(${c.account})` : ''} —— 建议跑一次 mmcode login 落盘`, source: clientAuthFile() };
76
+ }
77
+ catch (err) {
78
+ return { ok: false, detail: err.message };
79
+ }
80
+ },
81
+ async login() {
82
+ const cred = readClientCredential();
83
+ const path = saveCredential(cred);
84
+ /* 独立的状态文件:记录过期时刻,便于 doctor 展示(凭证本体在 credential.json)。 */
85
+ writeFileSync(statePath(), JSON.stringify({ expiresAt: cred.expiresAt, account: cred.account ?? null }, null, 2) + '\n', { mode: 0o600 });
86
+ try {
87
+ chmodSync(statePath(), 0o600);
88
+ }
89
+ catch {
90
+ /* Windows 语义有限 */
91
+ }
92
+ const left = cred.expiresAt ? Math.round((cred.expiresAt - Date.now()) / 60000) : null;
93
+ process.stdout.write(`已读取客户端登录态${cred.account ? `(账号 ${cred.account})` : ''}\n` +
94
+ `已保存到 ${path}\n` +
95
+ (left !== null ? `access token 剩余约 ${left} 分钟(到期自动刷新)\n` : '') +
96
+ `\n⚠️ 提醒:MiniMax 同一账号只能有一个有效会话 —— 本工具与官方客户端同时登录同一账号会互相踢下线。\n` +
97
+ ` 建议给本工具配专用账号(与日常使用的客户端分开)。\n`);
98
+ },
99
+ async listModels() {
100
+ /* 上游无清单端点(实测 503)—— 用内置清单,见 MINIMAX_MODELS 注释。 */
101
+ return MINIMAX_MODELS;
102
+ },
103
+ async doctorChecks() {
104
+ const checks = [];
105
+ /* [复核修正] 此前 .catch(()=>null) 吞掉了所有网络错误 → 「上游连通」恒报 ✓。
106
+ fetch 只在网络不可达(DNS/超时/拒连)时抛错;HTTP 404/502 不抛(反而证明可达)。 */
107
+ try {
108
+ await fetch(`${baseUrl}/mavis/api/v1/llm/v1/models`, { signal: AbortSignal.timeout(8000) });
109
+ checks.push({ name: '上游连通', ok: true, detail: `${baseUrl} 可达` });
110
+ }
111
+ catch (err) {
112
+ checks.push({ name: '上游连通', ok: false, detail: err.message });
113
+ }
114
+ /* 取一次 token:既验证刷新链是否可用,也把轮换写回(doctor 顺手做体检)。 */
115
+ try {
116
+ const t = await tokens.accessToken();
117
+ checks.push({ name: '令牌刷新', ok: true, detail: `可刷新(${t.slice(0, 8)}…)` });
118
+ }
119
+ catch (err) {
120
+ checks.push({ name: '令牌刷新', ok: false, detail: err.message });
121
+ }
122
+ return checks;
123
+ },
124
+ async chat(body, signal) {
125
+ const token = await tokens.accessToken();
126
+ const payload = JSON.stringify(toAnthropicRequest(body));
127
+ const res = await fetch(`${baseUrl}${chatPath}`, {
128
+ method: 'POST',
129
+ headers: {
130
+ authorization: `Bearer ${token}`,
131
+ 'content-type': 'application/json',
132
+ accept: 'text/event-stream',
133
+ 'anthropic-version': '2023-06-01',
134
+ 'user-agent': 'MiniMaxCode',
135
+ },
136
+ body: payload,
137
+ signal,
138
+ });
139
+ if (!res.ok || !res.body) {
140
+ const text = await res.text().catch(() => '');
141
+ if (res.status === 401 || res.status === 403) {
142
+ throw new CredentialError(`上游鉴权失败(HTTP ${res.status})—— 令牌可能已被官方客户端顶掉或过期:在客户端重新登录后跑一次 mmcode login`);
143
+ }
144
+ throw new UpstreamError(`上游 HTTP ${res.status}${text ? `:${text.slice(0, 200)}` : ''}`);
145
+ }
146
+ /* 上游 Anthropic 事件流 → OpenAI SSE(core 负责把流吐给客户端)。 */
147
+ return { stream: res.body.pipeThrough(anthropicToOpenAiSse()), status: 200 };
148
+ },
149
+ };
150
+ }