# 遥测、更新与诊断

> 三个默认关闭、逐项同意的遥测通道，本地的事故记录与观测日志，签名清单与多种安装来源下的自更新，每日签到，以及排查问题用的诊断命令。

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

模型请求之外，`mcode` 进程还有几类“产品自己的流量”：使用统计、运行时指标、错误诊断、更新检查、签到和用户反馈。源码导入当天的提交 `36bd09392a0b`（2026-09-18，“fix: require separate consent for automatic telemetry uploads”）把其中所有自动上传都改成了默认关闭、逐项同意。本篇先讲这几个通道现在的行为，再讲自更新、签到与诊断入口。

## 怎么用

三个开关都在 `config.yaml` 的 `telemetry` 段。`docs/telemetry.md:10` 给的示例只打开了使用事件：

```yaml
telemetry:
  enabled: true      # TUI usage events (anonymous, described below)
  metrics: false     # Runtime counters, gauges, and histograms
  diagnostics: false # Account-linked TUI and LLM error diagnostics
```

- 配置文件在数据目录（默认 `~/.minimax-code`，`packages/tui/src/runtime/data-dir.ts:21`）的 `config.yaml` 里。三个键缺省都是 `false`（`packages/config/src/config.ts:1772`），`metrics` 与 `diagnostics` 只有写成布尔值 `true` 才算同意（`config.ts:2105`）。
- 环境变量 `MCODE_DISABLE_TELEMETRY` 或 `DO_NOT_TRACK` 取 `1`、`true`、`yes`、`on`（不分大小写）时三个通道全关，优先于配置文件（`packages/config/src/telemetry-policy.ts:5`）。
- `mcode telemetry status` 打印每个通道的生效状态、被哪个环境变量挡住、配置文件路径；`mcode telemetry preview` 打印一条解码后的示例请求（固定是 `tui_launch`），不会发送，使用事件没开时 `request` 为 `null`（`packages/tui/src/cli/telemetry-command.ts:26`）。
- 更新：TUI 每次启动都在后台检查，有新版就在欢迎页提示；`/update` 在 TUI 里升级（要输入搜索才出现在菜单里），`mcode update` 在命令行升级，交互终端里会先问一句 `[y/N]`（`packages/tui/src/cli/update.ts:70`）。代码里没有找到关闭启动检查的开关。
- 签到用 `/checkin`；诊断命令见文末。

## 三个通道

| 通道 | 配置键 | 内容 | 发往 | 身份 |
| --- | --- | --- | --- | --- |
| 使用事件 | `telemetry.enabled` | 启动、登录、发消息、打开斜杠与 `@` 菜单等 10 种事件 | 按地区与构建环境选的 `meerkat-reporter` 地址 | 每条事件一个新的随机 ID，不带账号 |
| 运行时指标 | `telemetry.metrics` | 计数、仪表、直方图与少量低基数标签 | Matrix 的指标批量接口 | 文档称不附账号凭据 |
| 错误诊断 | `telemetry.diagnostics` | TUI 事故与 LLM 请求失败的最小化摘要 | `/minimax-cloud/api/v1/observability/desktop-errors/batch` | Bearer token 加 `user_id` 参数，绑定账号 |

后两个通道共用一个开关函数（`telemetry-policy.ts:11`）：

```ts
export function isTelemetryChannelEnabled(
  channel: TelemetryChannel,
  readConfigured: () => boolean | undefined = () => getConfig().telemetry[channel],
): boolean {
  for (const key of TELEMETRY_OPT_OUT_ENV) {
    if (/^(?:1|true|yes|on)$/iu.test(process.env[key]?.trim() ?? '')) return false;
  }
  try {
    return readConfigured() === true;
  } catch {
    return false;
  }
}
```

两条规则：环境变量先于一切；配置读不出来按“未同意”处理。使用事件走另一个函数 `resolveMcodeBusinessTelemetryPolicy()`，环境变量的判定一样（`packages/tui/src/analytics/business-telemetry.ts:126`），但只在启动时读一次配置，不开就根本不创建客户端（`packages/tui/src/tui/launcher.ts:237`），所以改完要重启。诊断通道则每次记录、每次上传前都现查一次；`/reload` 会清掉配置缓存，从代码推断，改了 `diagnostics` 之后执行一次 `/reload` 就能生效。

## 这个提交改了什么

按 `36bd09392a0b` 的 diff 对比：

