---
title: 项目资源
description: 给项目挂上有类型的指针（GitHub 仓库、本地目录，未来更多），让智能体执行任务时自动拿到对应上下文。
---

**项目资源（Project Resource）** 是挂在 [项目](/workspaces) 上的有类型的指针 —— 今天可以挂 GitHub 仓库 URL 或者本机的一个目录，未来还会有 Notion 页面、文档链接等等。当 [智能体](/agents) 在这个项目的 issue 上跑任务时，守护进程会自动把项目的资源列表写进智能体的工作目录，并塞进它的 [元 skill](/skills) prompt 里。

带来的结果：智能体知道应该 checkout 哪个仓库（或者在哪个本地目录里工作），知道项目"主要参考资料"是哪些 —— 没人需要把上下文复制粘贴进 issue 描述里。

## 心智模型

项目不再只是一个标签，而是一个小的 **资源容器**：

- 一个项目可以挂 0..N 个 **资源**。
- 每个资源有一个 `resource_type`（比如 `github_repo`、`local_directory`），以及一个 `resource_ref`（按 `resource_type` 定型的 JSON 负载）。
- 加新资源类型只需要加一个字符串 + 一个 handler。**不需要 schema 迁移，不需要重写前端。**

这个形状是有意的 —— 跟 Multica 里 agent provider 的设计完全一致：一个 `type` 判别字段 + 一个有类型的 payload。schema 保持稳定，未来加"Notion 页面"、"Google Doc"、"上传文件"、"外部 URL"都是小型的、增量的改动。

