AI Coding 采集方案探索

一、方案对比概述

1.1 两种方案的本质差异

在 AI Coding 工具使用数据的采集领域,目前主要有两种技术路线:

维度 代理网关方案(我们的设计) 客户端日志采集方案(ai-coding-trace)
核心思路 在 API 流量路径上插入中间人 在客户端读取 AI 工具的本地日志文件
数据来源 拦截 HTTP API 请求/响应 解析各工具写入本地的日志、transcript、数据库文件
部署侧重 服务端部署为主 客户端部署为主
类比 在高速公路收费站装摄像头(流量必经之路) 在停车场查看行车记录仪(事后读取记录)

1.2 架构对比图

复制代码
【代理网关方案】

  AI 工具  ──→  中心代理网关  ──→  上游 API
                    │
              拦截并记录流量
                    │
              数据库 + Dashboard

【客户端日志采集方案】

  AI 工具  ──→  上游 API(直连,不经过代理)
     │
     └─→  写入本地日志文件
              │
         采集守护进程读取日志
              │
         上报中心服务器
              │
         数据库 + Dashboard

二、ai-coding-trace 方案深度分析

2.1 核心原理

ai-coding-trace 的核心洞察是:几乎所有 AI Coding 工具都会在本地留下操作痕迹

  • Claude Code~/.claude/projects/ 下的 JSONL 会话记录(含完整 API 交互)
  • Cursor~/.cursor/ 下的 SQLite 数据库和日志文件
  • Codex CLI~/.codex/ 下的会话记录
  • Qoder → 本地日志和配置目录
  • Trae → 应用数据目录中的操作记录
  • GitHub Copilot → VS Code 扩展日志
  • 通义灵码 → 插件日志和使用记录

这些本地数据蕴含了与 API 流量几乎等价的信息量(Token 用量、模型选择、工具调用、生成代码等),而且无需修改任何网络配置即可获取。

2.2 架构详解

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     开发者工作站                                  │
│                                                                   │
│  ┌──────────┐  ┌────────┐  ┌───────┐  ┌───────────┐             │
│  │Claude Code│  │ Cursor │  │ Codex │  │ 通义灵码   │  ...        │
│  └────┬─────┘  └───┬────┘  └──┬────┘  └─────┬─────┘             │
│       │            │          │              │                     │
│       ▼            ▼          ▼              ▼                     │
│  ~/.claude/    ~/.cursor/  ~/.codex/    ~/.tongyi/                │
│  projects/     (SQLite)    logs/        logs/                     │
│  *.jsonl                                                          │
│       │            │          │              │                     │
│       └────────────┴──────────┴──────────────┘                    │
│                        │                                          │
│              ┌─────────▼──────────┐                               │
│              │  采集守护进程        │                               │
│              │  (Daemon Agent)     │                               │
│              │                     │                               │
│              │  · 文件系统监控      │                               │
│              │  · 多格式解析器      │                               │
│              │  · 增量采集          │                               │
│              │  · 本地缓存          │                               │
│              │  · 定时上报          │                               │
│              │  · 本地 Web UI       │                               │
│              │    :43124            │                               │
│              └─────────┬──────────┘                               │
│                        │                                          │
└────────────────────────┼──────────────────────────────────────────┘
                         │  HTTPS 上报
                         ▼
              ┌─────────────────────┐
              │   中心收集服务器      │
              │                     │
              │  · 数据接收 API      │
              │  · 数据清洗聚合      │
              │  · 持久化存储        │
              │  · Dashboard        │
              └─────────────────────┘

2.3 ai-coding-trace 关键特征

基于钉钉安装文档和相关技术分析,提炼出以下关键特征:

特征 详情
安装方式 npx @ali/ai-coding-trace@latest --workId=工号 一行命令
运行形态 后台守护进程(macOS LaunchAgent / Linux systemd / Windows Service)
本地配置 http://127.0.0.1:43124/v1/config 提供 Web 配置界面
日志目录 ~/.r2c/logs/code-collect/ 本地采集日志
支持工具 20+ 种(Cursor, Claude Code, Codex, Gemini, Qoder, Trae 等)
历史回填 http://127.0.0.1:43124/v1/config?ui=refresh 可回填历史数据
数据时效 T+1(次日可在 Dashboard 查看)
macOS 体验 菜单栏状态图标 + 本地 Dashboard 小组件
实时查看 提供实时数据查看器 Web 页面

2.4 ai-coding-trace 的优势分析

优势一:零网络配置,极低部署摩擦

