news 2026/9/7 4:19:41

opencode 项目级 API 设计解析:让单实例同时驾驭多项目与 Git Worktree

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
opencode 项目级 API 设计解析:让单实例同时驾驭多项目与 Git Worktree

opencode 项目级 API 设计解析:让单实例同时驾驭多项目与 Git Worktree

【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode

在 opencode 的 V2 架构设计中,specs/project.md 定义了一个关键目标:让单个 OpenCode 实例同时为多个项目、以及每个项目的不同 worktree 运行会话(Session)。本文围绕该设计文档展开,完整继承其 API 规划,并结合 core 项目服务、项目数据表 与 worktree 服务 等源码,说明这一设计的落地方式、当前实现状态与关键约束,帮助读者理解 opencode 如何用一套 API 支撑"多项目 + 多 worktree"的工作负载。

设计目标:单实例,多项目,多 worktree

specs/project.md 开篇即给出核心诉求:

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

这意味着:

  • 不再以"一个进程对应一个项目目录"为前提。同一 OpenCode 进程需要维护多个项目的上下文,并为每个项目持有独立的项目级状态(如 VCS 仓库位置、沙箱目录、启动命令等)。
  • worktree 成为一等概念。一个项目(例如一个 Git 仓库)可以同时存在多个 worktree 检出,每个 worktree 都可以承载独立的会话;项目 ID 必须在这些 worktree 之间保持稳定。

理解后续所有 API 的设计动机,都要回到这两点:项目是跨 worktree 的稳定标识,而 session 挂在具体目录(可能是某个 worktree)上运行。

完整 API 规划:project 与 session 两级资源

设计文档中的 API 规划将资源组织成两级:项目级端点与嵌套在/project/:projectID/session/...下的会话级端点。下面按文档原文完整保留并分组说明。

项目级端点

GET /project -> Project[] POST /project/init -> Project
  • GET /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的请求体:directory必填字段,即会话绑定到一个具体目录(可以是主检出,也可以是某个 worktree);idparentID可选,支持调用方指定会话 ID 和父子会话关系。这正对应"每个项目下不同 worktree 各自开会话"的目标。

会话控制端点

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 POST /project/:projectID/session/:sessionID/revert -> Session POST /project/:projectID/session/:sessionID/unrevert -> Session POST /project/:projectID/session/:sessionID/permission/:permissionID -> Session

这一组端点覆盖了会话的初始化、中止、共享开关、上下文压缩、撤销/恢复撤销,以及对具体权限请求(permissionID)的裁决。

消息端点

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[] }

消息以{ info: Message, parts: Part[] }的形态传输,即"消息元信息 + 若干片段(文本、工具调用、文件变更等)"。

文件检索端点

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[]

其他与"awkward"端点

文档还保留了两个全局端点,并明确标注了一批作者自己认为是"awkward(别扭)"的端点:

POST /log // These are awkward GET /provider?directory=<resolve path> -> Provider GET /config?directory=<resolve path> -> Config // think only tui uses this? GET /project/:projectID/agent?directory=<resolve path> -> Agent GET /project/:projectID/find/file?directory=<resolve path> -> File

这些端点需要显式传directory查询参数来定位项目/工作区上下文。文档作者用注释点出了设计上的不满足感(例如config端点"似乎只有 TUI 在用"),这属于设计阶段的自我审视,也为后续演进(如统一的路由中间件)留下了动机。

项目 ID 的解析:从源码看项目如何被识别与保持唯一

API 中反复出现的:projectID,其背后是 packages/core/src/project.ts 中ProjectV2服务的resolve逻辑。从源码结构看,解析优先级如下(见 resolve 实现):

  1. 发现 Git 仓库:对输入目录执行git.repo.discover;若找不到仓库,则返回全局 ID(ID.global),目录回退到文件系统根。
  2. git remote 归一化哈希:读取仓库originremote,把 URL(含user@host:path的 SCP 形式)归一化为host/pathname(小写、去.git后缀、file:协议视为无效),再用ID.make(Hash.fast(git-remote:${normalized}))生成 ID。这样同一远程仓库的所有 worktree/克隆会解析出相同的项目 ID——这正是"不同 worktree 归属同一项目"的关键。
  3. 仓库本地缓存:读取.git公共目录下的opencode文件(cached),这是旧版项目服务持久化的 ID;resolve会同时返回previous(缓存值)与新id,便于旧数据迁移。
  4. 根提交兜底:若既无 remote 也无缓存,则取仓库最早一次提交的 commit hash 作为 ID。

