# 账号、OAuth 与云端工具

> MiniMax 账号的 OAuth 设备码登录、按命名空间落盘的凭据与跨进程协调、Token Plan 额度查询、工具子进程的短期令牌，以及云端工具的调用凭据。

- 作者：David（道雾轩）
- 专栏：MiniMax Code 源码解读（https://daiw.net/manual/minimax-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.net/manual/minimax-code/accounts-and-tools
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

用 Token Plan 跑 MiniMax 自家的模型，先要登录 MiniMax 账号。这条链路横跨好几个包：`packages/oauth-core` 管登录状态、凭据与刷新；`packages/tui/src/auth` 与 `packages/tui/src/account` 把它接到命令行和 TUI；`packages/oauth-lease-protocol` 与 `packages/mcode-tools-host` 把短期令牌借给工具子进程；`packages/agent-tools/src/cloud` 是一组要带账号凭据才能调用的云端工具。官方文档的概括（`docs/architecture.md:20`）：

> MiniMax OAuth Core manages profile credentials, refresh, and logout. mcode-tools-host provides short-lived access tokens to tool subprocesses through a local lease broker.

## 怎么用

| 入口 | 作用 | 出处 |
| --- | --- | --- |
| `mcode login` | 登录中国区账号，打印授权地址与用户码，并尝试打开浏览器 | `packages/tui/src/cli/program.ts:128` |
| `mcode login --region global` | 登录国际区，内部记作 `en` | `program.ts:378` |
| `mcode login --no-browser` | 只打印地址，不开浏览器 | `program.ts:131` |
| `mcode logout`（可带 `--region`） | 撤销 refresh token、删本地凭据，再给出网页端登出地址 | `program.ts:140` |
| `mcode acp login` | 同一套登录，给 ACP 客户端用 | `program.ts:116` |
| `/login`、`/logout` | TUI 里的登录（先选区域）与登出；按是否已登录只显示其一 | `packages/tui/src/tui/commands/catalog.ts:306`、`:313` |
| `/status`、`/usage` | 账号、模型、工作区与 Token Plan 额度 | 见后文 |
| `/checkin` | 领取每日账号奖励 | `catalog.ts:354` |

`mcode login` 与 `mcode logout` 不带 `--region` 时固定作用于中国区，不参考区域偏好（`packages/tui/src/auth/factory.ts:31`、`packages/tui/src/auth/environment.ts:54`），国际区账号登出要写 `mcode logout --region global`。登录时终端打印 `Open:`、`Code:` 两行和一句等待提示（`packages/tui/src/cli/auth-command.ts:36`），授权地址上会追加 `client_surface=tui` 等来源参数（`packages/tui/src/auth/authorization-url.ts:7`）。在 TUI 里选了与当前运行区域不同的区域，登录成功后 TUI 会以新区域重启（`packages/tui/src/auth/application.ts:107`）。

相关环境变量：`MAVIS_REGION`（`cn` 或 `en`）优先于区域偏好；`MCODE_OAUTH_DEVICE_AUTHORIZATION_ENDPOINT`、`MCODE_OAUTH_TOKEN_ENDPOINT`、`MCODE_OAUTH_REVOCATION_ENDPOINT` 可整体替换 OAuth 端点，但必须三个一起给、都是 https（`packages/oauth-core/src/endpoint-config.ts:65`、`:86`）。账号相关文件都在数据目录 `~/.minimax-code` 下：

| 路径 | 内容 |
| --- | --- |
| `auth/prod/cn/mcode-public/auth.json` | 凭据：access token、refresh token、过期时间、代次，权限 0600 |
| `auth/prod/cn/mcode-public/auth-state.json` | 状态机，不许出现任何令牌字段 |
| `auth/prod/cn/mcode-public/auth.lock` | 跨进程锁 |
| `preferences/mcode-region.json` | 各构建环境上次登录的区域 |
| `cli-auth/prod/cn/account-identity.json` | 已核实的账号身份缓存，按令牌指纹对应 |

路径里的 `prod/cn` 随构建环境与区域变化（`packages/oauth-core/src/namespace.ts:22`、`packages/tui/src/auth/region-preference.ts:6`、`packages/tui/src/auth/identity-storage.ts:37`）。

## 设备码登录

OAuth 端点按区域取生产地址：中国区 `https://account.minimax.cn`，国际区 `https://account.minimax.io`，路径分别是 `/oauth2/device/code`、`/oauth2/token`、`/oauth2/revoke`（`endpoint-config.ts:24`、`:51`）。客户端身份写死为 `client_id` 取 `mcode-public`、scope 取 `agent.default`、audience 取 `agent-backend`（`packages/oauth-core/src/contracts.ts:1`）。

