架构
可部署应用只是同一个 core 外围的适配器
aibuddy 当前有三个可运行应用入口:apps/web、apps/server、apps/desktop。架构的目标是让 Web 与 Desktop 共享同一套产品 UI 和 agent runtime,同时承认二者在存储、鉴权和进程模型上的差异。
这套结构由三条规则支撑:
- 基础包不反向依赖产品层:
common、errors、logger提供浏览器与 Node 都能使用的类型、错误和日志基础。 - Agent 能力各自拥有边界:
ai、tool、sandbox、memory、knowledge、skill、compaction、workbench不再挤在core内;core负责编排这些能力。 - 应用负责组合适配器,而不是复制业务逻辑:server 把能力接到 PostgreSQL、Redis、R2、better-auth 和 BullMQ;desktop 把它们接到 SQLite、本地文件、Electron
safeStorage、内存锁和固定的local-user。
真正重要的是依赖方向:UI 契约依赖共享类型,共享服务依赖仓储与基础设施接口,可部署应用在组装根里提供具体实现。
设计判断
仓库优化的是“共享产品行为”,不是“平台内部完全相同”。Web 和 Desktop 应该像同一个产品,但不能假装 PostgreSQL 与 SQLite、JWT auth 与本地进程身份、远端 worker 与 Electron main process 是同一种东西。
所以包图把可复用能力和具体适配器分开:八个能力包持有各自机制,@aibuddy/core 负责运行时编排,@aibuddy/http 持有共享 Hono surface,各 app 则把这些契约接到自己的基础设施上。
15 个 packages 分成基础、能力与组合三层
aibuddy/
├── packages/
│ ├── common · errors · logger 基础类型、错误与日志
│ ├── ai · tool · sandbox 模型、工具与执行环境
│ ├── memory · knowledge · skill 跨会话信息与按需能力
│ ├── compaction · workbench 上下文压缩与结构化工作
│ └── core · http · app · web-admin 编排、协议、共享 UI 与 Web 管理面
└── apps/
├── web/ web Vite + React 前端,localhost:8000
├── server/ @aibuddy/server Hono API server,localhost:8001,以及 worker 入口
└── desktop/ @aibuddy/desktop Electron app,renderer 在 localhost:8002,loopback API 在 8003
依赖图刻意保持单向:
| 包 / 应用 | 依赖 | 原因 |
|---|---|---|
common、errors、logger | 外部运行时库 | 为浏览器与 Node 提供稳定基础。 |
| 八个能力包 | 基础包及少量同层端口 | 各自拥有模型、工具、沙箱、记忆、知识、Skill、压缩与 workbench 机制。 |
@aibuddy/core | 基础包与能力包 | 编排 Agent loop、turn services 和领域服务,不承载所有能力实现。 |
@aibuddy/http | core 与相关能力契约 | 路由调用服务,但不关心由 server 还是 desktop 承载。 |
@aibuddy/app | browser-safe 契约 | React UI 只看客户端契约和类型,不导入 Node runtime。 |
apps/web | @aibuddy/app、@aibuddy/common、@aibuddy/web-admin | 浏览器前端与管理后台。 |
apps/server | @aibuddy/core、@aibuddy/http、@aibuddy/common | 生产 API、worker、仓储、队列、鉴权、存储。 |
apps/desktop | @aibuddy/app、@aibuddy/core、@aibuddy/http、@aibuddy/common | Electron host 同时组合共享 UI 与本地服务。 |
React 面向契约,而不是面向传输
共享 UI 包为每个业务域定义一个客户端契约。任务域的契约在 packages/app/src/contracts/task.contract.ts:
export interface TaskClient {
list(options?: TaskListQuery): Promise<TaskListPage>;
get(id: string): Promise<TaskDetail>;
create(data: CreateTaskInput): Promise<CreateTaskData>;
update(id: string, data: UpdateTaskInput): Promise<TaskDetail>;
remove(id: string): Promise<void>;
getMessages(id: string, options?: { limit?: number; afterSequence?: number }): Promise<TaskMessagesResult>;
setMessageFeedback(id: string, messageId: string, feedback: MessageFeedback | null): Promise<void>;
abort(id: string): Promise<void>;
stream(id: string, content: string): Promise<{ status: string; sequenceNumber: number }>;
readArtifact(id: string, path: string): Promise<ArtifactContent>;
}
packages/app/src/context/service-context.tsx 通过 ClientProvider 和 useTaskClient() 等 hook 暴露这些客户端。组件不知道一个方法最终由浏览器 fetch、Electron IPC,还是 desktop loopback API 实现。
这条边界让页面、卡片和工具渲染器保持共享,同时允许每个应用选择自己的传输方式。
Web 与 Desktop 提供不同的契约适配器
Web 前端构建 HTTP 模块,例如 packages/app/src/api/modules/task.ts。每个方法都委托给共享的 request 函数:
export function createTaskModule(request: RequestFn) {
return {
list: (options) => request<TaskListPage>(`/api/tasks${query}`),
get: (id) => request<TaskDetail>(`/api/tasks/${id}`),
create: (data) => request<CreateTaskData>("/api/tasks", { method: "POST", body: JSON.stringify(data) }),
abort: (id) => request<void>(`/api/tasks/${id}/abort`, { method: "POST" }),
};
}
Desktop 现在是混合传输:
| Desktop 路径 | 用途 | 原因 |
|---|---|---|
localhost:8003 loopback HTTP | 共享 Hono 路由、task SSE、chat WebSocket、API keys、models、MCP、schedules、knowledge | renderer 可以消费与 Web 相同的 HTTP/SSE/WS 形状,而 main process 用 SQLite-backed services 承载。 |
| Electron IPC | 尚未完全迁到 loopback 的本地缺口,例如 task CRUD wrapper 与 deliverable read | IPC 仍适合 host-local 调用,但它不再是 desktop 唯一传输。 |
apps/desktop/src/main/local-api-server.ts 用固定 local-user 鉴权适配器启动 loopback server。它调用的正是 server 也使用的 @aibuddy/http createApp()。
createApp 是共享 HTTP surface
packages/http/src/create-app.ts 构建 transport-neutral 的 Hono API。apps/server 与 apps/desktop 都传入各自的 auth 和 service 依赖:
export function createApp(deps: AppDeps): Hono<AppEnv> {
const h = makeH({ authenticate: deps.authenticate, getUserRoles: deps.getUserRoles });
app.route("/api/projects", createProjectsRoute({ h, service: r.projectService }));
app.route("/api/users", createUsersRoute({ h, userService: r.userService, preferenceService: r.preferenceService }));
app.route("/api/api-keys", createApiKeysRoute({ h, service: r.apiKeyService }));
app.route("/api/agents", createAgentsRoute({ h, service: r.agentService, promptService: r.agentPromptService }));
if (r.mcpService && r.mcpClient) {
app.route("/api/mcp-servers", createMcpServersRoute({ h, service: r.mcpService, mcpClient: r.mcpClient }));
}
}
可选路由依赖就是 capability gate。某个平台不提供 knowledgeService、scheduleService 或 getTeamSnapshot,对应路由或子功能就不挂载。平台差异因此是显式的,不需要 fork 路由实现。
packages/http/src/h.ts 里的 h() 也是注入式的。Server 传入 JWT 鉴权与角色查询;desktop 则为单用户 loopback 传入固定 local-user 和 admin 角色。
@aibuddy/core 编排能力包,而不是重新吞并它们
任务执行主体在 packages/core/src/services/agent/run-task-turn.ts。它持有传输中立的顺序:加载任务数据、准备 sandbox、构建 RuntimeContext、组装 tool services、调用 runAgentLoop、把 agent stream 转成 UI chunks,并通过 finalizeTaskTurn 收尾。
宿主文件只负责适配这个 core:
| 宿主 | 适配文件 | 注入内容 |
|---|---|---|
| Server | apps/server/src/services/task-runner.ts | PostgreSQL repos、R2 file storage、Redis task lock 与 abort bus、BullMQ / inline JobQueue、额度检查、MCP、knowledge、workbench、schedules、WebSocket events。 |
| Desktop | apps/desktop/src/main/handlers/agent-stream.ts | SQLite repos、本地 file storage、内存 task lock、内存 abort registry、inline JobQueue、固定 local-user,不做 credit gate。 |
模型、工具、沙箱、记忆、知识、Skill 与压缩的实现分别位于对应能力包。@aibuddy/core 在 turn service 中取得这些端口并完成一次运行,工具侧则通过 ToolBuildDeps 与各能力自报的依赖组装 loadout。这次拆包把“能力归谁拥有”和“谁负责把能力接成一次任务”分开。
平台适配器只在边缘具体化
| 关注点 | Server | Desktop |
|---|---|---|
| 数据库 | PostgreSQL + apps/server/src/repositories 中的 Drizzle repositories | SQLite + apps/desktop/src/main/repositories 中的 Drizzle 风格 repositories |
| 鉴权 | better-auth / JWT,经 server auth helpers | loopback 中的固定 local-user + 本地桌面假设 |
| Task stream | HTTP SSE,可选 Redis StreamBuffer 支持跨实例 resume | loopback HTTP SSE,主进程内使用内存 StreamBuffer 支持断线 resume |
| Chat stream | WebSocket;配置 Redis 时支持跨进程 fan-out | DesktopWsHub 承载 loopback WebSocket |
| Job queue | 有 REDIS_URL 时用 BullMQ,否则 inline fallback | inline JobQueue |
| Task lock | Redis lock + heartbeat | 内存锁 |
| 文件存储 | Cloudflare R2 adapter | app data 目录下的本地文件系统 |
| 密钥加密 | 当前为 plaintext port | Electron safeStorage |
| MCP | Server-managed MCP OAuth / tool provider | stdio-capable local MCP manager,可启动本地 MCP server |
这张表才是当前代码里的“平台抽象”。新增平台意味着实现这些适配器,并决定它提供哪些可选 route capabilities。
相关阅读
- 快速开始——当前可运行的 Web 与 Desktop 路径。
- 任务编排——server 与 desktop 如何承载
runTaskTurn。 - Agent Engine——
@aibuddy/core内部的循环。 - 流式架构——SSE、WebSocket 与 resume 行为。
- 鉴权与身份——server JWT 与 desktop local identity。