桌面应用与多会话管理:让AI真正"住"进你的电脑
------CogitoAgent开发实战(第11篇)
📖 本文是专栏的第十一篇。前面十篇文章,我们一步步构建了一个完整的AI智能体------它能思考、能动手、能联网、能记忆、能执行代码。但它一直"住"在终端里,一个黑底白字的命令行窗口。这一篇,我们给AI盖一栋"房子"------一个真正的桌面应用,让它从"字符界面"走进"图形世界"。同时,我们还会给AI装上一个"档案室"------多会话管理系统,让它能同时处理多个独立的任务线索。

📌 从一个对比开始
先想象两个场景:
场景A:终端模式
你在命令行里启动AI,看着彩色文字一行行滚动。你和AI的对话像一条没有尽头的时间线------昨天聊的、今天聊的、全都混在一起。想切到另一个话题?得手动清空历史,或者忍受AI用旧话题的背景来回答新问题。
场景B:桌面模式
你打开一个毛玻璃质感的悬浮窗,AI的虚拟形象在窗口里静静等待。你点开"会话列表",看到三个独立的对话线索:"项目A设计"、"代码审查"、"日常记录"。点一下,就能切换到对应的对话上下文。每个会话都有自己的"记忆",互不干扰。
这就是CogitoAgent v2.3 带来的改变------从"终端里的聊天机器人"进化为"桌面上的智能体伴侣"。

