Files
metona-ai-desktop/electron/utils/slo.ts
T
thzxx e4d81d8247 feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题,
并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。

Critical (10/10 完成):
- C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配
  可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过

High (11/11 完成):
- 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等

Medium (55/55 完成):
- 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、
  React 组件 cancelled 标志、类型收窄等

Low (20/20 完成):
- 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等)
- nanoid 统一替代 Date.now()+Math.random()
- confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等

返工审计修复 (10/10 完成):
- L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert
- M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死
- M-42: 脱敏短值(length <= 4)泄露修复
- M-47: tasks:update 补全 title/description 类型校验
- L-9: ollama.adapter 非流式路径 nanoid 统一
- M-45: audit:query limit 策略与 memory:listAll 一致化
- SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup
- L-15: CommandPalette useMemo 补全 sessions 响应式依赖
- useAgentStream 事件类型补全 seq/timestamp 字段

新增依赖: shell-quote + @types/shell-quote
版本号: 0.3.0 -> 0.3.1
2026-07-13 22:36:58 +08:00

285 lines
7.9 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.
/**
* Health Checker + SLO Monitor — 健康检查器与 SLO 监控
*
* HealthChecker: 对数据库连通性、磁盘空间、内存使用执行真实检查。
* SLOMonitor: C-9 修复 — 补全 SLO 监控核心功能(错误率、延迟分布、吞吐量、燃烧速率)。
*
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十一章
*/
import { existsSync } from 'fs';
import { app } from 'electron';
import type Database from 'better-sqlite3';
export interface HealthReport {
healthy: boolean;
checks: HealthCheck[];
timestamp: Date;
}
export interface HealthCheck {
name: string;
healthy: boolean;
latencyMs?: number;
error?: string;
}
export class HealthChecker {
/**
* @param getDB 数据库获取器(用于真实验证数据库连通性)
* @param dbPath 数据库文件路径(后备:文件存在性检查)
*/
constructor(
private getDB?: () => Database.Database,
private dbPath?: string,
) {}
async check(): Promise<HealthReport> {
const checks: HealthCheck[] = [];
checks.push(await this.checkDatabase());
checks.push(await this.checkFreeMemory());
checks.push(await this.checkMemoryUsage());
const allHealthy = checks.every((c) => c.healthy);
return { healthy: allHealthy, checks, timestamp: new Date() };
}
private async checkDatabase(): Promise<HealthCheck> {
const start = Date.now();
try {
// 优先使用注入的 DB 实例执行真实 ping
if (this.getDB) {
const db = this.getDB();
// 执行真实查询验证数据库连通性
db.prepare('SELECT 1').get();
return { name: 'database', healthy: true, latencyMs: Date.now() - start };
}
// 后备:检查数据库文件是否存在
if (this.dbPath && existsSync(this.dbPath)) {
return { name: 'database', healthy: true, latencyMs: Date.now() - start };
}
return { name: 'database', healthy: false, error: 'Database not available' };
} catch (error) {
return {
name: 'database',
healthy: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
private async checkFreeMemory(): Promise<HealthCheck> {
try {
const userDataPath = app.getPath('userData');
if (!existsSync(userDataPath)) {
return { name: 'free_memory', healthy: false, error: 'User data path not accessible' };
}
// 检查实际可用内存(Windows/Linux 返回字节数)
const { freemem } = await import('os');
const freeBytes = freemem();
const freeMB = freeBytes / (1024 * 1024);
// 少于 200MB 视为不健康
if (freeMB < 200) {
return {
name: 'free_memory',
healthy: false,
error: `Free memory critically low: ${freeMB.toFixed(0)}MB available`,
};
}
return { name: 'free_memory', healthy: true };
} catch (error) {
return {
name: 'free_memory',
healthy: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
private async checkMemoryUsage(): Promise<HealthCheck> {
const used = process.memoryUsage();
const heapUsedMB = used.heapUsed / 1024 / 1024;
const thresholdMB = 512;
return {
name: 'memory_usage',
healthy: heapUsedMB < thresholdMB,
...(heapUsedMB >= thresholdMB
? { error: `Heap usage ${heapUsedMB.toFixed(0)}MB exceeds threshold ${thresholdMB}MB` }
: {}),
};
}
}
// ===== C-9 修复: SLO Monitor — SLO 监控核心功能 =====
/**
* SLO 监控配置
*/
export interface SLOConfig {
/** SLO 目标(如 0.999 表示 99.9% 可用性) */
target: number;
/** 滑动窗口大小(毫秒,默认 5 分钟) */
windowMs: number;
/** 延迟分位数(如 0.95 表示 P95) */
latencyPercentiles: number[];
/** 延迟告警阈值(毫秒) */
latencyThresholdMs: number;
}
/**
* SLO 请求记录
*/
interface SLORequestRecord {
timestamp: number;
latencyMs: number;
success: boolean;
}
/**
* SLO 状态报告
*/
export interface SLOStatus {
/** 当前错误率(0-1 */
errorRate: number;
/** 当前吞吐量(请求/秒) */
throughput: number;
/** 平均延迟(毫秒) */
avgLatencyMs: number;
/** 延迟分位数 */
percentiles: Record<string, number>;
/** 燃烧速率(实际错误率 / 错误预算) */
burnRate: number;
/** SLO 目标 */
target: number;
/** 错误预算(1 - target */
errorBudget: number;
/** 是否违反 SLO */
violated: boolean;
/** 窗口内总请求数 */
totalRequests: number;
/** 窗口内错误请求数 */
errorRequests: number;
/** 时间戳 */
timestamp: Date;
}
/**
* SLO Monitor — SLO 监控器
*
* C-9 修复: 补全 SLO 监控核心功能
*
* 功能:
* 1. 错误率追踪 — 记录请求成功/失败,计算滑动窗口内错误率
* 2. 延迟分布追踪 — 记录请求延迟,计算 P50/P95/P99 分位数
* 3. 吞吐量追踪 — 计算每秒请求数
* 4. 燃烧速率计算 — 实际错误率 / 错误预算,>1 表示 SLO 即将违反
*
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十一章
*/
export class SLOMonitor {
private records: SLORequestRecord[] = [];
private readonly config: SLOConfig;
constructor(config: Partial<SLOConfig> = {}) {
this.config = {
target: 0.999,
windowMs: 5 * 60 * 1000, // 5 分钟
latencyPercentiles: [0.5, 0.95, 0.99],
latencyThresholdMs: 5000,
...config,
};
}
/**
* 记录一次请求
* @param latencyMs 延迟(毫秒)
* @param success 是否成功
*/
recordRequest(latencyMs: number, success: boolean): void {
const record: SLORequestRecord = {
timestamp: Date.now(),
latencyMs,
success,
};
this.records.push(record);
// 清理过期记录
this.cleanup();
}
/**
* 清理窗口外的过期记录
*/
private cleanup(): void {
const cutoff = Date.now() - this.config.windowMs;
// 保留最近 10 分钟的数据(2 倍窗口),避免边界效应
this.records = this.records.filter((r) => r.timestamp >= cutoff);
}
/**
* 获取当前 SLO 状态
*/
getStatus(): SLOStatus {
this.cleanup();
const now = Date.now();
const windowStart = now - this.config.windowMs;
const windowRecords = this.records.filter((r) => r.timestamp >= windowStart);
const totalRequests = windowRecords.length;
const errorRequests = windowRecords.filter((r) => !r.success).length;
const errorRate = totalRequests > 0 ? errorRequests / totalRequests : 0;
const throughput = totalRequests / (this.config.windowMs / 1000);
const latencies = windowRecords.map((r) => r.latencyMs).sort((a, b) => a - b);
const avgLatencyMs = latencies.length > 0
? latencies.reduce((sum, l) => sum + l, 0) / latencies.length
: 0;
const percentiles: Record<string, number> = {};
for (const p of this.config.latencyPercentiles) {
const key = `P${(p * 100).toFixed(0)}`;
percentiles[key] = this.calculatePercentile(latencies, p);
}
const errorBudget = 1 - this.config.target;
const burnRate = errorBudget > 0 ? errorRate / errorBudget : 0;
return {
errorRate,
throughput,
avgLatencyMs,
percentiles,
burnRate,
target: this.config.target,
errorBudget,
violated: burnRate > 1,
totalRequests,
errorRequests,
timestamp: new Date(),
};
}
/**
* 计算分位数
*/
private calculatePercentile(sortedValues: number[], percentile: number): number {
if (sortedValues.length === 0) return 0;
const index = Math.ceil(percentile * sortedValues.length) - 1;
return sortedValues[Math.max(0, index)];
}
/**
* 重置所有记录
*/
reset(): void {
this.records = [];
}
/**
* 获取配置
*/
getConfig(): SLOConfig {
return { ...this.config };
}
}