# IT 运营 / AI Coding 用量 - API 文档

> **版本**: v0.1
> **最后更新**: 2026-05-15
> **维护者**: 后端团队

---

## ✅ 机器读取区（必填）

### 统一约定

- Base URL：`/api/v1/ai-usage`（MVP 顶层独立挂载；it-operations 模块外壳就绪后可加 alias，不改 URL）
- **Ingestion 端点**：`/api/v1/ai-usage/events`（Bearer = personal token；其他端点走 JWT，由 NestJS Guard 按路由分别配置）
- 其他端点鉴权：标准 JWT（FFOA 现有体系）+ `@RequirePermissions('ai-usage:*')` 装饰器
- 响应统一封装：成功 `{success, data, message, timestamp, path}`；分页 `{items, pagination}`；失败 `{success: false, error: {code, message}, ...}`
- 时间统一 UTC ISO8601
- 金额返回字符串十进制
- 错误码见 `08-error-codes.md`

### 端点清单

| 分组 | 端点 | 鉴权 | 权限 |
|------|------|------|------|
| Ingestion | `POST /api/v1/ai-usage/events` | Bearer personal token | (token 自带身份) |
| Token 自助 | `GET /api/v1/ai-usage/me/tokens` | JWT | `ai-usage:view-own` |
| Token 自助 | `POST /api/v1/ai-usage/me/tokens` | JWT | `ai-usage:view-own` |
| Token 自助 | `DELETE /api/v1/ai-usage/me/tokens/:id` | JWT | `ai-usage:view-own` |
| 员工自查 | `GET /api/v1/ai-usage/me/summary` | JWT | `ai-usage:view-own` |
| 员工自查 | `GET /api/v1/ai-usage/me/trend` | JWT | `ai-usage:view-own` |
| 员工自查 | `GET /api/v1/ai-usage/me/breakdown` | JWT | `ai-usage:view-own` |
| 员工自查 | `GET /api/v1/ai-usage/me/devices` | JWT | `ai-usage:view-own` |
| 员工自查 | `GET /api/v1/ai-usage/me/blocks` | JWT | `ai-usage:view-own` |
| 员工自查 v1.1 | `GET /api/v1/ai-usage/me/tool-frequency` | JWT | `ai-usage:view-own` |
| 员工自查 v1.1 | `GET /api/v1/ai-usage/me/session-stats` | JWT | `ai-usage:view-own` |
| 员工自查 v1.1 | `GET /api/v1/ai-usage/me/turn-gap-distribution` | JWT | `ai-usage:view-own` |
| 员工自查 v1.1 | `GET /api/v1/ai-usage/me/service-tier-mix` | JWT | `ai-usage:view-own` |
| 员工数据导出/删除 | `GET /api/v1/ai-usage/me/export` | JWT | `ai-usage:view-own` |
| Admin 总览 | `GET /api/v1/ai-usage/summary` | JWT | `ai-usage:view-all` |
| Admin 趋势 | `GET /api/v1/ai-usage/trend` | JWT | `ai-usage:view-all` |
| Admin 下钻 | `GET /api/v1/ai-usage/breakdown` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/tool-frequency` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/session-stats` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/turn-gap-distribution` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/service-tier-mix` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/stop-reason-mix` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/git-branch-heatmap` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/sessions/:sessionId/turns` | JWT | `ai-usage:view-all` |
| Admin v1.1 | `GET /api/v1/ai-usage/daily-user-matrix` | JWT | `ai-usage:view-all` |
| 员工自查 v1.1 | `GET /api/v1/ai-usage/me/sessions/:sessionId/turns` | JWT | `ai-usage:view-own` |
| Admin 导出 | `GET /api/v1/ai-usage/export?withRich=1` | JWT | `ai-usage:export` |
| Admin Device | `GET /api/v1/ai-usage/devices` | JWT | `ai-usage:view-all` |
| Admin Device | `POST /api/v1/ai-usage/devices/:id/block` | JWT | `ai-usage:block-device` |
| Admin Device | `POST /api/v1/ai-usage/devices/:id/unblock` | JWT | `ai-usage:block-device` |
| Admin Token | `GET /api/v1/ai-usage/tokens` | JWT | `ai-usage:manage-tokens-all` |
| Admin Token | `POST /api/v1/ai-usage/tokens/:id/revoke` | JWT | `ai-usage:manage-tokens-all` |
| Admin DLQ | `GET /api/v1/ai-usage/dlq` | JWT | `ai-usage:view-all` |
| 元数据 | `GET /api/v1/ai-usage/pricing` | Bearer or anonymous | - |

---

## 详细端点

### 1. `POST /api/v1/ai-usage/events`（核心 Ingestion）

**Headers**：
```
Authorization: Bearer ffai_xxxxxxxxxxxxxx
X-Device-Id: 7c1ab2e3-...
X-Hostname: chentao-mbp
X-Os-User: chentao
X-Os-Platform: darwin
X-Agent-Version: 0.1.0
Content-Type: application/json
```

**Body**：
```json
{
  "events": [
    {
      "rawMessageId": "msg_abc123",
      "tool": "claude-code",
      "sessionId": "01J...",
      "projectPath": "/Users/chentao/Code/workspace",
      "model": "claude-opus-4-7",
      "ts": "2026-05-15T08:30:11.123Z",
      "inputTokens": 1024,
      "outputTokens": 2048,
      "cacheCreationTokens": 512,
      "cacheReadTokens": 8192,
      "estimatedCostUsd": "0.043800",
      "gitBranch": "feature/ai-usage-aggregator-338",
      "agentVersionEvent": "2.1.119",
      "worktreeLabel": "slot-4",
      "cwdBasename": "ai-usage-338",
      "turnIndex": 42,
      "toolUseCount": 3,
      "toolNames": ["Bash", "Edit", "Read"],
      "stopReason": "tool_use",
      "serviceTier": "standard"
    }
  ]
}
```

> v1.1 富 metadata 字段（`gitBranch` 起 9 个字段）**全部可选**，缺省视为 `null`；老 agent 仍能正常上报，向后兼容。**所有富字段严格限定为 metadata**（枚举/计数/路径 basename/分支名/版本号），**禁止包含**消息内容、工具参数、工具输出、session 标题等 PII。详见 `06-data-model.md` § v1.1 节。

**约束**：
- `events.length` ∈ [1, 500]
- `rawMessageId` 必填 + 长度 ≤ 128
- `tool` ∈ enum
- `ts` ∈ [now-30d, now+5min]，否则进 DLQ
- 整体 body 大小 ≤ 1MB

**响应 202**：
```json
{
  "success": true,
  "data": {
    "accepted": 8,
    "deduped": 2,
    "rejected": 0,
    "dlq": 0,
    "deviceId": "uuid-of-device-row"
  }
}
```

**错误**：
- 401 `INVALID_TOKEN`（token 不存在 / 已撤销 / bcrypt 不匹配）
- 403 `DEVICE_BLOCKED`（device 被拉黑）
- 413 `BATCH_TOO_LARGE`
- 422 `INVALID_PAYLOAD`
- 429 `RATE_LIMIT_EXCEEDED`（60 req/min/token）

**注意**：
- 本端点不走 FFOA `TransformInterceptor` 标准前缀（性能/反代独立配置），直接挂在 `/api/v1/ai-usage/`
- 响应 header 含 `X-Pricing-Version: <date>`，客户端比对本地价目表 version，过期立即重拉 `/api/v1/ai-usage/pricing`
- 服务端实现批量 upsert 用 `prisma.aiUsageEvent.createMany({ skipDuplicates: true })`，**不要**用 `create` 循环（会因 UNIQUE 约束抛错中断整批）

---

### 2. Token 自助 (`/me/tokens`)

#### `GET /me/tokens` — 列出我的 token

**响应 200**：
```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "uuid",
        "name": "我的 Mac",
        "prefix": "ffai_a1b2c3d4",
        "lastUsedAt": "2026-05-15T08:30:00Z",
        "lastUsedIp": "10.x.x.x",
        "createdAt": "2026-05-10T...",
        "revokedAt": null
      }
    ]
  }
}
```

#### `POST /me/tokens` — 生成 token

**Body**：`{ "name": "我的 Mac" }`（长度 1-64）

**响应 201**：
```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "name": "我的 Mac",
    "prefix": "ffai_a1b2c3d4",
    "token": "ffai_a1b2c3d4e5f6789012345678901234567890",
    "createdAt": "..."
  },
  "message": "token.created.warning_visible_once"
}
```

⚠️ `token` 字段**仅本次响应返回**，后续 GET 永不暴露。

#### `DELETE /me/tokens/:id` — 撤销 token

**响应 204**

---

### 3. 员工自查 (`/me/...`)

#### `GET /me/summary?period=month`

`period` ∈ `today` / `week` / `month` / `year`

**响应**：
```json
{
  "success": true,
  "data": {
    "period": "month",
    "totalTokens": 12345678,
    "totalCostUsd": "32.4500",
    "activeDevices": 2,
    "activeProjects": 5,
    "deltaPct": { "tokens": 12.5, "cost": 15.2 }
  }
}
```

#### `GET /me/trend?period=month&granularity=day`

`granularity` ∈ `day` / `week` / `month`

**响应**：
```json
{
  "success": true,
  "data": {
    "points": [
      { "bucket": "2026-05-01", "tokens": 234567, "costUsd": "0.7800" }
    ]
  }
}
```

#### `GET /me/breakdown?by=project&period=month`

`by` ∈ `project` / `tool` / `model` / `device`

**响应**：
```json
{
  "success": true,
  "data": {
    "items": [
      { "key": "workspace", "tokens": 3456789, "costUsd": "10.2300", "share": 0.31 }
    ]
  }
}
```

#### `GET /me/devices`

**响应**：
```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "uuid",
        "hostname": "chentao-mbp",
        "osPlatform": "darwin",
        "osUser": "chentao",
        "agentVersion": "0.1.0",
        "firstSeenAt": "...",
        "lastSeenAt": "...",
        "blockedAt": null
      }
    ]
  }
}
```

#### `GET /me/export?format=csv&period=month`

**响应**：`text/csv` 流式下载，包含本人本期所有 event 明细

---

### 4. Admin 端 (`/it-operations/ai-usage/...`)

#### `GET /summary?period=month`

字段与 `/me/summary` 相同，**全 organization 聚合**。

#### `GET /trend?period=month&granularity=day&groupBy=tool`

`groupBy` ∈ `tool` / `model` / `topUser`（None = 总）

**响应**：
```json
{
  "success": true,
  "data": {
    "series": [
      {
        "key": "claude-code",
        "points": [
          { "bucket": "2026-05-01", "tokens": 12345678, "costUsd": "32.4500" }
        ]
      }
    ]
  }
}
```

#### `GET /breakdown?by=user&period=month&page=1&pageSize=20`

`by` ∈ `user` / `project` / `tool` / `model`

**响应**（分页）：
```json
{
  "success": true,
  "data": {
    "items": [
      {
        "key": "uuid-of-user",
        "label": "Chentao Jia",
        "email": "...",
        "deviceCount": 2,
        "tokens": 3456789,
        "costUsd": "10.2300",
        "share": 0.31,
        "deltaPct": 5.4
      }
    ],
    "pagination": { "page": 1, "pageSize": 20, "total": 45, "totalPages": 3 }
  }
}
```

#### `GET /export?by=user&period=month&format=csv`

CSV 流式下载。

---

### 5. Admin Device 管理

#### `GET /devices?status=all&q=hostname&page=1&pageSize=20`

`status` ∈ `all` / `active` / `blocked`

**响应**：
```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "uuid",
        "deviceIdShort": "7c1ab2e3",
        "hostname": "...",
        "user": { "id": "...", "name": "...", "email": "..." },
        "osPlatform": "darwin",
        "firstSeenAt": "...",
        "lastSeenAt": "...",
        "blockedAt": null,
        "blockedReason": null,
        "eventCount30d": 1234
      }
    ],
    "pagination": {...}
  }
}
```

#### `POST /devices/:id/block`

**Body**：`{ "reason": "陌生 device，等待确认" }`（长度 1-500）

**响应 200**：`{ "success": true, "data": { "id": "...", "blockedAt": "..." } }`

副作用：写 audit log `aiUsage.device.blocked`。

#### `POST /devices/:id/unblock`

**Body**：`{ "note": "...确认是合法 device" }`

---

### 6. Admin Token 管理

#### `GET /tokens?userId=...&status=active`

**响应**：与 `/me/tokens` 类似但跨用户。

#### `POST /tokens/:id/revoke`

**Body**：`{ "reason": "员工离职" }`

副作用：写 audit log `aiUsage.token.revoked.by-admin`。

---

### 7. Admin DLQ

#### `GET /dlq?reason=BAD_FORMAT&page=1`

**响应**：
```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "uuid",
        "deviceId": "uuid",
        "reason": "BAD_FORMAT",
        "rawPayloadPreview": {...},
        "createdAt": "..."
      }
    ],
    "pagination": {...}
  }
}
```

---

### 8. `GET /api/v1/ai-usage/pricing` — 模型价目表

**响应 200**（无鉴权，便于客户端拉取）：
```json
{
  "success": true,
  "data": {
    "version": "2026-05-01",
    "models": [
      {
        "id": "claude-opus-4-7",
        "tool": "claude-code",
        "inputUsdPerMTok": "15.000000",
        "outputUsdPerMTok": "75.000000",
        "cacheCreationUsdPerMTok": "18.750000",
        "cacheReadUsdPerMTok": "1.500000"
      }
    ]
  }
}
```

客户端每 24h 拉一次，本地存到 `~/.cache/ffctk/pricing.json`。

---

## i18n message keys

所有 message 字段返回 i18n key（前端翻译）：

- `aiUsage.token.created.warning_visible_once`
- `aiUsage.token.revoked.success`
- `aiUsage.device.blocked.success`
- `aiUsage.device.unblocked.success`
- `aiUsage.errors.invalid_token`
- `aiUsage.errors.device_blocked`
- `aiUsage.errors.rate_limit_exceeded`
- `aiUsage.errors.batch_too_large`
- `aiUsage.errors.invalid_payload`

---

### 9. v1.1 富 metadata 聚合端点

每个端点均接受 `period` / `from` / `to` 时间窗参数（同 trend）；`/me/...` 路径自动注入 `userId` scope，admin 路径走 organization-wide。

#### `GET /tool-frequency` — 工具调用频次热力图

**响应**：
```json
{
  "success": true,
  "data": {
    "items": [
      { "name": "Bash",  "eventCount": 142, "useCount": 412, "share": 0.42 },
      { "name": "Edit",  "eventCount": 88,  "useCount": 201, "share": 0.21 },
      { "name": "Read",  "eventCount": 76,  "useCount": 188, "share": 0.19 }
    ]
  }
}
```
`useCount` = 该工具被调用次数累计；`share` 占总调用数比例。后端用 `jsonb_array_elements_text(tool_names)` 展开。

#### `GET /session-stats` — Session 时长 / turn 分布

**响应**：
```json
{
  "success": true,
  "data": {
    "sessionCount": 187,
    "avgDurationSec": 4523,
    "avgTurns": 28.4,
    "durationBuckets": { "<10min": 42, "10-60min": 88, "1-4h": 47, ">4h": 10 },
    "turnBuckets":     { "1-10": 65, "11-50": 92, "51-200": 25, ">200": 5 },
    "recentSessions": [
      { "sessionId": "abc", "startedAt": "...", "endedAt": "...", "durationSec": 5430, "turnCount": 38, "tokens": 152340, "costUsd": "1.234560" }
    ]
  }
}
```

#### `GET /turn-gap-distribution` — Turn 间隔分布（思考密度）

后端用 PG `LAG()` 窗口函数计算同 session 内连续 turn 之间秒级间隔。

**响应**：
```json
{
  "success": true,
  "data": {
    "buckets":      { "<5s": 124, "5-30s": 388, "30s-2min": 75, ">2min": 12 },
    "avgGapSec":    18.7,
    "medianGapSec": 9.2
  }
}
```

#### `GET /service-tier-mix` — 服务等级配比

```json
{ "success": true, "data": { "items": [
  { "tier": "standard", "eventCount": 412, "tokens": 8421300, "costUsd": "12.34", "costShare": 0.78 },
  { "tier": "priority", "eventCount": 88,  "tokens": 2103400, "costUsd": "3.45",  "costShare": 0.22 }
]}}
```

#### `GET /stop-reason-mix` — Stop Reason 分布（**仅 admin**）

`max_tokens` 高占比 = 工程上需要拆 turn 或扩 max_tokens 的预警信号。

```json
{ "success": true, "data": { "items": [
  { "reason": "tool_use",   "eventCount": 312, "tokens": 5432100, "share": 0.62 },
  { "reason": "end_turn",   "eventCount": 175, "tokens": 1234500, "share": 0.35 },
  { "reason": "max_tokens", "eventCount": 13,  "tokens": 234100,  "share": 0.03 }
]}}
```

#### `GET /git-branch-heatmap` — 分支烧 token 热力图（**仅 admin**）

```json
{ "success": true, "data": { "items": [
  { "branch": "feature/ai-usage-aggregator-338", "eventCount": 412, "tokens": 8421300, "costUsd": "12.34" },
  { "branch": "develop", "eventCount": 88, "tokens": 1234500, "costUsd": "2.10" }
]}}
```

**性能约束**：所有 v1.1 端点对 30 天窗口扫描，单 org 数据量 ≤ 7M 行时 p95 < 1.5s（`session_id`, `gitBranch`, `serviceTier` 三个 index 覆盖）。超过 30 天时段会触发 `daily_rollups` 归档路径（v1.2 规划）。

---

### 10. v1.2 Drill-down 端点（2026-05-16）

#### `GET /trend?groupBy=project|user` — 时序按维度分组

`groupBy` 现支持：`none / tool / model / topUser / user / project`。

`user` 与 `topUser` 等价（按 user 维度），区别：`user` 会反查 `displayName` 一并返回 `series[].label`；`topUser` 仅返回 user_id 作为 key。`project` 按 `project_basename` 分组。

`user / project / topUser` 三种维度自动 top-N（N=10），按 series 总 tokens 降序取前 10 防止图表挤爆。

```json
{ "success": true, "data": {
  "series": [
    { "key": "user-uuid-1", "label": "张三", "points": [{"bucket":"2026-05-01","tokens":12345,"costUsd":"0.04"}] }
  ]
}}
```

#### `GET /session-stats?userId=&projectBasename=&page=&pageSize=`

`sessionStats` 现支持 admin 过滤：

- `userId` (UUID)：admin 拉某员工的 session 列表
- `projectBasename`：过滤指定项目
- `page` / `pageSize`：分页（默认 50/页，最大 200）

返回新字段：`pagination` + `recentSessions[].userLabel / projectBasename / model`。

#### `GET /sessions/:sessionId/turns` — Session Turn Timeline

**响应**：
```json
{
  "success": true,
  "data": {
    "sessionId": "...",
    "summary": {
      "userId": "...", "userLabel": "张三",
      "projectBasename": "workspace", "projectPath": "/home/x/Code/workspace",
      "tool": "claude-code",
      "startedAt": "...", "endedAt": "...", "durationSec": 5430,
      "turnCount": 38, "totalTokens": 152340, "totalCostUsd": "1.234560",
      "toolUseAgg": [
        { "name": "Bash", "count": 98 },
        { "name": "Edit", "count": 21 }
      ]
    },
    "turns": [
      {
        "id": "uuid", "ts": "...", "turnIndex": 1, "model": "claude-opus-4-7",
        "inputTokens": 1024, "outputTokens": 2048,
        "cacheCreationTokens": 0, "cacheReadTokens": 8192,
        "totalTokens": 11264, "costUsd": "0.0438",
        "toolUseCount": 3, "toolNames": ["Bash","Edit","Read"],
        "stopReason": "tool_use", "serviceTier": "standard",
        "gitBranch": "feature/...", "agentVersionEvent": "2.1.119",
        "worktreeLabel": "slot-4", "cwdBasename": "ai-usage-338"
      }
    ]
  }
}
```

权限：admin 走 `view-all` 可看任意 session；`/me/sessions/:id/turns` 走 `view-own` 强制 `userId = currentUser.id`。

#### `GET /daily-user-matrix?period=`

返回 date × user 矩阵（前端 calendar 热力图）。仅 admin。

```json
{ "success": true, "data": {
  "users": [ { "id": "...", "label": "张三" }, { "id": "...", "label": "李四" } ],
  "dates": ["2026-05-01", "2026-05-02", ...],
  "cells": [ { "date": "2026-05-01", "userId": "...", "tokens": 12345, "costUsd": "0.42" } ]
}}
```

Top 10 user × 日期，cells 缺失视为 0。

#### `GET /export?withRich=1`

加 `withRich=1` 切换到详细 event 行级 CSV（每个 event 一行，含 v1.1 富 metadata）。上限 10000 行。

CSV 头：
```
ts,user,project_basename,session_id,turn_index,tool,model,input_tokens,output_tokens,cache_creation_tokens,cache_read_tokens,total_tokens,cost_usd,tool_use_count,tool_names,stop_reason,service_tier,git_branch,worktree_label,agent_version_event
```

> 隐私：CSV 不含 `raw_message_id` / `project_path`（完整路径）；user 列输出 displayName 而非 UUID。
