Ctrl K
ComunidadGuías…

docs/plan/technical-design/session-runtime-data.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 会话运行时数据目录(Session Runtime)技术方案

> 版本:v1.0 | 日期:2026-07-06 | 状态:已实现

## 1. 背景与问题

### 1.1 现状

当前平台在**用户工作区**(`Session.workspace` 指向的项目目录)内写入隐藏目录 `.mao/`,主要包括:

| 子目录 / 文件 | 写入方 | 用途 |
|--------------|--------|------|
| `.mao/skills/` | `SkillSyncService`(CLOUD)、Electron `skill-sync`(LOCAL) | Agent 技能同步副本,供 `read_file` 读取 `SKILL.md` |
| `.mao/shellOutput/` | `ShellSessionManager`(CLOUD)、Electron shell 工具(LOCAL) | Shell 超长输出落盘 |
| `.mao/git-askpass.sh` | `ShellSessionManager`(CLOUD,有 Git 凭证时) | Git HTTPS 认证辅助脚本 |

相关代码:

- `backend/.../harness/skill/SkillSyncService.java` — `SKILLS_DIR_NAME = ".mao/skills"`
- `backend/.../harness/shell/ShellSessionManager.java` — `OUTPUT_DIR = ".mao/shellOutput"`
- `backend/.../harness/core/PromptEngine.java` — Prompt 注入 `.mao/skills/...` 相对路径
- `desktop/electron/main.cjs` — LOCAL 模式 skill 解压与 shell 输出落盘

### 1.2 问题

1. **污染用户项目**:LOCAL 模式下工作区往往是真实 Git 仓库,`.mao` 会出现在文件树中,可能被 `git status` 看到。
2. **职责混淆**:Skills 源文件已在 `${app.root-dir}/data/skills` 与 `${app.root-dir}/data/userskills` 维护;工作区内的 `.mao/skills` 只是运行时副本,不应与项目代码混放。
3. **与平台数据目录不一致**:日志、上传、工作区根目录等已统一在 `app.root-dir`(默认 `/opt/mao` 或 `~/.mao`)下分层,`.mao` 进项目目录是历史遗留设计。

### 1.3 目标

将 **skills 同步副本、shell 输出、git-askpass 脚本** 从用户工作区迁出,放入按**会话**隔离的平台运行时目录(Session Runtime),使用户项目目录只保留业务代码。

---

## 2. 核心设计

### 2.1 隔离粒度:按会话(session.id)

本方案中的「工作区」在隔离语义上指**会话**,即使用 `Session.id` 作为 runtime 路径的关键段。

- 每个会话拥有独立的 `skills/`、`shellOutput/` 目录。
- 同一云端共享项目(`projects/{slug}`)下的多个会话,各自维护一份 skills 副本(接受一定磁盘冗余,换取实现简单与会话级隔离)。
- 子会话(`SUBAGENT`、`SIDE_TASK`)使用**自身**的 `session.id`,不复用父会话 runtime。

### 2.2 Skills 的管理维度 vs 运行维度

| 维度 | 说明 |
|------|------|
| **管理维护** | 按 Agent 配置(`agent.skillNames`)+ 用户个人 Skills(`user-skills-dir`) |
| **运行隔离** | 按会话:同步结果写入该会话的 runtime 目录;换 Agent 时通过移除逻辑清理不再需要的 skill |

同步状态键由 `agentId:workspace`(绝对路径)改为 **`agentId:sessionId`**。

### 2.3 目录布局

#### CLOUD(服务端)

```text
${app.harness.runtime-dir}/{userId}/{sessionId}/
├── skills/
│   ├── {skillName}/
│   │   └── SKILL.md
│   └── .sync-manifest.json
├── shellOutput/
│   └── {shellSessionId}_{seq}.out
└── git-askpass.sh          # 按需生成
```

示例:

```text
/opt/mao/data/runtime/1/123/skills/bigdata-cli/SKILL.md
/opt/mao/data/runtime/1/123/shellOutput/sh-123-1700000000000_1.out
/opt/mao/data/runtime/1/123/git-askpass.sh
```

#### LOCAL(桌面端 Electron)