代理网关方案需要开发者:

  1. 修改环境变量(ANTHROPIC_BASE_URL, OPENAI_BASE_URL
  2. 配置 HTTPS 代理(HTTPS_PROXY
  3. 安装并信任 MITM CA 证书
  4. 确保网关服务高可用(否则所有 AI 工具瘫痪)

而 ai-coding-trace 仅需:

  1. 运行一行 NPX 命令 → 结束

这是最关键的优势。对于需要快速在 50 人团队中推广的场景,部署摩擦直接决定了方案的可行性。

优势二:无单点故障风险

代理网关是所有 AI 流量的必经之路。一旦网关宕机:

  • 所有开发者的 AI 工具无法工作
  • 需要紧急切换回直连模式

而日志采集方案中,守护进程仅负责读取和上报,即使守护进程崩溃:

  • AI 工具完全不受影响,正常使用
  • 仅暂时丢失采集数据,守护进程恢复后可通过历史回填补齐
优势三:天然支持更多工具

代理网关需要为每种 API 协议实现 Parser(Anthropic、OpenAI、Google、私有协议等),且正向代理 MITM 模式存在兼容性风险。

日志采集只需要知道每种工具的日志文件路径和格式,然后实现解析器。即使新工具的 API 协议完全私有,只要它在本地写了日志,就能采集。

优势四:历史数据回填

日志采集可以读取历史日志文件,回填过去的使用数据。代理网关只能从部署时刻开始记录。

优势五:隐私边界更清晰

日志采集可以在客户端本地做数据脱敏和过滤后再上报,开发者对上报内容有更强的可控感。代理网关则是"全流量过网关",虽然技术上也可以脱敏,但心理感受上更具侵入性。

2.5 ai-coding-trace 的劣势

劣势 说明
依赖工具日志格式 如果 AI 工具更新了日志格式或路径,采集器需要适配更新
日志可能不完整 部分工具的日志不包含完整的 Token usage 信息,需要用 tokenizer 估算
客户端可绕过 开发者可以卸载或禁用守护进程(代理网关也有类似问题)
多平台维护 需维护 macOS / Linux / Windows 三个平台的守护进程管理
数据实时性 T+1 的时效性不如代理网关的实时性

三、方案选择建议

3.1 决策矩阵

评估维度 权重 代理网关方案 客户端日志采集方案
部署难度 25% ★★☆☆☆ (2分) ★★★★★ (5分)
数据完整性 20% ★★★★★ (5分) ★★★★☆ (4分)
系统可靠性 20% ★★★☆☆ (3分) ★★★★★ (5分)
工具覆盖广度 15% ★★★☆☆ (3分) ★★★★★ (5分)
数据实时性 10% ★★★★★ (5分) ★★★☆☆ (3分)
维护成本 10% ★★★☆☆ (3分) ★★★★☆ (4分)
加权总分 3.45 4.60

3.2 结论

对于 <50 人团队的 AI Coding 工具监控需求,客户端日志采集方案(ai-coding-trace 路线)是更优选择

核心原因:

  1. 部署摩擦极低 ------ 一行命令安装,不改变任何开发流程
  2. 零单点故障 ------ 采集失败不影响开发
  3. 天然支持 20+ 工具 ------ 无需逐个实现 API Parser
  4. 历史回填 ------ 可补采安装前的数据

四、复刻实现方案 --- 完整技术设计

4.1 系统架构

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    Developer Workstations (×50)                      │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │              ai-code-collector (守护进程)                        │ │
│  │                                                                  │ │
│  │  ┌───────────────────────────────────────────────┐              │ │
│  │  │           Log Watcher (fsnotify)              │              │ │
│  │  │                                               │              │ │
│  │  │  ~/.claude/projects/**/*.jsonl  → ClaudeParser │              │ │
│  │  │  ~/.cursor/**/*.sqlite         → CursorParser │              │ │
│  │  │  ~/.codex/**/*.jsonl           → CodexParser  │              │ │
│  │  │  ~/.qoder/logs/               → QoderParser  │              │ │
│  │  │  ~/.trae/logs/                → TraeParser   │              │ │
│  │  │  ~/.copilot/logs/             → CopilotParser│              │ │
│  │  │  ...更多工具...                               │              │ │
│  │  └───────────────────┬───────────────────────────┘              │ │
│  │                      │                                          │ │
│  │  ┌───────────────────▼───────────────────────────┐              │ │
│  │  │           Data Extractor                       │              │ │
│  │  │                                                │              │ │
│  │  │  · Token usage 提取                             │              │ │
│  │  │  · 模型信息提取                                  │              │ │
│  │  │  · 工具调用统计                                  │              │ │
│  │  │  · 生成代码提取                                  │              │ │
│  │  │  · 会话时长计算                                  │              │ │
│  │  └───────────────────┬───────────────────────────┘              │ │
│  │                      │                                          │ │
│  │  ┌───────────────────▼───────────────────────────┐              │ │
│  │  │           Local Store + Reporter               │              │ │
│  │  │                                                │              │ │
│  │  │  · SQLite 本地缓存                              │              │ │
│  │  │  · 批量压缩上报                                  │              │ │
│  │  │  · 失败重试队列                                  │              │ │
│  │  │  · 增量位点记录                                  │              │ │
│  │  └───────────────────┬───────────────────────────┘              │ │
│  │                      │                                          │ │
│  │  ┌───────────────────▼───────────────────────────┐              │ │
│  │  │           Local Web Server (:43124)             │              │ │
│  │  │                                                │              │ │
│  │  │  GET /v1/config          → 配置管理界面         │              │ │
│  │  │  GET /v1/config?ui=refresh → 历史回填触发       │              │ │
│  │  │  GET /v1/status          → 采集状态查看         │              │ │
│  │  │  GET /v1/stats           → 本地统计数据         │              │ │
│  │  └───────────────────────────────────────────────┘              │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│                         │                                            │
└─────────────────────────┼────────────────────────────────────────────┘
                          │ HTTPS (批量上报,每 5 分钟)
                          ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      Central Server                                  │
│                                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────────────────┐  │
│  │  Collector    │  │  Aggregator  │  │  API Server               │  │
│  │  API          │  │  Worker      │  │                           │  │
│  │              │  │              │  │  · 开发者数据查询          │  │
│  │  POST /v1/   │  │  · 每日聚合  │  │  · 团队统计               │  │
│  │    report    │  │  · AI行匹配  │  │  · 工具对比               │  │
│  │              │  │  · 成本计算  │  │  · 导出报表               │  │
│  └──────┬───────┘  └──────┬───────┘  └──────────┬────────────────┘  │
│         │                 │                      │                   │
│         └─────────────────┴──────────────────────┘                   │
│                           │                                          │
│              ┌────────────▼────────────────┐                         │
│              │  PostgreSQL + TimescaleDB   │                         │
│              └────────────────────────────┘                          │
│                           │                                          │
│              ┌────────────▼────────────────┐                         │
│              │  React Dashboard            │                         │
│              └─────────────────────────────┘                         │
└─────────────────────────────────────────────────────────────────────┘

4.2 技术栈选择

组件 技术 理由
采集守护进程 Go 编译为单二进制,跨平台分发简单,无运行时依赖
文件监控 fsnotify Go 标准的跨平台文件系统事件库
本地存储 SQLite (go-sqlite3) 零配置嵌入式数据库,存储采集位点和缓存数据
本地 Web Go net/http 内置 HTTP 服务器,提供本地配置和状态查看
服务端 API Go + Chi 轻量路由框架
服务端数据库 PostgreSQL + TimescaleDB 时序数据分区压缩
消息队列 50 人规模,直接写库即可,无需 MQ
前端 React + TypeScript + Ant Design + Recharts 企业级 Dashboard
部署 Docker Compose (服务端) + 安装脚本 (客户端) 分离部署
进程管理 macOS LaunchAgent / Linux systemd / Windows Service 系统级守护进程

4.3 项目目录结构

复制代码
ai-code-collector/
├── cmd/
│   ├── collector/                # 客户端守护进程入口
│   │   └── main.go
│   └── server/                   # 服务端入口
│       └── main.go
│
├── internal/
│   ├── collector/                # ===== 客户端核心 =====
│   │   ├── daemon.go             # 守护进程生命周期管理
│   │   ├── watcher.go            # 文件系统监控 (fsnotify)
│   │   ├── checkpoint.go         # 增量采集位点管理
│   │   ├── reporter.go           # 数据上报(批量、重试、压缩)
│   │   ├── localdb.go            # SQLite 本地存储
│   │   ├── webui.go              # 本地 Web 配置/状态服务器
│   │   └── backfill.go           # 历史数据回填
│   │
│   ├── parser/                   # ===== 日志解析器 =====
│   │   ├── registry.go           # 解析器注册表
│   │   ├── types.go              # 统一数据模型
│   │   ├── claude.go             # Claude Code 日志解析
│   │   ├── cursor.go             # Cursor 日志解析
│   │   ├── codex.go              # Codex CLI 日志解析
│   │   ├── copilot.go            # GitHub Copilot 日志解析
│   │   ├── qoder.go              # Qoder 日志解析
│   │   ├── trae.go               # Trae 日志解析
│   │   ├── gemini.go             # Gemini CLI 日志解析
│   │   ├── tongyi.go             # 通义灵码日志解析
│   │   ├── kimi.go               # Kimi CLI 日志解析
│   │   └── generic.go            # 通用 JSONL 解析器
│   │
│   ├── server/                   # ===== 服务端核心 =====
│   │   ├── api.go                # HTTP API 路由
│   │   ├── ingest.go             # 数据接收处理
│   │   ├── aggregator.go         # 每日聚合计算
│   │   ├── cost.go               # 成本计算(模型价格表)
│   │   ├── query.go              # 数据查询服务
│   │   └── export.go             # 报表导出
│   │
│   └── model/                    # ===== 共享数据模型 =====
│       ├── record.go             # 采集记录
│       ├── stats.go              # 统计数据
│       └── config.go             # 配置定义
│
├── web/                          # ===== 前端 Dashboard =====
│   ├── src/
│   │   ├── pages/
│   │   │   ├── Overview.tsx      # 总览页
│   │   │   ├── Developer.tsx     # 个人详情
│   │   │   ├── ToolComparison.tsx # 工具对比
│   │   │   ├── CostAnalysis.tsx  # 成本分析
│   │   │   └── Admin.tsx         # 管理设置
│   │   ├── components/
│   │   └── App.tsx
│   └── package.json
│
├── scripts/
│   ├── install.sh                # macOS/Linux 一键安装脚本
│   ├── install.ps1               # Windows 安装脚本
│   ├── uninstall.sh              # 卸载脚本
│   └── com.ai-code-collector.plist  # macOS LaunchAgent 配置
│
├── deploy/
│   ├── docker-compose.yml        # 服务端部署
│   ├── Dockerfile.server         # 服务端镜像
│   └── nginx.conf                # 前端反向代理
│
├── go.mod
├── go.sum
└── README.md

五、客户端守护进程 --- 核心实现

5.1 守护进程生命周期

go 复制代码
// internal/collector/daemon.go

// Daemon 管理采集器的完整生命周期
type Daemon struct {
    config     *Config
    watcher    *Watcher
    parsers    *parser.Registry
    reporter   *Reporter
    localDB    *LocalDB
    webServer  *WebUIServer
    stopCh     chan struct{}
}

type Config struct {
    DeveloperID   string        // 开发者工号/ID
    ServerURL     string        // 中心服务器地址
    ReportInterval time.Duration // 上报间隔,默认 5 分钟
    LocalPort     int           // 本地 Web 服务端口,默认 43124
    DataDir       string        // 本地数据目录,默认 ~/.ai-code-collector/
    LogDir        string        // 日志目录,默认 ~/.ai-code-collector/logs/
}

func (d *Daemon) Run(ctx context.Context) error {
    // 1. 初始化本地 SQLite
    // 2. 注册所有解析器
    // 3. 启动文件监控
    // 4. 启动定时上报
    // 5. 启动本地 Web 服务
    // 6. 等待停止信号
}

5.2 文件系统监控

go 复制代码
// internal/collector/watcher.go

// Watcher 监控 AI 工具日志文件的变化
type Watcher struct {
    fsWatcher  *fsnotify.Watcher
    parsers    *parser.Registry
    checkpoint *CheckpointManager
    eventCh    chan *parser.CollectedRecord
}

// WatchTargets 定义需要监控的目录
var WatchTargets = []WatchTarget{
    {
        Tool:    "claude-code",
        // Claude Code 在 ~/.claude/projects/ 下按项目存储 JSONL 会话记录
        Pattern: "~/.claude/projects/**/*.jsonl",
        Parser:  "claude",
    },
    {
        Tool:    "cursor",
        // Cursor 在用户数据目录存储 SQLite 数据库
        Pattern: "~/Library/Application Support/Cursor/User/globalStorage/*.sqlite",
        // Linux: ~/.config/Cursor/User/globalStorage/
        Parser:  "cursor",
    },
    {
        Tool:    "codex",
        // Codex CLI 的会话记录
        Pattern: "~/.codex/**/*.jsonl",
        Parser:  "codex",
    },
    {
        Tool:    "copilot",
        // GitHub Copilot 的 VS Code 扩展日志
        Pattern: "~/.vscode/extensions/github.copilot-*/logs/*.log",
        Parser:  "copilot",
    },
    // ... 更多工具
}

func (w *Watcher) Start(ctx context.Context) error {
    // 1. 展开 ~ 为用户 home 目录
    // 2. 对每个 WatchTarget,检测目录是否存在
    // 3. 存在的目录添加 fsnotify 监控
    // 4. 监听 Create/Write 事件
    // 5. 事件触发时,调用对应 Parser 增量解析
    // 6. 解析结果发送到 eventCh
}

func (w *Watcher) handleFileEvent(event fsnotify.Event, target WatchTarget) {
    // 仅处理 Write 和 Create 事件
    if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
        return
    }

    // 获取上次读取的位点
    offset := w.checkpoint.Get(event.Name)

    // 从位点处增量读取
    p := w.parsers.Get(target.Parser)
    records, newOffset, err := p.ParseIncremental(event.Name, offset)
    if err != nil {
        log.Warn("parse error", "file", event.Name, "err", err)
        return
    }

    // 更新位点
    w.checkpoint.Set(event.Name, newOffset)

    // 发送采集记录
    for _, r := range records {
        r.DeveloperID = w.config.DeveloperID
        r.Tool = target.Tool
        w.eventCh <- r
    }
}

5.3 增量位点管理

go 复制代码
// internal/collector/checkpoint.go

// CheckpointManager 管理每个日志文件的读取位点
// 确保重启后不会重复采集
type CheckpointManager struct {
    db *sql.DB  // SQLite
}

// 表结构
// CREATE TABLE checkpoints (
//     file_path  TEXT PRIMARY KEY,
//     offset     INTEGER NOT NULL DEFAULT 0,
//     inode      INTEGER,           -- 用于检测文件轮转
//     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
// );

func (cm *CheckpointManager) Get(filePath string) int64 {
    // 1. 查询文件的 offset
    // 2. 检查 inode 是否变化(文件轮转检测)
    // 3. 如果 inode 变化,重置 offset 为 0
    // 4. 返回 offset
}

func (cm *CheckpointManager) Set(filePath string, offset int64) {
    // UPSERT 更新位点
}

5.4 数据上报

go 复制代码
// internal/collector/reporter.go

// Reporter 负责将采集数据批量上报到中心服务器
type Reporter struct {
    serverURL  string
    httpClient *http.Client
    localDB    *LocalDB
    batchSize  int           // 默认 100 条
    interval   time.Duration // 默认 5 分钟
}

// ReportBatch 上报请求体
type ReportBatch struct {
    DeveloperID string                    `json:"developer_id"`
    Hostname    string                    `json:"hostname"`
    Records     []*parser.CollectedRecord `json:"records"`
    ReportedAt  time.Time                 `json:"reported_at"`
}

func (r *Reporter) Start(ctx context.Context) {
    ticker := time.NewTicker(r.interval)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            r.flush()
        case <-ctx.Done():
            r.flush() // 退出前最后一次上报
            return
        }
    }
}

