MEMORY.md 保护: 新建 file-guard.ts 共享守卫模块; read_file/write_file/search_files/run_command 禁止访问根目录 MEMORY.md; permissions.ts 增加 deniedPatterns 深度防御。格式简化: 元数据从 # 注释改为 > 引用语法; 校验规则从 6 条简化为 3 条; context-builder extractContent 适配。预存问题: 修复路径遍历前缀碰撞漏洞; 移除 browser_extract 内容截断; SearXNG 配置实时读取; web_search/web_fetch 移除截断; 侧边栏动态获取工具列表; 版本号 0.1.1
612 lines
21 KiB
TypeScript
612 lines
21 KiB
TypeScript
/**
|
|
* IPC Handlers — 所有 IPC 通道处理逻辑
|
|
*
|
|
* 集成三层日志:
|
|
* - TOOL 层:AuditService 记录工具调用到 audit_logs 表
|
|
* - TRACE 层:SessionRecorder 写入 session_*.jsonl
|
|
* - AGENT 层:electron-log 记录状态转换
|
|
*
|
|
* @see docs/MetonaAI-Desktop 架构与交互设计.html — IPC 架构 + 日志分层
|
|
*/
|
|
|
|
import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron';
|
|
import type { SessionService } from '../services/session.service';
|
|
import type { ConfigService } from '../services/config.service';
|
|
import type { WorkspaceService } from '../services/workspace.service';
|
|
import type { ContextBuilder } from '../harness/prompts/context-builder';
|
|
import type { AgentLoopEngine } from '../harness/agent-loop';
|
|
import type { ToolRegistry } from '../harness/tools/registry';
|
|
import type { AuditService } from '../services/audit.service';
|
|
import type { SessionRecorder } from '../services/session-recorder.service';
|
|
import type { MemoryManager } from '../harness/memory/manager';
|
|
import type { MCPManager } from '../services/mcp-manager.service';
|
|
import type { PromptInjectionDefender } from '../harness/security/prompt-injection-defense';
|
|
import type { OutputValidator } from '../harness/verification/output-validator';
|
|
import type { MetonaMessage, MetonaStreamEvent } from '../harness/types';
|
|
import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types';
|
|
import type { MetonaError } from '../harness/types';
|
|
import log from 'electron-log';
|
|
|
|
/**
|
|
* 注册所有 IPC 处理器
|
|
*/
|
|
export function registerAllIPCHandlers(
|
|
mainWindow: BrowserWindow,
|
|
sessionService: SessionService,
|
|
configService: ConfigService,
|
|
workspaceService: WorkspaceService,
|
|
contextBuilder: ContextBuilder,
|
|
agentLoop: AgentLoopEngine,
|
|
toolRegistry: ToolRegistry,
|
|
auditService: AuditService,
|
|
sessionRecorder: SessionRecorder,
|
|
memoryManager: MemoryManager,
|
|
mcpManager: MCPManager,
|
|
reloadAdapter: () => void,
|
|
promptInjectionDefender: PromptInjectionDefender,
|
|
outputValidator: OutputValidator,
|
|
): void {
|
|
|
|
// ===== Agent 交互 =====
|
|
|
|
ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => {
|
|
log.info('[AGENT] sendMessage:', sessionId, userMessage.content.slice(0, 80));
|
|
|
|
// TRACE 层:开始录制
|
|
sessionRecorder.startRecording(sessionId);
|
|
|
|
// TOOL 层:记录会话开始
|
|
auditService.logSessionStart(sessionId);
|
|
|
|
// 保存用户消息到数据库
|
|
// IPC boundary: frontend sends attachments not defined in the MetonaMessage type
|
|
sessionService.saveMessage({
|
|
sessionId,
|
|
role: 'user',
|
|
content: userMessage.content,
|
|
attachments: (userMessage as MetonaMessage & { attachments?: unknown[] }).attachments,
|
|
});
|
|
|
|
// 加载历史消息
|
|
const historyRows = sessionService.getMessages(sessionId);
|
|
const history: MetonaMessage[] = historyRows
|
|
.filter((m) => m.role !== 'system')
|
|
.slice(0, -1)
|
|
.map((m) => ({
|
|
role: m.role as MetonaMessage['role'],
|
|
content: m.content,
|
|
reasoningContent: m.reasoningContent,
|
|
toolCalls: m.toolCalls as MetonaMessage['toolCalls'],
|
|
toolResult: m.toolResult as MetonaMessage['toolResult'],
|
|
timestamp: m.timestamp,
|
|
}));
|
|
|
|
// 从工作空间文件构建 System Prompt
|
|
const workspaceFiles = workspaceService.getFiles();
|
|
const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles);
|
|
|
|
// 监听 Agent Loop 事件
|
|
const onStreamEvent = (event: MetonaStreamEvent) => {
|
|
if (!mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('agent:streamEvent', event);
|
|
}
|
|
};
|
|
|
|
const onStateChange = (data: { previous?: string; current?: string; sessionId?: string; iteration?: number; state?: string }) => {
|
|
// AGENT 层:记录状态转换
|
|
if (data.previous) log.info(`[AGENT] State: ${data.previous} → ${data.current}`);
|
|
// 转发到渲染进程(携带迭代号)
|
|
if (!mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('agent:stateChange', data);
|
|
}
|
|
};
|
|
|
|
agentLoop.on('streamEvent', onStreamEvent);
|
|
agentLoop.on('stateChange', onStateChange);
|
|
|
|
try {
|
|
// 提示注入检测(安全模块)
|
|
const injectionResult = promptInjectionDefender.detect(userMessage.content);
|
|
if (injectionResult.riskScore >= 7) {
|
|
log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
|
|
if (!mainWindow.isDestroyed()) {
|
|
const metonaError: MetonaError = {
|
|
code: MetonaErrorCode.UNKNOWN,
|
|
message: `Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
|
|
retryable: false,
|
|
};
|
|
const errorEvent: MetonaStreamEvent = {
|
|
type: MetonaStreamEventType.ERROR,
|
|
requestId: '',
|
|
sessionId,
|
|
iteration: 0,
|
|
seq: 0,
|
|
timestamp: Date.now(),
|
|
error: metonaError,
|
|
};
|
|
mainWindow.webContents.send('agent:streamEvent', errorEvent);
|
|
}
|
|
// TRACE 层:停止录制
|
|
sessionRecorder.stopRecording({
|
|
totalIterations: 0,
|
|
totalTokens: 0,
|
|
durationMs: 0,
|
|
terminationReason: 'error',
|
|
});
|
|
return { success: false, error: 'Message blocked by prompt injection defense' };
|
|
}
|
|
if (injectionResult.riskScore >= 4) {
|
|
log.warn('[PromptInjectionDefender] Suspicious patterns detected:', injectionResult.findings);
|
|
}
|
|
|
|
// TRACE 层:记录上下文构建
|
|
sessionRecorder.recordContextBuilt({
|
|
tokenCount: history.reduce((sum, m) => sum + Math.ceil(m.content.length / 2), 0),
|
|
usageRatio: 0,
|
|
});
|
|
|
|
// 启动 Agent Loop
|
|
const output = await agentLoop.runStream(userMessage, sessionId, history, systemPrompt);
|
|
|
|
// 输出验证(不阻塞响应,仅记录警告)
|
|
try {
|
|
const validation = await outputValidator.validate(output.finalAnswer);
|
|
if (!validation.valid || validation.issues.length > 0) {
|
|
log.warn('[OutputValidator] Validation issues:', validation.issues);
|
|
}
|
|
log.debug(`[OutputValidator] Score: ${validation.score}, Valid: ${validation.valid}`);
|
|
} catch (err) {
|
|
log.error('[OutputValidator] Validation failed:', err);
|
|
}
|
|
|
|
// 保存每轮迭代的 assistant 消息到数据库(含思考内容和工具调用)
|
|
for (const step of output.iterations) {
|
|
if (!step.thought) continue;
|
|
|
|
// 将 MetonaToolCall + MetonaToolResult 转换为前端 ToolCallInfo 格式
|
|
const toolCallsWithResults = step.toolCalls?.map((tc) => {
|
|
const result = step.toolResults?.find((r) => r.toolCallId === tc.id);
|
|
return {
|
|
id: tc.id,
|
|
name: tc.name,
|
|
args: tc.args,
|
|
status: result?.success ? 'success' as const : 'error' as const,
|
|
result: result?.result,
|
|
durationMs: result?.durationMs,
|
|
error: result?.error,
|
|
};
|
|
});
|
|
|
|
// 只有当有内容、思考内容或工具调用时才保存
|
|
if (step.thought.content || step.thought.reasoningContent || toolCallsWithResults?.length) {
|
|
sessionService.saveMessage({
|
|
sessionId,
|
|
role: 'assistant',
|
|
content: step.thought.content,
|
|
reasoningContent: step.thought.reasoningContent || undefined,
|
|
toolCalls: toolCallsWithResults,
|
|
iteration: step.iteration,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 更新 Token 统计
|
|
if (output.totalTokenUsage.totalTokens > 0) {
|
|
sessionService.updateTokenUsage(sessionId, output.totalTokenUsage.totalTokens);
|
|
}
|
|
|
|
// 更新 MEMORY.md 时间戳
|
|
workspaceService.updateMemoryTimestamp();
|
|
|
|
// TOOL 层:记录会话结束
|
|
auditService.logSessionEnd({
|
|
sessionId,
|
|
totalIterations: output.iterations.length,
|
|
totalTokens: output.totalTokenUsage.totalTokens,
|
|
durationMs: output.durationMs,
|
|
terminationReason: output.terminationReason,
|
|
});
|
|
|
|
// TRACE 层:停止录制
|
|
sessionRecorder.stopRecording({
|
|
totalIterations: output.iterations.length,
|
|
totalTokens: output.totalTokenUsage.totalTokens,
|
|
durationMs: output.durationMs,
|
|
terminationReason: output.terminationReason,
|
|
});
|
|
|
|
log.info(`[AGENT] Completed: ${output.terminationReason}, ${output.iterations.length} iterations, ${output.durationMs}ms`);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
log.error('[AGENT] Error:', error);
|
|
|
|
// TOOL 层:记录错误
|
|
auditService.log({
|
|
sessionId,
|
|
eventType: 'error',
|
|
actor: 'agent',
|
|
target: 'agent_loop',
|
|
details: { error: (error as Error).message },
|
|
outcome: 'error',
|
|
});
|
|
|
|
// TRACE 层:停止录制
|
|
sessionRecorder.stopRecording({
|
|
totalIterations: 0,
|
|
totalTokens: 0,
|
|
durationMs: 0,
|
|
terminationReason: 'error',
|
|
});
|
|
|
|
// 发送错误事件到 UI
|
|
if (!mainWindow.isDestroyed()) {
|
|
const metonaError: MetonaError = {
|
|
code: MetonaErrorCode.UNKNOWN,
|
|
message: (error as Error).message,
|
|
retryable: false,
|
|
};
|
|
const errorEvent: MetonaStreamEvent = {
|
|
type: MetonaStreamEventType.ERROR,
|
|
requestId: '',
|
|
sessionId,
|
|
iteration: 0,
|
|
seq: 0,
|
|
timestamp: Date.now(),
|
|
error: metonaError,
|
|
};
|
|
mainWindow.webContents.send('agent:streamEvent', errorEvent);
|
|
}
|
|
|
|
return { success: false, error: (error as Error).message };
|
|
} finally {
|
|
agentLoop.off('streamEvent', onStreamEvent);
|
|
agentLoop.off('stateChange', onStateChange);
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('agent:abortSession', async (_event, sessionId) => {
|
|
log.info('[AGENT] Abort:', sessionId);
|
|
agentLoop.abort();
|
|
|
|
// TOOL 层:记录中断
|
|
auditService.log({
|
|
sessionId,
|
|
eventType: 'session_end',
|
|
actor: 'user',
|
|
target: 'session',
|
|
details: { reason: 'user_abort' },
|
|
outcome: 'denied',
|
|
});
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
// ===== 会话管理 =====
|
|
|
|
ipcMain.handle('sessions:list', async () => {
|
|
return sessionService.list();
|
|
});
|
|
|
|
ipcMain.handle('sessions:create', async (_event, title?: string) => {
|
|
return sessionService.create(title);
|
|
});
|
|
|
|
ipcMain.handle('sessions:rename', async (_event, sessionId, title) => {
|
|
return { success: sessionService.rename(sessionId, title) };
|
|
});
|
|
|
|
ipcMain.handle('sessions:delete', async (_event, sessionId) => {
|
|
return { success: sessionService.delete(sessionId) };
|
|
});
|
|
|
|
ipcMain.handle('sessions:getMessages', async (_event, sessionId) => {
|
|
return sessionService.getMessages(sessionId);
|
|
});
|
|
|
|
ipcMain.handle('sessions:pin', async (_event, sessionId, pinned) => {
|
|
return { success: sessionService.pin(sessionId, pinned) };
|
|
});
|
|
|
|
ipcMain.handle('sessions:archive', async (_event, sessionId, archived) => {
|
|
return { success: sessionService.archive(sessionId, archived) };
|
|
});
|
|
|
|
ipcMain.handle('sessions:deleteMessage', async (_event, messageId) => {
|
|
return { success: sessionService.deleteMessage(messageId) };
|
|
});
|
|
|
|
ipcMain.handle('sessions:clearMessages', async (_event, sessionId) => {
|
|
return { success: sessionService.clearMessages(sessionId) };
|
|
});
|
|
|
|
ipcMain.handle('sessions:saveTrace', async (_event, sessionId, data) => {
|
|
try {
|
|
sessionService.saveTraceData(sessionId, data);
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('sessions:getTrace', async (_event, sessionId) => {
|
|
return sessionService.getTraceData(sessionId);
|
|
});
|
|
|
|
// ===== MCP 管理 =====
|
|
|
|
ipcMain.handle('mcp:listServers', async () => {
|
|
return mcpManager.getServerStates();
|
|
});
|
|
|
|
ipcMain.handle('mcp:addServer', async (_event, config: { name: string; transport: string; command?: string; args?: string[]; url?: string }) => {
|
|
try {
|
|
await mcpManager.addServer({
|
|
name: config.name,
|
|
transport: config.transport as 'stdio' | 'sse',
|
|
command: config.command,
|
|
args: config.args,
|
|
url: config.url,
|
|
enabled: true,
|
|
});
|
|
log.info(`MCP server added: ${config.name}`);
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error instanceof Error ? error.message : String(error)) };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('mcp:removeServer', async (_event, name: string) => {
|
|
try {
|
|
await mcpManager.removeServer(name);
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error instanceof Error ? error.message : String(error)) };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => {
|
|
try {
|
|
await mcpManager.toggleServer(name, enabled);
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error instanceof Error ? error.message : String(error)) };
|
|
}
|
|
});
|
|
|
|
// ===== 记忆 =====
|
|
|
|
ipcMain.handle('db:searchMemories', async (_event, query, options) => {
|
|
return memoryManager.search(query, options);
|
|
});
|
|
|
|
// ===== 配置 =====
|
|
|
|
ipcMain.handle('config:get', async (_event, key) => {
|
|
return configService.get(key);
|
|
});
|
|
|
|
ipcMain.handle('config:set', async (_event, key, value) => {
|
|
configService.set(key, value);
|
|
|
|
// TOOL 层:记录配置变更
|
|
auditService.log({
|
|
sessionId: '',
|
|
eventType: 'config_change',
|
|
actor: 'user',
|
|
target: key,
|
|
details: { value },
|
|
outcome: 'success',
|
|
});
|
|
|
|
// LLM 相关配置变更时热重载 Adapter
|
|
if (['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx'].includes(key)) {
|
|
reloadAdapter();
|
|
log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`);
|
|
}
|
|
|
|
// Agent 配置变更时热更新 Engine
|
|
if (key === 'agent.maxIterations') {
|
|
agentLoop.updateConfig({ maxIterations: value as number });
|
|
log.info(`[CONFIG] Agent maxIterations updated to ${value}`);
|
|
} else if (key === 'agent.totalTimeoutMs') {
|
|
agentLoop.updateConfig({ totalTimeoutMs: value as number });
|
|
log.info(`[CONFIG] Agent totalTimeoutMs updated to ${value}`);
|
|
} else if (key === 'agent.enableThinking') {
|
|
agentLoop.updateConfig({ thinkingEnabled: value as boolean });
|
|
log.info(`[CONFIG] Agent thinkingEnabled updated to ${value}`);
|
|
} else if (key === 'agent.thinkingEffort') {
|
|
agentLoop.updateConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
|
|
log.info(`[CONFIG] Agent thinkingEffort updated to ${value}`);
|
|
} else if (key === 'ollama.numCtx') {
|
|
// numCtx 同时更新 Engine 的 contextLength
|
|
agentLoop.updateConfig({ contextLength: (value as number) || undefined });
|
|
log.info(`[CONFIG] Agent contextLength updated to ${value}`);
|
|
}
|
|
|
|
// 日志级别变更时即时应用
|
|
if (key === 'logging.level') {
|
|
const level = value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
|
|
log.transports.file.level = level;
|
|
log.transports.console.level = level;
|
|
log.info(`[CONFIG] Log level updated to ${level}`);
|
|
}
|
|
|
|
// 工作空间路径变更时写入独立文件(下次启动生效)
|
|
if (key === 'workspace.path') {
|
|
try {
|
|
const { writeWorkspacePathToFile } = await import('../main');
|
|
if (value) writeWorkspacePathToFile(value as string);
|
|
log.info(`[CONFIG] Workspace path saved (restart required): ${value}`);
|
|
} catch (err) {
|
|
log.error('[CONFIG] Failed to save workspace path:', err);
|
|
}
|
|
}
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
// ===== 应用工具 =====
|
|
|
|
ipcMain.handle('app:getVersion', async () => {
|
|
return app.getVersion();
|
|
});
|
|
|
|
ipcMain.handle('app:getAppDataPath', async () => {
|
|
return app.getPath('userData');
|
|
});
|
|
|
|
ipcMain.handle('app:openExternal', async (_event, url) => {
|
|
await shell.openExternal(url);
|
|
});
|
|
|
|
ipcMain.handle('app:showItemInFolder', async (_event, path) => {
|
|
shell.showItemInFolder(path);
|
|
});
|
|
|
|
ipcMain.handle('app:selectFolder', async (_event, defaultPath?: string) => {
|
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
properties: ['openDirectory', 'createDirectory'],
|
|
defaultPath: defaultPath ?? app.getPath('home'),
|
|
title: '选择工作空间目录',
|
|
});
|
|
if (result.canceled || result.filePaths.length === 0) return { canceled: true, path: '' };
|
|
return { canceled: false, path: result.filePaths[0] };
|
|
});
|
|
|
|
// ===== 工具管理 =====
|
|
|
|
ipcMain.handle('tools:list', async () => {
|
|
return toolRegistry.listAllTools().map((t) => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
category: t.category,
|
|
riskLevel: t.riskLevel,
|
|
requiresPermission: t.requiresPermission,
|
|
enabled: t.enabled,
|
|
}));
|
|
});
|
|
|
|
ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => {
|
|
// 工具开关通过配置持久化
|
|
configService.set(`tools.${toolName}.enabled`, enabled);
|
|
// 同步到 ToolRegistry(立即生效)
|
|
toolRegistry.setToolEnabled(toolName, enabled);
|
|
// 同步到 AgentLoop 的工具列表
|
|
agentLoop.setTools(toolRegistry.listTools());
|
|
log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`);
|
|
return { success: true };
|
|
});
|
|
|
|
// ===== 数据管理 =====
|
|
|
|
ipcMain.handle('data:export', async (_event, sessionId?: string) => {
|
|
try {
|
|
const db = sessionService.getDB();
|
|
if (sessionId) {
|
|
// 导出单个会话
|
|
const messages = sessionService.getMessages(sessionId);
|
|
return { success: true, data: messages };
|
|
}
|
|
// 导出所有会话
|
|
const sessions = sessionService.list();
|
|
const allData: Record<string, unknown> = { sessions: [], config: configService.getAll() };
|
|
for (const session of sessions) {
|
|
(allData.sessions as Array<Record<string, unknown>>).push({
|
|
...session,
|
|
messages: sessionService.getMessages(session.id),
|
|
});
|
|
}
|
|
return { success: true, data: allData };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('data:clearSessions', async () => {
|
|
try {
|
|
const db = sessionService.getDB();
|
|
db.exec('BEGIN');
|
|
db.exec('DELETE FROM messages');
|
|
db.exec('DELETE FROM sessions');
|
|
db.exec('COMMIT');
|
|
log.info('[DATA] All sessions cleared');
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('data:clearMemories', async () => {
|
|
try {
|
|
const db = sessionService.getDB();
|
|
db.exec('DELETE FROM episodic_memories');
|
|
db.exec('DELETE FROM semantic_memories');
|
|
db.exec('DELETE FROM working_memories');
|
|
log.info('[DATA] All memories cleared');
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('data:clearAuditLogs', async () => {
|
|
try {
|
|
// 审计日志是 INSERT-ONLY,需要先禁用触发器
|
|
const db = sessionService.getDB();
|
|
db.exec('BEGIN');
|
|
db.exec('DROP TRIGGER IF EXISTS audit_no_delete');
|
|
db.exec('DELETE FROM audit_logs');
|
|
db.exec(`
|
|
CREATE TRIGGER audit_no_delete BEFORE DELETE ON audit_logs
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY. Deletion is not allowed.');
|
|
END
|
|
`);
|
|
db.exec('COMMIT');
|
|
log.info('[DATA] Audit logs cleared');
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
// ===== SearXNG =====
|
|
|
|
ipcMain.handle('searxng:testConnection', async (_event, url: string, authKey: string, authType: string) => {
|
|
try {
|
|
if (!url || !/^https?:\/\//.test(url)) {
|
|
return { success: false, error: 'URL 需以 http:// 或 https:// 开头' };
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
const headers: Record<string, string> = {};
|
|
|
|
if (authKey) {
|
|
if (authType === 'bearer') {
|
|
headers['Authorization'] = `Bearer ${authKey}`;
|
|
} else if (authType === 'basic') {
|
|
headers['Authorization'] = `Basic ${Buffer.from(authKey).toString('base64')}`;
|
|
}
|
|
}
|
|
|
|
const testUrl = `${url.replace(/\/$/, '')}/search?q=test&format=json&pageno=1`;
|
|
const response = await fetch(testUrl, {
|
|
headers,
|
|
signal: AbortSignal.timeout(10_000),
|
|
});
|
|
|
|
const latencyMs = Date.now() - startTime;
|
|
|
|
if (response.ok) {
|
|
return { success: true, statusCode: response.status, latencyMs };
|
|
}
|
|
return { success: false, statusCode: response.status, latencyMs, error: `HTTP ${response.status} ${response.statusText}` };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
log.info('[SYS] All IPC handlers registered');
|
|
}
|