架构

可部署应用只是同一个 core 外围的适配器

aibuddy 当前有三个可运行应用入口:apps/web、apps/server、apps/desktop。架构的目标是让 Web 与 Desktop 共享同一套产品 UI 和 agent runtime,同时承认二者在存储、鉴权和进程模型上的差异。

这套结构由三条规则支撑:

  1. 基础包不反向依赖产品层:common、errors、logger 提供浏览器与 Node 都能使用的类型、错误和日志基础。
  2. Agent 能力各自拥有边界:ai、tool、sandbox、memory、knowledge、skill、compaction、workbench 不再挤在 core 内;core 负责编排这些能力。
  3. 应用负责组合适配器,而不是复制业务逻辑: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 则把这些契约接到自己的基础设施上。

分层架构 产品与运行时共享;具体平台适配器只在边缘组合。 产品界面 @aibuddy/app — React 页面、组件、hooks、i18n、任务与聊天界面 客户端契约 @aibuddy/app — TaskClient 与各业务域 client 隐藏 HTTP、SSE、WebSocket、IPC 细节 传输适配器 Web fetch modules;Desktop loopback HTTP、SSE、WebSocket 与 host-local IPC HTTP surface @aibuddy/http — createApp()、路由工厂、h() 校验与鉴权封装 Core runtime @aibuddy/core — 服务、仓储、runTaskTurn、runAgentLoop、工具、memory、MCP、sandbox 端口 共享数据契约 @aibuddy/common — Zod schema、类型、工具定义、模型目录、纯函数工具 Server 适配器 PostgreSQL、Redis、BullMQ、R2、JWT auth、server MCP Desktop 适配器 SQLite、本地文件、safeStorage、内存锁、本地 MCP 大多数应用行为位于适配器边界之上;平台差异在启动组装时注入。

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

依赖关系图 应用导入适配器与共享包;共享包保持单向无环。 apps/web Vite + React 前端 apps/server Hono API + workers apps/desktop Electron host @aibuddy/app React UI + contracts @aibuddy/http Hono 路由 + h() @aibuddy/core 服务 + agent runtime @aibuddy/common 类型、schema、工具、模型目录 导入方向保持单向:应用组合具体适配器,共享包不反向导入应用代码。

依赖图刻意保持单向:

包 / 应用依赖原因
common、errors、logger外部运行时库为浏览器与 Node 提供稳定基础。
八个能力包基础包及少量同层端口各自拥有模型、工具、沙箱、记忆、知识、Skill、压缩与 workbench 机制。
@aibuddy/core基础包与能力包编排 Agent loop、turn services 和领域服务,不承载所有能力实现。
@aibuddy/httpcore 与相关能力契约路由调用服务,但不关心由 server 还是 desktop 承载。
@aibuddy/appbrowser-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/commonElectron 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、knowledgerenderer 可以消费与 Web 相同的 HTTP/SSE/WS 形状,而 main process 用 SQLite-backed services 承载。
Electron IPC尚未完全迁到 loopback 的本地缺口,例如 task CRUD wrapper 与 deliverable readIPC 仍适合 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:

宿主适配文件注入内容
Serverapps/server/src/services/task-runner.tsPostgreSQL repos、R2 file storage、Redis task lock 与 abort bus、BullMQ / inline JobQueue、额度检查、MCP、knowledge、workbench、schedules、WebSocket events。
Desktopapps/desktop/src/main/handlers/agent-stream.tsSQLite repos、本地 file storage、内存 task lock、内存 abort registry、inline JobQueue、固定 local-user,不做 credit gate。

模型、工具、沙箱、记忆、知识、Skill 与压缩的实现分别位于对应能力包。@aibuddy/core 在 turn service 中取得这些端口并完成一次运行,工具侧则通过 ToolBuildDeps 与各能力自报的依赖组装 loadout。这次拆包把“能力归谁拥有”和“谁负责把能力接成一次任务”分开。

平台适配器只在边缘具体化

平台抽象 Web 与 Desktop 复用 UI、HTTP routes 和 core runtime,只替换边缘适配器。 Web Desktop apps/web renderer @aibuddy/app + HTTP client modules apps/desktop renderer @aibuddy/app + loopback / IPC clients HTTP / SSE / WS loopback + IPC apps/server @aibuddy/http createApp(),端口 8001 desktop main process @aibuddy/http createApp(),loopback 端口 8003 server task runner @aibuddy/core runTaskTurn() + 完整 server deps desktop agent stream @aibuddy/core runTaskTurn() + 本地 deps Server 边缘适配器 PostgreSQL repositories、Redis lock 与 stream buffer BullMQ 或 inline JobQueue、R2 storage、JWT auth Desktop 边缘适配器 SQLite repositories、本地文件、safeStorage 内存锁与 abort registry、本地 MCP manager @aibuddy/common 的类型与 schema 被两条路径共享。

关注点ServerDesktop
数据库PostgreSQL + apps/server/src/repositories 中的 Drizzle repositoriesSQLite + apps/desktop/src/main/repositories 中的 Drizzle 风格 repositories
鉴权better-auth / JWT,经 server auth helpersloopback 中的固定 local-user + 本地桌面假设
Task streamHTTP SSE,可选 Redis StreamBuffer 支持跨实例 resumeloopback HTTP SSE,主进程内使用内存 StreamBuffer 支持断线 resume
Chat streamWebSocket;配置 Redis 时支持跨进程 fan-outDesktopWsHub 承载 loopback WebSocket
Job queue有 REDIS_URL 时用 BullMQ,否则 inline fallbackinline JobQueue
Task lockRedis lock + heartbeat内存锁
文件存储Cloudflare R2 adapterapp data 目录下的本地文件系统
密钥加密当前为 plaintext portElectron safeStorage
MCPServer-managed MCP OAuth / tool providerstdio-capable local MCP manager,可启动本地 MCP server

这张表才是当前代码里的“平台抽象”。新增平台意味着实现这些适配器,并决定它提供哪些可选 route capabilities。

相关阅读

这页有帮助吗?