func (r *Reporter) flush() {
    // 1. 从本地 SQLite 取出待上报记录
    records := r.localDB.GetPendingRecords(r.batchSize)
    if len(records) == 0 {
        return
    }

    batch := &ReportBatch{
        DeveloperID: r.config.DeveloperID,
        Hostname:    hostname(),
        Records:     records,
        ReportedAt:  time.Now(),
    }

    // 2. gzip 压缩后 POST 到服务器
    data, _ := json.Marshal(batch)
    compressed := gzipCompress(data)

    resp, err := r.httpClient.Post(
        r.serverURL+"/v1/report",
        "application/json",
        bytes.NewReader(compressed),
    )

    if err != nil || resp.StatusCode != 200 {
        // 3. 上报失败,保留在本地队列,下次重试
        log.Warn("report failed, will retry", "err", err)
        return
    }

    // 4. 上报成功,标记已上报
    r.localDB.MarkReported(records)
}

5.5 本地 Web 服务

go 复制代码
// internal/collector/webui.go

// WebUIServer 提供本地 Web 配置和状态界面
type WebUIServer struct {
    port    int
    daemon  *Daemon
}

func (s *WebUIServer) Start() error {
    mux := http.NewServeMux()

    // 配置管理界面
    mux.HandleFunc("/v1/config", s.handleConfig)

    // 历史回填触发
    // GET /v1/config?ui=refresh
    mux.HandleFunc("/v1/refresh", s.handleRefresh)

    // 采集状态
    mux.HandleFunc("/v1/status", s.handleStatus)

    // 本地统计(不上报,仅本地查看)
    mux.HandleFunc("/v1/stats", s.handleStats)

    return http.ListenAndServe(
        fmt.Sprintf("127.0.0.1:%d", s.port),
        mux,
    )
}

func (s *WebUIServer) handleStatus(w http.ResponseWriter, r *http.Request) {
    status := map[string]any{
        "running":           true,
        "developer_id":      s.daemon.config.DeveloperID,
        "watched_tools":     s.daemon.watcher.ActiveTools(),
        "pending_records":   s.daemon.localDB.PendingCount(),
        "last_report_at":    s.daemon.reporter.LastReportTime(),
        "total_collected":   s.daemon.localDB.TotalCollected(),
        "uptime":            time.Since(s.daemon.startTime).String(),
    }
    json.NewEncoder(w).Encode(status)
}

func (s *WebUIServer) handleRefresh(w http.ResponseWriter, r *http.Request) {
    // 触发历史数据回填
    // 扫描所有已知日志目录,从头解析未采集的历史数据
    go s.daemon.backfill.RunFull()
    json.NewEncoder(w).Encode(map[string]string{
        "status": "backfill started",
    })
}

六、日志解析器 --- 多工具适配

6.1 统一数据模型

go 复制代码
// internal/parser/types.go

// CollectedRecord 是所有解析器的统一输出格式
type CollectedRecord struct {
    ID            string    `json:"id"`
    DeveloperID   string    `json:"developer_id"`
    Tool          string    `json:"tool"`          // claude-code, cursor, codex, ...
    Model         string    `json:"model"`         // claude-sonnet-4-20250514, gpt-4o, ...
    Provider      string    `json:"provider"`      // anthropic, openai, google, ...
    SessionID     string    `json:"session_id"`
    Timestamp     time.Time `json:"timestamp"`

    // Token 用量
    InputTokens       int `json:"input_tokens"`
    OutputTokens      int `json:"output_tokens"`
    CacheReadTokens   int `json:"cache_read_tokens"`
    CacheWriteTokens  int `json:"cache_write_tokens"`
    TotalTokens       int `json:"total_tokens"`

    // 交互信息
    ToolCallsCount    int      `json:"tool_calls_count"`
    ToolNames         []string `json:"tool_names"`

    // 生成代码(用于 AI 行匹配,上报后 7 天清理)
    GeneratedCodeLines int    `json:"generated_code_lines"`
    GeneratedCodeHash  string `json:"generated_code_hash"` // SHA256,用于去重

    // 会话信息
    RequestType       string `json:"request_type"` // chat, completion, edit
    DurationMs        int64  `json:"duration_ms"`

    // 原始数据(可选,默认不上报)
    RawDataPath       string `json:"-"` // 本地原始文件路径
}

6.2 解析器注册表

go 复制代码
// internal/parser/registry.go

