205 lines
5.1 KiB
TypeScript
205 lines
5.1 KiB
TypeScript
/**
|
||
* Agent Audit Utilities
|
||
* Helper functions for the Agent Audit page
|
||
*/
|
||
|
||
import type { AgentTreeNode, LogItem } from "./types";
|
||
|
||
/**
|
||
* Build tree structure from flat node list
|
||
*/
|
||
export function buildAgentTree(flatNodes: AgentTreeNode[]): AgentTreeNode[] {
|
||
if (!flatNodes || flatNodes.length === 0) return [];
|
||
|
||
// Create node map
|
||
const nodeMap = new Map<string, AgentTreeNode>();
|
||
flatNodes.forEach(node => {
|
||
nodeMap.set(node.agent_id, { ...node, children: [] });
|
||
});
|
||
|
||
// Build tree structure
|
||
const rootNodes: AgentTreeNode[] = [];
|
||
|
||
flatNodes.forEach(node => {
|
||
const currentNode = nodeMap.get(node.agent_id)!;
|
||
|
||
if (node.parent_agent_id && nodeMap.has(node.parent_agent_id)) {
|
||
const parentNode = nodeMap.get(node.parent_agent_id)!;
|
||
parentNode.children.push(currentNode);
|
||
} else {
|
||
rootNodes.push(currentNode);
|
||
}
|
||
});
|
||
|
||
return rootNodes;
|
||
}
|
||
|
||
/**
|
||
* Find agent by ID in tree
|
||
*/
|
||
export function findAgentInTree(nodes: AgentTreeNode[], id: string): AgentTreeNode | null {
|
||
for (const node of nodes) {
|
||
if (node.agent_id === id) return node;
|
||
const found = findAgentInTree(node.children, id);
|
||
if (found) return found;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Find agent name by ID in tree
|
||
*/
|
||
export function findAgentName(nodes: AgentTreeNode[], id: string): string | null {
|
||
const agent = findAgentInTree(nodes, id);
|
||
return agent?.agent_name || null;
|
||
}
|
||
|
||
/**
|
||
* Generate unique log ID
|
||
*/
|
||
let logIdCounter = 0;
|
||
export function generateLogId(): string {
|
||
return `log-${++logIdCounter}`;
|
||
}
|
||
|
||
/**
|
||
* Reset log ID counter (for testing)
|
||
*/
|
||
export function resetLogIdCounter(): void {
|
||
logIdCounter = 0;
|
||
}
|
||
|
||
/**
|
||
* Get current time string for logs
|
||
*/
|
||
export function getTimeString(): string {
|
||
return new Date().toLocaleTimeString('en-US', {
|
||
hour12: false,
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create a log item
|
||
*/
|
||
export function createLogItem(item: Omit<LogItem, 'id' | 'time'>): LogItem {
|
||
return {
|
||
...item,
|
||
id: generateLogId(),
|
||
time: getTimeString(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Clean thinking content (extract only the Thought part, remove Action/Action Input)
|
||
*/
|
||
export function cleanThinkingContent(content: string): string {
|
||
if (!content) return "";
|
||
|
||
let cleaned = content;
|
||
|
||
// 1. 尝试提取 Thought: 后面的内容
|
||
const thoughtMatch = cleaned.match(/Thought:\s*([\s\S]*?)(?=\n\s*Action\s*:|$)/i);
|
||
if (thoughtMatch && thoughtMatch[1]) {
|
||
cleaned = thoughtMatch[1].trim();
|
||
} else {
|
||
// 2. 如果没有 Thought: 前缀,尝试移除 Action 部分
|
||
// 匹配 Action: 及其后面的所有内容(包括开头的 Action)
|
||
cleaned = cleaned.replace(/^Action\s*:[\s\S]*$/i, "");
|
||
cleaned = cleaned.replace(/\n\s*Action\s*:[\s\S]*$/i, "");
|
||
}
|
||
|
||
// 3. 移除可能残留的 Action Input 部分
|
||
cleaned = cleaned.replace(/Action\s*Input\s*:[\s\S]*$/i, "");
|
||
|
||
// 4. 清理空白和特殊字符
|
||
cleaned = cleaned.trim();
|
||
|
||
// 5. 如果清理后只剩下 "Action" 或类似的碎片,返回空
|
||
if (/^Action\s*$/i.test(cleaned) || cleaned.length < 5) {
|
||
return "";
|
||
}
|
||
|
||
return cleaned;
|
||
}
|
||
|
||
/**
|
||
* Truncate output string
|
||
*/
|
||
export function truncateOutput(output: string, maxLength: number = 1000): string {
|
||
if (output.length <= maxLength) return output;
|
||
return output.slice(0, maxLength) + '\n... (truncated)';
|
||
}
|
||
|
||
/**
|
||
* Calculate severity counts from findings
|
||
*/
|
||
export function calculateSeverityCounts(findings: { severity: string }[]): Record<string, number> {
|
||
return {
|
||
critical: findings.filter(f => f.severity === 'critical').length,
|
||
high: findings.filter(f => f.severity === 'high').length,
|
||
medium: findings.filter(f => f.severity === 'medium').length,
|
||
low: findings.filter(f => f.severity === 'low').length,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Check if task is in running state
|
||
*/
|
||
export function isTaskRunning(status: string | undefined): boolean {
|
||
return status === 'running' || status === 'pending';
|
||
}
|
||
|
||
/**
|
||
* Check if task is complete
|
||
*/
|
||
export function isTaskComplete(status: string | undefined): boolean {
|
||
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
||
}
|
||
|
||
/**
|
||
* Format token count
|
||
*/
|
||
export function formatTokens(tokens: number): string {
|
||
return (tokens / 1000).toFixed(1) + 'k';
|
||
}
|
||
|
||
/**
|
||
* Filter logs by agent
|
||
*/
|
||
export function filterLogsByAgent(
|
||
logs: LogItem[],
|
||
selectedAgentId: string | null,
|
||
treeNodes: AgentTreeNode[],
|
||
showAllLogs: boolean
|
||
): LogItem[] {
|
||
if (showAllLogs || !selectedAgentId) {
|
||
return logs;
|
||
}
|
||
|
||
const selectedAgentName = findAgentName(treeNodes, selectedAgentId);
|
||
if (!selectedAgentName) return logs;
|
||
|
||
return logs.filter(log =>
|
||
log.agentName?.toLowerCase() === selectedAgentName.toLowerCase() ||
|
||
log.agentName?.toLowerCase().includes(selectedAgentName.toLowerCase().split('_')[0])
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Debounce function
|
||
*/
|
||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||
func: T,
|
||
wait: number
|
||
): (...args: Parameters<T>) => void {
|
||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
return (...args: Parameters<T>) => {
|
||
if (timeout) clearTimeout(timeout);
|
||
timeout = setTimeout(() => func(...args), wait);
|
||
};
|
||
}
|