跳到主要内容

Multica — 这是什么、全景与阅读地图

30 秒导读: Multica 是一个开源的「托管 agent 平台」。它把 Claude Code、Codex 这类编码 agent 的命令行工具,包装成看板(项目管理界面)上可以被指派任务、会自己汇报进度和提 blocker 的一等成员——就像你给同事派活一样给 AI 派活。本章只做导航:讲清它是什么、五大部件怎么连、一条任务如何从 issue 走到跑起来,然后把你带向 5 个分章。

本章的定位是门面(Layer 0 + Layer 1):只讲全貌与主线,不深入任何机制的实现。每个机制的细节留给对应分章,正文里用 相对链接 指过去。


1. 这是什么(零基础也能懂)

一句话定义: Multica 是一个自托管、厂商中立的平台,让你像管理人类同事一样管理一队编码 agent——在看板上给它们派 issue,它们自己接活、写代码、改状态、遇到障碍会主动提 blocker。

它解决谁的什么问题

传统用编码 agent 的方式是:你打开终端,手动粘一段 prompt,盯着它跑,跑完再粘下一段。Multica 想干掉这套「复制粘贴 + 保姆式盯梢」:

  • 给谁用: 小团队(几个人 + 一队 agent)。README 的口号是「两个工程师加一队 agent,能跑出二十人的产出」(依据:README.md:52)。
  • 解决什么: 把 agent 从「一次性对话工具」升级成「看板上的常驻成员」——有档案、能被 @、能评论、能建 issue、能报障碍(依据:README.md:58「Agents as Teammates」)。

它能做什么(核心功能)

功能白话依据
Agents as Teammatesagent 是一等 assignee,能拥有 issue、评论、改状态README.md:58
Squads把 agent 编成小组,派给「组」,由 leader agent 决定谁接README.md:59
自主执行全生命周期(入队/认领/开始/完成/失败)+ WebSocket 实时进度README.md:60
Autopilots定时/webhook 触发的周期任务,自动建 issue 并路由给 agentREADME.md:61
Reusable Skills每个解法沉淀成可复用技能,团队能力逐步累积README.md:62
Unified Runtimes一个面板管所有算力:本地 daemon + 云端 runtimeREADME.md:63

用起来什么样

multica setup # 配置 + 登录 + 启动本地 daemon
# 打开 Web 看板 → Settings→Agents 新建一个 agent(选 Claude Code / Codex …)
# 在看板上建一个 issue,把它 assign 给这个 agent
# → agent 自动接活、在你的机器上执行、像同事一样回报进度

关键点:agent 真正跑代码的地方,是你自己的机器(通过一个后台 daemon),不是某个云黑盒。服务端只负责「派活、记账、广播」,具体执行在本地(依据:README.md:159-177 架构图,daemon 标注 "runs on your machine")。

一句话直觉

把 Multica 想成 agent 版的 Jira/Linear:看板、issue、assignee、状态流转这套你熟悉的东西照搬,唯一区别是——assignee 可以是一个会自己写代码的 AI,而「执行引擎」是跑在你机器上的一个 daemon。


2. 顶层全景(五大部件与数据流)

2.1 五大部件

Multica 由五块组成。先看它们怎么连,再逐块看职责。

怎么读下图: 实线是「谁调用谁 / 数据往哪流」;最右是持久层,最下是跑在你机器上的执行器。前端有三种形态(Web / 桌面 / 手机),共享同一套业务逻辑包。

┌─────────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 前端 · 三种形态 │ HTTP │ Go 后端 │ SQL │ PostgreSQL │
│ Web(Next.js) │───────>│ Chi 路由 + sqlc │──────>│ + pgvector │
│ 桌面(Electron) │<───────│ gorilla/ws │<──────│ (无外键约束) │
│ 手机(Expo/RN) │ /ws │ │ └──────────────────┘
└─────────────────────────┘ 推送 └────────┬─────────┘
/api/daemon/ws │ 认领+上报
┌──────┴───────────┐
│ 本地 Daemon │ 跑在你的机器上
│ 认领任务→跑 CLI │ Claude Code / Codex /
└──────────────────┘ Cursor / Copilot … 15+ 种