// Parser 接口 ------ 每种 AI 工具实现一个
type Parser interface {
    // Name 返回解析器标识
    Name() string

    // LogPaths 返回该工具的日志文件路径模式列表
    // 路径中的 ~ 会被自动展开
    LogPaths() []string

    // ParseIncremental 从指定位点开始增量解析
    // 返回解析出的记录列表和新的位点
    ParseIncremental(filePath string, offset int64) ([]*CollectedRecord, int64, error)

    // ParseFull 全量解析(用于历史回填)
    ParseFull(filePath string) ([]*CollectedRecord, error)
}

// Registry 管理所有已注册的解析器
type Registry struct {
    parsers map[string]Parser
}

func NewRegistry() *Registry {
    r := &Registry{parsers: make(map[string]Parser)}

    // 注册所有内置解析器
    r.Register(&ClaudeParser{})
    r.Register(&CursorParser{})
    r.Register(&CodexParser{})
    r.Register(&CopilotParser{})
    r.Register(&QoderParser{})
    r.Register(&TraeParser{})
    r.Register(&GeminiParser{})
    r.Register(&TongyiParser{})
    r.Register(&KimiParser{})

    return r
}

6.3 Claude Code 解析器(详细实现)

Claude Code 的日志是最结构化的,以 JSONL 格式存储完整的 API 交互记录。

go 复制代码
// internal/parser/claude.go

type ClaudeParser struct{}

func (p *ClaudeParser) Name() string { return "claude" }

func (p *ClaudeParser) LogPaths() []string {
    return []string{
        "~/.claude/projects/**/*.jsonl",
    }
}

func (p *ClaudeParser) ParseIncremental(filePath string, offset int64) ([]*CollectedRecord, int64, error) {
    f, err := os.Open(filePath)
    if err != nil {
        return nil, offset, err
    }
    defer f.Close()

    // 跳到上次读取位置
    if offset > 0 {
        f.Seek(offset, io.SeekStart)
    }

    var records []*CollectedRecord
    scanner := bufio.NewScanner(f)
    scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB buffer

    for scanner.Scan() {
        line := scanner.Bytes()
        record, err := p.parseLine(line)
        if err != nil {
            continue // 跳过无法解析的行
        }
        if record != nil {
            records = append(records, record)
        }
    }

    // 计算新位点
    newOffset, _ := f.Seek(0, io.SeekCurrent)
    return records, newOffset, nil
}

func (p *ClaudeParser) parseLine(line []byte) (*CollectedRecord, error) {
    // Claude Code JSONL 格式示例:
    // {"type":"request","timestamp":"...","message":{"model":"claude-sonnet-4-20250514","messages":[...],"tools":[...]}}
    // {"type":"response","timestamp":"...","message":{"usage":{"input_tokens":1234,"output_tokens":567,...},...}}

    var entry struct {
        Type      string          `json:"type"`
        Timestamp string          `json:"timestamp"`
        Message   json.RawMessage `json:"message"`
    }

    if err := json.Unmarshal(line, &entry); err != nil {
        return nil, err
    }

    // 我们主要关注 response 类型的条目(包含 usage 信息)
    if entry.Type == "response" {
        return p.parseResponse(entry.Timestamp, entry.Message)
    }

    // 也可以解析 request 类型来获取模型、工具调用等信息
    if entry.Type == "request" {
        return p.parseRequest(entry.Timestamp, entry.Message)
    }

    return nil, nil
}

func (p *ClaudeParser) parseResponse(ts string, msg json.RawMessage) (*CollectedRecord, error) {
    var resp struct {
        Model string `json:"model"`
        Usage struct {
            InputTokens      int `json:"input_tokens"`
            OutputTokens     int `json:"output_tokens"`
            CacheReadTokens  int `json:"cache_read_input_tokens"`
            CacheWriteTokens int `json:"cache_creation_input_tokens"`
        } `json:"usage"`
        Content []struct {
            Type  string `json:"type"`
            Text  string `json:"text,omitempty"`
            Name  string `json:"name,omitempty"`
            Input any    `json:"input,omitempty"`
        } `json:"content"`
    }

    if err := json.Unmarshal(msg, &resp); err != nil {
        return nil, err
    }

    timestamp, _ := time.Parse(time.RFC3339, ts)

    record := &CollectedRecord{
        ID:               generateID(),
        Tool:             "claude-code",
        Model:            resp.Model,
        Provider:         "anthropic",
        Timestamp:        timestamp,
        InputTokens:      resp.Usage.InputTokens,
        OutputTokens:     resp.Usage.OutputTokens,
        CacheReadTokens:  resp.Usage.CacheReadTokens,
        CacheWriteTokens: resp.Usage.CacheWriteTokens,
        TotalTokens:      resp.Usage.InputTokens + resp.Usage.OutputTokens,
    }

    // 提取工具调用信息和生成代码
    for _, c := range resp.Content {
        if c.Type == "tool_use" {
            record.ToolCallsCount++
            record.ToolNames = append(record.ToolNames, c.Name)
        }
        if c.Type == "text" {
            // 提取代码块中的代码行数
            record.GeneratedCodeLines += countCodeLines(c.Text)
        }
    }

    return record, nil
}

6.4 Cursor 解析器

Cursor 的数据存储在 SQLite 数据库中,需要不同的解析策略。

go 复制代码
// internal/parser/cursor.go

type CursorParser struct{}

func (p *CursorParser) Name() string { return "cursor" }

func (p *CursorParser) LogPaths() []string {
    return []string{
        // macOS
        "~/Library/Application Support/Cursor/User/globalStorage/cursor.*.sqlite",
        "~/Library/Application Support/Cursor/logs/*.log",
        // Linux
        "~/.config/Cursor/User/globalStorage/cursor.*.sqlite",
        "~/.config/Cursor/logs/*.log",
        // Windows
        // %APPDATA%/Cursor/User/globalStorage/cursor.*.sqlite
    }
}

func (p *CursorParser) ParseIncremental(filePath string, offset int64) ([]*CollectedRecord, int64, error) {
    if strings.HasSuffix(filePath, ".sqlite") {
        return p.parseSQLite(filePath, offset)
    }
    return p.parseLogFile(filePath, offset)
}

func (p *CursorParser) parseSQLite(filePath string, lastRowID int64) ([]*CollectedRecord, int64, error) {
    db, err := sql.Open("sqlite3", filePath+"?mode=ro")
    if err != nil {
        return nil, lastRowID, err
    }
    defer db.Close()

    // 查询增量数据(rowid > lastRowID)
    // 具体表结构需要根据 Cursor 实际的 SQLite schema 适配
    rows, err := db.Query(`
        SELECT rowid, timestamp, model, prompt_tokens, completion_tokens, 
               total_tokens, request_type
        FROM usage_log
        WHERE rowid > ?
        ORDER BY rowid ASC
        LIMIT 1000
    `, lastRowID)
    if err != nil {
        return nil, lastRowID, err
    }
    defer rows.Close()

    var records []*CollectedRecord
    var maxRowID int64 = lastRowID

    for rows.Next() {
        var rowID int64
        var ts string
        var model string
        var promptTokens, completionTokens, totalTokens int
        var reqType string

        rows.Scan(&rowID, &ts, &model, &promptTokens,
            &completionTokens, &totalTokens, &reqType)

        timestamp, _ := time.Parse(time.RFC3339, ts)

        records = append(records, &CollectedRecord{
            ID:           generateID(),
            Tool:         "cursor",
            Model:        model,
            Provider:     inferProvider(model),
            Timestamp:    timestamp,
            InputTokens:  promptTokens,
            OutputTokens: completionTokens,
            TotalTokens:  totalTokens,
            RequestType:  reqType,
        })

        if rowID > maxRowID {
            maxRowID = rowID
        }
    }

    return records, maxRowID, nil
}

6.5 各工具日志路径与格式速查表

工具 日志路径 (macOS) 格式 Token 字段可用性
Claude Code ~/.claude/projects/**/*.jsonl JSONL(完整 API 交互) 完整(input/output/cache_read/cache_write)
Cursor ~/Library/Application Support/Cursor/ SQLite + Log 部分(可能需估算)
Codex CLI ~/.codex/**/*.jsonl JSONL 完整(OpenAI 格式)
GitHub Copilot VS Code 扩展目录 Log 文件 有限(需从日志中提取)
Gemini CLI ~/.gemini/logs/ JSONL/Log 完整(Google 格式)
Qoder ~/.qoder/logs/ JSONL 需适配
Trae ~/.trae/logs/ 或应用数据目录 Log/DB 需适配
通义灵码 VS Code 扩展日志 Log 文件 有限
Kimi CLI ~/.kimi/logs/ JSONL 完整

