---
title: "接入诊断与故障协作"
description: "诊断分为项目视图、平台视图和可选的应用上报。系统记录观察到的事实，不把 HTTP 状态码、没有后续请求或应用自报直接当作责任归属。诊断采集或告警不可用不会阻止正常登录。"
url: "https://auth.kai.com/docs/diagnostics"
language: "zh-CN"
version: "Better Auth 1.7.1"
updated: "2026-10-19"
---

# 接入诊断与故障协作

诊断分为项目视图、平台视图和可选的应用上报。系统记录观察到的事实，不把 HTTP 状态码、没有后续请求或应用自报直接当作责任归属。诊断采集或告警不可用不会阻止正常登录。

## 项目与平台能看到什么

项目控制台的“接入诊断”按 Organization → Project → Application 限定访问。具备项目查看权限的成员及组织角色可查询本项目最近 30 天的流程。筛选、统计、详情与 CSV/JSONL 导出使用相同的服务端授权和字段投影；知道 `requestId` 或 `flowId` 不会获得查看权限。平台管理员的诊断视图保留最近 90 天的安全运行信息。

项目侧可见应用、时间、阶段、请求编号、粗粒度结果、可执行的错误提示以及与本流程匹配的平台故障处理状态。用户成功同意授权后才会出现项目内用户代号；同一用户跨项目得到不同代号。代号仍可用于同项目内关联活动，不属于不可追踪的完全匿名数据，也没有邮箱反查入口。项目侧不显示姓名、邮箱、手机号、平台用户 UUID、完整 IP、User-Agent、账号是否存在、使用的登录方式、安全因子或其他应用活动。登录与额外验证的内部细节在项目视图统一为身份认证阶段。

平台管理员可见受控内部原因、依赖、发布版本和具体诊断阶段，但密码、验证码、授权码、access/refresh/ID token、Cookie、PKCE verifier、完整 OAuth 查询及上游用户资料均不进入诊断记录。平台故障的内部备注不会出现在项目视图、公共错误或 Lark 卡片里。读取、导出和故障处理修改都会留下管理审计记录。

| 分类            | 表示什么                                                | 不代表什么                                          |
| --------------- | ------------------------------------------------------- | --------------------------------------------------- |
| 开发者请求/配置 | Auth 校验到 scope、回调地址、客户端或协议请求不满足契约 | 公开 client_id 被使用不证明请求一定来自开发者服务器 |
| 平台            | Auth 内部数据库、执行或令牌签发等已观察到的技术异常     | 不推断第三方应用内部情况                            |
| 依赖            | Auth 与身份提供方或其他依赖的交互失败                   | 连接超时不直接证明依赖厂商自身故障                  |
| 用户/业务规则   | 取消授权、身份验证未完成或已知账号规则拒绝              | 不属于服务故障，也不把密码输错逐条发群              |
| 未确定          | 目前没有足够证据说明原因                                | 不默认归责给开发者                                  |

“授权码已签发”只表示 Auth 已产生回调；不能证明用户浏览器成功收到回调。“令牌已签发”只表示 Auth 完成令牌请求；不能证明应用已创建本地会话。没有后续请求显示为“未观察到”，不是“应用服务器出错”。应用主动提交的记录始终标记为“应用自报”，不计入 Auth 技术失败告警。

流程的 `completion` 与单个步骤的 `outcome` 分开：授权回调发出但没有观察到令牌兑换显示“尚未观察到令牌兑换”；令牌兑换成功但没有应用会话结果显示“应用会话结果未知”。“结果待确认”统计包括这些未完成观测的流程。普通个人中心登录和刷新在 Auth 完成其负责的处理后可以完成，不要求不存在的应用会话上报。应用报告会话创建成功始终标明“应用自行上报”，不能作为 Auth 独立证明的端到端成功。

## 请求编号与流程编号

