Compare commits
2 Commits
45efba50e4
...
30fa16ba7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fa16ba7a | |||
| c77ee0dc06 |
@@ -31,6 +31,7 @@ extensions_config.json
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
.qoder/
|
||||
|
||||
# Coverage report
|
||||
coverage.xml
|
||||
@@ -63,3 +64,4 @@ config.yaml.bak
|
||||
skills/gstack
|
||||
skills/superpowers
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
|
||||
+186
-74
@@ -1,20 +1,59 @@
|
||||
# 🦌 DeerFlow - 2.0
|
||||
# 🦌 DeerFlow - 2.0 · 多租户改造
|
||||
|
||||
[English](./README.md) | 中文 | [日本語](./README_ja.md) | [Français](./README_fr.md) | [Русский](./README_ru.md)
|
||||
|
||||
[](./backend/pyproject.toml)
|
||||
[](./Makefile)
|
||||
[](./config.example.yaml)
|
||||
[](./LICENSE)
|
||||
|
||||
<a href="https://trendshift.io/repositories/14699" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14699" alt="bytedance%2Fdeer-flow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
> 2026 年 2 月 28 日,DeerFlow 2 发布后登上 GitHub Trending 第 1 名。非常感谢社区的支持,这是大家一起做到的。
|
||||
DeerFlow(**D**eep **E**xploration and **E**fficient **R**esearch **Flow**)是一个开源的 **super agent harness**:它把 **sub-agents**、**memory**、**sandbox** 组织在一起,再配合可扩展的 **skills**,让 agent 可以完成几乎任何事情。
|
||||
|
||||
DeerFlow(**D**eep **E**xploration and **E**fficient **R**esearch **Flow**)是一个开源的 **super agent harness**。它把 **sub-agents**、**memory** 和 **sandbox** 组织在一起,再配合可扩展的 **skills**,让 agent 可以完成几乎任何事情。
|
||||
> [!IMPORTANT]
|
||||
> **本分支(`docs/multi-tenant-redesign`)是 DeerFlow 的多租户改造主线。** 在保留原有 super agent harness 全部能力的基础上,引入了 **workspace 租户模型、Postgres 为默认后端、带 `workspace_id` 的行级数据隔离、扩展后的 JWT(带 `wid`/`role`)、per-workspace 的文件系统布局、Headless API schema 底座,以及 `apps/` 上层应用脚手架**。下面的 [多租户改造](#多租户改造本分支主线) 一节是阅读本仓库的入口。
|
||||
|
||||
> [!NOTE]
|
||||
> **DeerFlow 2.0 是一次彻底重写。** 它和 v1 没有共用代码。如果你要找的是最初的 Deep Research 框架,可以前往 [`1.x` 分支](https://github.com/bytedance/deer-flow/tree/main-1.x)。
|
||||
|
||||
https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
|
||||
|
||||
> [!NOTE]
|
||||
> **DeerFlow 2.0 是一次彻底重写。** 它和 v1 没有共用代码。如果你要找的是最初的 Deep Research 框架,可以前往 [`1.x` 分支](https://github.com/bytedance/deer-flow/tree/main-1.x)。那里仍然欢迎贡献;当前的主要开发已经转向 2.0。
|
||||
## 多租户改造(本分支主线)
|
||||
|
||||
DeerFlow 原本面向"单机可信环境、单用户"。本分支按"以个人用户为主、少量小团队,统一只有 **workspace** 概念(个人 = 1 人 workspace),中心化 SaaS 为主线"的目标,把租户能力分 **Stage 0–4** 渐进落地。**目前 Stage 0(底座)工程层面已全部合入。**
|
||||
|
||||
### Stage 0 已落地的能力
|
||||
|
||||
| 能力 | 说明 | 落点 |
|
||||
|---|---|---|
|
||||
| **workspace 租户模型** | 新增 `workspaces` + `workspace_memberships` 两张表与仓储;每个用户注册时自动建 1 人 workspace(owner=自己);slug 唯一、黑名单校验。 | `persistence/workspace*` |
|
||||
| **Postgres 成为默认后端** | `config.example.yaml` / `.env.example` / `make dev` / `make doctor` 默认走 Postgres,与生产对齐;SQLite 保留为离线开发兜底。 | 见 [数据库后端](#数据库后端) |
|
||||
| **行级数据隔离** | 4 张业务表(`threads_meta` / `runs` / `run_events` / `feedback`)加 `workspace_id` 列(`NOT NULL` + `UNIQUE(workspace_id, thread_id)` 兜底),入口路由按 `(workspace_id, thread_id)` 强校验,跨 workspace 访问必 404。 | `persistence/*`、Gateway routers |
|
||||
| **扩展后的 JWT** | TokenPayload 一次到位为 `{sub, wid, role, exp, iat, ver}`;登录 / 改密 / `/auth/me` 全部带上 workspace 与角色;旧版 4 字段 JWT 被识别为 `WORKSPACE_MISSING` 并要求重登。 | `app/gateway/auth/` |
|
||||
| **per-workspace 文件系统** | 运行期状态从 `users/{uid}/...` 迁移到 `workspaces/{wid}/threads/{tid}/...`,提供 `make migrate-paths` 迁移脚本(支持 `DRY_RUN=1` 预览)。 | `config/paths.py`、`thread_data_middleware.py` |
|
||||
| **边界扫描围栏** | CI 静态扫描禁止任何路径绕过入口直连 LangGraph checkpoint/store,确保隔离不被旁路。 | `tests/boundary_allowlist.toml`、`test_workspace_boundary*.py` |
|
||||
| **Headless API schema 底座** | 预建 `service_accounts` / `api_keys`(`dfk_live_*` / `dfk_test_*`)/ `external_users` 三张表(schema-only),为 Stage 1 的无人值守接入做准备。 | `persistence/{service_account,api_key,external_user}` |
|
||||
| **Alembic 迁移 + 回填** | `0001`→`0003` 迁移链 + `backfill_workspace_id.py` 回填脚本,dev 用 `create_all()` 自愈、生产用迁移。 | `persistence/migrations/` |
|
||||
|
||||
> 数据库的事实参考(10 张表全字段 / 外键 / 索引)见 [`database-schema-as-built.zh-CN.md`](docs/multi-tenant-redesign/01-redesign/database-schema-as-built.zh-CN.md)。
|
||||
|
||||
### 路线图:Stage 0–4
|
||||
|
||||
| Stage | 目标 | 关键内容 | 状态 |
|
||||
|---|---|---|---|
|
||||
| **0** | workspace 模型立起来 + Postgres 切换 + auth 收紧 | 上表全部 | ✅ 工程层面已合入(业务门 / live 验证待跟进)|
|
||||
| **1** | 第一批付费客户 + 业务系统集成(双轨并行)| quota + 计费 + AioSandbox 轻量加固;Headless API(Pattern A/B)接通 PR8 三张表 | 🔜 已具备底座 |
|
||||
| **2** | 增长期,安全与隔离深化 | DeerFlow 自有表启用 RLS、KMS、ObjectStorage、完整 RBAC + invitation | 📋 规划中 |
|
||||
| **3** | 成熟期,K8s 隔离 + BYO | K8s namespace + NetworkPolicy、BYO LLM key、audit DB 拆分 | 📋 规划中 |
|
||||
| **4** | 企业化,按需开启 | SSO、自定义域名、per-tenant DB、gVisor/Kata、合规审计 | 📋 按合同 |
|
||||
|
||||
### 多租户文档入口
|
||||
|
||||
- **汇总索引(先读这个)**:[`docs/multi-tenant-redesign/README.zh-CN.md`](docs/multi-tenant-redesign/README.zh-CN.md)
|
||||
- **现状架构鸟瞰**:[`00-current-state/architecture-overview.zh-CN.md`](docs/multi-tenant-redesign/00-current-state/architecture-overview.zh-CN.md)
|
||||
- **决策(7 份 ADR + spike + 审计)**:[`01-redesign/`](docs/multi-tenant-redesign/01-redesign/)
|
||||
- **Stage 0 schema 锁定版 / 落地版**:[`workspace-schema-design`](docs/multi-tenant-redesign/01-redesign/workspace-schema-design.zh-CN.md) · [`database-schema-as-built`](docs/multi-tenant-redesign/01-redesign/database-schema-as-built.zh-CN.md)
|
||||
- **落地路线 + 集成轨道**:[`02-rollout/`](docs/multi-tenant-redesign/02-rollout/)
|
||||
- **Stage 0 进度面板(权威"现在到哪了")**:[`03-impl/STATUS.md`](docs/multi-tenant-redesign/03-impl/STATUS.md)
|
||||
|
||||
## 官网
|
||||
|
||||
@@ -32,13 +71,13 @@ https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
|
||||
|
||||
## 目录
|
||||
|
||||
- [🦌 DeerFlow - 2.0](#-deerflow---20)
|
||||
- [🦌 DeerFlow - 2.0 · 多租户改造](#-deerflow---20--多租户改造)
|
||||
- [多租户改造(本分支主线)](#多租户改造本分支主线)
|
||||
- [官网](#官网)
|
||||
- [InfoQuest](#infoquest)
|
||||
- [目录](#目录)
|
||||
- [一句话交给 Coding Agent 安装](#一句话交给-coding-agent-安装)
|
||||
- [字节跳动火山引擎方舟 Coding Plan](#字节跳动火山引擎方舟-coding-plan)
|
||||
- [快速开始](#快速开始)
|
||||
- [配置](#配置)
|
||||
- [数据库后端](#数据库后端)
|
||||
- [运行应用](#运行应用)
|
||||
- [部署建议与资源规划](#部署建议与资源规划)
|
||||
- [方式一:Docker(推荐)](#方式一docker推荐)
|
||||
@@ -48,7 +87,7 @@ https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
|
||||
- [MCP Server](#mcp-server)
|
||||
- [IM 渠道](#im-渠道)
|
||||
- [LangSmith 链路追踪](#langsmith-链路追踪)
|
||||
- [从 Deep Research 到 Super Agent Harness](#从-deep-research-到-super-agent-harness)
|
||||
- [多租户架构详解](#多租户架构详解)
|
||||
- [核心特性](#核心特性)
|
||||
- [Skills 与 Tools](#skills-与-tools)
|
||||
- [Claude Code 集成](#claude-code-集成)
|
||||
@@ -56,25 +95,14 @@ https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
|
||||
- [Sandbox 与文件系统](#sandbox-与文件系统)
|
||||
- [Context Engineering](#context-engineering)
|
||||
- [长期记忆](#长期记忆)
|
||||
- [推荐模型](#推荐模型)
|
||||
- [在 DeerFlow 之上构建应用(apps/)](#在-deerflow-之上构建应用apps)
|
||||
- [内嵌 Python Client](#内嵌-python-client)
|
||||
- [推荐模型](#推荐模型)
|
||||
- [文档](#文档)
|
||||
- [⚠️ 安全使用](#️-安全使用)
|
||||
- [参与贡献](#参与贡献)
|
||||
- [许可证](#许可证)
|
||||
- [致谢](#致谢)
|
||||
- [核心贡献者](#核心贡献者)
|
||||
- [Star History](#star-history)
|
||||
|
||||
## 一句话交给 Coding Agent 安装
|
||||
|
||||
如果你在用 Claude Code、Codex、Cursor、Windsurf 或其他 coding agent,可以直接把下面这句话发给它:
|
||||
|
||||
```text
|
||||
如果还没 clone DeerFlow,就先 clone,然后按照 https://raw.githubusercontent.com/bytedance/deer-flow/main/Install.md 把它的本地开发环境初始化好
|
||||
```
|
||||
|
||||
这条提示词是给 coding agent 用的。它会在需要时先 clone 仓库,优先选择 Docker,完成初始化,并在结束时告诉你下一条启动命令,以及还缺哪些配置需要你补充。
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -149,6 +177,48 @@ https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
|
||||
api_key: your-actual-api-key-here # 替换为真实 key
|
||||
```
|
||||
|
||||
### 数据库后端
|
||||
|
||||
多租户改造后,**Stage 0+ 默认后端是 Postgres**(与生产对齐,并为后续 RLS 留好空间)。`config.example.yaml` 默认带:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
backend: postgres
|
||||
postgres_url: $DATABASE_URL
|
||||
```
|
||||
|
||||
在 `.env` 中设置 `DATABASE_URL`:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql+asyncpg://deerflow:deerflow_dev@localhost:5432/deerflow
|
||||
# 远程 RDS / Cloud SQL 示例:
|
||||
# DATABASE_URL=postgresql+asyncpg://USER:PASSWORD@HOST:5432/DBNAME
|
||||
```
|
||||
|
||||
启动本地 Postgres 开发容器:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose-dev.yaml up -d postgres
|
||||
```
|
||||
|
||||
- `make doctor` 会报告当前配置的后端、尝试 asyncpg 连接,并给出可执行的修复建议。
|
||||
- `make dev` 在启动各服务前会先 preflight Postgres 可达性;`DATABASE_URL` 不可达时直接中止。
|
||||
- **dev** 启动时用 `Base.metadata.create_all()` 自动建缺失的表(不改已存在的表);**生产**用 Alembic 迁移(`backend/packages/harness/deerflow/persistence/migrations/`)。目标库不存在时会自动 `CREATE DATABASE` 后重试。
|
||||
|
||||
<details>
|
||||
<summary>离线开发(SQLite 兜底)</summary>
|
||||
|
||||
如果你不想起 Postgres,把 `config.yaml` 改成:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
backend: sqlite
|
||||
sqlite_dir: .deer-flow/data
|
||||
```
|
||||
|
||||
SQLite 仍是合法的离线开发后端;但 RLS / 多节点等 Stage 2+ 能力需要 Postgres。
|
||||
</details>
|
||||
|
||||
### 运行应用
|
||||
|
||||
#### 部署建议与资源规划
|
||||
@@ -194,7 +264,7 @@ make down # 停止并移除容器
|
||||
|
||||
如果你更希望直接在本地启动各个服务:
|
||||
|
||||
前提:先完成上面的“配置”步骤(`make config` 和模型 API key 配置)。`make dev` 需要有效配置文件,默认读取项目根目录下的 `config.yaml`。可以用 `DEER_FLOW_PROJECT_ROOT` 显式指定项目根目录,也可以用 `DEER_FLOW_CONFIG_PATH` 指向某个具体配置文件。运行期状态默认写到项目根目录下的 `.deer-flow`,可用 `DEER_FLOW_HOME` 覆盖;skills 默认读取项目根目录下的 `skills/`,可用 `DEER_FLOW_SKILLS_PATH` 覆盖。
|
||||
前提:先完成上面的"配置"步骤(`make config`、模型 API key、`DATABASE_URL`)。`make dev` 需要有效配置文件,默认读取项目根目录下的 `config.yaml`。可以用 `DEER_FLOW_PROJECT_ROOT` 显式指定项目根目录,也可以用 `DEER_FLOW_CONFIG_PATH` 指向某个具体配置文件。运行期状态默认写到项目根目录下的 `.deer-flow`,可用 `DEER_FLOW_HOME` 覆盖;skills 默认读取项目根目录下的 `skills/`,可用 `DEER_FLOW_SKILLS_PATH` 覆盖。
|
||||
在 Windows 上,请使用 Git Bash 运行本地开发流程。基于 bash 的服务脚本不支持直接在原生 `cmd.exe` 或 PowerShell 中执行,且 WSL 也不保证可用,因为部分脚本依赖 Git for Windows 的 `cygpath` 等工具。
|
||||
|
||||
1. **检查依赖环境**:
|
||||
@@ -220,6 +290,16 @@ make down # 停止并移除容器
|
||||
|
||||
5. **访问地址**:http://localhost:2026
|
||||
|
||||
> [!TIP]
|
||||
> `make dev` 是前台阻塞运行。日常调试更顺手的是仓库根 `scripts/` 下两个生命周期脚本(子命令统一为 `start / stop / restart / status / logs / run`):
|
||||
> - `scripts/dev-gateway.sh` — 只起 Gateway(`http://localhost:8001`),起得快,适合调后端 API / 接入示例。
|
||||
> - `scripts/dev-full.sh` — Gateway + 前端 + nginx(`http://localhost:2026`),连前端一起调。
|
||||
>
|
||||
> 例如 `./scripts/dev-gateway.sh start`、`./scripts/dev-gateway.sh logs`、`SKIP_INSTALL=1 ./scripts/dev-full.sh start`。详见 [apps/README.md](apps/README.md)。
|
||||
|
||||
> [!NOTE]
|
||||
> 把历史的 `users/` 目录树迁移到新的 per-workspace 布局:`make migrate-paths`(加 `DRY_RUN=1` 仅预览,`DEFAULT_WORKSPACE=<wid>` 指定未分配用户的归属 workspace)。
|
||||
|
||||
### 进阶配置
|
||||
#### Sandbox 模式
|
||||
|
||||
@@ -405,23 +485,48 @@ LANGSMITH_PROJECT=xxx
|
||||
|
||||
Docker 部署时,追踪默认关闭。在 `.env` 中设置 `LANGSMITH_TRACING=true` 和 `LANGSMITH_API_KEY` 即可启用。
|
||||
|
||||
## 从 Deep Research 到 Super Agent Harness
|
||||
## 多租户架构详解
|
||||
|
||||
DeerFlow 最初是一个 Deep Research 框架,后来社区把它一路推到了更远的地方。上线之后,开发者拿它去做的事情早就不止研究:搭数据流水线、生成演示文稿、快速起 dashboard、自动化内容流程,很多方向一开始连我们自己都没想到。
|
||||
> 这一节展开 [多租户改造](#多租户改造本分支主线) 里 Stage 0 已落地的实现细节。完整决策与路线见 [`docs/multi-tenant-redesign/`](docs/multi-tenant-redesign/)。
|
||||
|
||||
这让我们意识到一件事:DeerFlow 不只是一个研究工具。它更像一个 **harness**,一个真正让 agents 把事情做完的运行时基础设施。
|
||||
**workspace 是唯一的隔离粒度。** 个人用户 = 1 人 workspace,小团队 = 多人 workspace。骨架是两条主线:
|
||||
|
||||
所以我们把它从头重做了一遍。
|
||||
1. **租户骨架**:`users` ↔ `workspaces`(多对多经 `workspace_memberships`)。每个用户注册时自动建 1 人 workspace(owner=自己),slug 唯一且过黑名单校验。
|
||||
2. **业务数据**:`threads_meta` → `runs` → `run_events` / `feedback`,全部挂 `workspace_id`(行级隔离),workspace 删除时级联清空。
|
||||
|
||||
DeerFlow 2.0 不再是一个需要你自己拼装的 framework。它是一个开箱即用、同时又足够可扩展的 super agent harness。基于 LangGraph 和 LangChain 构建,默认就带上了 agent 真正会用到的关键能力:文件系统、memory、skills、sandbox 执行环境,以及为复杂多步骤任务做规划、拉起 sub-agents 的能力。
|
||||
**数据隔离怎么做的。** DeerFlow 自有表走行级 `workspace_id` + `UNIQUE(workspace_id, thread_id)` 兜底;入口路由(`threads.py` / `thread_runs.py`)按 `(workspace_id, thread_id)` 强校验,跨 workspace 访问必返 404。LangGraph 自己的 checkpointer / store 表(`langgraph-checkpoint-postgres==3.0.5` 无 `connection_factory`,无法注入 RLS)则走**应用层强校验**,并由 CI 静态扫描(`tests/boundary_allowlist.toml`)禁止任何路径绕过入口直连这些表。
|
||||
|
||||
你可以直接拿来用,也可以拆开重组,改成你自己的样子。
|
||||
**身份与会话。** JWT TokenPayload 一次到位为 `{sub, wid, role, exp, iat, ver}`:
|
||||
|
||||
- `wid` — 当前 workspace;`role` — workspace 内角色(Stage 0 简化为 owner-only,Stage 2 扩到 owner/admin/member)。
|
||||
- `ver` — `token_version`,bump 后旧 token 全失效。
|
||||
- 旧版 4 字段 JWT 会被识别为 `WORKSPACE_MISSING` 并要求重登;登录 / 改密 / `/auth/me` 都会带上 workspace 与角色(`/auth/me` 返回 `workspaces[]`,含 id/name/slug/role)。
|
||||
|
||||
**文件系统布局。** 运行期状态按 workspace 分目录:
|
||||
|
||||
```text
|
||||
${DEER_FLOW_HOME:-./.deer-flow}/
|
||||
└── workspaces/
|
||||
└── {workspace_id}/
|
||||
├── threads/{thread_id}/... ← 每个 thread 的 sandbox / 产物
|
||||
└── users/{user_id}/... ← 用户级状态
|
||||
```
|
||||
|
||||
历史的 `users/{uid}/...` 布局用 `make migrate-paths`(`DRY_RUN=1` 预览)迁移过来。
|
||||
|
||||
**Headless API schema 底座(Stage 0 末预建,schema-only)。** 为 Stage 1 的无人值守 / 业务系统接入准备:
|
||||
|
||||
- `service_accounts` — workspace 内的非人身份,带 `identity_mode` 三态(`collapsed` / `external_passthrough` / `both`)。
|
||||
- `api_keys` — service account 的凭证,格式 `dfk_live_*` / `dfk_test_*`,`key_prefix` 全局唯一 + 部分索引 `WHERE revoked_at IS NULL`。
|
||||
- `external_users` — passthrough 终端身份,`(service_account_id, external_id)` 复合唯一。
|
||||
|
||||
> ⚠️ Stage 0 只建表,**API Key 鉴权中间件尚未接入**。外部系统当前只能走会话 cookie(见 [apps/README.md](apps/README.md) 的鉴权说明);等 `Authorization: Bearer dfk_live_...` 在 Stage 1 落地后再补无人值守接入。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### Skills 与 Tools
|
||||
|
||||
Skills 是 DeerFlow 能做“几乎任何事”的关键。
|
||||
Skills 是 DeerFlow 能做"几乎任何事"的关键。
|
||||
|
||||
标准的 Agent Skill 是一种结构化能力模块,通常就是一个 Markdown 文件,里面定义了工作流、最佳实践,以及相关的参考资源。DeerFlow 自带一批内置 skills,覆盖研究、报告生成、演示文稿制作、网页生成、图像和视频生成等场景。真正有意思的地方在于它的扩展性:你可以加自己的 skills,替换内置 skills,或者把多个 skills 组合成复合工作流。
|
||||
|
||||
@@ -431,8 +536,6 @@ Skills 采用按需渐进加载,不会一次性把所有内容都塞进上下
|
||||
|
||||
Tools 也是同样的思路。DeerFlow 自带一组核心工具:网页搜索、网页抓取、文件操作、bash 执行;同时也支持通过 MCP Server 和 Python 函数扩展自定义工具。你可以替换任何一项,也可以继续往里加。
|
||||
|
||||
Gateway 生成后续建议时,现在会先把普通字符串输出和 block/list 风格的富文本内容统一归一化,再去解析 JSON 数组响应,因此不同 provider 的内容包装方式不会再悄悄把建议吞掉。
|
||||
|
||||
```text
|
||||
# sandbox 容器内的路径
|
||||
/mnt/skills/public
|
||||
@@ -456,24 +559,7 @@ Gateway 生成后续建议时,现在会先把普通字符串输出和 block/li
|
||||
npx skills add https://github.com/bytedance/deer-flow --skill claude-to-deerflow
|
||||
```
|
||||
|
||||
然后确认 DeerFlow 已经启动(默认地址是 `http://localhost:2026`),在 Claude Code 里使用 `/claude-to-deerflow` 命令即可。
|
||||
|
||||
**你可以做的事情包括:**
|
||||
- 给 DeerFlow 发送消息,并接收流式响应
|
||||
- 选择执行模式:flash(更快)、standard、pro(规划模式)、ultra(sub-agents 模式)
|
||||
- 检查 DeerFlow 健康状态,列出 models / skills / agents
|
||||
- 管理 threads 和会话历史
|
||||
- 上传文件做分析
|
||||
|
||||
**环境变量**(可选,用于自定义端点):
|
||||
|
||||
```bash
|
||||
DEERFLOW_URL=http://localhost:2026 # 统一代理基地址
|
||||
DEERFLOW_GATEWAY_URL=http://localhost:2026 # Gateway API
|
||||
DEERFLOW_LANGGRAPH_URL=http://localhost:2026/api/langgraph # LangGraph API
|
||||
```
|
||||
|
||||
完整 API 说明见 [`skills/public/claude-to-deerflow/SKILL.md`](skills/public/claude-to-deerflow/SKILL.md)。
|
||||
然后确认 DeerFlow 已经启动(默认地址是 `http://localhost:2026`),在 Claude Code 里使用 `/claude-to-deerflow` 命令即可。完整 API 说明见 [`skills/public/claude-to-deerflow/SKILL.md`](skills/public/claude-to-deerflow/SKILL.md)。
|
||||
|
||||
### Sub-Agents
|
||||
|
||||
@@ -485,12 +571,10 @@ lead agent 可以按需动态拉起 sub-agents。每个 sub-agent 都有自己
|
||||
|
||||
### Sandbox 与文件系统
|
||||
|
||||
DeerFlow 不只是“会说它能做”,它是真的有一台自己的“电脑”。
|
||||
DeerFlow 不只是"会说它能做",它是真的有一台自己的"电脑"。
|
||||
|
||||
每个任务都运行在隔离的 Docker 容器里,里面有完整的文件系统,包括 skills、workspace、uploads、outputs。agent 可以读写和编辑文件,可以执行 bash 命令和代码,也可以查看图片。整个过程都在 sandbox 内完成,可审计、会隔离,不会在不同 session 之间互相污染。
|
||||
|
||||
这就是“带工具的聊天机器人”和“真正有执行环境的 agent”之间的差别。
|
||||
|
||||
```text
|
||||
# sandbox 容器内的路径
|
||||
/mnt/user-data/
|
||||
@@ -509,16 +593,30 @@ DeerFlow 不只是“会说它能做”,它是真的有一台自己的“电
|
||||
|
||||
大多数 agents 会在对话结束后把一切都忘掉,DeerFlow 不一样。
|
||||
|
||||
跨 session 使用时,DeerFlow 会逐步积累关于你的持久 memory,包括你的个人偏好、知识背景,以及长期沉淀下来的工作习惯。你用得越多,它越了解你的写作风格、技术栈和重复出现的工作流。memory 保存在本地,控制权也始终在你手里。
|
||||
跨 session 使用时,DeerFlow 会逐步积累关于你的持久 memory,包括你的个人偏好、知识背景,以及长期沉淀下来的工作习惯。你用得越多,它越了解你的写作风格、技术栈和重复出现的工作流。
|
||||
|
||||
## 推荐模型
|
||||
## 在 DeerFlow 之上构建应用(apps/)
|
||||
|
||||
DeerFlow 对模型没有强绑定,只要实现了 OpenAI 兼容 API 的 LLM,理论上都可以接入。不过在下面这些能力上表现更强的模型,通常会更适合 DeerFlow:
|
||||
`apps/`(仓库根目录、与 `backend/` / `frontend/` 平级)用于存放**消费 DeerFlow 能力的上层应用**,遵循严格的依赖方向:**app 可以依赖 deerflow,deerflow 不能依赖 app / apps**。
|
||||
|
||||
- **长上下文窗口**(100k+ tokens),适合深度研究和多步骤任务
|
||||
- **推理能力**,适合自适应规划和复杂拆解
|
||||
- **多模态输入**,适合理解图片和视频
|
||||
- **稳定的 tool use 能力**,适合可靠的函数调用和结构化输出
|
||||
两种集成模式:
|
||||
|
||||
| 模式 | 适用场景 | 怎么连 | 示例 |
|
||||
|---|---|---|---|
|
||||
| **HTTP Gateway**(REST+SSE) | 上层是别的服务 / 多语言 | 调 `http://localhost:2026/api/*` | [`apps/examples/http-chat/`](apps/examples/http-chat/) |
|
||||
| **内嵌 DeerFlowClient** | 上层本身是 Python,进程内直接当 SDK 调 | `from deerflow.client import DeerFlowClient` | [`apps/examples/embedded-chat/`](apps/examples/embedded-chat/) |
|
||||
|
||||
每个示例自带 `run.sh`:
|
||||
|
||||
```bash
|
||||
# ① HTTP 模式:需要先起 Gateway(dev-gateway 或 dev-full 都行)
|
||||
./apps/examples/http-chat/run.sh
|
||||
|
||||
# ② 内嵌模式:不需要起任何服务,run.sh 自动进 backend uv 环境运行
|
||||
./apps/examples/embedded-chat/run.sh
|
||||
```
|
||||
|
||||
完整说明、鉴权流程与新建应用约定见 [apps/README.md](apps/README.md)。
|
||||
|
||||
## 内嵌 Python Client
|
||||
|
||||
@@ -546,12 +644,25 @@ client.upload_files("thread-1", ["./report.pdf"]) # {"success": True, "files":
|
||||
|
||||
所有返回 dict 的方法都会在 CI 中通过 Gateway 的 Pydantic 响应模型校验(`TestGatewayConformance`),以确保内嵌 client 始终和 HTTP API schema 保持同步。完整 API 说明见 `backend/packages/harness/deerflow/client.py`。
|
||||
|
||||
## 推荐模型
|
||||
|
||||
DeerFlow 对模型没有强绑定,只要实现了 OpenAI 兼容 API 的 LLM,理论上都可以接入。不过在下面这些能力上表现更强的模型,通常会更适合 DeerFlow:
|
||||
|
||||
- **长上下文窗口**(100k+ tokens),适合深度研究和多步骤任务
|
||||
- **推理能力**,适合自适应规划和复杂拆解
|
||||
- **多模态输入**,适合理解图片和视频
|
||||
- **稳定的 tool use 能力**,适合可靠的函数调用和结构化输出
|
||||
|
||||
## 文档
|
||||
|
||||
- [多租户改造汇总索引](docs/multi-tenant-redesign/README.zh-CN.md) - workspace / Postgres / RLS / Headless API 的决策与路线
|
||||
- [Stage 0 进度面板](docs/multi-tenant-redesign/03-impl/STATUS.md) - "现在到哪了"的权威来源
|
||||
- [数据库设计落地版](docs/multi-tenant-redesign/01-redesign/database-schema-as-built.zh-CN.md) - 10 张表全字段 / 外键 / 索引参考
|
||||
- [贡献指南](CONTRIBUTING.md) - 开发环境搭建与协作流程
|
||||
- [配置指南](backend/docs/CONFIGURATION.md) - 安装与配置说明
|
||||
- [架构概览](backend/CLAUDE.md) - 技术架构说明
|
||||
- [后端架构](backend/README.md) - 后端架构与 API 参考
|
||||
- [apps/ 上层应用](apps/README.md) - 在 DeerFlow 之上构建应用
|
||||
|
||||
## ⚠️ 安全使用
|
||||
|
||||
@@ -562,6 +673,9 @@ DeerFlow 具备**系统指令执行、资源操作、业务逻辑调用**等关
|
||||
- **未授权的非法调用**:agent 功能被未授权的第三方、公网恶意扫描程序探测到,进而发起批量非法调用请求,执行系统命令、文件读写等高危操作,可能导致安全后果。
|
||||
- **合规与法律风险**:若 agent 被非法调用用于实施网络攻击、信息窃取等违法违规行为,可能产生法律责任与合规风险。
|
||||
|
||||
> [!NOTE]
|
||||
> 多租户改造引入的 workspace 行级隔离 / 入口强校验 / 边界扫描,目标是**应用内**的租户隔离;它不替代上面的网络层 / 部署层防护。把 DeerFlow 曝光到不可信网络仍需配合下面的安全措施。多租户更强的 DB 层兜底(RLS / KMS / K8s)规划在 Stage 2–3。
|
||||
|
||||
### 安全使用建议
|
||||
|
||||
**注意:建议您将 DeerFlow 部署在本地可信的网络环境下。** 若您有跨设备、跨网络的部署需求,必须加入严格的安全措施。例如,采取如下手段:
|
||||
@@ -575,7 +689,12 @@ DeerFlow 具备**系统指令执行、资源操作、业务逻辑调用**等关
|
||||
|
||||
欢迎参与贡献。开发环境、工作流和相关规范见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
||||
|
||||
目前回归测试已经覆盖 Docker sandbox 模式识别,以及 `backend/tests/` 中 provisioner kubeconfig-path 处理相关测试。
|
||||
提 PR 前请先在本地跑通校验(CI 会在每个 PR 上执行 backend lint + 测试,含 Postgres matrix):
|
||||
|
||||
```bash
|
||||
cd backend && make lint && make test # ruff + pytest
|
||||
cd frontend && pnpm lint && pnpm typecheck
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
@@ -583,22 +702,15 @@ DeerFlow 具备**系统指令执行、资源操作、业务逻辑调用**等关
|
||||
|
||||
## 致谢
|
||||
|
||||
DeerFlow 建立在开源社区大量优秀工作的基础上。所有让 DeerFlow 成为可能的项目和贡献者,我们都心怀感谢。毫不夸张地说,我们是站在巨人的肩膀上继续往前走。
|
||||
DeerFlow 建立在开源社区大量优秀工作的基础上。所有让 DeerFlow 成为可能的项目和贡献者,我们都心怀感谢。
|
||||
|
||||
特别感谢以下项目带来的关键支持:
|
||||
|
||||
- **[LangChain](https://github.com/langchain-ai/langchain)**:它们提供的优秀框架支撑了我们的 LLM 交互与 chains,让整体集成和能力编排顺畅可用。
|
||||
- **[LangChain](https://github.com/langchain-ai/langchain)**:它们提供的优秀框架支撑了我们的 LLM 交互与 chains。
|
||||
- **[LangGraph](https://github.com/langchain-ai/langgraph)**:它们在多 agent 编排上的创新方式,是 DeerFlow 复杂工作流得以成立的重要基础。
|
||||
|
||||
这些项目体现了开源协作真正的力量,我们也很高兴能继续建立在这些基础之上。
|
||||
|
||||
### 核心贡献者
|
||||
|
||||
感谢 `DeerFlow` 的核心作者,是他们的判断、投入和持续推进,才让这个项目真正落地:
|
||||
|
||||
- **[Daniel Walnut](https://github.com/hetaoBackend/)**
|
||||
- **[Henry Li](https://github.com/magiccube/)**
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#bytedance/deer-flow&Date)
|
||||
</content>
|
||||
</invoke>
|
||||
|
||||
+13
-1
@@ -37,6 +37,18 @@ DF_BASE=http://localhost:8001 ./apps/examples/http-chat/run.sh # 也可手动
|
||||
|
||||
http-chat 的 `run.sh` 优先用 `uv run --no-project --with requests`(临时环境,不污染系统),没有 uv 才回退到本地 `.venv` + pip;可用 `DF_BASE` / `DF_EMAIL` / `DF_PASSWORD` 覆盖。embedded-chat 的 `run.sh` 自动定位 `backend/`、加载 `.env` 后用 `uv run` 启动,依赖 `config.yaml` 里有可用模型。
|
||||
|
||||
### 多租户并发验证(HTTP 模式)
|
||||
|
||||
[`examples/http-chat/multi_tenant.py`](examples/http-chat/multi_tenant.py) 是 `app.py` 的并发 / 多租户版:用 `/api/v1/auth/register` 并发创建多个租户(每个注册用户自带独立 workspace),各自一个 `requests.Session`(独立 cookie)同时跑 N 轮链式对话,并校验:① 真并发(对话时间窗重叠);② 多轮上下文按 `thread_id` 各自保持(第 2 轮起每轮都依赖上一轮结果);③ 租户隔离(`POST /api/threads/search` 仅见己有线程,跨租户 `GET /api/threads/{id}` 返回 404)。
|
||||
|
||||
```bash
|
||||
# 前提:已起 Gateway(dev-gateway 或 dev-full)
|
||||
DF_BASE=http://localhost:8001 DF_TENANTS=4 DF_TURNS=10 \
|
||||
uv run --no-project --with requests python apps/examples/http-chat/multi_tenant.py
|
||||
```
|
||||
|
||||
环境变量:`DF_BASE`(网关地址,默认 :8001)、`DF_TENANTS`(并发租户数,默认 3)、`DF_TURNS`(每租户轮数,默认 10)。每次运行用唯一邮箱新建租户,可重复跑,不撞 email、也不触发登录限流(`/register` 不限流;`setup-status` 全程只调一次以避开 60s/IP 限流)。
|
||||
|
||||
## 前置:先把 DeerFlow 跑起来
|
||||
|
||||
在**仓库根目录**:
|
||||
@@ -79,7 +91,7 @@ Gateway 是 **fail-closed** 的——除少数公开路径外所有请求都要
|
||||
3. 成功后 Session 里有 `access_token`(HttpOnly) + `csrf_token` 两个 cookie
|
||||
4. **所有写请求**(POST/PUT/DELETE/PATCH)必须带 `X-CSRF-Token` 头 = `csrf_token` 值
|
||||
|
||||
> 多租户:当前 `docs/multi-tenant-redesign` 分支的 API Key 鉴权中间件尚未接入,外部系统暂时只能走会话 cookie。等 `Authorization: Bearer dfk_live_...` 落地后再补无人值守接入。
|
||||
> 多租户:当前 `docs/multi-tenant-redesign` 分支的 API Key 鉴权中间件尚未接入,外部系统暂时只能走会话 cookie。等 `Authorization: Bearer dfk_live_...` 落地后再补无人值守接入。每个注册用户即一个独立租户(自带 workspace);并发与隔离行为可用上面的 [`multi_tenant.py`](examples/http-chat/multi_tenant.py) 验证。
|
||||
|
||||
## 新建一个应用
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
多租户并发测试:以 http-chat 的方式(Gateway REST + SSE),多个租户同时对话,
|
||||
并验证租户隔离。app.py 的并发 / 多租户版。
|
||||
|
||||
运行(前提:仓库根已起 Gateway,如 `./scripts/dev-gateway.sh start`):
|
||||
DF_BASE=http://localhost:8001 DF_TENANTS=4 \
|
||||
uv run --no-project --with requests python multi_tenant.py
|
||||
# 没有 uv 时:pip install -r requirements.txt && python multi_tenant.py
|
||||
环境变量:DF_BASE(网关地址,默认 :8001)、DF_TENANTS(并发租户数,默认 3)。
|
||||
|
||||
每个注册用户 = 一个独立租户(自带独立 workspace)。并发 = 每租户一个
|
||||
requests.Session(独立 cookie),放进线程池同时跑。
|
||||
|
||||
字段/事件名沿用 apps/examples/http-chat/app.py(已对照后端源码):
|
||||
鉴权 /api/v1/auth/{setup-status,register,me}
|
||||
线程 POST /api/threads ;列举 POST /api/threads/search ;单查 GET /api/threads/{id}
|
||||
SSE POST /api/threads/{id}/runs/stream
|
||||
|
||||
约束(来自后端源码):
|
||||
- GET /auth/setup-status 限流 1 次/60s/IP → 整个测试只调用一次
|
||||
- POST /auth/login/local 限流 5 次/5min/IP → 本测试用 /register 建新租户,不走 login
|
||||
- 跨租户访问线程返回 404(不是 403)→ 即隔离信号
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
import requests
|
||||
|
||||
BASE = os.environ.get("DF_BASE", "http://localhost:8001")
|
||||
N_TENANTS = int(os.environ.get("DF_TENANTS", "3"))
|
||||
N_TURNS = int(os.environ.get("DF_TURNS", "10")) # 每租户的对话轮数
|
||||
# 同一批次唯一后缀,避免重复运行时 email 冲突
|
||||
RUN_ID = uuid.uuid4().hex[:8]
|
||||
PASSWORD = "DeerTenant-7x9q!" # ≥8 位且不在弱口令黑名单
|
||||
|
||||
_print_lock = threading.Lock()
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
with _print_lock:
|
||||
print(msg, flush=True)
|
||||
|
||||
|
||||
def _csrf(s: requests.Session) -> dict:
|
||||
token = s.cookies.get("csrf_token")
|
||||
if not token:
|
||||
raise RuntimeError("缺少 csrf_token cookie —— 鉴权可能失败")
|
||||
return {"X-CSRF-Token": token}
|
||||
|
||||
|
||||
def register(s: requests.Session, email: str) -> dict:
|
||||
"""注册并自动登录(register 会同时下发 access_token + csrf_token cookie)。"""
|
||||
r = s.post(f"{BASE}/api/v1/auth/register", json={"email": email, "password": PASSWORD})
|
||||
r.raise_for_status()
|
||||
return r.json() # {id, email, system_role}
|
||||
|
||||
|
||||
def whoami(s: requests.Session) -> dict:
|
||||
r = s.get(f"{BASE}/api/v1/auth/me")
|
||||
r.raise_for_status()
|
||||
return r.json() # {id, email, default_workspace_id, workspaces:[...]}
|
||||
|
||||
|
||||
def create_thread(s: requests.Session) -> str:
|
||||
r = s.post(f"{BASE}/api/threads", json={}, headers=_csrf(s))
|
||||
r.raise_for_status()
|
||||
return r.json()["thread_id"]
|
||||
|
||||
|
||||
def stream_answer(s: requests.Session, thread_id: str, message: str) -> dict:
|
||||
"""发一条消息,按 message-id 分组收集 AI 增量文本。
|
||||
|
||||
注意:TitleMiddleware 会另起一条 AI 消息生成线程标题,它和正文答复
|
||||
是不同的 message-id。必须按 id 分组,否则正文数字会和标题数字粘连
|
||||
(如 180 + "12乘15..." → "18012"),导致校验误判。
|
||||
"""
|
||||
body = {
|
||||
"assistant_id": "lead_agent",
|
||||
"input": {"messages": [{"role": "user", "content": message}]},
|
||||
"stream_mode": ["messages-tuple", "values"],
|
||||
}
|
||||
headers = {**_csrf(s), "Accept": "text/event-stream"}
|
||||
by_id: dict[str, str] = {}
|
||||
with s.post(f"{BASE}/api/threads/{thread_id}/runs/stream", json=body, headers=headers, stream=True) as resp:
|
||||
resp.raise_for_status()
|
||||
event, buf = None, []
|
||||
for raw in resp.iter_lines(decode_unicode=True):
|
||||
if raw is None:
|
||||
continue
|
||||
line = raw.strip()
|
||||
if line == "":
|
||||
if event == "messages" and buf:
|
||||
_collect(by_id, "\n".join(buf))
|
||||
event, buf = None, []
|
||||
elif line.startswith(":"):
|
||||
continue
|
||||
elif line.startswith("event:"):
|
||||
event = line[6:].strip()
|
||||
elif line.startswith("data:"):
|
||||
buf.append(line[5:].strip())
|
||||
return by_id # {message_id: text}
|
||||
|
||||
|
||||
def _collect(by_id: dict, data: str) -> None:
|
||||
try:
|
||||
payload = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
chunk = payload[0] if isinstance(payload, list) and payload else {}
|
||||
if chunk.get("type") in ("ai", "AIMessageChunk"):
|
||||
content = chunk.get("content")
|
||||
if isinstance(content, str):
|
||||
text = content
|
||||
elif isinstance(content, list):
|
||||
text = "".join(b.get("text", "") for b in content if isinstance(b, dict))
|
||||
else:
|
||||
text = ""
|
||||
mid = chunk.get("id") or "_"
|
||||
by_id[mid] = by_id.get(mid, "") + text
|
||||
|
||||
|
||||
def search_threads(s: requests.Session) -> list:
|
||||
r = s.post(f"{BASE}/api/threads/search", json={"limit": 100, "offset": 0}, headers=_csrf(s))
|
||||
r.raise_for_status()
|
||||
return r.json() # bare array of ThreadResponse
|
||||
|
||||
|
||||
def get_thread_status(s: requests.Session, thread_id: str) -> int:
|
||||
return s.get(f"{BASE}/api/threads/{thread_id}").status_code
|
||||
|
||||
|
||||
def _contains(by_id: dict, n: int) -> bool:
|
||||
"""某条 AI 消息里是否独立出现数字 n(按 message-id 分组比对,避免与标题数字粘连)。"""
|
||||
return any(str(n) in re.findall(r"\d+", text.replace(",", "")) for text in by_id.values())
|
||||
|
||||
|
||||
def _main_text(by_id: dict) -> str:
|
||||
"""取最长的一条 AI 消息当正文(标题通常更短)。"""
|
||||
return (max(by_id.values(), key=len) if by_id else "").strip()
|
||||
|
||||
|
||||
# ── 一个租户的完整链路(在独立线程里跑)────────────────────────────────
|
||||
def run_tenant(idx: int) -> dict:
|
||||
"""N_TURNS 轮链式对话,复用同一 thread:
|
||||
T1 = a×b;之后每轮「把上一条数字再加 d」(d 每租户不同)。
|
||||
每轮都必须记得上一轮结果,逐轮校验,验证多轮上下文在并发下各自保持。
|
||||
"""
|
||||
email = f"tenant-{RUN_ID}-{idx}@example.com"
|
||||
a, b = 11 + idx, 13 + idx * 2 # 每租户不同算式
|
||||
d = 2 + idx # 每租户不同步长,进一步坐实无串扰
|
||||
|
||||
# 预先算出每轮期望值
|
||||
expected = [a * b]
|
||||
for _ in range(1, N_TURNS):
|
||||
expected.append(expected[-1] + d)
|
||||
|
||||
s = requests.Session()
|
||||
rec = {"idx": idx, "email": email, "d": d, "expected": expected, "turns": [], "ok": False}
|
||||
t0 = time.time()
|
||||
try:
|
||||
user = register(s, email)
|
||||
me = whoami(s)
|
||||
rec["user_id"] = user["id"]
|
||||
rec["workspace_id"] = me.get("default_workspace_id")
|
||||
rec["t_start"] = t0
|
||||
log(f"[租户{idx}] 注册完成 user={user['id'][:8]} ws={str(rec['workspace_id'])[:8]} d={d} email={email}")
|
||||
|
||||
tid = create_thread(s)
|
||||
rec["thread_id"] = tid
|
||||
|
||||
for k in range(N_TURNS):
|
||||
if k == 0:
|
||||
q = f"只回答最终数字:{a} 乘以 {b} 等于多少?"
|
||||
else:
|
||||
q = f"把你上一条回答的那个数字再加 {d},只回答最终数字。"
|
||||
by_id = stream_answer(s, tid, q)
|
||||
hit = _contains(by_id, expected[k])
|
||||
rec["turns"].append({"k": k + 1, "expected": expected[k], "text": _main_text(by_id), "ok": hit})
|
||||
mark = "✓" if hit else "✗"
|
||||
log(f"[租户{idx}] T{k + 1:>2}/{N_TURNS} 期望 {expected[k]:>5} → {mark} {rec['turns'][-1]['text'][:24]!r}")
|
||||
|
||||
rec["turns_passed"] = sum(t["ok"] for t in rec["turns"])
|
||||
rec["all_turns_ok"] = rec["turns_passed"] == N_TURNS
|
||||
rec["context_ok"] = all(t["ok"] for t in rec["turns"][1:]) # 第 2 轮起依赖上下文
|
||||
rec["t_end"] = time.time()
|
||||
rec["session"] = s
|
||||
rec["ok"] = True
|
||||
log(f"[租户{idx}] ✓ 完成 {rec['turns_passed']}/{N_TURNS} 轮")
|
||||
except Exception as e: # noqa: BLE001
|
||||
rec["error"] = f"{type(e).__name__}: {e}"
|
||||
log(f"[租户{idx}] ✗ 失败:{rec['error']}")
|
||||
return rec
|
||||
|
||||
|
||||
def main() -> None:
|
||||
log(f"=== 多租户并发测试 BASE={BASE} 租户数={N_TENANTS} 轮数={N_TURNS} 批次={RUN_ID} ===\n")
|
||||
|
||||
# setup-status 只调用一次(60s 限流)
|
||||
try:
|
||||
st = requests.get(f"{BASE}/api/v1/auth/setup-status", timeout=5)
|
||||
if st.status_code == 200:
|
||||
log(f"setup-status: {st.json()}")
|
||||
if st.json().get("needs_setup"):
|
||||
log("⚠ 系统尚未初始化管理员。请先创建管理员(apps/examples/http-chat/app.py 首启会建),再跑本测试。")
|
||||
return
|
||||
else:
|
||||
log(f"setup-status: HTTP {st.status_code}(限流则忽略,按已初始化处理)")
|
||||
except Exception as e: # noqa: BLE001
|
||||
log(f"setup-status 请求失败:{e}")
|
||||
|
||||
# 并发跑所有租户
|
||||
log(f"\n── 并发启动 {N_TENANTS} 个租户 ──")
|
||||
results = []
|
||||
with ThreadPoolExecutor(max_workers=N_TENANTS) as ex:
|
||||
futs = [ex.submit(run_tenant, i) for i in range(N_TENANTS)]
|
||||
for f in as_completed(futs):
|
||||
results.append(f.result())
|
||||
results.sort(key=lambda r: r["idx"])
|
||||
|
||||
ok = [r for r in results if r.get("ok")]
|
||||
|
||||
# 并发证据:对话时间窗是否重叠
|
||||
log("\n── 并发证据(对话时间窗,相对秒)──")
|
||||
if ok:
|
||||
base_t = min(r["t_start"] for r in ok)
|
||||
for r in ok:
|
||||
s_off = r["t_start"] - base_t
|
||||
e_off = r["t_end"] - base_t
|
||||
bar = " " * int(s_off * 4) + "█" * max(1, int((e_off - s_off) * 4))
|
||||
log(f" 租户{r['idx']}: [{s_off:5.1f}s → {e_off:5.1f}s] {bar}")
|
||||
spans = [(r["t_start"], r["t_end"]) for r in ok]
|
||||
overlapped = any(
|
||||
a[0] < b[1] and b[0] < a[1] for i, a in enumerate(spans) for b in spans[i + 1 :]
|
||||
)
|
||||
log(f" → 存在时间窗重叠(真并发):{overlapped}")
|
||||
|
||||
# 隔离校验
|
||||
log("\n── 隔离校验 ──")
|
||||
iso_pass = True
|
||||
own_thread = {r["idx"]: r["thread_id"] for r in ok}
|
||||
for r in ok:
|
||||
s = r["session"]
|
||||
mine = {t["thread_id"] for t in search_threads(s)}
|
||||
# 1) search 只含自己的线程
|
||||
only_own = mine == {r["thread_id"]} if mine else False
|
||||
leaked = {own_thread[j] for j in own_thread if j != r["idx"]} & mine
|
||||
# 2) 直接 GET 别人的线程 → 期望 404
|
||||
cross_ok = True
|
||||
for j, tid in own_thread.items():
|
||||
if j == r["idx"]:
|
||||
continue
|
||||
code = get_thread_status(s, tid)
|
||||
if code != 404:
|
||||
cross_ok = False
|
||||
log(f" ✗ 租户{r['idx']} 访问 租户{j} 的线程返回 {code}(期望 404)")
|
||||
if leaked:
|
||||
iso_pass = False
|
||||
log(f" ✗ 租户{r['idx']} 的 search 里出现了别人的线程:{leaked}")
|
||||
if not cross_ok:
|
||||
iso_pass = False
|
||||
if only_own and cross_ok and not leaked:
|
||||
log(f" ✓ 租户{r['idx']}:search 仅见己有线程,跨租户 GET 均 404")
|
||||
|
||||
# 汇总
|
||||
log(f"\n── 汇总(每租户 {N_TURNS} 轮链式对话:T1=a×b,之后每轮 +d)──")
|
||||
log(f"{'租户':<6}{'user_id':<12}{'thread':<14}{'步长d':<8}{'通过轮数':<12}{'逐轮':<14}{'状态'}")
|
||||
for r in results:
|
||||
if r.get("ok"):
|
||||
seq = "".join("✓" if t["ok"] else "✗" for t in r["turns"])
|
||||
passed = f"{r['turns_passed']}/{N_TURNS}"
|
||||
log(f"{r['idx']:<6}{r['user_id'][:8]:<12}{r['thread_id'][:10]:<14}{r['d']:<8}{passed:<12}{seq:<14}OK")
|
||||
else:
|
||||
log(f"{r['idx']:<6}{'-':<12}{'-':<14}{'-':<8}{'-':<12}{'-':<14}FAIL: {r.get('error')}")
|
||||
|
||||
all_ok = len(ok) == N_TENANTS
|
||||
turn1_ok = all(r["turns"][0]["ok"] for r in ok)
|
||||
context_ok = all(r.get("context_ok") for r in ok) # 第 2 轮起全对
|
||||
all_turns_ok = all(r.get("all_turns_ok") for r in ok) # N 轮全对
|
||||
log("\n=== 结果 ===")
|
||||
log(f" 租户全部成功: {all_ok} ({len(ok)}/{N_TENANTS})")
|
||||
log(f" 首轮答复无串扰: {turn1_ok}")
|
||||
log(f" 多轮上下文保持: {context_ok} ← 第 2~{N_TURNS} 轮每轮都依赖上一轮结果")
|
||||
log(f" 全程 {N_TURNS} 轮全对: {all_turns_ok}")
|
||||
log(f" 租户隔离: {iso_pass}")
|
||||
verdict = all_ok and turn1_ok and context_ok and iso_pass
|
||||
log(f" >>> {'PASS ✅' if verdict else 'FAIL ❌'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -277,19 +277,35 @@ phase_e2e() {
|
||||
|
||||
local tag
|
||||
tag=$(date +%s)
|
||||
local alice="alice-${tag}@verify.local"
|
||||
local bob="bob-${tag}@verify.local"
|
||||
local pw="VerifyStage0_${tag}"
|
||||
local alice="alice-${tag}@verify-stage0.com"
|
||||
local bob="bob-${tag}@verify-stage0.com"
|
||||
local pw="VerifyStage0_${tag}!"
|
||||
local jar_a=/tmp/verify_alice_${tag}.cookies
|
||||
local jar_b=/tmp/verify_bob_${tag}.cookies
|
||||
rm -f "$jar_a" "$jar_b"
|
||||
|
||||
# Ensure system is initialized (admin account exists) before registering users.
|
||||
info "ensuring admin account exists (POST /api/v1/auth/initialize)"
|
||||
local init_code
|
||||
init_code=$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"email\":\"admin-${tag}@verify-stage0.com\",\"password\":\"$pw\"}" \
|
||||
"$GATEWAY_URL/api/v1/auth/initialize")
|
||||
if [ "$init_code" = "201" ]; then
|
||||
ok "admin initialized (first boot)"
|
||||
elif [ "$init_code" = "409" ]; then
|
||||
ok "admin already exists (system previously initialized)"
|
||||
else
|
||||
fail "admin initialization returned $init_code"
|
||||
return
|
||||
fi
|
||||
|
||||
register_user() {
|
||||
local jar="$1"; local email="$2"
|
||||
curl -sS -c "$jar" -o /tmp/verify_register_$$.json -w '%{http_code}' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"email\":\"$email\",\"password\":\"$pw\"}" \
|
||||
"$GATEWAY_URL/api/auth/register"
|
||||
"$GATEWAY_URL/api/v1/auth/register"
|
||||
}
|
||||
|
||||
info "registering Alice + Bob"
|
||||
|
||||
Reference in New Issue
Block a user