注意:日志路径和格式可能随工具版本更新而变化。采集器需要实现版本检测和多格式兼容逻辑。


七、服务端实现

7.1 数据接收 API

go 复制代码
// internal/server/ingest.go

func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) {
    // 1. 验证请求(开发者 Token 或工号)
    devID := r.Header.Get("X-Developer-Id")
    if devID == "" {
        http.Error(w, "missing developer id", 401)
        return
    }

    // 2. 解压 gzip body
    reader, err := gzip.NewReader(r.Body)
    if err != nil {
        // 如果不是 gzip,直接读原始 body
        reader = nil
    }

    // 3. 解析 ReportBatch
    var batch ReportBatch
    if reader != nil {
        json.NewDecoder(reader).Decode(&batch)
    } else {
        json.NewDecoder(r.Body).Decode(&batch)
    }

    // 4. 写入数据库
    tx, _ := s.db.Begin()
    for _, record := range batch.Records {
        tx.Exec(`
            INSERT INTO api_requests 
                (developer_id, tool, model, provider, session_id,
                 input_tokens, output_tokens, cache_read_tokens,
                 cache_write_tokens, total_tokens,
                 tool_calls_count, tool_names,
                 generated_code_lines, generated_code_hash,
                 request_type, duration_ms, created_at)
            VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
            ON CONFLICT (id) DO NOTHING
        `, record.DeveloperID, record.Tool, record.Model, record.Provider,
           record.SessionID, record.InputTokens, record.OutputTokens,
           record.CacheReadTokens, record.CacheWriteTokens,
           record.TotalTokens, record.ToolCallsCount,
           pq.Array(record.ToolNames),
           record.GeneratedCodeLines, record.GeneratedCodeHash,
           record.RequestType, record.DurationMs, record.Timestamp)
    }
    tx.Commit()

    w.WriteHeader(200)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

7.2 数据库设计 (PostgreSQL + TimescaleDB)

sql 复制代码
-- 开发者表
CREATE TABLE developers (
    id              TEXT PRIMARY KEY,
    name            TEXT NOT NULL,
    email           TEXT,
    team            TEXT,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

-- API 请求记录(时序表,按天自动分区)
CREATE TABLE api_requests (
    id                  TEXT NOT NULL,
    developer_id        TEXT NOT NULL REFERENCES developers(id),
    tool                TEXT NOT NULL,      -- claude-code, cursor, codex, ...
    model               TEXT,               -- claude-sonnet-4-20250514, gpt-4o, ...
    provider            TEXT,               -- anthropic, openai, google, ...
    session_id          TEXT,
    
    input_tokens        INTEGER DEFAULT 0,
    output_tokens       INTEGER DEFAULT 0,
    cache_read_tokens   INTEGER DEFAULT 0,
    cache_write_tokens  INTEGER DEFAULT 0,
    total_tokens        INTEGER DEFAULT 0,
    
    tool_calls_count    INTEGER DEFAULT 0,
    tool_names          TEXT[],
    
    generated_code_lines INTEGER DEFAULT 0,
    generated_code_hash  TEXT,
    
    request_type        TEXT,               -- chat, completion, edit
    duration_ms         BIGINT,
    
    created_at          TIMESTAMPTZ NOT NULL
);

-- 转换为 TimescaleDB 时序表
SELECT create_hypertable('api_requests', 'created_at',
    chunk_time_interval => INTERVAL '1 day');

-- Git 提交记录
CREATE TABLE git_commits (
    id                  SERIAL PRIMARY KEY,
    developer_id        TEXT NOT NULL REFERENCES developers(id),
    repo                TEXT NOT NULL,
    commit_hash         TEXT NOT NULL UNIQUE,
    branch              TEXT,
    added_lines         INTEGER DEFAULT 0,
    deleted_lines       INTEGER DEFAULT 0,
    
    ai_generated_lines  INTEGER DEFAULT 0,
    ai_assisted_lines   INTEGER DEFAULT 0,
    human_lines         INTEGER DEFAULT 0,
    ai_confidence       REAL,               -- 0.0 ~ 1.0
    
    committed_at        TIMESTAMPTZ NOT NULL,
    created_at          TIMESTAMPTZ DEFAULT NOW()
);

-- 每日预聚合统计
CREATE TABLE daily_stats (
    developer_id        TEXT NOT NULL REFERENCES developers(id),
    date                DATE NOT NULL,
    tool                TEXT NOT NULL,
    
    -- Token 统计
    total_requests      INTEGER DEFAULT 0,
    total_input_tokens  BIGINT DEFAULT 0,
    total_output_tokens BIGINT DEFAULT 0,
    total_cache_tokens  BIGINT DEFAULT 0,
    estimated_cost_usd  NUMERIC(10,4) DEFAULT 0,
    
    -- 代码统计
    total_code_lines    INTEGER DEFAULT 0,
    total_tool_calls    INTEGER DEFAULT 0,
    
    -- 会话统计
    total_sessions      INTEGER DEFAULT 0,
    avg_session_duration_ms BIGINT DEFAULT 0,
    
    PRIMARY KEY (developer_id, date, tool)
);

-- 模型价格表
CREATE TABLE model_pricing (
    model               TEXT PRIMARY KEY,
    provider            TEXT NOT NULL,
    input_price_per_1m  NUMERIC(10,6),     -- 每百万 token 价格 (USD)
    output_price_per_1m NUMERIC(10,6),
    cache_read_price_per_1m  NUMERIC(10,6),
    cache_write_price_per_1m NUMERIC(10,6),
    effective_from      DATE NOT NULL,
    effective_to        DATE
);

-- 预置主流模型价格
INSERT INTO model_pricing (model, provider, input_price_per_1m, output_price_per_1m, cache_read_price_per_1m, effective_from) VALUES
('claude-opus-4-20250918',   'anthropic', 15.0, 75.0, 1.5, '2025-01-01'),
('claude-sonnet-4-20250514', 'anthropic', 3.0, 15.0, 0.3, '2025-01-01'),
('claude-haiku-4-5-20251001','anthropic', 0.8, 4.0, 0.08, '2025-01-01'),
('gpt-4o',                   'openai',    2.5, 10.0, 1.25, '2025-01-01'),
('gpt-4o-mini',              'openai',    0.15, 0.6, 0.075, '2025-01-01'),
('gemini-2.5-pro',           'google',    1.25, 10.0, 0.315, '2025-01-01');

-- 常用查询索引
CREATE INDEX idx_api_requests_developer ON api_requests (developer_id, created_at DESC);
CREATE INDEX idx_api_requests_tool ON api_requests (tool, created_at DESC);
CREATE INDEX idx_git_commits_developer ON git_commits (developer_id, committed_at DESC);

7.3 每日聚合计算

go 复制代码
// internal/server/aggregator.go

func (a *Aggregator) RunDailyAggregation(date time.Time) error {
    dateStr := date.Format("2006-01-02")

    _, err := a.db.Exec(`
        INSERT INTO daily_stats 
            (developer_id, date, tool,
             total_requests, total_input_tokens, total_output_tokens,
             total_cache_tokens, estimated_cost_usd,
             total_code_lines, total_tool_calls,
             total_sessions, avg_session_duration_ms)
        SELECT 
            r.developer_id,
            $1::date,
            r.tool,
            COUNT(*),
            SUM(r.input_tokens),
            SUM(r.output_tokens),
            SUM(r.cache_read_tokens + r.cache_write_tokens),
            SUM(
                r.input_tokens * p.input_price_per_1m / 1000000.0
                + r.output_tokens * p.output_price_per_1m / 1000000.0
                + r.cache_read_tokens * COALESCE(p.cache_read_price_per_1m, 0) / 1000000.0
            ),
            SUM(r.generated_code_lines),
            SUM(r.tool_calls_count),
            COUNT(DISTINCT r.session_id),
            AVG(r.duration_ms)
        FROM api_requests r
        LEFT JOIN model_pricing p ON r.model = p.model
        WHERE r.created_at >= $1::date
          AND r.created_at < ($1::date + INTERVAL '1 day')
        GROUP BY r.developer_id, r.tool
        ON CONFLICT (developer_id, date, tool) DO UPDATE SET
            total_requests = EXCLUDED.total_requests,
            total_input_tokens = EXCLUDED.total_input_tokens,
            total_output_tokens = EXCLUDED.total_output_tokens,
            total_cache_tokens = EXCLUDED.total_cache_tokens,
            estimated_cost_usd = EXCLUDED.estimated_cost_usd,
            total_code_lines = EXCLUDED.total_code_lines,
            total_tool_calls = EXCLUDED.total_tool_calls,
            total_sessions = EXCLUDED.total_sessions,
            avg_session_duration_ms = EXCLUDED.avg_session_duration_ms
    `, dateStr)

    return err
}

7.4 Dashboard API

go 复制代码
// internal/server/api.go

func (s *Server) setupRoutes() {
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(corsMiddleware)

    // 数据接收
    r.Post("/v1/report", s.handleReport)

    // Dashboard API
    r.Route("/api", func(r chi.Router) {
        // 总览
        r.Get("/overview", s.handleOverview)
        // 返回: 总 Token 数, 总成本, 活跃开发者数, 工具分布

        // 开发者列表
        r.Get("/developers", s.handleDeveloperList)
        // 返回: [{id, name, team, total_tokens, total_cost, primary_tool, last_active}]

        // 开发者详情
        r.Get("/developers/{id}", s.handleDeveloperDetail)
        // 返回: 日均 Token, 模型分布, 工具使用趋势, 会话统计

        // 工具对比
        r.Get("/tools/comparison", s.handleToolComparison)
        // 返回: 各工具的 Token 效率, 成本, 活跃用户数

        // 成本分析
        r.Get("/cost/breakdown", s.handleCostBreakdown)
        // 返回: 按日/周/月的成本趋势, 按模型/工具/团队分组

        // Token 趋势
        r.Get("/tokens/trend", s.handleTokenTrend)
        // 参数: granularity=day|week|month, from, to

        // Git 统计
        r.Get("/git/stats", s.handleGitStats)
        // 返回: AI 行占比, 采纳率, 提交行趋势

        // 导出
        r.Get("/export/csv", s.handleExportCSV)
        r.Get("/export/json", s.handleExportJSON)
    })

    // 静态文件(前端 Dashboard)
    r.Handle("/*", http.FileServer(http.Dir("./web/dist")))

    s.router = r
}

八、客户端安装脚本

8.1 macOS / Linux 安装脚本

bash 复制代码
#!/bin/bash
# scripts/install.sh
# AI Code Collector 一键安装脚本

set -e

# ============ 配置 ============
COLLECTOR_VERSION="latest"
SERVER_URL="${AI_COLLECTOR_SERVER:-https://ai-monitor.internal.company.com}"
INSTALL_DIR="$HOME/.ai-code-collector"
BIN_DIR="$INSTALL_DIR/bin"
LOG_DIR="$INSTALL_DIR/logs"
DATA_DIR="$INSTALL_DIR/data"
LOCAL_PORT=43124

# ============ 颜色输出 ============
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

info()  { echo -e "${GREEN}[INFO]${NC} $1"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }

# ============ 检测系统 ============
detect_os() {
    case "$(uname -s)" in
        Darwin*) OS="darwin" ;;
        Linux*)  OS="linux" ;;
        *)       error "不支持的操作系统: $(uname -s)" ;;
    esac

    case "$(uname -m)" in
        x86_64|amd64) ARCH="amd64" ;;
        arm64|aarch64) ARCH="arm64" ;;
        *)             error "不支持的架构: $(uname -m)" ;;
    esac

    info "检测到系统: ${OS}/${ARCH}"
}

