news 2026/9/13 6:58:00

KiloCode 多项目 API 设计:单实例如何同时服务多个项目与 Worktree

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KiloCode 多项目 API 设计:单实例如何同时服务多个项目与 Worktree

KiloCode 多项目 API 设计:单实例如何同时服务多个项目与 Worktree

【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode

本文基于仓库中的设计规范 specs/project.md,讲解 Kilo(opencode 包)如何设计 HTTP API,让同一个服务端实例为多个项目、每个项目的多个 worktree 并发运行会话。读完本篇,你会理解规范中列出的完整端点清单、请求通过directory参数路由到正确项目实例的中间件机制,以及Project服务在解析、落库与迁移上的源码级实现。

一、设计目标:一个实例,多项目,多 worktree

specs/project.md 开篇即给出核心目标:

The goal is to let a single instance of OpenCode run sessions for multiple projects and different worktrees per project.

即:启动一个 Kilo/opencode 服务进程后,客户端不必为每个项目分别拉起服务,而是通过 API 参数声明“当前要操作哪个项目、哪个工作目录”,由服务端在内存中维护每个目录对应的运行时实例(实例上下文)。这带来两个关键概念:

  • 项目(Project):以版本库(主要是 git 仓库)为边界的逻辑单元,拥有唯一id、主worktree路径、VCS 类型、图标、自定义命令等属性;
  • Worktree / Sandbox:同一个项目的多个检出目录。规范标题里 “different worktrees per project” 就是指:一个项目可以同时有多个 checkout,会话可以落在其中任意一个目录。

从源码结构看,这两个概念直接对应Project服务的接口定义(见 project.ts):

readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }> readonly discover: (input: Info) => Effect.Effect<void> readonly list: () => Effect.Effect<Info[]> readonly get: (id: ProjectV2.ID) => Effect.Effect<Info | undefined> readonly update: (input: UpdateInput) => Effect.Effect<Info, NotFoundError> readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect<Info> readonly sandboxes: (id: ProjectV2.ID) => Effect.Effect<string[]> readonly addSandbox: (id: ProjectV2.ID, directory: string) => Effect.Effect<void> readonly removeSandbox: (id: ProjectV2.ID, directory: string) => Effect.Effect<void>

其中fromDirectory是核心入口:给定任意一个目录,返回它所属的project以及该目录下实际生效的沙箱目录sandboxsandboxes字段(持久化在ProjectTable.sandboxes)就是“一个项目多个 worktree”的落地形式——凡是归属于该项目、但不等于主 worktree 的检出目录,都会被追加进sandboxes列表(project.ts)。

二、规范中的完整端点清单

specs/project.md 给出的 API 草案如下(原样继承,后文逐一说明其在当前仓库中的演进状态):

GET /project -> Project[] POST /project/init -> Project GET /project/:projectID/session -> Session[] GET /project/:projectID/session/:sessionID -> Session POST /project/:projectID/session -> Session { id?: string parentID?: string directory: string } DELETE /project/:projectID/session/:sessionID POST /project/:projectID/session/:sessionID/init POST /project/:projectID/session/:sessionID/abort POST /project/:projectID/session/:sessionID/share DELETE /project/:projectID/session/:sessionID/share POST /project/:projectID/session/:sessionID/compact GET /project/:projectID/session/:sessionID/message -> { info: Message, parts: Part[] }[] GET /project/:projectID/session/:sessionID/message/:messageID -> { info: Message, parts: Part[] } POST /project/:projectID/session/:sessionID/message -> { info: Message, parts: Part[] } POST /project/:projectID/session/:sessionID/revert -> Session POST /project/:projectID/session/:sessionID/unrevert -> Session POST /project/:projectID/session/:sessionID/permission/:permissionID -> Session GET /project/:projectID/session/:sessionID/find/file -> string[] GET /project/:projectID/session/:sessionID/file -> { type: "raw" | "patch", content: string } GET /project/:projectID/session/:sessionID/file/status -> File[] POST /log // These are awkward GET /provider?directory=<resolve path> -> Provider GET /config?directory=<resolve path> -> Config GET /project/:projectID/agent?directory=<resolve path> -> Agent GET /project/:projectID/find/file?directory=<resolve path> -> File