| 通道 | 之前 | 现在 |
| --- | --- | --- |
| 运行时指标 | 托管运行时一律创建云端上报器；生产与预发构建都算托管（`config.ts:166`） | 还要 `telemetry.metrics` 为真（`packages/local-runtime/src/runtime/host-metrics.ts:112`） |
| LLM 请求失败报告 | 登录即上传 | 进缓冲前、加密发送前各查一次（`packages/local-runtime/src/error-reporting/reporter.ts:107`、`reporter.ts:65`） |
| TUI 事故 | 写成待上传文件，登录后自动上报 | 未同意时写成只留本地的文件；排空和每批发送前再查一次（`packages/tui/src/observability/incident-reporter.ts:320`） |
| 使用事件 | 已经默认关闭 | 不变 |

指标与 LLM 报告的装配在第一代宿主的 `host-factory.ts` 里，运行时 v2 启动时会在内部建一个 v1 宿主（`packages/local-runtime-v2/src/runtime.ts:254`），所以命令行产品同样受这两处约束。

同一天的配套提交 `30dd6f27f1b0` 改写了 `docs/telemetry.md`，并让 `mcode telemetry status` 列出三个通道。更新日志却没跟上：中文更新日志 0.2.5 仍写着“TUI 会在本地有界保存经过脱敏的异常退出与生命周期故障信息，并在登录后自动上报”（`packages/tui/CHANGELOG.zh-CN.md:388`），这句话已不符合现在的代码，后面的版本也没有记这次改动。

## 使用事件

- **白名单**：只有 10 种事件，每种只取固定的几个属性，其余一概丢弃（`EVENT_PROPERTY_KEYS`，`business-telemetry.ts:111`）。每条都附 `surface`、`os`、`region`、`build_env`、`app_version` 五个公共字段。
- **没有持久身份**：`$identity_cookie_id` 与 `distinct_id` 每条事件现生成一个 UUID（`business-telemetry.ts:222`），只为满足接收端要求的信封格式，两条事件之间无法据此关联。
- **只在内存里排队**：最多 100 条，满了丢最旧的（`business-telemetry.ts:184`）；在微任务里逐条 POST，每条 3 秒超时，失败直接吞掉；退出时 `flush()` 一次。
- **编码**：请求体是表单，`data` 为 Base64 编码的 JSON，`ext` 为 `crc=` 加一个 Java 风格的字符串哈希（`business-telemetry.ts:266`）。

TUI 这边的取值：`chat_type` 在会话里有子 Agent 时报 `agent_team`，否则报 `chat`（`packages/tui/src/tui/app-composition.ts:514`）；`@` 菜单的点击只会报 `file` 或 `directory`，类型定义里的其余几种取值 TUI 用不到（`packages/tui/src/analytics/tui-business-event-tracker.ts:84`）；`tui_launch` 在首帧画出后报 `cold`，从挂起恢复时报 `hot`（`launcher.ts:547`）。命令行的 `mcode login` 不传遥测客户端，走那条路登录不产生事件（`packages/tui/src/cli/auth-command.ts:75`）。

## 错误诊断与本地记录

诊断相关的本地文件都在数据目录的 `v2/observability` 下：

| 子目录 | 内容 | 会不会上传 |
| --- | --- | --- |
| `cli/incidents` | TUI 事故记录（`incident-reporter.ts:130`） | 同意诊断且已登录时批量上传 |
| `mcode` | TUI 观测日志：启动各阶段耗时、终端画像、主题探测、事件流重连、进程停止原因，保留 7 天（`packages/tui/src/observability/local-observability.ts:168`） | 不自动上传 |
| `logs` | 运行时日志（`packages/tui/src/runtime/logging.ts:41`） | 不自动上传 |

三类文件都会在用户主动 `/feedback` 时以计数形式进入诊断包（只留本地的事故记录也算在内），见文末。观测日志里预留了采样渲染耗时的 `observeRender()`，但公开源码里没有任何调用方。

TUI 事故记录器的流程：

```mermaid
flowchart TD
  C["capture：最小化为 schema 2"] --> Q{"已同意 telemetry.diagnostics？"}
  Q -->|否| L["写 local-*.json，只留本地"]
  Q -->|是| P["写 pending-*.json"]
  P --> D{"排空时再查同意，并取登录凭据"}
  D -->|不满足| W["留待下次"]
  D -->|满足| S["每批 20 条，加密后 POST"]
  S --> SENT["改名为 sent-*.json"]
```

要点：