2.2 部件一句话职责

部件干什么在哪(克隆根相对路径)
前端(多端)看板 UI、issue/agent 管理;Web、桌面、手机三形态共享业务逻辑apps/web/apps/desktop/apps/mobile/packages/
Go 后端HTTP API(Chi)、SQL 访问(sqlc 生成)、两套 WebSocket;判定是否 enqueue run、记账、广播server/
PostgreSQL存 issue/agent/task/workspace;pgvector 做向量;刻意不建外键,关系全在应用层维护server/migrations/
本地 Daemon认领任务、准备工作目录、启动真实 agent CLI、把流式输出上报服务端server/internal/daemon/、CLI 入口 server/cmd/multica/
Agent 运行时抽象把 15+ 种编码 CLI 统一成一个 Backend.Execute 接口server/pkg/agent/

技术栈定档(依据:README.md:172-177):前端 Next.js 16(App Router);后端 Go(Chi + sqlc + gorilla/websocket);数据库 PostgreSQL 17 + pgvector;执行层是本地 daemon 跑各家 CLI。

2.3 「一等成员」这件事,落在数据模型上

Multica 之所以能让 agent 像人一样被派活,关键在一处设计:issue 的 assignee 是多态的——assignee_type"member""agent"assignee_id 指向对应实体。同一个「指派」动作,派给人还是派给 agent 走的是同一套字段(依据:server/internal/service/issue_trigger.go:117switch issue.AssigneeType.String"agent"/"squad" 两支)。这就是「agents as teammates」在底层的样子。


3. 主线走一遍:一条 issue 如何变成一次运行

这是理解 Multica 的中央链路。把它走通,五个分章就都有了挂靠点。全程高层视角,不进代码——细节在 第 3 章第 2 章

怎么读下图: 从左到右是时间顺序;上半段在服务端,下半段在你机器上的 daemon;虚线是 WebSocket 推送。

[1] 把 issue 指派给某 agent(或改状态出 backlog)


[2] 服务端问一句:这次写入会不会启动一次运行? ← WillEnqueueRun(唯一判定权威)
│ 会:产出 IssueRunTrigger 不会:静默停在 backlog

[3] enqueue:写一行 task 到队列(status=queued) ← EnqueueTaskForIssueWithHandoff


[4] 本地 daemon 轮询/被唤醒,认领这条 task ← ClaimTasksByRuntime(status→dispatched)


[5] daemon 准备工作目录 → StartTask(status→running) ← 跑真实 agent CLI
│ ├─ 流式上报进度 / 消息 / 用量 ┄┄┄┄┄┄┄┄┄┐
▼ ▼ ┊
[6] CompleteTask / FailTask,结果回写 ┊ WebSocket 推送
│ ▼
└──────────────────────────────────────> 前端 cache 失效、看板实时刷新

三个关键节点的权威在哪

节点谁说了算依据
② 会不会启动运行IssueService.WillEnqueueRun——preview 和真实写入共用同一个判定,前端不许自己复刻规则server/internal/service/issue_trigger.go:89
④ 谁来认领handler 层入口 ClaimTasksByRuntime(批量)/ ClaimTaskByRuntime(单条)按 runtime 收活,转手到 service 层 ClaimTask/ClaimTasksForRuntimes 做原子出队(状态机细节见第 3 章)server/internal/handler/daemon.go:1387:2431
⑤⑥ 状态推进daemon 依次调 StartTaskCompleteTask/FailTask 回写server/internal/daemon/client.go:321:373:396

一个易错点先点破:指派进 backlog 不会启动运行——backlog 是「停车场」。只有指派到非 backlog 状态、或把已指派的 issue 从 backlog 提升出来,才触发 run(依据:server/internal/service/issue_trigger.go:103if issue.Status == "backlog" 直接返回不触发)。


4. 阅读地图(建议按序读)

五个分章由浅入深。若你只想懂某一块,按下表直接跳。