`requestId` 是单个服务端请求的编号，`flowId` 关联一次已验证的 OAuth 授权流程。浏览器的多标签和不同授权请求不会仅因用户相同而合并。项目归属来自服务端验证的应用登记与绑定，不信任调用方自报的项目 ID 或关联请求头。授权码兑换与刷新由服务端 grant 关系关联，不记录凭据原文。每次 refresh 使用新的独立 flow；平台内部可通过 `grantReference` 关联同一授权记录，项目视图不提供该字段。它是已有授权记录标识的摘要，不是 token，也不是可兑换的授权码。

登录页遇到 Lark 等外部身份提供方错误时，使用对应的稳定错误码和本地化操作说明；HTTP 302 跳转成功与业务认证失败分别记录。账号关联冲突不再被等同于“获取资料失败”。

## 可选：补齐应用回调后的观测

普通 OAuth 接入不需要改变。需要查看应用内部阶段时，在原有 authorize 请求增加 `kai_diagnostics=1`。Auth 只向已验证登记的回调地址附加 `kai_flow_id` 和 `kai_diagnostic_token`，原有 `state`、授权码和错误语义保持不变。

诊断 token 是仅能向单次流程追加事件的短期凭据，15 分钟有效、最多 12 个唯一事件，不能获取账号、会话或令牌。仅保存在应用回调处理期间的内存中，不放入日志、分析系统、localStorage 或持久 Cookie。网页应用应立即从地址栏移除两个诊断参数；服务端应避免访问日志收集回调查询。不能把 `kai_flow_id`、诊断 token 或诊断成功当成 OAuth 登录的真实性证明；仍必须正常验证 state、PKCE、nonce、issuer、audience 和令牌签名。

`POST /api/auth/diagnostics/report` 使用 `Authorization: Bearer <kai_diagnostic_token>`。正文只接受 `eventId`、`code`、`stage`、`outcome` 及可选 `durationMs`、`requestId`，不支持任意日志文本、邮箱或 URL。同一 `eventId` 和同一正文重试幂等；相同 ID 配不同内容会拒绝。客户端不能自行指定项目或责任分类。

| code                                          | stage                   | outcome               |
| --------------------------------------------- | ----------------------- | --------------------- |
| `CALLBACK_RECEIVED`                           | `callback_received`     | `success`             |
| `CALLBACK_REJECTED` / `STATE_MISMATCH`        | `callback_received`     | `failure`             |
| `TOKEN_RECEIVED` / `TOKEN_EXCHANGE_FAILED`    | `token_exchange`        | `success` / `failure` |
| `TOKEN_VALIDATED` / `TOKEN_VALIDATION_FAILED` | `id_token_validation`   | `success` / `failure` |
| `SESSION_CREATED` / `SESSION_CREATION_FAILED` | `session_establishment` | `success` / `failure` |
| `CLIENT_CANCELLED`                            | `session_establishment` | `cancelled`           |
| `CLIENT_UNKNOWN_FAILURE`                      | `session_establishment` | `failure`             |

下例使用仓库 `examples/auth-diagnostics/report.mjs` 的完整辅助函数。`finishExistingOAuthLogin` 是应用现有的登录完成入口，必须验证 state、PKCE、nonce、issuer、audience、签名并成功创建应用会话。诊断参数在交给该入口之前从地址栏移除，标准 OAuth 参数保持不变；服务端回调同样不要把查询串写入访问日志。

```js
import { takeDiagnosticCallback, createDiagnosticReporter } from "./report.mjs"

const callback = takeDiagnosticCallback(window.location.href)
window.history.replaceState(null, "", callback.cleanUrl)
let reporter = null
try {
  reporter = createDiagnosticReporter({
    authOrigin: "https://auth.kai.com", // 来自应用配置，不能读取回调参数
    token: callback.token,
  })
} catch {
  // 可选诊断配置错误不阻断正常 OAuth；不记录回调或凭据。
} finally {
  callback.token = null
}

const session = await finishExistingOAuthLogin(callback.cleanUrl).catch(
  error => {
    reporter?.dispose()
    throw error // 保留原本的 OAuth/业务会话失败。
  },
)
if (reporter) {
  const reporting = reporter
  reporter = null
  void reporting.report("SESSION_CREATED").finally(() => reporting.dispose())
}
```