流程是 OAuth 2.0 设备授权，外加 PKCE（`packages/oauth-core/src/oauth-client.ts:79`）：

```ts
  async startDeviceAuthorization(options: OAuthRequestOptions = {}): Promise<DeviceAuthorization> {
    const codeVerifier = randomBytes(32).toString('base64url');
    const codeChallenge = createHash('sha256').update(codeVerifier, 'ascii').digest('base64url');
    const body = await this.postForm(
      this.options.deviceAuthorizationEndpoint,
      {
        client_id: MCODE_OAUTH_CLIENT_ID,
        scope: MCODE_OAUTH_SCOPES.join(' '),
        audience: MCODE_OAUTH_AUDIENCE,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256',
      },
      {
        headers: this.options.deviceAuthorizationHeaders,
        signal: options.signal,
      },
    );
```

紧接着的几行兼容 MiniMax 账号服务的另一种回包：没有 `device_code`，只给 `user_code` 和过期字段 `expired_in`（可能是秒数，也可能是毫秒时间戳），这时改用 `user_code` 轮询，间隔按毫秒换算（`oauth-client.ts:96`）。轮询同时认 JSON 里的 `status` 字段和标准的 OAuth 错误码，遇到 `slow_down` 就把间隔加 5 秒（`oauth-client.ts:159`）。拿到的令牌必须是 Bearer 且含 `agent.default`，`sub` 与 `account_id` 从 JWT 负载里解码出来，只解码、不验签（`oauth-client.ts:290`）。

## 凭据、锁与状态机

`MCodeOAuthCore`（`packages/oauth-core/src/auth-core.ts`）是唯一的凭据主人。几条关键设计：

- **命名空间**：凭据按“构建环境/区域/客户端”分目录，中国区与国际区各有一份（`namespace.ts:22`）。
- **文件存储**：凭据库只有一种实现，明文 JSON 的 `auth.json`，目录 0700、文件 0600；在 POSIX 上只要组或其他用户有任何权限位就拒绝读取，Windows 跳过这项检查（`packages/oauth-core/src/credential-store/file-store.ts:105`、`:108`）。写入走“独占创建临时文件、fsync、rename”（`packages/oauth-core/src/fs/atomic-write.ts:12`）。系统钥匙串 `os-keyring` 只作为旧状态里的取值被识别（`packages/oauth-core/src/credential-store/types.ts:4`）。
- **状态文件不藏秘密**：写 `auth-state.json` 前递归检查键名，出现 access token、refresh token、device code、code verifier 之类就抛错（`packages/oauth-core/src/state-store.ts:41`）。状态共九种，从 `anonymous`、`authorizing`、`authenticated` 到 `logout_pending`、`error`（`state-store.ts:10`）。
- **跨进程锁**：`proper-lockfile`，30 秒判为陈旧，最多重试 120 次（`packages/oauth-core/src/cross-process-lock.ts:21`）。
- **登录租约**：开始登录的进程在锁内写入 `authorizing` 和一个 10 分钟的租约（`auth-core.ts:27`）；别的进程此时再登录，只会等待并显示同一个用户码（`auth-core.ts:536`）。提交时代次加一，并生成新的 `loginEpoch`：刷新不变，每次交互登录都换（`auth-core.ts:493`）。

取令牌先走无锁快路径，不够新再进锁刷新（`auth-core.ts:249`）：

```ts
  async getAccessToken(options: AccessTokenOptions): Promise<AccessTokenLease> {
    await this.initialize();
    validateRequestedScopes(options.requiredScopes);
    try {
      const initial = await this.readUsableCredential(options.requiredScopes);
      if (this.satisfiesMinValidity(initial, options.minValidityMs)) {
        return toAccessTokenLease(initial.credential);
      }
    } catch (error) {
      if (!(error instanceof AuthRequiredError)) throw error;
      // A concurrent writer may have committed credentials before their state.
      // Recheck under the domain lock before reporting authentication as missing.
    }

    return this.lock.withLock(async () => {
      const current = await this.readUsableCredential(options.requiredScopes);
      if (this.satisfiesMinValidity(current, options.minValidityMs)) {
        return toAccessTokenLease(current.credential);
      }
      return this.refreshCredential(current);
    });
  }
```