顺序章节一句话导引什么时候读它
101-agent-runtime.md15+ 种 CLI 如何被抽象成一个 Backend.Execute 接口;New(agentType) 工厂如何分发想加一种新 agent、或搞懂各家 CLI 差异被藏到哪
202-local-daemon.mddaemon 的认领→准备 workdir→执行→上报四步循环,以及并发/租约/自愈想知道任务在你机器上到底怎么跑起来
303-task-dispatch-lifecycle.mdWillEnqueueRun 判定规则、任务状态机、admission reason codes、路由想搞懂「为什么这次没触发运行」「状态怎么流转」
404-realtime-protocol.md两套 WebSocket(daemon 通道 vs 浏览器广播)、scope 订阅、多实例 Redis 扇出想懂实时进度怎么推到前端、多服务器实例如何同步
505-frontend-architecture.md共享包依赖方向、服务态(TanStack Query) vs 客户态(Zustand)、多端适配想在 Web/桌面/手机上改 UI、或理解 cache 失效纪律

5. 巧妙之处提炼(值得带走的设计)

这些是读源码时不显然、但很聪明的决策。每条只点「妙在哪 + 在哪看」,展开留给分章。

5.1 Admission reason codes:拒绝一次运行,但不泄露私有 agent 的存在

服务端拒绝或跳过一次运行时,回给客户端的是一个稳定的、可本地化的原因码(ReasonCode),而不是一句人读的错误串。关键在于这套码是枚举安全的:一个码绝不透露某个私有 agent 是否存在、叫什么、归谁

最典型的是 ReasonInvocationNotAllowed——它故意含糊,不区分「目标是私有的」和「目标根本不存在」,这样攻击者无法靠试探反推出私有 agent 的存在(依据:server/internal/dispatch/reason.go:23-26 注释「Deliberately generic — it does not distinguish 'target is private' from 'target does not exist'」;枚举安全性见 reason.go:9-11)。

原因码还有一条纪律:在决定阻断的那个分支就地敲定,绝不从人读的错误串反向工程(依据:reason.go:6-9)。为此这套枚举被抽到一个无内部依赖的叶子包 dispatch,让「做决定的 service 层」和「序列化到 wire 的 handler 层」共用一份、永不漂移(handler 侧用 type DispatchReasonCode = dispatch.ReasonCode 别名,server/internal/handler/admission.go:50)。

常见原因码一览(全集见 第 3 章):

原因码含义成功路径?
queued / coalesced / deferred已入队 / 被合并 / 延后
invocation_not_allowed无权触发该目标(私有/不存在,不区分)
runtime_offline目标可运行,但其 runtime 此刻不在线
already_active该目标已有活跃/待处理运行,未合并
attribution_blocked无法定位对运行负责的人(fail-closed 工作区)

5.2 单一判定权威:preview 与真实写入共用 WillEnqueueRun

「这次写入会不会启动运行」这个问题,历史上在四个入口(创建 / 单个指派 / 单个改状态 / 批量)各写了一份,结果漂移了(squad 漏了、self-loop 漏了)。现在收敛成唯一一个谓词 WillEnqueueRun:真实写入路径和 preview 接口调的是同一个函数,前端因此永远不用自己复刻这条规则(依据:server/internal/service/issue_trigger.go:66-89 及 MUL-3375;preview 入口 server/internal/handler/issue_trigger.go:119PreviewIssueTrigger)。这样 preview 说「将启动 N 个运行」永远不会和真实写入的行为对不上。

5.3 乐观更新纪律:只在四条件同时成立时才乐观

前端对服务端状态的更新有一条硬规矩:只有当「结果本地可预测 + 用户停在同屏(不跳转) + 失败很罕见 + 回滚很轻」四者同时成立,才允许乐观更新(典型:改状态/改 assignee/切换字段)。凡是会跳转或需确认的流程(创建、删除、离开),必须等服务端返回再动,绝不乐观地把实体从 cache 里抹掉(依据:仓库根 CLAUDE.md「State Rules」,作为项目明文纪律;落地见 第 5 章)。WebSocket 事件的职责是让 Query cache 失效或打补丁,而不是把服务端数据镜像进客户态(实现见 packages/core/realtime/use-realtime-sync.ts:487-495 的一批 invalidateQueries)。

