Files
metona-ai-desktop/electron/services/audit.service.ts
T
thzxx 1d185db6b3 feat: MetonaAI Desktop 初始项目
- Electron + React + TypeScript 架构
- 三栏布局: Sidebar | ChatPanel | DetailPanel
- 9 个内置工具 (文件系统/网络/记忆/命令)
- SQLite 持久化 (better-sqlite3)
- MUI 暗色/亮色主题系统
- Agent Loop ReAct 状态机引擎
- DeepSeek / Agnes AI / Ollama Provider 适配器
- MCP 协议集成
- 系统托盘 + 全局快捷键
- Tailwind CSS v4 + Tailwind Merge
- 修复: Sidebar 缺失 TextField 导入导致黑屏
2026-06-27 21:33:27 +08:00

223 lines
5.4 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.
/**
* Audit Service — 审计日志服务(TOOL 层)
*
* 负责将工具调用、权限检查、错误等审计记录写入 SQLite audit_logs 表。
* 设计原则:
* 1. 所有重要操作必须记录
* 2. 日志不可篡改(INSERT-ONLY
* 3. 支持按时间/类型/会话查询
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 日志分层
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十章
*/
import type Database from 'better-sqlite3';
import log from 'electron-log';
export type AuditEventType = 'tool_call' | 'permission_check' | 'error' | 'llm_request' | 'llm_response' | 'session_start' | 'session_end' | 'config_change';
export type AuditActor = 'agent' | 'user' | 'system';
export type AuditOutcome = 'success' | 'denied' | 'error';
export interface AuditEntry {
sessionId: string;
iteration?: number;
eventType: AuditEventType;
actor: AuditActor;
target: string;
details?: Record<string, unknown>;
outcome?: AuditOutcome;
durationMs?: number;
}
export class AuditService {
constructor(private getDB: () => Database.Database) {}
/**
* 记录审计条目
*/
log(entry: AuditEntry): void {
try {
const db = this.getDB();
db.prepare(`
INSERT INTO audit_logs (session_id, iteration, event_type, actor, target, details, outcome, duration_ms, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
entry.sessionId,
entry.iteration ?? null,
entry.eventType,
entry.actor,
entry.target,
entry.details ? JSON.stringify(entry.details) : null,
entry.outcome ?? null,
entry.durationMs ?? null,
Date.now(),
);
} catch (error) {
// 审计日志写入失败不应影响主流程
log.error('Audit log write failed:', error);
}
}
/**
* 记录工具调用
*/
logToolCall(params: {
sessionId: string;
iteration: number;
toolName: string;
args: Record<string, unknown>;
outcome: AuditOutcome;
result?: unknown;
error?: string;
durationMs: number;
}): void {
this.log({
sessionId: params.sessionId,
iteration: params.iteration,
eventType: 'tool_call',
actor: 'agent',
target: params.toolName,
details: {
args: params.args,
result: typeof params.result === 'string' ? params.result.slice(0, 1000) : params.result,
error: params.error,
},
outcome: params.outcome,
durationMs: params.durationMs,
});
}
/**
* 记录 LLM 请求
*/
logLLMRequest(params: {
sessionId: string;
iteration: number;
provider: string;
model: string;
tokenCount: number;
}): void {
this.log({
sessionId: params.sessionId,
iteration: params.iteration,
eventType: 'llm_request',
actor: 'agent',
target: `${params.provider}/${params.model}`,
details: { tokenCount: params.tokenCount },
});
}
/**
* 记录 LLM 响应
*/
logLLMResponse(params: {
sessionId: string;
iteration: number;
provider: string;
model: string;
tokenUsage: { input: number; output: number; total: number };
durationMs: number;
}): void {
this.log({
sessionId: params.sessionId,
iteration: params.iteration,
eventType: 'llm_response',
actor: 'agent',
target: `${params.provider}/${params.model}`,
details: { tokenUsage: params.tokenUsage },
outcome: 'success',
durationMs: params.durationMs,
});
}
/**
* 记录会话开始
*/
logSessionStart(sessionId: string): void {
this.log({
sessionId,
eventType: 'session_start',
actor: 'user',
target: 'session',
});
}
/**
* 记录会话结束
*/
logSessionEnd(params: {
sessionId: string;
totalIterations: number;
totalTokens: number;
durationMs: number;
terminationReason: string;
}): void {
this.log({
sessionId: params.sessionId,
eventType: 'session_end',
actor: 'agent',
target: 'session',
details: {
totalIterations: params.totalIterations,
totalTokens: params.totalTokens,
terminationReason: params.terminationReason,
},
outcome: 'success',
durationMs: params.durationMs,
});
}
/**
* 查询审计日志
*/
query(filters?: {
sessionId?: string;
eventType?: AuditEventType;
limit?: number;
}): Array<{
id: number;
session_id: string;
iteration: number | null;
event_type: string;
actor: string;
target: string;
details: string | null;
outcome: string | null;
duration_ms: number | null;
created_at: number;
}> {
const db = this.getDB();
let sql = 'SELECT * FROM audit_logs WHERE 1=1';
const params: unknown[] = [];
if (filters?.sessionId) {
sql += ' AND session_id = ?';
params.push(filters.sessionId);
}
if (filters?.eventType) {
sql += ' AND event_type = ?';
params.push(filters.eventType);
}
sql += ' ORDER BY created_at DESC';
if (filters?.limit) {
sql += ' LIMIT ?';
params.push(filters.limit);
}
return db.prepare(sql).all(...params) as Array<{
id: number;
session_id: string;
iteration: number | null;
event_type: string;
actor: string;
target: string;
details: string | null;
outcome: string | null;
duration_ms: number | null;
created_at: number;
}>;
}
}