# ============ 获取工号 ============
get_developer_id() {
    if [ -n "$1" ]; then
        DEVELOPER_ID="$1"
    else
        read -p "请输入你的工号: " DEVELOPER_ID
    fi

    if [ -z "$DEVELOPER_ID" ]; then
        error "工号不能为空"
    fi

    info "工号: ${DEVELOPER_ID}"
}

# ============ 下载二进制 ============
download_binary() {
    info "下载 ai-code-collector..."
    mkdir -p "$BIN_DIR" "$LOG_DIR" "$DATA_DIR"

    DOWNLOAD_URL="${SERVER_URL}/download/ai-code-collector-${OS}-${ARCH}"

    if command -v curl &> /dev/null; then
        curl -fsSL "$DOWNLOAD_URL" -o "$BIN_DIR/ai-code-collector"
    elif command -v wget &> /dev/null; then
        wget -q "$DOWNLOAD_URL" -O "$BIN_DIR/ai-code-collector"
    else
        error "需要 curl 或 wget"
    fi

    chmod +x "$BIN_DIR/ai-code-collector"
    info "下载完成: $BIN_DIR/ai-code-collector"
}

# ============ 写入配置 ============
write_config() {
    cat > "$INSTALL_DIR/config.json" << EOF
{
    "developer_id": "${DEVELOPER_ID}",
    "server_url": "${SERVER_URL}",
    "local_port": ${LOCAL_PORT},
    "report_interval_seconds": 300,
    "data_dir": "${DATA_DIR}",
    "log_dir": "${LOG_DIR}",
    "enabled_tools": [
        "claude-code", "cursor", "codex", "copilot",
        "gemini", "qoder", "trae", "tongyi", "kimi"
    ]
}
EOF
    info "配置已写入: $INSTALL_DIR/config.json"
}

# ============ macOS LaunchAgent ============
install_macos_daemon() {
    PLIST_PATH="$HOME/Library/LaunchAgents/com.ai-code-collector.plist"

    cat > "$PLIST_PATH" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.ai-code-collector</string>
    <key>ProgramArguments</key>
    <array>
        <string>${BIN_DIR}/ai-code-collector</string>
        <string>--config</string>
        <string>${INSTALL_DIR}/config.json</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>${LOG_DIR}/stdout.log</string>
    <key>StandardErrorPath</key>
    <string>${LOG_DIR}/stderr.log</string>
    <key>WorkingDirectory</key>
    <string>${INSTALL_DIR}</string>
</dict>
</plist>
EOF

    launchctl load "$PLIST_PATH"
    info "macOS LaunchAgent 已安装并启动"
}

# ============ Linux systemd ============
install_linux_daemon() {
    SERVICE_PATH="$HOME/.config/systemd/user/ai-code-collector.service"
    mkdir -p "$(dirname "$SERVICE_PATH")"

    cat > "$SERVICE_PATH" << EOF
[Unit]
Description=AI Code Collector
After=network.target

[Service]
Type=simple
ExecStart=${BIN_DIR}/ai-code-collector --config ${INSTALL_DIR}/config.json
Restart=always
RestartSec=10
StandardOutput=append:${LOG_DIR}/stdout.log
StandardError=append:${LOG_DIR}/stderr.log
WorkingDirectory=${INSTALL_DIR}

[Install]
WantedBy=default.target
EOF

    systemctl --user daemon-reload
    systemctl --user enable ai-code-collector
    systemctl --user start ai-code-collector
    info "Linux systemd 用户服务已安装并启动"
}

# ============ 安装 Git Hook(可选)============
install_git_hook() {
    HOOK_DIR="$HOME/.config/git"
    mkdir -p "$HOOK_DIR/hooks"

    cat > "$HOOK_DIR/hooks/post-commit" << 'HOOKEOF'
#!/bin/bash
# AI Code Collector: Git post-commit hook
# 将提交信息上报到本地采集进程

COMMIT_HASH=$(git rev-parse HEAD)
REPO=$(basename "$(git rev-parse --show-toplevel)")
BRANCH=$(git rev-parse --abbrev-ref HEAD)
STATS=$(git diff --numstat HEAD~1 HEAD 2>/dev/null | awk '{a+=$1; d+=$2} END {printf "%d %d", a, d}')
ADDED=$(echo "$STATS" | cut -d' ' -f1)
DELETED=$(echo "$STATS" | cut -d' ' -f2)

curl -s "http://127.0.0.1:43124/v1/git-commit" \
    -H "Content-Type: application/json" \
    -d "{
        \"commit_hash\": \"${COMMIT_HASH}\",
        \"repo\": \"${REPO}\",
        \"branch\": \"${BRANCH}\",
        \"added_lines\": ${ADDED:-0},
        \"deleted_lines\": ${DELETED:-0}
    }" > /dev/null 2>&1 &

# 不阻塞 git commit
exit 0
HOOKEOF

    chmod +x "$HOOK_DIR/hooks/post-commit"

    # 配置 git 使用全局 hooks 目录
    git config --global core.hooksPath "$HOOK_DIR/hooks"
    info "Git post-commit hook 已安装"
}