规范的设计意图很清晰:会话及其一切子资源(消息、文件、权限、共享、压缩)都挂在projectID路径下,用 URL 显式表达“会话属于哪个项目”;创建会话的 body 里还带有directory字段,用于指定会话落在该项目的哪个 worktree。

规范作者自己也标注了尾部四个?directory=<resolve path>端点 “are awkward”——因为directory是路径参数之外的查询参数,语义上不如直接放在 URL 里自然。当前仓库的最终取舍(下文第三节详述)恰恰是反过来的:把directory统一提升为全局查询参数,会话则收敛到独立的/session根路径。

三、directory参数:请求如何被路由到正确的项目

当前实现中,所有需要“知道自己在哪个目录”的端点都继承同一套查询参数。定义见 workspace-routing.ts:

export const WorkspaceRoutingQueryFields = { directory: Schema.optional(Schema.String), workspace: Schema.optional(Schema.String), } export const WorkspaceRoutingQuery = Schema.Struct(WorkspaceRoutingQueryFields)

WorkspaceRoutingMiddleware是路由的第一层。它先为请求计算“计划”(RequestPlan:本地执行还是代理到远端 workspace),本地场景下工作目录的解析优先级为(workspace-routing.ts):

  1. URL 查询参数?directory=
  2. 请求头x-kilo-directory
  3. 进程当前工作目录process.cwd()兜底。

此外还有两个与“多项目/多工作区”直接相关的机制:

  • workspace参数与远端代理:请求携带workspace参数且对应 workspace 的目标是 remote 时,中间件不会本地执行,而是把请求(含 WebSocket)代理到远端目标地址,并在同步栅栏(Fence)上等待数据一致后才放行,见 workspace-routing.ts;
  • fork 目录覆盖(Kilo 的定制,源码中以kilocode_change注释标记):当请求是 fork 会话且带有显式目标目录(例如 fork 到某个 worktree)时,目标目录优先于源会话的目录继承,见 workspace-routing.ts。

也就是说,规范里被批评为 awkward 的?directory=<resolve path>模式被保留了下来,但不再零散地附着在个别端点上,而是成为整个实例路由的统一约定:项目类端点挂WorkspaceRoutingQuery,会话类端点(ListQueryMessagesQuery等,见 session.ts)同样展开这套字段。规范中的GET /provider?directory=...GET /config?directory=...对应的 provider、config 端点也依旧存在于同一 HttpApi 分组目录下(groups/provider.ts、groups/config.ts)。

四、InstanceContextMiddleware 与 InstanceStore:按目录维护实例生命周期

目录解析完成后,instance-context.ts 负责把目录变成完整的运行时上下文:

const route = yield* WorkspaceRouteContext const ctx = yield* store.load({ directory: decode(route.directory) }) return yield* effect.pipe( Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, route.workspaceID), )

这里InstanceStore(instance-store.ts)对外暴露load / reload / dispose / disposeDirectory / disposeAll / provide六个操作,内部用Map<directory, Entry>做实例缓存。load未命中时执行boot(instance-store.ts):

  • 若调用方已显式给出projectworktree(例如 reload 场景),直接使用;
  • 否则调用project.fromDirectory(input.directory)(见第五节),用返回的sandbox作为该实例的 worktree;
  • 随后在InstanceRef作用域内运行InstanceBootstrap(Kilo 定制:把 bootstrap 放进 Instance ALS 中执行,保证 fork 出的子逻辑能看到Instance.directory);
  • 整个 boot 用Deferred收敛:同一目录的并发请求共享同一次启动;且无论成功还是失败(含 fiber 被中断),缓存项都会被清理或正确完成,避免“卡死在启动中导致后续请求永远挂起”的问题(源码注释明确说明了这一动机)。

实例被回收时,InstanceStore会运行该目录注册的 disposer 并广播server.instance.disposed事件(instance-store.ts)。至此,“单实例多项目”的内存模型完整成立:一个进程 = 一个 InstanceStore = 若干以目录为键的实例上下文,每个上下文内持有该目录的项目信息与各类服务状态。

实例路由的中间件链顺序在 groups/project.ts 中固定为:

.middleware(InstanceContextMiddleware) .middleware(WorkspaceRoutingMiddleware) .middleware(Authorization)