辅助函数在内存中保留诊断凭据，15 分钟后自动清除；登录失败立即释放，登录成功的可选上报在后台完成后释放。上报不延迟 `session` 的使用，不影响已有 OAuth 的校验或错误处理。

使用应用配置的 Auth 地址，不能从回调参数决定投递地址。该上报端点允许不携带 Cookie 的跨域请求；Bearer 凭据只可追加本次流程的有限事件，不提供诊断读取权限。429、网络失败或服务停机时不循环重试，不延长登录耗时。返回成功仅表示 Auth 接受了一条“应用自报”，不证明应用正确完成认证。

服务端和原生应用使用相同 HTTP 契约。`requestId` 和 `eventId` 必须是 UUID；`durationMs` 为 0–900000 的整数。不要上传 stack、姓名、邮箱、URL 或自由文本。仓库中的 `examples/auth-diagnostics/report.mjs` 提供无依赖完整辅助函数，支持最多 12 次上报、2 秒超时、凭据过期和主动清理；对应测试使用 `node --test examples/auth-diagnostics/report.test.mjs`。

诊断错误可以按下表处理，具体业务 OAuth 错误仍按[错误码文档](/docs/errors)处理：

| 错误码                                   | 处理                                             |
| ---------------------------------------- | ------------------------------------------------ |
| `DIAGNOSTIC_REPORT_TOKEN_INVALID`        | 诊断凭据无效或到期，停止本次上报，不重新登录用户 |
| `DIAGNOSTIC_REPORT_EVIDENCE_INVALID`     | code、stage、outcome 不匹配，修正应用映射        |
| `DIAGNOSTIC_REPORT_IDEMPOTENCY_CONFLICT` | 同一 eventId 被用于不同内容，修正幂等实现        |
| `DIAGNOSTIC_REPORT_LIMIT_REACHED`        | 已达到单流程 12 个唯一事件的限额，停止上报       |
| `DIAGNOSTIC_REPORT_RATE_LIMITED`         | 按 Retry-After 等待；不要自动重走 OAuth          |
| `DIAGNOSTIC_REPORT_TOO_LARGE`            | 正文超过 4096 字节，移除契约外数据               |

## Web、iOS 与 CLI 接入示例

三个可运行示例位于仓库 `examples/auth-diagnostics/`。它们只补充应用上报，现有 OAuth/OIDC 库仍负责所有认证验证、业务会话创建与凭据保存；不要把示例中的诊断 token 当成 OAuth token。

- **Web**：使用 `report.mjs` 的 `takeDiagnosticCallback` 和 `createDiagnosticReporter`。回调加载后立即 `history.replaceState` 移除诊断参数，保留标准 code/state 供 OIDC 库处理。报告 token 仅保存在内存，15 分钟自动清除，完成后调用 `dispose()`。
- **iOS**：`ios.swift` 提供可编译的 `completeIOSOAuthCallback`、Actor reporter 和 `URLSession` 实现。把 `ASWebAuthenticationSession` 返回的 URL 交给它；回调诊断参数会在交给现有 OIDC SDK 前移除。通过 `authenticate` 闭包调用应用现有的授权码兑换、令牌验证和会话保存，只有闭包成功返回才报告 `SESSION_CREATED`。临时 URLSession 不共享 Cookie、URL 缓存或 HTTP 凭据，拒绝重定向，每次请求最多 2 秒，内存 token 15 分钟自动清除。报告在独立 Task 中执行，不延迟登录结果。
- **Node.js CLI**：`cli.mjs` 的 `completeCliOAuthCallback` 用于现有 loopback HTTP 回调处理器，不要把完整回调 URL 或诊断 token 放在命令行参数、终端历史或配置文件。它在交给现有 OIDC 实现前移除诊断参数；上报通过 `fetch`、不使用 Cookie、禁止重定向，完成或失败后释放内存 token。

