# 云端 Skill 同步与执行链路

更新日期：2026-03-26

## 1. 目的

本文档说明 QianjiangClaw 当前已经落地的云端 skill 链路实现，包括：

- desktop 如何从云端拉取 skill
- skill 下载后存放在本地哪里
- UI 如何读取 skill 状态
- 聊天里选中 skill 后如何执行
- 依赖什么 runtime 模式
- 当前已经实现了什么，哪些仍然是限制

这份文档描述的是当前真实代码路径，应作为现阶段实现的基准，而不是早期规划文档。

## 2. 高层概览

当前实际行为如下：

1. Desktop 启动。
2. Electron Main 从云端拉取 `openclaw-employee-config`。
3. 返回的 `skills` 被下载到本地磁盘 `userData/skills`。
4. Main 进程写入本地 `manifest.json`。
5. UI 从这份本地 manifest 派生出的摘要里读取 skill 可用状态。
6. 用户在聊天里选中某个 skill 后，Main 按 `skillId` 找到本地文件。
7. Main 把这个 skill 投影到 OpenClaw runtime 实际使用的 skill 目录，生成一个临时的 `qjclaw-cloud-*` skill。
8. Main 改写即将发送出去的 prompt，明确要求 OpenClaw 使用这个已经安装到本地的 runtime skill。
9. OpenClaw runtime 发现并读取这个本地投影出来的 skill，然后执行它。

重要说明：

- 当前云端 skill 执行只支持 `bundled-runtime` 模式。
- `external-gateway` 模式无法安全假设能访问同一块本地磁盘，所以 Main 会直接拒绝这种本地 skill 执行。

## 3. 主要组件

### 3.1 云端配置客户端

文件：

- `apps/desktop/src/main/services/cloud-api.ts`

职责：

- 拉取 `openclaw-employee-config`
- 校验返回 payload
- 维护 runtime cloud 状态缓存
- 暴露远端 skill 资源元数据
- 在 payload 变化时通知下游

相关行为：

- 启动拉取使用 `init`
- 后台同步使用 `sync`
- payload 更新后会触发下游本地 skill reconcile

### 3.2 本地 skill 存储层

文件：

- `apps/desktop/src/main/services/skill-store.ts`

职责：

- 将下载后的云端 skill 持久化到本地 user data 目录
- 维护 `manifest.json`
- 给 UI 暴露 workspace skill 摘要
- 按 `skillId` 暴露执行目标查询

本地存储结构：

- `userData/skills/manifest.json`
- `userData/skills/<skillId>/<file_name>`

manifest 当前记录这些字段：

- `skillId`
- `bindingId`
- `name`
- `description`
- `category`
- `fileName`
- `fileSize`
- `downloadUrl`
- `localPath`
- `downloadState`
- `lastSyncedAt`
- `lastDownloadedAt`
- `lastError`
- `remoteConfigVersion`

当前状态值：

- `pending`
- `downloading`
- `ready`
- `failed`
- `removed`

### 3.3 Runtime skill bridge

文件：

- `apps/desktop/src/main/services/runtime-skill-bridge.ts`

职责：

- 解析用户选中的本地已下载 skill 文件
- 把它投影成 OpenClaw 可读取的 runtime skill
- 分配一个类似 `qjclaw-cloud-*` 的 runtime skill 名
- 改写 `SKILL.md` frontmatter 里的 `name:`，保证与 runtime skill 名一致
- 清理旧的临时云端 skill

重要实现细节：

runtime bridge 并不是直接从 `userData/skills/...` 执行。

它实际会：

1. 从 `userData/skills/...` 读取已经下载好的本地 skill 文件。
2. 读取 `vendor/openclaw-runtime/runtime-manifest.json`，找到真正的 OpenClaw skill 根目录。
3. 解析 `sourceOpenClawEntry`。
4. 把临时 runtime skill 写入这个真实 OpenClaw skill 目录旁边的 `skills` 目录里。

之所以这样做，是因为当前 OpenClaw runtime 会从它自己实际安装的 skill 目录发现 skill，而不是从 `OPENCLAW_HOME/skills` 读取。

