diff --git a/.env.template b/.env.template index 278062c4..5c79a0d6 100644 --- a/.env.template +++ b/.env.template @@ -1,8 +1,5 @@ MODEL_DIR=./models SAVE_DIR=./saves -LANGGRAPH_CHECKPOINTER_BACKEND=postgres -VITE_USE_RUNS_API=false # 部分体验有待优化,debug -YUXI_SANDBOX_IMAGE=python:3.12-slim # region model_provider SILICONFLOW_API_KEY= # 推荐使用硅基流动免费服务 https://cloud.siliconflow.cn/i/Eo5yTHGJ diff --git a/.github/ISSUE_TEMPLATE/提交一个docker启动问题.md b/.github/ISSUE_TEMPLATE/提交一个docker启动问题.md index 1fbe0ca7..71bfade2 100644 --- a/.github/ISSUE_TEMPLATE/提交一个docker启动问题.md +++ b/.github/ISSUE_TEMPLATE/提交一个docker启动问题.md @@ -16,8 +16,6 @@ assignees: '' 例如:"执行 `docker compose up -d` 后,api-dev 服务一直重启,查看日志显示无法连接到 Milvus" -您可以先看一下常见问题与解决方案:https://xerrors.github.io/Yuxi-Know/latest/changelog/faq.html - ## 2️⃣ 环境信息 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6c090672 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing to Yuxi + +感谢你关注 Yuxi。欢迎提交 Issue、改进文档、修复 Bug 或贡献新功能。 + +更完整的开发文档可参考 [docs/develop-guides/contributing.md](docs/develop-guides/contributing.md)。 + +## 开始之前 + +- 提交前请先搜索现有 [Issues](https://github.com/xerrors/Yuxi-Know/issues) +- 对于较大的功能改动,建议先开 Issue 讨论方案 +- 保持改动聚焦,避免在一次 PR 中混入无关重构 + +## 开发方式 + +本项目通过 Docker Compose 进行开发,推荐直接在容器环境中调试。 + +```bash +docker compose up -d +docker ps +docker logs api-dev --tail 100 +``` + +项目中的 `api-dev` 和 `web-dev` 默认支持热重载,本地修改代码后通常无需重启容器。 + +## 提交流程 + +1. Fork 仓库并创建分支 +2. 在对应目录完成开发与测试 +3. 提交清晰的 Commit Message +4. 发起 Pull Request,并说明修改内容、原因和验证方式 + +示例: + +```bash +git checkout -b feature/your-change +git commit -m "feat: add knowledge graph import flow" +git push origin feature/your-change +``` + +## 代码要求 + +### 通用 + +- 保持实现简单直接,避免过度设计 +- 只修改当前任务所需内容,不顺手做额外重构 +- 更新相关文档 +- 如有必要,同步更新 [docs/develop-guides/roadmap.md](docs/develop-guides/roadmap.md) +- 设计部分请参考 [docs/develop-guides/design.md](docs/develop-guides/design.md) + +### 后端 + +- 使用 Python 3.12+ 风格 +- 提交前运行: + +```bash +make format +make lint +docker compose exec api uv run pytest +``` + +- 测试脚本建议放在 `backend/test` + +### 前端 + +- 使用 `pnpm` +- API 接口统一放在 `web/src/apis` +- 优先使用 `lucide-vue-next` 图标 +- 样式使用 `less` +- 非特殊情况优先复用 [web/src/assets/css/base.css](web/src/assets/css/base.css) 中的颜色变量 + +## Pull Request 建议 + +- 标题清晰,能说明变更目标 +- 描述中包含改动内容、影响范围和验证结果 +- 如果涉及 UI,请附截图或录屏 +- 如果涉及接口或行为变化,请补充文档 + +## 提交信息建议 + +推荐使用以下前缀: + +- `feat` +- `fix` +- `docs` +- `refactor` +- `test` +- `chore` + +## 问题反馈 + +- Bug 反馈/功能讨论: + +感谢你的贡献 ❤️。 diff --git a/README.md b/README.md index 009ef2d3..f008c34f 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ docker compose up --build 感谢所有贡献者的支持! - + diff --git a/backend/package/yuxi/config/static/info.template.yaml b/backend/package/yuxi/config/static/info.template.yaml index 9d0bda63..cbc34198 100644 --- a/backend/package/yuxi/config/static/info.template.yaml +++ b/backend/package/yuxi/config/static/info.template.yaml @@ -50,7 +50,7 @@ actions: url: "https://github.com/xerrors/Yuxi-Know/issues/new/choose" - name: "开发路线图" icon: "roadmap" - url: "https://xerrors.github.io/Yuxi-Know/latest/changelog/roadmap.html" + url: "https://xerrors.github.io/Yuxi-Know/latest/roadmap.html" diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index f7b7fb66..82fdb3fb 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -39,9 +39,9 @@ export default defineConfig({ text: '智能体开发', items: [ { text: '智能体配置', link: '/agents/agents-config' }, - { text: '上下文配置', link: '/agents/context-config' }, { text: '工具系统', link: '/agents/tools-system' }, { text: '中间件', link: '/agents/middleware' }, + { text: '沙盒架构与设计', link: '/agents/sandbox-architecture' }, { text: 'MCP 集成', link: '/agents/mcp-integration' }, { text: 'Skills 管理', link: '/agents/skills-management' }, { text: 'SubAgents 管理', link: '/agents/subagents-management' } @@ -51,7 +51,6 @@ export default defineConfig({ text: '高级配置', items: [ { text: '配置系统详解', link: '/advanced/configuration' }, - { text: '沙盒改造与验收', link: '/advanced/sandbox-validation' }, { text: '文档解析', link: '/advanced/document-processing' }, { text: '品牌自定义', link: '/advanced/branding' }, { text: '其他配置', link: '/advanced/misc' }, @@ -62,16 +61,9 @@ export default defineConfig({ { text: '开发指南', items: [ + { text: '参与贡献', link: '/develop-guides/contributing' }, + { text: '开发路线图', link: '/develop-guides/roadmap' }, { text: '界面设计规范', link: '/develop-guides/design' }, - { text: '路线图', link: '/develop-guides/roadmap' } - ] - }, - { - text: '更新日志', - items: [ - { text: '参与贡献', link: '/changelog/contributing' }, - { text: '常见问题', link: '/changelog/faq' }, - { text: '迁移至 v0.5', link: '/changelog/migrate_to_v0-5' } ] } ], diff --git a/docs/advanced/sandbox-validation.md b/docs/advanced/sandbox-validation.md deleted file mode 100644 index af243e40..00000000 --- a/docs/advanced/sandbox-validation.md +++ /dev/null @@ -1,960 +0,0 @@ -# Yuxi Sandbox 技术设计文档 - -## 文档目标 - -这份文档描述的是 **Yuxi 当前已经落地的沙盒系统设计**,不是一个理想化方案,也不是 API 参考手册。 - -::: warning 预发布 -此部分所涉及到的技术路线和方案文档,均可能会随时改变。 -::: - - -重点回答五个问题: - -1. 当前沙盒到底解决了什么问题 -2. 当前沙盒在系统里是怎么接入的 -3. Agent、工作台文件浏览器、Skills 与沙盒之间的关系是什么 -4. 线程级隔离、路径隔离、容器生命周期是如何实现的 -5. 当前方案的边界、局限性和后续演进方向是什么 - -如果只想快速建立整体认识,建议先看: - -- 第 1 节:设计结论 -- 第 2 节:系统边界 -- 第 3 节:整体架构 -- 第 5 节:生命周期 -- 第 11 节:当前局限性 - ---- - -## 1. 设计结论 - -当前 Yuxi 的沙盒不是“泛化的安全执行平台”,而是一套 **围绕线程工作区、Agent 文件工具、工作台文件浏览器** 设计的受控执行环境。 - -它的核心特征可以概括为: - -- **线程级隔离**:资源归属从早期的 user 级沙盒收敛为 `thread_id -> sandbox_id` -- **容器级执行**:默认用本机 Docker 容器承载执行环境 -- **命名空间约束**:Agent 和前端主要通过 `/mnt/user-data` 与 `/mnt/skills` 看到文件系统 -- **惰性初始化**:不再依赖公开的 `/api/sandbox/*` 生命周期接口;首次访问时自动获取沙盒 -- **双视图设计**: - - Agent 工具侧使用 composite backend,兼顾技能只读路由 - - 工作台侧使用 viewer-oriented filesystem service,强调真实目录浏览和原始文件内容 - -这意味着它更接近: - -- “给 Agent 提供一个线程隔离的工作目录和有限 shell 执行能力” - -而不是: - -- “一个对外承诺强安全边界的多租户通用沙箱平台” - -这个定位非常重要。后续阅读整份文档时,所有设计取舍都建立在这个现实目标之上。 - ---- - -## 2. 系统边界 - -### 2.1 当前沙盒负责什么 - -当前沙盒主要负责以下能力: - -- 为某个 thread 提供独立的用户数据目录 -- 在 Docker 容器中执行 Agent 发起的命令 -- 为 Agent 文件工具提供受限文件系统 -- 为工作台文件浏览器提供可视化文件访问入口 -- 将 skills 以只读目录暴露给 Agent 和工作台 - -### 2.2 当前沙盒不负责什么 - -当前实现并不试图完整解决以下问题: - -- 强多租户对抗场景下的高强度安全隔离 -- 资源配额的精细控制 -- 审计级别的命令执行记录 -- 持久化的沙盒元数据控制平面 -- 面向通用用户的文件编辑、删除、重命名、上传管理 API - -这不是缺陷描述,而是范围定义。文档后面提到的“限制”,也应放在这个范围下理解。 - ---- - -## 3. 整体架构 - -### 3.1 架构分层 - -当前沙盒相关实现大致分为五层: - -1. **sandbox 子包**(位于 `backends/sandbox/`) - - [sandbox_executor.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_executor.py) — 执行器 - - [sandbox_provisioner.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_provisioner.py) — 资源调配器 - - [sandbox_provisioner_base.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_provisioner_base.py) — provisioner 抽象基类 - - [sandbox_local_container.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_local_container.py) — 本地容器管理器 - - [sandbox_remote.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_remote.py) — 远程沙盒管理器 - - [sandbox_config.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_config.py) — 配置常量 - - [sandbox_info.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_info.py) — 沙盒信息 - - [path_security.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/path_security.py) — 路径安全 - - [docker_api.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/docker_api.py) — Docker API 封装 -2. **Agent / Viewer 接入层** - - [composite.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/composite.py) — Agent filesystem 路由 - - [filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/filesystem_service.py) — Agent 文件系统服务 - - [viewer_filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/viewer_filesystem_service.py) — 工作台文件浏览服务 -3. **HTTP Router / UI 使用层** - - [filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/filesystem_router.py) - - [viewer_filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/viewer_filesystem_router.py) - - [AgentPanel.vue](/Users/wenjie/Documents/projects/Yuxi-Know/web/src/components/AgentPanel.vue) -4. **Agent / Viewer 接入层** - - [composite.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/composite.py) - - [filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/filesystem_service.py) - - [viewer_filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/viewer_filesystem_service.py) -5. **HTTP Router / UI 使用层** - - [filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/filesystem_router.py) - - [viewer_filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/viewer_filesystem_router.py) - - [AgentPanel.vue](/Users/wenjie/Documents/projects/Yuxi-Know/web/src/components/AgentPanel.vue) - -### 3.2 一张图看清调用关系 - -```text -Agent / Frontend - | - +-- Agent filesystem tool - | | - | +-- create_agent_composite_backend(...) - | | - | +-- default: YuxiSandboxBackend - | +-- route /mnt/skills/: SelectedSkillsReadonlyBackend - | - +-- Workbench viewer panel - | - +-- /api/viewer/filesystem/* - | - +-- viewer_filesystem_service - | - +-- thread ownership check - +-- resolve agent config / visible skills - +-- provider.acquire(thread_id) - +-- sandbox backend or skills backend - -Provider - | - +-- LocalContainerBackend (default) - | | - | +-- docker run / docker exec / docker stop - | +-- Docker API fallback when no docker CLI - | - +-- RemoteSandboxBackend (reserved for remote provisioner mode) -``` - -### 3.3 当前最重要的设计分叉 - -当前系统里有两套“文件系统视图”,这不是重复实现,而是目标不同: - -- **Agent 文件系统视图** - - 关注工具调用和 prompt 约束 - - 通过 composite backend 暴露 `/mnt/skills` - - 语义偏 agent-oriented - -- **工作台文件浏览视图** - - 关注真实目录浏览、原始文件内容、下载 - - 通过独立 viewer service 暴露 - - 语义偏 viewer-oriented - -这套分离是当前架构里一个非常关键的改动。它解决了之前”工作台直接复用 agent backend 导致语义错位”的问题。 - -### 3.4 四种 Backend 的角色区分 - -理解这四个类的职责边界非常重要,它们分属不同层次: - -#### `SandboxBackend` — 抽象基类,生命周期接口 - -[sandbox_provisioner_base.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_provisioner_base.py) 定义了沙盒管理器的**抽象生命周期接口**: - -```python -class SandboxBackend(ABC): - def create(self, *, thread_id, sandbox_id, extra_mounts) -> SandboxInfo: ... - def destroy(self, info: SandboxInfo) -> None: ... - def is_alive(self, info: SandboxInfo) -> bool: ... - def discover(self, sandbox_id: str) -> SandboxInfo | None: ... -``` - -它只回答”如何创建一个沙盒实例、如何销毁它、沙盒是否还活着”——**不涉及任何文件操作或命令执行**。 - -#### `LocalContainerBackend` — 本地容器管理器 - -继承 `SandboxBackend`,是**本地 Docker 容器**的创建者和管理者: - -- 通过 `docker run` 启动沙盒容器 -- 通过 `docker exec` 在容器中执行命令(实际委托给 `YuxiSandboxBackend`) -- 通过 `docker stop` 销毁容器 -- 支持 Docker CLI 和 Docker Unix Socket API 两种调用方式 -- 负责端口分配、容器发现、warm pool - -#### `RemoteSandboxBackend` — 远程沙盒管理器 - -同样继承 `SandboxBackend`,但通过 **HTTP API** 与远程沙盒 provisioner 交互: - -- `create()` → POST `/api/sandboxes` -- `destroy()` → DELETE `/api/sandboxes/{id}` -- `is_alive()` → GET `/api/sandboxes/{id}` 检查状态 -- `discover()` → GET `/api/sandboxes/{id}` - -适用于无法在本地管理容器的场景(如云端沙盒服务)。 - -#### `YuxiSandboxBackend` — 执行后端,不是 SandboxBackend - -继承自 deepagents 的 `BaseSandbox`,是**在已有容器内执行操作的具体实现**: - -- `execute(command)` — 在容器内执行 shell 命令 -- `read() / write() / edit()` — 文件读写编辑 -- `ls_info() / glob_info()` — 目录扫描和 glob -- `upload_files() / download_files()` — 文件上传下载 -- 路径规范化与输出遮蔽(防止宿主机路径泄露) - -**关键区别**:`YuxiSandboxBackend` 不负责创建/销毁容器,它工作在容器创建好之后。它的定位是”执行器”,而 `LocalContainerBackend` / `RemoteSandboxBackend` 的定位是”容器管理器”。 - -#### 关系总结 - -``` -SandboxProvider - │ - ├── LocalContainerBackend — 创建/销毁本地 Docker 容器 - │ │ - │ └── YuxiSandboxBackend — 在容器内执行命令和文件操作 - │ - └── RemoteSandboxBackend — 通过 HTTP 与远程 provisioner 交互 - │ - └── 远程沙盒(由外部服务管理) -``` - ---- - -## 4. 运行拓扑 - -### 4.1 本地 Docker Compose 模式 - -在默认开发模式中,运行拓扑是: - -```text -宿主机 -├── Docker Daemon -│ ├── api-dev -│ ├── web-dev -│ └── yuxi-sandbox- -└── project workspace - -api-dev 容器 -└── Yuxi 后端进程 - └── 通过 docker CLI 或 Docker API 管理 yuxi-sandbox-* 容器 -``` - -关键点: - -- `api-dev` 并不在自身进程里执行用户命令 -- 真正的执行发生在独立的 `yuxi-sandbox-*` 容器中 -- `api-dev` 只是控制面,负责创建、发现、复用、销毁沙盒容器 - -### 4.2 本地容器模式与远程 provisioner 模式 - -[sandbox_provisioner.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_provisioner.py) 会根据配置决定底层 backend: - -- 如果设置了 `YUXI_SANDBOX_PROVISIONER_URL` - - 使用 `RemoteSandboxBackend` -- 否则 - - 使用 `LocalContainerBackend` - -当前主路径和测试覆盖都以 `LocalContainerBackend` 为主。 - -### 4.3 Docker CLI 与 Docker API 双路径 - -[sandbox_local_container.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_local_container.py) 和 [sandbox_executor.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_executor.py) 都支持两种运行方式: - -- 有 `docker` CLI 时,优先走 `docker run`、`docker exec`、`docker stop` -- 没有 `docker` CLI 时,退回到 Docker Unix Socket API - -这样做的原因很现实: - -- 开发机和容器内环境不总是完全一致 -- 有些部署只挂载了 docker socket,没有安装 docker CLI - -这套 fallback 的目标不是优雅,而是保证控制面在不同运行环境下还能工作。 - ---- - -## 5. 生命周期设计 - -### 5.1 当前资源归属:thread-local - -当前 provider 的核心规则是: - -```text -thread_id -> deterministic sandbox_id -> sandbox container -``` - -对应实现见 [sandbox_provisioner.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_provisioner.py)。 - -`sandbox_id` 由 thread_id 的 SHA256 前 8 位生成: - -```python -hashlib.sha256(thread_id.encode()).hexdigest()[:8] -``` - -这个设计带来的性质是: - -- 同一个 thread 多次访问时,倾向复用同一个沙盒 -- 不同 thread 不共享容器 -- thread 级的工作目录和运行上下文被绑定在一起 - -这与旧方案最大的差异在于: - -- **旧方案倾向 user 级复用** -- **当前方案明确是 thread 级隔离** - -### 5.2 acquire 的完整逻辑 - -`provider.acquire(thread_id)` 的流程不是单纯“起一个容器”,而是按顺序尝试: - -1. 看当前 thread 是否已经绑定活跃沙盒 -2. 看 warm pool 里是否有这个 sandbox_id -3. 看 Docker 里是否已经存在同名运行中容器 -4. 如果都没有,才真正创建新容器 - -这个流程很重要,因为它同时兼顾了: - -- 惰性初始化 -- 同线程复用 -- 进程重启后的容器发现 -- warm pool 热复用 - -### 5.3 release 与 destroy 的语义 - -当前 provider 仍然保留了 `release` 和 `destroy` 语义,但它们已经不再作为前端公开 API 的一部分。 - -- `release(thread_id)` - - 从活跃映射中移除 - - 放入 `_warm_pool` - - 容器保持运行 - -- `destroy(thread_id)` - - 同时处理 active 和 warm pool 中的对应沙盒 - - 真正调用底层 backend 停止容器 - -这里有一个历史经验教训:如果 `destroy()` 只认 active map,不认 warm pool,就会出现“释放后无法真正停掉容器”的泄漏问题。当前实现已经修正了这类问题。 - -### 5.4 空闲回收 - -provider 在初始化时会启动一个 idle checker 线程: - -- 间隔:`IDLE_CHECK_INTERVAL` -- 默认空闲超时:`DEFAULT_IDLE_TIMEOUT = 600` - -回收对象有两类: - -- 活跃但长期无访问的沙盒 -- 已经释放到 warm pool 且超时的沙盒 - -### 5.5 为什么现在不再依赖 `/api/sandbox/*` - -当前公开路由注册见 [server/routers/__init__.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/__init__.py)。可以看到: - -- 已注册: - - `/api/filesystem/*` - - `/api/viewer/filesystem/*` -- 未注册: - - `/api/sandbox/*` - -这代表生命周期已经被收回到系统内部,不再由前端显式驱动。 - -这么做的核心理由有三个: - -1. 避免前端为了“预热容器”而直接参与控制面 -2. 避免 thread ownership 校验分散在多个入口 -3. 让 Agent 工具调用和工作台访问都走同一种惰性初始化模型 - ---- - -## 6. 文件系统模型 - -### 6.1 真实宿主机目录 - -每个 thread 在宿主机侧都有独立的数据目录,根路径大致是: - -```text -saves/threads//user-data/ -``` - -provider 会确保下面这些子目录存在: - -- `workspace` -- `outputs` -- `uploads` -- `uploads/attachments` -- `large_tool_results` - -对应代码在 [sandbox_provisioner.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_provisioner.py) 的 `ensure_thread_dirs()`。 - -### 6.2 容器内固定命名空间 - -宿主机目录挂载进容器后,Agent 和工作台看到的是固定命名空间: - -- `/mnt/user-data` -- `/mnt/user-data/workspace` -- `/mnt/user-data/uploads` -- `/mnt/user-data/outputs` -- `/mnt/user-data/large_tool_results` -- `/mnt/skills` - -这里要区分两个概念: - -- **标准命名空间** - - 用于约束、归一化、提示词和工具行为 -- **真实 listing** - - 用于工作台展示实际文件结构 - -### 6.3 为什么工作台现在显示真实 listing - -之前工作台复用 agent filesystem 视图时,一个典型问题是: - -- Agent 在 `/mnt/user-data/bubble_sort.py` 创建了文件 -- 工作台却只显示硬编码的 `workspace/uploads/outputs` -- 用户会误以为文件没有创建成功 - -所以当前 viewer service 的策略是: - -- `/mnt/user-data` 按真实目录扫描结果返回 -- 不再人为注入固定目录 - -这使得工作台真正展示“当前线程在后端实际看到的文件系统”。 - -### 6.4 兼容路径为什么还存在 - -[path_security.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/path_security.py) 中仍然保留了若干兼容映射,例如: - -- `/workspace` -> `/mnt/user-data/workspace` -- `/uploads` -> `/mnt/user-data/uploads` -- `/outputs` -> `/mnt/user-data/outputs` -- `/skills` -> `/mnt/skills` -- `/attachments` -> `/mnt/user-data/uploads/attachments` -- `/home` -> `/mnt/user-data/workspace` - -它们存在的原因不是鼓励继续使用旧路径,而是为了兼容: - -- 模型自己生成的老路径 -- 某些工具或 prompt 里的历史路径语义 -- 迁移期间尚未完全收敛的调用方 - -文档层面应该明确: - -- **标准输出和标准认知应基于 `/mnt/...` 命名空间** -- 兼容映射只是过渡层 - ---- - -## 7. Skills 挂载与可见性模型 - -### 7.1 Skills 不是简单的本地目录暴露 - -skills 在物理上会被只读挂载到容器的 `/mnt/skills`,但“能否看见全部 skills”不是由挂载本身决定,而是由上层 backend 路由决定。 - -当前相关逻辑分成两层: - -1. **物理挂载** - - provider 在 `_get_extra_mounts()` 中将 skills 根目录以只读形式挂进容器 -2. **逻辑可见性** - - [composite.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/composite.py) 和 [viewer_filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/viewer_filesystem_service.py) 基于当前 runtime context 的 `skills` 选择可见 skills - -### 7.2 Agent 侧如何看到 Skills - -Agent 侧创建 backend 时,会调用: - -- [graph.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/buildin/chatbot/graph.py) -- [composite.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/composite.py) - -结果是: - -- 默认文件系统后端使用沙盒 backend -- `/mnt/skills/` 路由到 `SelectedSkillsReadonlyBackend` - -所以 Agent 看到的 `/mnt/skills` 是“当前 agent config 允许它看到的 skills 子集”,而不是物理目录的全量镜像。 - -### 7.3 工作台为什么也要按 agent config 过滤 Skills - -工作台的目标不是管理员视角的“看所有技能”,而是: - -- “当前这个 thread 对应的 agent,在此刻实际能看到什么” - -因此 viewer service 会先解析: - -- 当前用户 -- 当前 thread -- 当前 agent_id -- 可选的 agent_config_id - -然后再决定 `/mnt/skills` 下暴露哪些内容。 - -这保证了两件事: - -1. 工作台与 Agent 的 skills 可见范围一致 -2. 前端不会看到“实际 Agent 用不到的 skills” - ---- - -## 8. 路径安全与命令执行约束 - -### 8.1 核心约束原则 - -当前安全模型并不是一个完整的系统调用级隔离模型,而是建立在以下几层约束之上: - -1. 线程级独立容器 -2. 受限文件命名空间 -3. 绝对路径白名单检查 -4. 只读 skills 挂载 -5. 前端/服务层的 thread ownership 校验 - -### 8.2 路径白名单 - -[path_security.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/path_security.py) 中定义了允许的命名空间前缀: - -- `/mnt/user-data` -- `/mnt/skills` - -只要请求落在这两个根之外,就会被拒绝。 - -同时还会显式拦截: - -- `..` 路径遍历 -- 非允许命名空间访问 - -### 8.3 命令执行时的路径检查 - -`validate_execute_command_paths()` 会对 shell 命令做 token 级别的绝对路径检查: - -- `/mnt/...` 允许,但仍要经过 `ensure_path_allowed()` -- `/bin/`、`/usr/bin/`、`/usr/local/bin/` 放行 -- `/usr/lib/` 作为运行时依赖路径放行 -- 其他绝对路径拒绝 - -这意味着当前模型允许类似: - -```bash -python3 /mnt/user-data/workspace/app.py -ls /mnt/user-data -/bin/sh -c '...' -``` - -但不允许: - -```bash -cat /etc/passwd -python /tmp/evil.py -``` - -### 8.4 输出中的宿主机路径遮蔽 - -为了减少宿主机路径泄露,`sandbox_backend` 会对输出做一次映射遮蔽: - -- 宿主机 thread 目录 -> `/mnt/user-data` -- skills 宿主机目录 -> `/mnt/skills` - -这是一个很实用的细节。它不能提供真正的安全保证,但可以降低“内部物理路径暴露到模型上下文或前端界面”的概率。 - -### 8.5 需要明确的现实边界 - -当前这套安全模型是“工程上可控”,不是“高强度对抗级别安全”。 - -例如,当前仍然没有做到: - -- 细粒度 CPU / memory / pid 限制 -- 默认禁网 -- 严格只读 rootfs -- seccomp / apparmor 的定制化收敛 -- 进程树和系统调用级审计 - -当前本地容器启动只设置了: - -- `--security-opt seccomp=unconfined` - -这从强安全角度看其实是偏宽松的。文档必须如实反映这一点,而不是把当前方案描述成“高安全沙盒”。 - ---- - -## 9. Agent、旧 filesystem API 与 viewer API 的关系 - -### 9.1 Agent 侧接入 - -以 [chatbot/graph.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/buildin/chatbot/graph.py) 为例,FilesystemMiddleware 的 backend 由 `_create_fs_backend()` 动态创建。 - -关键流程是: - -1. 从 runtime context 里拿到 `thread_id` -2. `provider.acquire(thread_id)` -3. 用 `create_agent_composite_backend()` 组装: - - default = sandbox backend - - `/mnt/skills/` = SelectedSkillsReadonlyBackend - -这代表: - -- Agent 的工具调用第一次触发文件系统时,就会惰性初始化沙盒 -- 不需要先调用某个 prepare API - -### 9.2 旧 `/api/filesystem/*` 的定位 - -[filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/filesystem_service.py) 和 [filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/filesystem_router.py) 仍然存在。 - -它们的定位是: - -- 提供兼容性的 filesystem 访问接口 -- 继续基于 agent-oriented composite backend 工作 - -它们会: - -1. 校验 thread ownership -2. 解析当前用户可用的 agent config context -3. acquire sandbox -4. 组装 composite backend -5. 调用 `ls_info` 或 `download_files` - -### 9.3 新 `/api/viewer/filesystem/*` 的定位 - -viewer API 对应: - -- [viewer_filesystem_service.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/services/viewer_filesystem_service.py) -- [viewer_filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/server/routers/viewer_filesystem_router.py) - -这组接口的设计目标非常明确: - -- 为工作台文件浏览器服务 -- 强调真实目录浏览 -- 返回原始文件内容 -- 支持下载 -- 与 Agent 的 skills 可见范围一致 - -当前提供三类只读能力: - -- `tree` -- `file` -- `download` - -为什么说它不是“API 参考”而是“技术设计”的一部分?因为它的重要性不在于 URL 形状,而在于 **它代表了工作台视图从 agent backend 中解耦** 这个架构决定。 - -### 9.4 为什么不让工作台继续复用旧 filesystem API - -之前工作台直接复用 agent filesystem 逻辑,会遇到一串语义问题: - -- 文件读取带行号,因为底层 `read()` 更偏向 Agent 阅读语义 -- 目录展示不够真实,更偏向工具侧规范目录 -- skills 可见性容易与当前 agent config 脱节 -- 前端不得不理解更多 backend 细节 - -viewer API 的存在,实质上是承认: - -- “Agent 文件系统”与“用户浏览器文件系统”是两个不同产品对象 - -这是当前沙盒周边设计里最重要的一个收敛。 - ---- - -## 10. 典型执行流程 - -### 10.1 Agent 首次使用文件系统 - -```text -用户发起对话 - -> Agent graph 构建 middleware - -> Agent 首次触发 filesystem tool - -> _create_fs_backend() - -> provider.acquire(thread_id) - -> 若无现成容器则创建新容器 - -> 返回 composite backend - -> Agent 开始读写 /mnt/user-data 或访问 /mnt/skills -``` - -### 10.2 工作台首次打开文件系统 - -```text -前端打开 AgentPanel - -> 请求 /api/viewer/filesystem/tree?thread_id=...&path=/ - -> 后端校验 thread ownership - -> 解析当前 agent config - -> provider.acquire(thread_id) - -> 构造 sandbox backend + skills backend - -> 返回根目录条目 -``` - -### 10.3 点击进入某个目录 - -```text -前端点击目录 - -> 请求 viewer tree(path=) - -> 后端仅列当前层目录 - -> 前端懒加载子节点 -``` - -这意味着工作台不会一次性扫完整棵树,而是按层级逐步展开。 - -### 10.4 读取文件内容 - -```text -前端点击文件 - -> 请求 /api/viewer/filesystem/file - -> 后端 download_files([path]) - -> 取原始 bytes - -> UTF-8 decode,失败时 replace - -> 返回纯文本内容 -``` - -这里特意不走 deepagents 的 `read()`,就是为了避免“所有文件都被自动加行号”的 agent-oriented 行为污染工作台体验。 - ---- - -## 11. 容器管理细节 - -### 11.1 命名规则 - -容器名格式: - -```text -yuxi-sandbox- -``` - -其中 `` 是 thread_id 的确定性哈希截断值。 - -### 11.2 启动方式 - -本地容器 backend 典型启动命令等价于: - -```bash -docker run \ - --security-opt seccomp=unconfined \ - --rm \ - -d \ - -p :8080 \ - --name yuxi-sandbox- \ - -v :/mnt/user-data \ - -v :/mnt/skills:ro \ - \ - sh -c "sleep infinity" -``` - -为什么是 `sleep infinity`: - -- 容器被创建后保持存活 -- 真正的执行通过后续 `docker exec` 进入 -- 这样可以避免每次命令都重新起一个新容器 - -### 11.3 容器发现 - -当 provider 启动后,如果内存态映射丢失,但 Docker 中容器还活着,provider 会尝试 `discover(sandbox_id)`: - -- 根据容器名判断是否存在 -- 解析暴露端口 -- 重建 `SandboxInfo` - -这是一种“弱恢复”能力,不是严格的持久化控制面。 - -### 11.4 端口分配 - -本地 backend 会从 `base_port` 开始找空闲端口,并向后扫描。 - -需要诚实指出: - -- 这是典型的“先探测端口再绑定”的 TOCTOU 模式 -- 在高并发/高竞争环境下并不完美 - -当前之所以可接受,是因为它主要服务于本地开发和较小规模部署,而不是大规模沙盒平台。 - ---- - -## 12. 配置项 - -当前配置核心定义见 [sandbox_config.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/package/yuxi/agents/backends/sandbox/sandbox_config.py)。 - -常用项包括: - -- `YUXI_SANDBOX_IMAGE` - - 沙盒镜像 -- `YUXI_SANDBOX_BASE_PORT` - - 本地容器端口起始值 -- `YUXI_SANDBOX_CONTAINER_PREFIX` - - 容器名前缀 -- `YUXI_SANDBOX_IDLE_TIMEOUT` - - 空闲超时 -- `YUXI_SANDBOX_MAX_REPLICAS` - - 最大并发沙盒数 -- `YUXI_SANDBOX_HOST` - - 宿主机访问沙盒容器的 host -- `YUXI_SANDBOX_PROVISIONER_URL` - - 远程 provisioner 模式入口 -- `YUXI_DOCKER_API_BASE` - - Docker API base -- `YUXI_DOCKER_API_SOCKET` - - Docker Unix Socket 路径 - -理解这些配置时,要把它们分成三类: - -- **容器资源寻址** -- **provider 生命周期控制** -- **Docker 控制面访问方式** - ---- - -## 13. 测试与验证策略 - -### 13.1 单元与集成测试 - -当前沙盒和 viewer 相关测试覆盖包括: - -- [test_sandbox_provider_lifecycle.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/test_sandbox_provider_lifecycle.py) -- [test_sandbox_path_compat.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/test_sandbox_path_compat.py) -- [test_filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/api/test_filesystem_router.py) -- [test_viewer_filesystem_router.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/api/test_viewer_filesystem_router.py) - -覆盖点主要包括: - -- provider 生命周期逻辑 -- 路径兼容与路径安全 -- 旧 filesystem API 的 thread ownership 和命名空间行为 -- viewer API 的真实目录浏览、原始文件读取、下载行为 - -### 13.2 端到端测试 - -脚本式 E2E 见: - -- [test_sandbox_e2e_no_skip.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/api/test_sandbox_e2e_no_skip.py) -- [test_sandbox_e2e_reuse.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/api/test_sandbox_e2e_reuse.py) -- [test_viewer_filesystem_e2e.py](/Users/wenjie/Documents/projects/Yuxi-Know/backend/test/api/test_viewer_filesystem_e2e.py) - -这些脚本验证的是更接近真实使用链路的场景: - -- thread 创建 -- provider acquire -- 容器内写文件 -- API 层读取/浏览/下载 -- 同线程复用 -- 不同线程隔离 - -### 13.3 为什么测试里要主动清理 sandbox 容器 - -由于本地容器模式本质上会真正起 `yuxi-sandbox-*` 容器,如果测试没有处理好清理,会出现: - -- 端口耗尽 -- warm pool / discover 干扰后续用例 -- 测试之间状态污染 - -因此测试基建里增加了 session 级 sandbox cleanup。这个细节本身也说明: - -- 当前 provider 的控制面仍偏内存态 -- 真实容器状态与测试进程状态可能漂移 - ---- - -## 14. 当前局限性 - -### 14.1 安全强度仍然偏工程化 - -当前方案不是强隔离安全产品,其局限包括: - -- 没有严格资源配额 -- 没有默认禁网策略 -- 容器启动参数偏宽松 -- 依赖路径白名单而不是系统调用级控制 - -### 14.2 Provider 状态未持久化 - -provider 的核心状态都在进程内: - -- `_sandboxes` -- `_sandbox_infos` -- `_thread_sandboxes` -- `_warm_pool` - -这意味着: - -- 进程重启后要依赖 `discover()` -- 不能把当前方案理解为“有完整控制数据库的沙盒平台” - -### 14.3 viewer 仍然是只读 - -当前 viewer-oriented filesystem service 只做了: - -- tree -- file -- download - -没有做: - -- rename -- move -- delete -- upload -- inline edit - -这是刻意收敛范围的结果,不是遗漏。 - -### 14.4 兼容路径仍然存在 - -兼容路径仍保留,说明路径模型虽然已经大致收敛,但还没有完全把所有调用方都清到统一规范。 - -### 14.5 本地容器 backend 仍有工程折中 - -例如: - -- 端口分配不是完全无竞争 -- Docker CLI / Docker API 双实现会增加维护成本 -- 文件上传下载与命令执行仍不是完全同一种底层路径 - -这些都是当前方案的真实成本。 - ---- - -## 15. 后续演进方向 - -如果未来继续演进,这条线最值得做的事情不是再堆更多 viewer 功能,而是继续收敛基础模型: - -### 15.1 强化控制面 - -- 将 sandbox 元数据持久化 -- 更清晰地区分 active、warm、destroyed 状态 -- 降低 discover 对系统一致性的依赖 - -### 15.2 强化安全边界 - -- 增加资源限制 -- 明确网络策略 -- 收紧容器安全选项 -- 评估更适合沙盒场景的运行时 - -### 15.3 继续收敛路径模型 - -- 逐步减少兼容别名 -- 统一让 `/mnt/...` 成为唯一规范路径 -- 减少模型与前端对历史路径的依赖 - -### 15.4 继续拆分用户视图与 Agent 视图 - -viewer service 证明了一件事: - -- “给 Agent 用的 backend”和“给用户看的文件浏览器后端”应该明确区分 - -如果以后要做: - -- 文件编辑器 -- 差异查看 -- 搜索 -- 批量下载 -- 二进制预览 - -应该继续沿 viewer-oriented 的方向扩展,而不是回退去复用 agent-oriented backend 语义。 - ---- - -## 16. 一句话总结 - -Yuxi 当前的沙盒方案,本质上是一套 **线程级容器工作区 + `/mnt` 命名空间 + 惰性获取的 provider + Agent/Viewer 双视图文件系统接入模型**。 - -它已经足够支撑: - -- Agent 在独立线程工作区中执行命令 -- Skills 以只读方式注入 -- 工作台查看真实后端文件系统 - -但它仍然是一个 **面向当前产品场景的工程化沙盒**,而不是一个已经完成强安全与强控制面建设的通用沙箱平台。 diff --git a/docs/agents/agents-config.md b/docs/agents/agents-config.md index 083b5843..c66ed733 100644 --- a/docs/agents/agents-config.md +++ b/docs/agents/agents-config.md @@ -1,147 +1,309 @@ -# 智能体开发指南 +# 智能体配置 -Yuxi 的智能体系统基于 LangGraph 构建,提供了灵活而强大的 Agent 开发能力。通过统一的 `AgentManager`,系统能够自动发现和管理所有智能体,让开发者能够专注于业务逻辑的实现。 +Yuxi 的智能体系统基于 LangGraph 构建。对开发者来说,最重要的不是单独理解某个页面或某个字段,而是理解三件事: -## 智能体架构 +- Agent 如何被定义和发现 +- Context 如何驱动配置界面 +- Context 如何贯穿一次 Agent 运行周期 -### 核心概念 +本文聚焦这三部分。 -系统的智能体架构围绕几个核心组件展开: +## 1. 整体结构 -- **BaseAgent**:所有智能体的基类,定义了统一的接口规范 -- **AgentContext**:智能体的配置上下文,包含模型、提示词、工具等配置 -- **Graph**:LangGraph 图结构,定义智能体的执行流程 -- **Middleware**:中间件系统,用于扩展和定制智能体行为 +智能体开发围绕四个核心对象展开: -### 自动发现机制 +- **`BaseAgent`**:统一的 Agent 抽象,定义 `get_graph()`、`context_schema`、`capabilities` +- **`BaseContext`**:配置 Schema,也是前端配置项的来源 +- **Graph / Middleware**:LangGraph 图与中间件链,决定运行时行为 +- **AgentConfig**:数据库中的配置实例,前端侧边栏编辑的就是它 -智能体采用自动发现模式。在 `backend/package/yuxi/agents/__init__.py` 中,系统会遍历 `backend/package/yuxi/agents` 目录,自动注册所有继承自 `BaseAgent` 的类。这意味着开发者只需要按照规范编写代码,智能体就会自动被系统识别,无需手动配置。 +仓库中已经内置了可直接参考的智能体: -仓库预置了几个可以直接使用的智能体示例: +- `chatbot`:通用对话智能体 +- `deep_agent`:深度分析智能体 -- **chatbot**:通用对话智能体,支持动态工具调度 -- **reporter**:报表生成智能体,演示多工具协作 -- **deep_agent**:深度分析智能体,支持复杂推理任务 +## 2. Agent 的代码组织 -这些示例展示了如何组织代码结构、如何定义上下文、如何组合中间件,新增智能体时可以作为参考。 +建议在 `backend/package/yuxi/agents` 下按包组织一个智能体: -## 创建自定义智能体 - -### 目录结构 - -在 `backend/package/yuxi/agents` 目录下创建新的智能体包,建议保持以下结构: - -``` +```text backend/package/yuxi/agents/ └── my_agent/ - ├── __init__.py # 暴露主类 - ├── graph.py # Graph 构造逻辑 - └── metadata.toml # 元数据配置(可选) + ├── __init__.py + ├── context.py + └── graph.py ``` -### 基本实现 +最小实现通常包含: -智能体类需要继承 `BaseAgent` 并实现异步的 `get_graph` 方法: +- 一个继承 `BaseAgent` 的主类 +- 一个 `context_schema` +- 一个 `get_graph()` 实现 + +示例: ```python -from yuxi.agents import BaseAgent -from langgraph.prebuilt import create_agent +from yuxi.agents import BaseAgent, BaseContext, load_chat_model +from langchain.agents import create_agent + class MyAgent(BaseAgent): - async def get_graph(self, **kwargs): - # 获取配置上下文 - context = self.get_context() + name = "我的智能体" + description = "示例智能体" + context_schema = BaseContext - # 获取工具列表 - tools = await get_tools_from_context(context) - - # 构建 LangGraph 图 + async def get_graph(self, context=None, **kwargs): + context = context or self.context_schema() graph = create_agent( model=load_chat_model(context.model), - tools=tools, + system_prompt=context.system_prompt, checkpointer=await self._get_checkpointer(), ) - return graph ``` -### 能力配置 +## 3. Context 是配置模型,不只是运行时参数 -`capabilities` 属性用于声明智能体的前端能力,控制 UI 组件的显示: +### 3.1 `BaseContext` 的角色 + +`BaseContext` 定义在 `backend/package/yuxi/agents/context.py`,它不是一个普通的数据类,而是整个智能体配置链路的核心: + +- 它定义了 Agent 可以配置哪些字段 +- 它定义了这些字段在前端如何展示 +- 它也是运行期传入 Graph 和中间件的上下文对象 + +当前基础字段包括: + +| 字段 | 作用 | +| --- | --- | +| `system_prompt` | 系统提示词 | +| `model` | 主模型 | +| `tools` | 启用的内置工具 | +| `knowledges` | 关联知识库 | +| `mcps` | 启用的 MCP 服务器 | +| `skills` | 关联 Skills | +| `subagents_model` | 子智能体默认模型 | +| `subagents` | 启用的子智能体 | +| `summary_threshold` | 摘要触发阈值 | +| `thread_id` / `user_id` | 运行期标识,不作为页面配置项暴露 | + +### 3.2 前端配置项如何从 Context 生成 + +`BaseContext.get_configurable_items()` 会遍历字段定义,把字段类型、默认值、描述、模板元数据整理成 `configurable_items`。 + +随后: + +1. `BaseAgent.get_info()` 暴露 `configurable_items` +2. 前端读取 Agent 详情 +3. `AgentConfigSidebar` 按 `template_metadata.kind` 渲染不同控件 + +也就是说,`AgentConfigSidebar` 不是手写每个字段,而是直接消费 `context_schema` 生成的配置描述。 + +这也是为什么: + +- 新增一个 Context 字段,往往会直接影响侧边栏 +- 字段的 `metadata`、`Annotated` 类型信息,会直接影响展示方式 + +### 3.3 `AgentConfigSidebar` 与 AgentConfig 的联动关系 + +这部分是最关键的。 + +在前端: + +- `AgentConfigSidebar.vue` 负责渲染配置表单 +- `agentStore` 加载配置时,读取 `config_json.context` +- 如果某些字段未配置,会用 `configurable_items` 中的默认值补全 +- 保存时,前端将当前表单写回 `config_json: { context: agentConfig }` + +因此真实关系是: + +```text +context_schema + -> get_configurable_items() + -> Agent detail API 返回 configurable_items + -> AgentConfigSidebar 渲染表单 + -> 用户编辑后保存到 config_json.context +``` + +这里需要特别注意两点: + +- **侧边栏展示结构来自 `context_schema`** +- **配置实例值来自数据库中的 `config_json.context`** + +前者决定“能配什么、怎么展示”,后者决定“当前配置实际选了什么”。 + +### 3.4 自定义 Context 的推荐方式 + +如果某个智能体有额外配置,不要在前端单独加一套表单,而是直接扩展 Context: + +```python +from dataclasses import dataclass, field +from yuxi.agents import BaseContext + + +@dataclass(kw_only=True) +class MyAgentContext(BaseContext): + custom_mode: str = field( + default="default", + metadata={ + "name": "运行模式", + "description": "控制智能体的自定义行为", + "options": ["default", "strict"], + }, + ) +``` + +然后在 Agent 中声明: ```python class MyAgent(BaseAgent): - capabilities = ["file_upload", "files", "todo"] # 支持文件上传、文件管理、待办事项 + context_schema = MyAgentContext ``` -**可用能力:** +这会同时影响: -| capability | 说明 | 前端效果 | -|------------|------|----------| -| `file_upload` | 文件上传 | 显示上传按钮 | -| `files` | 文件管理 | 显示文件管理面板 | -| `todo` | 待办事项 | 显示待办组件 | +- 后端可接收的配置结构 +- 前端配置侧边栏的展示内容 +- 运行期 `context` 可访问的字段 -**示例:** +## 4. Context 如何贯穿 Agent 的运行周期 + +Context 的价值不只在“配置页面”。它贯穿了从配置加载到实际执行的整条链路。 + +### 4.1 配置加载阶段 + +在聊天请求进入后端时,服务会先解析 `agent_config_id`,再加载对应配置。 + +当前主流程在 `chat_stream_service.py` 中: + +1. 通过 `agent_config_id` 查找配置 +2. 若未指定,则获取该部门下该 Agent 的默认配置 +3. 取出 `config_json.context` +4. 与 `user_id`、`thread_id` 合并成运行时输入 + +也就是说,运行期 Context 的基础来源并不是前端临时状态,而是数据库中保存的 AgentConfig。 + +### 4.2 Context 实例化阶段 + +`BaseAgent` 在运行前会创建 `context_schema()` 实例,并通过 `update_from_dict()` 注入配置值。 + +这一步完成后,Context 才真正成为运行期对象。 + +可以把它理解为: + +```text +config_json.context + runtime ids -> context_schema instance +``` + +### 4.3 Graph 构建阶段 + +`get_graph(context=context)` 会收到这份 Context。 + +以内置 `chatbot` 为例,Context 会直接参与: + +- 主模型选择:`context.model` +- 系统提示词拼接:`context.system_prompt` +- 子智能体默认模型:`context.subagents_model` +- 子智能体列表:`context.subagents` +- 摘要阈值:`context.summary_threshold` + +因此 Graph 不是和 Context 解耦的。相反,Graph 的构造本身就依赖 Context。 + +### 4.4 中间件运行阶段 + +中间件通过 `request.runtime.context` 或 `runtime.context` 继续读取和修改 Context。 + +例如: + +- `RuntimeConfigMiddleware` + - 读取 `model`、`system_prompt`、`tools`、`mcps` + - 动态覆盖模型、系统提示词和工具列表 +- `SkillsMiddleware` + - 读取 `skills` + - 计算可见技能闭包 + - 将 skills 提示段注入 `system_prompt` + - 在运行期回写 `_visible_skills` +- 文件系统与沙盒接入 + - 通过 `thread_id` 获取对应沙盒 + - 通过 `skills` 决定 `/mnt/skills` 的可见范围 + +所以 Context 既是输入配置,也是中间件共享的运行时状态载体。 + +### 4.5 文件系统与 Viewer 阶段 + +文件系统服务不会重新发明一套配置结构,而是再次从 `config_json.context` 还原出 runtime context,用于: + +- 判断当前线程下 Agent 可见的 Skills +- 构造 Agent 视图的 composite backend +- 构造 Viewer 视图的文件系统展示 + +这也是为什么 Context 不只是聊天链路的一部分,它还影响: + +- Agent 文件工具 +- Viewer 文件浏览器 +- Skills 可见性 +- 沙盒挂载语义 + +### 4.6 恢复运行阶段 + +在 `resume` 流程中,系统同样会重新加载 AgentConfig,并重新构造 Context,再继续执行 Graph。 + +也就是说,无论是: + +- 首次对话 +- 中断恢复 +- 文件系统查看 + +它们都依赖同一份 Context 配置来源。 + +## 5. `capabilities` 的作用 + +`capabilities` 用于声明前端能力开关,控制 UI 组件显示,不等同于 Context。 + +示例: ```python -# 只需要文件上传能力 -capabilities = ["file_upload"] - -# 需要文件上传和待办事项 -capabilities = ["file_upload", "todo"] - -# 全部能力 -capabilities = ["file_upload", "files", "todo"] +class MyAgent(BaseAgent): + capabilities = ["file_upload", "files", "todo"] ``` -注意:即使启用了能力,也需要在中间件中正确配置对应的处理逻辑,功能才能正常工作。例如启用 `file_upload` 需要配合 `inject_attachment_context` 中间件。 +当前常见能力包括: -### 配置文件 +| capability | 说明 | +| --- | --- | +| `file_upload` | 启用上传入口 | +| `files` | 启用文件面板 | +| `todo` | 启用待办能力 | -可以通过 `metadata.toml` 定义智能体的元数据: +它解决的是“页面上显示什么”,而不是“运行时如何配置模型和工具”。 -```toml -name = "我的智能体" -description = "这是一个示例智能体" -examples = [ - "帮我写一首诗", - "解释一下量子计算", -] -``` +## 6. 开发建议 -这些信息会在前端界面展示,帮助用户了解每个智能体的用途。 +### 6.1 新增配置时优先改 Context -## 相关主题 +如果一个配置项会影响 Agent 行为,优先考虑把它做成 `context_schema` 字段,而不是前端单独维护状态。 -- [上下文配置](./context-config.md) - BaseContext 和自定义配置 -- [工具系统](./tools-system.md) - 工具获取机制和 Skills 集成 -- [中间件系统](./middleware.md) - 中间件开发与使用 -- [MCP 集成](./mcp-integration.md) - MCP 服务器配置 -- [SubAgents 管理](./subagents-management.md) - 子智能体配置、调用链和开发注意事项 +### 6.2 把 Graph 逻辑和配置逻辑分开 -## 开发建议 +推荐做法: -### 代码组织 +- `context.py` 定义配置模型 +- `graph.py` 使用这些配置构建 Graph -- 将智能体的核心逻辑放在 `graph.py` 中 -- 复杂的工具逻辑单独放在 `toolkits` 目录下 -- 共享的组件放在 `common` 目录下 +这样前后端联动关系会清晰很多。 -### 热重载 +### 6.3 把“配置来源”和“运行时状态”区分开 -在容器环境中,修改代码后会自动触发热重载。如果需要强制刷新,可以调用: +建议始终区分两层语义: -```python -agent_manager.get_agent(, reload=True) -``` +- `config_json.context`:持久化配置来源 +- `runtime.context`:实际运行对象,可能被中间件继续补充或修改 -### 调试技巧 +## 7. 相关主题 -1. 使用前端的「调试面板」查看详细的请求和响应 -2. 查看后端日志:`docker logs api-dev -f` -3. 利用 LangGraph 的可视化能力理解图结构 - ---- - -智能体系统的设计目标是让开发者能够快速构建和迭代 AI 应用。通过本文档介绍的概念和示例,你应该能够掌握创建自定义智能体的核心方法。遇到问题时,建议先参考预置智能体的实现,它们涵盖了大多数常见场景。 +- [工具系统](./tools-system.md) +- [中间件](./middleware.md) +- [沙盒架构与设计](./sandbox-architecture.md) +- [MCP 集成](./mcp-integration.md) +- [Skills 管理](./skills-management.md) +- [SubAgents 管理](./subagents-management.md) diff --git a/docs/agents/context-config.md b/docs/agents/context-config.md deleted file mode 100644 index 20a6b651..00000000 --- a/docs/agents/context-config.md +++ /dev/null @@ -1,60 +0,0 @@ -# 上下文配置 - -`BaseContext` 是智能体的配置基类,封装了常用的配置字段,定义了智能体的运行时行为。 - -## BaseContext 详解 - -```python -from yuxi.agents import BaseContext -from dataclasses import dataclass - -@dataclass(kw_only=True) -class MyAgentContext(BaseContext): - # 继承以下字段: - # model: str - 使用的语言模型 - # system_prompt: str - 系统提示词 - # tools: list[str] - 启用的工具列表 - # knowledges: list[str] - 关联的知识库 - # mcps: list[str] - 启用的 MCP 服务器 - # skills: list[str] - 关联的 Skills - - # 可在此添加自定义字段 - custom_field: str = "默认值" -``` - -### 字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| model | str | 使用的语言模型 | -| system_prompt | str | 系统提示词 | -| tools | list[str] | 启用的内置工具列表 | -| knowledges | list[str] | 关联的知识库 | -| mcps | list[str] | 启用的 MCP 服务器 | -| skills | list[str] | 关联的 Skills | - -## 自定义工具选项 - -有时需要自定义工具选项,比如 ReporterAgent 需要包含 MySQL 工具: - -```python -from yuxi.agents import BaseContext, get_tool_info -from yuxi.agents.toolkits.buildin import calculator, query_knowledge_graph -from yuxi.agents.toolkits.mysql import get_mysql_tools - -@dataclass(kw_only=True) -class ReporterContext(BaseContext): - tools: Annotated[list[dict], {"__template_metadata__": {"kind": "tools"}}] = field( - default_factory=lambda: [t.name for t in get_mysql_tools()], - metadata={ - "name": "工具", - "options": lambda: get_tool_info( - [calculator, query_knowledge_graph, _create_tavily_search()] + get_mysql_tools() - ), - "description": "包含内置工具和 MySQL 工具包。", - }, - ) - - def __post_init__(self): - self.mcps = ["mcp-server-chart"] # 默认启用图表 MCP -``` diff --git a/docs/agents/sandbox-architecture.md b/docs/agents/sandbox-architecture.md new file mode 100644 index 00000000..1ea9bd98 --- /dev/null +++ b/docs/agents/sandbox-architecture.md @@ -0,0 +1,569 @@ +# Yuxi 沙盒架构与设计 + +## 文档说明 + +本文描述的是 **Yuxi 当前已经落地的沙盒实现**,目标是解释它的职责边界、系统结构、运行机制与工程限制。 + +这不是理想化方案说明,也不是 API 参考手册。阅读本文时,应始终基于一个前提: + +> Yuxi 当前的沙盒,定位是“为线程级 Agent 运行与文件访问提供受控工作区”,而不是“对外承诺强安全边界的通用多租户沙箱平台”。 + +::: warning 预发布 +此部分涉及的技术路线与实现细节仍可能调整,文档会随实现演进而更新。 +::: + +## 1. 设计目标与边界 + +### 1.1 设计目标 + +Yuxi 的沙盒主要服务于以下场景: + +- 为每个对话线程提供隔离的工作目录 +- 为 Agent 提供有限的命令执行能力 +- 为 Agent 文件工具提供受控文件系统访问 +- 为工作台文件浏览器提供真实目录浏览与文件读取能力 +- 将 Skills 以只读形式暴露给 Agent 与工作台 + +### 1.2 非目标 + +当前实现明确不覆盖以下目标: + +- 强对抗场景下的高强度多租户隔离 +- 完整的资源配额与调度系统 +- 持久化的沙盒控制平面 +- 审计级命令记录与合规追踪 +- 面向终端用户的完整文件管理系统 + +这一点不是缺陷描述,而是范围定义。很多工程选择都建立在这个前提上。 + +## 2. 总体设计结论 + +当前沙盒方案可以概括为五个关键词: + +- **线程级隔离**:资源归属以 `thread_id` 为核心,采用 `thread_id -> sandbox_id` 的映射 +- **容器级执行**:默认使用独立 Docker 容器承载执行环境 +- **固定命名空间**:Agent 与工作台通过 `/mnt/user-data`、`/mnt/skills` 访问文件系统 +- **惰性获取**:不再依赖公开的 `/api/sandbox/*` 生命周期接口,首次使用时自动获取沙盒 +- **双视图接入**:Agent 与工作台共享底层沙盒,但使用不同的文件系统语义与接口 + +这意味着 Yuxi 当前沙盒的本质是: + +- 一个线程隔离的容器工作区 +- 一个受限的文件访问模型 +- 一个围绕 Agent 与 Viewer 场景设计的工程化执行环境 + +## 3. 系统架构 + +### 3.1 架构分层 + +从职责上,当前实现可以分为四层: + +1. **Provisioner 层** + 负责创建、发现、复用和销毁沙盒实例。 + +2. **Executor 层** + 负责在已经存在的沙盒中执行命令、读写文件、列目录、下载文件。 + +3. **接入层** + 负责把沙盒能力接入 Agent 文件系统与工作台文件浏览器。 + +4. **HTTP/UI 层** + 负责将文件浏览能力暴露给前端页面。 + +### 3.2 关键模块 + +核心实现主要位于以下位置: + +- `backend/package/yuxi/agents/backends/sandbox/` +- `backend/package/yuxi/agents/backends/composite.py` +- `backend/package/yuxi/services/filesystem_service.py` +- `backend/package/yuxi/services/viewer_filesystem_service.py` +- `backend/server/routers/filesystem_router.py` + +### 3.3 调用关系 + +```text +Agent / Frontend + | + +-- Agent filesystem tool + | | + | +-- create_agent_composite_backend(...) + | | + | +-- default: sandbox backend + | +-- route /mnt/skills/: readonly skills backend + | + +-- Viewer filesystem panel + | + +-- /api/viewer/filesystem/* + | + +-- viewer_filesystem_service + | + +-- thread ownership check + +-- runtime context / selected skills + +-- provider.acquire(thread_id) + +Sandbox Provider + | + +-- LocalContainerBackend + | | + | +-- create/discover/destroy docker container + | +-- YuxiSandboxBackend executes inside container + | + +-- RemoteSandboxBackend + | + +-- delegate lifecycle to remote provisioner +``` + +## 4. 核心抽象与职责划分 + +这一部分是理解整套实现的关键。当前代码中有多个名字相近的 backend,但职责并不相同。 + +### 4.1 `SandboxBackend` + +定义于 `sandbox_provisioner_base.py`,是生命周期管理接口,负责回答四个问题: + +- 如何创建沙盒 +- 如何销毁沙盒 +- 沙盒是否存活 +- 是否可以发现已有沙盒 + +它只关心 **实例生命周期**,不关心文件访问和命令执行。 + +### 4.2 `LocalContainerBackend` + +定义于 `sandbox_local_container.py`,是默认实现,负责: + +- 使用 `docker run` 创建本地沙盒容器 +- 使用 `docker stop` 销毁容器 +- 发现现存的 `yuxi-sandbox-*` 容器 +- 在本地模式下管理端口、容器命名与 warm pool + +它的定位是 **本地容器管理器**。 + +### 4.3 `RemoteSandboxBackend` + +定义于 `sandbox_remote.py`,负责通过 HTTP 与远程 provisioner 交互。 + +它解决的问题是:如果运行环境不适合由当前进程直接管理 Docker 容器,就把生命周期操作交给外部服务。 + +它的定位是 **远程生命周期代理**。 + +### 4.4 `YuxiSandboxBackend` + +定义于 `sandbox_executor.py`,继承 deepagents 的 `BaseSandbox`,负责: + +- 在容器内执行 shell 命令 +- 读写和编辑文件 +- 列目录、glob、上传、下载 +- 进行路径标准化与输出遮蔽 + +它不负责创建和销毁容器。它工作的前提是:沙盒实例已经存在。 + +它的定位是 **执行器**,不是生命周期管理器。 + +### 4.5 `YuxiSandboxProvider` + +定义于 `sandbox_provisioner.py`,是实际的协调者,负责: + +- 维护 `thread_id -> sandbox_id` 的内存态映射 +- 首次访问时创建或发现沙盒 +- 重复访问时复用已有沙盒 +- `release()` 后放入 warm pool +- 在超时或进程退出时回收资源 + +它的定位是 **统一入口与资源调度器**。 + +## 5. 运行模式与部署拓扑 + +### 5.1 默认模式:本地 Docker 容器 + +在默认开发环境中,系统拓扑如下: + +```text +宿主机 +├── Docker Daemon +│ ├── api-dev +│ ├── web-dev +│ └── yuxi-sandbox- +└── project workspace + +api-dev 容器 +└── Yuxi 后端进程 + └── 通过 docker CLI 或 Docker API 管理 yuxi-sandbox-* 容器 +``` + +关键点: + +- `api-dev` 不直接执行用户命令 +- 用户命令实际运行在独立的 `yuxi-sandbox-*` 容器中 +- 后端承担的是控制面职责,而不是执行面职责 + +### 5.2 远程 provisioner 模式 + +如果设置了 `YUXI_SANDBOX_PROVISIONER_URL`,provider 会切换为 `RemoteSandboxBackend`。 + +此时: + +- 当前进程不直接创建本地沙盒容器 +- 生命周期由远程服务负责 +- 本地主路径不再是 Docker 容器管理,而是 HTTP 协调 + +当前主路径与主要测试覆盖仍以本地容器模式为主。 + +### 5.3 Docker CLI 与 Docker API 双路径 + +当前实现支持两种与 Docker 交互的方式: + +- 优先使用 Docker CLI +- 当环境中不可用时,回退到 Docker Unix Socket API + +这是一种工程兼容设计,目的是提升不同运行环境下的可用性。 + +## 6. 生命周期模型 + +### 6.1 线程级资源归属 + +当前资源归属模型是: + +```text +thread_id -> deterministic sandbox_id -> sandbox instance +``` + +`sandbox_id` 由 `thread_id` 的 SHA256 截断生成。这样做有两个直接收益: + +- 同一个线程重复访问时更容易复用沙盒 +- provider 重启后可以通过 `discover(sandbox_id)` 找回仍然存活的容器 + +### 6.2 `acquire()` 逻辑 + +`provider.acquire(thread_id)` 的核心流程如下: + +1. 检查当前线程是否已绑定活跃沙盒 +2. 若无,检查 warm pool 中是否存在对应 `sandbox_id` +3. 若无,尝试 `discover(sandbox_id)` 发现已有容器 +4. 若仍不存在,则创建新沙盒 +5. 将结果注册到 provider 的内存态映射中 + +这套流程兼顾了: + +- 同线程复用 +- provider 重建后的容器发现 +- 空闲沙盒快速回收后的再利用 + +### 6.3 `release()` 与 `destroy()` + +两者语义不同: + +- `release(thread_id)`:解除线程绑定,把实例放入 warm pool,等待后续复用或超时清理 +- `destroy(thread_id)`:直接销毁对应实例,不再保留 + +这一区分是为了兼顾响应速度和资源回收。 + +### 6.4 空闲回收 + +provider 内部有空闲检查线程,按固定周期清理: + +- 长时间无访问的活跃沙盒 +- 已经进入 warm pool 且超时的沙盒 + +相关默认值定义在 `sandbox_config.py`: + +- `DEFAULT_IDLE_TIMEOUT = 600` +- `IDLE_CHECK_INTERVAL = 60` + +### 6.5 为什么不再依赖 `/api/sandbox/*` + +当前设计不再暴露公开的沙盒生命周期 API,原因很明确: + +- 生命周期属于内部资源管理逻辑 +- 前端不应承担“先准备沙盒、再访问文件”的协调责任 +- 惰性获取可以减少状态同步与接口耦合 + +因此当前模式是: + +- 首次访问文件系统时自动 `acquire()` +- 不再要求前端显式调用沙盒准备接口 + +## 7. 文件系统模型 + +### 7.1 虚拟命名空间 + +当前沙盒对外暴露两个核心命名空间: + +- `/mnt/user-data` +- `/mnt/skills` + +其中: + +- `/mnt/user-data` 是线程私有工作区 +- `/mnt/skills` 是可见 Skills 的只读视图 + +### 7.2 `user-data` 目录结构 + +每个线程会在宿主机侧创建自己的 `user-data` 根目录,并确保以下子目录存在: + +- `workspace` +- `outputs` +- `uploads` +- `large_tool_results` +- `uploads/attachments` + +这套目录最终映射到容器内的 `/mnt/user-data/*`。 + +### 7.3 路径别名 + +为了兼容 Agent 使用习惯,当前实现保留了若干路径别名,会被统一映射到 `/mnt` 命名空间,例如: + +- `/workspace` -> `/mnt/user-data/workspace` +- `/outputs` -> `/mnt/user-data/outputs` +- `/uploads` -> `/mnt/user-data/uploads` +- `/attachments` -> `/mnt/user-data/uploads/attachments` +- `/skills` -> `/mnt/skills` + +这些别名是兼容层,不是推荐的长期抽象。新的系统语义应以 `/mnt/*` 为准。 + +### 7.4 路径安全约束 + +路径校验主要由 `path_security.py` 负责。核心规则如下: + +- 仅允许访问 `/mnt/user-data` 与 `/mnt/skills` +- 禁止路径穿越 +- 命令中的绝对路径只允许 `/mnt/*` 与少量系统运行时前缀 + +允许的系统前缀主要包括: + +- `/bin/` +- `/usr/bin/` +- `/usr/local/bin/` +- `/usr/lib/` + +这类放行是为了保证容器内基础命令与运行时依赖可用。 + +### 7.5 输出遮蔽 + +执行结果在返回前会做宿主机路径遮蔽,避免把宿主机真实路径直接暴露给 Agent 或前端。 + +这一步不是强安全措施,但它对保持虚拟路径语义一致非常重要。 + +## 8. Agent 与 Viewer 的双视图设计 + +### 8.1 为什么需要双视图 + +当前文件系统接入并不是一套接口复用到底,而是明确拆成两种语义: + +- **Agent 视图** + 面向工具调用与 prompt 约束,强调“模型可以怎样访问文件” + +- **Viewer 视图** + 面向工作台浏览、读取和下载,强调“用户可以怎样查看文件” + +这不是重复建设,而是避免语义错位。 + +### 8.2 Agent 侧接入 + +Agent 侧通过 `create_agent_composite_backend(...)` 构建 composite backend: + +- 默认 backend 为 sandbox backend +- `/mnt/skills/` 路由到只读的 `SelectedSkillsReadonlyBackend` + +这保证了: + +- 线程工作区来自沙盒 +- Skills 只读且受当前 agent 配置约束 + +### 8.3 Viewer 侧接入 + +工作台不再直接复用 Agent backend,而是通过 `viewer_filesystem_service.py` 暴露独立能力: + +- `/api/viewer/filesystem/tree` +- `/api/viewer/filesystem/file` +- `/api/viewer/filesystem/download` + +它的特点是: + +- 只暴露浏览、读取、下载语义 +- 根目录视图明确展示 `user-data` 与 `skills` +- Skills 可见范围仍受当前 agent config 约束 + +### 8.4 旧 `/api/filesystem/*` 的定位 + +当前仍保留旧的 Agent 文件系统接口: + +- `/api/filesystem/ls` +- `/api/filesystem/cat` + +它们的定位是: + +- 服务于 Agent 语义下的文件浏览 +- 使用 composite backend +- 保持与 Agent 可见文件系统一致 + +因此,不应把它视为工作台浏览器的长期接口。 + +## 9. Skills 的挂载与可见性 + +Skills 不是简单的宿主机目录透传,而是受运行时上下文控制的只读视图。 + +当前模型有两个关键点: + +- Skills 物理上以只读挂载方式进入沙盒 +- 逻辑上只有当前 agent config 选中的 skills 对 Agent 和 Viewer 可见 + +这意味着: + +- “挂载存在”不等于“对当前线程可见” +- Viewer 与 Agent 必须共享同一套可见性规则 + +这是当前设计中很重要的一条一致性约束。 + +## 10. 典型执行流程 + +### 10.1 Agent 首次访问文件系统 + +```text +Agent tool call + -> resolve_sandbox_backend(thread_id) + -> provider.acquire(thread_id) + -> create or discover sandbox + -> build composite backend + -> execute ls/read/write/command inside sandbox +``` + +### 10.2 工作台首次打开文件浏览器 + +```text +Viewer request + -> /api/viewer/filesystem/tree + -> verify thread ownership + -> load runtime context and selected skills + -> provider.acquire(thread_id) + -> list sandbox or skills namespace +``` + +### 10.3 读取与下载文件 + +读取和下载都遵循同样的原则: + +- `user-data` 路径走 sandbox backend +- `skills` 路径走只读 skills backend +- 非 `/mnt` 命名空间路径会被拒绝 + +## 11. 配置项 + +沙盒核心配置定义于 `sandbox_config.py`。常用项如下: + +| 配置项 | 作用 | +| --- | --- | +| `YUXI_SANDBOX_IMAGE` | 沙盒镜像 | +| `YUXI_SANDBOX_BASE_PORT` | 本地沙盒基础端口 | +| `YUXI_SANDBOX_CONTAINER_PREFIX` | 容器名前缀 | +| `YUXI_SANDBOX_IDLE_TIMEOUT` | 空闲超时时间 | +| `YUXI_SANDBOX_MAX_REPLICAS` | 最大并发沙盒数 | +| `YUXI_SANDBOX_HOST` | 宿主机访问沙盒容器的地址 | +| `YUXI_SANDBOX_PROVISIONER_URL` | 远程 provisioner 地址 | +| `YUXI_SANDBOX_SECURITY_OPTS` | 传递给容器的安全选项 | +| `YUXI_HOST_PROJECT_DIR` | 宿主机项目目录映射辅助配置 | +| `YUXI_DOCKER_API_BASE` | Docker API base URL | +| `YUXI_DOCKER_API_SOCKET` | Docker Unix Socket 路径 | + +## 12. 测试与验证 + +当前与沙盒相关的测试主要覆盖三类问题: + +### 12.1 后端与 provider 行为 + +- `backend/test/test_sandbox_backends.py` + +覆盖内容包括: + +- composite backend 与 sandbox backend 的接入关系 +- provider warm pool 与生命周期逻辑 +- remote backend 的基础校验 +- 路径规范化与异常输入处理 + +### 12.2 文件系统接口 + +- `backend/test/api/test_filesystem_router.py` +- `backend/test/api/test_viewer_filesystem_router.py` + +覆盖内容包括: + +- Agent 视图与 Viewer 视图接口行为 +- 用户权限与线程归属校验 +- 目录浏览、文件读取与错误分支 + +### 12.3 端到端验证 + +- `backend/test/api/test_sandbox_e2e.py` +- `backend/test/api/test_viewer_filesystem_e2e.py` + +覆盖内容包括: + +- 真实起沙盒容器 +- 在容器内执行命令 +- 验证文件写入、读取、附件复制与 viewer 行为 + +测试中会主动清理 `yuxi-sandbox-*` 容器,这本身也说明当前方案依赖真实容器资源,而不是纯内存 mock。 + +## 13. 当前限制 + +### 13.1 安全强度是工程化的,不是强安全承诺 + +当前实现通过容器隔离、路径约束、命名空间限制来降低风险,但它并不等同于强对抗场景下的高强度沙箱。 + +### 13.2 Provider 状态未持久化 + +当前 `YuxiSandboxProvider` 的核心状态保存在进程内存中,例如: + +- `_sandboxes` +- `_sandbox_infos` +- `_thread_sandboxes` +- `_warm_pool` + +这意味着它更适合当前产品架构,而不是完整的分布式控制面设计。 + +### 13.3 Viewer 仍以只读为主 + +当前工作台文件浏览器主要提供: + +- 浏览 +- 读取 +- 下载 + +它不是完整的文件操作终端,也不是通用文件管理器。 + +### 13.4 兼容路径仍然存在 + +`/workspace`、`/uploads`、`/attachments` 等别名仍在使用,这说明路径模型尚处于兼容收敛阶段。 + +### 13.5 本地容器模式仍有工程折中 + +例如: + +- 依赖 Docker 运行环境 +- 兼容 Docker CLI 与 Docker API 双路径 +- 依赖本地资源条件与端口可用性 + +这些都属于工程现实,而不是抽象层面的完美设计。 + +## 14. 后续演进方向 + +后续如需继续完善,优先方向应是: + +1. **强化控制面** + 将沙盒元数据、生命周期和回收策略从单进程状态进一步抽离。 + +2. **收敛路径模型** + 逐步减少历史别名,统一到 `/mnt/user-data` 与 `/mnt/skills`。 + +3. **强化安全边界** + 在容器运行时、权限模型、配额控制和审计能力上继续补强。 + +4. **继续区分 Agent 视图与用户视图** + 保持两类接口语义清晰,避免再次把 Viewer 语义挤回 Agent backend。 + +## 15. 总结 + +Yuxi 当前的沙盒方案,本质上是一套 **线程级容器工作区 + `/mnt` 命名空间 + provider 惰性获取 + Agent/Viewer 双视图接入模型**。 + +它已经能够较好支撑当前产品中的 Agent 文件工具、工作台文件浏览和 Skills 只读挂载场景,但它依然是 **面向当前业务目标的工程化沙盒**,不是已经完成强安全、强控制面建设的通用沙箱平台。 diff --git a/docs/agents/tools-system.md b/docs/agents/tools-system.md index 842ee510..c55041b6 100644 --- a/docs/agents/tools-system.md +++ b/docs/agents/tools-system.md @@ -1,32 +1,12 @@ # 工具系统 -Yuxi 提供了统一的工具获取机制,支持多种工具类型的动态组装。 - -## 工具获取机制 - -系统提供统一的工具获取入口 `get_tools_from_context(context)`,它会自动组装三类工具: - -1. **基础工具**:从 `context.tools` 筛选的内置工具 -2. **知识库工具**:根据 `context.knowledges` 自动生成检索工具 -3. **MCP 工具**:根据 `context.mcps` 加载并过滤的 MCP 服务器工具 - -```python -from yuxi.agents.tools import get_tools_from_context - -async def get_graph(self, **kwargs): - context = self.get_context() - tools = await get_tools_from_context(context) -``` +Yuxi 的工具系统基于注册机制,支持多种工具类型的动态组装。 ## 工具注册机制 -Yuxi 的工具系统基于注册机制而非继承体系,这一点与 LangChain 原生的 `@tool` 装饰器有本质区别。 +Yuxi 的工具系统采用 `@tool` 装饰器注册机制,核心位于 `backend/package/yuxi/agents/toolkits/registry.py`。 -LangChain 的 `@tool` 装饰器通常需要继承特定基类或实现特定接口,创建的工具有着强烈的框架耦合。而 Yuxi 的工具注册表是一个独立的全局注册中心,任何符合规范的函数都可以通过 `@tool` 装饰器注册到系统中,无需继承任何基类,也不需要了解框架内部实现。 - -需要特别说明的是,Yuxi 的 `@tool` 装饰器并非全新实现,而是基于 LangChain 原生 `@tool` 的扩展。装饰器的核心逻辑继承自 LangChain,新增了 `category`、`tags`、`display_name` 等元数据字段用于前端展示和分类,原有的 LangChain 特性(如函数参数注解、描述文档等)完全兼容。 - -注册表的核心位于 `backend/package/yuxi/agents/common/toolkits/registry.py`,它维护着一个全局的工具实例列表。当系统启动时,所有导入 `toolkits` 包的模块都会自动执行其内部的工具注册逻辑,这意味着开发者只需要在自己的模块中添加装饰器,工具就会自动被发现和使用。 +### @tool 装饰器 ```python from yuxi.agents.toolkits.registry import tool @@ -34,29 +14,86 @@ from yuxi.agents.toolkits.registry import tool @tool(category="buildin", tags=["计算"], display_name="计算器") def calculator(a: float, b: float, operation: str) -> float: """计算器:对给定的2个数字进行基本数学运算""" - if operation == "add": - return a + b - # ... + ... ``` -使用这个装饰器时,需要指定 `category` 和 `tags`,前者用于工具分类,后者用于前端展示。装饰器内部仍然调用 LangChain 的工具封装逻辑,因此 LangChain 工具的所有特性(如多参数支持、参数类型注解等)都保持兼容。 +装饰器参数: +- **category**: 工具分类,用于分组(`buildin`、`mysql`、`debug`) +- **tags**: 标签列表,用于前端展示 +- **display_name**: 显示名称(给人看的名字) +- **icon**: 图标名称(可选) -获取工具时,通过 `get_all_tool_instances()` 可以拿到所有已注册的工具实例列表,这个函数会被 `get_tools_from_context` 调用,根据上下文配置筛选出需要使用的工具。 +### 自动发现 -## 内置工具 +导入 `toolkits` 包时会自动触发注册: -系统内置了几类常用工具。计算类包括 calculator,可进行加减乘除运算。搜索类包括 tavily_search,需要在环境变量中配置 `TAVILY_API_KEY` 才能启用。知识图谱类包括 query_knowledge_graph,用于查询通过三元组导入的全局知识图谱。交互类包括 ask_user_question,用于在智能体执行过程中向用户发起交互式提问,当前协议支持一次提交 `questions` 数组并返回 `{question_id: answer}` 的批量答案映射。数据库类包括 mysql_list_tables、mysql_describe_table 和 mysql_query,用于连接和查询 MySQL 数据库。 +```python +from yuxi.agents.toolkits import buildin, mysql # 触发 @tool 装饰器执行 +``` -这些工具都通过上述注册机制自动加载,开发者无需手动引入。 +`toolkits/__init__.py` 中已包含 `buildin`、`mysql`、`debug` 模块的导入,这些模块加载时会自动注册所有带 `@tool` 装饰器的函数。 -## 知识库工具 +## 工具分类 -与内置工具不同,知识库工具是动态生成的。当在智能体配置中指定 `context.knowledges` 时,系统会根据指定的 knowledge 名称动态创建对应的检索工具。这种设计使得知识库工具不需要预先注册,而是在运行时按需生成。 +### 内置工具 (buildin) + +| 工具 | 说明 | +|------|------| +| `calculator` | 计算器,支持加减乘除 | +| `ask_user_question` | 向用户发起交互式提问 | +| `query_knowledge_graph` | 查询知识图谱三元组 | +| `text_to_img_qwen_image` | 使用 Qwen-Image 生成图片 | +| `tavily_search` | Tavily 网页搜索(需配置 `TAVILY_API_KEY`) | + +### MySQL 工具 (mysql) + +| 工具 | 说明 | +|------|------| +| `mysql_list_tables` | 列出数据库中所有表 | +| `mysql_describe_table` | 获取表结构信息 | +| `mysql_query` | 执行只读 SQL 查询 | + +### 知识库工具 (kbs) + +知识库工具通过 `get_common_kb_tools()` 获取,不通过 `@tool` 装饰器注册: ```python from yuxi.agents.toolkits.kbs import get_common_kb_tools -kb_tools = get_common_kb_tools(knowledge_names=["kb1", "kb2"]) +kb_tools = get_common_kb_tools() +# 返回: [list_kbs, get_mindmap, query_kb] +``` + +| 工具 | 说明 | +|------|------| +| `list_kbs` | 列出用户可访问的知识库 | +| `get_mindmap` | 获取知识库的思维导图结构 | +| `query_kb` | 在指定知识库中检索内容 | + +## 工具组装 + +工具组装在 `RuntimeConfigMiddleware` 中完成。根据上下文配置筛选工具: + +1. **基础工具**:从 `context.tools` 中按名称筛选 +2. **MCP 工具**:根据 `context.mcps` 加载 MCP 服务器工具 +3. **知识库工具**:由 `KnowledgeBaseMiddleware` 独立处理 + +```python +# 中间件中的工具筛选逻辑 +async def get_tools_from_context(self, context) -> list: + selected_tools = [] + + # 1. 基础工具 + for tool_name in context.tools or []: + if tool_name in tools_map: + selected_tools.append(tools_map[tool_name]) + + # 2. MCP 工具 + for server_name in context.mcps or []: + mcp_tools = await get_enabled_mcp_tools(server_name) + selected_tools.extend(mcp_tools) + + return selected_tools ``` ## Skills 集成 diff --git a/docs/changelog/contributing.md b/docs/changelog/contributing.md deleted file mode 100644 index 16a456b9..00000000 --- a/docs/changelog/contributing.md +++ /dev/null @@ -1,120 +0,0 @@ -# 参与贡献 - -感谢你对 Yuxi 项目的兴趣!我们欢迎任何形式的贡献,包括但不限于代码提交、功能建议、问题反馈和文档改进。 - - - 贡献者名单 - - -## 贡献流程 - -### 1. Fork 项目 - -在 GitHub 上点击 Fork 按钮,将项目复制到你的账户。 - -### 2. 创建功能分支 - -```bash -git checkout -b feature/amazing-feature -``` - -### 3. 开发并提交 - -```bash -git commit -m 'feat: 添加新功能' -``` - -### 4. 推送代码 - -```bash -git push origin feature/amazing-feature -``` - -### 5. 创建 Pull Request - -在 GitHub 上创建 PR,详细描述你的更改内容和动机。 - -## 代码规范 - -项目对代码质量有一定要求,提交前请确保: - -- Python 代码使用 `make format` 格式化 -- 使用 `make lint` 检查代码质量 -- 添加必要的测试用例 -- 更新相关文档 - -## 提交信息规范 - -使用清晰规范的提交信息: - -``` -feat: 添加新功能 -fix: 修复 bug -docs: 更新文档 -style: 代码格式调整 -refactor: 代码重构 -test: 添加测试 -chore: 构建过程或辅助工具的变动 -``` - -## Bug 修复发布流程 - -当发布后发现 bug 需要修复时: - -### 情况 1:main 上没有未完成的新功能 - -直接在 main 修复并发布: - -```bash -git commit -m "fix: 解决配置解析器崩溃问题" -git tag -a v0.3.1 -m "Hotfix v0.3.1" -git push origin main --tags -``` - -### 情况 2:main 上已有新功能未完成 - -从上一个 tag 建立 hotfix 分支: - -```bash -git checkout -b hotfix/0.3.1 v0.3.0 -# 修复问题 -git commit -m "fix: 解决配置解析器崩溃问题" -git push origin hotfix/0.3.1 - -# 测试后合并回 main 并打 tag -git checkout main -git merge --no-ff hotfix/0.3.1 -git tag -a v0.3.1 -m "Hotfix v0.3.1" -git push origin main --tags - -# 删除临时分支 -git branch -d hotfix/0.3.1 -git push origin --delete hotfix/0.3.1 -``` - -## 测试指南 - -### 运行测试 - -```bash -# 全量路由测试 -make router-tests - -# 运行特定测试 -make router-tests PYTEST_ARGS="-k knowledge_router" - -# 直接运行 pytest -uv run --group test pytest test/api -vv -``` - -### 测试配置 - -首次运行测试前,需要配置测试凭据: - -```bash -cp test/.env.test.example test/.env.test -``` - ---- - -感谢每一位贡献者的付出! diff --git a/docs/changelog/faq.md b/docs/changelog/faq.md deleted file mode 100644 index 88dd37db..00000000 --- a/docs/changelog/faq.md +++ /dev/null @@ -1,98 +0,0 @@ -# 常见问题 - -以下是 Yuxi 在安装和使用过程中最常见的问题及其解决方案。 - -## Docker 与启动问题 - -### 镜像拉取或构建失败 - -**镜像拉取问题**: - -```bash -# Linux/macOS -bash scripts/pull_image.sh - -# Windows PowerShell -powershell -ExecutionPolicy Bypass -File scripts/pull_image.ps1 -``` - -**构建失败问题**: - -如果配置了代理仍然失败,尝试以下步骤: - -1. 注释 `api.Dockerfile` 中的代理配置 -2. 注释 `docker-compose.yml` 中的代理构建参数 -3. 添加国内镜像源加速: - -```dockerfile -RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --no-dev --index-url https://pypi.tuna.tsinghua.edu.cn/simple -``` - -### 服务启动失败 - -1. 检查端口占用:`lsof -i :5050` 或 `netstat -tuln | grep 5050` -2. 确认 Docker 服务状态 -3. 查看日志定位问题: - ```bash - docker logs --tail=100 api-dev - docker logs --tail=100 web-dev - ``` - -### 数据库服务问题 - -**Milvus / Neo4j 启动失败**: - -```bash -# 重启服务 -docker compose up milvus -d && docker restart api-dev -``` - -**Neo4j 连接信息**: -- 用户名:neo4j -- 密码:0123456789 -- 管理界面:http://localhost:7474 - -### 账号相关问题 - -**首次运行创建管理员**: - -Web 首次启动会引导初始化。也可以通过 API 创建: - -```bash -# 检查是否首次运行 -GET /api/auth/check-first-run - -# 初始化管理员账号 -POST /api/auth/initialize -# Body: {"user_id": "your_username", "password": "your_password"} -``` - -### 日志查看 - -```bash -# 查看所有容器状态 -docker ps - -# 查看实时日志 -docker logs api-dev -f -docker logs web-dev -f - -# 查看所有服务日志 -docker compose logs --tail=100 -``` - -## 功能使用问题 - -### OCR 服务不可用 - -- **RapidOCR**:确保 `MODEL_DIR/SWHL/RapidOCR` 下存在 `PP-OCRv4` 模型 -- **MinerU / PP-Structure-V3**:检查 GPU 和 CUDA 版本是否兼容 - -### 登录失败被锁定 - -多次登录失败会临时锁定账户,请根据页面提示等待后重试。 - ---- - -如果以上问题无法解决你的问题,欢迎在 GitHub Issues 中提问。 diff --git a/docs/changelog/migrate_to_v0-5.md b/docs/changelog/migrate_to_v0-5.md deleted file mode 100644 index 283420db..00000000 --- a/docs/changelog/migrate_to_v0-5.md +++ /dev/null @@ -1,108 +0,0 @@ -# v0.5 数据迁移指南 - -v0.5 版本将数据存储从 SQLite + JSON 文件迁移到 PostgreSQL。本指南帮助你完成数据迁移。 - -::: tip warning -迁移脚本可能会存在问题,不建议在生产环境下尝试,生产环境下,请新建或仔细检查迁移脚本,慎重迁移。 -::: - -## 迁移内容 - -| 数据类型 | 源 | 目标 | 存储内容 | -|---------|-----|------|---------| -| 业务数据 | SQLite (`saves/database/server.db`) | PostgreSQL | 用户、部门、对话、消息、工具调用、MCP 服务器等 | -| 知识库元数据 | JSON 文件 (`saves/knowledge_base_data/`) | PostgreSQL | 知识库配置、文件信息、评估数据 | -| Tasker 任务记录 | JSON 文件 (`saves/tasks/tasks.json`) | PostgreSQL | 后台任务状态、进度、结果(独立存储) | - -## 迁移前准备 - -### 1. 启动服务 - -```bash -docker compose up -d --build -``` - -### 2. 备份数据 - -**重要:** 迁移前必须备份数据! - -```bash -# 备份 saves 目录(包含 SQLite 数据库和知识库元数据) -cp -r saves saves_backup_$(date +%Y%m%d) - -# 如果使用外部数据库,也请备份 PostgreSQL -pg_dump -U postgres -d yuxi_know > pg_backup_$(date +%Y%m%d).sql -``` - -### 3. 确保 PostgreSQL 已启动 - -```bash -docker compose up -d postgres -# 等待健康检查通过 -``` - -## 执行迁移 - -### 方式一:使用统一迁移脚本(推荐) - -```bash -# 1. 预览迁移(不执行) -docker compose exec api python scripts/migrate_all.py --dry-run - -# 2. 执行迁移 -docker compose exec api python scripts/migrate_all.py --execute - -# 3. 验证迁移结果 -docker compose exec api python scripts/migrate_all.py --verify -``` - -### 方式二:分阶段迁移 - -```bash -# 只迁移业务数据 -docker compose exec api python scripts/migrate_all.py --execute --stage business - -# 只迁移知识库元数据 -docker compose exec api python scripts/migrate_all.py --execute --stage knowledge - -# 只迁移 Tasker 任务记录 -docker compose exec api python scripts/migrate_all.py --execute --stage tasker -``` - -## 重启服务 - -```bash -docker compose up -d -``` - -## 验证迁移 - -### 检查服务状态 - -```bash -# 查看 API 服务日志 -docker logs api-dev --tail 50 - -# 检查健康状态 -curl http://localhost:5050/api/system/health -``` - -### 验证数据 - -```bash -# 使用迁移脚本验证 -docker compose exec api python scripts/migrate_all.py --verify -``` - -预期输出: - -``` -============================================================ -📊 验证结果汇总 -============================================================ -✅ departments: SQLite=X, PostgreSQL=X -✅ users: SQLite=X, PostgreSQL=X -✅ conversations: SQLite=X, PostgreSQL=X -... -全部匹配: ✅ 是 -``` diff --git a/docs/changelog/run-architecture-migration-v4.md b/docs/changelog/run-architecture-migration-v4.md deleted file mode 100644 index 6b229134..00000000 --- a/docs/changelog/run-architecture-migration-v4.md +++ /dev/null @@ -1,185 +0,0 @@ -# Run 流式架构改造说明 - -## 1. 改造目标 - -本次改造将对话输出从 **HTTP 直连流** 升级为 **异步任务执行 + SSE 增量拉取**,目标是: - -1. 页面离开/刷新不影响后台执行。 -2. 前端支持断线重连与续流。 -3. 提升系统稳定性、并发能力与可观测性。 -4. 控制存储成本:过程数据短期保存,结果数据长期保存。 - ---- - -## 2. 改造前后对比(仅与 HTTP 直连流对比) - -| 维度 | 改造前(HTTP 直连流) | 改造后(Run + SSE) | -|---|---|---| -| 触发方式 | `POST /agent/{id}` 后长连接直接流式输出 | `POST /runs` 创建任务,worker 异步执行 | -| 任务生命周期 | 绑定前端连接 | 与前端连接解耦 | -| 页面离开/刷新 | 常导致任务中断或前端丢上下文 | 任务继续执行,前端可续流 | -| 前端消费方式 | 同一个请求内读取 chunk | `GET /runs/{id}/events?after_seq=...` 增量拉取 | -| 恢复能力 | 弱,重连后难恢复 | 强,依赖 seq 游标恢复 | -| 取消语义 | 中断连接即可能影响任务 | 仅显式 `cancel` 才取消任务 | - ---- - -## 3. 架构方案 - -### 3.1 组件职责 - -1. **FastAPI**:负责创建 run、查询 run、SSE 输出、cancel 接口。 -2. **ARQ Worker**:负责真正执行模型流式任务。 -3. **Redis**: - - ARQ 队列 - - run 过程事件流(Redis Stream) - - 取消信号(key + pub/sub) -4. **Postgres**: - - run 执行状态(`agent_runs`) - - 最终业务消息(`messages/tool_calls`) - - checkpointer(会话运行状态) - -### 3.2 架构图 - -```mermaid -flowchart LR - FE["Frontend"] -->|"POST /runs"| API["FastAPI"] - API -->|"create run"| PG[("Postgres")] - API -->|"enqueue"| R[("Redis")] - W["ARQ Worker"] -->|"dequeue"| R - W -->|"update run status"| PG - W -->|"write stream events"| R - FE -->|"GET /runs/:id/events?after_seq=..."| API - API -->|"read incremental events"| R - API -->|"SSE events"| FE - FE -->|"POST /runs/:id/cancel"| API - API -->|"cancel mark"| PG - API -->|"publish cancel"| R - W -->|"persist messages/tool_calls"| PG -``` - ---- - -## 4. 端到端流程(事件流转) - -```mermaid -sequenceDiagram - participant FE as Frontend - participant API as FastAPI - participant R as Redis - participant W as ARQ Worker - participant PG as Postgres - - FE->>API: POST /api/chat/agent/{agent_id}/runs - API->>PG: create agent_runs(status=pending) - API->>R: enqueue process_agent_run(run_id) - API-->>FE: run_id - - FE->>API: GET /api/chat/runs/{run_id}/events?after_seq=0 - W->>R: dequeue run job - W->>PG: mark running - W->>R: append loading/tool/state events (stream) - API->>R: read events after_seq - API-->>FE: SSE incremental events - - FE->>API: POST /api/chat/runs/{run_id}/cancel (optional) - API->>PG: mark cancel_requested - API->>R: publish cancel signal - W->>W: cancel current task - W->>PG: mark terminal status - W->>PG: persist messages/tool_calls - API-->>FE: close event -``` - ---- - -## 5. 接口与协议变更 - -### 5.1 对外路径(保持稳定) - -1. `POST /api/chat/agent/{agent_id}/runs` -2. `GET /api/chat/runs/{run_id}` -3. `GET /api/chat/runs/{run_id}/events?after_seq=...` -4. `POST /api/chat/runs/{run_id}/cancel` - -### 5.2 `after_seq` 语义 - -1. 主格式为字符串游标(Redis Stream ID,如 `1700000000000-3`)。 -2. 兼容旧整数参数输入。 -3. SSE 返回 `seq` 字段统一为字符串,前端按单调递增去重。 - -### 5.3 SSE 事件格式 - -```json -{ - "run_id": "...", - "seq": "1700000000000-3", - "event_type": "loading", - "payload": {"items": [...]}, - "ts": 1700000000000 -} -``` - -控制事件:`heartbeat` / `error` / `close`。 - ---- - -## 6. 前端行为变化 - -1. 本地记录活跃 run 快照:`active_run:{threadId}`。 -2. 刷新/切回页面时按 `run_id + last_seq` 自动续流。 -3. 接收事件先做 seq 去重,再更新 UI。 -4. 保留打字机效果(`requestAnimationFrame + throttle`)。 -5. 保留首条消息自动更新会话标题逻辑。 - ---- - -## 7. 稳定性设计 - -1. 幂等:`request_id` 避免重复创建 run。 -2. 重试:仅可恢复错误触发 ARQ 重试(`max_tries=2`)。 -3. 取消:DB 状态 + Redis 信号双通道。 -4. SSE 生命周期:心跳、超时、终态关闭、断线重连。 -5. 状态单一真相:执行态在 `agent_runs`,业务态在 `messages/tool_calls + checkpointer`。 - ---- - -## 8. 本次代码变更范围(未提交部分) - -### 后端 - -1. `/Yuxi-Know/backend/package/yuxi/services/run_queue_service.py` -2. `/Yuxi-Know/backend/package/yuxi/services/run_worker.py` -3. `/Yuxi-Know/backend/package/yuxi/services/agent_run_service.py` -4. `/Yuxi-Know/backend/package/yuxi/repositories/agent_run_repository.py` -5. `/Yuxi-Know/server/routers/chat_router.py` -6. `/Yuxi-Know/server/worker_main.py` -7. `/Yuxi-Know/backend/package/yuxi/storage/postgres/manager.py` -8. `/Yuxi-Know/backend/package/yuxi/storage/postgres/models_business.py` - -### 前端 - -1. `/Yuxi-Know/web/src/apis/agent_api.js` -2. `/Yuxi-Know/web/src/components/AgentChatComponent.vue` - -### 测试 - -1. `/Yuxi-Know/test/test_run_queue_service.py` -2. `/Yuxi-Know/test/test_agent_run_service.py` -3. `/Yuxi-Know/test/test_run_worker.py` - -### 配置 - -1. `/Yuxi-Know/docker-compose.yml` -2. `/Yuxi-Know/docker-compose.prod.yml` -3. `/Yuxi-Know/.env.template` - ---- - -## 9. 验收标准 - -1. 发送消息后,SSE 能持续收到增量事件。 -2. 页面刷新后,可按 `after_seq` 恢复输出。 -3. 页面离开不影响后台执行。 -4. 取消后 run 状态正确收敛,输出停止。 -5. 最终消息与工具调用正常入库。 diff --git a/docs/develop-guides/contributing.md b/docs/develop-guides/contributing.md new file mode 100644 index 00000000..d53424f3 --- /dev/null +++ b/docs/develop-guides/contributing.md @@ -0,0 +1,211 @@ +# 参与贡献 + +感谢你对 Yuxi-Know 的兴趣。我们欢迎 Issue、文档改进、Bug 修复、测试补充以及新功能贡献。 + +如果你只是想快速了解仓库入口信息,可以先看根目录的 [CONTRIBUTING.md](../../CONTRIBUTING.md)。 + + + 贡献者名单 + + +## 开始之前 + +提交前建议先完成以下检查: + +- 搜索已有 [Issues](https://github.com/xerrors/Yuxi-Know/issues) 和 [Discussions](https://github.com/xerrors/Yuxi-Know/discussions) +- 对较大的功能改动,先发 Issue 讨论设计和边界 +- 保持一次 PR 只解决一个明确问题,避免把无关重构混在一起 + +## 开发原则 + +本项目默认遵循以下开发原则: + +- 避免过度设计,只做当前需求直接需要的改动 +- 不额外添加“顺手优化”、兼容层或未来需求抽象 +- 尽量复用现有实现,保持代码简单、聚焦、可维护 +- 只在系统边界做必要校验,不为不可能发生的内部场景增加复杂度 + +## 开发环境 + +Yuxi 基于 Docker Compose 管理开发环境。开发、调试、测试都应尽量在运行中的容器中完成。 + +### 启动项目 + +```bash +docker compose up -d +``` + +### 常用检查命令 + +```bash +docker ps +docker logs api-dev --tail 100 +``` + +`api-dev` 和 `web-dev` 默认支持热重载。通常情况下,本地修改代码后不需要重启容器。 + +如需进一步了解服务定义,可查看 [docker-compose.yml](../../docker-compose.yml)。 + +## 贡献流程 + +### 1. Fork 仓库 + +在 GitHub 上 Fork 本仓库到你的个人账户。 + +### 2. 创建分支 + +请使用语义明确的分支名,例如: + +```bash +git checkout -b feature/amazing-feature +git checkout -b fix/chat-stream-interrupt +git checkout -b docs/update-contributing-guide +``` + +### 3. 开发与验证 + +按项目规范完成代码、测试与文档更新。开发完成后,至少完成: + +- 检查 +- 测试 +- Lint +- 必要的端到端验证 + +如果现有测试脚本不足以覆盖你的改动,应补充对应测试,测试脚本优先放在 `backend/test`。 + +### 4. 提交代码 + +```bash +git commit -m "feat: add knowledge graph import flow" +``` + +### 5. 推送并发起 Pull Request + +```bash +git push origin feature/amazing-feature +``` + +创建 PR 时,请写清楚: + +- 修改内容 +- 修改原因 +- 影响范围 +- 验证方式 + +如果涉及 UI 改动,建议附上截图或录屏。 + +## 前端贡献规范 + +前端目录位于 `web/`,提交前请遵循以下约束: + +- 包管理器使用 `pnpm` +- 所有 API 接口定义统一放在 `web/src/apis` +- Icon 优先使用 `lucide-vue-next` +- 样式使用 `less` +- 非特殊情况必须优先复用 [web/src/assets/css/base.css](../../web/src/assets/css/base.css) 中的颜色变量 + +界面设计和样式约束可参考 [design.md](./design.md)。 + +## 后端贡献规范 + +后端目录位于 `backend/`,提交时请注意: + +- Python 风格尽量保持 pythonic +- 优先使用较新的语法,兼容目标为 Python 3.12+ +- 优先在容器内运行调试和测试命令 + +示例: + +```bash +docker compose exec api uv run python test/your_script.py +``` + +测试脚本建议放在 `backend/test` 下。 + +## 质量检查 + +提交前请至少完成以下检查: + +### 格式化与静态检查 + +```bash +make format +make lint +``` + +如果测试依赖管理员账户,可从项目根目录的 `.env` 中读取相关配置。 + +## 文档维护要求 + +代码改动后,请同步检查是否需要更新文档。 + +- 通用开发文档位于 `docs/` +- 文档导航定义在 `docs/.vitepress/config.mts` +- 若本次改动值得记录,请更新 [roadmap.md](./roadmap.md) +- 若确需新增仅开发者可见的说明文档,放在 `docs/vibe/` + +## 提交信息规范 + +推荐使用清晰、可检索的提交前缀: + +```text +feat: 添加新功能 +fix: 修复 bug +docs: 更新文档 +refactor: 代码重构 +test: 添加测试 +chore: 构建过程或辅助工具的变动 +``` + +## Bug 修复发布流程 + +当版本发布后发现 Bug,需要按实际分支状态处理。 + +### 情况 1:`main` 上没有未完成的新功能 + +直接在 `main` 修复并发布: + +```bash +git commit -m "fix: resolve config parser crash" +git tag -a v0.3.1 -m "Hotfix v0.3.1" +git push origin main --tags +``` + +### 情况 2:`main` 上已有未完成的新功能 + +从上一个 tag 创建 hotfix 分支: + +```bash +git checkout -b hotfix/0.3.1 v0.3.0 + +# 修复问题 +git commit -m "fix: resolve config parser crash" +git push origin hotfix/0.3.1 + +# 测试后合并回 main 并打 tag +git checkout main +git merge --no-ff hotfix/0.3.1 +git tag -a v0.3.1 -m "Hotfix v0.3.1" +git push origin main --tags + +# 删除临时分支 +git branch -d hotfix/0.3.1 +git push origin --delete hotfix/0.3.1 +``` + +## 测试配置 + +首次运行部分测试前,需要准备测试环境变量: + +```bash +cp test/.env.test.example test/.env.test +``` + +如果你的改动涉及认证、知识库、文件系统或智能体流程,建议补充对应集成测试,避免只覆盖单元逻辑。 + +## 反馈渠道 + +- Bug 反馈: +- 功能讨论: + +感谢每一位贡献者的投入。 diff --git a/docs/develop-guides/design.md b/docs/develop-guides/design.md index c1ccd4c3..a9d481fb 100644 --- a/docs/develop-guides/design.md +++ b/docs/develop-guides/design.md @@ -18,12 +18,6 @@ - 颜色、字体、间距等遵循 [base.css](web/src/assets/css/base.css) 中的设计变量 - 相似功能的组件使用相同的设计模式 -### 稳定性 - -- 禁止悬停位移:交互元素在悬停时应保持位置稳定,避免视觉跳动 -- 避免过度阴影:阴影仅用于层级区分,不得用于装饰目的 -- 避免过度渐变:渐变色应谨慎使用,优先使用纯色或微弱渐变 - ## 禁止项 以下 UI 做法在本项目中明确禁止: @@ -42,29 +36,10 @@ ### 技术栈 - 包管理器:pnpm -- 图标库:lucide-vue-next(注意尺寸控制) +- 图标库:lucide-vue-next(注意尺寸控制),作为 button 需要添加 lucide-icon-btn class 来保证居中 - 样式语言:LESS -- 样式变量:必须使用 [base.css](web/src/assets/css/base.css) 中定义的颜色变量 +- 样式变量:必须使用 [base.css](web/src/assets/css/base.css) 中定义的颜色变量,且适配暗色模式 -### 开发规范 - -1. **API 接口**:所有接口定义在 `web/src/apis` 目录下 -2. **样式复用**:优先复用现有组件和样式,避免重复定义 -3. **响应式设计**:适配不同屏幕宽度,保持布局稳定 -4. **无障碍设计**:保证基本的无障碍支持,如适当的对比度、键盘可操作等 - -### 颜色变量 - -必须从 `web/src/assets/css/base.css` 中获取以下变量: - -```less -// 典型变量示例(实际以 base.css 为准) -@primary-color: // 主色调 -@text-color: // 文本颜色 -@background-color: // 背景颜色 -@border-color: // 边框颜色 -// ... 其他变量 -``` ### 组件示例 @@ -77,7 +52,6 @@