固定根目录,**不做可配置项**:

```text
~/.mao/runtime/{sessionId}/
├── skills/
├── shellOutput/
└── git-askpass.sh          # 若 LOCAL 后续支持 Git 凭证则同理
```

示例:

```text
~/.mao/runtime/123/skills/bigdata-cli/SKILL.md
~/.mao/runtime/123/shellOutput/sh-123-1700000000000_1.out
```

> LOCAL 路径不含 `userId` 前缀:桌面端默认单 OS 用户,与 `~/.mao/logs` 等现有约定一致。

### 2.4 与用户工作区的关系

```text
Session.workspace          → 用户项目目录(代码、Git 仓库),工具 cwd 仍在此
Session Runtime(本方案)  → 平台内部数据,按 session.id 隔离,不在项目树内

CLOUD 示例:
  workspace:  /opt/mao/data/workspace/1/projects/my-app
  runtime:    /opt/mao/data/runtime/1/123/

LOCAL 示例:
  workspace:  /Users/dev/my-repo
  runtime:    ~/.mao/runtime/123/
```

---

## 3. 配置

### 3.1 新增配置项(仅服务端)

```yaml
app:
  harness:
    runtime-dir: ${RUNTIME_DIR:${app.root-dir}/data/runtime}
```

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `app.harness.runtime-dir` | `${app.root-dir}/data/runtime` | CLOUD 模式会话运行时数据根目录 |

LOCAL 端固定为 `path.join(os.homedir(), '.mao', 'runtime')`,不读取 Spring 配置。

### 3.2 与现有目录的关系

```text
${app.root-dir}/
├── data/
│   ├── workspace/      # 用户项目文件(已有)
│   ├── skills/         # 系统 Skills 源(已有)
│   ├── userskills/     # 用户 Skills 源(已有)
│   ├── uploads/        # 上传文件(已有)
│   └── runtime/        # 本会话运行时数据(新增)
└── logs/
```

---

## 4. 路径解析与访问控制

### 4.1 RuntimeDataResolver(新增)

统一封装 runtime 路径计算,供 `SkillSyncService`、`ShellSessionManager`、`PromptEngine` 等调用:

```java
public final class RuntimeDataResolver {

    /** CLOUD: {runtime-dir}/{userId}/{sessionId} */
    public Path resolveSessionRuntimeDir(Long userId, Long sessionId);

    public Path resolveSkillsDir(Long userId, Long sessionId);
    public Path resolveShellOutputDir(Long userId, Long sessionId);
    public Path resolveGitAskpassScript(Long userId, Long sessionId);

    /** LOCAL Prompt 用:~/.mao/runtime/{sessionId}/skills/... */
    public String formatLocalSkillsPath(Long sessionId, String skillName);

    /** CLOUD Prompt 用:绝对路径 */
    public String formatCloudSkillsPath(Long userId, Long sessionId, String skillName);
}
```

Electron 侧实现等价函数(`resolveLocalRuntimeDir(sessionId)` 等)。

### 4.2 PathSandbox 扩展

runtime 目录在用户工作区**之外**,`read_file` / `write_file` 等工具需能访问:

1. 会话开始或首次写入 runtime 时,对该会话的 runtime 根目录调用 `pathSandbox.addAllowedRoot(runtimeDir)`。
2. `resolve()` 对已注册的 `allowedRoots` 保持现有放行逻辑。

### 4.3 Prompt 路径策略

| 模式 | Prompt 中 skills 路径格式 | 说明 |
|------|---------------------------|------|
| **CLOUD** | 服务端绝对路径 | 例:`/opt/mao/data/runtime/1/123/skills/foo/SKILL.md` |
| **LOCAL** | `~` 前缀路径 | 例:`~/.mao/runtime/123/skills/foo/SKILL.md`;Electron `read_file` 须展开 `~` |

Shell 截断返回的 `output_file` 与上述规则一致:

- CLOUD:服务端绝对路径
- LOCAL:`~/.mao/runtime/{sessionId}/shellOutput/{file}`

> LOCAL 不使用服务端绝对路径的原因:Prompt 在服务端生成,无法获知客户端 OS 用户名与家目录。

### 4.4 read_file 工具变更