### 3.4 IPC 聊天集成

文件：

- `apps/desktop/src/main/ipc.ts`

职责：

- 为 renderer 构建 workspace 摘要
- 解析执行策略
- 为 gateway 准备 prompt
- 在未选 skill 时清理临时 runtime skills
- 在 `external-gateway` 下拒绝本地 skill 执行

prompt 流程：

- 未选 skill：
  - 清理临时 `qjclaw-cloud-*` skills
  - 发送原始 prompt
- 选中 skill：
  - 确认当前 runtime mode 是 `bundled-runtime`
  - 解析本地已下载 skill
  - 投影 runtime skill
  - 改写 prompt，明确要求 OpenClaw 使用这个已安装的 runtime skill
  - 把改写后的 prompt 发给 gateway

## 4. 端到端数据流

### 4.1 同步链路

1. 应用在 `apps/desktop/src/main/index.ts` 中启动 Main 服务。
2. `OpenClawConfigClient.fetchConfig("init")` 拉取 runtime cloud 配置。
3. `runtimeCloudClient.onPayloadUpdated(...)` 被触发。
4. `skillStore.reconcile(skills, configVersion)` 对比远端 skills 和本地 manifest。
5. 新增或变更的 skill 被下载到 `userData/skills/...`。
6. `manifest.json` 被重写。
7. Renderer 通过 `workspace.getSummary()` 和 `skills.list()` 读取基于 manifest 派生出的摘要。

### 4.2 聊天执行链路

1. Renderer 发送聊天消息和选中的 `skillId`。
2. Main 检查 `skillId`。
3. 如果存在：
   - 通过 `skillStore.getExecutionTarget(skillId)` 找到本地 skill 文件
   - 把它投影到 OpenClaw skill 根目录，生成 `qjclaw-cloud-*`
   - 改写 prompt，要求必须使用这个已安装 skill
4. Gateway 收到改写后的 prompt。
5. OpenClaw runtime 读取这个已安装到本地的 skill 文件，并执行它自己的 skill 逻辑。

## 5. 时间与同步策略

当前真实同步策略：

- 应用启动时执行一次 `init`
- runtime 在 `bundled-runtime` 模式运行时周期性执行 `sync`
- 当前默认后台配置同步间隔是 4 小时

实现位置：

- `apps/desktop/src/main/services/runtime-cloud-supervisor.ts`

当前默认值：

- `DEFAULT_CONFIG_SYNC_INTERVAL_MS = 4 * 60 * 60 * 1000`

说明：

- 这不是每天早上 6 点的任务
- 如果应用没有运行，就不会发生同步
- 仍然可以通过环境变量覆盖这个间隔：
  - `QJCLAW_RUNTIME_CLOUD_CONFIG_SYNC_INTERVAL_MS`

## 6. UI 行为

文件：

- `apps/ui/src/App.tsx`

当前 renderer 行为：

- skill 列表来自 workspace summary
- 只有 `ready === true` 的 skill 能在聊天里被选中
- skill 页面可以展示非 ready 状态
- 默认对话表示没有选中任何云端 skill

这意味着 UI 现在已经由本地可用状态驱动，而不是直接由原始云端 payload 驱动。

## 7. Runtime 模式

### 7.1 Bundled runtime

支持云端 skill 执行。

原因：

- Electron Main、本地下载文件和 OpenClaw runtime 都在同一台机器上
- Main 可以把 runtime skill 投影到 OpenClaw 实际 skill 根目录

### 7.2 External gateway

不支持本地云端 skill 执行。

原因：

- external gateway 可能是远程的，或者与当前机器不共享同一块本地磁盘
- Main 无法保证 external gateway 能读取本地 skill 文件

当前行为：

- 在 `external-gateway` 下，如果用户选中了云端 skill，Main 会直接抛出明确错误

## 8. 清理行为

当前已经有清理逻辑，用来避免临时 runtime skills 残留：

- 启动时清理
- 未选 skill 时清理
- 应用退出前清理