- **记什么**：9 种事件类型，从启动失败、渲染失败、运行时桥接错误到上次非正常退出（`TuiIncidentEventType`，`incident-reporter.ts:20`）。只留固定的类别、阶段、严重度、错误名与错误码、HTTP 状态、最多 20 个预定义的面包屑，自由文本、堆栈、调用方上下文一概不收；具体字段见 `docs/tui-capabilities.md` 的“Diagnostic upload privacy”。
- **限流**：每次运行最多 100 条非致命事故，同一指纹 60 秒内去重（致命事故 2 秒）；目录里最多留 200 个文件、7 天（`incident-reporter.ts:136`），清理时先删 `local-`，再删 `sent-`，最后才动 `pending-`。
- **非正常退出**：启动时写一个 `active-` 开头的标记文件，正常结束时删掉；下次启动若发现对应进程已不在，就补记一条 `cli_unclean_exit`（`incident-reporter.ts:457`）。
- **上传**：只读 `pending-` 开头的文件（`incident-reporter.ts:743`），发送前重新做一遍最小化；事件日志用 AES-256-GCM 加密，密钥由访问令牌经 HKDF 派生，用户 ID 作为 HKDF 的 info 参数（`incident-reporter.ts:715`）；请求带 Bearer 令牌，URL 上带 `user_id`，单次 10 秒超时，退出时最多再等 300 毫秒。

所以即便内容已经最小化、又做了加密，这个通道仍然是绑定账号的，`docs/telemetry.md` 也这样写。另一个小差别是国内区的主机：TUI 事故发往 `agent.minimaxi.com`（`incident-reporter.ts:151`），LLM 失败报告发往 `agent.minimax.cn`（`packages/local-runtime/src/error-reporting/matrix-api-client.ts:51`），路径相同；文档只笼统说“regional MiniMax host”。LLM 失败报告本身只接受 `llm_request_failure` 一种事件，明文超过 16 KiB 的直接丢弃，不截断（`reporter.ts:123`）。

## 自更新

启动检查的入口在 `app.start()`（`packages/tui/src/tui/app.ts:790`），更新模块按需动态加载，结果只用来在欢迎页显示一个新版本号（`packages/tui/src/update/startup-notice.ts:7`）。真正的逻辑在 `McodeUpdateApplication`（`packages/tui/src/update/application.ts:141`），先判断这份 `mcode` 是怎么装上的（`packages/tui/src/update/install-source.ts:75`）：

| 安装来源 | 怎么认出来 | 怎么升级 |
| --- | --- | --- |
| 官方安装器 | 安装根目录的 `install.json` 里 `updateOwner` 为 `mcode-installer` | 拉签名清单，下载校验后装进新版本目录 |
| npm 前缀 | 前缀下的 `install.json` 回执写着 `npm-prefix`，或前缀目录名为 `.minimax-code` 且 Node 所在目录里有 npm | 在暂存目录里 `npm install`，校验后切换指针 |
| pnpm、yarn、bun 全局 | 包路径匹配各自的全局目录 | 运行对应的全局安装命令 |
| npm 全局 | 包路径等于 `npm prefix --global` 下的位置 | `npm install --global` |
| 认不出 | 以上都不是 | 只打印手动升级命令 |

官方安装器的安装根取 `MCODE_INSTALL_ROOT`；没设时，Windows 上是 `LOCALAPPDATA` 下的 `MinimaxCode`，其他平台是 `XDG_DATA_HOME`（缺省为主目录下的 `.local/share`）下的 `minimax-code`（`packages/tui/src/update/service.ts:260`）。升级时：

1. 按 `update.json` 选渠道（`stable` 或 `preview`，缺省 `stable`），并行下载 `channels/<渠道>.json` 与同名的 `.sig`，8 秒超时（`service.ts:103`）。清单地址的缺省值是一个阿里云 OSS 路径（`service.ts:25`）。
2. 用 `install.json` 里记的公钥（或 `MCODE_RELEASE_PUBLIC_KEY_FILE` 指定的文件）调 `crypto.verify(null, …)` 验签，算法由公钥类型决定（`packages/tui/src/update/release.ts:69`）；再核对渠道、版本与当前平台是否有包。
3. 下载安装包，比对大小与 SHA-256（`service.ts:434`），在 `versions` 目录下的暂存目录里安装、验证版本号，改名为以版本号命名的目录，最后改写 `current` 文件（`service.ts:447`）。任何一步失败都删掉暂存目录，旧版本不动。