CLI 调用形态如下，`finishExistingOAuthLogin` 是当前 CLI 已有的、完整校验并保存应用会话的实现：

```js
import { completeCliOAuthCallback } from "./cli.mjs"

const session = await completeCliOAuthCallback(callbackUrl, {
  authOrigin: configuredAuthOrigin,
  authenticate: finishExistingOAuthLogin,
})
```

iOS 对应调用形态如下，`existingOIDCFlow.finish` 是当前 App 已有的 OIDC 完成入口；SDK 和业务会话失败仍按原行为向调用方返回，诊断失败不会覆盖它：

```swift
let session = try await completeIOSOAuthCallback(
    callbackURL,
    authOrigin: configuredAuthOrigin
) { cleanURL in
    try await existingOIDCFlow.finish(callbackURL: cleanURL)
}
```

这些入口不会根据任意异常消息猜测具体失败原因，未知应用异常只报告 `CLIENT_UNKNOWN_FAILURE`。若要区分 token 兑换和 ID token 校验失败，请在相应的真实处理步骤调用 typed `DiagnosticEvidence` 或 JS 的闭合集合 code，不上传异常文本。

示例的本地验证命令（不访问真实 Auth 或 Lark）为：

```sh
node --test examples/auth-diagnostics/report.test.mjs examples/auth-diagnostics/cli.test.mjs
swiftc -swift-version 6 -parse-as-library examples/auth-diagnostics/ios.swift examples/auth-diagnostics/ios-test.swift -o /tmp/kai-diagnostics-ios-example-test
/tmp/kai-diagnostics-ios-example-test
```

Swift 示例使用 `ContinuousClock` 和 async URLSession，要求 iOS 16 / macOS 13 或更新系统。它不修改 App 的原有登录协议，也不注册新的回调方案。

## 聚合告警与恢复

后台 worker 读取已落库的结构化记录。可信技术告警只使用 Auth 自己观察到的事件；项目级统计需要受信应用归属，未归属的平台异常仅用于全局运行诊断。

| 规则              | 触发条件                                                                                                       |
| ----------------- | -------------------------------------------------------------------------------------------------------------- |
| 相同平台/依赖异常 | 5 分钟内至少 5 个不同 flow                                                                                     |
| 技术失败率异常    | 5 分钟内至少 50 个已完成观测 flow、至少 5 个技术失败 flow，失败率达到 10%                                      |
| 未知异常          | 新指纹首次出现即记录，30 分钟内合并                                                                            |
| 业务体验异常      | 10 分钟内特定账号规则拒绝达到 20 个 flow 且占该阶段观测 flow 至少 30%；低优先级，不包括普通密码/验证码输入错误 |
| 恢复              | 连续两个互不重叠的 5 分钟窗口没有同类失败，且两个窗口都有真实成功流量                                          |

技术失败率仅计算最新平台事件体现的终态：已验证应用的 token/refresh 成功，未关联 OAuth 应用的直接登录已完成会话建立，或上述流程的明确失败。OAuth 流程的 authorize/authentication 中间成功、发送验证码和等待验证器批准都不扩大分母；用户取消和预期业务拒绝不计技术失败分子。恢复窗口采用同样的完成口径，没有流量不会自动宣布恢复；错过的恢复窗口不会补算为成功。

上述统计仅采用 Auth 自身的事件，包含已验证应用的流程及未关联应用的直接登录。服务端已确认的 Lark 身份冲突等也适用体验异常阈值；客户端上报和未经验证的应用归属不进入告警聚合。普通密码、验证码错误和明确限流保留在诊断中，不触发未知异常首报。

指纹由错误码、阶段、依赖、环境组成，项目作为受影响范围而不是每项目重复通知。同一事件的首次通知、每 30 分钟的持续摘要和恢复通知分别去重。故障支持“调查中、已确认、观察恢复、已解决”、指派平台管理员和内部备注；手动标记已解决只是人工处理状态，不伪造“自动验证已恢复”的通知。新失败仍可创建新故障。

