Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 99 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,137 +1,151 @@
# Persona
<div align="center">

Persona 是一个 AI Agent 桌面应用。用户可以在本地创建和管理多个 AI Agent,通过聊天界面与 Agent 交互,Agent 能够调用工具(执行命令、读写文件)来完成任务。应用内置 Cloudflare Tunnel 支持,可以将本地 Agent 暴露到公网,方便从手机或其他设备远程访问。
# Persona Agent

项目由两个核心部分组成:server 负责对话管理、工具执行、MCP 连接等后端逻辑;desktop 是 Electron 桌面客户端,提供图形界面。
**你的本地 AI Agent 工作站**

## 技术栈
创建和管理多个 AI Agent,赋予它们工具、技能和性格,让它们帮你完成任务。

**Server**(`packages/server`):TypeScript + Bun 运行时,使用 Express 提供 HTTP/WebSocket API,支持 OpenAI 和 Anthropic 等多个 LLM 供应商。Bun 的 `--compile` 将整个 server 打包成单个二进制文件,不需要用户安装任何运行时。
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

**Desktop**(`packages/desktop`):Electron + React 18 + TypeScript,使用 electron-vite 构建,Tailwind CSS 做样式,Zustand 管理状态。应用启动时从 App Bundle 内直接运行 server 二进制,以子进程方式管理其生命周期。
</div>

## 开发环境准备
<!-- ![Persona Agent 主界面](./docs/screenshot-main.png) -->