# ============ 主流程 ============
main() {
    echo "=============================="
    echo "  AI Code Collector 安装程序"
    echo "=============================="
    echo ""

    detect_os
    get_developer_id "$1"
    download_binary
    write_config

    # 根据系统安装守护进程
    if [ "$OS" = "darwin" ]; then
        install_macos_daemon
    else
        install_linux_daemon
    fi

    # 可选:安装 Git hook
    read -p "是否安装 Git post-commit hook?(y/N) " -n 1 -r
    echo
    if [[ $REPLY =~ ^[Yy]$ ]]; then
        install_git_hook
    fi

    echo ""
    echo "=============================="
    info "安装完成!"
    echo ""
    info "本地配置界面: http://127.0.0.1:${LOCAL_PORT}/v1/config"
    info "采集状态查看: http://127.0.0.1:${LOCAL_PORT}/v1/status"
    info "历史数据回填: http://127.0.0.1:${LOCAL_PORT}/v1/config?ui=refresh"
    info "日志目录:     ${LOG_DIR}/"
    echo ""
    info "提示: 守护进程会在开机时自动启动,无需手动管理"
    echo "=============================="
}

main "$@"

8.2 卸载脚本

bash 复制代码
#!/bin/bash
# scripts/uninstall.sh

set -e

INSTALL_DIR="$HOME/.ai-code-collector"

echo "正在卸载 AI Code Collector..."

# 停止并移除守护进程
case "$(uname -s)" in
    Darwin*)
        PLIST="$HOME/Library/LaunchAgents/com.ai-code-collector.plist"
        if [ -f "$PLIST" ]; then
            launchctl unload "$PLIST" 2>/dev/null || true
            rm -f "$PLIST"
        fi
        ;;
    Linux*)
        systemctl --user stop ai-code-collector 2>/dev/null || true
        systemctl --user disable ai-code-collector 2>/dev/null || true
        rm -f "$HOME/.config/systemd/user/ai-code-collector.service"
        systemctl --user daemon-reload
        ;;
esac

# 移除 Git hook
HOOK="$HOME/.config/git/hooks/post-commit"
if [ -f "$HOOK" ] && grep -q "AI Code Collector" "$HOOK"; then
    rm -f "$HOOK"
    git config --global --unset core.hooksPath 2>/dev/null || true
fi

# 移除安装目录
rm -rf "$INSTALL_DIR"

echo "卸载完成"

九、服务端部署

9.1 Docker Compose

yaml 复制代码
# deploy/docker-compose.yml
version: '3.8'

services:
  postgres:
    image: timescale/timescaledb:latest-pg16
    environment:
      POSTGRES_DB: ai_code_monitor
      POSTGRES_USER: monitor
      POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U monitor"]
      interval: 10s
      timeout: 5s
      retries: 5

  server:
    build:
      context: ..
      dockerfile: deploy/Dockerfile.server
    environment:
      DATABASE_URL: postgres://monitor:${DB_PASSWORD:-changeme}@postgres:5432/ai_code_monitor?sslmode=disable
      SERVER_PORT: 8080
      AGGREGATION_CRON: "0 2 * * *"  # 每天凌晨 2 点聚合
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_healthy

  web:
    build:
      context: ../web
      dockerfile: ../deploy/Dockerfile.web
    ports:
      - "3000:80"
    depends_on:
      - server

volumes:
  pgdata:

9.2 服务端 Dockerfile

dockerfile 复制代码
# deploy/Dockerfile.server
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server/

FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /server /usr/local/bin/server
ENTRYPOINT ["server"]

十、AI 行识别方案

在客户端日志采集架构下,AI 行识别策略需要调整(因为不再有代理网关的实时流量):

10.1 调整后的识别策略

复制代码
最终置信度 = 0.7×日志代码匹配 + 0.3×Git 启发式
≥ 0.7 → AI 生成行    0.3~0.7 → AI 辅助行    < 0.3 → 人工行
信号 1:日志中的 AI 生成代码与 Git Diff 匹配(权重 70%)
go 复制代码
// internal/server/matcher.go

// MatchAILines 将 AI 工具生成的代码与 Git diff 新增行进行匹配
func MatchAILines(aiOutputs []AICodeOutput, diffLines []DiffLine) []MatchResult {
    var results []MatchResult

    for _, diffLine := range diffLines {
        bestMatch := 0.0
        var matchedOutput *AICodeOutput

        for i := range aiOutputs {
            // 使用编辑距离计算相似度
            similarity := normalizedLevenshtein(
                normalize(diffLine.Content),
                normalize(aiOutputs[i].Code),
            )
            if similarity > bestMatch {
                bestMatch = similarity
                matchedOutput = &aiOutputs[i]
            }
        }

        results = append(results, MatchResult{
            Line:       diffLine,
            Similarity: bestMatch,
            MatchedBy:  matchedOutput,
            IsAI:       bestMatch >= 0.85,
        })
    }

    return results
}

func normalize(code string) string {
    // 去除缩进、空行、注释等差异
    code = strings.TrimSpace(code)
    code = strings.ReplaceAll(code, "\t", "    ")
    return code
}
信号 2:Git 启发式分析(权重 30%)
go 复制代码
// internal/server/heuristic.go

// HeuristicScore 基于 Git commit 特征判断 AI 生成可能性
func HeuristicScore(commit GitCommit) float64 {
    score := 0.0

    // 1. 单次提交大量结构化代码(AI 倾向于一次生成大量代码)
    if commit.AddedLines > 100 {
        score += 0.3
    } else if commit.AddedLines > 50 {
        score += 0.15
    }

    // 2. 代码格式高度一致(AI 生成的代码格式通常非常规整)
    if commit.FormattingConsistency > 0.95 {
        score += 0.2
    }

    // 3. commit 时间集中在短时间内完成大量代码
    if commit.AddedLines > 50 && commit.DurationMinutes < 10 {
        score += 0.2
    }

    // 4. 提交中包含典型 AI 代码模式
    if hasAICodePatterns(commit.DiffContent) {
        score += 0.15
    }

    // 5. 开发者在提交前有活跃的 AI 工具会话
    if commit.HasRecentAISession {
        score += 0.3
    }

    if score > 1.0 {
        score = 1.0
    }
    return score
}

10.2 匹配流程

复制代码
┌───────────────────────────────┐
│     Git post-commit hook      │
│   上报 commit 信息到本地进程    │
└──────────────┬────────────────┘
               │
┌──────────────▼────────────────┐
│   本地守护进程接收 commit       │
│   提取 git diff 新增行         │
└──────────────┬────────────────┘
               │
┌──────────────▼────────────────┐
│   查找时间窗口内的 AI 输出      │
│   (commit 前 2 小时内的        │
│    AI 工具生成代码)             │
└──────────────┬────────────────┘
               │
┌──────────────▼────────────────┐
│   逐行模糊匹配                 │
│   diff 新增行 ↔ AI 输出代码    │
│   相似度 ≥ 85% → AI 生成行     │
└──────────────┬────────────────┘
               │
┌──────────────▼────────────────┐
│   + Git 启发式分析             │
│   综合计算最终置信度             │
└──────────────┬────────────────┘
               │
┌──────────────▼────────────────┐
│   上报到中心服务器              │
│   存入 git_commits 表          │
└───────────────────────────────┘

十一、Dashboard 核心页面设计

11.1 总览页 (Overview)

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  AI Coding Monitor Dashboard                     [日期范围选择]  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ 总 Token  │  │ 总成本    │  │ 活跃人数  │  │ AI 行占比 │       │
│  │ 12.5M    │  │ $847.32  │  │ 42/50    │  │ 38.5%    │       │
│  │ ↑15.2%   │  │ ↑8.3%    │  │ ↑2       │  │ ↑2.1%    │       │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  Token 消耗趋势(折线图 + 柱状图)                            ││
│  │  x: 日期    y: Token 数量                                    ││
│  │  按工具分色:Claude Code(紫) Cursor(蓝) Codex(绿) 其他(灰)   ││
│  │                                                              ││
│  │  ──────╱──╲──────╱╲──────╱╲──────                           ││
│  │       ╱    ╲    ╱  ╲    ╱  ╲                                ││
│  │  ────╱──────╲──╱────╲──╱────╲────                           ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  ┌──────────────────────┐  ┌──────────────────────────────────┐│
│  │  工具使用分布(饼图)   │  │  Top 10 Token 消耗者(柱状图)   ││
│  │                       │  │                                  ││
│  │  Claude Code  45%     │  │  张三  ████████████  320K       ││
│  │  Cursor       30%     │  │  李四  █████████     250K       ││
│  │  Codex        15%     │  │  王五  ████████      210K       ││
│  │  其他         10%     │  │  ...                            ││
│  └──────────────────────┘  └──────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