目前内置两种资源类型：[`github_repo`](#resource-type-github_repo)（每次任务克隆出独立 worktree）和 [`local_directory`](#resource-type-local_directory)（直接在某台守护进程所在机器的目录里运行）。

## 资源类型：`github_repo`

默认的资源类型 —— 每次任务都在隔离的 worktree 里 checkout：

```json
{
  "resource_type": "github_repo",
  "resource_ref": {
    "url": "https://github.com/owner/repo",
    "default_branch_hint": "main"
  }
}
```

`default_branch_hint` 可选 —— 填了之后，守护进程会把它写进元 skill，告诉智能体该基于哪个分支干活。

## 资源类型：`local_directory`

对于那些不适合每次任务重新 clone 的仓库 —— 几十 GB 的游戏项目、超大 monorepo、或者本身就不想被多次复制的目录 —— 项目可以改为指向 **某台 [守护进程](/daemon-runtimes) 机器上的一个已有目录**。智能体会 **直接在这个目录里运行**，不 clone、不复制、不创建 worktree。

```json
{
  "resource_type": "local_directory",
  "resource_ref": {
    "local_path": "/Users/me/code/big-game",
    "daemon_id": "0001234e-…",
    "label": "主开发目录"
  }
}
```

跟 `github_repo` 相比的取舍是有意的：只有绑定的那台守护进程会使用这个本地目录执行任务，而且 **同一个目录上的任务串行执行**，不再并行。换来的是：你的现有 checkout、当前分支、未提交的脏改动一切照旧 —— Multica 不会重新 clone。

### 什么时候选 `local_directory`、什么时候继续用 `github_repo`

| 关注点 | `github_repo`（worktree 模式） | `local_directory` |
| --- | --- | --- |
| 每次任务的 checkout 成本 | 重新 clone + worktree | 0 —— 智能体原地干活 |
| 同一仓库的并发 | 任意条任务并行 | 同一目录每次一条 |
| 分支 / 脏改动 | 每条任务从默认分支拉一个干净分支 | 用目录当前的状态 |
| 可在哪台机器跑 | 任意守护进程 | 仅绑定的那一台 |
| 磁盘占用 | 每条任务一个 worktree | 0 —— 用你已有的目录 |

下面两种场景下推荐选 `local_directory`：

1. **重新 clone 的成本过高** —— 几十 GB 的游戏 checkout、带大量 LFS 资源的 monorepo、或者任何场景下 `git clone` 的耗时会盖过实际工作量。你拿并发能力换"零 clone"的运行。
2. **改动很细碎，需要频繁在本地 review** —— 你在反复打磨某个组件，几分钟就要切回编辑器看一眼智能体改了什么；与其每次去 `~/multica_workspaces/` 翻一个新 worktree，不如让它就在你已有的 checkout 里干活。

两种情况下你接受的取舍是同一个：**当前版本没有任何文件级的写入锁**。"同一目录上的任务串行执行"这一道闸是唯一防止两个 issue 的智能体同时改同一份文件的保护。如果你把两个 issue 的智能体都指向同一个 `local_directory`，它们的任务会排队、而不是并行 —— 这是有意的。如果需要在同一份代码上真正的并行，请继续用 `github_repo`。

### 添加本地目录

文件夹选择器 **只在桌面端** 提供 —— 浏览器没法读 OS 路径，所以网页端不显示"添加本地目录"按钮。桌面端的流程是：

1. 打开项目 → **Resources（资源）** 面板。
2. 点击 **Add local directory（添加本地目录）**，系统会弹出原生文件夹选择器。
3. 选一个文件夹。这个路径会被绑定到 **当前这台桌面端注册的守护进程** —— 资源记录里同时保存路径和该守护进程的 ID。

在桌面端，当本机的守护进程离线、或者这个项目已经在本机绑定了一个 `local_directory` 时，按钮 **会保留显示但置灰**，并在下方给出一行说明 —— 让你看得到为什么暂时不能用。（网页端则是直接隐藏整个按钮，因为网页端本来就没有文件夹选择器。）要把另一台机器上的目录绑过来，需要在那台机器装上桌面端、并在那边添加资源。

也可以用 CLI（即便环境是纯网页端也行，只要你自己提供守护进程 ID）：

```bash
multica project resource add <project-id> \
  --type local_directory \
  --local-path /Users/me/code/big-game \
  --daemon-id <daemon-uuid> \
  --ref-label "主开发目录"        # 可选

multica project resource update <project-id> <resource-id> \
  --local-path /Users/me/code/big-game-new
```

`--daemon-id` 可以通过 `multica daemon list` 拿到。CLI 也接受 `--ref '<json>'` 这种通用形式，直接传 payload。

### 路径规则

挂资源时和每次任务启动时，守护进程都会校验一次路径（服务器只负责存 JSON，不做校验）。任何一条不满足的，任务会以一个有类型的错误失败，**不会动你的目录**：

- 必须是 **绝对路径**。
- 必须 **存在**，并且是 **目录**（不能是普通文件、设备节点、或者指向文件的 symlink）。
- 守护进程进程必须能 **读 + 写**。
- 不能是系统根目录或整个用户配置区 —— `/`、`/Users`、`/home`、`/root`、`/etc`、`/tmp`、`/var`、`/usr`、`/opt`、`/Users/Shared`、你自己的 `$HOME`、任意 Windows 盘根（`C:\`、`D:\`…），以及 `C:\Users` / `C:\ProgramData` / `C:\Program Files` / `C:\Program Files (x86)` / `C:\Windows`。
- 指向上述任何路径的 symlink 也会被拒；macOS 的 canonical 别名（比如手动输入 `/private/tmp`）和 `/tmp` 受同样的限制。

黑名单故意做得激进 —— 选你自己的 home 目录就意味着 Multica 的运行时文件会出现在你账号的根下，这不会是任何人想要的结果。请选一个子目录（一般就是你具体的项目 checkout）。

### 每台守护进程每个项目最多一条

同一项目上 **每台守护进程最多只能绑一个 `local_directory`**。在同一台机器上想再加一条会被 API 用 `409` 拒绝；桌面端的按钮在达到上限时会自动隐藏，并在 tooltip 里说明原因。

不同的守护进程互不影响 —— 一个共享的项目可以为每个队友的机器各绑一个 `local_directory`，把同一个项目挂到不同主机上的不同目录。守护进程领任务时会挑跟自己 ID 匹配的那一行，其它的忽略。

### 混用资源类型，以及多个 `local_directory` 资源

实际会碰到两种跨资源的组合：

- **同一个项目同时挂 `github_repo` 和 `local_directory`。** 在拥有匹配 `local_directory` 绑定的那台守护进程上，本地目录 **优先**：智能体跑在你的目录里，这次任务不会为项目的 `github_repo` 创建或使用 worktree。（每工作区的仓库缓存可能仍会照常 sync，这层是和具体任务无关的后台行为。）`github_repo` 的 URL 仍然会出现在 `.multica/project/resources.json` 和智能体的 `## Repositories` 段里供参考，但智能体真正在编辑的工作树是你的本地目录，不是 worktree。在 **没有** 匹配 `local_directory` 行的其他守护进程上（不同机器、或者那位队友还没挂本地目录），任务按原本的 `github_repo` worktree 流程走。本质上 `local_directory` 是某台守护进程对 worktree 路径的一个覆盖。
- **同一个项目挂两个 `local_directory`。** 每个 `local_directory` 只能绑一台守护进程，所以这只会发生在两台不同机器之间（同一台机器上想加第二条会被 API 当场拒掉，见上一节）。任务的去向由智能体 runtime 绑定决定，不是由"哪台守护进程有本地目录"决定 —— 任务会落到承载这条智能体 runtime 的那台守护进程上，由它挑出匹配自己 ID 的那一行、忽略其他行。这里没有负载均衡：如果你想让某台机器跑某条任务，请把任务派给绑在那台机器 runtime 上的智能体。

如果某台守护进程没有匹配这个项目的 `local_directory` 行，**不会**因为别处有人绑了就被拦下 —— 它的任务会照常走项目里的其他资源（一般就是 `github_repo` 那条 fallback 路径）。`local_directory` 只对它绑定的那台守护进程生效。

### 任务在本地目录里如何运行

当一条任务被分发到某个 issue、并且该项目在领任务的守护进程上绑了 `local_directory` 时，守护进程会：

1. 再次校验路径（按上面那套规则）。
2. 以 symlink 解析后的真实路径为 key 取一把"目录锁" —— 即便两条路径走不同路由（一条经过 symlink，一条直接）指向同一个文件夹，也会被串行化。
3. 把智能体的 `CLAUDE.md` / `AGENTS.md`（以及 `.multica/project/resources.json`）**写进你的目录**。智能体就在那里工作，跟你自己打开这个文件夹后启动它是一样的体验。
4. Multica 自己的运行时产物（`output/`、`logs/`、`.gc_meta.json`）放在 **你目录之外** 的一个独立 envRoot 里。

如果第一条任务还在跑、第二条针对同一目录的任务又来了，第二条会停在 **Waiting for local directory（等待本地目录释放）** 状态。这个状态在所有相关 UI 都看得见 —— 聊天里的任务状态条、智能体横幅、执行记录、活动指示器；等待中的任务会被计入该智能体的"排队"占用。取消等待中的任务会立即释放它的槽位；取消正在跑的那条会让下一条立即顶上。

等待没有超时 —— 一条等待中的任务会一直等到锁释放、或者被用户/智能体取消为止。

### Multica 会写、不会动什么

- **会写入** `CLAUDE.md` / `AGENTS.md`（或对应 provider 的等价文件）以及 `.multica/project/resources.json` 到目录根 —— 这样智能体才有自己的元 skill 和资源清单。如果不想把这些提交到 git，请加进 `.gitignore`。
- **会写入** 智能体决定做出的所有代码改动 —— 跟你自己在本地跑这个智能体没有区别。
- **永远不会物理删除** 你的目录或里面任何内容。垃圾回收对路径是有判断的：对 `local_directory` 的 envRoot，它只会清自己挂在 `workspacesRoot` 下的 `output/` 与 `logs/`，**完全不碰** 你的目录。

### v1 阶段的限制（后续会逐步收敛）

第一版有意比 `github_repo` 留了更多锋利的边。下面这份清单会随着后续工作逐步缩短；这里列出的是 **今天**的真实行为：

- **不会自动切分支。** 智能体跑在你当前 checkout 的分支上。如果分支选择重要，请在分发任务前自己切换好。
- **不会保护脏工作区，也不会自动 commit。** 未提交的改动智能体看得到、可能就地修改，不会被 stash。请把这个目录当作真实的工作区来对待，重要的运行前自己先 commit。
- **不会自动开 PR。** 任务结束后，改动就停在它实际做出来的分支上 —— Multica 不会 push、不会开 PR。需要 push、需要 PR 的话请自己来。
- **`waiting_local_directory` 只显示状态，不显示是谁占着。** 这个徽标只告诉你"任务在等"，不会告诉你具体是哪条任务、哪个路径正在持有目录。

这些都列在 local-directory 工作的"智能体任务生命周期"后续项里；在那之前，请把 `local_directory` 当作"智能体在你的目录里跑，跟你自己跑没区别"来用。

## 在创建项目时挂仓库

在 **网页端** 或 **桌面端**，打开 *新建项目* 时，Status / Priority / Lead 旁边多了一个 **Repos** 标签。选已绑到工作区的仓库（或者粘贴一个临时 URL），项目创建的瞬间它们就以 `github_repo` 资源的形式挂上去。

通过 **CLI**：

```bash
# 创建 + 挂资源一步到位。资源是在同一个事务里挂上的 ——
# 任何一个资源不合法都会让整个 create 回滚，绝不会出现
# "项目建好了但资源只挂了一半"的状态。
multica project create \
  --title "Agent UX 2026" \
  --repo https://github.com/multica-ai/multica

# 后续管理资源
multica project resource list <project-id>
multica project resource add  <project-id> --type github_repo --url <url>
multica project resource remove <project-id> <resource-id>

# 任何服务器认识的 resource_type 都可以用这个通用入口 ——
# 加新类型时不需要改 CLI：
multica project resource add <project-id> \
  --type notion_page \
  --ref '{"page_id":"…","title":"…"}'
```

`--repo` 可以重复指定；每个值会被当作一条单独的 `github_repo` 资源挂上去。

## 运行时智能体看到什么

当守护进程为一个项目内的 issue 启动智能体时，会发生两件事：

### 1. `.multica/project/resources.json`

API 响应的结构化透传，写进智能体的工作目录里：

```json
{
  "project_id": "…",
  "project_title": "Agent UX 2026",
  "resources": [
    {
      "id": "…",
      "resource_type": "github_repo",
      "resource_ref": {
        "url": "https://github.com/multica-ai/multica",
        "default_branch_hint": "main"
      }
    }
  ]
}
```

Skill、辅助脚本、或者智能体自己想拿到本次运行**精确的**资源列表时，解析这个文件即可。

### 2. 元 skill prompt 里的 "Project Context" 段

智能体的 `CLAUDE.md` / `AGENTS.md`（按 provider 不同）里多出一段人类可读的摘要：

```
## Project Context

This issue belongs to **Agent UX 2026**.

Project resources (also written to `.multica/project/resources.json`):

- **GitHub repo**: https://github.com/multica-ai/multica (default branch: `main`)

Resources are pointers — open them only when relevant to the task. For
`github_repo` resources, use `multica repo checkout <url>` to fetch the code.
```

这段文字故意做得很精简：完整 payload 已经写到磁盘上了，prompt 只是给智能体一个定位 —— 让它知道项目存在、知道挂了什么。

### 失败时

资源拉取是 **best-effort**。如果 API 调失败了，prompt 里的项目段会省掉、文件也不写，但任务照常启动 —— 智能体永远不会因为缺少项目上下文而卡住。

## 加一种新资源类型

抽象的全部目的就是让加新类型变便宜。完整流程：

1. **服务端校验器**（`server/internal/handler/project_resource.go`）—— 在 `validateAndNormalizeResourceRef` 里加一个 case，解析并标准化新的 payload。
2. **守护进程元 skill 格式化器**（`server/internal/daemon/execenv/runtime_config.go`）—— 在 `formatProjectResource` 里加一个 case，让智能体 prompt 把新类型渲染成一条可读的列表项。
3. **TypeScript 类型**（`packages/core/types/project.ts`）—— 给 `ProjectResourceType` 加值，并加上对应的 payload 接口。
4. **UI 渲染**（`packages/views/projects/components/project-resources-section.tsx`）—— 在 `ResourceRow` 里加一个 case 渲染新类型。

**不需要 schema 迁移、不需要新的 sqlc query、不需要新的 endpoint、也不需要改 CLI** —— CLI 的通用 `--ref '<json>'` 选项接受校验器认识的任何 payload，第 0 天就能用。（之后如果想加按类型的 CLI 快捷参数，可以加，但不是必须。）

同一张 `project_resource` 表、同一套 CRUD 调用，处理所有类型。

## 工作区仓库 vs. 项目仓库

智能体看到的仓库列表（`CLAUDE.md` / `AGENTS.md` 里的 `## Repositories` 段）由守护进程的领任务逻辑按以下优先级决定：

- **项目挂了至少一条 `github_repo` 资源** → 仅显示项目挂的仓库。工作区绑定的仓库会被隐藏，避免智能体猜哪一个属于这个 issue。
- **项目没挂 `github_repo` 资源（或 issue 根本不在项目里）** → 回落到工作区的仓库列表，跟以前一样。

这样可以把智能体的工作集压紧：项目把仓库说清楚了，那就是权威答案。而 `.multica/project/resources.json` 里始终带着完整列表，需要查看全部资源的 skill 仍然能拿到。

守护进程在 checkout 这一侧也是配套的：当任务带着项目级别的 `github_repo` URL 进来时，这些 URL 会被合并进每工作区的白名单，并在智能体启动前同步进本地仓库缓存。所以即便某个项目仓库 URL 没被绑到工作区，`multica repo checkout` 也能正常处理它，不会以"未配置"为由拒绝。白名单的划分只是内部实现：工作区绑定的 URL 和任务级别的 URL 分开记录，工作区仓库列表的刷新不会顺手吊销某条项目 URL。

## 当前**不**做的事

- **跨项目共享**。每条资源现在只能挂在一个项目上。
- **按 skill 划定资源可见性**。所有资源对智能体本次运行里的每个 skill 都可见；按类型过滤是后续工作。
- **缓存 / 同步**。`github_repo` 目前只是元数据 —— checkout 仍然按需走 `multica repo checkout`。Notion / Google Docs 之类的文档文本缓存会随对应类型一起到位。

这些是有意的留白 —— 第一版的目标是用最小的活动部件去验证这个抽象。