需要安装 [Bun](https://bun.sh/) 和 Node.js(18+)。
## 它能做什么

```bash
# 安装依赖
cd packages/server && bun install
cd packages/desktop && npm install
```
Persona Agent 让你在本地创建多个 AI Agent,每个有独立的角色设定、模型配置和会话历史。你可以用一个 Agent 做编程助手,另一个做写作顾问,互不干扰。Agent 可以调用内置工具来执行命令、读写文件,也可以通过 MCP 协议连接外部工具服务器扩展能力,还可以加载 Skill 来获得专业知识。

## 日常开发
对话支持流式输出和完整的 Markdown 渲染,代码块一键复制,Agent 的推理过程和工具调用细节都可以展开查看。如果你想换一种氛围,可以打开伴侣模式:全屏展示角色立绘,Agent 会根据对话自动切换表情,配合语音合成朗读回复。

在项目根目录运行一条命令即可启动开发环境:
内置 Cloudflare Tunnel,一键把本地服务暴露到公网,从手机或其它设备远程访问你的 Agent。

```bash
npm run dev
```
**支持的模型供应商:** OpenAI、Anthropic、Google、DeepSeek、MiniMax、xAI、Groq、Mistral、OpenRouter、Cerebras、Fireworks 等 17+ 家。每个 Agent 独立配置默认模型,每个会话可以临时切换。

这个命令会先编译 server 的二进制文件,然后启动 Electron 开发服务器。前端代码修改会通过热更新即时生效。如果修改了 server 的代码,需要退出后重新运行 `npm run dev`,因为 server 需要重新编译
**跨平台:** macOS(Apple Silicon / Intel)、Windows x64。下载安装包即可使用,无需安装任何运行时

也可以单独操作某个包:
## 下载安装

```bash
npm run build:server # 只编译 server 二进制
npm run build:desktop # 只编译前端(不启动 Electron)
```
前往 [GitHub Releases](https://github.com/Code-MonkeyZhang/persona-agent/releases) 下载对应平台的安装包:

## 构建安装包
| 平台 | 文件 |
|------|------|
| macOS Apple Silicon | `Persona-mac-arm64-{version}.dmg` |
| macOS Intel | `Persona-mac-x64-{version}.dmg` |
| Windows x64 | `Persona-win-x64-{version}.exe` |

```bash
npm run dist
```
macOS 打开 DMG 拖入 Applications,Windows 运行 exe 安装即可。

## 快速开始

1. 从 [Releases](https://github.com/Code-MonkeyZhang/persona-agent/releases) 下载并安装
2. 打开应用,进入 Settings → Model Providers,填入至少一个供应商的 API Key 并验证
3. 点击左侧栏的「+」创建 Agent,填写名称、人设,选择模型
4. 开始对话

这会先编译 server 和 desktop,然后用 electron-builder 打包成 .dmg(macOS)。打包后的安装包在 `packages/desktop/dist/` 目录下。安装后的应用是一个独立可执行文件,不需要用户安装 Bun 或 Node.js。
## 从源码构建

## 其他命令
需要 [Bun](https://bun.sh/) 1.0+ 和 Node.js 18+。

```bash
npm run typecheck # 对两个包一起做类型检查
# 安装依赖
cd packages/server && bun install
cd packages/desktop && npm install

# 开发模式(编译 server + 启动 Electron 开发服务器)
npm run dev

# 构建安装包
npm run dist # 当前平台
npm run dist:mac # macOS
npm run dist:win # Windows
```

Server 包还支持 lint 和格式化(需要在 `packages/server` 目录下运行)
其他命令

```bash
bun run lint # oxlint 检查
bun run format # Prettier 格式化
bun run test # 运行测试
npm run typecheck # 类型检查
npm run lint # 代码检查
npm run format # 格式化
npm run check # 一键检查(lint + format + typecheck)
```

### 集成测试配置

`packages/server/tests/chat.test.ts` 是端到端集成测试,会调用真实的 LLM API,需要配置环境变量:
### 测试

```bash
cd packages/server
cp .env.test.example .env.test.local
# 编辑 .env.test.local,填入你的 API Key
cd packages/server && bun test
cd packages/desktop && npm run test
```

必填 `TEST_LLM_API_KEY`,可选 `TEST_LLM_PROVIDER`(默认 `minimax-cn`)和 `TEST_LLM_MODEL`(默认 `MiniMax-M2.7`)。`.env.test.local` 不会被提交到版本控制。如果不配置,chat 集成测试会被自动跳过,其他测试不受影响。可用的 Provider 列表见 `.env.test.example`。

Desktop 包的测试:
Server 的集成测试需要配置环境变量:

```bash
cd packages/desktop && npm run test
cd packages/server
cp .env.test.example .env.test.local
# 编辑 .env.test.local,填入 API Key
```

不配置时集成测试会自动跳过,其他测试不受影响。

## 项目结构

```
persona-agent/
├── package.json # 根编排脚本
├── packages/
│ ├── server/ # 后端服务(Bun 项目)
│ │ ├── src/
│ │ │ ├── agent/ # Agent 定义、配置存储、运行逻辑
│ │ │ ├── session/ # 会话管理、消息持久化
│ │ │ ├── server/ # HTTP/WebSocket 服务、路由、隧道
│ │ │ ├── tools/ # 工具实现(bash、文件读写、pose)
│ │ │ ├── mcp/ # MCP 协议连接管理
│ │ │ ├── skill/ # Skill 加载与管理
│ │ │ ├── auth/ # 认证
│ │ │ ├── config/ # 配置加载
│ │ │ ├── schema/ # 事件与数据 schema
│ │ │ ├── converters/ # LLM 响应格式转换
│ │ │ └── util/ # 工具函数、日志、路径
│ │ ├── bin/ # 预置的 cloudflared 二进制
│ │ └── dist/ # 编译输出的 server 二进制
│ └── desktop/ # 桌面客户端(Electron + npm 项目)
│ ├── src/
│ │ ├── main/ # Electron 主进程(生命周期、窗口、IPC)
│ │ ├── preload/ # 预加载脚本
│ │ └── renderer/ # React 前端(组件、Store、样式)
│ ├── electron-builder.yml
│ └── electron.vite.config.ts
│ ├── server/ # 后端服务(Bun)
│ │ └── src/
│ │ ├── agent/ # Agent 运行时、配置存储
│ │ ├── session/ # 会话管理、消息持久化
│ │ ├── server/ # HTTP/WebSocket、路由、隧道
│ │ ├── tools/ # 内置工具
│ │ ├── mcp/ # MCP 协议连接
│ │ ├── skill/ # Skill 加载
│ │ ├── auth/ # API Key 管理
│ │ └── converters/ # LLM 响应格式转换
│ └── desktop/ # 桌面客户端(Electron + React)
│ └── src/
│ ├── main/ # 主进程
│ ├── preload/ # 预加载脚本
│ └── renderer/ # React 前端
```

## 用户数据目录

应用运行时会在系统标准数据目录下创建 `persona-agent/`,存放配置、Agent、会话和日志等数据。

| 平台 | 路径 |
|------|------|
| macOS | `~/.local/share/persona-agent/` |
| Windows | `%APPDATA%/persona-agent/` |
| Linux | `~/.local/share/persona-agent/` |

```
persona-agent/
├── config/
│ ├── config.yaml # 全局配置
│ └── auth.json # LLM 供应商 API Key
├── agents/
│ └── {agentId}/
│ ├── config.json # Agent 配置
│ ├── assets/ # Agent 资源(头像、语音、姿态、背景)
│ ├── sessions/ # 会话历史
│ └── memory/ # Agent 记忆
├── skills/ # 用户自定义 Skill
├── mcp/
│ ├── mcp.json # MCP 服务器配置
│ └── servers/ # MCP 服务器运行时数据
├── workspace/ # 默认工作目录
└── logs/ # 运行日志
├── config/ # 全局配置、API Key
├── agents/{id}/ # Agent 配置、资源、会话
├── skills/ # 自定义 Skill
├── mcp/ # MCP 服务器配置和运行时数据
└── logs/ # 运行日志
```

## 致谢

### 参考项目

- [Chatbox](https://github.com/chatboxai/chatbox) — 跨平台 AI 桌面客户端
- [Cherry Studio](https://github.com/CherryHQ/cherry-studio) — 全功能 AI 助手,多供应商 LLM 支持
- [Halo](https://github.com/openkursar/hello-halo) — 24/7 自主桌面 AI Agent,数字人形象系统
- [OpenCode](https://github.com/anomalyco/opencode) — AI 编程工具,本项目架构与构建体系的重要参考
- [ZcChat](https://github.com/Zao-chen/ZcChat) — 桌面 AI 伴侣,Galgame 风格角色立绘与语音交互

### 技术依赖

- [Bun](https://bun.sh/) — Server 运行时,单文件编译分发
- [Electron](https://www.electronjs.org/) — 跨平台桌面应用框架
- [React](https://react.dev/) — UI 框架
- [pi-ai](https://github.com/mariozechner/pi-ai) — 统一多供应商 LLM 调用接口
- [Model Context Protocol](https://modelcontextprotocol.io/) — 工具扩展协议
- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — 内网穿透
- [MiniMax](https://www.minimaxi.com/) — TTS 语音合成

## License

[MIT](LICENSE)
80 changes: 8 additions & 72 deletions packages/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @description Electron 主进程入口文件 - 负责应用程序生命周期管理、窗口创建、进程管理和 IPC 通信
*/

import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron';
import { join } from 'path';
import { spawn } from 'child_process';
import type { ChildProcess } from 'child_process';
Expand All @@ -25,7 +25,6 @@ const isWin = process.platform === 'win32';
const BINARY_NAME = isWin ? 'persona-agent-server.exe' : 'persona-agent-server';

let serverProcess: ChildProcess | null = null;
let settingsWindow: BrowserWindow | null = null;

// 日志配置:开发环境写文件,生产环境不写
if (is.dev) {
Expand Down Expand Up @@ -166,68 +165,6 @@ function createWindow(): void {
}
}

/**
* 创建设置窗口
* 用于显示应用程序设置界面
* @returns {void}
*/
function createSettingsWindow(): void {
log.info('Creating settings window');

// 如果设置窗口已经存在,直接聚焦到已有窗口,避免重复创建
if (settingsWindow) {
log.info('Settings window already exists, focusing');
settingsWindow.focus();
return;
}

// 创建一个新的浏览器窗口作为设置界面
settingsWindow = new BrowserWindow({
width: 720,
height: 600,
minWidth: 600,
minHeight: 400,
show: false, // 先不显示,等 ready-to-show 事件再显示,避免白屏闪烁
autoHideMenuBar: true,
title: '设置中心',
resizable: true,
maximizable: false, // 禁止最大化,设置窗口不需要那么大
fullscreenable: false, // 禁止全屏
webPreferences: {
preload: join(__dirname, '../preload/index.cjs'),
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
},
});

// 窗口内容加载完成后才显示,用户体验更好
settingsWindow.on('ready-to-show', () => {
settingsWindow?.show();
});

// 窗口关闭时清空引用,允许下次重新创建
settingsWindow.on('closed', () => {
settingsWindow = null;
});

// 拦截页面内的新窗口打开请求(如 <a target="_blank">),用系统浏览器打开
settingsWindow.webContents.setWindowOpenHandler((details) => {
require('electron').shell.openExternal(details.url);
return { action: 'deny' };
});

// 加载同一个 index.html,但 URL 末尾拼接 #settings
// 渲染进程的 App 组件会读取这个 hash 值,据此渲染 <SettingsWindow /> 而非聊天界面
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
settingsWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#settings`);
} else {
settingsWindow.loadFile(join(__dirname, '../renderer/index.html'), {
hash: 'settings',
});
}
}

/**
* 应用的主要入口
* @returns {Promise<void>}
Expand Down Expand Up @@ -255,14 +192,6 @@ app.whenReady().then(async () => {
optimizer.watchWindowShortcuts(window);
});

/**
* IPC 处理器:接受前端发来的消息 打开设置窗口
*/
ipcMain.handle('open-settings-window', () => {
log.info('IPC: open-settings-window received');
createSettingsWindow();
});

/**
* IPC 处理器:获取当前服务器 URL
*/
Expand Down Expand Up @@ -361,6 +290,13 @@ app.whenReady().then(async () => {
}
);

/**
* IPC 处理器:使用系统默认浏览器打开指定 URL
*/
ipcMain.handle('open-external', (_event, url: string) => {
return shell.openExternal(url);
});

await startServer();

createWindow();
Expand Down
13 changes: 7 additions & 6 deletions packages/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*
* preload 运行在有 Node.js 权限的特殊环境中,
* 通过 contextBridge 将以下操作暴露到 window.api:
* - 打开设置窗口
* - 系统文件夹选择器
* - 后端服务地址查询
* - 日志代理写入
Expand All @@ -18,11 +17,6 @@ import { electronAPI } from '@electron-toolkit/preload';
* 每个方法底层通过 ipcRenderer.invoke 向主进程发送 IPC 消息
*/
const api = {
/**
* 打开设置窗口,通过 IPC 通知主进程。
*/
openSettingsWindow: () => ipcRenderer.invoke('open-settings-window'),

/**
* 弹出系统原生的文件夹选择对话框
* @param options - 对话框配置,可指定标题和默认打开路径
Expand Down Expand Up @@ -69,6 +63,13 @@ const api = {
body: ArrayBuffer;
}> => ipcRenderer.invoke('proxy-fetch', url, options),

/**
* 使用系统默认浏览器打开指定 URL
* @param url - 要打开的 URL
*/
openExternal: (url: string): Promise<void> =>
ipcRenderer.invoke('open-external', url),

/** 窗口控制方法集合,每个方法通过 IPC 转发到主进程执行。 */
windowControls: {
minimize: () => ipcRenderer.invoke('window:minimize'),
Expand Down
Loading
Loading