管理员告警链接使用 `?incident=<故障编号>` 定位具体事件，保留期内的旧故障不受列表首页数量限制。故障列表按最近发生时间分页，支持按处理状态筛选和继续加载；查询与定位仍要求当前平台管理员权限，平台故障查询同样限制在最近 90 天。

Lark 卡片只包含固定模板、安全错误码、汇总流程数、时间、发布版本、示例 flowId 及需登录的平台控制台链接。不包含用户身份、登录因子、项目内部详情、Webhook 或自由文本内部备注。测试投递仅发送固定测试卡，页面先显示“已排队”；可查询投递状态，不能把入队当作已送达。

## 配置、观察期与发布

采集、项目展示、应用上报分别由 `DIAGNOSTICS_CAPTURE_ENABLED`、`DIAGNOSTICS_PROJECT_ENABLED`、`DIAGNOSTICS_REPORT_ENABLED` 控制，默认关闭。项目内用户代号使用独立的 `DIAGNOSTICS_PSEUDONYM_SECRET` 与 `DIAGNOSTICS_PSEUDONYM_KEY_VERSION`；不要复用 OAuth、会话、验证码或其他日志系统的密钥。账号删除时清除关联诊断的用户引用、用户代号与 `grantReference`，包括尚未形成授权同意或用户代号的流程。延迟采集不会重新建立已删除账号的关联。

采集队列有固定容量与写入时限。队列溢出、写入超时或进程重启可能造成观测缺口；平台可查看采集状态、待写入数量与丢失计数，数据不足时不把它当作完整可用率。报告凭据与安全开关不会改变 OAuth 登录权限。

`DIAGNOSTICS_ALERTS_ENABLED` 控制后台聚合，`DIAGNOSTICS_ALERTS_SEND_ENABLED` 控制自动对外发送；二者默认关闭。Webhook 通过服务端 `DIAGNOSTICS_LARK_WEBHOOK` 注入，可选签名密钥通过 `DIAGNOSTICS_LARK_SIGNING_SECRET` 注入，控制台不接收、不显示密钥。

只允许国际版 Lark 官方 `https://open.larksuite.com/open-apis/bot/v2/hook/...` 地址，不接受任意 URL、凭据、查询参数或重定向。签名遵循 [Lark 自定义机器人官方说明](https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot)。投递设置不复用面向用户的邮箱/短信渠道。

首次启用聚合后有至少 24 小时观察期，起始时间持久化，部署和重启不会重新计时。观察期只记录模拟通知；期满仍需要显式打开发送开关。观察期积累的旧通知不会批量补发。平台管理员可以单独请求固定安全测试卡验证通道，不改变正式发送开关。

队列在事务中认领，有 30 秒租约；每次网络请求限制 5 秒、禁止跟随重定向并限制响应体大小。失败采用 30 秒、2 分钟、5 分钟、15 分钟退避，最多 5 次，之后进入失败队列；全局每分钟最多 5 次、每小时最多 30 次投递尝试。超时后的远端结果可能不确定，Lark Webhook 没有业务幂等承诺，因此极少量重复卡片仍可能出现，卡片中的故障编号/详情链接可识别同一事件。

告警自身的数据库或投递失败显示在 worker/队列状态中，不再递归产生群告警。服务或数据库整体不可用时，进程内 worker 无法独立证明状态，需要部署方已有的外部健康检查补充；“无日志”不能当作“服务正常”。

推荐启用顺序：应用兼容数据库迁移 → 启用采集 → 验证项目隔离与错误提示 → 启用聚合观察 24 小时 → 测试安全卡 → 核对阈值和噪声 → 显式开启自动发送。关闭发送开关立即停止自动投递，保留诊断和处理状态供排查；回滚应用不删除新增诊断表。