11.2 开发者详情页

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  开发者: 张三 (工号: 123456)                        [时间范围]   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  本月统计:                                                       │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ Token 用量│  │ 估算成本  │  │ AI 采纳率 │  │ 提交行数  │       │
│  │ 850K     │  │ $52.30   │  │ 72.5%    │  │ 3,200    │       │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  日均使用趋势                                                ││
│  │  Token (左轴)    提交行 (右轴)    AI行占比 (标注)             ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  ┌──────────────────────┐  ┌──────────────────────────────────┐│
│  │  模型使用分布          │  │  工具切换时间线                    ││
│  │                       │  │  8:00 Claude Code                ││
│  │  claude-sonnet-4 65%  │  │  10:30 Cursor                   ││
│  │  claude-opus-4   20%  │  │  14:00 Claude Code              ││
│  │  gpt-4o          15%  │  │  16:30 Codex                    ││
│  └──────────────────────┘  └──────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

11.3 成本分析页

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  成本分析                                          [时间范围]    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  月度成本趋势(堆叠面积图,按模型分色)                        ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  ┌──────────────────────────────────────────────────────────────┐│
│  │  成本明细表                                                   ││
│  │                                                               ││
│  │  模型              │ 请求数 │ Input Token │ Output Token │ 成本 ││
│  │  ──────────────────┼────────┼─────────────┼──────────────┼────── ││
│  │  claude-sonnet-4   │ 5,230  │ 8.2M        │ 1.5M         │ $47  ││
│  │  claude-opus-4     │ 320    │ 2.1M        │ 0.8M         │ $92  ││
│  │  gpt-4o            │ 1,850  │ 3.5M        │ 0.6M         │ $15  ││
│  └──────────────────────────────────────────────────────────────┘│
│                                                                  │
│  ┌────────────────────────┐  ┌─────────────────────────────────┐│
│  │  缓存命中率             │  │  成本优化建议                    ││
│  │                        │  │                                  ││
│  │  Claude: 82% ████████  │  │  · Opus 使用占比高,建议更多     ││
│  │  OpenAI: 45% ████      │  │    使用 Sonnet 处理日常任务      ││
│  │  总体:   71% ███████   │  │  · 缓存命中率有提升空间          ││
│  └────────────────────────┘  └─────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

十二、与代理网关方案的完整对比

维度 代理网关方案 客户端日志采集方案
部署步骤 修改环境变量 + 安装证书 + 配置代理 一行命令安装
对开发流程的影响 需要改变 AI 工具的连接方式 完全无感知
网关宕机影响 AI 工具全部不可用 无影响
数据实时性 实时 近实时(5 分钟间隔上报)
数据完整性 100%(全部流量经过) 95%+(取决于日志完整度)
支持新工具 需实现 API Parser 需实现日志 Parser
历史回填 不支持 支持
Token 准确性 从 API 响应直接提取,100% 准确 从日志提取,大部分准确,少数需估算
AI 行匹配 实时获取 AI 输出,匹配更及时 从日志提取 AI 输出,延迟匹配
隐私感知 "全流量过网关",心理侵入性较强 "本地采集 + 脱敏上报",接受度更高
维护复杂度 中心网关需高可用保障 客户端更新分发
扩展到 100+ 人 网关需考虑性能瓶颈 天然分布式,无瓶颈
合规审计 更容易实现流量审计 依赖客户端诚实上报

十三、实施路线图

阶段 时间 内容 产出
Phase 1 第 1-2 周 核心采集框架 Go 守护进程 + Claude Code Parser + 本地 SQLite + 数据上报
Phase 2 第 3-4 周 多工具 + 服务端 Cursor/Codex/Copilot Parser + 服务端 API + PostgreSQL + 基础 Dashboard
Phase 3 第 5-6 周 Git 分析 + AI 行 Git Hook + AI 行匹配 + 采纳率计算 + 成本分析面板
Phase 4 第 7-8 周 完善 + 推广 更多工具 Parser + 安装脚本打磨 + 团队推广 + 告警配置

Phase 1 详细任务(第 1-2 周)

复制代码
第 1 周:
  Day 1-2: 项目初始化 + 守护进程框架 (daemon.go, watcher.go)
  Day 3:   Claude Code JSONL 解析器 (claude.go)
  Day 4:   增量位点管理 + SQLite 本地存储 (checkpoint.go, localdb.go)
  Day 5:   数据上报模块 (reporter.go)

第 2 周:
  Day 1:   本地 Web 服务 + 配置管理 (webui.go)
  Day 2:   服务端数据接收 API (ingest.go)
  Day 3:   PostgreSQL schema + 基础查询
  Day 4:   安装脚本 (install.sh) + LaunchAgent/systemd
  Day 5:   端到端集成测试

十四、风险与应对

风险 影响 应对措施
AI 工具更新日志格式 解析器失效,数据丢失 版本检测 + 多格式兼容 + 快速更新机制
部分工具无可用日志 该工具数据缺失 退化到 Git 启发式分析;联系工具团队
开发者卸载采集器 个人数据缺失 管理层推动 + 提供有价值的个人 Dashboard
日志文件过大 磁盘/内存压力 增量读取 + 只提取元数据 + 不复制原始内容
Token 估算不准确 成本数据偏差 优先使用 API 响应中的 usage 字段

十五、总结

15.1 方案选择

客户端日志采集方案(ai-coding-trace 路线)是 <50 人团队的最佳选择,核心优势:

  1. 一行命令安装 --- 推广阻力最小
  2. 零网络配置 --- 不改变任何开发流程
  3. 无单点故障 --- 采集器崩溃不影响开发
  4. 天然支持 20+ 工具 --- 新增工具只需写解析器
  5. 历史数据回填 --- 安装后立即有数据

15.2 与原方案的关系

本方案并非完全取代代理网关方案,而是选择了更适合当前需求的技术路线

  • 代理网关方案的 数据库设计、Dashboard 设计、API 设计 可以复用
  • 核心差异在于数据采集层:从"网络流量拦截"改为"本地日志读取"
  • AI 行识别的模糊匹配算法和启发式分析完全复用
  • 整体架构更轻量、更易维护、更容易推广

15.3 产出清单

本报告覆盖了从需求分析到可落地实施的完整方案:

产出 说明
方案对比分析 代理网关 vs 日志采集的完整对比
系统架构设计 客户端守护进程 + 中心服务器的完整架构
项目目录结构 Go monorepo 完整目录规划
守护进程实现 文件监控、增量采集、位点管理、数据上报
日志解析器 Claude Code / Cursor 等多工具解析器接口与实现
数据库设计 PostgreSQL + TimescaleDB 完整 DDL
安装脚本 macOS / Linux 一键安装与卸载
Docker Compose 服务端一键部署
Dashboard 设计 总览、个人、成本分析三大核心页面
AI 行识别 日志匹配 + Git 启发式双信号融合方案
实施路线图 4 个阶段、8 周完成

相关推荐
jooloo34 分钟前
Codex 间歇性 400 之谜:一条对话里,它为什么有时候用 chat/completions,有时候切到 responses?
人工智能
用户5191495848451 小时前
OpenSSL PKCS#12 PBMAC1 堆栈缓冲区溢出漏洞 (CVE-2025-11187) 分析与验证
人工智能·aigc
用户5191495848452 小时前
HP Sound Research SECOMNService 权限提升漏洞利用工具
人工智能·aigc
用户018349301692 小时前
给 AI 智能体能力包一层 BFF,前端只调一个接口
人工智能
这token有力气6 小时前
Function Calling 格式漂移
人工智能
onething3656 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈
onething3656 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 6 —— 业务完善 + 会话消息预览
人工智能·后端·全栈
IT_陈寒7 小时前
SpringBoot自动配置的坑,我爬了三天才出来
前端·人工智能·后端
甲维斯8 小时前
笑抽了!DeepSeek识图,豆包完胜了!
人工智能·deepseek
Lei活在当下16 小时前
【AI手记系列-2026/6/18】iSparto & Harness,Caveman 以及AI时代的生存指南
人工智能·llm·openai