即先解析 workspace/目录,再装载实例上下文,最后做鉴权。

五、Project 服务:目录解析、落库与 ID 迁移

Project.fromDirectory是“任意目录 → 项目”的核心实现(project.ts),流程如下:

  1. 解析projectV2.resolve(absolutePath)向上查找版本库根,得到{ id, directory, vcs, previous };全局(无 VCS)且非项目 id 时 worktree 取"/"
  2. ID 迁移:若解析出新的项目 id 与历史 id 不同(典型场景:仓库根变化或首次识别到 VCS),migrateProjectId在单个数据库事务中复制项目行、清空ProjectDirectoryTable旧目录列表、把SessionTableWorkspaceTable中挂旧 id 的记录整体改挂新 id,最后删除旧行(project.ts)。这保证了“同一项目 ID 变化时,历史会话与工作区不会丢失归属”;
  3. 沙箱归集:当前目录若不属于ProjectV2.ID.global、不等于主 worktree、且不在sandboxes中,就追加进去;随后过滤掉磁盘上已不存在的沙箱路径,再upsertProjectTable
  4. 会话归属修正:将project_idglobaldirectory匹配当前目录的会话批量改挂到新项目(project.ts);
  5. 目录登记与事件saveProjectDirectory写入项目-目录关联表(global 项目跳过);emitUpdated通过GlobalBus广播project.updated事件(project.ts),前端/插件即可感知项目信息变化;
  6. VCS 快照:git 项目还会调用projectV2.commit更新 VCS 存储(project.ts)。

返回的{ project, sandbox }sandbox的选择规则值得注意:有 VCS 时取实际检出目录,无 VCS 时取 worktree(project.ts)。

其余接口要点:

  • initGit(project.ts):先确认系统装有 git(which("git")),执行git init --quiet,再重新走一遍fromDirectory返回刷新后的项目信息。这对应规范中的POST /project/init(当前 HttpApi 中端点名为project.initGit,路径POST /project/git/init);
  • update:修改nameiconcommands,未命中返回Project.NotFoundError(project.ts);
  • 图标发现:开启experimentalIconDiscovery标志后,discover会在 worktree 内 glob**/favicon.{ico,png,svg,jpg,jpeg,webp},取路径最短的一个转成 data URL 写回项目图标(project.ts)。

六、当前实验性 HttpApi 中的落地形态

规范草案按“项目挂会话”的方式组织路由;当前仓库的实验性 HttpApi(packages/opencode/src/server/routes/instance/httpapi/)落地时做了结构调整:

项目端点(groups/project.ts):

