feat: 初始化 SSH AI Agent 桌面客户端 v0.1.0

- Electron + React + TypeScript 桌面应用
- SSH 连接管理(ssh2)
- AI Agent ReAct 循环(OpenAI 兼容 API)
- 5 个内置工具(命令执行、文件读写、目录列表、系统信息)
- 6 个模型预设(GPT-4o、DeepSeek、通义千问、豆包、Ollama、LM Studio)
- 自定义模型支持
- 高风险工具审批流
- 无边框窗口 + 自定义标题栏
- AI 对话区右侧布局
- xterm.js 终端仿真
- 配置持久化(electron-store)
This commit is contained in:
Your Name
2026-06-12 18:11:06 +08:00
commit d93b07f0a9
42 changed files with 9649 additions and 0 deletions
+347
View File
@@ -0,0 +1,347 @@
import type { BrowserWindow, IpcMainEvent } from 'electron';
import { ipcMain } from 'electron';
import OpenAI from 'openai';
import { IPC } from '../../shared/types/ipc';
import type { ModelConfig } from '../../shared/types/ai';
import type { EnvironmentInfo, SessionConfig } from '../../shared/types/session';
import { ModelProvider } from '../services/ai/modelProvider';
import { getTool, getToolDefinitions, type ToolContext } from '../services/ai/toolRegistry';
import { modelStore } from '../services/storage/modelStore';
import { sessionStore } from '../services/storage/sessionStore';
import { sshManager } from '../services/ssh/sshManager';
// 系统提示词模板
function buildSystemPrompt(environment?: EnvironmentInfo, customContext?: string): string {
let prompt = `你是一个专业的Linux运维AI助手,运行在SSH AI Agent桌面客户端中。
你可以使用工具在远程服务器上执行命令、读写文件、查看系统状态来帮助用户完成运维任务。
**重要对话规则**
1. **先用自然语言回复用户**:除非用户明确要求执行操作,否则先口头回复
2. **闲聊/打招呼(你好、哈喽、hi 等)必须直接用文字回复**,绝对不要调用任何工具
3. 只有当用户明确要求"执行命令"、"查看"、"读取"、"写入"、"列出"等操作时,才调用相应工具
4. **执行任何危险操作前**,必须先告知用户风险
5. **修改文件前**要自动备份
6. **不要执行 rm -rf / 等极端危险命令**
7. 如果不确定用户意图,先询问确认
8. 命令输出可能被截断,可以用 read_file 查看完整内容
9. 使用 sudo 命令时,用户已预配置 sudo 密码,可以直接使用
**可用工具说明**(仅在用户明确要求操作时使用):
- execute_command: 在远程服务器执行 Shell 命令(需要用户审批,**仅运维任务使用**)
- read_file: 读取文件内容(自动批准)
- write_file: 写入/创建文件(需要用户审批,自动备份)
- list_directory: 列出目录内容(自动批准)
- system_info: 查看系统状态(自动批准)`;
if (environment) {
prompt += `\n\n当前服务器环境信息:
- 操作系统: ${environment.os}
- 主机名: ${environment.hostname || '未知'}
- Docker: ${environment.docker ? `已安装 (${environment.dockerVersion || '未知版本'})` : '未检测到'}
- 运行中的服务: ${environment.services.join(', ') || '无'}
- 内存: ${environment.memoryTotal || '未知'}
- 磁盘: ${environment.diskTotal || '未知'}`;
}
if (customContext) {
prompt += `\n\n用户自定义环境上下文:\n${customContext}`;
}
return prompt;
}
// 会话级别的 AI 对话状态
interface AiConversation {
messages: OpenAI.ChatCompletionMessageParam[];
environment?: EnvironmentInfo;
sessionConfig?: SessionConfig;
interrupted: boolean;
}
const conversations = new Map<string, AiConversation>();
// 构建 ToolContext
function buildToolContext(sessionId: string | null): ToolContext {
return {
sessionId,
execCommand: (sid: string, cmd: string) => sshManager.exec(sid, cmd),
readFile: async (sid: string, path: string) => {
const result = await sshManager.exec(sid, `cat ${path}`);
return result.stdout;
},
writeFile: async (sid: string, path: string, content: string) => {
const b64 = Buffer.from(content).toString('base64');
await sshManager.exec(sid, `echo '${b64}' | base64 -d > ${path}`);
},
listDirectory: async (sid: string, path: string) => {
const result = await sshManager.exec(sid, `ls -la ${path}`);
return result.stdout;
},
};
}
// 构建 OpenAI tools 定义
function buildOpenAITools(): OpenAI.ChatCompletionTool[] {
return getToolDefinitions().map((def) => ({
type: 'function' as const,
function: {
name: def.name,
description: def.description,
parameters: {
type: 'object' as const,
properties: Object.fromEntries(
Object.entries(def.parameters).map(([key, param]) => [
key,
{ type: param.type, description: param.description, enum: param.enum },
])
),
required: Object.entries(def.parameters)
.filter(([, p]) => p.required)
.map(([key]) => key),
},
},
}));
}
export function registerAiHandlers(mainWindow: BrowserWindow) {
// AI 聊天
ipcMain.on(IPC.AI_CHAT, async (event, sessionId: string | null, message: string, modelConfigId: string) => {
const modelConfig = modelStore.list().find((m) => m.id === modelConfigId);
if (!modelConfig) {
mainWindow.webContents.send(IPC.AI_ERROR, { message: '模型配置不存在,请先配置 AI 模型' });
return;
}
const convKey = sessionId || '__global__';
if (!conversations.has(convKey)) {
const sessionConfig = sessionId ? sessionStore.get(sessionId) : undefined;
conversations.set(convKey, {
messages: [],
environment: undefined,
sessionConfig,
interrupted: false,
});
}
const conv = conversations.get(convKey)!;
conv.interrupted = false;
// 添加用户消息
conv.messages.push({ role: 'user', content: message });
// 构建 system prompt
const systemPrompt = buildSystemPrompt(conv.environment, conv.sessionConfig?.customContext);
const allMessages: OpenAI.ChatCompletionMessageParam[] = [
{ role: 'system', content: systemPrompt },
...conv.messages,
];
try {
const provider = new ModelProvider(modelConfig);
const openaiTools = buildOpenAITools();
const ctx = buildToolContext(sessionId);
// ReAct 循环
let maxIterations = 10;
while (maxIterations-- > 0 && !conv.interrupted) {
let iterationContent = '';
let toolCalls: OpenAI.ChatCompletionMessageToolCall[] = [];
for await (const chunk of provider.chat(allMessages, openaiTools)) {
if (conv.interrupted) break;
if (chunk.content) {
iterationContent += chunk.content; // 增量累加成完整文本
mainWindow.webContents.send(IPC.AI_CHAT_STREAM, {
content: chunk.content,
done: false,
});
}
if (chunk.toolCalls) {
toolCalls = chunk.toolCalls;
}
if (chunk.done && !chunk.toolCalls) {
// 纯文本回复,循环结束
mainWindow.webContents.send(IPC.AI_CHAT_STREAM, { done: true });
conv.messages.push({ role: 'assistant', content: iterationContent });
return;
}
}
// 有工具调用需要处理
if (toolCalls.length > 0 && !conv.interrupted) {
console.log(`[AI] 工具调用: ${toolCalls.map(t => t.function.name).join(', ')}`);
// 把本轮 assistant 文本+工具调用一起 push(保留多轮内容)
conv.messages.push({
role: 'assistant',
content: iterationContent || null,
tool_calls: toolCalls,
} as OpenAI.ChatCompletionMessageParam);
allMessages.push({
role: 'assistant',
content: iterationContent || null,
tool_calls: toolCalls,
} as OpenAI.ChatCompletionMessageParam);
// 处理每个工具调用
for (const tc of toolCalls) {
if (conv.interrupted) break;
const tool = getTool(tc.function.name);
if (!tool) {
conv.messages.push({
role: 'tool',
content: `未知工具: ${tc.function.name}`,
tool_call_id: tc.id,
});
allMessages.push({
role: 'tool',
content: `未知工具: ${tc.function.name}`,
tool_call_id: tc.id,
});
continue;
}
// 解析参数
let args: Record<string, unknown>;
try {
args = JSON.parse(tc.function.arguments);
} catch {
args = {};
}
// 高风险工具需要用户审批
if (tool.definition.riskLevel === 'high') {
const desc = tc.function.name === 'execute_command'
? `执行命令: \`${args.command}\``
: tc.function.name === 'write_file'
? `编辑文件: ${args.path}`
: `${tc.function.name}: ${JSON.stringify(args)}`;
// 通知前端展示审批卡片
mainWindow.webContents.send(IPC.AI_TOOL_CALL, {
toolCallId: tc.id,
toolName: tc.function.name,
args,
description: desc,
riskLevel: tool.definition.riskLevel,
});
// 等待用户审批 - 通过 Promise 暂停
const approved = await waitForApproval(tc.id);
if (!approved) {
conv.messages.push({
role: 'tool',
content: '用户取消了此操作',
tool_call_id: tc.id,
});
allMessages.push({
role: 'tool',
content: '用户取消了此操作',
tool_call_id: tc.id,
});
mainWindow.webContents.send(IPC.AI_TOOL_RESULT, {
toolCallId: tc.id,
success: false,
output: '用户取消了此操作',
});
continue;
}
}
// 执行工具
mainWindow.webContents.send(IPC.AI_TOOL_RESULT, {
toolCallId: tc.id,
success: true,
output: '执行中...',
});
const result = await tool.execute(args, ctx);
mainWindow.webContents.send(IPC.AI_TOOL_RESULT, {
toolCallId: tc.id,
success: result.success,
output: result.output,
error: result.error,
});
const toolMessage: OpenAI.ChatCompletionMessageParam = {
role: 'tool',
content: result.output + (result.error ? `\n[错误: ${result.error}]` : ''),
tool_call_id: tc.id,
};
conv.messages.push(toolMessage);
allMessages.push(toolMessage);
}
// 继续循环,让 LLM 处理工具结果
continue;
}
// 没有工具调用,循环结束
mainWindow.webContents.send(IPC.AI_CHAT_STREAM, { done: true });
conv.messages.push({ role: 'assistant', content: iterationContent });
return;
}
// 超出最大迭代次数
mainWindow.webContents.send(IPC.AI_CHAT_STREAM, { done: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'AI 调用失败';
console.error('[AI] 调用失败:', message);
mainWindow.webContents.send(IPC.AI_ERROR, { message });
}
});
// 工具审批
ipcMain.on(IPC.AI_TOOL_ACTION, (_event, toolCallId: string, action: string, editedContent?: string) => {
const pending = pendingApprovals.get(toolCallId);
if (pending) {
if (action === 'cancel') {
pending.resolve(false);
} else {
pending.resolve(true);
}
pendingApprovals.delete(toolCallId);
}
});
// 中断 AI
ipcMain.on(IPC.AI_INTERRUPT, () => {
for (const [, conv] of conversations) {
conv.interrupted = true;
}
// 拒绝所有等待的审批
for (const [, pending] of pendingApprovals) {
pending.resolve(false);
}
pendingApprovals.clear();
mainWindow.webContents.send(IPC.AI_CHAT_STREAM, { done: true });
});
// 环境信息更新(由主进程探测环境后推送到自身会话状态,供 AI 上下文使用)
// 此处保留为占位 - 实际环境信息已在 sessionHandler 中通过 ENV_DETECTED 事件推送
void mainWindow; // keep parameter for API symmetry
}
// ---- 审批等待机制 ----
const pendingApprovals = new Map<string, { resolve: (approved: boolean) => void }>();
function waitForApproval(toolCallId: string): Promise<boolean> {
return new Promise((resolve) => {
pendingApprovals.set(toolCallId, { resolve });
// 5分钟超时自动取消
setTimeout(() => {
if (pendingApprovals.has(toolCallId)) {
pendingApprovals.delete(toolCallId);
resolve(false);
}
}, 5 * 60 * 1000);
});
}
+25
View File
@@ -0,0 +1,25 @@
import { ipcMain } from 'electron';
import { IPC } from '../../shared/types/ipc';
import { modelStore } from '../services/storage/modelStore';
export function registerModelHandlers() {
// 获取模型列表
ipcMain.handle(IPC.MODEL_LIST, () => {
return modelStore.list();
});
// 保存模型配置
ipcMain.handle(IPC.MODEL_SAVE, (_event, config) => {
return modelStore.save(config);
});
// 删除模型配置
ipcMain.handle(IPC.MODEL_DELETE, (_event, id: string) => {
modelStore.delete(id);
});
// 获取预设模板
ipcMain.handle(IPC.MODEL_PRESETS, () => {
return modelStore.getPresets();
});
}
+111
View File
@@ -0,0 +1,111 @@
import { ipcMain, BrowserWindow } from 'electron';
import { IPC } from '../../shared/types/ipc';
import { sessionStore, groupStore } from '../services/storage/sessionStore';
import { sshManager } from '../services/ssh/sshManager';
export function registerSessionHandlers() {
// 获取会话列表
ipcMain.handle(IPC.SESSION_LIST, () => sessionStore.list());
// 创建会话
ipcMain.handle(IPC.SESSION_CREATE, (_event, config) => sessionStore.create(config));
// 更新会话
ipcMain.handle(IPC.SESSION_UPDATE, (_event, id: string, updates) =>
sessionStore.update(id, updates)
);
// 删除会话
ipcMain.handle(IPC.SESSION_DELETE, (_event, id: string) => {
sshManager.disconnect(id);
sessionStore.delete(id);
});
// 连接SSH
ipcMain.handle(IPC.SESSION_CONNECT, async (event, sessionId: string) => {
console.log(`[SSH] ====== 收到连接请求: sessionId=${sessionId} ======`);
const config = sessionStore.get(sessionId);
if (!config) {
console.error(`[SSH] 会话配置不存在: ${sessionId}`);
throw new Error('会话配置不存在');
}
console.log(`[SSH] 配置: ${config.username}@${config.host}:${config.port}, authType=${config.authType}, password=${config.password ? '已设置' : '空'}, privateKey=${config.privateKey ? '已设置' : '空'}`);
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) {
console.error('[SSH] 找不到 BrowserWindow');
}
// 认证方式与凭据校验
if (config.authType === 'password' && !config.password) {
console.warn('[SSH] 密码为空,提示用户设置');
win?.webContents.send('session:status', sessionId, 'error');
throw new Error('未设置密码: 请右键会话 → 编辑 输入密码');
}
if (config.authType === 'privateKey' && !config.privateKey) {
console.warn('[SSH] 私钥为空,提示用户设置');
win?.webContents.send('session:status', sessionId, 'error');
throw new Error('未设置私钥: 请右键会话 → 编辑 输入私钥内容');
}
try {
console.log(`[SSH] 发送 connecting 状态...`);
win?.webContents.send('session:status', sessionId, 'connecting');
console.log(`[SSH] 调用 sshManager.connect()...`);
const session = await sshManager.connect(config);
console.log(`[SSH] ✓ 连接成功: ${config.username}@${config.host}:${config.port}`);
// 创建 Shell 并设置数据流
const shell = await sshManager.createShell(sessionId);
if (!shell) {
console.error('[SSH] createShell 返回 null');
throw new Error('创建 Shell 失败');
}
console.log(`[SSH] Shell 创建成功,绑定数据流...`);
if (win) {
shell.on('data', (data: Buffer) => {
win.webContents.send(IPC.TERMINAL_OUTPUT, sessionId, data.toString('utf-8'));
});
shell.on('close', () => {
console.log(`[SSH] Shell closed: ${sessionId}`);
win.webContents.send(IPC.TERMINAL_DISCONNECTED, sessionId, 'Shell closed');
win.webContents.send('session:status', sessionId, 'disconnected');
});
shell.stderr?.on('data', (data: Buffer) => {
win.webContents.send(IPC.TERMINAL_OUTPUT, sessionId, data.toString('utf-8'));
});
}
console.log(`[SSH] 发送 connected 状态`);
win?.webContents.send(IPC.TERMINAL_CONNECTED, sessionId);
win?.webContents.send('session:status', sessionId, 'connected');
// 异步探测环境
sshManager.detectEnvironment(sessionId).then((env) => {
win?.webContents.send(IPC.ENV_DETECTED, sessionId, env);
}).catch(() => {
// 环境探测失败不影响连接
});
return { success: true };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '连接失败';
console.error(`[SSH] ✗ 连接失败 ${config.username}@${config.host}:${config.port}:`, message, err);
win?.webContents.send('session:status', sessionId, 'error');
throw new Error(message);
}
});
// 断开SSH
ipcMain.handle(IPC.SESSION_DISCONNECT, (_event, sessionId: string) => {
sshManager.disconnect(sessionId);
});
// ---- 分组管理 ----
ipcMain.handle(IPC.GROUP_LIST, () => groupStore.list());
ipcMain.handle(IPC.GROUP_CREATE, (_event, config) => groupStore.create(config));
ipcMain.handle(IPC.GROUP_UPDATE, (_event, id: string, updates) =>
groupStore.update(id, updates)
);
ipcMain.handle(IPC.GROUP_DELETE, (_event, id: string) => groupStore.delete(id));
}
+15
View File
@@ -0,0 +1,15 @@
import { ipcMain } from 'electron';
import { IPC } from '../../shared/types/ipc';
import { sshManager } from '../services/ssh/sshManager';
export function registerTerminalHandlers() {
// 终端输入
ipcMain.on(IPC.TERMINAL_INPUT, (_event, sessionId: string, data: string) => {
sshManager.writeShell(sessionId, data);
});
// 终端尺寸调整
ipcMain.on(IPC.TERMINAL_RESIZE, (_event, sessionId: string, cols: number, rows: number) => {
sshManager.resizeShell(sessionId, cols, rows);
});
}
+93
View File
@@ -0,0 +1,93 @@
import { app, BrowserWindow, ipcMain, Menu } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import { registerSessionHandlers } from './handlers/sessionHandler';
import { registerTerminalHandlers } from './handlers/terminalHandler';
import { registerModelHandlers } from './handlers/modelHandler';
import { registerAiHandlers } from './handlers/aiHandler';
import { sshManager } from './services/ssh/sshManager';
let mainWindow: BrowserWindow | null = null;
// ============== 禁用应用菜单 ==============
function buildMenu() {
// 设置为空菜单,隐藏顶部菜单栏
Menu.setApplicationMenu(null);
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 900,
minHeight: 600,
title: '智能SSH终端',
backgroundColor: '#0a0a0f',
frame: false, // 无边框窗口,去掉顶部系统标题栏
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
// 开发环境加载 dev server,生产环境加载打包文件
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools({ mode: 'detach' });
} else {
const rendererPath = path.join(__dirname, '..', '..', 'renderer', 'index.html');
const fallbackPath = path.join(__dirname, '../../renderer/index.html');
const htmlPath = fs.existsSync(rendererPath) ? rendererPath : fallbackPath;
mainWindow.loadFile(htmlPath);
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
buildMenu();
createWindow();
// 注册窗口控制
ipcMain.on('window:close', () => mainWindow?.close());
ipcMain.on('window:minimize', () => mainWindow?.minimize());
ipcMain.on('window:maximize', () => {
if (mainWindow?.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow?.maximize();
}
});
// 注册 IPC handlers
registerSessionHandlers();
registerTerminalHandlers();
registerModelHandlers();
registerAiHandlers(mainWindow!);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
// 清理所有 SSH 连接
sshManager.disconnectAll();
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
sshManager.disconnectAll();
});
export function getMainWindow(): BrowserWindow | null {
return mainWindow;
}
+126
View File
@@ -0,0 +1,126 @@
// IPC 通道常量(内联在 preload 中,避免在 asar 内相对 require 失败)
const IPC = {
SESSION_LIST: 'session:list',
SESSION_CREATE: 'session:create',
SESSION_UPDATE: 'session:update',
SESSION_DELETE: 'session:delete',
SESSION_CONNECT: 'session:connect',
SESSION_DISCONNECT: 'session:disconnect',
GROUP_LIST: 'group:list',
GROUP_CREATE: 'group:create',
GROUP_UPDATE: 'group:update',
GROUP_DELETE: 'group:delete',
TERMINAL_INPUT: 'terminal:input',
TERMINAL_RESIZE: 'terminal:resize',
TERMINAL_OUTPUT: 'terminal:output',
TERMINAL_CONNECTED: 'terminal:connected',
TERMINAL_DISCONNECTED: 'terminal:disconnected',
AI_CHAT: 'ai:chat',
AI_CHAT_STREAM: 'ai:chat:stream',
AI_TOOL_ACTION: 'ai:tool_action',
AI_TOOL_CALL: 'ai:tool_call',
AI_TOOL_RESULT: 'ai:tool_result',
AI_INTERRUPT: 'ai:interrupt',
AI_ERROR: 'ai:error',
MODEL_LIST: 'model:list',
MODEL_SAVE: 'model:save',
MODEL_DELETE: 'model:delete',
MODEL_PRESETS: 'model:presets',
ENV_DETECTED: 'env:detected',
} as const;
import { contextBridge, ipcRenderer } from 'electron';
const electronAPI = {
// ---- 窗口控制 ----
windowClose: () => ipcRenderer.send('window:close'),
windowMinimize: () => ipcRenderer.send('window:minimize'),
windowMaximize: () => ipcRenderer.send('window:maximize'),
// ---- 会话管理 (invoke) ----
sessionList: () => ipcRenderer.invoke(IPC.SESSION_LIST),
sessionCreate: (config: unknown) => ipcRenderer.invoke(IPC.SESSION_CREATE, config),
sessionUpdate: (id: string, updates: unknown) => ipcRenderer.invoke(IPC.SESSION_UPDATE, id, updates),
sessionDelete: (id: string) => ipcRenderer.invoke(IPC.SESSION_DELETE, id),
sessionConnect: (sessionId: string) => ipcRenderer.invoke(IPC.SESSION_CONNECT, sessionId),
sessionDisconnect: (sessionId: string) => ipcRenderer.invoke(IPC.SESSION_DISCONNECT, sessionId),
// ---- 分组管理 (invoke) ----
groupList: () => ipcRenderer.invoke(IPC.GROUP_LIST),
groupCreate: (config: unknown) => ipcRenderer.invoke(IPC.GROUP_CREATE, config),
groupUpdate: (id: string, updates: unknown) => ipcRenderer.invoke(IPC.GROUP_UPDATE, id, updates),
groupDelete: (id: string) => ipcRenderer.invoke(IPC.GROUP_DELETE, id),
// ---- 终端 (send) ----
terminalInput: (sessionId: string, data: string) =>
ipcRenderer.send(IPC.TERMINAL_INPUT, sessionId, data),
terminalResize: (sessionId: string, cols: number, rows: number) =>
ipcRenderer.send(IPC.TERMINAL_RESIZE, sessionId, cols, rows),
// ---- AI (send) ----
aiChat: (sessionId: string | null, message: string, modelConfigId: string) =>
ipcRenderer.send(IPC.AI_CHAT, sessionId, message, modelConfigId),
aiToolAction: (toolCallId: string, action: string, editedContent?: string) =>
ipcRenderer.send(IPC.AI_TOOL_ACTION, toolCallId, action, editedContent),
aiInterrupt: () => ipcRenderer.send(IPC.AI_INTERRUPT),
// ---- 模型配置 (invoke) ----
modelList: () => ipcRenderer.invoke(IPC.MODEL_LIST),
modelSave: (config: unknown) => ipcRenderer.invoke(IPC.MODEL_SAVE, config),
modelDelete: (id: string) => ipcRenderer.invoke(IPC.MODEL_DELETE, id),
modelPresets: () => ipcRenderer.invoke(IPC.MODEL_PRESETS),
// ---- 事件监听 (on) ----
onTerminalOutput: (callback: (sessionId: string, data: string) => void) => {
const handler = (_event: Electron.IpcRendererEvent, sessionId: string, data: string) => callback(sessionId, data);
ipcRenderer.on(IPC.TERMINAL_OUTPUT, handler);
return () => ipcRenderer.removeListener(IPC.TERMINAL_OUTPUT, handler);
},
onTerminalConnected: (callback: (sessionId: string) => void) => {
const handler = (_event: Electron.IpcRendererEvent, sessionId: string) => callback(sessionId);
ipcRenderer.on(IPC.TERMINAL_CONNECTED, handler);
return () => ipcRenderer.removeListener(IPC.TERMINAL_CONNECTED, handler);
},
onTerminalDisconnected: (callback: (sessionId: string, reason?: string) => void) => {
const handler = (_event: Electron.IpcRendererEvent, sessionId: string, reason?: string) => callback(sessionId, reason);
ipcRenderer.on(IPC.TERMINAL_DISCONNECTED, handler);
return () => ipcRenderer.removeListener(IPC.TERMINAL_DISCONNECTED, handler);
},
onSessionStatus: (callback: (sessionId: string, status: string) => void) => {
const handler = (_event: Electron.IpcRendererEvent, sessionId: string, status: string) => callback(sessionId, status);
ipcRenderer.on('session:status', handler);
return () => ipcRenderer.removeListener('session:status', handler);
},
onAiChatStream: (callback: (chunk: { content?: string; done: boolean }) => void) => {
const handler = (_event: Electron.IpcRendererEvent, chunk: { content?: string; done: boolean }) => callback(chunk);
ipcRenderer.on(IPC.AI_CHAT_STREAM, handler);
return () => ipcRenderer.removeListener(IPC.AI_CHAT_STREAM, handler);
},
onAiToolCall: (callback: (toolCall: unknown) => void) => {
const handler = (_event: Electron.IpcRendererEvent, toolCall: unknown) => callback(toolCall);
ipcRenderer.on(IPC.AI_TOOL_CALL, handler);
return () => ipcRenderer.removeListener(IPC.AI_TOOL_CALL, handler);
},
onAiToolResult: (callback: (result: unknown) => void) => {
const handler = (_event: Electron.IpcRendererEvent, result: unknown) => callback(result);
ipcRenderer.on(IPC.AI_TOOL_RESULT, handler);
return () => ipcRenderer.removeListener(IPC.AI_TOOL_RESULT, handler);
},
onAiError: (callback: (error: { message: string }) => void) => {
const handler = (_event: Electron.IpcRendererEvent, error: { message: string }) => callback(error);
ipcRenderer.on(IPC.AI_ERROR, handler);
return () => ipcRenderer.removeListener(IPC.AI_ERROR, handler);
},
onEnvDetected: (callback: (sessionId: string, environment: unknown) => void) => {
const handler = (_event: Electron.IpcRendererEvent, sessionId: string, environment: unknown) => callback(sessionId, environment);
ipcRenderer.on(IPC.ENV_DETECTED, handler);
return () => ipcRenderer.removeListener(IPC.ENV_DETECTED, handler);
},
// 移除所有监听
removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel),
};
export type ElectronAPI = typeof electronAPI;
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
+100
View File
@@ -0,0 +1,100 @@
import OpenAI from 'openai';
import type { ModelConfig } from '../../../shared/types/ai';
// 所有模型调用统一走 OpenAI 兼容接口
// 用户只需配置 endpoint + model + apiKey 即可接入任意兼容 API
export class ModelProvider {
private client: OpenAI;
private model: string;
private maxTokens: number;
private temperature: number;
constructor(config: ModelConfig) {
this.client = new OpenAI({
apiKey: config.apiKey || 'dummy-key',
baseURL: config.endpoint,
defaultHeaders: config.customHeaders,
});
this.model = config.model;
this.maxTokens = config.maxTokens || 4096;
this.temperature = config.temperature ?? 0.7;
}
async *chat(
messages: OpenAI.ChatCompletionMessageParam[],
tools?: OpenAI.ChatCompletionTool[]
): AsyncGenerator<{
content?: string;
toolCalls?: OpenAI.ChatCompletionMessageToolCall[];
done: boolean;
}> {
const params: OpenAI.ChatCompletionCreateParamsStreaming = {
model: this.model,
messages,
max_tokens: this.maxTokens,
temperature: this.temperature,
stream: true,
};
if (tools && tools.length > 0) {
params.tools = tools;
}
const stream = await this.client.chat.completions.create(params);
let content = '';
let toolCalls: OpenAI.ChatCompletionMessageToolCall[] = [];
let currentToolCall: Partial<OpenAI.ChatCompletionMessageToolCall> | null = null;
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (!delta) continue;
// 文本内容 - yield 增量,前端 store 用 append 累加成完整内容
if (delta.content) {
content += delta.content;
yield { content: delta.content, done: false };
}
// 工具调用
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
if (tc.index !== undefined && tc.function) {
if (!toolCalls[tc.index]) {
toolCalls[tc.index] = {
id: tc.id || '',
type: 'function',
function: {
name: tc.function.name || '',
arguments: tc.function.arguments || '',
},
};
currentToolCall = toolCalls[tc.index];
} else {
if (tc.function?.name) {
toolCalls[tc.index].function.name += tc.function.name;
}
if (tc.function?.arguments) {
toolCalls[tc.index].function.arguments += tc.function.arguments;
}
}
}
}
}
// 流结束(finish_reason = tool_calls 表示要进入工具调用循环)
if (chunk.choices[0]?.finish_reason === 'tool_calls') {
yield { content: undefined, toolCalls, done: false };
return;
}
}
// 流正常结束(无工具调用)
if (toolCalls.length > 0) {
yield { content: undefined, toolCalls, done: false };
} else {
yield { content: undefined, done: true };
}
}
}
+204
View File
@@ -0,0 +1,204 @@
import type { ToolDefinition, ToolRiskLevel } from '../../../shared/types/ai';
export interface Tool {
definition: ToolDefinition;
execute: (args: Record<string, unknown>, context: ToolContext) => Promise<ToolResult>;
}
export interface ToolContext {
sessionId: string | null;
execCommand: (sessionId: string, command: string) => Promise<{ stdout: string; stderr: string; exitCode: number | null }>;
readFile: (sessionId: string, path: string) => Promise<string>;
writeFile: (sessionId: string, path: string, content: string) => Promise<void>;
listDirectory: (sessionId: string, path: string) => Promise<string>;
}
export interface ToolResult {
success: boolean;
output: string;
error?: string;
}
// ---- 工具注册中心 ----
const tools = new Map<string, Tool>();
export function registerTool(tool: Tool) {
tools.set(tool.definition.name, tool);
}
export function getTool(name: string): Tool | undefined {
return tools.get(name);
}
export function getAllTools(): Tool[] {
return Array.from(tools.values());
}
export function getToolDefinitions(): ToolDefinition[] {
return getAllTools().map((t) => t.definition);
}
// ---- 注册内置工具 ----
// 执行命令工具
registerTool({
definition: {
name: 'execute_command',
description: '在远程服务器上执行 Shell 命令。支持 sudo 命令,需要预先配置 sudo 密码。',
parameters: {
command: { type: 'string', description: '要执行的 Shell 命令', required: true },
},
riskLevel: 'high',
},
execute: async (args, ctx) => {
const command = args.command as string;
if (!ctx.sessionId) {
return { success: false, output: '', error: '没有活跃的 SSH 会话' };
}
try {
const result = await ctx.execCommand(ctx.sessionId, command);
let output = result.stdout;
if (result.stderr) {
output += `\n[stderr]\n${result.stderr}`;
}
if (result.exitCode !== 0 && result.exitCode !== null) {
output += `\n[exit code: ${result.exitCode}]`;
}
return { success: result.exitCode === 0, output: output.substring(0, 8000), error: result.exitCode !== 0 ? `Exit code: ${result.exitCode}` : undefined };
} catch (err: unknown) {
return { success: false, output: '', error: err instanceof Error ? err.message : '执行失败' };
}
},
});
// 读取文件工具
registerTool({
definition: {
name: 'read_file',
description: '读取远程服务器上的文件内容。支持指定行范围。',
parameters: {
path: { type: 'string', description: '文件路径', required: true },
start_line: { type: 'number', description: '起始行号(可选)' },
end_line: { type: 'number', description: '结束行号(可选)' },
},
riskLevel: 'low',
},
execute: async (args, ctx) => {
const filePath = args.path as string;
const startLine = args.start_line as number | undefined;
const endLine = args.end_line as number | undefined;
if (!ctx.sessionId) {
return { success: false, output: '', error: '没有活跃的 SSH 会话' };
}
try {
let cmd = `cat ${filePath}`;
if (startLine && endLine) {
cmd = `sed -n '${startLine},${endLine}p' ${filePath}`;
} else if (startLine) {
cmd = `sed -n '${startLine},$p' ${filePath}`;
} else if (endLine) {
cmd = `sed -n '1,${endLine}p' ${filePath}`;
}
const result = await ctx.execCommand(ctx.sessionId, cmd);
return { success: result.exitCode === 0, output: result.stdout.substring(0, 8000), error: result.exitCode !== 0 ? result.stderr : undefined };
} catch (err: unknown) {
return { success: false, output: '', error: err instanceof Error ? err.message : '读取失败' };
}
},
});
// 写入文件工具
registerTool({
definition: {
name: 'write_file',
description: '写入或创建远程服务器上的文件。会自动备份原文件。',
parameters: {
path: { type: 'string', description: '文件路径', required: true },
content: { type: 'string', description: '文件内容', required: true },
backup: { type: 'boolean', description: '是否备份原文件(默认 true' },
},
riskLevel: 'high',
},
execute: async (args, ctx) => {
const filePath = args.path as string;
const content = args.content as string;
const backup = args.backup !== false;
if (!ctx.sessionId) {
return { success: false, output: '', error: '没有活跃的 SSH 会话' };
}
try {
// 备份原文件
if (backup) {
await ctx.execCommand(ctx.sessionId, `cp ${filePath} ${filePath}.bak.$(date +%s) 2>/dev/null || true`);
}
// 写入文件(使用 base64 编码避免特殊字符问题)
const b64 = Buffer.from(content).toString('base64');
const result = await ctx.execCommand(ctx.sessionId, `echo '${b64}' | base64 -d > ${filePath}`);
return { success: result.exitCode === 0, output: `文件已写入: ${filePath}`, error: result.exitCode !== 0 ? result.stderr : undefined };
} catch (err: unknown) {
return { success: false, output: '', error: err instanceof Error ? err.message : '写入失败' };
}
},
});
// 列出目录工具
registerTool({
definition: {
name: 'list_directory',
description: '列出远程服务器上目录的内容。',
parameters: {
path: { type: 'string', description: '目录路径', required: true },
recursive: { type: 'boolean', description: '是否递归列出(默认 false' },
},
riskLevel: 'low',
},
execute: async (args, ctx) => {
const dirPath = args.path as string;
const recursive = args.recursive as boolean;
if (!ctx.sessionId) {
return { success: false, output: '', error: '没有活跃的 SSH 会话' };
}
try {
const cmd = recursive
? `find ${dirPath} -maxdepth 3 -type f -o -type d | head -200`
: `ls -la ${dirPath}`;
const result = await ctx.execCommand(ctx.sessionId, cmd);
return { success: result.exitCode === 0, output: result.stdout.substring(0, 8000), error: result.exitCode !== 0 ? result.stderr : undefined };
} catch (err: unknown) {
return { success: false, output: '', error: err instanceof Error ? err.message : '列出目录失败' };
}
},
});
// 系统信息工具
registerTool({
definition: {
name: 'system_info',
description: '获取远程服务器的系统状态信息。',
parameters: {
type: { type: 'string', description: '信息类型: cpu, memory, disk, network, processes', required: true, enum: ['cpu', 'memory', 'disk', 'network', 'processes'] },
},
riskLevel: 'low',
},
execute: async (args, ctx) => {
const infoType = args.type as string;
if (!ctx.sessionId) {
return { success: false, output: '', error: '没有活跃的 SSH 会话' };
}
const commands: Record<string, string> = {
cpu: 'top -bn1 | head -5',
memory: 'free -h',
disk: 'df -h',
network: 'ss -tulnp | head -30',
processes: 'ps aux --sort=-%mem | head -20',
};
try {
const cmd = commands[infoType] || commands.cpu;
const result = await ctx.execCommand(ctx.sessionId, cmd);
return { success: result.exitCode === 0, output: result.stdout, error: result.exitCode !== 0 ? result.stderr : undefined };
} catch (err: unknown) {
return { success: false, output: '', error: err instanceof Error ? err.message : '获取系统信息失败' };
}
},
});
+214
View File
@@ -0,0 +1,214 @@
import { Client, ClientChannel, type ConnectConfig } from 'ssh2';
import type { SessionConfig, EnvironmentInfo } from '../../../shared/types/session';
export interface SshSession {
id: string;
config: SessionConfig;
client: Client;
shell?: ClientChannel;
connected: boolean;
}
class SshManager {
private sessions = new Map<string, SshSession>();
async connect(config: SessionConfig): Promise<SshSession> {
// 如果已存在,先断开
if (this.sessions.has(config.id)) {
this.disconnect(config.id);
}
const client = new Client();
const session: SshSession = {
id: config.id,
config,
client,
connected: false,
};
return new Promise((resolve, reject) => {
const connectConfig: ConnectConfig = {
host: config.host,
port: config.port,
username: config.username,
readyTimeout: 15000,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
};
if (config.authType === 'password' && config.password) {
connectConfig.password = config.password;
} else if (config.authType === 'privateKey' && config.privateKey) {
connectConfig.privateKey = config.privateKey;
}
client.on('ready', () => {
session.connected = true;
this.sessions.set(config.id, session);
resolve(session);
});
client.on('error', (err) => {
session.connected = false;
reject(new Error(`SSH连接失败: ${err.message}`));
});
client.on('close', () => {
session.connected = false;
session.shell = undefined;
});
client.on('end', () => {
session.connected = false;
});
client.connect(connectConfig);
});
}
async createShell(sessionId: string): Promise<ClientChannel | null> {
const session = this.sessions.get(sessionId);
if (!session || !session.connected) return null;
return new Promise((resolve) => {
session.client.shell(
{
term: 'xterm-256color',
cols: 80,
rows: 24,
},
(err, stream) => {
if (err) {
console.error(`[SSH] shell() 回调出错: ${err.message}`);
resolve(null);
return;
}
session.shell = stream;
resolve(stream);
},
);
});
}
writeShell(sessionId: string, data: string): boolean {
const session = this.sessions.get(sessionId);
if (!session?.shell) return false;
session.shell.write(data);
return true;
}
resizeShell(sessionId: string, cols: number, rows: number): boolean {
const session = this.sessions.get(sessionId);
if (!session?.shell) return false;
session.shell.setWindow(rows, cols, rows * 16, cols * 8);
return true;
}
disconnect(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
if (session.shell) {
session.shell.close();
}
if (session.client) {
session.client.end();
}
session.connected = false;
this.sessions.delete(sessionId);
}
disconnectAll(): void {
for (const sessionId of this.sessions.keys()) {
this.disconnect(sessionId);
}
}
getSession(sessionId: string): SshSession | undefined {
return this.sessions.get(sessionId);
}
isConnected(sessionId: string): boolean {
return this.sessions.get(sessionId)?.connected ?? false;
}
// 在远程服务器执行命令(非交互式)
async exec(sessionId: string, command: string): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
const session = this.sessions.get(sessionId);
if (!session?.connected) {
throw new Error('SSH会话未连接');
}
return new Promise((resolve, reject) => {
session.client.exec(command, (err, stream) => {
if (err) {
reject(new Error(`执行命令失败: ${err.message}`));
return;
}
let stdout = '';
let stderr = '';
let exitCode: number | null = null;
stream.on('data', (data: Buffer) => {
stdout += data.toString();
});
stream.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
stream.on('close', (code: number | null) => {
exitCode = code;
resolve({ stdout, stderr, exitCode });
});
stream.on('error', (err: Error) => {
reject(new Error(`命令流错误: ${err.message}`));
});
});
});
}
// 检测服务器环境
async detectEnvironment(sessionId: string): Promise<EnvironmentInfo> {
const commands = {
os: 'cat /etc/os-release 2>/dev/null | head -5',
hostname: 'hostname 2>/dev/null',
docker: 'docker --version 2>/dev/null',
services: 'systemctl list-units --type=service --state=running 2>/dev/null | grep -oP "\\S+\\.service" | head -20',
dirs: 'ls -la ~ 2>/dev/null | tail -20',
cpu: 'nproc 2>/dev/null',
memory: 'free -h 2>/dev/null | head -2',
disk: 'df -h / 2>/dev/null | tail -1',
};
const results: Record<string, string> = {};
for (const [key, cmd] of Object.entries(commands)) {
try {
const { stdout } = await this.exec(sessionId, cmd);
results[key] = stdout.trim();
} catch {
results[key] = '';
}
}
// 解析结果
const osMatch = results.os?.match(/PRETTY_NAME="(.+?)"/);
const dockerVersion = results.docker?.replace('Docker version ', '').trim();
const env: EnvironmentInfo = {
os: osMatch?.[1] || 'Unknown Linux',
hostname: results.hostname || undefined,
docker: !!results.docker,
dockerVersion: dockerVersion || undefined,
services: results.services ? results.services.split('\n').filter(Boolean) : [],
commonDirs: [],
detectedAt: Date.now(),
};
return env;
}
}
export const sshManager = new SshManager();
+96
View File
@@ -0,0 +1,96 @@
import Store from 'electron-store';
import type { ModelConfig, ModelPreset } from '../../../shared/types/ai';
const store = new Store();
const MODELS_KEY = 'modelConfigs';
// 预设模型模板
export const MODEL_PRESETS: ModelPreset[] = [
{
id: 'preset-openai-gpt4o',
name: 'GPT-4o',
provider: 'openai',
endpoint: 'https://api.openai.com/v1',
model: 'gpt-4o',
maxTokens: 4096,
},
{
id: 'preset-deepseek-chat',
name: 'DeepSeek Chat',
provider: 'custom',
endpoint: 'https://api.deepseek.com/v1',
model: 'deepseek-chat',
maxTokens: 4096,
},
{
id: 'preset-qwen',
name: '通义千问',
provider: 'custom',
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: 'qwen-plus',
maxTokens: 4096,
},
{
id: 'preset-doubao',
name: '豆包',
provider: 'custom',
endpoint: 'https://ark.cn-beijing.volces.com/api/v3',
model: 'doubao-pro-32k',
maxTokens: 4096,
},
{
id: 'preset-ollama',
name: 'Ollama (本地)',
provider: 'custom',
endpoint: 'http://localhost:11434/v1',
model: 'qwen2.5:14b',
maxTokens: 4096,
},
{
id: 'preset-lmstudio',
name: 'LM Studio (本地)',
provider: 'custom',
endpoint: 'http://localhost:1234/v1',
model: 'local-model',
maxTokens: 4096,
},
];
export const modelStore = {
list(): ModelConfig[] {
return store.get(MODELS_KEY, []) as ModelConfig[];
},
save(config: Omit<ModelConfig, 'id' | 'createdAt' | 'updatedAt'>): ModelConfig {
const models = this.list();
const now = Date.now();
const newConfig: ModelConfig = {
...config,
id: `model_${now}_${Math.random().toString(36).substring(2, 6)}`,
createdAt: now,
updatedAt: now,
};
models.push(newConfig);
store.set(MODELS_KEY, models);
return newConfig;
},
update(id: string, updates: Partial<ModelConfig>): ModelConfig | undefined {
const models = this.list();
const idx = models.findIndex((m) => m.id === id);
if (idx === -1) return undefined;
models[idx] = { ...models[idx], ...updates, updatedAt: Date.now() };
store.set(MODELS_KEY, models);
return models[idx];
},
delete(id: string): void {
const models = this.list().filter((m) => m.id !== id);
store.set(MODELS_KEY, models);
},
getPresets(): ModelPreset[] {
return MODEL_PRESETS;
},
};
+110
View File
@@ -0,0 +1,110 @@
import Store from 'electron-store';
import type { SessionConfig, SessionGroup } from '../../../shared/types/session';
const store = new Store();
const SESSIONS_KEY = 'sessions';
const GROUPS_KEY = 'sessionGroups';
// 兼容旧数据:旧会话没有 groupId 时补 null
function migrateSessions(raw: unknown): SessionConfig[] {
if (!Array.isArray(raw)) return [];
return raw.map((s: SessionConfig) => ({
...s,
groupId: s.groupId === undefined ? null : s.groupId,
}));
}
export const sessionStore = {
list(): SessionConfig[] {
return migrateSessions(store.get(SESSIONS_KEY, []));
},
get(id: string): SessionConfig | undefined {
return this.list().find((s) => s.id === id);
},
create(config: Omit<SessionConfig, 'id' | 'createdAt' | 'updatedAt' | 'order'>): SessionConfig {
const sessions = this.list();
const now = Date.now();
const session: SessionConfig = {
...config,
groupId: config.groupId ?? null,
id: `session_${now}_${Math.random().toString(36).substring(2, 8)}`,
order: sessions.length,
createdAt: now,
updatedAt: now,
};
sessions.push(session);
store.set(SESSIONS_KEY, sessions);
return session;
},
update(id: string, updates: Partial<SessionConfig>): SessionConfig | undefined {
const sessions = this.list();
const idx = sessions.findIndex((s) => s.id === id);
if (idx === -1) return undefined;
sessions[idx] = { ...sessions[idx], ...updates, updatedAt: Date.now() };
store.set(SESSIONS_KEY, sessions);
return sessions[idx];
},
delete(id: string): void {
const sessions = this.list().filter((s) => s.id !== id);
store.set(SESSIONS_KEY, sessions);
},
};
export const groupStore = {
list(): SessionGroup[] {
const raw = store.get(GROUPS_KEY, []) as SessionGroup[];
return Array.isArray(raw) ? raw : [];
},
get(id: string): SessionGroup | undefined {
return this.list().find((g) => g.id === id);
},
create(config: Omit<SessionGroup, 'id' | 'createdAt' | 'updatedAt' | 'order' | 'collapsed'>): SessionGroup {
const groups = this.list();
const now = Date.now();
const group: SessionGroup = {
...config,
id: `group_${now}_${Math.random().toString(36).substring(2, 8)}`,
order: groups.length,
collapsed: false,
createdAt: now,
updatedAt: now,
};
groups.push(group);
store.set(GROUPS_KEY, groups);
return group;
},
update(id: string, updates: Partial<SessionGroup>): SessionGroup | undefined {
const groups = this.list();
const idx = groups.findIndex((g) => g.id === id);
if (idx === -1) return undefined;
groups[idx] = { ...groups[idx], ...updates, updatedAt: Date.now() };
store.set(GROUPS_KEY, groups);
return groups[idx];
},
/**
* 删除分组:递归删除所有子分组及其中会话
*/
delete(id: string): void {
const groups = this.list();
const sessions = sessionStore.list();
const toDelete = new Set<string>();
const collect = (gid: string) => {
toDelete.add(gid);
groups.filter((g) => g.parentId === gid).forEach((g) => collect(g.id));
};
collect(id);
const remainingGroups = groups.filter((g) => !toDelete.has(g.id));
const remainingSessions = sessions.filter((s) => !s.groupId || !toDelete.has(s.groupId));
store.set(GROUPS_KEY, remainingGroups);
store.set(SESSIONS_KEY, remainingSessions);
},
};
+72
View File
@@ -0,0 +1,72 @@
// ============================================
// AI Types
// ============================================
export type ModelProvider = 'openai' | 'custom';
export interface ModelConfig {
id: string;
name: string;
provider: ModelProvider;
endpoint: string;
model: string;
apiKey: string;
maxTokens?: number;
temperature?: number;
customHeaders?: Record<string, string>;
enabled: boolean;
isPreset?: boolean;
createdAt: number;
updatedAt: number;
}
export interface ChatMessage {
id: string;
role: 'system' | 'user' | 'assistant' | 'tool';
content: string;
toolCalls?: ToolCall[];
toolCallId?: string;
timestamp: number;
}
export interface ToolCall {
id: string;
name: string;
arguments: Record<string, unknown>;
result?: ToolResult;
status: 'pending' | 'approved' | 'executing' | 'completed' | 'cancelled' | 'error';
}
export interface ToolResult {
success: boolean;
output: string;
error?: string;
}
export type ToolRiskLevel = 'low' | 'high';
export interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, ToolParameter>;
riskLevel: ToolRiskLevel;
}
export interface ToolParameter {
type: 'string' | 'number' | 'boolean';
description: string;
required?: boolean;
enum?: string[];
}
export type CommandAction = 'cancel' | 'copy' | 'terminal_exec' | 'ai_exec';
export type FileEditAction = 'cancel' | 'manual_edit' | 'allow';
export interface ModelPreset {
id: string;
name: string;
provider: ModelProvider;
endpoint: string;
model: string;
maxTokens?: number;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './session';
export * from './ai';
export * from './ipc';
+91
View File
@@ -0,0 +1,91 @@
// ============================================
// IPC Channel Types (Electron Main <-> Renderer)
// ============================================
import type { SessionConfig, SessionGroup, EnvironmentInfo, SessionStatus } from './session';
import type { ModelConfig, ChatMessage, ToolCall, ModelPreset } from './ai';
// ---- IPC 通道名 ----
export const IPC = {
// 会话管理
SESSION_LIST: 'session:list',
SESSION_CREATE: 'session:create',
SESSION_UPDATE: 'session:update',
SESSION_DELETE: 'session:delete',
SESSION_CONNECT: 'session:connect',
SESSION_DISCONNECT: 'session:disconnect',
// 分组管理
GROUP_LIST: 'group:list',
GROUP_CREATE: 'group:create',
GROUP_UPDATE: 'group:update',
GROUP_DELETE: 'group:delete',
// 终端
TERMINAL_INPUT: 'terminal:input',
TERMINAL_RESIZE: 'terminal:resize',
TERMINAL_OUTPUT: 'terminal:output',
TERMINAL_CONNECTED: 'terminal:connected',
TERMINAL_DISCONNECTED: 'terminal:disconnected',
// AI
AI_CHAT: 'ai:chat',
AI_CHAT_STREAM: 'ai:chat:stream',
AI_TOOL_ACTION: 'ai:tool_action',
AI_TOOL_CALL: 'ai:tool_call',
AI_TOOL_RESULT: 'ai:tool_result',
AI_INTERRUPT: 'ai:interrupt',
AI_ERROR: 'ai:error',
// 模型配置
MODEL_LIST: 'model:list',
MODEL_SAVE: 'model:save',
MODEL_DELETE: 'model:delete',
MODEL_PRESETS: 'model:presets',
// 环境
ENV_DETECTED: 'env:detected',
} as const;
// ---- IPC 通信类型 ----
export interface IpcInvoke {
'session:list': () => SessionConfig[];
'session:create': (config: Omit<SessionConfig, 'id' | 'createdAt' | 'updatedAt' | 'order'>) => SessionConfig;
'session:update': (id: string, updates: Partial<SessionConfig>) => SessionConfig;
'session:delete': (id: string) => void;
'session:connect': (sessionId: string) => void;
'session:disconnect': (sessionId: string) => void;
'group:list': () => SessionGroup[];
'group:create': (config: Omit<SessionGroup, 'id' | 'createdAt' | 'updatedAt' | 'order' | 'collapsed'>) => SessionGroup;
'group:update': (id: string, updates: Partial<SessionGroup>) => SessionGroup;
'group:delete': (id: string) => void;
'model:list': () => ModelConfig[];
'model:save': (config: Omit<ModelConfig, 'id' | 'createdAt' | 'updatedAt'>) => ModelConfig;
'model:delete': (id: string) => void;
'model:presets': () => ModelPreset[];
}
export interface IpcSend {
'terminal:input': (sessionId: string, data: string) => void;
'terminal:resize': (sessionId: string, cols: number, rows: number) => void;
'ai:chat': (sessionId: string | null, message: string, modelConfigId: string) => void;
'ai:tool_action': (toolCallId: string, action: string, editedContent?: string) => void;
'ai:interrupt': () => void;
}
export interface IpcOn {
'terminal:output': (sessionId: string, data: string) => void;
'terminal:connected': (sessionId: string) => void;
'terminal:disconnected': (sessionId: string, reason?: string) => void;
'ai:chat:stream': (chunk: { content?: string; done: boolean }) => void;
'ai:tool_call': (toolCall: { toolCallId: string; toolName: string; args: Record<string, unknown>; description: string; riskLevel: 'low' | 'high' }) => void;
'ai:tool_result': (result: { toolCallId: string; success: boolean; output: string; error?: string }) => void;
'ai:error': (error: { message: string }) => void;
'env:detected': (sessionId: string, environment: EnvironmentInfo) => void;
'session:status': (sessionId: string, status: SessionStatus) => void;
}
export * from './session';
export * from './ai';
+48
View File
@@ -0,0 +1,48 @@
// ============================================
// Session Types
// ============================================
export type AuthType = 'password' | 'privateKey';
export interface SessionGroup {
id: string;
name: string;
parentId: string | null;
order: number;
collapsed: boolean;
createdAt: number;
updatedAt: number;
}
export interface SessionConfig {
id: string;
name: string;
host: string;
port: number;
username: string;
authType: AuthType;
password?: string;
privateKey?: string;
sudoPassword?: string;
customContext?: string;
groupId: string | null;
order: number;
createdAt: number;
updatedAt: number;
}
export type SessionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
export interface EnvironmentInfo {
os: string;
osVersion?: string;
hostname?: string;
docker: boolean;
dockerVersion?: string;
services: string[];
commonDirs: string[];
cpuInfo?: string;
memoryTotal?: string;
diskTotal?: string;
detectedAt: number;
}