Files
zclaw_openfang/desktop/src/lib/telemetry-collector.ts
iven 5fdf96c3f5 chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
2026-03-29 10:46:41 +08:00

210 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Telemetry Collector — 桌面端遥测收集器
*
* 收集本地 LLM 调用的 Token 用量统计和审计日志摘要(均无内容),
* 定期批量上报到 SaaS。
*
* 用量缓冲区上限 100 条,审计缓冲区上限 200 条,超限自动 flush。
* 定时 flush 每 5 分钟。仅在 SaaS 已登录时上报。
*/
import { saasClient } from './saas-client';
import { createLogger } from './logger';
const log = createLogger('TelemetryCollector');
// === Types ===
export interface TelemetryEntry {
model_id: string;
input_tokens: number;
output_tokens: number;
latency_ms?: number;
success: boolean;
error_type?: string;
timestamp: string;
connection_mode: string;
}
interface AuditEntry {
action: string;
target: string;
result: string;
timestamp: string;
}
// === State ===
const USAGE_BUFFER_LIMIT = 100;
const AUDIT_BUFFER_LIMIT = 200;
const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
let usageBuffer: TelemetryEntry[] = [];
let auditBuffer: AuditEntry[] = [];
let flushTimer: ReturnType<typeof setInterval> | null = null;
let deviceId: string | null = null;
// === Public API ===
/**
* 初始化遥测收集器(在 SaaS 登录后调用)。
* @param devId 设备 ID与 saasStore 使用的相同)
*/
export function initTelemetryCollector(devId: string): void {
deviceId = devId;
if (flushTimer) {
clearInterval(flushTimer);
}
flushTimer = setInterval(() => {
flushAll().catch((err: unknown) => {
log.warn('Scheduled telemetry flush failed:', err);
});
}, FLUSH_INTERVAL_MS);
log.info('Telemetry collector initialized');
}
/**
* 停止遥测收集器(在 SaaS 登出时调用)。
* 会尝试 flush 剩余条目。
*/
export function stopTelemetryCollector(): void {
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
// 尝试最后一次 flush
flushAll().catch(() => {
// 登出时不阻塞
});
usageBuffer = [];
auditBuffer = [];
deviceId = null;
log.info('Telemetry collector stopped');
}
/**
* 记录一次 LLM 调用的用量。
*
* @param modelId 模型标识
* @param inputTokens 输入 Token 数
* @param outputTokens 输出 Token 数
* @param options 可选参数
*/
export function recordLLMUsage(
modelId: string,
inputTokens: number,
outputTokens: number,
options?: {
latencyMs?: number;
success?: boolean;
errorType?: string;
connectionMode?: string;
},
): void {
if (!deviceId) return;
usageBuffer.push({
model_id: modelId,
input_tokens: inputTokens,
output_tokens: outputTokens,
latency_ms: options?.latencyMs,
success: options?.success ?? true,
error_type: options?.errorType,
timestamp: new Date().toISOString(),
connection_mode: options?.connectionMode || 'tauri',
});
if (usageBuffer.length >= USAGE_BUFFER_LIMIT) {
flushUsage().catch((err: unknown) => {
log.warn('Auto-flush usage triggered but failed:', err);
});
}
}
/**
* 记录一条审计日志摘要(仅操作类型,无内容)。
*
* @param action 操作类型(如 "hand.trigger", "agent.create"
* @param target 操作目标(如 Agent/Hand 名称)
* @param result 操作结果
*/
export function recordAuditEvent(
action: string,
target: string,
result: 'success' | 'failure' | 'pending',
): void {
if (!deviceId) return;
auditBuffer.push({
action,
target,
result,
timestamp: new Date().toISOString(),
});
if (auditBuffer.length >= AUDIT_BUFFER_LIMIT) {
flushAudit().catch((err: unknown) => {
log.warn('Auto-flush audit triggered but failed:', err);
});
}
}
// === Internal ===
async function flushAll(): Promise<void> {
await Promise.allSettled([
flushUsage(),
flushAudit(),
]);
}
async function flushUsage(): Promise<void> {
if (usageBuffer.length === 0 || !deviceId || !saasClient.isAuthenticated()) {
return;
}
const entries = usageBuffer;
usageBuffer = [];
try {
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'unknown';
const result = await saasClient.reportTelemetry({
device_id: deviceId,
app_version: appVersion,
entries,
});
log.info(`Usage telemetry flushed: ${result.accepted} accepted, ${result.rejected} rejected`);
} catch (err: unknown) {
usageBuffer = [...entries, ...usageBuffer].slice(0, USAGE_BUFFER_LIMIT * 2);
log.warn('Usage telemetry flush failed, entries re-buffered:', err);
}
}
async function flushAudit(): Promise<void> {
if (auditBuffer.length === 0 || !deviceId || !saasClient.isAuthenticated()) {
return;
}
const entries = auditBuffer;
auditBuffer = [];
try {
const result = await saasClient.reportAuditSummary({
device_id: deviceId,
entries,
});
log.info(`Audit summary flushed: ${result.accepted} accepted`);
} catch (err: unknown) {
auditBuffer = [...entries, ...auditBuffer].slice(0, AUDIT_BUFFER_LIMIT * 2);
log.warn('Audit summary flush failed, entries re-buffered:', err);
}
}