此外,Interface 还声明了一个commit方法,源码注释明确说明它是"临时桥接":由 core 解析 ID,由旧项目服务负责落库;注释还提到旧服务应在新迁移完成后调用commit把 ID 写回仓库本地缓存(即.git/opencode文件),这与上面第 3 步的cached读取形成闭环。

项目 ID 与 VCS 的 Schema 定义在 packages/core/src/project/schema.ts:Vcs目前是一个只含git变体的联合类型({ type: "git", store: AbsolutePath }),store指向 Git 公共目录。

项目数据模型:worktree 是一级字段

packages/core/src/project/sql.ts 中的 SQLite 表结构印证了设计目标中的 worktree 语义:

export const ProjectTable = sqliteTable("project", { id: text().$type<ProjectSchema.ID>().primaryKey(), worktree: DatabasePath.absoluteColumn().notNull(), // 项目"主" worktree 绝对路径 vcs: text(), name: text(), ... sandboxes: DatabasePath.absoluteArrayColumn().notNull(), // 沙箱目录数组 commands: text({ mode: "json" }).$type<{ start?: string }>(), // 启动命令 }) export const ProjectDirectoryTable = sqliteTable( "project_directory", { project_id: ..., // 级联删除外键 directory: DatabasePath.absoluteColumn().notNull(), type: text().$type<"main" | "root" | "git_worktree">(), // 目录角色 strategy: text(), time_created: ..., }, (table) => [primaryKey({ columns: [table.project_id, table.directory] })], )

两个要点:

  • project.worktree是 NOT NULL 的绝对路径,代表该项目的主检出;
  • project_directory表以(project_id, directory)为主键记录项目下的每个已知目录,并用type区分main(主检出)、rootgit_worktree(派生 worktree)。

这与 API 设计中"一个 projectID 下可存在多个会话目录"完全对应:worktree 不是另一个项目,而是同一项目下的一类目录。

当前实现:project 路由与 worktree 实验端点

设计文档描述的是 V2 目标形态,而当前仓库中已存在与之对应的部分实现,可据此判断落地进度。

已实现的 project 路由

packages/opencode/src/server/routes/instance/httpapi/groups/project.ts 定义了以下端点:

方法路径OpenAPI identifier说明
GET/projectproject.list列出用 OpenCode 打开过的项目
GET/project/currentproject.current获取当前活动项目
POST/project/git/initproject.initGit为当前项目创建 git 仓库
PATCH/project/:projectIDproject.update更新 name / icon / commands
GET/project/:projectID/directoriesproject.directories列出项目的已知本地绝对目录

可以看到,设计中的GET /project已实现,而POST /project/init在当前形态下拆成了更具体的POST /project/git/init/:projectID/directories端点则直接对应project_directory表的查询能力(经由 ProjectV2.directories)。该路由组还挂了三类中间件:InstanceContextMiddlewareWorkspaceRoutingMiddlewareAuthorization(见 路由组定义),其中 workspace 路由中间件正是"多项目/多工作区"场景下把请求分发到正确实例上下文的机制。

worktree 服务与实验端点

packages/opencode/src/worktree/index.ts 实现了 worktree 的完整生命周期服务:

  • Info结构为{ name, branch?, directory }(定义见 L23-L28);
  • create接受可选namestartCommand("在项目启动命令之后追加运行的启动脚本"),非 Git 目录、名称生成失败、启动命令失败等均有独立错误类型(NotGitErrorNameGenerationFailedErrorStartCommandFailedError等);
  • remove按目录移除 worktree,reset将 worktree 分支重置回主默认分支。

对应的 HTTP 端点位于 实验路由组:

GET /experimental/worktree # 列出项目所有 sandbox worktree POST /experimental/worktree # 创建 git worktree 并运行配置的启动脚本 DELETE /experimental/worktree # 移除 worktree 并删除其分支 POST /experimental/worktree/reset # 将 worktree 分支重置回主默认分支

注意其 OpenAPI 描述措辞:"List allsandbox worktreesfor thecurrent project"——这与project表中的sandboxes字段和project_directory表的git_worktree类型相互印证。worktree 相关能力目前仍在experimental命名空间下,说明该部分属于渐进开放中。

会话执行如何支撑"多项目单实例"

项目 API 的最终服务对象是会话。specs/v2/session.md 中给出的执行路由,解释了单实例如何为不同项目的会话正确分发执行:

SessionExecution.resume(sessionID) -> SessionStore.get(sessionID) -> LocationServiceMap.get(session.location) -> SessionRunner.run({ sessionID, force? })

从源码结构看,SessionExecution与读侧SessionStore是进程全局的,而SessionRunner、catalog、模型解析器、工具注册表、权限状态和文件系统都按Location(即会话所属的项目位置/目录)缓存;任何一层都不接收 Session ID 作为入口参数。这意味着:同一个进程内,不同项目、不同 worktree 的会话可以并发运行,而它们各自命中自己 Location 的服务实例,互不串扰。会话创建时的directory(如前文POST .../session请求体所要求)就是会话定位到具体 worktree 的依据。

设计状态小结:哪些已落地,哪些是规划

综合 specs/project.md 与当前源码,可以这样把握该设计的成熟度:

  • 已落地GET /project列表、/project/current/project/git/initPATCH /project/:projectID/project/:projectID/directories;项目 ID 的多级解析(remote 哈希 → 本地缓存 → 根提交);project/project_directory数据表及 worktree 类型枚举;worktree 的创建/删除/重置服务与实验端点;基于 Location 的多会话执行路由。
  • 规划/部分落地:文档中POST /project/init/project/:projectID/session/...这一整套嵌套会话端点,与当前仓库中以 Location/Session 独立路由为主的形态存在差异,属于设计文档描述的目标 API 形态;作者自注"awkward"的?directory=<resolve path>端点体现了该层 API 仍在收敛过程中。
  • 适用前提:worktree 能力仅对 Git 仓库项目有效(非 Git 目录会触发WorktreeNotGitError);remote 归一化依赖originremote 可解析(file:协议 remote 会退化为缓存或根提交策略);experimental命名空间下的端点语义可能随迭代调整。

通过这份设计文档与其源码实现的对照,可以看到 opencode 用"稳定的项目 ID + 目录角色表 + Location 级服务缓存 + worktree 生命周期端点"这条链路,把"一个实例服务多个项目、每个项目多 worktree"从一句目标拆解成了可验证的 API 契约与数据结构。

【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode

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

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

MiniSQL源码解析:从SQL解析到B+树索引的数据库内核入门

简介&#xff1a;一套基于C的MiniSQL数据库管理系统完整源码&#xff0c;参考CMU15445的BusTub框架并进行修改扩展&#xff0c;兼容原MiniSQL实验指导要求&#xff0c;面向数据库原理课程设计、实验或自学数据库内核的开发者。系统实现了缓冲池管理、B树索引、记录管理等核心模…

作者头像 李华
网站建设 2026/9/7 4:14:34

输入治理实战:从JSON反序列化到Vue事件,一套方案搞定Input难题

做接口和前端交互时间久了&#xff0c;你会发现一个特别反直觉的现象&#xff1a;真正把系统搞挂的&#xff0c;往往不是业务逻辑多复杂&#xff0c;而是“输入”这一关没守住。我一直在维护一个叫Lyra6-Input的内部输入处理项目&#xff0c;名字听起来像某个硬件型号&#xff…

作者头像 李华
网站建设 2026/9/7 4:14:16

波士顿房价数据集解析:从嵌套ZIP解压到回归建模实战

简介&#xff1a;这是经典的波士顿房价回归数据集配套压缩包&#xff0c;面向机器学习初学者、数据建模人员及高校相关课程学生&#xff0c;适用于房价预测、特征相关性分析和回归算法教学实践等场景。包内共3个文件&#xff0c;涵盖CSV格式的房屋样本数据、Python数据处理与建…

作者头像 李华