一、为什么需要桌面应用?
1.1 终端的局限性
命令行界面的优势是轻量、高效、适合开发者。但它的局限性也很明显:
| 问题 | 说明 |
|---|---|
| 视觉单调 | 只有文字,没有图形界面,无法展示复杂信息 |
| 交互单一 | 只能输入文本,无法点击、拖拽、直观操作 |
| 缺少陪伴感 | AI只是一个"程序",不是一个"伙伴" |
| 会话混乱 | 所有对话挤在同一条时间线上,难以组织 |
1.2 桌面应用的目标
CogitoAgent的桌面模式不是简单的"网页包装",而是围绕三个核心目标设计的:
- 让AI可视:通过虚拟人物形象、界面布局、视觉反馈,让AI"活"起来
- 让交互更自然:支持点击、拖拽、多窗口,降低使用门槛
- 让会话可管理:引入多会话系统,让对话像文档一样可组织、可切换
二、整体架构:两条腿走路
CogitoAgent 的桌面方案采用 Electron + WebSocket + 独立Agent进程 的架构,而不是把Agent直接嵌入Electron主进程。
2.1 为什么是独立进程?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 嵌入Electron | 通信简单 | Agent崩溃会拖垮整个界面,难以独立重启 |
| 独立进程 | 隔离性好,可独立重启 | 需要进程间通信(IPC/WebSocket) |
CogitoAgent选择了独立进程方案:
#mermaid-svg-cOBZxlzBakGJ5gLC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cOBZxlzBakGJ5gLC .error-icon{fill:#552222;}#mermaid-svg-cOBZxlzBakGJ5gLC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cOBZxlzBakGJ5gLC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cOBZxlzBakGJ5gLC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cOBZxlzBakGJ5gLC .marker.cross{stroke:#333333;}#mermaid-svg-cOBZxlzBakGJ5gLC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cOBZxlzBakGJ5gLC p{margin:0;}#mermaid-svg-cOBZxlzBakGJ5gLC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC .cluster-label text{fill:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC .cluster-label span{color:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC .cluster-label span p{background-color:transparent;}#mermaid-svg-cOBZxlzBakGJ5gLC .label text,#mermaid-svg-cOBZxlzBakGJ5gLC span{fill:#333;color:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC .node rect,#mermaid-svg-cOBZxlzBakGJ5gLC .node circle,#mermaid-svg-cOBZxlzBakGJ5gLC .node ellipse,#mermaid-svg-cOBZxlzBakGJ5gLC .node polygon,#mermaid-svg-cOBZxlzBakGJ5gLC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cOBZxlzBakGJ5gLC .rough-node .label text,#mermaid-svg-cOBZxlzBakGJ5gLC .node .label text,#mermaid-svg-cOBZxlzBakGJ5gLC .image-shape .label,#mermaid-svg-cOBZxlzBakGJ5gLC .icon-shape .label{text-anchor:middle;}#mermaid-svg-cOBZxlzBakGJ5gLC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cOBZxlzBakGJ5gLC .rough-node .label,#mermaid-svg-cOBZxlzBakGJ5gLC .node .label,#mermaid-svg-cOBZxlzBakGJ5gLC .image-shape .label,#mermaid-svg-cOBZxlzBakGJ5gLC .icon-shape .label{text-align:center;}#mermaid-svg-cOBZxlzBakGJ5gLC .node.clickable{cursor:pointer;}#mermaid-svg-cOBZxlzBakGJ5gLC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cOBZxlzBakGJ5gLC .arrowheadPath{fill:#333333;}#mermaid-svg-cOBZxlzBakGJ5gLC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cOBZxlzBakGJ5gLC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cOBZxlzBakGJ5gLC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cOBZxlzBakGJ5gLC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cOBZxlzBakGJ5gLC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cOBZxlzBakGJ5gLC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cOBZxlzBakGJ5gLC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cOBZxlzBakGJ5gLC .cluster text{fill:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC .cluster span{color:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cOBZxlzBakGJ5gLC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cOBZxlzBakGJ5gLC rect.text{fill:none;stroke-width:0;}#mermaid-svg-cOBZxlzBakGJ5gLC .icon-shape,#mermaid-svg-cOBZxlzBakGJ5gLC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cOBZxlzBakGJ5gLC .icon-shape p,#mermaid-svg-cOBZxlzBakGJ5gLC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cOBZxlzBakGJ5gLC .icon-shape .label rect,#mermaid-svg-cOBZxlzBakGJ5gLC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cOBZxlzBakGJ5gLC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cOBZxlzBakGJ5gLC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cOBZxlzBakGJ5gLC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Agent 独立进程
Electron 桌面应用
WebSocket
spawn 启动
main.js
主进程
preload.cjs
安全桥接
UI 渲染进程
桌面/仪表盘/配置向导
agent-bridge.js
WebSocket 桥接
ws-server.js
WebSocket 服务
(端口 9527)
agent/Agent.js
思考循环 + 工具执行
session.js
会话管理
2.2 进程分工
| 进程 | 职责 |
|---|---|
| Electron主进程 | 窗口管理、系统托盘、启动Agent子进程、路由模式切换 |
| Agent独立进程 | 思考循环、工具执行、会话管理、WebSocket服务 |
| UI渲染进程 | 界面展示、用户交互、消息渲染 |
关键设计:Agent进程不依赖Electron,即使桌面窗口关闭,Agent也可以继续在后台运行(当然,当前版本设计为窗口关闭时Agent也随之退出)。
三、启动流程解析
3.1 入口:main.js
当用户执行 npm run electron:desktop 或 npm run electron:dashboard 时,package.json 脚本会调用 Electron,加载 electron/main.js。
javascript
// main.js 的核心启动流程
app.whenReady().then(async () => {
// 1. 检查配置
if (!isConfigured()) {
createSetupWindow(); // 打开配置向导
} else {
launchMainApp(); // 直接启动主应用
}
});
async function launchMainApp() {
// 2. 清理端口(防止旧进程占用)
killPortProcess(9527);
// 3. 启动Agent子进程
await startAgentProcess();
// 4. 读取启动模式
const mode = envConfig['COGITO_MODE'] || 'desktop';
// 5. 创建对应模式的窗口
if (mode === 'dashboard') {
createDashboardWindow();
} else {
createMainWindow();
}
}
3.2 关键细节:Agent启动等待
startAgentProcess 函数会等待Agent输出 WebSocket 服务已启动 后才返回,确保WebSocket服务就绪后再创建窗口:
javascript
function startAgentProcess() {
agentProcess = spawn('node', ['src/index.js'], { ... });
return new Promise((resolve) => {
agentProcess.stdout.on('data', (data) => {
if (data.toString().includes('WebSocket 服务已启动')) {
resolve(); // 收到这个日志才继续
}
});
});
}
3.3 配置向导与主应用分离
首次运行时,会先打开一个独立的配置向导窗口(setup/),完成API配置、模型选择、工作区设置后才进入主应用。这个设计让首次配置流程清晰、专注,不会与主界面混杂。
四、双模式设计:桌面 vs 仪表盘
CogitoAgent 提供了两种桌面交互模式,满足不同使用场景。
4.1 桌面模式(Desktop Mode)
启动命令 :npm run electron:desktop
定位:轻量级悬浮窗,像"桌面宠物"一样常驻。
界面特点:
- 半透明毛玻璃效果,融入桌面背景
- 左侧是虚拟人物视频(可拖拽移动)
- 右侧是对话面板(可点击人物头像切换显示/隐藏)
- 极简交互,专注于快速问答
适用场景:
- 日常快速问答
- 作为桌面"伴侣"常驻
- 资源占用少,适合长时间后台运行
核心文件:
electron/desktop/index.html--- 界面electron/desktop/renderer.js--- 逻辑electron/desktop/style.css--- 毛玻璃样式
4.2 仪表盘模式(Dashboard Mode)
启动命令 :npm run electron:dashboard
定位:全功能工作台,像IDE一样集成所有工具。
界面特点:
- 左侧边栏:会话列表 + 快捷操作 + 人物形象
- 主内容区:欢迎页 / 技能浏览 / 对话视图(三态切换)
- 全窗口设计,最大化信息展示空间
- 工具调用可视化(黄色卡片显示调用,绿色卡片显示结果)
适用场景:
- 复杂任务处理
- 需要多会话切换的场景
- 工具探索和管理
核心文件:
electron/dashboard/index.html--- 界面electron/dashboard/renderer.js--- 逻辑electron/dashboard/style.css--- 布局样式
4.3 双模式切换
用户可以在两种模式间自由切换:
javascript
// renderer.js
// 桌面模式点击"工作台模式"按钮
btnDashboard.addEventListener('click', () => {
window.electronAPI.switchToDashboard();
});
// 仪表盘模式点击"桌宠模式"按钮
btnDesktop.addEventListener('click', () => {
window.electronAPI.switchToDesktop();
});
主进程处理模式切换:
javascript
ipcMain.on('switch-to-dashboard', () => {
if (mainWindow) mainWindow.close();
if (!dashboardWindow) createDashboardWindow();
});
五、WebSocket桥接:连接桌面与Agent
5.1 为什么需要桥接?
Electron渲染进程不能直接与Node.js子进程通信(出于安全隔离考虑)。因此需要一个桥接层:
Agent.js ws-server.js agent-bridge.js preload.cjs UI渲染进程 Agent.js ws-server.js agent-bridge.js preload.cjs UI渲染进程 #mermaid-svg-KGSo4CVS8pOCFrci{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KGSo4CVS8pOCFrci .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KGSo4CVS8pOCFrci .error-icon{fill:#552222;}#mermaid-svg-KGSo4CVS8pOCFrci .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KGSo4CVS8pOCFrci .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KGSo4CVS8pOCFrci .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KGSo4CVS8pOCFrci .marker.cross{stroke:#333333;}#mermaid-svg-KGSo4CVS8pOCFrci svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KGSo4CVS8pOCFrci p{margin:0;}#mermaid-svg-KGSo4CVS8pOCFrci .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KGSo4CVS8pOCFrci text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-KGSo4CVS8pOCFrci .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KGSo4CVS8pOCFrci .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-KGSo4CVS8pOCFrci .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-KGSo4CVS8pOCFrci .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-KGSo4CVS8pOCFrci #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-KGSo4CVS8pOCFrci .sequenceNumber{fill:white;}#mermaid-svg-KGSo4CVS8pOCFrci #sequencenumber{fill:#333;}#mermaid-svg-KGSo4CVS8pOCFrci #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-KGSo4CVS8pOCFrci .messageText{fill:#333;stroke:none;}#mermaid-svg-KGSo4CVS8pOCFrci .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KGSo4CVS8pOCFrci .labelText,#mermaid-svg-KGSo4CVS8pOCFrci .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-KGSo4CVS8pOCFrci .loopText,#mermaid-svg-KGSo4CVS8pOCFrci .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-KGSo4CVS8pOCFrci .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KGSo4CVS8pOCFrci .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-KGSo4CVS8pOCFrci .noteText,#mermaid-svg-KGSo4CVS8pOCFrci .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-KGSo4CVS8pOCFrci .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KGSo4CVS8pOCFrci .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KGSo4CVS8pOCFrci .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KGSo4CVS8pOCFrci .actorPopupMenu{position:absolute;}#mermaid-svg-KGSo4CVS8pOCFrci .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-KGSo4CVS8pOCFrci .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KGSo4CVS8pOCFrci .actor-man circle,#mermaid-svg-KGSo4CVS8pOCFrci line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-KGSo4CVS8pOCFrci :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ipcRenderer.send('user-message') 转发IPC事件 ws.send(JSON) 解析并处理 广播回复 WebSocket消息 ipcRenderer.send('agent-reply') 渲染消息
5.2 桥接模块:agent-bridge.js
agent-bridge.js 是连接Electron和Agent的"翻译官":
javascript
// 连接WebSocket
function connect() {
ws = new WebSocket('ws://localhost:9527');
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
// 向所有已注册窗口转发消息
connectedWindows.forEach(win => {
win.webContents.send('agent-reply', msg);
});
});
}
// 处理来自渲染进程的消息
ipcMain.on('user-message', (_event, text) => {
ws.send(JSON.stringify({ type: 'user-message', text }));
});
5.3 多窗口支持
agent-bridge.js 通过 connectedWindows Set 支持多个窗口同时连接到同一个Agent,适合未来扩展多窗口场景。
六、多会话管理:AI的"档案室"
6.1 什么是多会话?
在终端模式下,所有对话都在同一个上下文中。多会话系统允许用户创建多个独立的对话空间:
| 会话 | 用途 | 状态 |
|---|---|---|
| 默认会话 | 日常对话 | 当前激活 |
| 项目A讨论 | 记录项目A的设计决策 | 已归档 |
| 代码审查 | 代码评审记录 | 活跃 |
每个会话拥有:
- 独立的对话历史
- 独立的上下文(不会被其他会话污染)
- 独立的人设(可单独设置)
- 持久化存储(关闭程序后不丢失)
6.2 会话数据结构
元数据(meta.json):
json
{
"sessions": [
{
"id": "sess_abc123",
"name": "项目A讨论",
"createdAt": "2024-06-20T10:00:00Z",
"lastActiveAt": "2024-06-20T15:30:00Z",
"messageCount": 45
}
],
"activeId": "sess_abc123"
}
会话文件(sess_abc123.json):
json
[
{ "role": "system", "content": "...系统提示词..." },
{ "role": "user", "content": "我们开始讨论项目A" },
{ "role": "assistant", "content": "好的,我已经记录了..." }
]
6.3 会话管理命令
在CLI模式下,用户可以通过以下命令管理会话:
| 命令 | 功能 |
|---|---|
/sessions |
列出所有会话 |
/new |
创建新会话 |
/switch <id> |
切换到指定会话 |
/delete <id> |
删除会话 |
/rename <name> |
重命名当前会话 |
6.4 会话切换流程
javascript
function switchSession(sessionId) {
// 1. 归档当前会话
archiveCurrentSession();
saveSession(currentSessionId, conversationHistory);
// 2. 切换
currentSessionId = sessionId;
meta.activeId = sessionId;
// 3. 加载新会话
const messages = loadSession(sessionId);
conversationHistory = messages.length > 0
? messages
: [{ role: 'system', content: buildSystemPrompt() }];
// 4. 更新元数据
saveMeta(meta);
}
6.5 会话压缩与归档
长时间对话会导致上下文膨胀,CogitoAgent采用自动压缩 + 归档策略:
- 触发条件:对话轮数超过150轮 或 Token估算超过100K
- 压缩策略 :
- 保留最近10轮对话
- 将更早的对话生成摘要
- 完整历史归档到文件
- 归档保留:每个会话最多保留3个归档文件
javascript
function compressHistory() {
const nonSystemMessages = conversationHistory.filter(m => m.role !== 'system');
if (nonSystemMessages.length <= 10) return;
// 归档旧对话
const toArchive = nonSystemMessages.slice(0, -10);
archiveConversation(toArchive);
// 保留最近10轮
const recentMessages = nonSystemMessages.slice(-10);
const summary = generateSummary(recentMessages);
// 重建历史
conversationHistory = [
{ role: 'system', content: buildSystemPrompt() },
{ role: 'user', content: `[上下文摘要] ${summary}` },
...recentMessages
];
}
七、工具调用可视化
7.1 为什么需要可视化?
在终端模式下,工具调用显示为灰色小框。在桌面模式下,我们可以做得更好:
| 终端显示 | 桌面显示 |
|---|---|
| 灰色小框 | 彩色卡片(黄色=调用,绿色=成功,红色=失败) |
| 纯文本 | 可折叠展开 |
| 无法交互 | 点击展开/收起详情 |
7.2 工具卡片的实现
Dashboard模式下的工具调用通过 MessageRenderer 渲染为卡片:
javascript
// message-renderer.js
addToolCall(toolName, args) {
const card = document.createElement('div');
card.className = 'tool-result-card tool-result-collapsed';
// 头部:工具名称 + 状态
const header = `
<div class="tool-result-header">
<div class="tool-icon">✓</div>
<div class="tool-name">${toolName}</div>
<div class="tool-status success">成功</div>
</div>
`;
// 内容:参数和结果(可折叠)
const content = `
<div class="tool-result-content">
<pre>${JSON.stringify(data, null, 2)}</pre>
</div>
`;
// 点击切换折叠状态
card.addEventListener('click', () => {
card.classList.toggle('tool-result-collapsed');
card.classList.toggle('tool-result-expanded');
});
}
7.3 流式渲染优化
Dashboard模式使用双缓冲 + requestAnimationFrame 节流优化流式输出:
javascript
// 流式渲染
_streamBuffer = '';
_streamPending = false;
_appendToStream(text) {
this._streamBuffer = text;
if (!this._streamPending) {
this._streamPending = true;
requestAnimationFrame(() => this._flushStream(false));
}
}
_flushStream(isFinal) {
this._streamPending = false;
// 用 textContent 更新,不重建DOM
this._streamTextNode.textContent = this._streamBuffer;
}
效果:文字逐字出现,流畅无闪烁,性能优于直接DOM操作。
八、配置向导:首次体验的设计
8.1 向导流程
首次运行时,CogitoAgent会打开一个独立的配置向导窗口(setup/),引导用户完成5步配置:
| 步骤 | 配置项 | 说明 |
|---|---|---|
| 1 | API Base URL | 如 https://api.openai.com/v1 |
| 2 | API Key | 用户的API密钥 |
| 3 | 模型名称 | 如 gpt-4o |
| 4 | 工作区路径 | AI可访问的目录 |
| 5 | 人设选择 | 从13种人设中选择 |
8.2 预设按钮
向导界面提供了常用服务的预设按钮,一键填入:
html
<div class="preset-btns">
<button data-preset="https://api.openai.com/v1">OpenAI</button>
<button data-preset="https://api.moark.com/v1">Moark</button>
<button data-preset="https://api.deepseek.com/v1">DeepSeek</button>
<button data-preset="https://openrouter.ai/api/v1">OpenRouter</button>
</div>
8.3 配置存储
配置完成后,系统会:
- 将敏感信息写入
.env文件 - 将非敏感配置写入
config.json - 将人设文件复制为
persona.md
这种双文件存储策略兼顾了安全性和可读性。
九、共享组件库的设计
9.1 为什么需要共享库?
Desktop 和 Dashboard 两种模式有很多重复的UI逻辑------消息渲染、工具卡片、人物形象管理。如果各自实现,会导致代码重复和维护困难。
9.2 共享组件
electron/shared/ 目录下包含了公共组件:
| 文件 | 功能 |
|---|---|
common.css |
消息气泡、Markdown、工具卡片等公共样式 |
utils.js |
Markdown渲染、HTML净化、工具调用过滤 |
message-renderer.js |
消息渲染引擎(支持两种布局模式) |
persona-manager.js |
人物形象管理(视频/图片切换) |
9.3 使用方式
javascript
// desktop/renderer.js 和 dashboard/renderer.js 共用
MessageRenderer.init({
messagesEl,
layout: 'desktop', // 或 'dashboard'
showAvatar: true, // dashboard显示头像,desktop不显示
});
PersonaManager.init({
containerEl: characterEl,
defaultMediaPath: '../shared/video.mp4',
});
十、设计原则与启发
10.1 渐进增强
CogitoAgent的桌面方案遵循"渐进增强"原则:
- 终端模式:核心功能完整可用
- 桌面模式:增加视觉和交互增强
- 仪表盘模式:增加会话管理和工具可视化
用户可以从终端开始,逐步升级到桌面模式,不需要一次性学习所有功能。
10.2 关注点分离
| 关注点 | 实现位置 |
|---|---|
| AI推理 | Agent独立进程 |
| 界面交互 | Electron渲染进程 |
| 进程通信 | WebSocket + IPC |
| 会话管理 | session.js(Agent侧) |
| 数据持久化 | 文件系统 |
10.3 善用现有生态
- Electron:成熟的桌面应用框架
- WebSocket:标准的双向通信协议
- V8 Isolate :
isolated-vm提供进程级隔离 - CSS毛玻璃 :
backdrop-filter: blur()实现视觉效果
十一、小结
这一篇我们看了CogitoAgent的桌面应用与多会话管理:
| 模块 | 核心实现 |
|---|---|
| 桌面架构 | Electron + 独立Agent进程 + WebSocket桥接 |
| 双模式 | 桌面模式(轻量悬浮窗)+ 仪表盘模式(全功能工作台) |
| 进程通信 | agent-bridge.js 桥接Electron IPC和WebSocket |
| 多会话管理 | 独立会话文件 + 自动压缩归档 + CLI命令管理 |
| 工具可视化 | 彩色卡片 + 折叠展开 + 实时流式渲染 |
| 共享组件 | 公共样式、渲染器、人物形象管理 |
| 配置向导 | 5步引导 + 预设按钮 + 双文件存储 |
核心设计原则:
- 独立进程隔离风险,WebSocket桥接通信
- 双模式满足不同使用场景
- 多会话让对话可组织、可管理
- 共享组件避免代码重复
- 渐进增强降低使用门槛
开源仓库:
- Gitee: https://gitee.com/cnt-code/cogito-agent
- GitHub: https://github.com/SnowLeopard-io/CogitoAgent
如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!