清理目标：

- 仅清理临时的 `qjclaw-cloud-*` skill 目录

不会清理的内容：

- `userData/skills/...` 下的已下载源文件
- manifest 中的记录

这是刻意区分的：

- `userData/skills/...` 是持久化缓存
- `qjclaw-cloud-*` runtime skill 是一次执行期的临时投影

## 9. 真实验证过的行为

已经使用真实员工 `api_key` 做过验证：

- 云端配置拉取成功
- 远端 skills 成功下载到本地磁盘
- manifest 正确更新
- 选中的云端 skill 能被投影到 OpenClaw 真正的 skill 根目录
- OpenClaw 能发现并读取这个投影后的 skill
- 对至少一个真实 skill，renderer 流式链路已经端到端跑通

真实测试中观察到的限制：

- 某些云端 skill 自身还依赖外部前置条件
  - 例如：`tmux`、`ralphy`、ACP runtime
  - 例如：浏览器自动化和登录状态
- 这种情况下，desktop 到 runtime skill 的链路本身是通的，但 skill 自己的运行流程仍可能因为环境问题而失败或卡住

这属于 skill 内容或 runtime 环境问题，不属于云端下载链路问题。

## 10. 当前限制

### 10.1 现在是 prompt 改写，不是协议级 skill 执行

当前实现没有在 gateway 协议里显式传一个专门的 `skillPath` 或 `skillId` 执行契约。

现在依赖的是：

- 先把 runtime skill 投影到本地
- 再通过 prompt 改写，要求 OpenClaw 去使用它

这在当前版本能工作，但不是长期来看最干净的执行契约。

### 10.2 OpenClaw 的 skill 发现路径不在 QianjiangClaw 自己的 userData 里

当前 runtime bridge 需要根据 `runtime-manifest.json`，把 skill 投影到 OpenClaw 自己真正使用的 skill 根目录里。

这意味着：

- 当前执行链依赖 bundled OpenClaw 如何解析 skill 目录
- 如果上游 OpenClaw 后续修改了 skill 发现逻辑，这里的 bridge 可能也要跟着调整

### 10.3 还没有做 skill 前置依赖预检

当前 desktop 链路还没有正式的 skill 依赖预检。

例如：

- `tmux`
- `ralphy`
- ACP runtime backend
- 浏览器自动化登录状态

建议后续补充：

- 对已知依赖做 preflight 校验
- 缺依赖时快速失败并明确提示

## 11. 建议的后续工作

### 11.1 增加协议级 skill 执行契约

可能方向：

- 在 gateway 或 chat 协议中增加结构化的 selected skill 元数据
- 让 runtime 通过正式结构化契约解析本地 skill 执行，而不是继续依赖 prompt 改写

### 11.2 增加 skill 前置检查

例如：

- 依赖工具可用性
- 浏览器自动化可用性
- 需要登录的能力是否就绪
- ACP runtime 是否可用

### 11.3 增加专门的 smoke 覆盖

建议拆成几层：

- cloud config fetch smoke
- 本地下载与 manifest smoke
- runtime materialization smoke
- 针对简单无交互 skill 的端到端执行 smoke

## 12. 关键文件

后续开发重点关注这些文件：

- `apps/desktop/src/main/index.ts`
- `apps/desktop/src/main/ipc.ts`
- `apps/desktop/src/main/services/cloud-api.ts`
- `apps/desktop/src/main/services/skill-store.ts`
- `apps/desktop/src/main/services/skill-client.ts`
- `apps/desktop/src/main/services/runtime-skill-bridge.ts`
- `apps/desktop/src/main/services/runtime-cloud-supervisor.ts`
- `apps/ui/src/App.tsx`
- `packages/shared-types/src/index.ts`

## 13. 一句话理解

云端返回 skill 元数据和下载地址，desktop 把 skill 下载到本地缓存，聊天选中 skill 后，Main 再把这份本地缓存投影到 OpenClaw 真正的 skill 目录里，最后由 bundled runtime 执行这个本地投影出来的 skill。