刷新时服务端回 HTTP 400 的 `invalid_grant`，就先把状态写成 `anonymous` 再删凭据，要求重新登录；其他错误只要旧令牌还没过期，会话照常保留（`auth-core.ts:682`、`:727`）。调用方遇到 401 时交回 `handleUnauthorized`（`auth-core.ts:285`）：`loginEpoch` 变了说明中途换过登录，只拒绝这一个过期请求；否则比代次，更新过就直接重试，落后说明已被登出，相同则强制刷新一次再重试。每个进程都用 Node 的 `watchFile` 每 250 毫秒检查一次状态文件，感知别处的登录与登出（`auth-core.ts:364`）。

登出先撤销 refresh token；撤销失败时状态记为 `logout_pending` 并提前返回，凭据文件暂不删除，但这个状态已经取不出令牌（`auth-core.ts:331`）。TUI 随后给出 `/auth/logout` 网页地址，用来清掉浏览器里的登录态（`packages/tui/src/auth/logout-url.ts:23`）。

旧版把状态放在 `auth/` 根目录。迁移只做一件事：把与当前环境、区域匹配的根目录状态标成 `error`，若新命名空间还没有当前版本的状态，就写一份代次加一的 `anonymous` 状态（`packages/oauth-core/src/namespace-migration.ts:9`）。令牌本身不搬，从代码看，老用户升级后要重新登录一次。

登录态进入运行时的方式是“投影”：TUI 启动时向 OAuth Core 要一个至少还剩 60 秒的令牌（`packages/tui/src/runtime/lifecycle.ts:218`），再查一次账号信息补上 `realUserID`、邮箱，放进运行时的认证上下文；此后跟着状态文件的变化更新。

## 区域与环境：公开版只连生产

启动时区域按这个顺序定：显式参数、`MAVIS_REGION`、区域偏好文件、唯一一个有可用登录的命名空间，最后兜底 `cn`（`packages/tui/src/cli/environment.ts:51`）。构建环境则不给用户选：仓库的构建脚本把 `__TUI_BUILD_ENV__` 定义成 `"prod"`（`scripts/build.mjs:107`），启动时据此覆盖进程里的 `MAVIS_BUILD_ENV`（`cli/environment.ts:63`）。切到测试或预发的 `--env` 只在内部包 `@minimax/code` 里可用，其他包名一律报错（`cli/environment.ts:108`，包名见 `packages/tui/src/update/install-source.ts:13`），而构建产物的 `package.json` 写的正是公开包名 `@minimax-ai/code`（`scripts/build.mjs:146`）；指定后端泳道的 `--lane` 只允许 test、staging 构建（`cli/environment.ts:120`）。各处地址表里的非生产项都是 `.example.invalid` 占位（例如 `endpoint-config.ts:24`），`check:source` 还会拦截内部域名（`scripts/source-inventory.mjs:46`）。这与 `docs/architecture.md:24` 的说法一致：

> The public distribution configures production regional services only. Non-production environments retained in shared types use reserved `.example.invalid` placeholders. The public CLI rejects internal environment switches and provides no default connection to internal services.

不过“拒绝切换”管的是构建环境；OAuth 端点与下文的 Matrix 网关地址仍能用环境变量整体替换。另一个小出入：账号信息接口的中国区生产域名是 `https://agent.minimaxi.com`（`packages/tui/src/account/matrix-account-client.ts:9`），登出页与云端工具网关用的却是 `https://agent.minimax.cn`，前者在网关代码里被列为旧托管地址（`packages/local-runtime/src/matrix/matrix-env.ts:23`）。

## Token Plan 额度

`/status` 先渲染本地能拿到的模型、工作区、权限、规则文件，再异步补账号部分（`packages/tui/src/tui/controller/product/feature-flow.ts:1109`）。额度由 `TuiMatrixAccountClient` 查询，最多四步，每步都带 `Authorization: Bearer` 令牌（`matrix-account-client.ts:168`）：

| 步骤 | 接口 | 取什么 |
| --- | --- | --- |
| 1 | `GET /v1/api/user/info` | `realUserID`、邮箱、用户名 |
| 2 | `POST /matrix/api/v1/user/get_user_extra_info` | 个人工作区（`workspace_type` 为 0）及其 `op_group_id` |
| 3 | `POST /matrix/api/v1/commerce/get_membership_info` | 是否订阅 Token Plan、档位、到期时间、余额 |
| 4 | `GET /v1/api/openplatform/coding_plan/remains` | 额度，请求头带 `X-Group-Id` |

