🎉 TJUClaw 0.1 已发布:智能体可接入 MCP 服务与技能,查看更新日志 →
TJUClaw 2026

系统架构:知识工作区、代码工作区与受控执行

TJUClaw 从跨端客户端、Go API、知识库、Forgejo 代码工作区到加密附件与隔离沙箱的整体架构说明。

系统架构:知识工作区、代码工作区与受控执行

TJUClaw 的核心不是一个把所有东西都塞进聊天框的机器人。

用户在这里整理笔记、保存资料、配置智能体;智能体在需要时读取被授权的内容,进入隔离环境执行代码,再把可检查的结果交回来。不同类型的数据走不同的路径,权限也在每一层重新确认。

这件事听起来没有那么热闹,却很重要:正文、文件、代码历史、搜索索引和执行环境不能互相冒充。

一张图看懂

┌──────────────────────────────────────────────────────────────┐
│ Web / Desktop / Mobile                                       │
│ React + Vite + Tauri                                         │
│                                                              │
│ 知识树 · Markdown 笔记 · Agent 会话 · 文件入口 · 设置         │
└──────────────────────────────┬───────────────────────────────┘
                               │ 同源 HTTPS /api/*
                               ▼
┌──────────────────────────────────────────────────────────────┐
│ TJUClaw API                                                  │
│ Go stdlib net/http                                           │
│                                                              │
│ 身份校验 · 所有权 · 知识库 · 会话 · 模型网关 · Run 编排        │
└───────────────┬──────────────────┬───────────────────────────┘
                │                  │
                │                  ├──────────────► 模型服务
                │                  │                公网 HTTPS / 受控网关
                │                  │
                │                  ├──────────────► WeKnora
                │                  │                派生检索索引
                │                  │
                │                  └──────────────► Sandbox Controller
                │                                   临时密钥 · Agent · 工具
                │
                ├──────────────► PostgreSQL
                │                账户归属 · 条目树 · 会话 · 发布关系
                │
                ├──────────────► R2
                │                附件密文 · Git LFS 对象 · 交付物
                │
                └──────────────► Forgejo
                                 代码工作区 · commit · checkpoint

边缘网络只负责把客户端和 API 接到正确的入口。它不拥有用户身份,也不替 API 做资源授权。

先把三类数据分开

知识工作区

知识工作区是用户每天打开的产品界面。它有一棵条目树,笔记、智能体、工作环境说明和文件入口都可以放在同一棵树里。

条目用途权威数据
笔记Markdown 正文、标题和层级关系产品 PostgreSQL
智能体人设、工具范围、知识范围和会话入口条目记录与会话记录
工作环境远程主机或执行环境的说明书条目元数据
文件图片、课件、代码包等二进制资料条目元数据 + R2 对象

条目的 parent_id 决定用户看到的路径。对象存储的 object key、数据库行号和内部 blob ID 都不应该变成用户界面里的路径。

笔记正文保持为普通 Markdown。图片和二进制资料不写成 data URI,也不把对象存储地址硬编码进正文;正文只保留稳定的引用关系。

代码工作区

代码工作区是给真实 shell 和 Agent 使用的文件空间。它以 Forgejo Git 仓库为权威,而不是以某个沙箱实例的本地磁盘为权威。

一次执行至少绑定以下信息:

  • 用户和知识库归属;
  • 服务器选定的仓库;
  • 完整的 commit SHA;
  • 目标分支;
  • 是否允许写回;
  • Agent Profile、资源预算和截止时间。

沙箱可以拥有工作树,但不能自行选择仓库 URL、分支或能力。所有写回都通过受控的 commit-and-push 路径完成。

派生索引

全文索引、模糊搜索索引、Embedding、摘要和缩略图都是派生数据。它们可以被重建、替换和删除,不能反过来成为正文或权限的来源。

这条规则也适用于 WeKnora:

WeKnora 负责召回,不负责产品身份、知识库 ACL 或正文保存。

客户端:一个工作区,多种入口

客户端使用 React 组件和共享状态逻辑构建 Web、桌面与移动端。Tauri 负责原生宿主,Web 端和原生端复用同一套产品界面。

客户端有几条明确的边界:

  1. 浏览器只请求同源 /api/*,不直接连接数据库、Forgejo、R2 私有管理接口或 WeKnora。
  2. 认证凭据由浏览器自动携带 HttpOnly Cookie,JavaScript 不读取会话 Token。
  3. 条目树、笔记编辑器和会话页面只展示服务端授权后的投影。
  4. 外观、侧栏排序、标签页和设备偏好属于客户端状态,不冒充账号级业务数据。
  5. 编辑器只修改原始 Markdown,不为了预览而重写用户正文。

工作区的主流程是:

登录
  ↓
读取当前身份可以看到的知识库
  ↓
展开同一棵条目树
  ↓
打开笔记,或进入某个智能体条目的会话
  ↓
保存正文 / 发送消息 / 请求一次受控执行

智能体不是第二套文件树。它看到的条目范围、能否写入,以及是否可以进入代码工作区,都由服务端授权结果决定。

API:身份和所有权的唯一裁判

Go API 使用标准库 net/http 组织路由。它不是模型推理服务,也不应该成为用户代码的执行主机。

API 主要负责五件事:

  • 验证浏览器会话;
  • 根据认证源返回的 identity 判定资源归属;
  • 读写知识库、条目、会话、发布快照和模型配置;
  • 把受控请求交给模型网关、检索服务或沙箱控制面;
  • 记录 Run 状态、产物引用和清理结果。

同源认证

Kratos 是浏览器身份的权威来源。API 通过受保护的同源接口完成邮箱验证码、会话读取和退出;浏览器不会保存可读的 Bearer Token。

所有受保护请求都经过同一条检查链:

Cookie
  ↓
同源与 Origin 校验
  ↓
向身份服务确认 Session
  ↓
检查会话有效期与邮箱验证状态
  ↓
使用 identity.id 作为 owner_id
  ↓
读取或修改资源

客户端传来的 user_id、owner_id 或类似字段不能改变归属。访问别人的私有资源时,API 使用与“资源不存在”一致的响应,避免通过状态码差异枚举数据。

存储分层

产品 PostgreSQL 保存需要事务和关系约束的数据:

  • 用户可见的知识库和条目;
  • 条目树的父子关系;
  • Agent 会话与消息;
  • 模型配置和产品额度;
  • 发布快照、撤回状态和只读接入关系;
  • 文件对象的元数据。

文件字节不应因为“方便”就塞进每一次 API 响应。小规模本地开发可以使用 FileStore;生产环境使用 PostgreSQL 保存元数据,并由对象存储承载大文件。

存储驱动可以更换,但业务契约不能跟着环境变化。FileStore 和 PostgreSQL 都必须遵守相同的归属、只读快照、路径和大小限制。

稳定的错误边界

API 向客户端返回稳定的机器错误码,例如:

{
  "error": {
    "id": "library_read_only"
  }
}

上游模型、数据库、Git、对象存储和 Kubernetes 的内部错误不直接穿过浏览器。日志也不记录 Cookie、授权头、验证码、模型 Key、完整 Prompt 或附件正文。

模型网关:让模型能工作,但不能变成任意代理

用户可以配置一个 OpenAI 兼容的公网 HTTPS 上游,也可以使用产品提供的模型额度。

API 在保存和使用自定义模型时都要验证:

  • 只接受 HTTPS;
  • 拒绝回环、RFC1918、链路本地、云元数据和内部域名;
  • 校验 DNS 解析结果,防止域名解析到受保护网段;
  • 限制请求体、响应体、超时和重定向;
  • 不把 API Key 返回客户端,也不写入条目、会话或模型上下文。

这个网关只支持产品需要的模型协议。它不是一个让调用者自由拼接目标地址的 HTTP 代理。

Agent 工具调用同样经过服务端定义的工具面。第一层工具可以只操作当前知识库的条目:列出树、创建条目、改标题、改 Markdown、移动和删除。工具结果回到会话历史,但不会把内部凭据或对象 key 交给模型。

代码执行:Forgejo 是历史,沙箱是一次工作树

当一次会话需要真实 shell、编译器或大量文件时,API 不在自己的进程里执行命令,而是把已经授权的请求交给沙箱控制面。

启动顺序

API 根据身份和会话确定 workspace
  ↓
解析固定 Forgejo 仓库与最新允许 commit
  ↓
创建 Run,绑定 Profile、资源、期限和工具范围
  ↓
沙箱创建隔离执行边界
  ↓
拉取指定 commit,校验 origin 与 HEAD
  ↓
注入必要的短期能力
  ↓
启动 Pi 和真实 shell
  ↓
收集 stdout、stderr、产物清单和资源用量
  ↓
可写工作区按步骤 checkpoint commit 并 push
  ↓
清理沙箱和临时凭据

沙箱工作区使用固定布局:

/workspace/
├── repo/      # 指定 Forgejo commit 的代码工作树
├── inputs/    # 任务输入,只读
├── scratch/   # 依赖和中间文件,执行结束丢弃
└── outputs/   # 可收集交付物,其他路径不进入产物

/workspace/repo 可以被 Agent 修改,但每一步有意义的变更都应该尽快形成可追踪的 checkpoint。不能把未推送的 .git 历史交给一个可能随时被回收的沙箱实例。

权限和凭据

沙箱不接收:

  • Kratos Cookie;
  • API Key;
  • 数据库凭据;
  • 长期校园会话;
  • 集群管理员凭据;
  • 未限定范围的工具 Token。

Git 访问通过短期 askpass 或同等受控通道完成。工具调用使用绑定到 Run 的短期 grant,服务端代理每次检查 Run、scope 和过期时间。

控制面和执行面也要分开:控制面可以创建、观察和清理工作负载;用户代码不能借此取得 Kubernetes API 或宿主机文件系统权限。

加密工作区:先分清浏览器私密笔记与沙箱工作树

当前「Git 笔记」有两个互不自动迁移的入口。普通 Markdown 写入 Forgejo 工作树, Agent 在沙箱内可以读取,尚未实现全仓库端到端加密;旧知识库条目仍存在 PostgreSQL 中,也不能因设置了工作区口令就视作已加密。工作区口令验证访问 并不等于对这些旧数据加密。

「私密笔记」使用另一组 Forgejo 密文对象:标题清单和正文在浏览器用工作区口令 加密后,通过同源 /api/vault/objects/{id} 保存。明文不发往沙箱,也不进入 服务端搜索或 Agent 上下文。此模式目前只覆盖纯文本私密笔记,不加密 旧笔记的原件、附件和整个 Git 仓库。用户可手动把单篇旧 Markdown 复制到 密文对象并回读核对,原件仍留在原资料库;目录、富文本、附件与会话不会迁移。 Forgejo 仍可观察对象数量、大小和更新时间; 浏览器运行环境及所加载的脚本必须可信,服务端若能替换客户端代码,也可能 窃取用户在页面中输入的口令。不要把这个能力等同于防范服务运营方的完全零知识。

目标架构:沙箱可读的加密代码仓库

如果代码仓库本身也属于敏感资料,传输层 HTTPS 和 Forgejo 的磁盘加密还不够。目标架构采用应用层加密,让 Forgejo 和普通对象存储只接触密文。

用户授权一次 Run
  ↓
Key Broker 根据用户、仓库、commit、Run 和 TTL 签发短期解密能力
  ↓
沙箱拉取密文仓库
  ↓
在内存或受限临时目录中解包、解密指定 commit
  ↓
Agent、编译器和本地索引只访问明文工作树
  ↓
输出和 checkpoint 按同一策略加密后写回
  ↓
Run 结束,密钥、明文目录和索引一起销毁

这个目标与浏览器私密笔记的信任边界不同:一旦把解密能力交给沙箱, 沙箱运营方及其执行的 Agent 就有机会接触明文,不能再承诺它们无法读取。 任何自动化读取都应取得用户明确授权,并单独限制有效期、范围和日志。

密钥注入不应使用普通环境变量、命令行参数或会被日志采集的配置文件。更合适的通道是受限 Unix socket、memfd、一次性文件描述符或权限为 0600 的短生命周期文件。

密钥至少要绑定以下上下文:

  • 用户身份;
  • 仓库和知识库;
  • 允许的 commit;
  • Run ID 和沙箱实例;
  • Agent Profile;
  • 过期时间和一次性 nonce。

这样,泄露一个 Run 的能力不会自然变成整个仓库或整个账号的长期能力。

为什么不在 Forgejo 上继续做明文模糊搜索

严格的端到端加密和服务器明文代码搜索不能同时成立。服务器没有明文,就不能构建普通的全文或模糊索引。

搜索因此移动到已经获授权的沙箱或专用可信执行面:

密文 commit
  ↓ 只在授权沙箱中解密
明文文件
  ↓ 首次建立 trigram / n-gram / 语言索引
本地搜索索引
  ↓ 后续查询只访问索引
模糊搜索结果

索引性能的关键不在于“每次搜索都重新解密”,而在于把解密和索引建立变成一次性的物化过程:

  • 同一个 commit 只建立一次索引;
  • 保存已索引的 commit、文件摘要和索引版本;
  • 只对 diff 中新增或修改的文件增量更新;
  • 删除文件时删除对应文档或写入 tombstone;
  • 查询阶段不重新解密整个仓库;
  • 如果索引需要跨 Run 保存,就使用独立的索引密钥,不复用仓库主密钥。

这会把成本从“每次查询都付一次解密”变成“首次 materialize 和变更时付一次索引成本”。模糊搜索的热路径仍然是本地索引,而不是 Git、Forgejo 或 R2。

R2 附件:加速下载不等于明文存储

R2 适合承载附件、Git LFS 对象和任务产物,但默认的静态加密不等于应用层端到端加密。对于私有附件,目标是让 R2 成为高速的密文传输层,而不是权限裁判或明文处理器。

私有附件的路径

客户端或授权沙箱
  ├─ 生成随机 file DEK
  ├─ 按 chunk 使用 AEAD 加密
  ├─ 用用户/工作区公钥封装 DEK
  └─ 通过短期授权上传密文到 R2

R2
  └─ 保存密文对象和加密 manifest

下载端
  ├─ 获取短期 GET 授权
  ├─ 获取被授权的 wrapped DEK
  ├─ 通过 R2/CDN 下载密文 chunk
  └─ 在客户端或授权沙箱中解密

附件不宜把整份文件加密成一个不可分段的 blob。分块 AEAD 可以保留:

  • Range 请求;
  • 断点续传;
  • 并发下载;
  • 大文件的渐进加载;
  • 视频、音频和 PDF 的局部读取。

每个文件使用独立的 file_dek,文件密钥轮换时只重新封装密钥,不重新加密整份文件。文件名、真实路径和权限信息放在 manifest 或产品元数据中;R2 object key 使用随机对象 ID,不直接暴露用户目录。

公开附件和私有附件

公开发布的资料可以选择普通 R2 对象,但公开的决定必须发生在产品发布流程里,不能因为“知道 URL”就自动公开。

私有附件需要:

  • 短期下载授权;
  • 对象级所有权检查;
  • 不把密钥放进 URL;
  • 不把原始文件名作为对象 key;
  • 下载、解密和预览都记录在明确的权限范围内。

R2 和 CDN 不负责病毒扫描、缩略图、转码、PDF 提取或全文检索。需要看见明文的处理应在客户端或已经授权的执行面完成;一旦由边缘 Worker 解密,Worker 就属于明文信任边界。

检索:正文和索引各司其职

TJUClaw 的检索分成两条路径。

知识库检索

笔记正文和被授权的资料进入派生索引。检索引擎保存文档 ID、标题、片段和必要的向量或全文特征,但权限判断仍回到 Go API。

删除、撤回或取消接入时,检索索引必须同步删除或标记不可见。搜索结果不能绕过知识库的所有权和只读快照规则。

代码工作区检索

敏感代码不进入公共 WeKnora,也不由 Forgejo 构建服务器明文索引。它在授权沙箱中完成本地索引,索引生命周期与仓库 commit、Run 或受控工作区绑定。

两类检索都遵守同一个原则:

索引是为了更快地找到内容,不是为了扩大内容的可见范围。

发布与接入:分享的是快照,不是编辑权

知识库发布生成不可变快照。其他用户接入后获得只读范围,不获得原库的写权限,也不能借此访问所有者的会话、模型配置、工具 grant、私有附件或工作环境凭据。

公开快照应当是一次明确的投影:

  • 包含所有者选择公开的条目;
  • 保留必要的标题、正文、层级和公开附件引用;
  • 去除会话、模型 Key、内部 object ID、执行记录和私有配置;
  • 撤回后不再出现在市场和新的读取路径中。

发布前的检查不是装饰。对用户来说,真正需要确认的是:这份资料会被谁看到,文件是否真的应该公开,以及正文里有没有把凭据或私人信息带出去。

失败时,数据应该落在哪里

系统把“能不能重试”和“什么是权威”分开处理。

失败位置可重试对象权威数据
客户端请求超时API 请求或页面读取PostgreSQL / R2
模型上游失败当前会话轮次已保存的会话消息
沙箱创建失败Run 或会话确保操作API 的 Run 状态
沙箱被回收从指定 commit 重新建立工作树Forgejo commit
索引构建失败派生索引任务原始笔记或代码 commit
附件下载中断当前 chunkR2 密文对象
产物收集失败重新收集 allowlist 路径沙箱 Run 记录与 checkpoint

persist/restore、本地缓存、搜索索引和模型上下文都不能取代 PostgreSQL、Forgejo 或 R2 这些权威来源。

这套架构刻意不做什么

  • 不让 API Pod 直接执行用户生成的任意代码。
  • 不让浏览器直接拥有数据库、Forgejo 管理接口或 WeKnora Key。
  • 不把 WeKnora 当成用户身份或知识库 ACL。
  • 不把 R2 object key 当成用户路径。
  • 不把模型网关做成任意 URL 转发器。
  • 不把沙箱本地磁盘当成代码历史。
  • 不用服务端明文搜索去抵消端到端加密。
  • 不把用户文件、Agent 会话和公开采集库混成一份数据。

架构的目标不是让每一层都知道所有事情,而是让每一层只知道完成自己工作所需的那一小部分。

当用户打开一篇笔记时,系统应当足够快;当用户交给 Agent 一份代码时,沙箱应当足够自由;当数据离开用户授权范围时,每一层都应当知道自己不该再往前走。