Project AIRI
AIRI 是一个雄心勃勃的开源项目,旨在创建一个能够承载 AI 虚拟角色(如 VTuber)的“灵魂容器”。受 Neuro-sama 启发,AIRI 不仅仅是一个聊天机器人,而是一个完整的框架,让开发者可以构建、部署和交互具备个性、视觉表现和多种能力的 AI 角色。
功能特性
- 多平台支持 (Multi-Platform):采用模块化架构,支持多种运行环境。
- 桌面端 (Desktop):基于 Electron 构建,提供原生应用体验。
- Web端 (Web):基于 Vue 3 的单页应用,方便快速访问和演示。
- 移动端 (Mobile):使用 Capacitor 打包,可构建 iOS 和 Android 应用。
- 丰富的角色系统 (Rich Character System):角色不仅包含对话能力,还拥有完整的资料、配置和互动状态。
- 角色资料:包含名称、描述、头像、封面等基础信息。
- 能力配置 (Capabilities):支持配置 LLM(大语言模型)、TTS(文本转语音)、VLM(视觉语言模型)和 ASR(语音识别)等多种 AI 能力。
- 角色互动:实现了对角色点赞、收藏、分叉(Fork)等社交功能,并统计互动数据。
- 语音交互 (Voice Interaction):内置音频输入、录制和语音活动检测 (VAD) 功能。
- 音频输入管理:通过
useAudioInput和useAudioRecord等 Composables,轻松管理麦克风设备、获取用户媒体流和录制音频。 - 语音活动检测 (VAD):集成了基于 Silero VAD 模型的处理器,能够实时检测用户语音起止,优化对话体验。
- 音频输入管理:通过
- 实时通信与同步 (Real-time Communication & Sync):前后端采用多种机制保证数据实时性和一致性。
- Eventa IPC/RPC:桌面端通过自定义的
@moeru/eventa库实现高效、类型安全的进程间通信。 - 聊天同步 (Chat Sync):前端可以与服务端同步聊天记录、成员和消息,支持多种聊天类型(私聊、群聊)和角色(用户、AI角色)。
- Eventa IPC/RPC:桌面端通过自定义的
- 高度可扩展性 (High Extensibility):
- 插件系统 (Plugin System):定义了清晰的 Manifest 规范 (
manifestV1Schema),允许开发者通过插件扩展 Electron 主进程和渲染进程的功能。插件可以声明自己的能力(Capability),并被宿主应用发现和管理。 - MCP 服务器集成 (MCP Server Integration):支持管理和运行 Model Context Protocol (MCP) 服务器。通过
StdioClientTransport与 MCP 服务器通信,可以动态获取并调用服务器提供的工具(Tools),极大地扩展了 AI 角色的能力边界。
- 插件系统 (Plugin System):定义了清晰的 Manifest 规范 (
- 开发者友好的配置与工具 (Dev-friendly Configuration & Tools):
- 统一的代码规范:通过
@moeru/eslint-config统一管理 ESLint 规则,保证代码质量。 - 现代化的前端技术栈:Web 和桌面端渲染进程采用 Vue 3、Vite、Pinia、UnoCSS 等前沿技术,开发体验流畅。
- 数据库集成:服务端使用 Drizzle ORM 配合 PostgreSQL,提供类型安全的数据库操作。Schema 定义清晰,并支持迁移。
- 统一的代码规范:通过
安装指南
系统要求
- Node.js (最新稳定版)
- pnpm (包管理器)
- 根据目标平台,可能需要相应的开发环境:
- 桌面端 (Electron):Windows、macOS 或 Linux 构建工具链。
- 移动端 (Capacitor):Android Studio (Android) 或 Xcode (iOS)。
分步安装
-
克隆仓库
git clone https://github.com/moeru-ai/airi.git cd airi -
安装依赖 项目是一个 monorepo,使用 pnpm 管理依赖。
pnpm install -
环境配置 根据需要,为特定的应用或服务配置环境变量。例如,Web 端可能需要设置 PostHog 的 API Key (在
posthog.config.ts中查看或覆盖)。# 复制并修改环境变量示例文件(如果有) # cp apps/stage-web/.env.example apps/stage-web/.env -
启动开发服务器 你可以启动不同平台的开发版本。
# 启动 Web 应用 pnpm --filter stage-web dev # 启动 Electron 桌面应用 pnpm --filter stage-tamagotchi dev # 启动服务端 pnpm --filter server dev
使用说明
基础使用示例:创建一个角色 (服务端 API)
以下示例展示了如何使用 AIRI 服务端的 API 创建一个新角色。这需要服务端已运行,并通过了认证。
// 假设你有一个认证后的用户 token
const createCharacter = async (authToken: string) => {
const response = await fetch('http://localhost:your_server_port/api/characters', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
character: {
version: '1.0.0',
coverUrl: 'https://example.com/cover.jpg',
characterId: 'my-ai-assistant', // 角色的唯一标识符
name: 'Aria', // 通过 i18n 字段传递
},
i18n: [
{
language: 'en',
name: 'Aria',
description: 'A helpful and friendly AI assistant.',
tags: ['assistant', 'friendly'],
},
],
capabilities: [
{
type: 'llm',
config: {
apiKey: 'your-llm-api-key',
apiBaseUrl: 'https://api.openai.com/v1',
llm: {
model: 'gpt-3.5-turbo',
temperature: 0.7,
},
},
},
{
type: 'tts',
config: {
apiKey: 'your-tts-api-key',
apiBaseUrl: 'your-tts-endpoint',
tts: {
voiceId: 'en-US-AriaNeural',
speed: 1.0,
pitch: 1.0,
},
},
},
],
// 可选:配置角色的 3D/2D 模型
avatarModels: [
{
type: 'vrm',
config: {
vrm: {
urls: ['https://example.com/model.vrm'],
},
},
},
],
}),
});
if (response.ok) {
const newCharacter = await response.json();
console.log('Character created:', newCharacter);
} else {
console.error('Failed to create character', await response.text());
}
};
典型使用场景:前端实时显示鼠标位置 (Electron)
在 Electron 渲染进程中,你可以使用 Eventa 提供的 API 轻松获取并显示主进程中的屏幕光标位置。
<script setup lang="ts">
import { cursorScreenPoint } from '@proj-airi/eventa'
import { useElectronInvoke, useElectronSubscription } from '@proj-airi/electron-vueuse'
import { ref } from 'vue'
// 调用主进程方法,开始循环发送光标位置
useElectronInvoke(startLoopGetCursorScreenPoint, [])
const cursorPos = ref({ x: 0, y: 0 })
// 订阅主进程发送的光标位置更新
useElectronSubscription(cursorScreenPoint, (point) => {
cursorPos.value = point
})
</script>
<template>
<div>
当前鼠标位置: X: {{ cursorPos.x }}, Y: {{ cursorPos.y }}
</div>
</template>
核心代码
1. 角色服务 (Character Service) - 业务逻辑核心
此模块处理与角色相关的所有核心业务逻辑,如查找、创建、更新角色,以及处理点赞和收藏等互动。
// apps/server/src/services/characters.ts (简化版)
import type { Database } from '../libs/db'
import { and, eq, isNull, sql } from 'drizzle-orm'
import * as schema from '../schemas/characters'
import * as userCharacterSchema from '../schemas/user-character'
export function createCharacterService(db: Database) {
return {
// 根据ID查找角色,并关联加载其能力、i18n、封面等数据
async findById(id: string) {
return await db.query.character.findFirst({
where: and(eq(schema.character.id, id), isNull(schema.character.deletedAt)),
with: { capabilities: true, avatarModels: true, i18n: true, prompts: true, likes: true, bookmarks: true, cover: true, },
})
},
// 处理用户对角色点赞/取消点赞
async like(userId: string, characterId: string) {
return await db.transaction(async (tx) => {
// 检查是否已经点赞
const existing = await tx.query.characterLikes.findFirst({
where: and(eq(userCharacterSchema.characterLikes.userId, userId), eq(userCharacterSchema.characterLikes.characterId, characterId)),
})
if (existing) {
// 已点赞,则取消点赞
await tx.delete(userCharacterSchema.characterLikes)
.where(and(eq(userCharacterSchema.characterLikes.userId, userId), eq(userCharacterSchema.characterLikes.characterId, characterId)))
await tx.update(schema.character).set({ likesCount: sql`${schema.character.likesCount} - 1` }).where(eq(schema.character.id, characterId))
return { liked: false }
} else {
// 未点赞,则添加点赞
await tx.insert(userCharacterSchema.characterLikes).values({ userId, characterId })
await tx.update(schema.character).set({ likesCount: sql`${schema.character.likesCount} + 1` }).where(eq(schema.character.id, characterId))
return { liked: true }
}
})
},
// ... 其他方法:findAll, create, update, delete, bookmark 等
}
}
2. 语音活动检测 (VAD) - 前端交互核心
该模块负责在前端实时处理音频流,判断用户何时开始和结束说话。
// packages/stage-ui/libs/audio/vad/vad.ts (简化版)
import type { PreTrainedModel } from '@huggingface/transformers'
import { AutoModel, Tensor } from '@huggingface/transformers'
export class VAD {
private model: PreTrainedModel | undefined
private state: Tensor
private sampleRateTensor: Tensor
private buffer: Float32Array
// ... 其他属性和构造函数
/**
* 初始化并加载 Silero VAD 模型
*/
public async initialize(): Promise<void> {
try {
this.emit('status', { type: 'info', message: 'Loading VAD model...' })
// 从 Hugging Face 加载预训练的 Silero VAD 模型 (ONNX 格式)
this.model = await AutoModel.from_pretrained('onnx-community/silero-vad', {
config: { model_type: 'custom' } as any,
dtype: 'fp32',
})
this.isReady = true
this.emit('status', { type: 'info', message: 'VAD model loaded successfully' })
} catch (error) {
// 错误处理...
}
}
/**
* 处理输入的音频块,返回是否检测到语音的概率
*/
public async process(audioChunk: Float32Array): Promise<number | null> {
if (!this.model || !this.isReady) return null
// 将音频块转换为模型所需的 Tensor 格式
const inputTensor = new Tensor('float32', audioChunk, [1, audioChunk.length])
// 执行模型推理
const output = await this.model({ input: inputTensor, sr: this.sampleRateTensor, state: this.state, sr: this.sampleRateTensor })
this.state = output.stateNew // 更新内部状态
const speechProb = output.output[0].data[0] // 获取语音概率
return speechProb
}
// ... 其他方法
}
3. MCP 服务器管理 - 能力扩展核心
此代码展示了如何在 Electron 主进程中管理 MCP 服务器,包括启动、停止和调用其工具。
// apps/stage-tamagotchi-electron/src/services/airi/mcp-servers.ts (简化版)
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import type { ElectronMcpStdioServerConfig } from '../../../shared/eventa'
interface McpServerSession {
client: Client
transport: StdioClientTransport
config: ElectronMcpStdioServerConfig
}
export class McpStdioManager {
private servers: Map<string, McpServerSession> = new Map()
// 启动或重启一个 MCP 服务器
async startServer(name: string, config: ElectronMcpStdioServerConfig): Promise<void> {
await this.stopServer(name) // 停止已存在的实例
const client = new Client({ name: `mcp-client-${name}`, version: '1.0.0' })
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: config.env,
cwd: config.cwd,
})
await client.connect(transport)
this.servers.set(name, { client, transport, config })
console.info(`MCP server "${name}" started.`)
}
// 列出所有已连接 MCP 服务器提供的工具
async listTools(): Promise<any[]> {
const allTools = []
for (const [name, session] of this.servers.entries()) {
if (!session.config.enabled) continue
try {
const result = await session.client.listTools()
allTools.push(...result.tools.map(tool => ({ ...tool, serverName: name })))
} catch (error) {
console.error(`Failed to list tools from ${name}:`, error)
}
}
return allTools
}
// 调用特定服务器的特定工具
async callTool(serverName: string, toolName: string, args: any): Promise<any> {
const session = this.servers.get(serverName)
if (!session) throw new Error(`Server "${serverName}" not found`)
const result = await session.client.callTool({ name: toolName, arguments: args })
return result
}
// ... 其他方法:stopAll, getRuntimeStatus 等
}
nXsqf0J/5YTRWD2RR/QZbIcCSjkalHy+DDXL7orjx3E=