前三步发往 Agent 站点（中国区 `https://agent.minimaxi.com`，国际区 `https://agent.minimax.io`），第四步发往开放平台（中国区 `https://www.minimaxi.com`，国际区 `https://platform.minimax.io`），见 `matrix-account-client.ts:22`、`:42`。单次请求超时 3 秒，会员信息缓存 30 秒，额度缓存 5 秒（`matrix-account-client.ts:27`）。发往 Agent 站点的请求还带 `yy`、`x-timestamp`、`x-signature` 三个头，由两段写死的字面量算出；注释说明它们只标记“第一方客户端”，不是凭据，鉴权靠的仍是 Bearer（`matrix-account-client.ts:32`）。

额度取 `model_remains` 的第一项：`current_interval_*` 是 5 小时窗口，`current_weekly_*` 是周窗口，状态值 3 表示不限量，剩余百分比优先取服务端给的值，否则按用量与总量换算（`matrix-account-client.ts:672`）。`/status` 的 Quota 一节显示 5-hour 与 Weekly 两行，视频额度只在 `/usage` 里出现（`packages/tui/src/tui/features/inspection/product-inspection.ts:196`、`:585`）；没订阅就显示一行警告。任一窗口剩余不到 30% 时，状态栏右侧会出现提醒，不到 10% 变红（`packages/tui/src/tui/shell/chrome.ts:526`、`:559`）。

## 给工具子进程发短期令牌