**服务端(CLOUD)**

- `ReadFileTool` 在 `pathSandbox.resolve()` 前,对以 `~` 开头的路径不做处理(仅 LOCAL 委托场景不涉及服务端读文件)。
- 绝对路径落在已注册的 runtime `allowedRoot` 内则允许读取。

**客户端(LOCAL)**

- `resolveWorkspacePath()` 或 read 工具入口:若路径以 `~` 开头,替换为 `os.homedir()` 后再访问。
- 绝对路径若落在 `~/.mao/runtime/{sessionId}/` 下则允许(不再要求位于 `Session.workspace` 内)。

---

## 5. 各模块改造要点

### 5.1 SkillSyncService

| 项目 | 改前 | 改后 |
|------|------|------|
| 同步目标 | `{workspace}/.mao/skills/` | `{runtime-dir}/{userId}/{sessionId}/skills/` |
| 方法签名 | `syncToWorkspace(agent, workspace, userId)` | 增加 `sessionId` 参数,或传入 `Session` 对象 |
| 状态键 | `agentId:workspace` | `agentId:sessionId` |
| `cleanWorkspace` | 删除工作区内 `.mao/skills` | 重命名为 `cleanSessionRuntime(sessionId)` 或暂不实现(见 §7) |

LOCAL 模式仍通过 `writeSyncZip` 打包;Electron 解压目标改为 `~/.mao/runtime/{sessionId}/skills/`。

### 5.2 ShellSessionManager

| 项目 | 改前 | 改后 |
|------|------|------|
| 输出目录 | `{workspace}/.mao/shellOutput/` | `{runtime-dir}/{userId}/{sessionId}/shellOutput/` |
| git-askpass | `{workspace}/.mao/git-askpass.sh` | `{runtime-dir}/{userId}/{sessionId}/git-askpass.sh` |
| Shell cwd | `Session.workspace` | **不变**,仍在用户项目目录执行命令 |

`createSession` 需增加 `userId`、`sessionId` 参数以定位 runtime 目录。

### 5.3 PromptEngine

- 删除「技能会同步到工作区的 `.mao/skills/`」表述。
- `buildRelativeSkillCatalog` 改为 `buildSkillCatalog`,按执行模式注入绝对路径或 `~` 路径(见 §4.3)。
- 需从 `AgentExecutionContext` 获取 `sessionId`、`userId`、`executionMode`。

### 5.4 StreamingWsHandler

skill 同步调用处传入 `session.getId()`;LOCAL 的 `skill_sync_required` 事件需让客户端知晓 `sessionId`(通常 WS 已绑定 session,IPC 沿用现有 `sessionId` 参数)。

### 5.5 desktop/electron/main.cjs

| 位置 | 改动 |
|------|------|
| `skill-sync` IPC | 解压到 `~/.mao/runtime/{sessionId}/skills/` |
| `truncateAndSave` | 输出写入 `~/.mao/runtime/{sessionId}/shellOutput/` |
| `read_file` 等工具 | 支持 `~` 展开与 runtime 绝对路径 |

### 5.6 配置示例文件

在 `application-example.yml`、`application-acg.yml` 等 profile 中补充:

```yaml
app:
  harness:
    runtime-dir: ${RUNTIME_DIR:${app.root-dir}/data/runtime}
```

---

## 6. 数据流示意

### 6.1 CLOUD — Skills 同步

```text
用户发送消息
  → StreamingWsHandler
    → skillSyncService.syncToSession(agent, userId, sessionId)
      → 拷贝到 /opt/mao/data/runtime/{userId}/{sessionId}/skills/
      → pathSandbox.addAllowedRoot(runtimeDir)
    → PromptEngine 注入 /opt/mao/data/runtime/.../SKILL.md
    → Agent 调用 read_file(绝对路径) → PathSandbox 放行
```

### 6.2 LOCAL — Skills 同步

```text
用户发送消息
  → StreamingWsHandler 触发 skill_sync_required
    → Electron skill-sync(sessionId, ...)
      → 下载 zip → 解压到 ~/.mao/runtime/{sessionId}/skills/
    → PromptEngine 注入 ~/.mao/runtime/{sessionId}/skills/.../SKILL.md
    → Agent 调用 read_file → Electron 展开 ~ 后读取
```