npm 前缀安装走类似的“暂存、校验、切指针”：在前缀的 `releases` 目录下建一个暂存目录执行 `npm install`，版本和入口都验过之后改名为以版本号命名的目录，最后原子地写入 `current`，代码注释称之为提交点（`packages/tui/src/update/versioned-prefix.ts:119`）。npm 的安装命令都带 `--ignore-scripts=false --include=optional --allow-scripts=<包名>,better-sqlite3` 与显式的 `--registry`（`install-source.ts:201`），与 README 的安装命令一致。包管理器类的来源在每次启动检查时都要跑一次 `npm view <包>@<标签> version --json` 查最新版本（`install-source.ts:276`）。

TUI 里的 `/update` 还多一道闸：当前会话有回合在跑、有排队消息、有待回答的提问或待处理的审批时一律不让升级（`packages/tui/src/tui/controller/product/update-admission.ts:17`）。

## 签到

`/checkin` 先确认已登录，没登录就先弹登录，登录完接着签到（`packages/tui/src/tui/controller/product/command-flow.ts:1449`）。之后由 `TuiDailyCheckinApplication` 执行，同一时刻只跑一个请求（`packages/tui/src/checkin/application.ts:34`）：先 GET `/minimax-cloud/api/v1/signin/status` 取七天的签到面板，今天已签就直接返回，没有可签的日子就报暂不可用，否则 POST `/minimax-cloud/api/v1/signin/claim`（`packages/tui/src/checkin/http-gateway.ts:17`）。网关请求 30 秒超时，遇到 401 强制刷新令牌重试一次；两次请求之间若换了账号就中止，提示重试。

`packages/shared/src/daily-signin.ts` 是这套协议的共享定义：天的状态、领取结果、面板场景三个枚举，面板与领取结果的校验（必须正好 7 天、编号 1 到 7 不重复、可领取与“今天”各至多一天，`daily-signin.ts:92`），以及连签天数的计算（`daily-signin.ts:70`）。它放在跨产品共用的 `shared` 包里，但公开源码中只有 TUI 的签到模块引用它；界面上的输出是“Checked in: Cycle day N, +P Credits”加一行本周期的 ✓ 与 · 进度（`packages/tui/src/checkin/presentation.ts:11`）。

## 诊断命令

| 入口 | 看什么 |
| --- | --- |
| `/doctor` | 只读的配置检查：`config.yaml` 是否有效、路径、文件在不在；有效时显示默认模型与提供方，否则列出问题、影响与下一步（`packages/tui/src/tui/features/inspection/product-inspection.ts:366`） |
| `/status` | 版本、模型与推理强度、账号、Token Plan 的 5 小时与每周额度、权限模式、Git 信息、生效的指令文件 |
| `/context` | 运行时上下文快照，按系统提示词、记忆、工具、技能、消息、其他分别统计 token（`packages/tui/src/tui/transcript/model.ts:75`） |
| `/usage` | 本会话的输入输出 token、缓存命中与上下文占用 |
| `/feedback` | 需登录；先预览再提交，附带一个诊断包 |
| `mcode telemetry status` | 三个遥测通道的生效状态 |

`/feedback` 的诊断包从当前会话的记录，以及最近两天的运行时日志、观测日志、运行时事件、CLI 日志与事故记录里收集（`packages/tui/src/runtime/feedback/diagnostic-upload.ts:198`），但每个文件只留角色、状态、错误类型、日志级别、HTTP 状态这类计数，文件名换成不透明的编号，策略名为 `diagnostic-counts-v1`（`diagnostic-upload.ts:134`）。反馈文字本身会发送出去，只去掉控制字符并把识别出的凭据打码（`packages/tui/src/runtime/feedback/ticket.ts:177`）。这样的包没法用来复现具体的回答或看原始堆栈，`docs/tui-capabilities.md` 对此有明确说明。

## 与文档对照

`docs/telemetry.md` 与代码基本一致：默认全关、逐项同意、环境变量优先、未同意时事故只留本地且 LLM 报告在缓冲前丢弃，都能在代码里对上。几处文档没写或写得笼统的地方：

- 运行时指标除了 `telemetry.metrics`，还要求是托管运行时（`host-metrics.ts:112`）；`mcode telemetry status` 只按配置与环境变量报告，不反映这一层，也不反映是否已登录。
- 国内区两类诊断发往不同的主机，见上文。
- 中文更新日志 0.2.5 那句“登录后自动上报”已经过时。
- `docs/tui-capabilities.md` 说更新“public packages use public npm”，这对 npm 类安装成立；官方安装器那条路拉的是 OSS 上的签名清单，安装包地址写在清单里。

下一篇：[回顾](https://daiw.net/manual/minimax-code/recap)——把前面各部分串起来再看一遍。