多模态能力走的是 bash 里的 `mcode-tools` 命令（怎样放上 PATH 见[工具体系](https://daiw.net/manual/minimax-code/tool-system)）。子进程要以用户身份调后端，但 refresh token 不能离开宿主，README 写得很直接（`packages/mcode-tools-host/README.md:7`）：

> The host remains the only credential-store, Refresh Token, refresh and logout owner.

办法是在本机起一个租约代理：

- **端点**：TUI 在 `/tmp` 下建一个 `mcode-tools-tui-<pid>-` 开头的 0700 私有目录（Windows 用系统临时目录），因为 macOS 的 Unix socket 路径很短，数据目录一长就放不下（`packages/tui/src/runtime/mcode-tools-integration.ts:149`）。socket 是其中的 `run/mcode-auth-lease-v1.sock`，权限 0600；Windows 上是 `\\.\pipe\mcode-auth-lease-` 加 16 位哈希的命名管道（`packages/oauth-lease-protocol/src/endpoints.ts:6`）。
- **能力口令**：代理启动时生成 32 字节随机数，base64url 编码后正好 43 个字符，原子写进同目录的 `mcode-auth-lease-v1.cap`，权限 0600（`packages/mcode-tools-host/src/lease-broker.ts:46`、`:145`）。每个请求都要带上它，服务端用 `timingSafeEqual` 比对（`packages/oauth-lease-protocol/src/node-server.ts:126`）。
- **线协议**：4 字节大端长度加一段 JSON，单帧不超过 64 KiB；只有 `status`、`lease`、`unauthorized` 三个方法，`lease` 要求的最短剩余有效期不超过 5 分钟（`packages/oauth-lease-protocol/src/contracts.ts:6`、`:14`）。解析一律严格：多一个键就拒绝，错误消息只能是固定文本。服务端单连接空闲 30 秒断开，最多 32 个连接、每连接 256 个请求（`node-server.ts:16`）。

代理的处理函数只做三件事（`lease-broker.ts:126`）：

```ts
  async function handleRequest(request: AuthLeaseRequest): Promise<AuthLeaseSuccessResult> {
    if (disposed) throw new AuthLeaseProtocolError('BROKER_UNAVAILABLE');
    if (request.method === 'status') return await getCredentialFreeStatus(options.session);
    if (request.method === 'lease') {
      const lease = await acquireLease(request.minValidityMs);
      return {
        method: 'lease',
        accessToken: lease.accessToken,
        expiresAtMs: lease.expiresAtMs,
        generation: lease.generation,
        audience: AUTH_LEASE_AUDIENCE,
        scopes: AUTH_LEASE_SCOPES,
      };
    }
    invalidate();
    const action = await options.session.handleUnauthorized(request.generation);
    return { method: 'unauthorized', action };
  }
```

`lease` 背后就是 OAuth Core 的 `getAccessToken`（`lifecycle.ts:654`），代理自己缓存一份并合并并发请求。它也订阅登录状态：一旦不再是 `authenticated` 或 `refreshing` 就关闭发放、清空缓存，并记住见过的最高代次，不再发更旧的令牌（`lease-broker.ts:152`）。子进程这一侧，入口会先删掉继承来的 `MCODE_API_BASE_URL`、`MCODE_AUTH_BASE_URL`、`MCODE_CLIENT_ID`、`MCODE_SCOPE` 等变量，再写入 `MCODE_AUTH_PROVIDER=shared-broker` 与代理坐标（`packages/tui/src/cli/mcode-tools-environment.ts:92`、`:124`），继承来的环境变量改不了它连哪个后端、用哪种认证。宿主校验内嵌产物时，也要求清单声明 `shared-broker` 模式、协议版本一致，生产包名必须是 `@minimax/mcode-tools`（`packages/mcode-tools-host/src/resource.ts:338`、`:365`）。

`mcode-tools-host` 里另有一套往数据目录 `bin/` 下安装分区域启动器的流程（`packages/mcode-tools-host/src/integration.ts:45`），公开仓库里没有调用方，TUI 用的是自己的 `internal-bin` 方案。

## 云端工具

`packages/agent-tools/src/cloud/matrix-tools` 定义了 18 个工具：`web_search` 加上 `MATRIX_TOOL_NAMES` 里的 17 个（`packages/agent-tools/src/cloud/matrix-tools/index.ts:137`）。

| 类别 | 工具 |
| --- | --- |
| 搜索 | `web_search`、`images_search_and_download`、`image_reverse_search` |
| 理解 | `images_understand`、`videos_understand`、`audios_understand`、`transcribe_audio` |
| 图像生成 | `image_synthesize` |
| 视频生成 | `submit_video_generation`、`query_video_generation`、`gen_videos`、`batch_text_to_video`、`batch_image_to_video` |
| 语音与音乐 | `get_voice_list`、`synthesize_speech`、`batch_synthesize_speech`、`batch_text_to_audio`、`batch_text_to_music` |

每个工具对应网关上的 `/matrix/api/v1/mcp/<工具名>`，只有 `transcribe_audio` 映射到 `listen_audio`（`packages/agent-tools/src/cloud/matrix-tools/tool-defs.ts:571`、`:589`）；生成类超时 10 分钟，其余 2 分钟（`tool-defs.ts:604`）。输入输出都用工作区里的本地路径：工具先把输入文件传到对象存储、换成一小时有效的签名 URL，再把结果下载回指定路径（`tool-defs.ts:7`）。失败时只返回固定文案，不透传服务端消息（`packages/agent-tools/src/cloud/matrix-tools/client.ts:63`）。当前模型能原生看图、看视频时，`images_understand`、`videos_understand` 会被撤掉（`index.ts:312`）。

凭据怎么带：请求头统一是 `Authorization: Bearer <令牌>`。发往托管网关 `https://agent.minimax.cn` 或 `https://agent.minimax.io` 时，令牌取当前登录的 access token；用 `MATRIX_BASE_URL` 指向非托管地址时，必须另设 `MATRIX_TOKEN`（`packages/agent-tools/src/desktop/matrix-client.ts:194`，地址表在 `matrix-env.ts:13`）。路径还会从 `/matrix/api/v1/mcp/` 改写成 `/mavis/api/v1/mcp/`（`matrix-client.ts:226`）。

在命令行版里真正在用的只有 `web_search`：它对 `electron`、`tui`、`cli` 三种宿主是原生工具（`packages/local-runtime/src/api/host-turn-tools.ts:82`），直接用上面的客户端调网关（`packages/local-runtime/src/web-search/local-web-search-client.ts:26`）。其余 17 个原本经内置的 Matrix MCP 子进程暴露，令牌通过环境变量 `MAVIS_MATRIX_ACCESS_TOKEN` 传给子进程（`packages/local-runtime-v2/src/service/mcp/runtime/builtin-matrix.ts:124`），但公开运行时固定传 `builtinMatrix: false`（`packages/local-runtime-v2/src/compat/v1/runtime.ts:580`），这条路没有打开，图像、视频、语音改由 `mcode-tools` 的 Connector 提供。`mcode-tools` 的源码不在仓库里，它背后接了哪些服务无从核实；从内置技能看，至少包括云盘资产的下载地址与本地文件的临时上传（`packages/local-runtime/assets/skills/mcode-tools-master/SKILL.md:36`）。

下一篇：[TUI 引擎](https://daiw.net/manual/minimax-code/tui-engine)——产品界面底下那份 pi-tui 分叉怎样重绘、布局与解析输入。