5.4 多端共享包:依赖方向单向,手机独立

Web 和桌面共享业务逻辑、hooks、stores、组件,靠三个包分层,且依赖方向单向views → core + ui,而 coreui 必须互相独立(依据:CLAUDE.md「Project Shape」明文;结构见 packages/core/packages/ui/packages/views/)。手机端刻意独立:只从 @multica/core 借类型和纯函数,UI/状态/构建/发布节奏全部自己拥有。这条方向纪律是「一处改动、三端复用」不塌方的前提。

5.5 无外键约束的 DB 纪律:关系全在应用层

数据库刻意不建外键、不用级联删除/更新,关系校验和依赖清理全部在应用代码里显式做(需要原子性时用应用事务)。这是为了让 schema 演进和迁移不被外键锁死(依据:CLAUDE.md「Database and Migration Rules」明文硬约束;配套还有「每个索引必须 CREATE INDEX CONCURRENTLY、单独成文件」)。读 SQL 时看不到 REFERENCES 不是遗漏,是纪律。


6. 全库代码地图(总导航表)

一张跳转表:按主题给出关键文件与真实符号名(符号名比行号抗漂移,可直接 grep)。深入某主题时,进对应分章。

主题文件路径(克隆根相对)关键符号
项目对外形态 / 架构图README.md(架构图 :159-177、Features :54-64)
Agent CLI 统一接口server/pkg/agent/agent.goBackend(接口)、ExecuteExecOptionsSupportedTypesNew
各家 CLI 后端实现server/pkg/agent/claude.gocodex.gocursor.goclaudeBackendcodexBackendcursorBackend
「是否启动运行」判定server/internal/service/issue_trigger.goWillEnqueueRunIssueTriggerProbeIssueRunTriggerhasPendingRun
触发的 preview / 写入分发server/internal/handler/issue_trigger.goPreviewIssueTriggerdispatchIssueRunissueTriggerPreviewProbe
入队任务server/internal/service/task.goEnqueueTaskForIssueEnqueueTaskForIssueWithHandoff
Admission 原因码(叶子枚举)server/internal/dispatch/reason.goReasonCodeReasonInvocationNotAllowedReasonRuntimeOfflineReasonAlreadyActive
原因码 wire 序列化server/internal/handler/admission.goDispatchReasonCodewriteDispatchBlockeddispatchBlockedFallbackMessage
daemon 认领任务(HTTP)server/internal/handler/daemon.goClaimTasksByRuntimeClaimTaskByRuntime
daemon 主循环 / 执行server/internal/daemon/daemon.goDaemonhandleTaskrunTaskNew
daemon → 服务端回写server/internal/daemon/client.goStartTaskCompleteTaskFailTask
浏览器 WebSocket hubserver/internal/realtime/hub.goHubHandleWebSocketBroadcastToScopefanoutAll
广播抽象 / scope 常量server/internal/realtime/broadcaster.goBroadcasterScopeWorkspaceScopeTaskScopeDaemonRuntime
多实例扇出(Redis)server/internal/realtime/redis_relay.goStreamKeyNodesKeyHeartbeatKey
daemon WebSocket hubserver/internal/daemonws/hub.goHub(daemon 侧)
HTTP / WS 路由总表server/cmd/server/router.goNewRouterWithOptions(/ws/api/daemon/ws/tasks/{id}/start|complete|fail)
前端 realtime → cache 失效packages/core/realtime/use-realtime-sync.tsuseRealtimeSyncinvalidateQueries(工作区级一批)
API 响应容错解析packages/core/api/schema.tsparseWithFallback
共享业务逻辑 / 客户态packages/core/各域 hooks、TanStack Query keys、Zustand stores
共享业务视图packages/views/Web + 桌面共享页面/组件
平台适配层apps/web/platform/apps/desktop/src/renderer/src/platform/Next.js / react-router 导航接线

本章是导航门面,不含机制实现。任一主题要看真章,请进对应分章;所有引用锚定 commit 8d18d3a