### 6.3 Shell 输出落盘

```text
Shell 命令输出超长
  → 预览 + truncated: true
  → output_file: {runtime}/shellOutput/{shellSessionId}_{seq}.out
  → Agent 可用 read_file 读取完整输出
```

---

## 7. 范围外(本期不做)

| 项 | 说明 |
|----|------|
| **runtime 目录清理** | 会话删除或归档时不级联删除 runtime;后续可加 TTL 或手动清理 |
| **历史 `.mao` 迁移** | 不自动搬迁工作区内已有 `.mao`;新会话走新路径,旧目录可手动删除 |
| **LOCAL git-askpass** | 若 LOCAL 尚无 Git 凭证注入,本期可不改;架构上预留同路径 |

---

## 8. 实现清单

| 序号 | 任务 | 文件 / 模块 |
|------|------|-------------|
| 1 | 新增 `RuntimeDataResolver` | `backend/.../harness/runtime/` |
| 2 | 配置项 `app.harness.runtime-dir` | `application-*.yml` |
| 3 | 改造 Skill 同步与状态键 | `SkillSyncService` |
| 4 | 改造 Shell 输出与 git-askpass | `ShellSessionManager` |
| 5 | 改造 Prompt 技能路径 | `PromptEngine`、`AgentExecutionContext` |
| 6 | 注册 runtime allowedRoot | `StreamingWsHandler` 或会话执行入口 |
| 7 | PathSandbox / ReadFile 绝对路径 | `PathSandbox`、`ReadFileTool` |
| 8 | Electron runtime 路径与 `~` 展开 | `desktop/electron/main.cjs` |
| 9 | 更新相关设计文档交叉引用 | `skill-workspace-sync.md` 等(注明被本方案替代) |

---

## 9. 风险与注意事项

| 风险 | 缓解 |
|------|------|
| 同项目多会话重复同步 Skills | 接受冗余;后续可按需做共享缓存 |
| LOCAL `~` 路径在 Prompt 中不够「绝对」 | 文档与工具层统一约定;Electron 必须实现 `~` 展开 |
| 子会话 runtime 独立 | 子 Agent 需单独 skill 同步;与现有子会话模型一致 |
| 工作区文件浏览器不展示 skills | 预期行为;skills 非项目文件 |

---

## 10. 相关文档

- [Skills 工作区同步方案](./skill-workspace-sync.md) — 旧方案(`.skills/` / `.mao/skills` 在工作区内),**由本文档替代**;其第 11 节记录了下方「本地未同步 Skill」的详细实现
- [Shell 会话系统技术方案](../2026-05-29-shell-session-design.md) — shellOutput 落盘机制
- [用户 Git 凭证设计](../design/user-git-credential-design.md) — git-askpass 脚本(路径将迁至 runtime)
- [云端模式可选工作区](../2026-07-01-cloud-workspace-project-design.md) — `Session.workspace` 与项目共享

---

## 11. 后续演进:LOCAL 模式本地未同步 Skill

在上述 runtime 目录方案基础上,LOCAL 模式进一步支持**无需同步/上传**即可使用桌面端本地 Skill:

- 桌面端「技能管理」抽屉扫描到的 `~/.agents/skills` 下未上传 Skill,会在 LOCAL 模式发送消息时随 WS 消息一并上报(`LocalSkillRegistry`,内存态、按 `sessionId`)。
- `PromptEngine` 为这些 Skill 注入桌面端本机路径(`~/.agents/skills/{folderName}/SKILL.md`),而非本节的 runtime 副本路径;`read_file` 由 `LocalToolExecutor` 委托 Electron 直接读取本机文件,不经过 `SkillSyncService` 的 CLOUD 拷贝 / LOCAL zip 下发流程。
- CLOUD 模式不受影响:仍要求 Skill 已上传(写入 `user-skills-dir`)才能参与 `SkillSyncService.syncToSession`。

详见 [Skills 工作区同步方案 §11](./skill-workspace-sync.md#11-local-模式本地未上传-skill-直接可用无需上传)。