fix: proxy.js 修复
This commit is contained in:
parent
2b7ead30be
commit
810fe105d0
@ -5,6 +5,7 @@ const https = require('https');
|
|||||||
const http2 = require('http2');
|
const http2 = require('http2');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
// os 模块不引用 — 避免暴露真实主机信息
|
||||||
|
|
||||||
// ─── 配置 ───────────────────────────────────────────────
|
// ─── 配置 ───────────────────────────────────────────────
|
||||||
const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'api.anthropic.com';
|
const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'api.anthropic.com';
|
||||||
@ -17,6 +18,8 @@ const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED !== 'false'; // 默认
|
|||||||
const DD_API_KEY = process.env.DD_API_KEY || 'pubbbf48e6d78dae54bceaa4acf463299bf';
|
const DD_API_KEY = process.env.DD_API_KEY || 'pubbbf48e6d78dae54bceaa4acf463299bf';
|
||||||
const CLI_VERSION = process.env.CLI_VERSION || '2.1.81';
|
const CLI_VERSION = process.env.CLI_VERSION || '2.1.81';
|
||||||
const BUILD_TIME = process.env.BUILD_TIME || '2026-03-20T21:26:18Z';
|
const BUILD_TIME = process.env.BUILD_TIME || '2026-03-20T21:26:18Z';
|
||||||
|
// 伪装的 Node 版本(CLI 2.1.81 打包的 Node 版本)
|
||||||
|
const FAKE_NODE_VERSION = process.env.FAKE_NODE_VERSION || 'v22.14.0';
|
||||||
|
|
||||||
const log = (level, msg, extra = {}) => {
|
const log = (level, msg, extra = {}) => {
|
||||||
const entry = { time: new Date().toISOString(), level, msg, ...extra };
|
const entry = { time: new Date().toISOString(), level, msg, ...extra };
|
||||||
@ -27,6 +30,121 @@ const HEALTH_PATH = '/__health';
|
|||||||
const h2Hosts = new Set();
|
const h2Hosts = new Set();
|
||||||
const h2Sessions = new Map();
|
const h2Sessions = new Map();
|
||||||
|
|
||||||
|
// ─── 虚拟主机身份生成 ─────────────────────────────────────
|
||||||
|
// 每个账号基于 seed 生成全局唯一的主机身份,看起来像一台真实的个人开发机
|
||||||
|
// 匹配 CLI 的 OTEL detectResources: hostDetector + processDetector + serviceInstanceIdDetector
|
||||||
|
//
|
||||||
|
// 设计原则:
|
||||||
|
// 1. 同一账号(seed)永远产出同一台"机器"的特征
|
||||||
|
// 2. 不同账号的特征互不相同(无共享池、无碰撞)
|
||||||
|
// 3. 每个字段都像人手动设置的,不是程序生成的
|
||||||
|
|
||||||
|
// hostname 构造词表 — 组合后空间 > 100万,基本不碰撞
|
||||||
|
const HN_PREFIX = ['dev','code','work','build','my','home','lab','eng','hack','prog','desk','box','main','personal','linux'];
|
||||||
|
const HN_MIDDLE = ['','station','machine','server','node','pc','setup','rig','env','hub'];
|
||||||
|
const HN_STYLE = ['dash','dot','bare']; // 连接风格
|
||||||
|
|
||||||
|
// 用户名词表 — 真实开发者常用,组合后也是高基数
|
||||||
|
const UN_FIRST = ['alex','sam','chris','jordan','max','lee','kai','pat','jamie','taylor','morgan','casey','drew','avery','riley','blake','quinn','reese','cameron','skyler','dev','coder','user','admin','ubuntu','runner'];
|
||||||
|
const UN_SUFFIX = ['','dev','eng','42','_dev','01','x','z','_','99','007'];
|
||||||
|
|
||||||
|
function generateHostIdentity(seed) {
|
||||||
|
// 确定性哈希工具:同一 seed+suffix 永远返回同一结果
|
||||||
|
const h = (suffix) => crypto.createHash('sha256').update(seed + ':' + suffix).digest();
|
||||||
|
|
||||||
|
// ── hostname: 组合生成,如 "alex-devstation", "work-box-7f3a" ──
|
||||||
|
const hb = h('hostname');
|
||||||
|
const prefix = HN_PREFIX[hb.readUInt8(0) % HN_PREFIX.length];
|
||||||
|
const middle = HN_MIDDLE[hb.readUInt8(1) % HN_MIDDLE.length];
|
||||||
|
const style = HN_STYLE[hb.readUInt8(2) % HN_STYLE.length];
|
||||||
|
const tail = hb.slice(3, 5).toString('hex'); // 4 hex chars 保证唯一
|
||||||
|
let hostname;
|
||||||
|
if (middle) {
|
||||||
|
const sep = style === 'dot' ? '.' : style === 'dash' ? '-' : '';
|
||||||
|
hostname = `${prefix}${sep}${middle}`;
|
||||||
|
} else {
|
||||||
|
// 无中间词时必须加 hex 尾缀,避免 hostname 太短(如裸 "my"、"dev")
|
||||||
|
hostname = `${prefix}-${tail}`;
|
||||||
|
}
|
||||||
|
// 有中间词时 50% 概率加 hex 尾缀(真实场景很多人用 hostname 如 "dev-box-a3f2")
|
||||||
|
if (middle && hb.readUInt8(5) % 2 === 0) hostname += `-${tail}`;
|
||||||
|
|
||||||
|
// ── username: 组合生成,如 "alexdev", "sam42", "chris_dev" ──
|
||||||
|
const ub = h('username');
|
||||||
|
const first = UN_FIRST[ub.readUInt8(0) % UN_FIRST.length];
|
||||||
|
const suffix = UN_SUFFIX[ub.readUInt8(1) % UN_SUFFIX.length];
|
||||||
|
const username = `${first}${suffix}`;
|
||||||
|
|
||||||
|
// ── terminal & shell: 按权重分布(xterm-256color 占大多数) ──
|
||||||
|
const termRoll = h('terminal').readUInt8(0) % 100;
|
||||||
|
const terminal = termRoll < 60 ? 'xterm-256color' :
|
||||||
|
termRoll < 75 ? 'screen-256color' :
|
||||||
|
termRoll < 88 ? 'tmux-256color' :
|
||||||
|
termRoll < 95 ? 'alacritty' : 'rxvt-unicode-256color';
|
||||||
|
|
||||||
|
const shellRoll = h('shell').readUInt8(0) % 100;
|
||||||
|
const shell = shellRoll < 55 ? '/bin/bash' :
|
||||||
|
shellRoll < 85 ? '/bin/zsh' :
|
||||||
|
shellRoll < 95 ? '/usr/bin/bash' : '/usr/bin/zsh';
|
||||||
|
|
||||||
|
// ── host.id: /etc/machine-id (32 hex chars, Linux 标准) ──
|
||||||
|
const machineId = h('machine-id').slice(0, 16).toString('hex');
|
||||||
|
|
||||||
|
// ── PID: 每个 session 随机生成,模拟每次启动新进程 ──
|
||||||
|
// 不用 seed 确定性生成,因为真实 CLI 每次启动都是新 PID
|
||||||
|
const pid = 1000 + Math.floor(Math.random() * 64000);
|
||||||
|
|
||||||
|
// ── kernel version: 模拟真实 Linux 发行版 ──
|
||||||
|
const kb = h('kernel');
|
||||||
|
const kernelMajor = 5 + (kb.readUInt8(0) % 2); // 5 or 6
|
||||||
|
const kernelMinor = kb.readUInt8(1) % 20;
|
||||||
|
const kernelPatch = kb.readUInt8(2) % 200;
|
||||||
|
const ubuntuBuild = 50 + (kb.readUInt8(3) % 150);
|
||||||
|
const osVersion = `#${ubuntuBuild}-Ubuntu SMP`;
|
||||||
|
|
||||||
|
// ── 可执行文件路径: 按安装方式分布 ──
|
||||||
|
const pathRoll = h('execpath').readUInt8(0) % 100;
|
||||||
|
const executablePath = pathRoll < 40 ? `/home/${username}/.claude/local/claude` :
|
||||||
|
pathRoll < 70 ? '/usr/local/bin/claude' :
|
||||||
|
pathRoll < 90 ? `/home/${username}/.local/bin/claude` :
|
||||||
|
'/usr/bin/claude';
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
username,
|
||||||
|
terminal,
|
||||||
|
shell,
|
||||||
|
machineId,
|
||||||
|
pid,
|
||||||
|
arch: 'x64',
|
||||||
|
osType: 'Linux',
|
||||||
|
osVersion,
|
||||||
|
kernelRelease: `${kernelMajor}.${kernelMinor}.${kernelPatch}-generic`,
|
||||||
|
// service.instance.id: 每个 session 唯一(CLI 用 randomUUID)
|
||||||
|
serviceInstanceId: crypto.randomUUID(),
|
||||||
|
executablePath,
|
||||||
|
executableName: 'claude',
|
||||||
|
command: 'claude',
|
||||||
|
commandArgs: [],
|
||||||
|
runtimeName: 'nodejs',
|
||||||
|
runtimeVersion: FAKE_NODE_VERSION.replace('v', ''),
|
||||||
|
// ripgrep 信息也按 seed 生成,不同账号不一样
|
||||||
|
ripgrepVersion: (() => {
|
||||||
|
const rv = h('ripgrep');
|
||||||
|
const versions = ['14.1.1','14.1.0','14.0.2','13.0.0','13.0.1','14.0.1','14.0.0'];
|
||||||
|
return versions[rv.readUInt8(0) % versions.length];
|
||||||
|
})(),
|
||||||
|
ripgrepPath: (() => {
|
||||||
|
const rp = h('rgpath');
|
||||||
|
const paths = ['/usr/bin/rg','/usr/local/bin/rg','/home/'+username+'/.cargo/bin/rg','/snap/bin/rg','/usr/bin/rg','/usr/bin/rg'];
|
||||||
|
return paths[rp.readUInt8(0) % paths.length];
|
||||||
|
})(),
|
||||||
|
// MCP server 数量(真实用户 0~6 个,影响启动事件序列)
|
||||||
|
mcpServerCount: 1 + (h('mcp').readUInt8(0) % 5), // 1~5
|
||||||
|
mcpFailCount: h('mcp').readUInt8(1) % 3, // 0~2 个失败
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 遥测模拟 ────────────────────────────────────────────
|
// ─── 遥测模拟 ────────────────────────────────────────────
|
||||||
|
|
||||||
// 每个 device_id 的会话状态
|
// 每个 device_id 的会话状态
|
||||||
@ -34,11 +152,15 @@ const sessionStates = new Map();
|
|||||||
|
|
||||||
function getOrCreateSession(deviceId) {
|
function getOrCreateSession(deviceId) {
|
||||||
if (sessionStates.has(deviceId)) return sessionStates.get(deviceId);
|
if (sessionStates.has(deviceId)) return sessionStates.get(deviceId);
|
||||||
|
const hostId = generateHostIdentity(deviceId);
|
||||||
const state = {
|
const state = {
|
||||||
sessionId: crypto.randomUUID(),
|
sessionId: crypto.randomUUID(),
|
||||||
deviceId,
|
deviceId,
|
||||||
|
hostId,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
requestCount: 0,
|
requestCount: 0,
|
||||||
|
// 追踪 ripgrep 是否已上报
|
||||||
|
ripgrepReported: false,
|
||||||
};
|
};
|
||||||
sessionStates.set(deviceId, state);
|
sessionStates.set(deviceId, state);
|
||||||
return state;
|
return state;
|
||||||
@ -48,11 +170,13 @@ function generateDeviceId(accountSeed) {
|
|||||||
return crypto.createHash('sha256').update(`device:${accountSeed}`).digest('hex');
|
return crypto.createHash('sha256').update(`device:${accountSeed}`).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEnvBlock() {
|
// ─── OTEL Resource Attributes (匹配 CLI 的 detectResources) ───
|
||||||
|
|
||||||
|
function buildEnvBlock(hostId) {
|
||||||
return {
|
return {
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
node_version: process.version,
|
node_version: FAKE_NODE_VERSION,
|
||||||
terminal: 'xterm-256color',
|
terminal: hostId.terminal,
|
||||||
package_managers: 'npm',
|
package_managers: 'npm',
|
||||||
runtimes: 'node',
|
runtimes: 'node',
|
||||||
is_running_with_bun: false,
|
is_running_with_bun: false,
|
||||||
@ -62,7 +186,7 @@ function buildEnvBlock() {
|
|||||||
is_claude_code_action: false,
|
is_claude_code_action: false,
|
||||||
is_claude_ai_auth: false,
|
is_claude_ai_auth: false,
|
||||||
version: CLI_VERSION,
|
version: CLI_VERSION,
|
||||||
arch: 'x64',
|
arch: hostId.arch,
|
||||||
is_claude_code_remote: false,
|
is_claude_code_remote: false,
|
||||||
deployment_environment: 'unknown-linux',
|
deployment_environment: 'unknown-linux',
|
||||||
is_conductor: false,
|
is_conductor: false,
|
||||||
@ -75,59 +199,82 @@ function buildEnvBlock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildProcessMetrics(uptime) {
|
function buildProcessMetrics(uptime) {
|
||||||
const rss = 200_000_000 + Math.floor(Math.random() * 100_000_000);
|
// 模拟真实 CLI 的内存曲线:RSS 随 uptime 缓慢增长
|
||||||
|
const baseRss = 180_000_000 + Math.min(uptime * 50_000, 200_000_000);
|
||||||
|
const rss = Math.floor(baseRss + Math.random() * 80_000_000);
|
||||||
|
const heapTotal = Math.floor(rss * 0.6 + Math.random() * 10_000_000);
|
||||||
|
const heapUsed = Math.floor(heapTotal * 0.5 + Math.random() * heapTotal * 0.3);
|
||||||
return Buffer.from(JSON.stringify({
|
return Buffer.from(JSON.stringify({
|
||||||
uptime,
|
uptime,
|
||||||
rss,
|
rss,
|
||||||
heapTotal: 30_000_000 + Math.floor(Math.random() * 5_000_000),
|
heapTotal,
|
||||||
heapUsed: 40_000_000 + Math.floor(Math.random() * 20_000_000),
|
heapUsed,
|
||||||
external: 14_000_000 + Math.floor(Math.random() * 2_000_000),
|
external: 14_000_000 + Math.floor(Math.random() * 2_000_000),
|
||||||
arrayBuffers: Math.floor(Math.random() * 10_000),
|
arrayBuffers: Math.floor(Math.random() * 10_000),
|
||||||
constrainedMemory: 0,
|
constrainedMemory: 0,
|
||||||
cpuUsage: { user: 100_000 + Math.floor(Math.random() * 300_000), system: 20_000 + Math.floor(Math.random() * 80_000) },
|
cpuUsage: {
|
||||||
|
user: Math.floor(uptime * 10_000 + Math.random() * 300_000),
|
||||||
|
system: Math.floor(uptime * 2_000 + Math.random() * 80_000),
|
||||||
|
},
|
||||||
cpuPercent: Math.random() * 200,
|
cpuPercent: Math.random() * 200,
|
||||||
})).toString('base64');
|
})).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEvent(eventName, session, model, betas) {
|
function buildEvent(eventName, session, model, betas, extraData, timestampOverride) {
|
||||||
const uptime = (Date.now() - session.startTime) / 1000;
|
const uptime = (Date.now() - session.startTime) / 1000;
|
||||||
return {
|
const processMetrics = buildProcessMetrics(uptime);
|
||||||
event_type: 'ClaudeCodeInternalEvent',
|
// 缓存最近一次的 process metrics,供 DataDog 日志复用(保持两边一致)
|
||||||
event_data: {
|
session._lastProcessMetrics = { uptime, raw: processMetrics };
|
||||||
|
const eventData = {
|
||||||
event_name: eventName,
|
event_name: eventName,
|
||||||
client_timestamp: new Date().toISOString(),
|
client_timestamp: timestampOverride || new Date().toISOString(),
|
||||||
model: model || 'claude-sonnet-4-6',
|
model: model || 'claude-sonnet-4-6',
|
||||||
session_id: session.sessionId,
|
session_id: session.sessionId,
|
||||||
user_type: 'external',
|
user_type: 'external',
|
||||||
betas: betas || 'claude-code-20250219,interleaved-thinking-2025-05-14',
|
betas: betas || 'claude-code-20250219,interleaved-thinking-2025-05-14',
|
||||||
env: buildEnvBlock(),
|
env: buildEnvBlock(session.hostId),
|
||||||
entrypoint: 'cli',
|
entrypoint: 'cli',
|
||||||
is_interactive: true,
|
is_interactive: true,
|
||||||
client_type: 'cli',
|
client_type: 'cli',
|
||||||
process: buildProcessMetrics(uptime),
|
process: processMetrics,
|
||||||
event_id: crypto.randomUUID(),
|
event_id: crypto.randomUUID(),
|
||||||
device_id: session.deviceId,
|
device_id: session.deviceId,
|
||||||
},
|
// 注意:不加 resource 字段 — event_logging/batch 是自定义端点,
|
||||||
|
// OTEL resource attributes 由 CLI 通过单独的 OTLP exporter 发送,不在这里
|
||||||
|
};
|
||||||
|
// 合并额外字段(用于特定事件的附加数据)
|
||||||
|
if (extraData) Object.assign(eventData, extraData);
|
||||||
|
return {
|
||||||
|
event_type: 'ClaudeCodeInternalEvent',
|
||||||
|
event_data: eventData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送遥测到 api.anthropic.com/api/event_logging/batch
|
// 发送遥测到 api.anthropic.com/api/event_logging/batch
|
||||||
function sendTelemetryEvents(events) {
|
function sendTelemetryEvents(events, session) {
|
||||||
if (!TELEMETRY_ENABLED || events.length === 0) return;
|
if (!TELEMETRY_ENABLED || events.length === 0) return;
|
||||||
|
|
||||||
const body = JSON.stringify({ events });
|
const body = JSON.stringify({ events });
|
||||||
const opts = {
|
const headers = {
|
||||||
hostname: 'api.anthropic.com',
|
|
||||||
port: 443,
|
|
||||||
path: '/api/event_logging/batch',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json, text/plain, */*',
|
'Accept': 'application/json, text/plain, */*',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': `claude-code/${CLI_VERSION}`,
|
'User-Agent': `claude-code/${CLI_VERSION}`,
|
||||||
'x-service-name': 'claude-code',
|
'x-service-name': 'claude-code',
|
||||||
'Content-Length': Buffer.byteLength(body),
|
'Content-Length': Buffer.byteLength(body),
|
||||||
},
|
};
|
||||||
|
// 如果有 session,注入 OTEL trace headers(匹配 CLI 的 W3C Trace Context)
|
||||||
|
if (session) {
|
||||||
|
const traceId = crypto.randomBytes(16).toString('hex');
|
||||||
|
const spanId = crypto.randomBytes(8).toString('hex');
|
||||||
|
headers['traceparent'] = `00-${traceId}-${spanId}-01`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
hostname: 'api.anthropic.com',
|
||||||
|
port: 443,
|
||||||
|
path: '/api/event_logging/batch',
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -146,13 +293,40 @@ function sendTelemetryEvents(events) {
|
|||||||
function sendDatadogLog(eventName, session, model) {
|
function sendDatadogLog(eventName, session, model) {
|
||||||
if (!TELEMETRY_ENABLED) return;
|
if (!TELEMETRY_ENABLED) return;
|
||||||
|
|
||||||
|
const hostId = session.hostId;
|
||||||
const uptime = (Date.now() - session.startTime) / 1000;
|
const uptime = (Date.now() - session.startTime) / 1000;
|
||||||
|
|
||||||
|
// 复用 Anthropic 事件侧缓存的 process metrics(保持两边数值一致)
|
||||||
|
// 如果没有缓存(首次调用),现场生成
|
||||||
|
let pm;
|
||||||
|
if (session._lastProcessMetrics && Math.abs(session._lastProcessMetrics.uptime - uptime) < 2) {
|
||||||
|
pm = JSON.parse(Buffer.from(session._lastProcessMetrics.raw, 'base64').toString());
|
||||||
|
} else {
|
||||||
|
const baseRss = 180_000_000 + Math.min(uptime * 50_000, 200_000_000);
|
||||||
|
const rss = Math.floor(baseRss + Math.random() * 80_000_000);
|
||||||
|
const heapTotal = Math.floor(rss * 0.6 + Math.random() * 10_000_000);
|
||||||
|
const heapUsed = Math.floor(heapTotal * 0.5 + Math.random() * heapTotal * 0.3);
|
||||||
|
pm = {
|
||||||
|
uptime,
|
||||||
|
rss,
|
||||||
|
heapTotal,
|
||||||
|
heapUsed,
|
||||||
|
external: 14_000_000 + Math.floor(Math.random() * 2_000_000),
|
||||||
|
arrayBuffers: Math.floor(Math.random() * 10_000),
|
||||||
|
constrainedMemory: 0,
|
||||||
|
cpuUsage: {
|
||||||
|
user: Math.floor(uptime * 10_000 + Math.random() * 300_000),
|
||||||
|
system: Math.floor(uptime * 2_000 + Math.random() * 80_000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const entry = {
|
const entry = {
|
||||||
ddsource: 'nodejs',
|
ddsource: 'nodejs',
|
||||||
ddtags: `event:${eventName},arch:x64,client_type:cli,model:${model || 'claude-sonnet-4-6'},platform:linux,user_type:external,version:${CLI_VERSION},version_base:${CLI_VERSION}`,
|
ddtags: `event:${eventName},arch:${hostId.arch},client_type:cli,model:${model || 'claude-sonnet-4-6'},platform:linux,user_type:external,version:${CLI_VERSION},version_base:${CLI_VERSION}`,
|
||||||
message: eventName,
|
message: eventName,
|
||||||
service: 'claude-code',
|
service: 'claude-code',
|
||||||
hostname: 'claude-code',
|
hostname: hostId.hostname,
|
||||||
env: 'external',
|
env: 'external',
|
||||||
model: model || 'claude-sonnet-4-6',
|
model: model || 'claude-sonnet-4-6',
|
||||||
session_id: session.sessionId,
|
session_id: session.sessionId,
|
||||||
@ -160,20 +334,11 @@ function sendDatadogLog(eventName, session, model) {
|
|||||||
entrypoint: 'cli',
|
entrypoint: 'cli',
|
||||||
is_interactive: 'true',
|
is_interactive: 'true',
|
||||||
client_type: 'cli',
|
client_type: 'cli',
|
||||||
process_metrics: {
|
process_metrics: pm,
|
||||||
uptime,
|
|
||||||
rss: 200_000_000 + Math.floor(Math.random() * 100_000_000),
|
|
||||||
heapTotal: 30_000_000 + Math.floor(Math.random() * 5_000_000),
|
|
||||||
heapUsed: 40_000_000 + Math.floor(Math.random() * 20_000_000),
|
|
||||||
external: 14_000_000 + Math.floor(Math.random() * 2_000_000),
|
|
||||||
arrayBuffers: Math.floor(Math.random() * 10_000),
|
|
||||||
constrainedMemory: 0,
|
|
||||||
cpuUsage: { user: 100_000 + Math.floor(Math.random() * 300_000), system: 20_000 + Math.floor(Math.random() * 80_000) },
|
|
||||||
},
|
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
platform_raw: 'linux',
|
platform_raw: 'linux',
|
||||||
arch: 'x64',
|
arch: hostId.arch,
|
||||||
node_version: process.version,
|
node_version: FAKE_NODE_VERSION,
|
||||||
version: CLI_VERSION,
|
version: CLI_VERSION,
|
||||||
version_base: CLI_VERSION,
|
version_base: CLI_VERSION,
|
||||||
build_time: BUILD_TIME,
|
build_time: BUILD_TIME,
|
||||||
@ -219,18 +384,38 @@ function emitPreRequestTelemetry(reqHeaders, body) {
|
|||||||
|
|
||||||
const betas = reqHeaders['anthropic-beta'] || 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24';
|
const betas = reqHeaders['anthropic-beta'] || 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24';
|
||||||
|
|
||||||
// 首次请求:发完整启动事件序列(匹配真实 CLI 抓包:4-6 个事件)
|
// 首次请求:发完整启动事件序列(匹配真实 CLI 的时序)
|
||||||
if (session.requestCount === 1) {
|
if (session.requestCount === 1) {
|
||||||
// 第一批:MCP 连接事件(真实 CLI 有多个 MCP server)
|
const hostId = session.hostId;
|
||||||
|
// 生成递增的时间戳,模拟真实 CLI 启动流程的时间差
|
||||||
|
const baseTime = Date.now();
|
||||||
|
const ts = (offsetMs) => new Date(baseTime + offsetMs).toISOString();
|
||||||
|
|
||||||
|
// 第一批:启动 + 工具检测 + MCP 连接事件
|
||||||
const batch1 = [
|
const batch1 = [
|
||||||
buildEvent('tengu_started', session, model, betas),
|
buildEvent('tengu_started', session, model, betas, null, ts(0)),
|
||||||
buildEvent('tengu_init', session, model, betas),
|
buildEvent('tengu_init', session, model, betas, null, ts(80 + Math.floor(Math.random() * 120))),
|
||||||
buildEvent('tengu_mcp_server_connection_failed', session, model, betas),
|
// tengu_ripgrep_availability — CLI 必发的工具检测事件,版本/路径按账号不同
|
||||||
buildEvent('tengu_mcp_server_connection_failed', session, model, betas),
|
buildEvent('tengu_ripgrep_availability', session, model, betas, {
|
||||||
buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas),
|
ripgrep_available: true,
|
||||||
buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas),
|
ripgrep_version: hostId.ripgrepVersion,
|
||||||
|
ripgrep_path: hostId.ripgrepPath,
|
||||||
|
}, ts(200 + Math.floor(Math.random() * 150))),
|
||||||
];
|
];
|
||||||
sendTelemetryEvents(batch1);
|
// MCP 连接事件:数量按账号不同(真实用户配置的 MCP server 数量差异很大)
|
||||||
|
let mcpOffset = 400;
|
||||||
|
const mcpSuccessCount = hostId.mcpServerCount - hostId.mcpFailCount;
|
||||||
|
for (let i = 0; i < hostId.mcpFailCount; i++) {
|
||||||
|
mcpOffset += 100 + Math.floor(Math.random() * 300);
|
||||||
|
batch1.push(buildEvent('tengu_mcp_server_connection_failed', session, model, betas, null, ts(mcpOffset)));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < mcpSuccessCount; i++) {
|
||||||
|
mcpOffset += 200 + Math.floor(Math.random() * 500);
|
||||||
|
batch1.push(buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas, null, ts(mcpOffset)));
|
||||||
|
}
|
||||||
|
|
||||||
|
session.ripgrepReported = true;
|
||||||
|
sendTelemetryEvents(batch1, session);
|
||||||
sendDatadogLog('tengu_started', session, model);
|
sendDatadogLog('tengu_started', session, model);
|
||||||
sendDatadogLog('tengu_init', session, model);
|
sendDatadogLog('tengu_init', session, model);
|
||||||
|
|
||||||
@ -240,7 +425,7 @@ function emitPreRequestTelemetry(reqHeaders, body) {
|
|||||||
buildEvent('tengu_session_init', session, model, betas),
|
buildEvent('tengu_session_init', session, model, betas),
|
||||||
buildEvent('tengu_context_loaded', session, model, betas),
|
buildEvent('tengu_context_loaded', session, model, betas),
|
||||||
];
|
];
|
||||||
sendTelemetryEvents(batch2);
|
sendTelemetryEvents(batch2, session);
|
||||||
}, 25000 + Math.floor(Math.random() * 10000));
|
}, 25000 + Math.floor(Math.random() * 10000));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +433,7 @@ function emitPreRequestTelemetry(reqHeaders, body) {
|
|||||||
const events = [
|
const events = [
|
||||||
buildEvent('tengu_api_request_started', session, model, betas),
|
buildEvent('tengu_api_request_started', session, model, betas),
|
||||||
];
|
];
|
||||||
sendTelemetryEvents(events);
|
sendTelemetryEvents(events, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求后发遥测
|
// 请求后发遥测
|
||||||
@ -270,16 +455,28 @@ function emitPostRequestTelemetry(reqHeaders, statusCode, body) {
|
|||||||
buildEvent('tengu_api_request_completed', session, model, betas),
|
buildEvent('tengu_api_request_completed', session, model, betas),
|
||||||
buildEvent('tengu_conversation_turn_completed', session, model, betas),
|
buildEvent('tengu_conversation_turn_completed', session, model, betas),
|
||||||
];
|
];
|
||||||
sendTelemetryEvents(events);
|
sendTelemetryEvents(events, session);
|
||||||
sendDatadogLog('tengu_api_request_completed', session, model);
|
sendDatadogLog('tengu_api_request_completed', session, model);
|
||||||
|
|
||||||
// 随机发额外事件(模拟用户行为:打开文件、查看搜索等)
|
// 模拟错误遥测(低概率,匹配 TelemetrySafeError)
|
||||||
|
if (statusCode >= 400 && Math.random() < 0.5) {
|
||||||
|
const errorEvent = buildEvent('tengu_api_request_error', session, model, betas, {
|
||||||
|
error_type: 'TelemetrySafeError',
|
||||||
|
error_code: statusCode,
|
||||||
|
error_message: statusCode === 429 ? 'rate_limit_exceeded' :
|
||||||
|
statusCode === 529 ? 'overloaded' :
|
||||||
|
statusCode >= 500 ? 'server_error' : 'client_error',
|
||||||
|
});
|
||||||
|
sendTelemetryEvents([errorEvent], session);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机发额外事件(仅使用已知的真实 CLI 事件名)
|
||||||
if (Math.random() < 0.3) {
|
if (Math.random() < 0.3) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const extra = [
|
const extra = [
|
||||||
buildEvent('tengu_tool_use_completed', session, model, betas),
|
buildEvent('tengu_tool_use_completed', session, model, betas),
|
||||||
];
|
];
|
||||||
sendTelemetryEvents(extra);
|
sendTelemetryEvents(extra, session);
|
||||||
}, 2000 + Math.floor(Math.random() * 5000));
|
}, 2000 + Math.floor(Math.random() * 5000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user