端点规范对应说明
GET /projectProject[]GET /project列出所有已打开的项目(project.list
GET /project/current无(演进新增)返回当前实例上下文中的项目(project.current
POST /project/git/initPOST /project/init初始化 git 并返回刷新后的项目
PATCH /project/:projectID演进新增更新name/icon/commands
GET /project/:projectID/directories演进新增列出该项目已知的所有本地绝对目录

处理逻辑在 handlers/project.ts 中。其中initGit有一个细节值得注意:当 git 初始化导致项目idvcsworktree发生变化时,handler 会调用markInstanceForReload标记当前实例需要重载(handlers/project.ts),让后续请求拿到刷新后的实例上下文。

会话端点则从/project/:projectID/session/*收敛为独立的/session根路径(session.ts),规范中列出的会话子资源大多可以找到对应项:

规范端点当前对应
POST /project/:projectID/session(body 含directoryPOST /session,目录由WorkspaceRoutingQuery携带
GET/DELETE .../session/:sessionIDGET /session/:sessionIDDELETE /session/:sessionID
.../abort.../share(POST/DELETE)POST /session/:sessionID/abortPOST /session/:sessionID/share
.../message(GET 列表 / GET 单条 / POST 提交)GET /session/:sessionID/messageGET .../message/:messageIDPOST /session/:sessionID/message
.../revert.../unrevertPOST /session/:sessionID/revertPOST /session/:sessionID/unrevert
.../permission/:permissionIDPOST /session/:sessionID/permissions/:permissionID
会话列表GET /session,查询参数支持scope: "project"按项目过滤(session.ts)

规范里没有、当前实现中额外提供的相关端点包括forksummarize(对应规范compact语义的演进)、commandshelltododiffstatus等;abort还新增了scope: session | tree参数,可选择只停当前会话还是连同子代理整树停止(session.ts)。文件类端点(规范中的find/filefilefile/status)由同一 HttpApi 分组目录下的 file 端点组承担(groups/file.ts)。

七、实践要点:如何用 API 操作多项目

结合前述机制,调用方在操作不同项目时的实际用法是:

  1. 对需要区分项目的请求,统一携带?directory=<absolute path>(或x-kilo-directory请求头);服务端据此解析项目并装载对应实例上下文,目录缺省时回退到进程 cwd;
  2. 需要跨 workspace(含远端同步的工作区)操作时追加?workspace=<workspaceID>;远端目标由中间件自动代理;
  3. 会话相关操作走/session/*端点,同一 URL 模板可服务任意项目,区分维度完全由directory参数表达;按项目筛选会话时用scope=project
  4. 项目元数据(名称、图标、命令)用PATCH /project/:projectID更新;目录与项目的关联关系可用GET /project/:projectID/directories查询;
  5. 所有事件广播(project.updatedserver.instance.disposed等)都带有projectdirectory维度,客户端可按项目订阅过滤。

需要注意的适用前提:以上HttpApi路由在源码中明确标注为 Experimental 表面(groups/project.ts 的 OpenAPI 描述为 “Experimental HttpApi surface for selected instance routes”),端点名称与路径可能随版本演进调整,集成时建议以仓库中当前的分组定义与 OpenAPI 注解为准。

参考文件

  • 设计规范:specs/project.md
  • 项目端点定义与 OpenAPI 注解:groups/project.ts
  • 项目端点处理逻辑:handlers/project.ts
  • 目录/workspace 路由中间件:middleware/workspace-routing.ts
  • 实例上下文中间件:middleware/instance-context.ts
  • 项目服务(解析、迁移、initGit、图标发现):project/project.ts
  • 实例缓存与生命周期:project/instance-store.ts
  • 会话端点定义:groups/session.ts

【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:55:06

OPC UA如何兼容MCP与Rules实现工业AI集成

1. 这不是一场“选边站”&#xff0c;而是一场接口标准的生存博弈最近在几个工业自动化工程师群和AI工具开发者频道里&#xff0c;几乎每天都能刷到类似标题的讨论&#xff1a;“Skills广场、MCP协议、Rules规范……AI编码工具的生态战争已经打响&#xff0c;OPC该怎么选边&…

作者头像 李华
网站建设 2026/9/13 6:53:29

基于Seq2Seq深度学习的单通道EEG睡眠分期系统

1. 项目概述&#xff1a;基于序列到序列深度学习的自动睡眠阶段评分系统睡眠质量监测在现代健康管理中扮演着越来越重要的角色。作为一名长期关注医疗AI应用的开发者&#xff0c;我发现传统睡眠监测存在两个核心痛点&#xff1a;专业多导睡眠图(PSG)检查需要住院且费用高昂&…

作者头像 李华
网站建设 2026/9/13 6:52:17

text-to-CAD技术解析:从工程语义到STEP文件的工业落地路径

1. 什么是text-to-cad&#xff1a;不是“文字变图纸”的魔法&#xff0c;而是工程语义落地的硬核桥梁 你搜“text-to-cad”时&#xff0c;看到的大多是零散提问&#xff1a;cad下载、cad画直线显示2.1616e、solidworks导入step、cad标注卡住……这些看似琐碎的问题&#xff0c;…

作者头像 李华
网站建设 2026/9/13 6:51:55

VBA事件编程实战:Excel自动化进阶指南

1. VBA事件编程入门&#xff1a;从手动到自动的蜕变在Excel办公自动化领域&#xff0c;VBA&#xff08;Visual Basic for Applications&#xff09;一直是提升效率的利器。但很多初学者止步于录制宏和手动执行代码的阶段&#xff0c;殊不知VBA事件机制才是实现真正自动化的钥匙…

作者头像 李华
网站建设 2026/9/13 6:49:54

Maven安装与配置详解:环境变量、阿里云镜像及IDEA集成避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华