Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 792bf222de | |||
| 03725fc679 | |||
| c82de18830 | |||
| df42971011 | |||
| 45618e6834 | |||
| 0c75fc15da | |||
| c3f4e91ed4 | |||
| 6fdd1cd8c8 | |||
| 7348388341 | |||
| 172ada0349 | |||
| fde5ef1419 | |||
| c97375d63f | |||
| b502337c06 |
@@ -1,30 +0,0 @@
|
||||
---
|
||||
globs: ["src/frontend/client/**"]
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Client Frontend Development Rules (src/frontend/client/)
|
||||
|
||||
## Tech Stack
|
||||
Vite + React 18 + TypeScript + TailwindCSS 3 + Radix UI (shadcn/ui) + Recoil + react-i18next + react-router-dom v6 + lucide-react + axios (wrapped in `~/api/request.ts`)
|
||||
|
||||
## Mandatory Rules
|
||||
- **TypeScript only**: All new files must use `.ts` / `.tsx`.
|
||||
- **Functional Components**: Use Hooks; class components are prohibited.
|
||||
- **Path Aliases**: Use `~/` for absolute imports (equivalent to `@/`, both map to `src/`).
|
||||
- **HTTP Requests**: Must use `~/api/request.ts`. Do not import `axios` directly.
|
||||
- **State Management**: Use Recoil (`~/store/`). Context or other solutions are prohibited for new state.
|
||||
- **UI Components**: Use `~/components/ui/` (Radix-based). Do not introduce new UI libraries.
|
||||
- **Code Comments**: All comments must be in English.
|
||||
- **Component Size**: Keep individual components under 600 lines. Extract sub-components or custom hooks when exceeded.
|
||||
- **Toast Notifications**: Use `const { showToast } = useToastContext(); showToast?.({ message, severity: 'error' | 'success' })`.
|
||||
- **i18n**: Use `useLocalize()` from `~/hooks`. Locale files at `src/locales/{en,zh-Hans,ja}/translation.json`. New keys use nested namespace format (see `/i18n-localizer` skill).
|
||||
|
||||
## Coding Style
|
||||
- **Naming**: `interface` for Props; `type` for internal types. PascalCase for components, camelCase for utilities/hooks.
|
||||
- **Event Handling**: `handleXxx` for internal logic, `onXxx` for props.
|
||||
- **Any Type**: Minimize usage. If necessary, add `// eslint-disable-next-line` with a brief explanation.
|
||||
- **Exports**: Named exports (`export function`), no default exports for components.
|
||||
|
||||
## Known Pitfalls
|
||||
- **403 Errors**: Handled automatically in the response interceptor with redirection. No manual handling needed in business logic.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
globs: ["src/frontend/platform/**"]
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Platform Frontend Development Rules (src/frontend/platform/)
|
||||
|
||||
## Tech Stack
|
||||
Vite + React 18 + TypeScript + TailwindCSS 3 + Radix UI (bs-ui) + Zustand + React Context + react-i18next + react-router-dom v6 + @xyflow/react + axios (wrapped in `@/controllers/request.ts`)
|
||||
|
||||
## Mandatory Rules
|
||||
- **TypeScript only**: All new files must use `.ts` / `.tsx`.
|
||||
- **Functional Components**: Use Hooks; class components are prohibited.
|
||||
- **Path Aliases**: Use `@/` for absolute imports (maps to `src/`).
|
||||
- **HTTP Requests**: Must use `@/controllers/request.ts`. Do not import `axios` directly. API modules in `@/controllers/API/`.
|
||||
- **State Management**: Zustand stores in `@/store/` for cross-page state. React Context (`@/contexts/`) for UI-scoped state (alerts, theme, tabs). Do not introduce new state libraries.
|
||||
- **UI Components**: Use `@/components/bs-ui/` (Radix-based). Icons from `@/components/bs-icons/`. Do not introduce new UI libraries.
|
||||
- **Code Comments**: All comments must be in English.
|
||||
- **Component Size**: Keep individual components under 600 lines. Extract sub-components or custom hooks when exceeded.
|
||||
- **Toast Notifications**: `import { toast } from "@/components/bs-ui/toast/use-toast"; toast({ title, variant: 'error' | 'success', description })`.
|
||||
- **Confirm Dialogs**: `import { bsConfirm } from "@/components/bs-ui/alertDialog/useConfirm"`.
|
||||
- **i18n**: Use `useTranslation()` from `react-i18next`. Locale files at `public/locales/{en-US,zh-Hans,ja}/{ns}.json`. Namespaces: bs, flow, model, tool, dashboard, knowledge.
|
||||
|
||||
## Coding Style
|
||||
- **Naming**: `interface` for Props; `type` for internal types. PascalCase for components, camelCase for utilities/hooks.
|
||||
- **Event Handling**: `handleXxx` for internal logic, `onXxx` for props.
|
||||
- **Any Type**: Minimize usage. If necessary, add `// eslint-disable-next-line` with a brief explanation.
|
||||
- **Exports**: Named exports (`export function`), no default exports for components.
|
||||
|
||||
## API Layer Pattern
|
||||
```typescript
|
||||
// @/controllers/API/xxx.ts
|
||||
import axios from "@/controllers/request"
|
||||
export async function getSomething(): Promise<SomeType> {
|
||||
return await axios.get(`/api/v1/something`)
|
||||
}
|
||||
|
||||
// Component usage
|
||||
import { captureAndAlertRequestErrorHoc } from "@/controllers/request"
|
||||
import { getSomething } from "@/controllers/API"
|
||||
captureAndAlertRequestErrorHoc(getSomething()).then(res => { ... })
|
||||
```
|
||||
|
||||
## Known Pitfalls
|
||||
- **403 Errors**: Handled automatically in the response interceptor. No manual handling needed.
|
||||
- **MinIO Image Proxy**: Vite `fileServiceTarget` must match backend `config.yaml` `object_storage.minio.sharepoint`, otherwise image requests get 403.
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash -c 'FILE=\"$CLAUDE_FILE\"; if [ -n \"$FILE\" ] && echo \"$FILE\" | grep -q \"\\.py$\"; then cd /Users/lilu/Projects/bisheng/src/backend && if [ -f .venv/bin/ruff ]; then .venv/bin/ruff format \"$FILE\" 2>/dev/null; .venv/bin/ruff check --fix \"$FILE\" 2>/dev/null; fi; fi'",
|
||||
"async": true
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash /Users/lilu/Projects/bisheng/scripts/arch-guard.sh \"$CLAUDE_FILE\"",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
# Skill: code-review
|
||||
|
||||
## 描述
|
||||
|
||||
L2 特性级多维度代码审查。Feature 全部任务完成后执行。
|
||||
|
||||
## 触发
|
||||
|
||||
```
|
||||
/code-review --base 2.5.0-PM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 审查流程
|
||||
|
||||
1. 执行 `git diff 2.5.0-PM...HEAD --stat` 获取变更文件列表
|
||||
2. 执行 `git diff 2.5.0-PM...HEAD` 获取完整 diff
|
||||
3. 对照 Feature 的 `spec.md` 和 `tasks.md`
|
||||
4. 按 6 维度逐一审查
|
||||
5. 输出审查报告
|
||||
|
||||
---
|
||||
|
||||
## 6 维度审查框架
|
||||
|
||||
### 维度 1:边界条件
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| null/None 处理 | 外部输入是否校验 None/空字符串 |
|
||||
| 空集合 | 列表/字典为空时是否正确处理(不抛异常) |
|
||||
| 数值边界 | 分页 page/size 合法性、ID 为 0/-1 |
|
||||
| 字符串长度 | 数据库字段长度限制是否在 API 层校验 |
|
||||
| 超时处理 | 外部调用(LLM/MCP/HTTP)是否设置超时 |
|
||||
| 分页溢出 | 请求超出总页数时返回空列表而非错误 |
|
||||
|
||||
### 维度 2:权限与认证
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| 认证注入 | 需要认证的端点是否使用 `UserPayload = Depends(UserPayload.get_login_user)` |
|
||||
| 五级权限链路 | 是否遵循:super_admin → tenant 归属 → tenant admin → ReBAC → RBAC 菜单 |
|
||||
| PermissionService | 权限检查是否走 `PermissionService.check()` 而非直接查旧表 |
|
||||
| 资源授权 | 创建资源时是否调用 `PermissionService.authorize()` 写入 owner 元组 |
|
||||
| tenant_id 隔离 | 跨租户访问是否被阻止(SQLAlchemy event 自动注入) |
|
||||
| WebSocket 认证 | WS 端点是否使用 `UserPayload.get_login_user_from_ws` |
|
||||
|
||||
### 维度 3:并发安全
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| OpenFGA 双写 | MySQL + OpenFGA 写入是否有失败补偿(failed_tuples 表) |
|
||||
| 数据库事务 | 多表写入是否在同一事务内 |
|
||||
| Celery 幂等 | 异步任务是否支持重试不产生副作用 |
|
||||
| 竞态条件 | 并发创建同名资源是否有唯一约束或乐观锁 |
|
||||
| 会话状态 | Redis 缓存读写是否考虑过期和并发更新 |
|
||||
|
||||
### 维度 4:信息泄漏
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| 硬编码敏感信息 | 代码中无明文密码/密钥/token |
|
||||
| 错误信息 | 异常响应不暴露堆栈/SQL/内部路径 |
|
||||
| 日志脱敏 | logger 输出中敏感字段已脱敏 |
|
||||
| 前端暴露 | 前端代码不包含后端 IP/密钥/内部 API 路径 |
|
||||
| tenant_id 泄漏 | API 响应不向前端返回其他租户的 tenant_id |
|
||||
|
||||
### 维度 5:测试覆盖
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| Service 测试 | 核心 Service 方法有单元测试(mock DAO) |
|
||||
| API 测试 | 新端点有集成测试(happy path + 主要 error path) |
|
||||
| AC 覆盖 | spec 中每条 AC 都有对应测试或手动验证 |
|
||||
| 错误路径 | 权限拒绝、参数校验失败等错误路径有测试 |
|
||||
| 测试质量 | mock 合理,不 mock 掉核心逻辑 |
|
||||
|
||||
> **务实适配**:当前测试基础薄弱,降低阈值但要求核心 Service 方法必须有测试。
|
||||
> 前端暂用手动验证替代(tasks.md 中有「手动验证」描述即可)。
|
||||
|
||||
### 维度 6:代码风格
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| DDD 分层 | 新代码在正确的层级(domain/services vs api/endpoints) |
|
||||
| 命名一致 | DAO/Service/错误码命名遵循项目约定 |
|
||||
| 代码重复 | 无复制粘贴式重复逻辑(应提取到 Service 或工具函数) |
|
||||
| 未使用代码 | 无 dead code、注释掉的代码块、空函数 |
|
||||
| 格式化 | Python 代码通过 ruff check(hook 自动处理) |
|
||||
|
||||
---
|
||||
|
||||
## 判定规则
|
||||
|
||||
| 结果 | 条件 | 动作 |
|
||||
|------|------|------|
|
||||
| **PASS** | 无 HIGH 或 MEDIUM | 可合并 |
|
||||
| **PASS_WITH_WARNINGS** | 仅 MEDIUM 级 | 可合并,记录待改进 |
|
||||
| **NEEDS_FIX** | 有 HIGH 级 | 修复后重审(最多 2 轮) |
|
||||
|
||||
---
|
||||
|
||||
## 输出格式
|
||||
|
||||
```markdown
|
||||
# Code Review Report
|
||||
|
||||
**Feature**: <feature_name>
|
||||
**Review scope**: <描述>
|
||||
**Base branch**: 2.5.0-PM
|
||||
**Changed files**: <数量>
|
||||
|
||||
## Summary
|
||||
|
||||
| Dimension | High | Medium | Low | Status |
|
||||
|-----------|------|--------|-----|--------|
|
||||
| Boundary Conditions | 0 | 0 | 0 | PASS |
|
||||
| Permission & Auth | 0 | 0 | 0 | PASS |
|
||||
| Concurrency Safety | 0 | 0 | 0 | PASS |
|
||||
| Information Leakage | 0 | 0 | 0 | PASS |
|
||||
| Test Coverage | 0 | 0 | 0 | PASS |
|
||||
| Code Style | 0 | 0 | 0 | PASS |
|
||||
|
||||
## Findings(如有)
|
||||
|
||||
### HIGH
|
||||
- [Permission] `xxx_endpoint.py:42` — 缺少 PermissionService.check() 调用
|
||||
|
||||
### MEDIUM
|
||||
- [Style] `xxx_service.py:18` — DAO 方法未使用 @classmethod
|
||||
|
||||
## Overall: PASS / PASS_WITH_WARNINGS / NEEDS_FIX
|
||||
```
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
name: e2e-test
|
||||
description: >-
|
||||
为 BiSheng 生成和运行 E2E 测试。两种模式:
|
||||
(1) SDD 模式 — 基于 feature spec.md 的 AC 生成覆盖;
|
||||
(2) 自由模式 — 对指定页面/功能写测试。
|
||||
采用双层策略:API 端到端测试(pytest + httpx)+ 页面手动验证清单。
|
||||
自动处理认证、多租户隔离、权限检查、Radix UI 交互等常见问题。
|
||||
用法:/e2e-test [feature_dir] 或 /e2e-test <描述>
|
||||
当用户说"写 E2E 测试"、"端到端测试"、"E2E coverage",
|
||||
或使用 /e2e-test 命令时触发。
|
||||
---
|
||||
|
||||
# E2E Test Skill
|
||||
|
||||
## 概述
|
||||
|
||||
生成并运行 BiSheng 的 E2E 测试,覆盖 API 链路和 UI 交互流程。自动处理 JWT 认证、多租户数据隔离、OpenFGA 权限检查验证、UnifiedResponseModel 响应断言等 BiSheng 特有问题。
|
||||
|
||||
## 调用方式
|
||||
|
||||
```
|
||||
/e2e-test <feature_dir> # SDD 模式:基于 spec.md AC 生成
|
||||
/e2e-test <描述> # 自由模式:对指定页面/功能写测试
|
||||
```
|
||||
|
||||
示例:
|
||||
```
|
||||
/e2e-test features/v2.5.0/004-rebac-core
|
||||
/e2e-test 为租户管理页面写创建流程测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六步流程
|
||||
|
||||
### Step 1:模式识别
|
||||
|
||||
解析用户参数:
|
||||
|
||||
- **SDD 模式**:参数是 `features/` 开头的路径 → 读取该目录下的 `spec.md`
|
||||
- **自由模式**:参数是自由文本描述 → 直接进入 Step 3
|
||||
|
||||
### Step 2:AC 分析(仅 SDD 模式)
|
||||
|
||||
读取 `<feature_dir>/spec.md`,从 AC 表格中分类:
|
||||
|
||||
**API 行为类**(自动化 pytest 测试):
|
||||
- CRUD 操作及响应格式
|
||||
- 权限检查(允许/拒绝)
|
||||
- 分页、过滤、排序
|
||||
- 错误码返回(MMMEE)
|
||||
- 跨租户访问拒绝
|
||||
|
||||
**UI 交互类**(手动验证清单):
|
||||
- 表单填写、按钮点击
|
||||
- 列表展示、搜索过滤
|
||||
- 弹窗/抽屉交互
|
||||
- 路由跳转
|
||||
- 权限控制(按钮隐藏/禁用)
|
||||
|
||||
**排除**:
|
||||
- 纯样式/布局调整
|
||||
- 纯内部状态逻辑
|
||||
|
||||
输出分类后的 AC 列表,作为测试用例依据。
|
||||
|
||||
### Step 3:基础设施检查
|
||||
|
||||
检查共享 helpers 是否存在:
|
||||
|
||||
```
|
||||
src/backend/test/e2e/
|
||||
├── conftest.py # pytest fixtures(认证、client、cleanup)
|
||||
├── helpers/
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py # JWT 认证 + 用户创建
|
||||
│ ├── api.py # API 常量 + 通用 CRUD helpers
|
||||
│ └── cleanup.py # 数据隔离 + 安全 cleanup
|
||||
└── test_e2e_xxx.py # 各 Feature 的测试文件
|
||||
```
|
||||
|
||||
如果不存在,按照 `references/test-template.md` 创建基础设施。
|
||||
如果需要新增共享函数,先加到对应的 helpers 文件中。
|
||||
|
||||
### Step 4:生成测试
|
||||
|
||||
基于 `references/test-template.md` 生成测试文件。
|
||||
|
||||
**文件命名**:`src/backend/test/e2e/test_e2e_{feature_name}.py`
|
||||
|
||||
**强制生成规则(12 条)**:
|
||||
|
||||
1. **数据隔离(红线)**:测试数据统一 `e2e-{feature}-` 前缀(≥5 字符)。**禁止无条件删除所有资源**——cleanup 必须按前缀过滤,只删本套件创建的数据。E2E 运行前后,非测试数据必须保持不变
|
||||
2. **双重 cleanup**:setup fixture 清理上次残留 + teardown 清理本次数据
|
||||
3. **测试租户隔离**:使用专用 `test_tenant_id`,不影响正式租户数据。创建测试数据前先确保测试租户存在
|
||||
4. **认证流程**:通过 helpers 获取 JWT token,注入到请求 headers。测试管理员和普通用户两种角色
|
||||
5. **响应格式断言**:所有 API 响应必须断言 `UnifiedResponseModel` 格式(`status_code`, `status_message`, `data`)
|
||||
6. **权限测试配对**:每个"允许"操作配对一个"拒绝"测试(不同角色/不同租户)
|
||||
7. **共享 helpers**:导入 `test/e2e/helpers/` 的函数,**禁止在测试文件内重新定义**通用工具函数
|
||||
8. **AC 追溯**:每个测试方法的 docstring 标注 `AC-NN: <描述>`
|
||||
9. **API 验证**:数据变更操作后,通过 GET 请求断言最终状态(不仅依赖创建响应)
|
||||
10. **错误码精确断言**:业务错误断言具体的 MMMEE 错误码,不仅检查非 200
|
||||
11. **串行执行**:使用 pytest-ordering 或 class 内方法顺序保证 setup → tests → cleanup
|
||||
12. **幂等性**:测试可重复运行,不依赖特定的数据库状态(除测试自己创建的数据)
|
||||
|
||||
### Step 5:运行与修复
|
||||
|
||||
运行生成的测试:
|
||||
|
||||
```bash
|
||||
cd src/backend
|
||||
.venv/bin/pytest test/e2e/test_e2e_{feature_name}.py -v
|
||||
```
|
||||
|
||||
如果失败,按照 `references/common-pitfalls.md` 的诊断表定位问题。
|
||||
|
||||
**最多 3 轮修复**。如果 3 轮后仍有失败,输出剩余问题让用户决定。
|
||||
|
||||
**调试技巧**:
|
||||
```bash
|
||||
# 单个测试
|
||||
.venv/bin/pytest test/e2e/test_e2e_{feature}.py::TestE2E{Feature}::test_ac01 -v -s
|
||||
|
||||
# 显示完整请求/响应
|
||||
.venv/bin/pytest test/e2e/test_e2e_{feature}.py -v -s --log-cli-level=DEBUG
|
||||
|
||||
# 只运行失败的
|
||||
.venv/bin/pytest test/e2e/test_e2e_{feature}.py --lf -v
|
||||
```
|
||||
|
||||
### Step 6:覆盖报告
|
||||
|
||||
输出 AC 覆盖表:
|
||||
|
||||
```markdown
|
||||
# E2E 覆盖报告: <feature_name>
|
||||
|
||||
## API 测试结果
|
||||
|
||||
| AC-ID | 描述 | 状态 | 测试方法 |
|
||||
|-------|------|------|---------|
|
||||
| AC-01 | 创建租户成功 | ✅ 通过 | test_ac01_create_tenant |
|
||||
| AC-02 | 重复租户名拒绝 | ✅ 通过 | test_ac02_duplicate_name |
|
||||
| AC-05 | 表单提交创建 | ⏭️ 跳过(UI 交互,见手动清单) | — |
|
||||
|
||||
通过: N/M | 跳过: K(UI 交互类)| 失败: J
|
||||
|
||||
## 手动验证清单
|
||||
|
||||
生成位置: `features/v2.5.0/{NNN}-{name}/e2e-checklist.md`
|
||||
覆盖 AC: AC-05, AC-06, ...
|
||||
|
||||
## 整体状态: PASS / PARTIAL / FAIL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手动验证清单格式
|
||||
|
||||
当 AC 涉及 UI 交互时,生成结构化验证清单。
|
||||
|
||||
**文件位置**:`features/v2.5.0/{NNN}-{name}/e2e-checklist.md`
|
||||
|
||||
```markdown
|
||||
# E2E 验证清单: {feature_name}
|
||||
|
||||
**测试环境**: http://192.168.106.114:4001 (Platform) / :4001/workspace (Client)
|
||||
**前置条件**: <描述测试前需要的数据/账号>
|
||||
|
||||
## Platform 前端
|
||||
|
||||
### AC-05: <描述>
|
||||
- [ ] 步骤 1: 以管理员登录 Platform (admin/admin123)
|
||||
- [ ] 步骤 2: 导航到 <页面路径>
|
||||
- [ ] 步骤 3: 点击 <按钮/元素>
|
||||
- [ ] 步骤 4: 填写表单: <字段=值>
|
||||
- [ ] 预期: <具体可观察结果,如 toast 提示、列表刷新>
|
||||
- [ ] 验证: 刷新页面后数据仍存在
|
||||
|
||||
### AC-06: <错误场景描述>
|
||||
- [ ] 步骤: <触发错误的操作>
|
||||
- [ ] 预期: <错误提示内容>
|
||||
|
||||
## Client 前端(如适用)
|
||||
|
||||
### AC-07: <描述>
|
||||
- [ ] ...
|
||||
|
||||
## 回归检查
|
||||
- [ ] 相关页面(<列出>)正常加载,无 console 错误
|
||||
- [ ] 既有功能(<列出>)不受影响
|
||||
- [ ] 不同角色(管理员/普通用户)看到的内容符合权限设定
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考文件
|
||||
|
||||
生成测试前**必须阅读**以下参考文件:
|
||||
|
||||
| 文件 | 用途 | 何时阅读 |
|
||||
|------|------|---------|
|
||||
| `references/test-template.md` | pytest E2E 测试骨架模板 | 生成新测试文件时 |
|
||||
| `references/common-pitfalls.md` | BiSheng E2E 常见陷阱诊断表 | 测试失败时 |
|
||||
|
||||
---
|
||||
|
||||
## 已有共享 Helpers 清单
|
||||
|
||||
> 首次运行时由 Step 3 自动创建。以下是目标结构。
|
||||
|
||||
### `test/e2e/helpers/auth.py`
|
||||
|
||||
| 函数 | 签名 | 用途 |
|
||||
|------|------|------|
|
||||
| `get_admin_token` | `(client) -> str` | 获取管理员 JWT token |
|
||||
| `get_user_token` | `(client, username, password) -> str` | 获取指定用户 JWT token |
|
||||
| `create_test_user` | `(client, admin_token, username, role_id) -> dict` | 创建测试用户 |
|
||||
| `auth_headers` | `(token) -> dict` | 构建认证请求头 |
|
||||
|
||||
### `test/e2e/helpers/api.py`
|
||||
|
||||
| 导出 | 用途 |
|
||||
|------|------|
|
||||
| `API_BASE` | 后端 API 基础 URL 常量 (`http://localhost:7860/api/v1`) |
|
||||
| `assert_resp_200(resp)` | 断言 UnifiedResponseModel 成功响应 |
|
||||
| `assert_resp_error(resp, code)` | 断言 UnifiedResponseModel 错误码 |
|
||||
| `create_resource(client, path, data, token)` | 通用 POST 创建 |
|
||||
| `list_resources(client, path, token, params)` | 通用 GET 列表 |
|
||||
| `delete_resource(client, path, resource_id, token)` | 通用 DELETE |
|
||||
|
||||
### `test/e2e/helpers/cleanup.py`
|
||||
|
||||
| 函数 | 用途 |
|
||||
|------|------|
|
||||
| `cleanup_by_prefix(client, path, prefix, token)` | 安全删除指定前缀的资源。**前缀必须 ≥5 字符**,否则抛错防止误删 |
|
||||
| `ensure_test_tenant(client, admin_token, tenant_code)` | 确保测试租户存在(不存在则创建) |
|
||||
|
||||
---
|
||||
|
||||
## 新增 Helper 的规则
|
||||
|
||||
当测试需要新的共享函数时:
|
||||
|
||||
1. **认证相关** → 加到 `helpers/auth.py`
|
||||
2. **API 请求/断言** → 加到 `helpers/api.py`
|
||||
3. **数据管理/fixtures** → 加到 `helpers/cleanup.py`
|
||||
4. **特定 feature 的 helper** → 留在测试文件内,不提取
|
||||
|
||||
提取标准:**2 个以上测试文件使用** → 提取到 helpers。
|
||||
@@ -1,216 +0,0 @@
|
||||
# BiSheng E2E 测试常见陷阱与诊断修复
|
||||
|
||||
## 陷阱 1:业务错误 HTTP 200
|
||||
|
||||
**症状**:`assert resp.status_code == 400` 失败,实际收到 200。
|
||||
|
||||
**原因**:BiSheng 的 `UnifiedResponseModel` 将业务错误包装在 HTTP 200 响应体中,通过 `status_code` 字段区分。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ❌ BiSheng 业务错误也返回 HTTP 200
|
||||
assert resp.status_code == 400
|
||||
|
||||
# ✅ 检查响应体中的 status_code
|
||||
body = resp.json()
|
||||
assert body["status_code"] == 10901 # 具体 MMMEE 错误码
|
||||
assert body["status_message"] != "SUCCESS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 2:认证 Token 获取失败
|
||||
|
||||
**症状**:登录 API 返回错误,或后续请求 401。
|
||||
|
||||
**原因**:BiSheng 登录密码需要 RSA 加密。前端从 `/api/v1/user/public_key` 获取公钥后加密。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 先获取公钥,再加密密码
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
resp = await client.get("/user/public_key")
|
||||
public_key_pem = resp.json()["data"]["public_key"]
|
||||
|
||||
# 加密密码
|
||||
public_key = serialization.load_pem_public_key(public_key_pem.encode())
|
||||
encrypted = public_key.encrypt(password.encode(), padding.PKCS1v15())
|
||||
encrypted_password = base64.b64encode(encrypted).decode()
|
||||
|
||||
# 登录
|
||||
resp = await client.post("/user/login", json={
|
||||
"user_name": username,
|
||||
"password": encrypted_password,
|
||||
})
|
||||
```
|
||||
|
||||
**建议**:将此逻辑封装在 `helpers/auth.py` 中,测试文件直接调用 `get_admin_token()`。
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 3:tenant_id 自动注入导致测试数据不可见
|
||||
|
||||
**症状**:创建了数据但 GET 列表查不到。
|
||||
|
||||
**原因**:SQLAlchemy event 自动注入 `tenant_id` 过滤,测试用户的 tenant_id 与数据不匹配。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 确保测试用户属于正确的租户
|
||||
# 1. 创建测试租户
|
||||
# 2. 将测试用户加入该租户
|
||||
# 3. 用该用户的 token 创建和查询数据
|
||||
|
||||
# ❌ 不要试图绕过 tenant_id(那是安全底线)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 4:OpenFGA 权限未同步
|
||||
|
||||
**症状**:创建资源后,同一用户立即查询却被权限拒绝。
|
||||
|
||||
**原因**:资源创建时应同步写入 OpenFGA owner 元组,如果 `PermissionService.authorize()` 调用失败或遗漏,用户虽然创建了资源但没有 owner 权限。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 创建后验证权限元组已写入
|
||||
resp = await client.post("/resource", json={...}, headers=admin_headers)
|
||||
data = assert_resp_200(resp)
|
||||
|
||||
# 紧接着用同一用户查询,应该能看到
|
||||
get_resp = await client.get(f"/resource/{data['id']}", headers=admin_headers)
|
||||
assert_resp_200(get_resp) # 如果失败,说明 OpenFGA 元组没写入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 5:cleanup 顺序错误
|
||||
|
||||
**症状**:`DELETE /resource/{id}` 返回错误,因为有关联数据未先删除。
|
||||
|
||||
**原因**:BiSheng 资源间有关联关系(如知识库→文件、助手→工具/技能/知识库),删除有顺序要求。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 正确的 cleanup 顺序(依赖关系逆序)
|
||||
async def cleanup_feature_data(client, token, prefix):
|
||||
headers = auth_headers(token)
|
||||
|
||||
# 1. 先删除依赖方(如关联表、子资源)
|
||||
# 2. 再删除主资源
|
||||
# 3. 最后清理 OpenFGA 元组(如有直接操作的话)
|
||||
|
||||
# 示例:删除知识库
|
||||
# 先删知识库文件 → 再删知识库空间
|
||||
|
||||
# ❌ 不要假设可以直接删除主资源
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 6:Celery 异步任务未完成就断言
|
||||
|
||||
**症状**:创建知识库文件后立即查询,状态还是 `WAITING` 而非 `SUCCESS`。
|
||||
|
||||
**原因**:文件处理通过 Celery `knowledge_celery` 队列异步执行,创建 API 返回后任务可能还在处理。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 轮询等待异步任务完成
|
||||
import asyncio
|
||||
|
||||
async def wait_for_status(client, path, token, expected_status, timeout=30):
|
||||
headers = auth_headers(token)
|
||||
for _ in range(timeout):
|
||||
resp = await client.get(path, headers=headers)
|
||||
data = resp.json()["data"]
|
||||
if data["status"] == expected_status:
|
||||
return data
|
||||
await asyncio.sleep(1)
|
||||
raise TimeoutError(f"Status not reached: {expected_status}")
|
||||
|
||||
# 使用
|
||||
data = await wait_for_status(
|
||||
client, f"/knowledge_file/{file_id}",
|
||||
admin_token, expected_status=2 # SUCCESS
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 7:分页参数不一致
|
||||
|
||||
**症状**:列表查询返回的数据数量不对。
|
||||
|
||||
**原因**:BiSheng 不同 API 的分页参数名称可能不同(`page`/`page_num`、`limit`/`page_size`/`size`)。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 先查看 API 文档确认参数名
|
||||
# 常见模式:
|
||||
resp = await client.get("/resource", params={
|
||||
"page": 1, # 或 page_num
|
||||
"limit": 10, # 或 page_size 或 size
|
||||
}, headers=headers)
|
||||
|
||||
# ✅ 响应分页格式(PageData)
|
||||
data = resp.json()["data"]
|
||||
items = data["data"] # 列表数据
|
||||
total = data["total"] # 总数
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 8:WebSocket 测试
|
||||
|
||||
**症状**:WebSocket 连接失败或消息收不到。
|
||||
|
||||
**原因**:BiSheng 的 WebSocket 使用特殊的认证方式(`UserPayload.get_login_user_from_ws`),token 通过 query 参数传递。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ WebSocket 认证
|
||||
import websockets
|
||||
|
||||
async with websockets.connect(
|
||||
f"ws://localhost:7860/api/v1/chat/{flow_id}?t={token}"
|
||||
) as ws:
|
||||
# 发送消息
|
||||
await ws.send(json.dumps({"message": "hello"}))
|
||||
# 接收响应
|
||||
response = await ws.recv()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 陷阱 9:RSA 公钥缓存
|
||||
|
||||
**症状**:多个测试用不同用户登录,部分登录失败。
|
||||
|
||||
**原因**:公钥可能在短时间内变化,或 RSA 加密使用了错误的 padding。
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# ✅ 每次登录前重新获取公钥(不缓存)
|
||||
# helpers/auth.py 中的 get_token() 函数应每次都获取新公钥
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速诊断表
|
||||
|
||||
| 错误关键词 | 可能原因 | 首先检查 |
|
||||
|-----------|---------|---------|
|
||||
| HTTP 200 但 status_code 非 200 | 业务错误 | 检查 MMMEE 错误码含义 |
|
||||
| 401 Unauthorized | Token 过期或格式错误 | 重新获取 token,检查 Cookie/Header |
|
||||
| 查不到刚创建的数据 | tenant_id 不匹配 | 确认用户与资源同租户 |
|
||||
| 权限拒绝(刚创建的资源) | OpenFGA 元组未写入 | 检查 PermissionService.authorize() |
|
||||
| DELETE 失败 400/409 | 有关联数据 | 按依赖逆序删除 |
|
||||
| 异步操作状态不对 | Celery 任务未完成 | 轮询等待 + 增加 timeout |
|
||||
| 分页数据数量不对 | 参数名不一致 | 查 API 文档确认 page/limit 参数名 |
|
||||
| 登录失败 | RSA 加密问题 | 检查公钥获取和加密 padding |
|
||||
| `Connection refused` | 后端未启动 | 确认 localhost:7860 可访问 |
|
||||
| `Redis connection error` | Redis 未启动 | 确认 Redis 服务运行中 |
|
||||
@@ -1,226 +0,0 @@
|
||||
# E2E 测试文件骨架模板
|
||||
|
||||
## 完整 pytest 模板
|
||||
|
||||
```python
|
||||
"""
|
||||
E2E tests for <FEATURE_NAME>
|
||||
|
||||
Prerequisites:
|
||||
- Backend running on localhost:7860
|
||||
- MySQL/Redis/Milvus/ES/OpenFGA services running
|
||||
|
||||
Covers:
|
||||
- AC-01: <description>
|
||||
- AC-02: <description>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import httpx
|
||||
|
||||
from test.e2e.helpers.auth import get_admin_token, get_user_token, auth_headers, create_test_user
|
||||
from test.e2e.helpers.api import API_BASE, assert_resp_200, assert_resp_error
|
||||
from test.e2e.helpers.cleanup import cleanup_by_prefix, ensure_test_tenant
|
||||
|
||||
# Data prefix for test isolation (must be >= 5 chars)
|
||||
PREFIX = "e2e-<feature>-"
|
||||
|
||||
# Test tenant for multi-tenant isolation
|
||||
TEST_TENANT = "e2e-<feature>-tenant"
|
||||
|
||||
|
||||
class TestE2E<FeatureName>:
|
||||
"""E2E: <feature_name>"""
|
||||
|
||||
# ──────── Fixtures ────────
|
||||
|
||||
@pytest.fixture(autouse=True, scope="class")
|
||||
async def setup_and_teardown(self):
|
||||
"""双重 cleanup: setup 清理上次残留 + teardown 清理本次"""
|
||||
async with httpx.AsyncClient(base_url=API_BASE, timeout=30.0) as client:
|
||||
# Setup: 获取 admin token
|
||||
admin_token = await get_admin_token(client)
|
||||
headers = auth_headers(admin_token)
|
||||
|
||||
# Setup: 确保测试租户存在
|
||||
await ensure_test_tenant(client, admin_token, TEST_TENANT)
|
||||
|
||||
# Setup: 清理上次残留的测试数据
|
||||
await cleanup_by_prefix(client, "/resource", PREFIX, admin_token)
|
||||
|
||||
yield # 运行测试
|
||||
|
||||
# Teardown: 清理本次创建的测试数据
|
||||
await cleanup_by_prefix(client, "/resource", PREFIX, admin_token)
|
||||
|
||||
@pytest.fixture
|
||||
async def client(self):
|
||||
"""提供 httpx AsyncClient"""
|
||||
async with httpx.AsyncClient(base_url=API_BASE, timeout=30.0) as client:
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
async def admin_token(self, client):
|
||||
"""获取管理员 token"""
|
||||
return await get_admin_token(client)
|
||||
|
||||
@pytest.fixture
|
||||
async def user_token(self, client, admin_token):
|
||||
"""创建并返回普通用户 token"""
|
||||
user = await create_test_user(
|
||||
client, admin_token,
|
||||
username=f"{PREFIX}user",
|
||||
role_id=2 # DefaultRole
|
||||
)
|
||||
return await get_user_token(client, user["user_name"], "test_password")
|
||||
|
||||
# ──────── Happy Path Tests ────────
|
||||
|
||||
async def test_ac01_create_success(self, client, admin_token):
|
||||
"""AC-01: <操作描述> → <预期结果>"""
|
||||
headers = auth_headers(admin_token)
|
||||
|
||||
# 创建资源
|
||||
resp = await client.post(
|
||||
"/resource",
|
||||
json={"name": f"{PREFIX}test-entity"},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# 断言 UnifiedResponseModel 成功格式
|
||||
data = assert_resp_200(resp)
|
||||
assert data["name"] == f"{PREFIX}test-entity"
|
||||
assert "id" in data
|
||||
|
||||
# 通过 GET 验证最终状态(不仅依赖创建响应)
|
||||
get_resp = await client.get(f"/resource/{data['id']}", headers=headers)
|
||||
get_data = assert_resp_200(get_resp)
|
||||
assert get_data["name"] == f"{PREFIX}test-entity"
|
||||
|
||||
async def test_ac02_list_with_pagination(self, client, admin_token):
|
||||
"""AC-02: 分页查询资源列表"""
|
||||
headers = auth_headers(admin_token)
|
||||
|
||||
resp = await client.get(
|
||||
"/resource",
|
||||
params={"page": 1, "limit": 10},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
data = assert_resp_200(resp)
|
||||
assert "data" in data # PageData format
|
||||
assert "total" in data
|
||||
|
||||
# ──────── Error Path Tests ────────
|
||||
|
||||
async def test_ac03_duplicate_name_rejected(self, client, admin_token):
|
||||
"""AC-03: 重复名称 → 返回 MMMEE 错误码"""
|
||||
headers = auth_headers(admin_token)
|
||||
|
||||
# 创建第一个
|
||||
await client.post(
|
||||
"/resource",
|
||||
json={"name": f"{PREFIX}duplicate"},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# 创建同名第二个
|
||||
resp = await client.post(
|
||||
"/resource",
|
||||
json={"name": f"{PREFIX}duplicate"},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# 断言具体错误码(不仅检查非 200)
|
||||
assert_resp_error(resp, expected_code=10901) # MMMEE
|
||||
|
||||
# ──────── Permission Tests ────────
|
||||
|
||||
async def test_ac04_unauthorized_access_denied(self, client, user_token, admin_token):
|
||||
"""AC-04: 普通用户无权访问管理接口 → 权限拒绝"""
|
||||
headers = auth_headers(user_token)
|
||||
|
||||
resp = await client.get("/admin-only-resource", headers=headers)
|
||||
assert_resp_error(resp, expected_code=10601) # user permission denied
|
||||
|
||||
async def test_ac05_cross_tenant_blocked(self, client, admin_token):
|
||||
"""AC-05: 跨租户访问 → tenant_id 不匹配拒绝"""
|
||||
# 创建资源属于 tenant A
|
||||
headers_a = auth_headers(admin_token) # tenant A
|
||||
resp = await client.post(
|
||||
"/resource",
|
||||
json={"name": f"{PREFIX}tenant-a-only"},
|
||||
headers=headers_a,
|
||||
)
|
||||
resource_id = assert_resp_200(resp)["id"]
|
||||
|
||||
# 用 tenant B 的 token 尝试访问
|
||||
# (需要创建 tenant B 的用户和 token)
|
||||
# headers_b = auth_headers(tenant_b_token)
|
||||
# resp = await client.get(f"/resource/{resource_id}", headers=headers_b)
|
||||
# assert resp.status_code == 200
|
||||
# body = resp.json()
|
||||
# assert body["status_code"] != 200 # 应该被拒绝
|
||||
```
|
||||
|
||||
## 关键结构规则
|
||||
|
||||
1. **class-based 组织** — 每个 Feature 一个 TestClass,fixture 管理生命周期
|
||||
2. **setup_and_teardown 是 class-scoped** — 确保整个类运行前清理 + 运行后清理
|
||||
3. **每个测试方法 docstring 标注 AC-NN** — 追溯到 spec.md 的 AC 表格
|
||||
4. **PREFIX 常量** — 所有测试数据以 `e2e-{feature}-` 开头
|
||||
5. **API 验证** — 数据变更后,通过 GET 断言最终状态
|
||||
6. **共享 helpers** — 认证/断言/清理使用 `test/e2e/helpers/`,不在文件内重定义
|
||||
7. **权限配对** — 每个 "允许" 操作配对一个 "拒绝" 测试
|
||||
|
||||
## 响应断言模式
|
||||
|
||||
```python
|
||||
# ✅ 正确:断言 UnifiedResponseModel 完整格式
|
||||
def assert_resp_200(resp):
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status_code"] == 200
|
||||
assert body["status_message"] == "SUCCESS"
|
||||
return body["data"]
|
||||
|
||||
# ✅ 正确:断言具体 MMMEE 错误码
|
||||
def assert_resp_error(resp, expected_code):
|
||||
body = resp.json()
|
||||
assert body["status_code"] == expected_code
|
||||
|
||||
# ❌ 错误:只检查 HTTP 状态码
|
||||
assert resp.status_code == 400 # BiSheng 业务错误也返回 HTTP 200
|
||||
```
|
||||
|
||||
## 认证模式
|
||||
|
||||
```python
|
||||
# ✅ JWT Cookie 认证(BiSheng 主要认证方式)
|
||||
headers = {"Cookie": f"access_token_cookie={token}"}
|
||||
|
||||
# ✅ 或 Header 认证
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 获取 token
|
||||
resp = await client.post("/user/login", json={
|
||||
"user_name": "admin",
|
||||
"password": "<rsa_encrypted_password>"
|
||||
})
|
||||
token = resp.json()["data"]["access_token"]
|
||||
```
|
||||
|
||||
## 多租户测试模式
|
||||
|
||||
```python
|
||||
# ✅ 测试租户隔离
|
||||
TEST_TENANT_CODE = "e2e-feature-tenant"
|
||||
|
||||
# setup: 确保测试租户存在
|
||||
await ensure_test_tenant(client, admin_token, TEST_TENANT_CODE)
|
||||
|
||||
# 创建属于测试租户的数据
|
||||
# (tenant_id 由 SQLAlchemy event 自动注入,不需手动设置)
|
||||
|
||||
# 验证:不同租户的用户看不到此数据
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: i18n-localizer
|
||||
description: Internationalize a module by extracting hardcoded Chinese strings, generating translation keys, and updating all three locale files (en, zh-Hans, ja).
|
||||
---
|
||||
|
||||
# i18n Localizer
|
||||
|
||||
This skill extracts hardcoded Chinese strings from a React module and replaces them with `useLocalize()` calls, keeping all three locale files in sync.
|
||||
|
||||
## Instructions
|
||||
1. **Read the Workflow**: Read the content of `resources/INSTRUCTIONS.md` for the complete step-by-step process.
|
||||
2. **Read the Conventions**: Read `resources/CONVENTIONS.md` for key naming rules and usage patterns.
|
||||
4. **Execute**: Follow the workflow to localize the target module.
|
||||
@@ -1,133 +0,0 @@
|
||||
# i18n Conventions for This Project
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Library**: `i18next` (v24+) + `react-i18next` (v15+) + `i18next-browser-languagedetector` (v8+)
|
||||
- **Supported Languages**: `en` (English), `zh-Hans` (Simplified Chinese), `ja` (Japanese)
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/locales/i18n.ts` | i18next initialization and configuration |
|
||||
| `src/locales/en/translation.json` | English translations |
|
||||
| `src/locales/zh-Hans/translation.json` | Simplified Chinese translations |
|
||||
| `src/locales/ja/translation.json` | Japanese translations |
|
||||
| `src/hooks/useLocalize.ts` | Custom hook wrapping `useTranslation` with Recoil lang state |
|
||||
|
||||
## Key Naming Convention
|
||||
|
||||
### Domain Namespaces
|
||||
|
||||
Keys are organized by domain namespace. Each domain is a top-level object in the JSON:
|
||||
|
||||
| Namespace | Scope |
|
||||
|-----------|-------|
|
||||
| `com_ui` | General UI elements (buttons, labels, status text) |
|
||||
| `com_nav` | Navigation, sidebar, top bar, menus |
|
||||
| `com_auth` | Authentication (login, register, password) |
|
||||
| `com_endpoint` | LLM endpoint configuration |
|
||||
| `com_sop` | SOP / task execution features |
|
||||
| `com_knowledge` | Knowledge base management |
|
||||
| `com_tools` | Tool panel and tool-related features |
|
||||
| `com_agent` | Agent-related features |
|
||||
| `com_app` | App center / agent marketplace |
|
||||
| `com_invite` | Invitation features |
|
||||
| `com_linsight` | Linsight-specific features |
|
||||
| `com_label` | Label / tagging features |
|
||||
| `com_search` | Search-related features |
|
||||
| `com_file` | File management |
|
||||
| `com_message` | Chat message related |
|
||||
| `com_segment` | Mode segment features |
|
||||
|
||||
### Key Naming Rules
|
||||
|
||||
1. Use **snake_case** (all lowercase, underscores between words).
|
||||
2. Keep keys **descriptive but concise** (2-5 words).
|
||||
3. For similar operations, use consistent suffixes: `_success`, `_error`, `_failed`, `_confirm`, `_placeholder`, `_title`, `_desc`.
|
||||
4. Do NOT include the translated text in the key name.
|
||||
|
||||
## JSON File Format
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Legacy keys** (flat format like `"com_ui_cancel": "Cancel"`) MUST be left as-is. Do NOT refactor them.
|
||||
> **New keys** MUST use the nested namespace format described below.
|
||||
|
||||
### New Key Format (Nested)
|
||||
|
||||
New keys use nested objects grouped by domain namespace:
|
||||
|
||||
```json
|
||||
{
|
||||
"com_ui_cancel": "Cancel",
|
||||
"com_ui_delete": "Delete",
|
||||
|
||||
"com_knowledge": {
|
||||
"space_create_success": "Knowledge space created",
|
||||
"space_deleted": "Space has been dissolved",
|
||||
"folder_max_depth": "Folder depth limit reached (10 levels)",
|
||||
"drop_to_upload": "Drop files here to upload"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Old flat keys like `"com_ui_cancel"` stay untouched at root level.
|
||||
- New keys go inside their namespace object (e.g. `com_knowledge.space_create_success`).
|
||||
- Within each namespace object, keys are sorted alphabetically.
|
||||
- Namespace objects are placed after all legacy flat keys, also sorted alphabetically.
|
||||
|
||||
### Interpolation
|
||||
|
||||
- Use `{{0}}`, `{{1}}` for positional args; `{{name}}` for named args.
|
||||
- Use `$t(keyName)` to reference other keys inline.
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### Import Pattern
|
||||
|
||||
```tsx
|
||||
// Preferred: via the barrel export
|
||||
import { useLocalize } from "~/hooks";
|
||||
|
||||
// Alternative: direct import
|
||||
import useLocalize from "~/hooks/useLocalize";
|
||||
```
|
||||
|
||||
### Component Usage
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* New nested key — use dot notation */}
|
||||
<h1>{localize("com_knowledge.title")}</h1>
|
||||
|
||||
{/* Legacy flat key — unchanged */}
|
||||
<button>{localize("com_ui_cancel")}</button>
|
||||
|
||||
{/* With interpolation */}
|
||||
<p>{localize("com_knowledge.files_count", { 0: fileCount })}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Toast Messages
|
||||
|
||||
```tsx
|
||||
showToast({
|
||||
message: localize("com_knowledge.space_create_success"),
|
||||
severity: NotificationSeverity.SUCCESS
|
||||
});
|
||||
```
|
||||
|
||||
## Interpolation Examples
|
||||
|
||||
| Pattern | Locale Value | Code |
|
||||
|---------|-------------|------|
|
||||
| Positional | `"已选择 {{0}} 个文件(共 {{1}} 个文件)"` | `localize("key", { 0: selected, 1: total })` |
|
||||
| Named | `"File: {{name}} exceeds {{size}}MB"` | `localize("key", { name, size })` |
|
||||
| Nested ref | `"$t(linsight)正在规划..."` | Automatically resolved by i18next |
|
||||
| Plural (count) | `"剩余任务次数: {{count}}次"` | `localize("key", { count: remaining })` |
|
||||
@@ -1,124 +0,0 @@
|
||||
# i18n Conventions for Platform Frontend (src/frontend/platform/)
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Library**: `i18next` (v23+) + `react-i18next` (v15+) + `i18next-http-backend` (v2+)
|
||||
- **Supported Languages**: `en-US` (English), `zh-Hans` (Simplified Chinese), `ja` (Japanese)
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/i18n.js` | i18next initialization (HTTP backend loader) |
|
||||
| `public/locales/en-US/{ns}.json` | English translations |
|
||||
| `public/locales/zh-Hans/{ns}.json` | Simplified Chinese translations |
|
||||
| `public/locales/ja/{ns}.json` | Japanese translations |
|
||||
|
||||
## Namespace Files
|
||||
|
||||
Platform uses **multiple namespace files** per language (loaded via HTTP backend at runtime):
|
||||
|
||||
| Namespace | File | Scope |
|
||||
|-----------|------|-------|
|
||||
| `bs` | `bs.json` | General UI, common labels, system messages |
|
||||
| `flow` | `flow.json` | Flow/workflow builder, nodes, edges |
|
||||
| `model` | `model.json` | LLM model management, fine-tuning |
|
||||
| `tool` | `tool.json` | Tool/plugin management |
|
||||
| `dashboard` | `dashboard.json` | Dashboard, charts, analytics |
|
||||
| `knowledge` | `knowledge.json` | Knowledge base management |
|
||||
|
||||
> When adding keys, choose the namespace that best matches the module the string belongs to. Default to `bs` for cross-cutting or ambiguous strings.
|
||||
|
||||
## Key Naming Convention
|
||||
|
||||
### Key Naming Rules
|
||||
|
||||
1. Use **dot-separated paths** for hierarchy: `knowledge.spaceCreateSuccess`.
|
||||
2. Use **camelCase** for leaf keys.
|
||||
3. Keep keys **descriptive but concise** (2-5 words).
|
||||
4. For similar operations, use consistent suffixes: `Success`, `Error`, `Failed`, `Confirm`, `Placeholder`, `Title`, `Desc`.
|
||||
|
||||
### Example Keys
|
||||
|
||||
```json
|
||||
// public/locales/zh-Hans/bs.json
|
||||
{
|
||||
"deleteConfirm": "确定要删除吗?",
|
||||
"saveSuccess": "保存成功",
|
||||
"cancel": "取消"
|
||||
}
|
||||
|
||||
// public/locales/zh-Hans/knowledge.json
|
||||
{
|
||||
"spaceCreateSuccess": "知识空间创建成功",
|
||||
"dropToUpload": "松手即可上传文件至此处",
|
||||
"folderMaxDepth": "文件夹层级已达上限(10层)"
|
||||
}
|
||||
```
|
||||
|
||||
## JSON File Format
|
||||
|
||||
- Each namespace is a **flat key-value** JSON object (no nesting).
|
||||
- Keys are sorted alphabetically.
|
||||
- Use `{{0}}`, `{{1}}` for positional interpolation, `{{name}}` for named interpolation.
|
||||
- Do NOT duplicate existing keys — search before adding.
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### Import Pattern
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from "react-i18next"
|
||||
```
|
||||
|
||||
### Component Usage
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Default namespace (bs) */}
|
||||
<button>{t('cancel')}</button>
|
||||
|
||||
{/* Specific namespace */}
|
||||
<h1>{t('knowledge:spaceCreateSuccess')}</h1>
|
||||
|
||||
{/* With interpolation */}
|
||||
<p>{t('knowledge:filesCount', { 0: fileCount })}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Toast Messages
|
||||
|
||||
```tsx
|
||||
import { toast } from "@/components/bs-ui/toast/use-toast"
|
||||
|
||||
toast({
|
||||
title: t('prompt'),
|
||||
variant: 'success',
|
||||
description: t('knowledge:spaceCreateSuccess')
|
||||
})
|
||||
```
|
||||
|
||||
### Specifying Namespace via useTranslation
|
||||
|
||||
```tsx
|
||||
// Load a specific namespace
|
||||
const { t } = useTranslation('knowledge')
|
||||
// Now t('spaceCreateSuccess') resolves from knowledge.json
|
||||
|
||||
// Load multiple namespaces
|
||||
const { t } = useTranslation(['bs', 'knowledge'])
|
||||
```
|
||||
|
||||
## Interpolation Examples
|
||||
|
||||
| Pattern | Locale Value | Code |
|
||||
|---------|-------------|------|
|
||||
| Positional | `"已选择 {{0}} 个文件(共 {{1}} 个文件)"` | `t('key', { 0: selected, 1: total })` |
|
||||
| Named | `"文件: {{name}} 超过 {{size}}MB"` | `t('key', { name, size })` |
|
||||
| Count | `"剩余任务次数:{{count}}次"` | `t('key', { count: remaining })` |
|
||||
@@ -1,78 +0,0 @@
|
||||
# i18n Localization Workflow
|
||||
|
||||
## Step 1 — Scan the Module
|
||||
|
||||
1. Read all `.tsx` and `.ts` files in the target module directory.
|
||||
2. Identify every hardcoded user-facing string (Chinese text, toast messages, placeholders, button labels, titles, tooltips, error messages, etc.).
|
||||
3. Ignore: code comments, CSS class names, variable names, enum values, strings already wrapped in `t()` / `localize()` / `i18n.t()`, and dev-only content (`console.log`).
|
||||
|
||||
## Step 2 — Generate Translation Keys
|
||||
|
||||
For each extracted string, determine which domain namespace it belongs to (e.g. `com_knowledge`, `com_ui`, `com_sop`), then generate a concise snake_case key name.
|
||||
|
||||
Example: `"知识空间创建成功"` → namespace `com_knowledge`, key `space_create_success` → used as `com_knowledge.space_create_success`
|
||||
|
||||
Refer to `CONVENTIONS.md` and `SAMPLE_KEYS.json` for naming details.
|
||||
|
||||
## Step 3 — Update Locale Files
|
||||
|
||||
> **CRITICAL**: Legacy flat keys (like `"com_ui_cancel"`) MUST be left untouched. Only ADD new keys using the nested namespace format.
|
||||
|
||||
Add new keys to **all three** translation files using nested structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"com_ui_cancel": "Cancel",
|
||||
|
||||
"com_knowledge": {
|
||||
"space_create_success": "Knowledge space created",
|
||||
"drop_to_upload": "Drop files here to upload"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| File | Value |
|
||||
|------|-------|
|
||||
| `src/locales/zh-Hans/translation.json` | Original Chinese string |
|
||||
| `src/locales/en/translation.json` | Professional English translation |
|
||||
| `src/locales/ja/translation.json` | Professional Japanese translation |
|
||||
|
||||
Rules:
|
||||
- Do NOT modify or restructure existing flat keys.
|
||||
- New keys go inside their namespace object, sorted alphabetically.
|
||||
- If the namespace object already exists, append to it. If not, create it.
|
||||
- Namespace objects are placed after all legacy flat keys, sorted alphabetically.
|
||||
- Use `{{0}}` for positional interpolation, `{{name}}` for named interpolation.
|
||||
- Do NOT duplicate existing keys — search before adding.
|
||||
|
||||
## Step 4 — Update Component Code
|
||||
|
||||
1. Import (if not present): `import { useLocalize } from "~/hooks";`
|
||||
2. Initialize (if not present): `const localize = useLocalize();`
|
||||
3. Replace hardcoded strings using **dot notation** for new nested keys:
|
||||
```tsx
|
||||
// Before
|
||||
showToast({ message: "知识空间创建成功" });
|
||||
// After
|
||||
showToast({ message: localize("com_knowledge.space_create_success") });
|
||||
|
||||
// Before (with dynamic values)
|
||||
message: `已开始处理 ${files.length} 个文件`
|
||||
// After
|
||||
message: localize("com_knowledge.files_processing_started", { 0: files.length })
|
||||
|
||||
// Before (JSX)
|
||||
<p>松手即可上传文件至此处</p>
|
||||
// After
|
||||
<p>{localize("com_knowledge.drop_to_upload")}</p>
|
||||
```
|
||||
|
||||
## Step 5 — Verify
|
||||
|
||||
1. No hardcoded Chinese remains in modified files (excluding code comments).
|
||||
2. Every new key exists in all three locale JSON files.
|
||||
3. No existing flat keys were modified or restructured.
|
||||
|
||||
## Output
|
||||
|
||||
After completing, provide a summary: number of strings extracted, list of new keys, and files modified.
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: react-component-refactor
|
||||
description: Refactor large React components by extracting hooks, splitting sub-components, and organizing directory structure following established patterns.
|
||||
---
|
||||
|
||||
# React Component Refactor
|
||||
|
||||
This skill provides a systematic approach for refactoring complex React components. Use it when a module has overgrown files, tangled state, or unclear separation of concerns.
|
||||
|
||||
## Instructions
|
||||
1. **Read the Guidelines**: Read `resources/GUIDELINES.md` for the complete refactoring checklist and rules.
|
||||
2. **Read the Examples**: Read `resources/EXAMPLES.md` for concrete before/after patterns from real refactoring work.
|
||||
3. **Execute**: Follow the guidelines to refactor the target module.
|
||||
@@ -1,185 +0,0 @@
|
||||
# React Component Refactoring — Real Examples
|
||||
|
||||
These examples are drawn from the `Subscription` module refactoring and demonstrate each pattern in context.
|
||||
|
||||
---
|
||||
|
||||
## Example 1: Extract Sub-Component
|
||||
|
||||
### Before (in `CreateChannelDrawer.tsx`, ~120 lines inline)
|
||||
```tsx
|
||||
// Inline sub-component buried inside the main component
|
||||
function CreateChannelDrawer({ open, onOpenChange, ... }) {
|
||||
// ... 18 useState calls ...
|
||||
|
||||
// Inline sub-component — hard to find, test, or reuse
|
||||
function SubChannelBlock({ data, onNameChange, ... }) {
|
||||
// 120 lines of JSX + local state
|
||||
}
|
||||
|
||||
return ( /* uses SubChannelBlock inline */ );
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
CreateChannel/
|
||||
├── CreateChannelDrawer.tsx # imports SubChannelBlock
|
||||
└── SubChannelBlock.tsx # standalone, with exported Props interface
|
||||
```
|
||||
|
||||
```tsx
|
||||
// SubChannelBlock.tsx
|
||||
export interface SubChannelData { id: string; name: string; ... }
|
||||
|
||||
interface SubChannelBlockProps {
|
||||
data: SubChannelData;
|
||||
onNameChange: (name: string) => void;
|
||||
onRemove: () => void;
|
||||
// ...
|
||||
}
|
||||
|
||||
export function SubChannelBlock({ data, onNameChange, ... }: SubChannelBlockProps) {
|
||||
// self-contained component
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Extract Form State Hook
|
||||
|
||||
### Before (`CreateChannelDrawer.tsx` — 18 useState + handlers)
|
||||
```tsx
|
||||
function CreateChannelDrawer(...) {
|
||||
const [channelName, setChannelName] = useState("");
|
||||
const [channelDesc, setChannelDesc] = useState("");
|
||||
const [visibility, setVisibility] = useState("private");
|
||||
const [sources, setSources] = useState([]);
|
||||
// ... 14 more useState calls ...
|
||||
|
||||
const resetForm = () => { /* reset all 18 states */ };
|
||||
const handleAddSubChannel = () => { /* manipulate subChannels state */ };
|
||||
// ... more handlers ...
|
||||
|
||||
return ( /* 400+ lines of JSX using all these states */ );
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
hooks/
|
||||
└── useCreateChannelForm.ts # all 18 states + handlers
|
||||
CreateChannel/
|
||||
└── CreateChannelDrawer.tsx # clean UI component
|
||||
```
|
||||
|
||||
```tsx
|
||||
// hooks/useCreateChannelForm.ts
|
||||
export function useCreateChannelForm() {
|
||||
const [channelName, setChannelName] = useState("");
|
||||
// ... all states ...
|
||||
const resetForm = () => { /* ... */ };
|
||||
const handleAddSubChannel = () => { /* ... */ };
|
||||
|
||||
return { channelName, setChannelName, ..., resetForm, handleAddSubChannel };
|
||||
}
|
||||
|
||||
// CreateChannelDrawer.tsx — now a presentational component
|
||||
function CreateChannelDrawer(...) {
|
||||
const form = useCreateChannelForm();
|
||||
return (
|
||||
<Input value={form.channelName} onChange={e => form.setChannelName(e.target.value)} />
|
||||
// ... form.visibility, form.handleAddSubChannel, etc.
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Extract Data Manager Hook
|
||||
|
||||
### Before (`AddSourceDropdown.tsx` — 497 lines with data loading + UI)
|
||||
```tsx
|
||||
function AddSourceDropdown({ sources, onSourcesChange, expanded, ... }) {
|
||||
const [wechatSources, setWechatSources] = useState([]);
|
||||
const [websiteSources, setWebsiteSources] = useState([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
|
||||
// Data loading effect
|
||||
useEffect(() => {
|
||||
if (!expanded) return;
|
||||
const load = async () => { /* API call + state mapping */ };
|
||||
load(currentType);
|
||||
}, [expanded, activeTab]);
|
||||
|
||||
// WeChat auto-detection effect
|
||||
useEffect(() => { /* 50 lines of async logic */ }, [expanded, viewMode]);
|
||||
|
||||
// Filtering logic
|
||||
const filteredSources = useMemo(() => { /* ... */ }, [...]);
|
||||
|
||||
return ( /* 200+ lines of UI */ );
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
hooks/
|
||||
└── useSourceManager.ts # API calls, filtering, toggle logic
|
||||
CreateChannel/
|
||||
└── AddSourceDropdown.tsx # pure UI (328 lines, down from 497)
|
||||
```
|
||||
|
||||
```tsx
|
||||
// AddSourceDropdown.tsx — clean separation
|
||||
function AddSourceDropdown({ sources, onSourcesChange, expanded, ... }) {
|
||||
const mgr = useSourceManager(sources, onSourcesChange, expanded, onExpandChange);
|
||||
|
||||
return (
|
||||
<Input value={mgr.searchKeyword} onChange={e => mgr.setSearchKeyword(e.target.value)} />
|
||||
// ... mgr.filteredSources, mgr.toggleSource, mgr.handleConfirm, etc.
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Extract Validation to Utility
|
||||
|
||||
### Before (inline in submit handler — 45 lines of validation)
|
||||
```tsx
|
||||
onClick={async () => {
|
||||
if (form.sources.length < 1) { showToast({ message: "..." }); return; }
|
||||
if (!form.channelName.trim()) { showToast({ message: "..." }); return; }
|
||||
if (form.contentFilter) {
|
||||
const err = validateFilterGroups(form.filterGroups);
|
||||
if (err) { showToast({ message: err }); return; }
|
||||
}
|
||||
if (form.createSubChannel) {
|
||||
for (const sub of form.subChannels) { /* more checks */ }
|
||||
}
|
||||
// ... then build data and submit
|
||||
}}
|
||||
```
|
||||
|
||||
### After
|
||||
```tsx
|
||||
// channelUtils.ts — pure validation function
|
||||
export function validateCreateChannelForm(
|
||||
data: CreateChannelFormData,
|
||||
localize: (key: string) => string
|
||||
): string | null {
|
||||
if (data.sources.length < 1) return localize("need_one_source") || "至少需添加 1 个信息源";
|
||||
if (!data.channelName.trim()) return localize("cannot_empty_channel_name");
|
||||
// ... all checks ...
|
||||
return null;
|
||||
}
|
||||
|
||||
// CreateChannelDrawer.tsx — clean submit handler
|
||||
onClick={async () => {
|
||||
const data = { /* assemble form data */ };
|
||||
const error = validateCreateChannelForm(data, localize);
|
||||
if (error) { showToast({ message: error, severity: "warning" }); return; }
|
||||
// submit
|
||||
}}
|
||||
```
|
||||
@@ -1,159 +0,0 @@
|
||||
# React Component Refactoring Guidelines
|
||||
|
||||
This document defines the standard refactoring methodology for this project. Follow these rules when adding new features or refactoring existing modules to keep code maintainable and consistent.
|
||||
|
||||
---
|
||||
|
||||
## 1. Directory Structure Rules
|
||||
|
||||
### When to create a sub-directory
|
||||
- When a feature area has **3+ closely related component files**, group them into a named sub-directory.
|
||||
- The directory name should describe the **feature**, not the component (e.g., `CreateChannel/`, not `CreateChannelDrawerFiles/`).
|
||||
|
||||
### Standard layout
|
||||
|
||||
```
|
||||
src/pages/ModuleName/
|
||||
├── index.tsx # Page entry, layout & routing
|
||||
├── moduleUtils.ts # Pure utility functions (validation, data transform, payload builders)
|
||||
├── hooks/ # Custom hooks (one hook per file)
|
||||
│ ├── useFeatureForm.ts # Form state & handlers
|
||||
│ └── useDataManager.ts # Data fetching, filtering, CRUD
|
||||
├── FeatureA/ # Feature sub-directory
|
||||
│ ├── MainComponent.tsx # Top-level feature component
|
||||
│ ├── SubComponentA.tsx # Extracted sub-component
|
||||
│ └── SubComponentB.tsx # Another extracted sub-component
|
||||
└── FeatureB/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Import path conventions
|
||||
- Components within the same feature directory use relative imports: `./SubComponent`
|
||||
- Hooks are imported from `../hooks/useXxx`
|
||||
- Utils are imported from `../moduleUtils`
|
||||
|
||||
---
|
||||
|
||||
## 2. Component Splitting Rules
|
||||
|
||||
### When to extract a sub-component
|
||||
- An inline function component is **>120 lines**.
|
||||
- A block of JSX is **self-contained** (has its own props/state concept).
|
||||
- A component is **reused** or could be tested independently.
|
||||
|
||||
### How to extract
|
||||
1. Create a new file in the same feature directory.
|
||||
2. Define a clear `Props` interface and export it.
|
||||
3. Move the component body; keep UI unchanged.
|
||||
4. Import and use in the parent — the parent JSX should only change the component reference.
|
||||
|
||||
### Naming conventions
|
||||
- Sub-component file name = component name (PascalCase): `SubChannelBlock.tsx`
|
||||
- Always `export function ComponentName` (named exports, no default).
|
||||
- Co-export related types/interfaces that are tightly coupled.
|
||||
|
||||
---
|
||||
|
||||
## 3. Hook Extraction Rules
|
||||
|
||||
### When to extract a hook
|
||||
- A component has **≥8 `useState` calls**.
|
||||
- There is a block of **`useEffect` + state** that handles data loading or side effects.
|
||||
- Multiple event handlers share the same state and form a logical unit.
|
||||
|
||||
### Naming conventions
|
||||
- File: `hooks/useFeatureName.ts` (camelCase with `use` prefix)
|
||||
- Hook function: `useFeatureName`
|
||||
- Return a flat object: `{ stateA, setStateA, handlerB, ... }`
|
||||
- The consuming component accesses via `const form = useFeatureName(...)` and references `form.stateA`
|
||||
|
||||
### What belongs in a hook
|
||||
| Belongs in Hook | Stays in Component |
|
||||
|---|---|
|
||||
| `useState` declarations | JSX rendering |
|
||||
| Derived/computed values (`useMemo`) | Layout-specific handlers (e.g., scroll position) |
|
||||
| Data loading `useEffect`s | Event handlers that only call `showToast` |
|
||||
| CRUD handlers (add/remove/update) | Direct UI event wiring |
|
||||
| Form reset logic | |
|
||||
|
||||
### What does NOT belong in a hook
|
||||
- UI library calls (`showToast`, `localize`) — pass as params if needed
|
||||
- API layer definitions — keep in `~/api/`
|
||||
- Component-specific render helpers
|
||||
|
||||
---
|
||||
|
||||
## 4. Utility / Validation Extraction Rules
|
||||
|
||||
### When to extract to `moduleUtils.ts`
|
||||
- **Validation functions** that check form data and return error messages.
|
||||
- **Payload builders** that transform form data into API payloads.
|
||||
- **Data transformers** that convert between API types and UI types.
|
||||
- **Pure functions** that don't depend on React state or hooks.
|
||||
|
||||
### Function signature pattern
|
||||
```typescript
|
||||
// Validation: returns error message or null
|
||||
export function validateFormData(
|
||||
data: FormDataType,
|
||||
localize: (key: string) => string
|
||||
): string | null;
|
||||
|
||||
// Payload builder: transforms form → API payload
|
||||
export function buildPayload(data: FormDataType): ApiPayloadType;
|
||||
```
|
||||
|
||||
### Rules
|
||||
- Keep functions pure — no side effects.
|
||||
- Accept `localize` as a parameter for i18n error messages.
|
||||
- The component is responsible for displaying errors (toast/UI).
|
||||
|
||||
---
|
||||
|
||||
## 5. Refactoring Checklist
|
||||
|
||||
When refactoring a module, follow this order:
|
||||
|
||||
1. **[ ] Analyze** — Count lines, identify state density, find inline sub-components.
|
||||
2. **[ ] Restructure directories** — Group files by feature if threshold met.
|
||||
3. **[ ] Extract sub-components** — Move inline components to separate files.
|
||||
4. **[ ] Extract hooks** — Pull state management into `hooks/useXxx.ts`.
|
||||
5. **[ ] Extract utilities** — Move validation and data transforms to `moduleUtils.ts`.
|
||||
6. **[ ] Clean imports** — Remove unused imports, verify all paths resolve.
|
||||
7. **[ ] Verify** — Run `yarn start` to ensure compilation succeeds.
|
||||
|
||||
### DO NOT change during refactoring
|
||||
- **UI/JSX structure** — no visual changes.
|
||||
- **CSS classes** — keep exact same styling.
|
||||
- **API layer** — do not restructure API files unless explicitly requested.
|
||||
- **i18n hardcoded strings** — handle separately with the `i18n-localizer` skill.
|
||||
|
||||
---
|
||||
|
||||
## 6. File Size Guidelines
|
||||
|
||||
| File Type | Target Lines | Action if exceeded |
|
||||
|---|---|---|
|
||||
| Page component (`index.tsx`) | < 600 | Extract sub-sections |
|
||||
| Feature component | < 600 | Extract hooks & sub-components |
|
||||
| Custom hook | < 200 | Split by concern |
|
||||
| Utility file | < 300 | Split by domain |
|
||||
| Sub-component | < 150 | Already well-scoped |
|
||||
|
||||
---
|
||||
|
||||
## 7. Data Flow Conventions
|
||||
|
||||
```
|
||||
API Layer (~/api/)
|
||||
↕ raw types
|
||||
Hooks (hooks/useXxx.ts)
|
||||
↕ processed state + handlers
|
||||
Component (Feature/Main.tsx)
|
||||
↕ props
|
||||
Sub-components (Feature/Sub.tsx)
|
||||
```
|
||||
|
||||
- **Unidirectional**: Parent → Child via props; Child → Parent via callback props.
|
||||
- **No prop drilling beyond 3 levels** — if deeper, use a hook or context.
|
||||
- **Hooks own the state**, components own the rendering.
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
name: sdd-review
|
||||
description: 对 BiSheng 项目的 SDD 文档执行审查。
|
||||
- spec:写完 spec.md 后调用,同时检查 PRD gap 和架构合规性,生成报告供用户参考
|
||||
- tasks:写完 tasks.md 后自动调用,检查 AC 追溯、任务拆解质量和技术债预防
|
||||
用法:/sdd-review <feature_dir> <doc_type>,doc_type 为 spec / tasks。
|
||||
TRIGGER when: 用户完成了 SDD 的 spec.md / tasks.md 编写,或者用户使用 /sdd-review 命令,或者 Claude 完成了这些文件的编写后需要审查。
|
||||
---
|
||||
|
||||
# SDD Review Skill
|
||||
|
||||
## 调用方式
|
||||
|
||||
```
|
||||
/sdd-review <feature_dir> <doc_type>
|
||||
```
|
||||
|
||||
例:
|
||||
```
|
||||
/sdd-review features/v2.5.0/004-rebac-core spec
|
||||
/sdd-review features/v2.5.0/001-multi-tenant tasks
|
||||
```
|
||||
|
||||
## 审查流程
|
||||
|
||||
### 第一步:解析参数
|
||||
|
||||
从用户输入或调用上下文中提取:
|
||||
- `feature_dir`:特性目录路径(如 `features/v2.5.0/004-rebac-core`)
|
||||
- `doc_type`:文档类型,必须是 `spec` 或 `tasks`
|
||||
|
||||
若参数缺失或无效,向用户报告错误后停止。
|
||||
|
||||
---
|
||||
|
||||
### spec 模式(辅助审查,不自动推进)
|
||||
|
||||
spec.md 合并了需求规范和技术设计,因此 spec 审查同时覆盖需求覆盖和架构合规检查。
|
||||
|
||||
**第二步(spec):执行合并审查**
|
||||
|
||||
读取文件:
|
||||
- `<feature_dir>/spec.md`(已写的规格文档)
|
||||
- spec.md 中"关联 PRD"字段指向的文件(若未标注,读取 `docs/PRD/` 下与特性名最相关的文件)
|
||||
- `features/v2.5.0/release-contract.md`(不变量约束,确认 spec 未越界)
|
||||
- `docs/architecture/02-backend-modules.md`(后端模块架构)
|
||||
- `docs/architecture/10-permission-rbac.md`(权限体系)
|
||||
|
||||
按 `references/spec-checklist.md` 中的检查清单执行 14 项检查。
|
||||
|
||||
**第三步(spec):展示报告,等待用户确认**
|
||||
|
||||
向用户展示分析结果:
|
||||
- 无 gap / 无问题:告知"审查通过,可继续确认"
|
||||
- 有 gap / 有问题:展示每个问题(MISSING / FORMAT / CONFLICT / ISSUE),供用户决定是否修改
|
||||
|
||||
**等待用户确认**(唯一手动暂停点)。用户确认后,将 `<feature_dir>/tasks.md` 状态表中 spec.md 行更新为 `✅ 已评审`。
|
||||
|
||||
---
|
||||
|
||||
### tasks 模式(自动审查)
|
||||
|
||||
**第二步(tasks):执行审查**
|
||||
|
||||
读取文件:
|
||||
- `<feature_dir>/tasks.md`
|
||||
- `<feature_dir>/spec.md`(验收标准 + 技术方案)
|
||||
- `features/v2.5.0/release-contract.md`(领域归属 + 不变量)
|
||||
|
||||
按 `references/tasks-checklist.md` 中的检查清单执行 21 项检查。
|
||||
|
||||
**第三步(tasks):处理审查结果**
|
||||
|
||||
**输出格式**:
|
||||
- 有问题:`ISSUE: <描述> | SEVERITY: high/medium/low | TASK: <T-NN 若适用>`
|
||||
- 无问题:`LGTM`
|
||||
|
||||
**处理逻辑**:
|
||||
- `LGTM` → 更新 `<feature_dir>/tasks.md` 状态表,将 `tasks.md` 行改为 `✅ 已拆解`
|
||||
- 有 `high`/`medium` ISSUE → 修复后重新审查(最多 2 轮)
|
||||
- `low` ISSUE → 记录但跳过
|
||||
- 2 轮后仍有 `high`/`medium` → 停止,向用户报告剩余问题
|
||||
|
||||
## 错误处理
|
||||
|
||||
- feature_dir 不存在 → 报告路径错误,停止
|
||||
- doc_type 不是 spec / tasks → 报告参数错误,停止
|
||||
- spec.md 不存在 → 报告"找不到 spec.md,请先完成 spec",停止
|
||||
- tasks.md 不存在(tasks 模式)→ 报告"找不到 tasks.md,请先完成 tasks",停止
|
||||
@@ -1,48 +0,0 @@
|
||||
你是 BiSheng 项目的需求分析师兼架构评审员。请对比 PRD 和已写的 spec.md,同时检查需求覆盖和架构合规性。
|
||||
|
||||
spec.md 合并了需求规范和技术设计(用户故事 + AC + 架构决策 + API 契约 + 数据模型)。
|
||||
|
||||
请自行读取以下文件:
|
||||
- {feature_dir}/spec.md(已写的规格文档)
|
||||
- {prd_path}(从 spec.md"关联 PRD"字段获取路径,若未标注则读取 docs/PRD/ 下与特性名最相关的文件)
|
||||
- features/v2.5.0/release-contract.md(不变量约束 + 领域归属,确认 spec 未越界)
|
||||
- docs/architecture/02-backend-modules.md(后端模块架构)
|
||||
- docs/architecture/10-permission-rbac.md(权限体系规范)
|
||||
|
||||
**需求分析维度**:
|
||||
1. PRD 中描述的功能点 / 用户场景,spec 是否有对应 AC?
|
||||
2. PRD 中提到的边界条件、错误场景,spec 是否有覆盖?
|
||||
3. PRD 中提到的 UI 交互细节,spec 是否有对应业务 AC?
|
||||
4. spec 的 AC 表格格式是否正确:`| ID | 角色 | 操作 | 预期结果 |`,ID 格式 AC-NN?
|
||||
5. 是否有 AC 不可测试(过于模糊)?
|
||||
6. 是否与 release-contract.md 的 INV 不变量冲突?
|
||||
|
||||
**架构合规维度**:
|
||||
7. spec.md 中每条 AC 是否都有技术覆盖(API 端点 / 数据库模型 / 前端组件)?
|
||||
8. 是否越界进入 release-contract.md 表 1 中归属其他 Feature 的领域?
|
||||
9. API 契约是否完整:端点表、请求/响应示例(UnifiedResponseModel 包装)、错误码表格(含 MMMEE 编码 + 关联 AC)?
|
||||
10. 架构决策是否符合项目规范:
|
||||
- 分层严格自顶向下:Endpoint → Service → DAO,禁止反向导入
|
||||
- API 响应用 `UnifiedResponseModel[T]`(`resp_200(data)` / `resp_500(code, msg)`)
|
||||
- 分页用 `PageData[T]`(推荐)或 `PageList[T]`(旧接口兼容)
|
||||
- 错误码 5 位 MMMEE 编码,类名 `{Module}{Error}Error`,继承 `BaseErrorCode`
|
||||
- 认证注入 `UserPayload = Depends(UserPayload.get_login_user)`
|
||||
- 权限检查 `PermissionService.check()`,禁止直接查 role_access/group_resource
|
||||
11. 设计部分是否只写 Why+What(不写 How,不写测试策略)?
|
||||
|
||||
**BiSheng 特有检查**:
|
||||
12. 新 ORM 模型是否包含 `tenant_id` 字段?(INV-1 要求所有业务表含 tenant_id)
|
||||
13. 错误码是否遵循 MMMEE 且不与 release-contract「已分配模块编码」冲突?
|
||||
14. 权限相关 AC 是否使用 PermissionService 而非旧 role_access/group_resource?(INV-3)
|
||||
|
||||
返回格式(必须严格遵守):
|
||||
|
||||
有 gap / 问题时,每个问题单独一行:
|
||||
- MISSING: <PRD 中的功能/场景,spec 未覆盖> | SEVERITY: high/medium/low | PRD_REF: <PRD 原文片段或章节>
|
||||
- FORMAT: <格式问题描述> | SEVERITY: high/medium/low
|
||||
- CONFLICT: <与 INV 冲突描述> | SEVERITY: high | INV: <INV-N>
|
||||
- ISSUE: <架构/设计问题描述> | SEVERITY: high/medium/low | AC: <AC-NN 若适用>
|
||||
|
||||
无 gap 且无问题时,只返回一行:LGTM
|
||||
|
||||
注意:本报告供参考,是否修改由用户决定。不要建议修改 spec,只列出观察到的 gap 和问题。
|
||||
@@ -1,55 +0,0 @@
|
||||
你是 BiSheng 项目的任务计划评审员。请审查 {feature_dir}/tasks.md。
|
||||
|
||||
请自行读取以下文件:
|
||||
- {feature_dir}/spec.md(验收标准 + 技术方案)
|
||||
- features/v2.5.0/release-contract.md(领域归属 + 不变量)
|
||||
|
||||
任务规范要求:
|
||||
- Test-First:后端测试任务必须先于其配对的实现任务
|
||||
- 每个测试任务必须有"覆盖 AC: AC-NN, AC-NN"标注
|
||||
- 基础设施任务(ORM 模型、错误码、配置)无测试配对,排在最前面
|
||||
- 每个任务应在一次 AI 会话内可完成(目标约 30 分钟,最多 1-2 个文件)
|
||||
- 依赖关系:依赖的任务 ID 必须存在且顺序合理
|
||||
- 每个任务必须自包含:内联文件路径、逻辑、测试上下文(实现阶段不需要回读 spec.md)
|
||||
- 任务分 6 类:基础设施 / 后端 Domain / 后端 API / 前端 Platform / 前端 Client / Worker
|
||||
- 前端任务必须区分 Platform(src/frontend/platform/)和 Client(src/frontend/client/)
|
||||
- Worker 任务须说明 tenant_id 传递方式(Celery headers → ContextVar)
|
||||
- 「测试降级」标注仅在测试成本极高时允许,必须说明理由
|
||||
|
||||
审查清单(4 组 17 条 + BiSheng 特有 4 条):
|
||||
|
||||
**A. 形式合规**
|
||||
1. **AC 追溯完整性** — spec.md 中每条 AC 是否都有至少一个测试任务覆盖(带"覆盖 AC:"标注)?
|
||||
2. **AC 标注完整性** — 是否存在缺少"覆盖 AC:"标注的测试任务?
|
||||
3. **Test-First 顺序** — 后端测试任务是否先于其配对的实现任务?
|
||||
4. **依赖关系正确性** — 被依赖的任务 ID 是否存在、顺序是否合理?
|
||||
5. **原子化** — 每个任务范围是否 ≤ 2 个文件,能在一次会话内完成?
|
||||
6. **自包含** — 每个任务是否内联了文件路径、逻辑描述、测试上下文?
|
||||
|
||||
**B. 任务拆解质量**
|
||||
7. **粒度合理性** — 单个任务不超过 3 个文件、不跨前后端?
|
||||
8. **顺序高效性** — 不存在任务 A 的输出被后续任务覆盖/重写的返工情况?
|
||||
9. **无重复工作** — 不存在多个任务对同一文件同一部分做非增量的重复修改?
|
||||
10. **spec 覆盖完整性** — spec.md 中定义的每个 API 端点、ORM 模型、Service 方法、前端组件都有对应实现任务?
|
||||
11. **任务间接口清晰** — 任务描述中明确前驱任务的产出(DAO 方法签名、Service 接口、API 端点路径)?
|
||||
12. **无过度工程** — 不存在 spec.md 中未提及但 tasks.md 中新增的实现内容?
|
||||
|
||||
**C. AC 标注规范**
|
||||
13. **AC 标注格式** — 必须逐条列举 `AC-01, AC-02`,禁止 `AC-01~AC-05` 范围写法?
|
||||
14. **测试任务纯净性** — 标注了"覆盖 AC"的测试任务不得混入实现逻辑?
|
||||
|
||||
**D. 技术债预防**
|
||||
15. **无延迟 TODO** — 任务描述中不得有 TODO/FIXME/HACK 将本 Feature 范围内问题推迟?
|
||||
16. **数据库回滚** — 数据库模型变更任务需包含回滚方案或说明不可逆原因?
|
||||
17. **跨 Feature 副作用** — 修改其他 Feature 领域对象的写入行为需检查 release-contract.md;修改共享文件需说明影响范围?
|
||||
|
||||
**E. BiSheng 特有**
|
||||
18. **前端分区** — 前端任务是否区分 Platform / Client 两个分区,不混在一起?
|
||||
19. **Worker tenant_id** — Worker/Celery 任务是否说明 tenant_id 传递方式(headers → ContextVar)?
|
||||
20. **基础设施优先** — 基础设施任务(ORM/错误码/conftest)是否排在所有业务任务之前?
|
||||
21. **测试降级理由** — 标注「测试降级」的任务是否说明了充分理由(如需要 Milvus/ES mock)?
|
||||
|
||||
返回格式(必须严格遵守):
|
||||
有问题时,每个问题单独一行:
|
||||
- ISSUE: <描述> | SEVERITY: high/medium/low | TASK: <T-NN 若适用>
|
||||
无问题时,只返回一行:LGTM
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
name: task-review
|
||||
description: L1 任务级代码审查。在每个任务完成后执行轻量级约定合规检查,
|
||||
确保架构红线和编码约定在任务级别被守住,不让违规累积到特性级审查(L2)才发现。
|
||||
用法:/task-review <feature_dir> <task_id>
|
||||
TRIGGER when: 用户完成了一个 SDD 任务(实现或测试),或者用户使用 /task-review 命令。
|
||||
---
|
||||
|
||||
# Task Review Skill(L1 任务级审查)
|
||||
|
||||
## 调用方式
|
||||
|
||||
```
|
||||
/task-review <feature_dir> <task_id>
|
||||
```
|
||||
|
||||
例:
|
||||
```
|
||||
/task-review features/v2.5.0/004-rebac-core T003
|
||||
/task-review features/v2.5.0/007-resource-permission-ui T007
|
||||
```
|
||||
|
||||
## 审查流程
|
||||
|
||||
### Step 1: 解析参数 + 收集变更范围
|
||||
|
||||
1. 验证参数:
|
||||
- `feature_dir` 必须存在且包含 `tasks.md`
|
||||
- `task_id` 必须匹配 tasks.md 中的某个任务(格式:`T001`、`T003` 等)
|
||||
- 若参数缺失或无效,报告错误后停止
|
||||
|
||||
2. 从 `<feature_dir>/tasks.md` 中读取指定任务的元数据:
|
||||
- 任务类型(测试 / 实现 / 基础设施 / Worker)
|
||||
- 目标文件列表
|
||||
- 前置依赖
|
||||
- 配对任务(测试↔实现)
|
||||
- 覆盖 AC 标注(测试任务)
|
||||
|
||||
3. 读取任务声明的所有目标文件内容(直接读取文件,不依赖 git diff)
|
||||
|
||||
### Step 2: 判断任务类型,选择检查子集
|
||||
|
||||
根据任务类型确定适用的检查项(参见 `references/task-checklist.md`):
|
||||
|
||||
| 任务类型 | 适用检查项 | 额外检查 |
|
||||
|---------|-----------|---------|
|
||||
| **测试任务** | #2 命名 + #5 前端约定 | AC 标注格式(`覆盖 AC: AC-NN`) |
|
||||
| **实现任务** | 完整 #1~#6 | 配对测试任务已完成(tasks.md 中已打勾) |
|
||||
| **基础设施任务** | #1 架构分层 + #4 数据库约定 + #6 信息泄漏 | 无 |
|
||||
| **Worker 任务** | #1 架构 + #4 数据库 + #6 信息泄漏 | tenant_id 通过 Celery headers 传递 |
|
||||
|
||||
任务类型判断规则:
|
||||
- 文件路径包含 `test/` 或 `__tests__/` → 测试任务
|
||||
- 文件路径包含 `domain/models/` 或 `common/errcode/` 或任务描述含"ORM""迁移""错误码""配置" → 基础设施任务
|
||||
- 文件路径包含 `worker/` 或任务描述含"Celery""异步任务" → Worker 任务
|
||||
- 其他 → 实现任务
|
||||
- 若任务同时包含测试和实现文件,按实现任务处理
|
||||
|
||||
### Step 3: 按检查清单执行检查
|
||||
|
||||
逐项执行 `references/task-checklist.md` 中适用的检查项。
|
||||
|
||||
### Step 4: 元数据交叉验证
|
||||
|
||||
- **文件范围**:任务声明的目标文件是否实际存在,是否存在范围蔓延(修改了任务未声明的文件)
|
||||
- **配对测试**:若为实现任务,检查 tasks.md 中配对的测试任务是否已打勾 ✅
|
||||
- **前置依赖**:检查任务声明的依赖项是否已完成(tasks.md 中已打勾)
|
||||
|
||||
### Step 5: 输出报告
|
||||
|
||||
按以下格式输出:
|
||||
|
||||
```markdown
|
||||
## Task Review: <task_id>
|
||||
|
||||
**任务**: <任务标题>
|
||||
**类型**: 测试 / 实现 / 基础设施 / Worker
|
||||
**文件**: <文件列表>
|
||||
|
||||
| # | 检查项 | 结果 | 说明 |
|
||||
|---|--------|------|------|
|
||||
| 1 | 架构分层 | PASS / FAIL / N/A | <若 FAIL,具体描述> |
|
||||
| 2 | 命名规范 | PASS / FAIL / N/A | |
|
||||
| 3 | 序列化约定 | PASS / FAIL / N/A | |
|
||||
| 4 | 数据库约定 | PASS / FAIL / N/A | |
|
||||
| 5 | 前端约定 | PASS / FAIL / N/A | |
|
||||
| 6 | 信息泄漏 | PASS / FAIL / N/A | |
|
||||
|
||||
**元数据验证**: 文件范围 PASS/FAIL | 配对测试 PASS/FAIL/N/A | 依赖 PASS/FAIL
|
||||
|
||||
**结果**: PASS / PASS_WITH_NOTES / NEEDS_FIX
|
||||
```
|
||||
|
||||
### Step 6: 处理结果
|
||||
|
||||
| 结果 | 条件 | 动作 |
|
||||
|------|------|------|
|
||||
| **PASS** | 全部通过 | 告知用户可以打勾 |
|
||||
| **PASS_WITH_NOTES** | 仅 MEDIUM 级提醒,无 HIGH | 告知用户可以打勾,列出提醒供参考 |
|
||||
| **NEEDS_FIX** | 任何 HIGH 违规 | 列出需要修复的具体问题,修复后可再次调用 `/task-review` 重审(最多 1 轮重审) |
|
||||
|
||||
## 错误处理
|
||||
|
||||
- `feature_dir` 不存在 → 报告路径错误,停止
|
||||
- `tasks.md` 不存在 → 报告"找不到 tasks.md",停止
|
||||
- `task_id` 不匹配 → 报告"未找到任务 <task_id>",停止
|
||||
- 目标文件不存在 → 标记为 WARNING(文件可能尚未创建),继续检查其他文件
|
||||
@@ -1,41 +0,0 @@
|
||||
# L1 任务审查检查清单
|
||||
|
||||
本清单定义了 `/task-review` 在每个任务完成后执行的精简检查项。
|
||||
L1 聚焦约定合规和架构红线,不检查边界条件、权限、并发、测试覆盖(留给 L2)。
|
||||
|
||||
## 检查项
|
||||
|
||||
| # | 检查项 | 适用文件 | 严重度 | 检查方法 |
|
||||
|---|--------|---------|--------|---------|
|
||||
| 1 | 架构分层 | 后端 `*.py` | HIGH | Endpoint 不直接实例化 DAO 做复杂业务(应通过 Service);Service 不导入 FastAPI 对象(Request/Response/Depends/APIRouter);`domain/models/` 不得 `from bisheng.*.domain.services`;`common/` 和 `core/` 不得导入领域模块;新代码不放 `api/services/`(旧服务层),应放 `{module}/domain/services/`;Worker 只导入 Domain Services 不导入 Endpoint |
|
||||
| 2 | 命名规范 | 全部 | MEDIUM | DAO 方法:同步 `get_xxx`/`create_xxx`/`update_xxx`/`delete_xxx`,异步 `aget_xxx`/`acreate_xxx`/`aupdate_xxx`/`adelete_xxx`;DAO 为 `@classmethod`;Service 类名 `{Module}{Function}Service`;错误码类名 `{Module}{Error}Error`,Code 遵循 MMMEE;前端页面 PascalCase,store 文件 camelCase+Store,API 函数 camelCase;i18n key 小写+点分隔 |
|
||||
| 3 | 序列化约定 | 后端 `*.py` | HIGH | ORM 继承 `SQLModelSerializable`;API 响应用 `UnifiedResponseModel`(`resp_200`/`resp_500`/`ErrorClass.return_resp`);分页用 `PageData[T]`(新代码);枚举序列化为 `.value`;SSE 用 `to_sse_event()`;WS 关闭用 `websocket_close_message()` |
|
||||
| 4 | 数据库约定 | 后端 models/migration `*.py` | HIGH | 新表必须含 `tenant_id`(`index=True`);必须含 `create_time`/`update_time`;禁止手动 `WHERE tenant_id=`(SQLAlchemy event 自动注入);禁止 Service 层直接写 SQL(用 DAO classmethod);新模块 DAO 放 `{module}/domain/models/` 而非 `database/models/`;使用 `get_sync_db_session()`/`get_async_db_session()` |
|
||||
| 5 | 前端约定 | `*.tsx`/`*.ts` | MEDIUM | Platform: 全局状态用 Zustand store(`src/store/`),API 通过 `controllers/API/` 封装,用户可见文字走 `t('key')` i18n,新路由在 `src/routes/` 注册。Client: API 通过 `src/api/` 封装,store 用 Zustand(`src/store/`),路由基础路径 `/workspace` |
|
||||
| 6 | 信息泄漏 | 全部 | HIGH | 无硬编码密码/密钥/token(`password = "xxx"` 等);错误响应不暴露堆栈/SQL(用 BaseErrorCode);日志中敏感字段脱敏;API 不返回 tenant_id 到前端;前端不硬编码后端 IP。排除:测试 fixtures、config.yaml.example |
|
||||
|
||||
## 差异化处理规则
|
||||
|
||||
### 测试任务
|
||||
- 仅检查:#2 命名规范 + #5 前端约定中的 i18n + AC 标注格式(`覆盖 AC: AC-NN`)
|
||||
- 跳过:#1 架构分层、#3 序列化、#4 数据库
|
||||
|
||||
### 实现任务
|
||||
- 完整执行 #1~#6
|
||||
- 额外验证:配对的测试任务是否已完成(tasks.md 中已打勾)
|
||||
|
||||
### 基础设施任务(ORM 模型、错误码、配置)
|
||||
- 检查:#1 架构分层、#4 数据库约定、#6 信息泄漏
|
||||
- 跳过:#5 前端约定
|
||||
|
||||
### Worker 任务
|
||||
- 检查:#1 架构分层、#4 数据库约定、#6 信息泄漏
|
||||
- 额外检查:tenant_id 是否通过 Celery headers 传递并在 Worker 侧恢复 ContextVar
|
||||
|
||||
## 判定规则
|
||||
|
||||
| 结果 | 条件 | 动作 |
|
||||
|------|------|------|
|
||||
| **PASS** | 全部通过 | 打勾,继续下一任务 |
|
||||
| **PASS_WITH_NOTES** | 仅 MEDIUM 级信息性提醒 | 打勾 + 记录偏差,继续 |
|
||||
| **NEEDS_FIX** | 任何 HIGH 违规 | 修复 → 重审(最多 1 轮) |
|
||||
@@ -1,236 +1,131 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: feat_cicd
|
||||
kind: pipeline # 定义对象类型,还有secret和signature两种类型
|
||||
type: docker # 定义流水线类型,还有kubernetes、exec、ssh等类型
|
||||
name: cicd # 定义流水线名称
|
||||
|
||||
# 禁用 Drone 默认 clone
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
|
||||
# =========================================
|
||||
# 手动 clone(保留你原版)
|
||||
# 因为默认 clone 不走代理
|
||||
# =========================================
|
||||
|
||||
steps: # 定义流水线执行步骤,这些步骤将顺序执行
|
||||
- name: clone
|
||||
|
||||
image: alpine/git
|
||||
|
||||
pull: if-not-exists
|
||||
|
||||
environment:
|
||||
|
||||
http_proxy:
|
||||
http_proxy:
|
||||
from_secret: PROXY
|
||||
|
||||
https_proxy:
|
||||
from_secret: PROXY
|
||||
|
||||
commands:
|
||||
- git config --global core.compression 0
|
||||
- git clone https://github.com/dataelement/bisheng.git .
|
||||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- 'git config --global http.version HTTP/1.1'
|
||||
|
||||
- 'git config --global core.compression 0'
|
||||
|
||||
- 'git clone https://github.com/dataelement/bisheng.git .'
|
||||
|
||||
- 'git checkout $DRONE_COMMIT'
|
||||
|
||||
# =========================================
|
||||
# frontend 镜像构建
|
||||
# =========================================
|
||||
- name: build_frontend
|
||||
|
||||
image: docker:24.0.6
|
||||
|
||||
- name: set poetry
|
||||
pull: if-not-exists
|
||||
image: golang
|
||||
environment:
|
||||
RELEASE_VERSION: 99.99.99
|
||||
NEXUS_PUBLIC:
|
||||
from_secret: NEXUS_PUBLIC
|
||||
NEXUS_PUBLIC_PASSWORD:
|
||||
from_secret: NEXUS_PUBLIC_PASSWORD
|
||||
REPO:
|
||||
from_secret: PY_NEXUS
|
||||
PROXY:
|
||||
from_secret: APT-GET
|
||||
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
|
||||
- name: bisheng-cache
|
||||
path: /app/build/
|
||||
commands:
|
||||
- cd ./src/backend
|
||||
- echo $REPO
|
||||
- REPO2=$(echo $REPO | sed 's/http:\\/\\///g')
|
||||
- sed '/apt-get/ s|$| '"$PROXY"'|' Dockerfile
|
||||
- sed -i '6i\RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
|
||||
- sed -i '7i\RUN poetry source add --priority=supplemental foo http://'$NEXUS_PUBLIC':'$NEXUS_PUBLIC_PASSWORD'@'$REPO2'simple' Dockerfile
|
||||
- sed -i '8i\RUN poetry source add --priority=primary qh https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
|
||||
- cat Dockerfile
|
||||
|
||||
- name: build_docker
|
||||
pull: if-not-exists
|
||||
image: docker:24.0.6
|
||||
privileged: true
|
||||
|
||||
volumes:
|
||||
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
|
||||
- name: apt-cache
|
||||
path: /var/cache/apt/archives # 将应用打包好的Jar和执行脚本挂载出来
|
||||
- name: socket
|
||||
path: /var/run/docker.sock
|
||||
|
||||
- name: pro-cache
|
||||
path: /root/.local/share/pypoetry
|
||||
environment:
|
||||
|
||||
http_proxy:
|
||||
from_secret: PROXY
|
||||
|
||||
https_proxy:
|
||||
from_secret: PROXY
|
||||
|
||||
no_proxy: 192.168.106.8
|
||||
|
||||
DOCKER_REGISTRY: http://192.168.106.8:6082
|
||||
|
||||
DOCKER_REPO: 192.168.106.8:6082/dataelement/bisheng-frontend
|
||||
|
||||
DOCKER_USER:
|
||||
version: release
|
||||
docker_registry: http://192.168.106.8:6082
|
||||
docker_repo: 192.168.106.8:6082/dataelement/bisheng-backend
|
||||
docker_user:
|
||||
from_secret: NEXUS_USER
|
||||
|
||||
DOCKER_PASSWORD:
|
||||
docker_password:
|
||||
from_secret: NEXUS_PASSWORD
|
||||
|
||||
commands:
|
||||
- cd ./src/backend/
|
||||
- docker login -u $docker_user -p $docker_password $docker_registry
|
||||
- docker build -t $docker_repo:$version .
|
||||
- docker push $docker_repo:$version
|
||||
|
||||
- 'VERSION=2.5.0-sg'
|
||||
|
||||
- 'echo "======= build frontend ======="'
|
||||
|
||||
- 'echo "当前版本: $VERSION"'
|
||||
|
||||
# 登录 docker 仓库
|
||||
- 'echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USER" --password-stdin $DOCKER_REGISTRY'
|
||||
|
||||
- 'cd src/frontend'
|
||||
|
||||
# 构建 frontend 镜像
|
||||
- 'docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t $DOCKER_REPO:$VERSION .'
|
||||
|
||||
# 推送镜像
|
||||
- 'docker push $DOCKER_REPO:$VERSION'
|
||||
|
||||
|
||||
# =========================================
|
||||
# backend 镜像构建
|
||||
# =========================================
|
||||
- name: build_backend
|
||||
|
||||
image: docker:24.0.6
|
||||
|
||||
- name: build_docker_frontend
|
||||
pull: if-not-exists
|
||||
|
||||
image: docker:24.0.6
|
||||
privileged: true
|
||||
|
||||
volumes:
|
||||
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
|
||||
- name: apt-cache
|
||||
path: /var/cache/apt/archives # 将应用打包好的Jar和执行脚本挂载出来
|
||||
- name: socket
|
||||
path: /var/run/docker.sock
|
||||
|
||||
environment:
|
||||
|
||||
http_proxy:
|
||||
from_secret: PROXY
|
||||
|
||||
https_proxy:
|
||||
from_secret: PROXY
|
||||
|
||||
no_proxy: 192.168.106.8
|
||||
|
||||
DOCKER_REGISTRY: http://192.168.106.8:6082
|
||||
|
||||
DOCKER_REPO: 192.168.106.8:6082/dataelement/bisheng-backend
|
||||
|
||||
DOCKER_USER:
|
||||
version: release
|
||||
docker_registry: http://192.168.106.8:6082
|
||||
docker_repo: 192.168.106.8:6082/dataelement/bisheng-frontend
|
||||
docker_user:
|
||||
from_secret: NEXUS_USER
|
||||
|
||||
DOCKER_PASSWORD:
|
||||
docker_password:
|
||||
from_secret: NEXUS_PASSWORD
|
||||
|
||||
commands:
|
||||
- cd ./src/frontend/
|
||||
- docker login -u $docker_user -p $docker_password $docker_registry
|
||||
- docker build -t $docker_repo:$version .
|
||||
- docker push $docker_repo:$version
|
||||
|
||||
- 'VERSION=2.5.0-sg'
|
||||
|
||||
- 'echo "======= build backend ======="'
|
||||
|
||||
- 'echo "当前版本: $VERSION"'
|
||||
|
||||
# 登录 docker 仓库
|
||||
- 'echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USER" --password-stdin $DOCKER_REGISTRY'
|
||||
|
||||
- 'cd src/backend'
|
||||
|
||||
# 构建 backend 镜像
|
||||
- 'docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t $DOCKER_REPO:$VERSION .'
|
||||
|
||||
# 推送镜像
|
||||
- 'docker push $DOCKER_REPO:$VERSION'
|
||||
|
||||
# =========================================
|
||||
# 拉取 sg 分支最新镜像并部署
|
||||
# =========================================
|
||||
- name: deploy_sg_images
|
||||
|
||||
- name: ssh deploy
|
||||
image: appleboy/drone-ssh
|
||||
|
||||
pull: if-not-exists
|
||||
|
||||
environment:
|
||||
|
||||
NEXUS_USER:
|
||||
from_secret: NEXUS_USER
|
||||
|
||||
NEXUS_PASSWORD:
|
||||
from_secret: NEXUS_PASSWORD
|
||||
|
||||
settings:
|
||||
|
||||
host: 192.168.106.171
|
||||
|
||||
host: 192.168.106.116
|
||||
username: root
|
||||
|
||||
password:
|
||||
from_secret: sshpwd
|
||||
script:
|
||||
- echo =======找到目录=======
|
||||
- cd /opt/server/bisheng-test
|
||||
- echo =======直接启动=======
|
||||
- docker compose pull
|
||||
- docker compose up -d
|
||||
|
||||
port: 22
|
||||
|
||||
command_timeout: 20m
|
||||
|
||||
envs:
|
||||
- NEXUS_USER
|
||||
- NEXUS_PASSWORD
|
||||
|
||||
script: |
|
||||
|
||||
set -eu
|
||||
|
||||
echo "======= deploy latest sg images ======="
|
||||
|
||||
export https_proxy=http://118.195.232.223:39995
|
||||
export http_proxy=http://118.195.232.223:39995
|
||||
export all_proxy=socks5://118.195.232.223:39995
|
||||
|
||||
cd /opt/code/bisheng
|
||||
|
||||
echo "======= update deployment config ======="
|
||||
|
||||
git fetch origin feat/2.5.0-sg
|
||||
git checkout feat/2.5.0-sg
|
||||
git pull --ff-only origin feat/2.5.0-sg
|
||||
|
||||
echo "======= login docker registry ======="
|
||||
|
||||
echo "$NEXUS_PASSWORD" | docker login cr.dataelem.com -u "$NEXUS_USER" --password-stdin
|
||||
|
||||
echo "======= pull application images ======="
|
||||
|
||||
docker compose -f docker/docker-compose.yml pull backend backend_worker frontend
|
||||
|
||||
echo "======= recreate application containers ======="
|
||||
|
||||
docker compose -f docker/docker-compose.yml up -d --no-deps --force-recreate backend backend_worker frontend
|
||||
|
||||
echo "======= application status ======="
|
||||
|
||||
docker compose -f docker/docker-compose.yml ps backend backend_worker frontend
|
||||
|
||||
# =========================================
|
||||
# 飞书通知
|
||||
# =========================================
|
||||
- name: notify_result
|
||||
|
||||
image: plugins/webhook
|
||||
|
||||
- name: notify-start # notify
|
||||
pull: if-not-exists
|
||||
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
|
||||
debug: true
|
||||
|
||||
urls:
|
||||
urls:
|
||||
from_secret: FEISHU_URL
|
||||
|
||||
content_type: application/json
|
||||
|
||||
template: |
|
||||
{
|
||||
"msg_type": "interactive",
|
||||
@@ -240,34 +135,201 @@ steps:
|
||||
"template_id": "AAqkI9bnY5FUs",
|
||||
"template_variable": {
|
||||
"repo_name": "{{ repo.name }}",
|
||||
"build_branch": "{{ build.branch }}",
|
||||
"build_branch": "{{build.branch}}",
|
||||
"build_author": "{{ DRONE_COMMIT_AUTHOR }}",
|
||||
"link": "{{ build.link }}",
|
||||
"link": "{{build.link}}",
|
||||
"commit_msg": "{{ trim build.message }}",
|
||||
"build_tag":"{{build.tag}}",
|
||||
"build_start":"{{build.started}}",
|
||||
"status": "{{ build.status }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failure: ignore
|
||||
|
||||
when:
|
||||
when: # 成功
|
||||
status:
|
||||
- success
|
||||
- failure
|
||||
|
||||
trigger:
|
||||
|
||||
branch:
|
||||
- feat/2.5.0-sg
|
||||
|
||||
- release
|
||||
event:
|
||||
- push
|
||||
- push
|
||||
|
||||
volumes:
|
||||
- name: bisheng-cache
|
||||
host:
|
||||
path: /opt/drone/data/bisheng/
|
||||
- name: pro-cache
|
||||
host:
|
||||
path: /opt/drone/data/pro/
|
||||
- name: apt-cache
|
||||
host:
|
||||
path: /opt/drone/data/bisheng/apt/
|
||||
- name: socket
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
- name: socket
|
||||
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
---
|
||||
|
||||
kind: pipeline # 定义对象类型,还有secret和signature两种类型
|
||||
type: docker # 定义流水线类型,还有kubernetes、exec、ssh等类型
|
||||
name: feat_cicd # 定义流水线名称
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps: # 定义流水线执行步骤,这些步骤将顺序执行
|
||||
- name: clone
|
||||
image: alpine/git
|
||||
pull: if-not-exists
|
||||
environment:
|
||||
http_proxy:
|
||||
from_secret: PROXY
|
||||
https_proxy:
|
||||
from_secret: PROXY
|
||||
commands:
|
||||
- git config --global core.compression 0
|
||||
- git clone https://github.com/dataelement/bisheng.git .
|
||||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: set poetry
|
||||
pull: if-not-exists
|
||||
image: golang
|
||||
environment:
|
||||
NEXUS_PUBLIC:
|
||||
from_secret: NEXUS_PUBLIC
|
||||
NEXUS_PUBLIC_PASSWORD:
|
||||
from_secret: NEXUS_PUBLIC_PASSWORD
|
||||
REPO:
|
||||
from_secret: PY_NEXUS
|
||||
PROXY:
|
||||
from_secret: APT-GET
|
||||
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
|
||||
- name: bisheng-cache
|
||||
path: /app/build/
|
||||
commands:
|
||||
- cd ./src/backend
|
||||
- echo $REPO
|
||||
- REPO2=$(echo $REPO | sed 's/http:\\/\\///g')
|
||||
- sed '/apt-get/ s|$| '"$PROXY"'|' Dockerfile
|
||||
- sed -i '6i\RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
|
||||
- sed -i '7i\RUN poetry source add --priority=supplemental foo http://'$NEXUS_PUBLIC':'$NEXUS_PUBLIC_PASSWORD'@'$REPO2'simple' Dockerfile
|
||||
- sed -i '8i\RUN poetry source add --priority=primary qh https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
|
||||
- cat Dockerfile
|
||||
|
||||
- name: build_docker
|
||||
pull: if-not-exists
|
||||
image: docker:24.0.6
|
||||
privileged: true
|
||||
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
|
||||
- name: apt-cache
|
||||
path: /var/cache/apt/archives # 将应用打包好的Jar和执行脚本挂载出来
|
||||
- name: socket
|
||||
path: /var/run/docker.sock
|
||||
- name: pro-cache
|
||||
path: /root/.local/share/pypoetry
|
||||
environment:
|
||||
http_proxy:
|
||||
from_secret: PROXY
|
||||
https_proxy:
|
||||
from_secret: PROXY
|
||||
no_proxy: 192.168.106.8
|
||||
version: ${DRONE_BRANCH}
|
||||
docker_registry: http://192.168.106.8:6082
|
||||
docker_repo: 192.168.106.8:6082/dataelement/bisheng-backend
|
||||
docker_user:
|
||||
from_secret: NEXUS_USER
|
||||
docker_password:
|
||||
from_secret: NEXUS_PASSWORD
|
||||
commands:
|
||||
- echo "old tag is $version"
|
||||
- version=$(echo $version | sed 's/\\//_/g')
|
||||
- echo "build image tag is $version"
|
||||
- cd ./src/backend/
|
||||
- docker login -u $docker_user -p $docker_password $docker_registry
|
||||
- docker build -t $docker_repo:$version .
|
||||
- docker push $docker_repo:$version
|
||||
|
||||
- name: build_docker_frontend
|
||||
pull: if-not-exists
|
||||
image: docker:24.0.6
|
||||
privileged: true
|
||||
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
|
||||
- name: apt-cache
|
||||
path: /var/cache/apt/archives # 将应用打包好的Jar和执行脚本挂载出来
|
||||
- name: socket
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
http_proxy:
|
||||
from_secret: PROXY
|
||||
https_proxy:
|
||||
from_secret: PROXY
|
||||
no_proxy: 192.168.106.8
|
||||
version: ${DRONE_BRANCH}
|
||||
docker_registry: http://192.168.106.8:6082
|
||||
docker_repo: 192.168.106.8:6082/dataelement/bisheng-frontend
|
||||
docker_user:
|
||||
from_secret: NEXUS_USER
|
||||
docker_password:
|
||||
from_secret: NEXUS_PASSWORD
|
||||
commands:
|
||||
- echo "old tag is $version"
|
||||
- version=$(echo $version | sed 's/\\//_/g')
|
||||
- echo "build image tag is $version"
|
||||
- cd ./src/frontend/
|
||||
- docker login -u $docker_user -p $docker_password $docker_registry
|
||||
- docker build -t $docker_repo:$version .
|
||||
- docker push $docker_repo:$version
|
||||
|
||||
- name: notify-start # notify
|
||||
pull: if-not-exists
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
debug: true
|
||||
urls:
|
||||
from_secret: FEISHU_URL
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"type": "template",
|
||||
"data": {
|
||||
"template_id": "AAqkI9bnY5FUs",
|
||||
"template_variable": {
|
||||
"repo_name": "{{ repo.name }}",
|
||||
"build_branch": "{{build.branch}}",
|
||||
"build_author": "{{ DRONE_COMMIT_AUTHOR }}",
|
||||
"link": "{{build.link}}",
|
||||
"commit_msg": "{{ trim build.message }}",
|
||||
"build_tag":"{{build.tag}}",
|
||||
"build_start":"{{build.started}}",
|
||||
"status": "{{ build.status }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
when: # 成功
|
||||
status:
|
||||
- success
|
||||
trigger:
|
||||
branch:
|
||||
- add_some_branch_you_need
|
||||
- chore/fix-vulnerability
|
||||
event:
|
||||
- push
|
||||
|
||||
volumes:
|
||||
- name: bisheng-cache
|
||||
host:
|
||||
path: /opt/drone/data/bisheng/
|
||||
- name: pro-cache
|
||||
host:
|
||||
path: /opt/drone/data/pro/
|
||||
- name: apt-cache
|
||||
host:
|
||||
path: /opt/drone/data/bisheng/apt/
|
||||
- name: socket
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{js,jsx,ts,tsx,json,yml,yaml,css,scss,html}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@@ -1,5 +1,5 @@
|
||||
# 默认:自动识别文本,统一用 LF 存库
|
||||
#* text=auto eol=lf
|
||||
* text=auto eol=lf
|
||||
|
||||
# 明确常见文本文件用 LF
|
||||
*.py text eol=lf
|
||||
@@ -31,4 +31,4 @@
|
||||
*.mp4 binary
|
||||
*.docx binary
|
||||
*.xlsx binary
|
||||
*.pptx binary
|
||||
*.pptx binary
|
||||
@@ -1,20 +0,0 @@
|
||||
## What
|
||||
|
||||
简要描述做了什么改动。
|
||||
|
||||
## Why
|
||||
|
||||
为什么需要这个改动?
|
||||
|
||||
## How
|
||||
|
||||
实现方式、设计决策(如有)。
|
||||
|
||||
## Test
|
||||
|
||||
- [ ] 本地测试通过
|
||||
- [ ] 114 测试服务器验证通过
|
||||
|
||||
## Related
|
||||
|
||||
- Issue/ticket:
|
||||
@@ -15,7 +15,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build_bisheng_arm:
|
||||
runs-on: ubuntu-22.04-arm
|
||||
runs-on: ubuntu-latest
|
||||
# if: startsWith(github.event.ref, 'refs/tags')
|
||||
steps:
|
||||
- name: checkout
|
||||
@@ -38,15 +38,13 @@ jobs:
|
||||
# DOCKERHUB_TOKEN: docker hub => Account Setting => Security 创建.
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# 构建 backend 并推送到 Docker hub
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# because ibm-db driver not support linux arm64
|
||||
- name: fix ibm-db lib error
|
||||
run: |
|
||||
# remove ibm-db lib
|
||||
sed -i '/ibm-db*/d' ./src/backend/pyproject.toml
|
||||
|
||||
- name: Build backend arm64 and push
|
||||
id: docker_build_backend
|
||||
|
||||
@@ -52,20 +52,7 @@ jobs:
|
||||
id: docker_build_backend
|
||||
run: |
|
||||
docker buildx build --file ./src/backend/Dockerfile --platform linux/amd64 --provenance false --tag ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-amd64 --push ./src/backend/
|
||||
|
||||
- name: Login to CR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: https://cr.dataelem.com/
|
||||
username: ${{ secrets.CR_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.CR_DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Sync backend amd64 to CR
|
||||
run: |
|
||||
docker pull ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-amd64
|
||||
docker tag ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-amd64 cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-amd64
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-amd64
|
||||
|
||||
|
||||
build_backend_arm:
|
||||
runs-on: ubuntu-22.04-arm
|
||||
steps:
|
||||
@@ -96,30 +83,11 @@ jobs:
|
||||
- name: set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# because ibm-db driver not support linux arm64
|
||||
- name: fix ibm-db lib error
|
||||
run: |
|
||||
# remove ibm-db lib
|
||||
sed -i '/ibm-db*/d' ./src/backend/pyproject.toml
|
||||
|
||||
- name: Build backend and push
|
||||
id: docker_build_backend
|
||||
run: |
|
||||
docker buildx build --file ./src/backend/Dockerfile --platform linux/arm64 --provenance false --tag ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64 --push ./src/backend/
|
||||
|
||||
- name: Login to CR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: https://cr.dataelem.com/
|
||||
username: ${{ secrets.CR_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.CR_DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Sync backend arm64 to CR
|
||||
run: |
|
||||
docker pull ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker tag ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64 cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
|
||||
build_bisheng_frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -149,19 +117,6 @@ jobs:
|
||||
run: |
|
||||
docker buildx build --file ./src/frontend/Dockerfile --platform linux/amd64 --provenance false --tag ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-amd64 --push ./src/frontend/
|
||||
|
||||
- name: Login to CR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: https://cr.dataelem.com/
|
||||
username: ${{ secrets.CR_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.CR_DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Sync frontend amd64 to CR
|
||||
run: |
|
||||
docker pull ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-amd64
|
||||
docker tag ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-amd64 cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-amd64
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-amd64
|
||||
|
||||
build_frontend_arm:
|
||||
runs-on: ubuntu-22.04-arm
|
||||
steps:
|
||||
@@ -197,19 +152,6 @@ jobs:
|
||||
run: |
|
||||
docker buildx build --file ./src/frontend/Dockerfile --platform linux/arm64 --provenance false --tag ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64 --push ./src/frontend/
|
||||
|
||||
- name: Login to CR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: https://cr.dataelem.com/
|
||||
username: ${{ secrets.CR_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.CR_DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Sync frontend arm64 to CR
|
||||
run: |
|
||||
docker pull ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker tag ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64 cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
|
||||
notify_feishu:
|
||||
needs:
|
||||
- build_bisheng_backend
|
||||
|
||||
@@ -89,15 +89,6 @@ jobs:
|
||||
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:latest
|
||||
|
||||
echo "sync arm image"
|
||||
docker pull ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker tag ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64 cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
|
||||
docker pull ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker tag ${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64 cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
docker push cr.dataelem.com/${{ env.DOCKERHUB_REPO }}bisheng-frontend:${{ steps.get_version.outputs.VERSION }}-arm64
|
||||
echo "--- sync over ---"
|
||||
|
||||
test_pull_images:
|
||||
|
||||
@@ -13,14 +13,6 @@ autogen_coding/
|
||||
# Mac
|
||||
.DS_Store
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
# Personal notes (lilu's local reference docs, not for sharing)
|
||||
.claude/docs/
|
||||
|
||||
# Local feature planning artifacts
|
||||
features/
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
.vscode/settings.json
|
||||
@@ -28,7 +20,6 @@ features/
|
||||
.ruff_cache
|
||||
.isort.cfg
|
||||
.idea/
|
||||
.trae/
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
@@ -132,14 +123,16 @@ __pycache__/
|
||||
*$py.class
|
||||
notebooks
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
./third_party
|
||||
output/
|
||||
develop-eggs/
|
||||
config.dev.yaml
|
||||
src/backend/bisheng/config.yaml
|
||||
src/backend/data/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -271,26 +264,4 @@ sftp-config.json
|
||||
# Docker local files
|
||||
docker/data/*
|
||||
docker/mysql/data/*
|
||||
docker/office/bisheng/*.gz
|
||||
|
||||
bisheng-sync.sh
|
||||
/deploy.sh
|
||||
.omx/
|
||||
|
||||
# Local installers / stray root files (do not commit)
|
||||
*.msi
|
||||
|
||||
# Permission migration checkpoint (per-deployment local state)
|
||||
src/backend/bisheng/permission/migration/migration_f006_checkpoint.json
|
||||
pandoc-3.10-x86_64-macOS.pkg
|
||||
test.sh
|
||||
sg_test.sh
|
||||
sg-sync-api-preview.md
|
||||
sg-sync-api-内部.md
|
||||
mdf_file_read_count.sh
|
||||
.dev-stack/
|
||||
|
||||
# Local-only 门户联调 (do not commit)
|
||||
.cursor/rules/local-portal-dev-stack.mdc
|
||||
scripts/dev-stack.sh
|
||||
.cursor/
|
||||
docker/office/bisheng/*.gz
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "src/bisheng-unstructured"]
|
||||
path = src/bisheng-unstructured
|
||||
url = https://github.com/dataelement/bisheng-unstructured.git
|
||||
[submodule "src/bisheng-rt"]
|
||||
path = src/bisheng-rt
|
||||
url = https://github.com/dataelement/bisheng-rt.git
|
||||
|
||||
@@ -1,26 +1,57 @@
|
||||
exclude: ^(scripts|docs|docker|requirements|test|experimental)/
|
||||
|
||||
exclude: ^scripts|docs|docker|requirements|README.md|test|experimental
|
||||
repos:
|
||||
- repo: https://github.com/PyCQA/flake8.git
|
||||
rev: 3.8.3
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["--max-line-length=120"]
|
||||
- repo: https://github.com/asottile/seed-isort-config
|
||||
rev: v2.2.0
|
||||
hooks:
|
||||
- id: seed-isort-config
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 4.3.21
|
||||
hooks:
|
||||
- id: isort
|
||||
files: \.(py|pyd)$
|
||||
args: ["-l 100"]
|
||||
- repo: https://github.com/pre-commit/mirrors-yapf
|
||||
rev: v0.32.0
|
||||
hooks:
|
||||
- id: yapf
|
||||
files: \.(py|pyd)$
|
||||
args: ["--style={column_limit: 120}"]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: \.(py|pyd|ts|tsx|js|jsx)$
|
||||
files: \.(py|pyd)$
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
files: \.(py|pyd|ts|tsx|js|jsx)$
|
||||
files: \.(py|pyd)$
|
||||
- id: requirements-txt-fixer
|
||||
- id: double-quote-string-fixer
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=500"]
|
||||
- id: fix-encoding-pragma
|
||||
args: ["--remove"]
|
||||
- id: mixed-line-ending
|
||||
args: ["--fix=lf"]
|
||||
files: \.(py|pyd|ts|tsx|js|jsx)$
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.8
|
||||
files: \.(py|pyd)$
|
||||
# - repo: https://github.com/jumanjihouse/pre-commit-hooks
|
||||
# rev: 2.1.4
|
||||
# hooks:
|
||||
# - id: markdownlint
|
||||
# args: ["-r", "~MD002,~MD013,~MD029,~MD033,~MD034,~MD005"]
|
||||
# - repo: https://github.com/myint/docformatter
|
||||
# rev: v1.3.1
|
||||
# hooks:
|
||||
# - id: docformatter
|
||||
# args: ["--in-place", "--wrap-descriptions", "79"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
types_or: [python, pyi]
|
||||
- id: ruff-format
|
||||
types_or: [python, pyi]
|
||||
- id: clang-format
|
||||
name: clang-format
|
||||
description: Format files with ClangFormat
|
||||
entry: clang-format -i
|
||||
language: system
|
||||
files: \.(c|cc|cxx|cpp|cu|h|hpp|hxx|cuh|proto)$
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI agents and Claude Code. Loaded every session — only P0 rules live here.
|
||||
Deeper backend reference (module map, subsystems): `src/backend/AGENTS.md` (auto-loaded when editing backend files).
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Identity
|
||||
|
||||
**BiSheng (毕昇)** — Enterprise LLM application DevOps platform. Monorepo, three sub-projects:
|
||||
|
||||
| Path | Project | Stack |
|
||||
|------|---------|-------|
|
||||
| `src/backend/` | FastAPI + Celery Workers + Linsight Worker | Python 3.10+, uv, SQLModel, LangGraph |
|
||||
| `src/frontend/platform/` | Admin / builder UI | Vite 5 + **Zustand** + react-query v3 + bs-ui |
|
||||
| `src/frontend/client/` | End-user chat UI (`/workspace` base path) | Vite 6 + **Recoil** + react-query v5 + shadcn/ui |
|
||||
|
||||
---
|
||||
|
||||
## 2. Commands
|
||||
|
||||
Backend commands (test, lint, start, Celery, Alembic) → `src/backend/CLAUDE.md`.
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
cd src/frontend/platform && npm install && npm start -- --host 0.0.0.0 # :3001
|
||||
cd src/frontend/client && npm install && npm run dev # :4001
|
||||
# Commercial Gateway proxy mode:
|
||||
VITE_PROXY_TARGET=http://localhost:8180 npm start -- --host 0.0.0.0
|
||||
|
||||
# Middleware (Docker only)
|
||||
bash docker/local-dev/start-middleware.sh # MySQL / Redis / Milvus / ES / MinIO / OpenFGA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend Rules (P0)
|
||||
|
||||
### 3.1 Layered Architecture (DDD)
|
||||
|
||||
Call chain — never skip layers:
|
||||
```
|
||||
Router → Endpoint → Service → Repository → DB
|
||||
```
|
||||
|
||||
- **Never** `import bisheng.database.models.*` in endpoints (arch-guard RULE-3 WARNING).
|
||||
- **Never** write ORM queries in Service; **never** add new DAO entry points for new features.
|
||||
- New module layout: `<module>/{api/router.py, api/endpoints/, domain/services/, domain/models/, domain/schemas/, domain/repositories/}`
|
||||
- Register the router in `bisheng/api/router.py`.
|
||||
|
||||
### 3.2 Dual-DB Compatibility (MySQL + DM8) ⚠️
|
||||
|
||||
Every new feature must work on both dialects. DM8 is not optional.
|
||||
|
||||
| ✅ Use | ❌ Never use |
|
||||
|--------|-------------|
|
||||
| `dialect_helpers.JsonType` | `sqlalchemy.JSON`, `mysql.JSON` |
|
||||
| `dialect_helpers.LargeText` | `LONGTEXT`, `MEDIUMTEXT` |
|
||||
| `dialect_helpers.UPDATE_TIME_SERVER_DEFAULT` | `ON UPDATE CURRENT_TIMESTAMP` |
|
||||
| `SQLAlchemy inspect()` | `information_schema`, `DATABASE()` |
|
||||
| Explicit relational columns | `JSON_EXTRACT` / `JSON_CONTAINS` / `JSON_SEARCH` |
|
||||
|
||||
macOS: DM8 driver (`dmPython`/`dmAsync`) is not installed (`sys_platform != 'darwin'`). Real DM8 validation runs on CI/Linux only.
|
||||
|
||||
### 3.3 Multi-Tenancy — Auto-Injected, Never Manual
|
||||
|
||||
**Never write `WHERE tenant_id = X` manually.** SQLAlchemy events handle it automatically for 23+ tables.
|
||||
`multi_tenant.enabled=false` behaves identically to single-tenant (default `tenant_id=1`).
|
||||
|
||||
### 3.4 Permissions — Unified Entry Point
|
||||
|
||||
```python
|
||||
from bisheng.permission.domain.services.permission_service import PermissionService
|
||||
await PermissionService.check(...) # check access
|
||||
await PermissionService.authorize(...) # write OpenFGA owner tuple on resource creation (required)
|
||||
```
|
||||
|
||||
**Never** query `role_access` directly for resource authorization (arch-guard RULE-8 VIOLATION).
|
||||
Resource creation **must** call `PermissionService.authorize()`; failures go to the `failed_tuples` retry table.
|
||||
|
||||
Five-level short-circuit: `super_admin` → tenant mismatch deny → tenant admin → ReBAC (OpenFGA) → RBAC menu.
|
||||
|
||||
### 3.5 API Conventions
|
||||
|
||||
```python
|
||||
from bisheng.common.dependencies.user_deps import UserPayload
|
||||
user: UserPayload = Depends(UserPayload.get_login_user) # WebSocket: get_login_user_from_ws
|
||||
|
||||
from bisheng.common.schemas.api import resp_200, resp_500
|
||||
return resp_200(data) # success
|
||||
return resp_500(code, msg) # business error
|
||||
```
|
||||
|
||||
Error codes: 5-digit `MMMEE` (3-digit module + 2-digit error), defined in `common/errcode/`.
|
||||
Module numbers: 100=server, 104=assistant, 105=flow, 106=user, 108=llm, 109=knowledge, 110=linsight, 120=workstation, 130=chat, 140=message, 150=tool, 180=knowledge_space.
|
||||
|
||||
Pagination: `PageData[T]` (new code) with fields `data` + `total`; `PageList[T]` is legacy-compat only.
|
||||
|
||||
### 3.6 Backend Coding Standards
|
||||
|
||||
- Python code must follow PEP 8, use type hints for function/method parameters and return values, and keep naming conventional: modules/functions/variables `snake_case`, classes `PascalCase`, constants `UPPER_SNAKE_CASE`.
|
||||
- Keep formatting compatible with project tooling: `ruff format`, `ruff check --fix`, and import ordering.
|
||||
- Repository interfaces live under `domain/repositories/interfaces/` and extend `BaseRepository[ModelClass, IDType]` + `ABC`; implementations live under `domain/repositories/implementations/` and extend `BaseRepositoryImpl[ModelClass, IDType]` + the module repository interface.
|
||||
- Schema changes must go through Alembic migrations in `bisheng/core/database/alembic/versions`; never edit database tables directly.
|
||||
- CRUD must use SQLModel / SQLAlchemy ORM models through Repository methods. Avoid raw SQL; if a complex query is required, encapsulate it behind a clear Repository API.
|
||||
- Database sessions must be managed by dependency injection or existing session helpers such as `@db_session`; never create ad-hoc unmanaged sessions in business code.
|
||||
- Business errors belong in `bisheng/common/errcode/` as `BaseErrorCode` subclasses with explicit `Code` and `Msg`; do not return raw error strings from business logic.
|
||||
- Log critical business flow, exception boundaries, and important operations with useful context such as user ID or request ID. Log messages, comments, and docstrings should be concise English; comments explain why non-obvious logic exists.
|
||||
|
||||
---
|
||||
|
||||
## 4. Frontend Rules (P0)
|
||||
|
||||
The two React apps **must not be mixed**. Apply rules by directory:
|
||||
|
||||
| Dimension | Platform (`src/frontend/platform/`) | Client (`src/frontend/client/`) |
|
||||
|-----------|-------------------------------------|--------------------------------|
|
||||
| State | **Zustand** (`@/store/`) + Context for local UI | **Recoil** (`~/store/`) |
|
||||
| Server state | react-query **v3** (`useQuery({ queryFn })`) | react-query **v5** |
|
||||
| Path alias | `@/` → `src/` | `~/` (or `@/`) → `src/` |
|
||||
| HTTP layer | `@/controllers/request.ts` | `~/api/request.ts` |
|
||||
| UI library | `@/components/bs-ui/` (Radix-based) | `~/components/ui/` (shadcn) |
|
||||
| Icons | `@/components/bs-icons/` | `lucide-react` |
|
||||
| i18n hook | `useTranslation()` → `t()` | `useLocalize()` → `localize()` |
|
||||
| i18n files | `public/locales/{lang}/{ns}.json` (multi-namespace) | `src/locales/{lang}/translation.json` (single file) |
|
||||
| Toast | `toast({ title, variant: 'error'\|'success', description })` | `showToast({ message, severity: 'error'\|'success' })` |
|
||||
| Confirm dialog | `bsConfirm(...)` (bs-ui) | — |
|
||||
| Workflow editor | `@xyflow/react` (**not** `react-flow-renderer`), nodes in `src/CustomNodes/` | — |
|
||||
|
||||
**Hard rules (both apps):**
|
||||
- TypeScript only (`.ts` / `.tsx`); functional components only; no class components.
|
||||
- Single file ≤ 600 lines. Extract sub-components or hooks when exceeded.
|
||||
- `interface` for Props; `type` for internal types.
|
||||
- `handleXxx` for internal handlers; `onXxx` for props.
|
||||
- **Never** `import axios` directly — always use the wrapped request module above.
|
||||
- **Never** introduce new UI libraries or state management libraries.
|
||||
- All code comments in English.
|
||||
- 403: handled automatically by response interceptors. Never add 403 branches in business code.
|
||||
|
||||
---
|
||||
|
||||
## 5. Architecture Guard (Auto-enforced)
|
||||
|
||||
`scripts/arch-guard.sh` runs after every Write/Edit via PostToolUse hook:
|
||||
|
||||
| # | Rule | Severity |
|
||||
|---|------|----------|
|
||||
| 1 | `common/`, `core/` must not import `domain/`, `api/` | VIOLATION |
|
||||
| 2 | `database/models/` must not import `domain/` | VIOLATION |
|
||||
| 3 | Endpoints must not directly import `database/models/` | WARNING (migration period) |
|
||||
| 4 | `domain/models/` must not import `domain/services/` | VIOLATION |
|
||||
| 5 | API layer must not cross-import between modules | VIOLATION |
|
||||
| 6 | Frontend store must not call HTTP directly (use `controllers/API/` or `api/`) | WARNING |
|
||||
| 7 | No hardcoded secrets (password/secret/token literals) | WARNING |
|
||||
| 8 | DAO/Model must not read `RoleAccessDao` for permission filtering | VIOLATION |
|
||||
|
||||
**VIOLATION rules must be fixed immediately** — these are v2.5 refactor boundaries.
|
||||
|
||||
---
|
||||
|
||||
## 6. SDD Workflow (Required for non-trivial features)
|
||||
|
||||
```
|
||||
0. release-contract.md (once per version)
|
||||
1. Spec Discovery → ★ user confirms
|
||||
2. spec.md → /sdd-review <dir> spec → ★ user confirms
|
||||
3. tasks.md → /sdd-review <dir> tasks
|
||||
4. branch: feat/<version>/{NNN}-{name}
|
||||
5. implement task-by-task → /task-review <dir> <id> → check off
|
||||
6. /e2e-test <dir> (mandatory)
|
||||
7. /code-review --base <main branch>
|
||||
8. merge
|
||||
```
|
||||
|
||||
Artifacts: `features/v{X.Y.Z}/{NNN}-{name}/spec.md` and `tasks.md`. Templates: `features/_templates/`.
|
||||
**★ pause points cannot be skipped.** Deviations must be recorded in `tasks.md §实际偏差记录`.
|
||||
|
||||
Tests: file new backend tests under `test/<module>/` (e.g., `test/approval/`), not in `test/` root. `asyncio_mode=auto`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Common Pitfalls
|
||||
|
||||
| Pitfall | Reality |
|
||||
|---------|---------|
|
||||
| `/api/v1/env` version field | Hardcoded `2.4.0` in source — unreliable. Use route probing instead. |
|
||||
| MinIO image 403 | Vite `fileServiceTarget` must exactly match `config.yaml` `object_storage.minio.sharepoint`. |
|
||||
| Passwords in config.yaml | Fernet-encrypted. Never write plaintext passwords into the YAML. |
|
||||
| First registered user | Becomes `super_admin` automatically. In multi-tenant mode, create the tenant first. |
|
||||
| `BISHENG_PRO=true` | Must be set **before** starting the backend, or `/api/v1/user/sso` endpoint won't exist. |
|
||||
| DB config changes | 100s Redis TTL — wait or flush Redis after changing DB-stored config. |
|
||||
| Celery Beat + multi-tenant | Beat iterates all active tenants; adding a task multiplies load by N tenants. |
|
||||
| API proxy modes | Default: Vite → `:7860`. Commercial: set `VITE_PROXY_TARGET=http://localhost:8180`. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Reference
|
||||
|
||||
- **Backend module map, subsystem internals** → `src/backend/CLAUDE.md`
|
||||
- **Architecture docs** → `docs/architecture/` (`10-permission-rbac.md`, `11-gateway.md`)
|
||||
- **SDD guide** → `docs/SDD-Guide.md`
|
||||
- **v2.5 permission/multi-tenant PRD** → `docs/PRD/`
|
||||
- **Skills**: `/sdd-review`, `/task-review`, `/code-review`, `/e2e-test`, `/i18n-localizer`, `/react-component-refactor`
|
||||
@@ -34,27 +34,25 @@ BISHENG is an open LLM application devops platform, focusing on enterprise scena
|
||||
|
||||
|
||||
## Features
|
||||
1. **Lingsight, a general-purpose agent with expert-level taste**: Through the [AGL](https://github.com/dataelement/AgentGuidanceLanguage)(Agent Guidance Language) framework, we embed domain experts’ preferences, experience, and business logic into the AI, enabling the agent to exhibit “expert-level understanding” when handling tasks.
|
||||
<p align="center"><img src="https://dataelem.com/bs/Linsight.png" alt="sence1"></p>
|
||||
|
||||
2. **Unique [BISHENG Workflow](https://dataelem.feishu.cn/wiki/R7HZwH5ZGiJUDrkHZXicA9pInif)**
|
||||
1. Unique [BISHENG Workflow](https://dataelem.feishu.cn/wiki/R7HZwH5ZGiJUDrkHZXicA9pInif)
|
||||
- 🧩 **Independent and comprehensive application orchestration framework**: Enables the execution of various tasks within a single framework (while similar products rely on bot invocation or separate chatflow and workflow modules for different tasks).
|
||||
- 🔄 **Human in the loop**: Allows users to intervene and provide feedback during the execution of workflows (including multi-turn conversations), whereas similar products can only execute workflows from start to finish without intervention.
|
||||
- 💥 **Powerful**: Supports loops, parallelism, batch processing, conditional logic, and free combination of all logic components. It also handles complex scenarios such as multi-type input/output, report generation, content review, and more.
|
||||
- 🖐️ **User-friendly and intuitive**: Operations like loops, parallelism, and batch processing, which require specialized components in similar products, can be easily visualized in BISHENG as a "flowchart" (drawing a loop forms a loop, aligning elements creates parallelism, and selecting multiple items enables batch processing).
|
||||
<p align="center"><img src="https://dataelem.com/bs/bisheng_workflow.png" alt="sence0"></p>
|
||||
|
||||
3. <b>Designed for Enterprise Applications</b>: Document review, fixed-layout report generation, multi-agent collaboration, policy update comparison, support ticket assistance, customer service assistance, meeting minutes generation, resume screening, call record analysis, unstructured data governance, knowledge mining, data analysis, and more.
|
||||
The platform supports the construction of <b>highly complex enterprise application scenarios</b> and offers <b>deep optimization</b> with hundreds of components and thousands of parameters.
|
||||
2. <b>Designed for Enterprise Applications</b>: Document review, fixed-layout report generation, multi-agent collaboration, policy update comparison, support ticket assistance, customer service assistance, meeting minutes generation, resume screening, call record analysis, unstructured data governance, knowledge mining, data analysis, and more.
|
||||
|
||||
The platform supports the construction of <b>highly complex enterprise application scenarios</b> and offers <b>deep optimization</b> with hundreds of components and thousands of parameters.
|
||||
<p align="center"><img src="https://dataelem.com/bs/chat.png" alt="sence1"></p>
|
||||
|
||||
4. <b>Enterprise-grade</b> features are the fundamental guarantee for application implementation: security review, RBAC, user group management, traffic control by group, SSO/LDAP, vulnerability scanning and patching, high availability deployment solutions, monitoring, statistics, and more.
|
||||
3. <b>Enterprise-grade</b> features are the fundamental guarantee for application implementation: security review, RBAC, user group management, traffic control by group, SSO/LDAP, vulnerability scanning and patching, high availability deployment solutions, monitoring, statistics, and more.
|
||||
<p align="center"><img src="https://dataelem.com/bs/pro.png" alt="sence2"></p>
|
||||
|
||||
5. <b>High-Precision Document Parsing</b>: Our high-precision document parsing model is trained on a vast amount of high-quality data accumulated over past 5 years. It includes high-precision printed text, handwritten text, and rare character recognition models, table recognition models, layout analysis models, and seal models., table recognition models, layout analysis models, and seal models. You can deploy it privately for free.
|
||||
4. <b>High-Precision Document Parsing</b>: Our high-precision document parsing model is trained on a vast amount of high-quality data accumulated over past 5 years. It includes high-precision printed text, handwritten text, and rare character recognition models, table recognition models, layout analysis models, and seal models., table recognition models, layout analysis models, and seal models. You can deploy it privately for free.
|
||||
<p align="center"><img src="https://dataelem.com/bs/ocr.png" alt="sence3"></p>
|
||||
|
||||
6. A community for sharing best practices across various enterprise scenarios: An open repository of application cases and best practices.
|
||||
5. A community for sharing best practices across various enterprise scenarios: An open repository of application cases and best practices.
|
||||
## Quick start
|
||||
|
||||
Please ensure the following conditions are met before installing BISHENG:
|
||||
@@ -107,54 +105,3 @@ Welcome to join our discussion group
|
||||
|
||||
[](https://star-history.com/#dataelement/bisheng&Date)
|
||||
-->
|
||||
|
||||
|
||||
驗證結果
|
||||
|
||||
管理員流程:
|
||||
1. Platform → 日常模式 → 看到「可用工具 (1/20)」含 web_search,switch 可改 default
|
||||
2. 保存 → POST /api/v1/workstation/config 寫入 tools[]
|
||||
3. GET /api/v1/workstation/config 讀回 tools[] 正確
|
||||
|
||||
用戶流程(Client):
|
||||
1. 打開對話 → bsConfig.tools 加載 → AgentToolSelector 自動按 default_checked 勾選
|
||||
2. 發送消息 → payload 含 tools: [...] → 後端分派到 Agent 流
|
||||
3. SSE 返回 agent_thinking/agent_answer → useAiChatSSE 新 compat 代碼轉成 :::thinking...:::answer 給現有 renderer 消費
|
||||
4. UI 呈現:思考塊 + 最終回覆 (跟舊格式視覺等價)
|
||||
|
||||
遺留 / 已跳過
|
||||
|
||||
- PlusMenu (+ 按鈕分組 files/knowledge-space/org-kb): UX 細化,不阻塞功能
|
||||
- Agent 消息的原生渲染: 當前用兼容 mapper 把 agent_* 折回 :::thinking:::answer 格式複用舊 renderer。後續專屬的 AgentMessageBubble /
|
||||
ToolCallDisplay 屬於獨立的「渲染改造」任務
|
||||
- web_search 在 3003 環境的 API key: 環境問題,非代碼問題
|
||||
|
||||
|
||||
----
|
||||
幾個小提醒:
|
||||
|
||||
🚀 啟動命令(備忘)
|
||||
# 後端
|
||||
cd /Users/shanghang/dataelem/bisheng/src/backend
|
||||
config=$(pwd)/config_3003.yaml PYTHONPATH=./ \
|
||||
uv run uvicorn bisheng.main:app --host 0.0.0.0 --port 7860 --reload
|
||||
|
||||
# Client (端口 4001,訪問 http://localhost:4001/workspace/__dev/login 登錄)
|
||||
cd /Users/shanghang/dataelem/bisheng/src/frontend/client && pnpm start
|
||||
|
||||
# Platform (端口 3001)
|
||||
cd /Users/shanghang/dataelem/bisheng/src/frontend/platform && pnpm start
|
||||
|
||||
🐛 萬一遇到問題
|
||||
- 後端日誌:/tmp/bisheng-backend.log
|
||||
- 改 yaml 配置(如 daily_chat.agent_max_iterations):去 Platform 系統設置或直接調 POST /api/v1/config/save,redis 緩存會自動失效
|
||||
- web_search 工具空配置會被優雅降級,Agent 流仍能跑(只是不調用該工具)
|
||||
- 若 SSE 流卡住,先看 /tmp/bisheng-backend.log 看是 LLM 還是 LangGraph 問題
|
||||
|
||||
📝 主要新功能驗收清單
|
||||
1. Platform → 日常模式 → 「可用工具」+「組織知識庫」配置
|
||||
2. Client → 對話界面 → 工具下拉 + 結構化思考/工具調用渲染
|
||||
3. Client → 訂閱頻道 → 設置 → 「同步至知識空間」
|
||||
4. 後端 yaml 改 daily_chat.agent_max_iterations → 立即生效
|
||||
|
||||
|
||||
|
||||
@@ -31,27 +31,24 @@ BISHENG毕昇 是一款 <b>开源</b> LLM应用开发平台,主攻<b>企业场
|
||||
|
||||
|
||||
## 特点
|
||||
1. **具备专家级品味的通用Agent灵思**:通过 [AGL](https://github.com/dataelement/AgentGuidanceLanguage)(Agent Guidance Language)框架,将领域专家的偏好、经验与业务逻辑融入 AI 之中,让 Agent 在处理任务时能具备 「专家级理解」。
|
||||
<p align="center"><img src="https://dataelem.com/bs/Linsight.png" alt="sence1"></p>
|
||||
|
||||
2. **独具特色的[BISHENG workflow](https://dataelem.feishu.cn/wiki/R7HZwH5ZGiJUDrkHZXicA9pInif)**
|
||||
1. **独具特色的[BISHENG workflow](https://dataelem.feishu.cn/wiki/R7HZwH5ZGiJUDrkHZXicA9pInif)**
|
||||
|
||||
- 🧩 **独立、完备的应用编排框架**:可在一个框架下实现各类任务(同类产品需要被 bot 调用,或划分成 chatflow 与 workflow 来完成不同类型的任务)。
|
||||
- 🔄 **Human in the loop**:支持用户在Workflow执行的中间过程进行干预和反馈(包括多轮对话),而同类产品只能从头执行到尾。
|
||||
- 💥 **强大**:支持成环、并行、跑批、判断逻辑以及所有逻辑的任意自由组合;支持多类型输入输出、撰写报告、内容审核等复杂场景。
|
||||
- 🖐️ **易用、符合直觉**:如成环、并行、批量运行操作,在同类产品中用户需借助专门组件实现,在BISHENG中只需完全按照直觉连接成“流程图”即可(画圈成环、并列即并行、多选即批量)。
|
||||
<p align="center"><img src="https://dataelem.com/bs/bisheng_workflow.png" alt="sence0"></p>
|
||||
|
||||
3. **专为企业应用而生**:文档审核、固定版式报告生成、多智能体协作、规范制度更新差异比对、工单问答、客服辅助、会议纪要生成、简历筛选、通话记录分析、非结构化数据治理、知识挖掘、数据分析...平台支持高复杂度企业应用场景构建,支持数百个组件与数千个参数的深度调优。
|
||||
|
||||
2. **专为企业应用而生**:文档审核、固定版式报告生成、多智能体协作、规范制度更新差异比对、工单问答、客服辅助、会议纪要生成、简历筛选、通话记录分析、非结构化数据治理、知识挖掘、数据分析...平台支持高复杂度企业应用场景构建,支持数百个组件与数千个参数的深度调优。
|
||||
<p align="center"><img src="https://dataelem.com/bs/chat.png" alt="sence1"></p>
|
||||
|
||||
4. **企业级特性是应用落地的基本保障**:安全审查、基于角色的细颗粒度权限管理、用户组管理、分组流量控制、SSO/LDAP、漏洞扫描修复、高可用部署方案、监控、统计...
|
||||
3. **企业级特性是应用落地的基本保障**:安全审查、基于角色的细颗粒度权限管理、用户组管理、分组流量控制、SSO/LDAP、漏洞扫描修复、高可用部署方案、监控、统计...
|
||||
<p align="center"><img src="https://dataelem.com/bs/pro.png" alt="sence2"></p>
|
||||
|
||||
5. **高精度文档解析**:5年海量数据沉淀,高精度文档解析模型支持免费私有化部署使用,包括高精度印刷体、手写体与生僻字识别模型、表格识别模型、版式分析模型、印章模型...
|
||||
4. **高精度文档解析**:5年海量数据沉淀,高精度文档解析模型支持免费私有化部署使用,包括高精度印刷体、手写体与生僻字识别模型、表格识别模型、版式分析模型、印章模型
|
||||
<p align="center"><img src="https://dataelem.com/bs/ocr.png" alt="sence3"></p>
|
||||
|
||||
6. **大量企业场景落地最佳实践分享社区**:开放的应用案例与最佳实践库。
|
||||
5. **大量企业场景落地最佳实践分享社区**:开放的应用案例与最佳实践库。
|
||||
<p align="center"><img src="https://dataelem.com/bs/sence.png" alt="sence4"></p>
|
||||
|
||||
|
||||
|
||||
@@ -33,10 +33,7 @@ BISHENGは、エンタープライズシナリオに焦点を当てたオープ
|
||||
「畢昇(Bi Sheng)」は、活版印刷の発明者であり、人類の知識の伝播に重要な役割を果たしました。我々は、BISHENGがインテリジェントアプリケーションの広範な実装に強力なサポートを提供できることを願っています。皆さんの参加を歓迎します。
|
||||
|
||||
## 特徴
|
||||
1. **専門家級のセンスを備えた汎用エージェント「灵思」:**:[AGL](https://github.com/dataelement/AgentGuidanceLanguage)(Agent Guidance Language)フレームワークを通じて、分野の専門家の志向・経験・業務ロジックをAIに組み込み、エージェントがタスク処理時に「専門家レベルの理解」を備えられるようにします。
|
||||
<p align="center"><img src="https://dataelem.com/bs/Linsight.png" alt="sence1"></p>
|
||||
|
||||
2. **独自の特徴を持つ[BISHENG workflow](https://dataelem.feishu.cn/wiki/R7HZwH5ZGiJUDrkHZXicA9pInif)**
|
||||
1. **独自の特徴を持つ[BISHENG workflow](https://dataelem.feishu.cn/wiki/R7HZwH5ZGiJUDrkHZXicA9pInif)**
|
||||
|
||||
- 🧩 **独立性と完備性を備えたアプリケーションオーケストレーションフレームワーク**:1つのフレームワーク内でさまざまなタスクを実現可能(類似製品では、botの呼び出しが必要だったり、chatflowとworkflowに分けて異なるタスクを処理する必要があります)。
|
||||
- 🔄 **Human in the loop**:Workflowの実行途中でユーザーが介入やフィードバック(多ターン対話を含む)を行えます(類似製品では最初から最後まで一貫して実行されるのみ)。
|
||||
@@ -45,16 +42,16 @@ BISHENGは、エンタープライズシナリオに焦点を当てたオープ
|
||||
|
||||
<p align="center"><img src="https://dataelem.com/bs/bisheng_workflow.png" alt="sence0"></p>
|
||||
|
||||
3. **エンタープライズアプリケーション向けに設計**: ドキュメントレビュー、固定レイアウトレポート生成、マルチエージェント協働、ポリシー更新比較、サポートチケット支援、カスタマーサービス支援、会議議事録生成、履歴書スクリーニング、通話記録分析、非構造化データガバナンス、知識採掘、データ分析など。プラットフォームは、**高度に複雑なエンタープライズアプリケーションシナリオの構築**をサポートし、**深い最適化**を行い、数百のコンポーネントと数千のパラメータを提供します。
|
||||
2. **エンタープライズアプリケーション向けに設計**: ドキュメントレビュー、固定レイアウトレポート生成、マルチエージェント協働、ポリシー更新比較、サポートチケット支援、カスタマーサービス支援、会議議事録生成、履歴書スクリーニング、通話記録分析、非構造化データガバナンス、知識採掘、データ分析など。プラットフォームは、**高度に複雑なエンタープライズアプリケーションシナリオの構築**をサポートし、**深い最適化**を行い、数百のコンポーネントと数千のパラメータを提供します。
|
||||
<p align="center"><img src="https://dataelem.com/bs/chat.png" alt="sence1"></p>
|
||||
|
||||
4. **エンタープライズグレード**の機能は、アプリケーション実装の基本的な保証です: セキュリティレビュー、RBAC、ユーザーグループ管理、グループごとのトラフィックコントロール、SSO/LDAP、脆弱性スキャンとパッチ適用、高可用性デプロイメントソリューション、モニタリング、統計など。
|
||||
3. **エンタープライズグレード**の機能は、アプリケーション実装の基本的な保証です: セキュリティレビュー、RBAC、ユーザーグループ管理、グループごとのトラフィックコントロール、SSO/LDAP、脆弱性スキャンとパッチ適用、高可用性デプロイメントソリューション、モニタリング、統計など。
|
||||
<p align="center"><img src="https://dataelem.com/bs/pro.png" alt="sence2"></p>
|
||||
|
||||
5. **高精度ドキュメント解析**: 私たちの高精度ドキュメント解析モデルは、過去5年間にわたる大量の高品質データに基づいてトレーニングされています。高精度な印刷テキスト、手書きテキスト、稀少文字認識モデル、テーブル認識モデル、レイアウト解析モデル、印鑑モデルを含みます。プライベートに無料で展開することができます。
|
||||
4. **高精度ドキュメント解析**: 私たちの高精度ドキュメント解析モデルは、過去5年間にわたる大量の高品質データに基づいてトレーニングされています。高精度な印刷テキスト、手書きテキスト、稀少文字認識モデル、テーブル認識モデル、レイアウト解析モデル、印鑑モデルを含みます。プライベートに無料で展開することができます。
|
||||
<p align="center"><img src="https://dataelem.com/bs/ocr.png" alt="sence3"></p>
|
||||
|
||||
6. 様々なエンタープライズシナリオにおけるベストプラクティスを共有するコミュニティ: オープンなアプリケーションケースとベストプラクティスのリポジトリ。
|
||||
5. 様々なエンタープライズシナリオにおけるベストプラクティスを共有するコミュニティ: オープンなアプリケーションケースとベストプラクティスのリポジトリ。
|
||||
|
||||
|
||||
## クイックスタート
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Debug Session: knowledge-title-task-not-executed
|
||||
|
||||
**Status**: `[OPEN]`
|
||||
|
||||
**Problem**:
|
||||
- User reports that `extract_knowledge_file_title_celery` task registration status is unclear.
|
||||
- In the testing environment, the title extraction task is not being executed after file upload.
|
||||
|
||||
**Expected Behavior**:
|
||||
- After uploading a file to a knowledge space, `extract_knowledge_file_title_celery` should be enqueued and executed by a Celery worker.
|
||||
- The worker should extract the title, generate an AI alias, and then trigger `parse_knowledge_file_celery`.
|
||||
|
||||
**Actual Behavior**:
|
||||
- It is unclear whether the task is registered.
|
||||
- In testing environment, the title extraction task does not appear to run.
|
||||
|
||||
**Known Code State**:
|
||||
- `bisheng/worker/__init__.py` imports `extract_knowledge_file_title_celery` from `bisheng.worker.knowledge.file_title_worker`.
|
||||
- `bisheng/worker/main.py` uses `include=["bisheng.worker"]` for Celery app creation.
|
||||
- `knowledge_space_service.py` now imports and calls `extract_knowledge_file_title_celery.delay(...)` from `file_title_worker`.
|
||||
- `file_title_worker.py` defines the task and calls `parse_knowledge_file_celery.delay(...)` afterwards.
|
||||
|
||||
**Reproduction Steps (TBD in testing env)**:
|
||||
1. Upload a file via `POST /api/v1/knowledge/space/{id}/files`.
|
||||
2. Check Celery worker logs for `extract_knowledge_file_title_celery start file_id=...`.
|
||||
3. Check Redis broker for enqueued task messages.
|
||||
4. Check `KnowledgeFile.alias_name` after parsing completes.
|
||||
|
||||
**Hypotheses**:
|
||||
|
||||
| ID | Hypothesis | Likelihood | Effort | Expected Signal |
|
||||
|----|------------|------------|--------|-----------------|
|
||||
| A | Celery worker was not restarted after code deployment; task not registered in worker memory. | High | Low | `inspect registered` does **not** list `bisheng.worker.knowledge.file_title_worker.extract_knowledge_file_title_celery`. |
|
||||
| B | Task is registered but routed to a queue not consumed by any worker. | Medium | Low | `inspect registered` lists the task, but `inspect active/scheduled` never shows it; Redis queue length grows for a queue the worker is **not** listening to. |
|
||||
| C | Task was enqueued by API but failed during execution (exception swallowed in best-effort code). | Medium | Low | Worker log shows `extract_knowledge_file_title_celery start ...` followed by a warning/exception; `parse_knowledge_file_celery` is still enqueued. |
|
||||
| D | Task executed but skipped because preconditions not met. | Medium | Low | Worker log shows `title extraction skipped, ...` with reason (status, missing object_name, no title extracted). |
|
||||
| E | Redis broker contains stale messages or API/worker code versions are inconsistent. | Low | Medium | Old error `module 'bisheng.worker.knowledge.file_worker' has no attribute 'extract_knowledge_file_title_celery'` still appears in worker logs after API restart. |
|
||||
|
||||
**Code Changes Made (Instrumentation / Logging)**:
|
||||
|
||||
To make root-cause identification easier in the testing environment, the following logging-only changes were applied (no business logic changed):
|
||||
|
||||
1. [src/backend/bisheng/worker/knowledge/file_title_worker.py](file:///Users/xuhualiang/ai_coding/shougang/online/bisheng/src/backend/bisheng/worker/knowledge/file_title_worker.py)
|
||||
- Added `title extraction preparing ...` log showing `status`, `object_name`, `tenant_id`.
|
||||
- Added `title extraction skipped, file status=... is not WAITING` guard (precondition check).
|
||||
- Added `title extraction downloaded ... local_path=... exists=...` log.
|
||||
- Added `title extraction result ... raw_title=...` log.
|
||||
- Added `alias generation result ... alias_name=...` log.
|
||||
|
||||
2. [src/backend/bisheng/knowledge/domain/services/file_alias_name_generator.py](file:///Users/xuhualiang/ai_coding/shougang/online/bisheng/src/backend/bisheng/knowledge/domain/services/file_alias_name_generator.py)
|
||||
- Added `alias generation config ... file_alias_model_id=...` log.
|
||||
- Upgraded `file_alias_model_id not configured` from `debug` to `warning`.
|
||||
- Added `alias generation llm response ... content=...` log.
|
||||
- Added `alias generation parsed raw_alias=...` log.
|
||||
|
||||
3. [src/backend/bisheng/knowledge/domain/services/file_title_extractor.py](file:///Users/xuhualiang/ai_coding/shougang/online/bisheng/src/backend/bisheng/knowledge/domain/services/file_title_extractor.py)
|
||||
- Added `title extraction dispatch ... extension=... extractor=...` log.
|
||||
- Added `title extraction done ... title=...` log.
|
||||
|
||||
4. [src/backend/bisheng/knowledge/domain/services/file_alias_name_generator.py](file:///Users/xuhualiang/ai_coding/shougang/online/bisheng/src/backend/bisheng/knowledge/domain/services/file_alias_name_generator.py) — robustness & fallback improvements
|
||||
- Changed `_JSON_BLOCK_RE` from greedy `{.*}` to non-greedy `{.*?}` so it does not swallow trailing explanation text.
|
||||
- Added `_CODE_BLOCK_RE` to support JSON wrapped in markdown code blocks (e.g. ```json {...} ```).
|
||||
- Refactored `_parse_llm_json` to try: code block -> direct JSON -> first JSON object.
|
||||
- Added detailed logs in `_extract_alias_from_dict` and `_normalize_alias_name`.
|
||||
- **Added fallback logic**: when `file_alias_model_id` is empty, use `extract_title_model_id` instead. Only return `None` when both are missing.
|
||||
|
||||
**Verification Commands (run in testing env)**:
|
||||
|
||||
1. Check task registration:
|
||||
```bash
|
||||
cd src/backend
|
||||
celery -A bisheng.run_celery inspect registered
|
||||
```
|
||||
|
||||
2. Check active/scheduled tasks:
|
||||
```bash
|
||||
celery -A bisheng.run_celery inspect active
|
||||
celery -A bisheng.run_celery inspect scheduled
|
||||
```
|
||||
|
||||
3. Check Redis queue lengths (adjust db index if needed):
|
||||
```bash
|
||||
redis-cli -n 0 LLEN celery
|
||||
redis-cli -n 0 LLEN knowledge_celery
|
||||
```
|
||||
|
||||
4. Check worker logs for title extraction logs after uploading a file:
|
||||
- Look for `extract_knowledge_file_title_celery start file_id=...`
|
||||
- Look for `title extraction preparing ...`
|
||||
- Look for `title extraction dispatch ... extension=... extractor=...`
|
||||
- Look for `title extraction result ... raw_title=...`
|
||||
- Look for `alias generation config ... file_alias_model_id=...`
|
||||
- Look for `alias generation llm response ...`
|
||||
- Look for `file alias generated file_id=... alias_name=...`
|
||||
- Look for any traceback after the start line.
|
||||
|
||||
5. Verify database state for a recently uploaded file:
|
||||
```sql
|
||||
SELECT id, file_name, alias_name, status, object_name, parse_type
|
||||
FROM knowledge_file
|
||||
WHERE id = <file_id>;
|
||||
```
|
||||
|
||||
**Mandatory Deployment Note**: Because new files (`file_title_worker.py`, `file_title_extractor.py`, `file_alias_name_generator.py`, `gen_title.yaml`) were added, the Docker image **must be rebuilt** and the API + Celery Worker containers **must be restarted** for any of these changes to take effect in the testing environment.
|
||||
@@ -3,37 +3,10 @@
|
||||
database_url:
|
||||
"mysql+pymysql://root:gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==@mysql:3306/bisheng?charset=utf8mb4"
|
||||
|
||||
# 数据库连接池配置(每个进程分别创建同步和异步连接池)。
|
||||
# 单个连接池的并发连接上限为 pool_size + max_overflow;多进程部署时需乘以进程数。
|
||||
database_pool:
|
||||
sync:
|
||||
pool_size: 20 # 常驻连接数
|
||||
max_overflow: 10 # 高峰期允许额外创建的临时连接数
|
||||
pool_timeout: 30 # 等待空闲连接的超时秒数
|
||||
pool_recycle: 3600 # 连接回收重建周期,单位秒
|
||||
pool_pre_ping: true # 使用连接前检查连接是否有效
|
||||
async:
|
||||
pool_size: 40
|
||||
max_overflow: 20
|
||||
pool_timeout: 30
|
||||
pool_recycle: 3600
|
||||
pool_pre_ping: true
|
||||
|
||||
# 缓存配置 redis://[[username]:[password]]@localhost:6379/0
|
||||
# 如果设置了密码,需要参考MySQL密码的加密逻辑对密码进行加密。eg: redis://root:gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==@redis:6379/0
|
||||
# 普通模式:
|
||||
redis_url: "redis://redis:6379/1"
|
||||
|
||||
# 与 docker-compose 中 openfga 服务对应(容器内网络)
|
||||
openfga:
|
||||
enabled: true
|
||||
api_url: "http://openfga:8080"
|
||||
|
||||
# 网关 HMAC 推送验签(与 bisheng-gateway 的 bisheng.gateway-hmac-secret 一致)
|
||||
sso_sync:
|
||||
gateway_hmac_secret: "bisheng-local-hmac-20260422"
|
||||
signature_header: "X-Signature"
|
||||
|
||||
# 集群模式或者哨兵模式(只能选其一):
|
||||
# redis_url:
|
||||
# mode: "cluster"
|
||||
@@ -47,41 +20,16 @@ sso_sync:
|
||||
# sentinel_password: encrypt(gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==)
|
||||
# db: 1
|
||||
|
||||
# Celery broker Redis 配置
|
||||
# 单点模式(兼容现有写法):
|
||||
# celery的broken地址
|
||||
celery_redis_url: "redis://redis:6379/2"
|
||||
# 哨兵模式:
|
||||
# celery_redis_url:
|
||||
# mode: "sentinel"
|
||||
# sentinel_hosts:
|
||||
# - {"host": "redis-sentinel-1", "port": 26379}
|
||||
# - {"host": "redis-sentinel-2", "port": 26379}
|
||||
# - {"host": "redis-sentinel-3", "port": 26379}
|
||||
# sentinel_master: "mymaster"
|
||||
# sentinel_password: encrypt(gAAAAABlp5vQN7g85IfoeK8nLCb9cfVqpy9ZK9kN7b0qbAXZ4NOZT_Ef7stKJBY6PjL0dngQnCvQMdsavuGu-EE2o6Zlvv6l-Frye08DBXRR4WmL7y7EfK4=)
|
||||
# password: encrypt(gAAAAABlp5vQN7g85IfoeK8nLCb9cfVqpy9ZK9kN7b0qbAXZ4NOZT_Ef7stKJBY6PjL0dngQnCvQMdsavuGu-EE2o6Zlvv6l-Frye08DBXRR4WmL7y7EfK4=)
|
||||
# db: 10
|
||||
|
||||
|
||||
celery_task:
|
||||
# 对celery熟悉的用户可以自定义配置任务的路由,启动不同类型的worker处理不同类型的异步任务。注意工作流的执行只能在一个进程内!!!
|
||||
task_routers:
|
||||
bisheng.worker.knowledge.pdf_artifact_worker.generate_knowledge_file_pdf_celery:
|
||||
queue: knowledge_pdf_celery
|
||||
bisheng.worker.knowledge.*: # 知识库文件处理相关任务
|
||||
queue: knowledge_celery
|
||||
bisheng.worker.workflow.*: # 工作流相关任务
|
||||
queue: workflow_celery
|
||||
|
||||
knowledges:
|
||||
pdf_artifact:
|
||||
enabled: true
|
||||
queue_name: knowledge_pdf_celery
|
||||
max_retries: 3
|
||||
retry_base_seconds: 30
|
||||
retry_max_seconds: 300
|
||||
conversion_timeout_seconds: 300
|
||||
|
||||
# 知识库的milvus和es配置 支持使用 !env ${PATH} 填写环境变量的值, 若环境变量不存在则会报错
|
||||
vector_stores:
|
||||
milvus:
|
||||
@@ -103,8 +51,8 @@ object_storage:
|
||||
sharepoint: !env ${BS_MINIO_SHAREPOINT}
|
||||
access_key: !env ${BS_MINIO_ACCESS_KEY}
|
||||
secret_key: !env ${BS_MINIO_SECRET_KEY}
|
||||
public_bucket: !env ${BS_MINIO_PUBLIC_BUCKET} # 公共bucket,存储平台上一些需要持久化的文件。会设置为可公开访问
|
||||
tmp_bucket: !env ${BS_MINIO_TMP_BUCKET} # 临时bucket,会对传到此bucket内的文件设置有效期
|
||||
public_bucket: 'bisheng' # 公共bucket,存储平台上一些需要持久化的文件。会设置为可公开访问
|
||||
tmp_bucket: 'tmp-dir' # 临时bucket,会对传到此bucket内的文件设置有效期
|
||||
|
||||
environment:
|
||||
env: dev
|
||||
@@ -136,26 +84,3 @@ logger_conf:
|
||||
rotation: "00:00"
|
||||
retention: "3 Days"
|
||||
enqueue: ture
|
||||
|
||||
# 应用内消息转发配置
|
||||
in_app_message_forwarding:
|
||||
# 首钢企业微信消息推送总开关
|
||||
shougang_wechat:
|
||||
enabled: true
|
||||
api_url: "https://mobms.sggf.com.cn:30201/madp-app/madp/qywxPush-api/pushMessage"
|
||||
id: ""
|
||||
agentid: "1000053"
|
||||
key: "FveVv2PHybeI3PSFjPcrJgFGUfuF5-ZGg6ca3NJkwFk"
|
||||
sys_id: "1"
|
||||
msg_type: "text"
|
||||
timeout_seconds: 10
|
||||
max_retries: 3
|
||||
batch_size: 100
|
||||
scan_interval_seconds: 30
|
||||
retry_base_seconds: 60
|
||||
retry_max_seconds: 3600
|
||||
templates:
|
||||
qa_expert_invited: "{applicant} 邀请你回答问题「{resource}」\n{preview}"
|
||||
qa_expert_answered: "{applicant} 回答了问题「{resource}」\n{preview}"
|
||||
qa_answer_commented: "{applicant} 评论了回答「{resource}」\n{preview}"
|
||||
qa_answer_accepted: "你的回答「{resource}」被 {applicant} 采纳\n{preview}"
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
## 数据库配置
|
||||
#database_url:
|
||||
# "mysql+pymysql://root:gAAAAABlsc05yYeNuwuU_m2g7sGMMq2y2jPA72SaUnR05KFPz9uSawL82EXC3tgyFVl_Zp8kEPrfV-eB2SRyk1P_F4JbaXiGDg==@127.0.0.1:3306/bisheng?charset=utf8mb4"
|
||||
#
|
||||
## 缓存配置 redis://[[username]:[password]]@localhost:6379/0
|
||||
#redis_url: "redis://:gAAAAABlskJXI3rk_IxiOVTbg9eP1aqYxJ1I3ciGYJFmpiBJxOo0YJbC_oP60g3EXZTS1fgJJRStx2ehZuRzEkZB9I72Tjqwrw==@127.0.0.1:6379/0"
|
||||
#
|
||||
## 知识库的向量库配置 支持使用 !env ${PATH} 填写环境变量的值, 若环境变量不存在则会报错
|
||||
#vector_stores:
|
||||
# milvus:
|
||||
# connection_args: '{"host":"127.0.0.1","port":"19530","user":"","password":"","secure":false}'
|
||||
# is_partition: true
|
||||
# partition_suffix: '1'
|
||||
# elasticsearch:
|
||||
# url: 'http://127.0.0.1:9200'
|
||||
# ssl_verify: '{"basic_auth": ("elastic", "elastic")}'
|
||||
#
|
||||
#
|
||||
## 对象存储, 目前只支持minio
|
||||
#object_storage:
|
||||
# type: minio
|
||||
# minio:
|
||||
# schema: false
|
||||
# cert_check: false
|
||||
# endpoint: '127.0.0.1:9000'
|
||||
# sharepoint: '127.0.0.1:9000'
|
||||
# access_key: minioadmin
|
||||
# secret_key: minioadmin
|
||||
|
||||
# 数据库配置database_url: "mysql+pymysql://root:1234@mysql:3306/bisheng?charset=utf8mb4"database_url: "mysql+pymysql://root:1234@mysql:3306/bisheng?charset=utf8mb4"
|
||||
database_url: "mysql+pymysql://root:gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==@192.168.106.171:3306/bisheng?charset=utf8mb4"
|
||||
#database_url: "mysql+pymysql://root:1234@192.168.106.125:3306/bisheng"
|
||||
#database_url: "mysql+pymysql://root:gAAAAABlp5vQN7g85IfoeK8nLCb9cfVqpy9ZK9kN7b0qbAXZ4NOZT_Ef7stKJBY6PjL0dngQnCvQMdsavuGu-EE2o6Zlvv6l-Frye08DBXRR4WmL7y7EfK4=@192.168.106.116:3306/langflow?charset=utf8mb4"
|
||||
redis_url: "redis://192.168.106.171:6379/11"
|
||||
|
||||
# celery的broken地址
|
||||
celery_redis_url: "redis://192.168.106.171:6379/13"
|
||||
celery_task:
|
||||
# 文件解析处理的速率 多少文件并发每秒。不能高于celery的worker并发数
|
||||
knowledge_file_time_limit: null # 每秒处理10个文件 null: 表示无限制
|
||||
# 对celery熟悉的用户可以自定义配置任务的路由,启动不同类型的worker处理不同类型的异步任务,注意工作流的执行只能在一个进程内
|
||||
task_routers:
|
||||
bisheng.worker.knowledge.*: # 知识库文件处理相关任务
|
||||
queue: knowledge_celery
|
||||
bisheng.worker.workflow.*: # 工作流相关任务
|
||||
queue: workflow_celery
|
||||
|
||||
# 知识库的向量库配置 支持使用 !env ${PATH} 填写环境变量的值, 若环境变量不存在则会报错
|
||||
vector_stores:
|
||||
milvus:
|
||||
connection_args: '{"host":"192.168.106.171","port":"19530","user":"","password":"","secure":false}'
|
||||
is_partition: true
|
||||
partition_suffix: '1'
|
||||
elasticsearch:
|
||||
url: 'http://192.168.106.171:9200'
|
||||
ssl_verify: '{}'
|
||||
|
||||
|
||||
# 对象存储, 目前只支持minio
|
||||
object_storage:
|
||||
type: minio
|
||||
minio:
|
||||
schema: false
|
||||
cert_check: false
|
||||
endpoint: '192.168.106.171:9100'
|
||||
sharepoint: '192.168.106.171:9100'
|
||||
access_key: minioadmin
|
||||
secret_key: minioadmin
|
||||
public_bucket: 'bisheng' # 公共bucket,存储平台上一些需要持久化的文件。会设置为可公开访问
|
||||
tmp_bucket: 'tmp-dir' # 临时bucket,会对传到此bucket内的文件设置有效期
|
||||
|
||||
cookie_conf:
|
||||
jwt_token_expire_time: 86400 # JWT token过期时间,单位秒,默认一天
|
||||
|
||||
multi_tenant:
|
||||
enabled: false
|
||||
default_tenant_code: default
|
||||
admin_scope_ttl_seconds: 14400
|
||||
|
||||
openfga:
|
||||
api_url: "http://192.168.106.171:8080"
|
||||
# store_id: "01KPMS9N1CJF4MYV2RV4SRWE3E"
|
||||
# model_id: "01KQ4DVN3K0240TMBYDQZ4AG7B"
|
||||
timeout: 15
|
||||
|
||||
|
||||
# 可根据loguru的文档配置不同 handlers
|
||||
logger_conf:
|
||||
# 默认输出到sys.stdout的日志级别, 大于等于此级别都会输出
|
||||
level: INFO
|
||||
# 默认输出格式
|
||||
format: '<level>[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}] [{level.name} process-{process.id}-{thread.id} {name}:{line}]</level> - <level>trace={extra[trace_id]} {message}</level>'
|
||||
# 参考loguru.add()中的参数可以配置多个handler
|
||||
handlers:
|
||||
# 文件路径,支持插入一些系统环境变量,若环境变量不存在则置空。例如 HOSTNAME: 主机名。后端会处理环境变量的替换
|
||||
- sink: "../logs/bisheng/bisheng.log"
|
||||
# 日志级别
|
||||
level: INFO
|
||||
# 日志格式化函数,extra内支持trace_id
|
||||
format: '<level>[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}] [{level.name} process-{process.id}-{thread.id} {name}:{line}]</level> - <level>trace={extra[trace_id]} {message}</level>'
|
||||
# 每天的几点进行切割
|
||||
rotation: "00:00"
|
||||
retention: "3 Days"
|
||||
enqueue: ture
|
||||
|
||||
# 应用内消息转发配置
|
||||
in_app_message_forwarding:
|
||||
# 首钢企业微信消息推送总开关
|
||||
shougang_wechat:
|
||||
enabled: true
|
||||
api_url: "https://mobms.sggf.com.cn:30201/madp-app/madp/qywxPush-api/pushMessage"
|
||||
id: "ww09d8b84921e7fd3f"
|
||||
agentid: "1000008"
|
||||
key: "BH27OF9Xn5n2mxDJ6z2Tkpk01USKmoUdyjbbFXTEONg"
|
||||
sys_id: "1"
|
||||
msg_type: "text"
|
||||
timeout_seconds: 10
|
||||
max_retries: 3
|
||||
batch_size: 100
|
||||
scan_interval_seconds: 30
|
||||
retry_base_seconds: 60
|
||||
retry_max_seconds: 3600
|
||||
templates:
|
||||
qa_expert_invited: "{applicant} 邀请你回答问题「{resource}」\n{preview}"
|
||||
qa_expert_answered: "{applicant} 回答了问题「{resource}」\n{preview}"
|
||||
qa_answer_commented: "{applicant} 评论了回答「{resource}」\n{preview}"
|
||||
qa_answer_accepted: "你的回答「{resource}」被 {applicant} 采纳\n{preview}"
|
||||
@@ -1,76 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -xe
|
||||
|
||||
export PYTHONPATH="./"
|
||||
|
||||
start_mode=${1:-api}
|
||||
|
||||
start_knowledge(){
|
||||
# 知识库解析的celery worker
|
||||
celery -A bisheng.worker.main worker -l info -c 20 -P threads -Q knowledge_celery -n knowledge@%h
|
||||
}
|
||||
|
||||
start_workflow(){
|
||||
# 工作流相关的celery worker
|
||||
celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q workflow_celery -n workflow@%h
|
||||
}
|
||||
|
||||
start_beat(){
|
||||
# 定时任务调度
|
||||
celery -A bisheng.worker.main beat -l info
|
||||
}
|
||||
|
||||
start_linsight(){
|
||||
# 灵思后台任务worker
|
||||
python bisheng/linsight/worker.py --worker_num 4 --max_concurrency 5
|
||||
}
|
||||
start_default(){
|
||||
# 默认其他任务的执行worker,目前是定时统计埋点数据
|
||||
celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q celery -n celery@%h
|
||||
}
|
||||
|
||||
start_pdf(){
|
||||
# 统一 PDF 派生产物使用独立低并发 worker
|
||||
celery -A bisheng.worker.main worker -l info -c "${KNOWLEDGE_PDF_CONCURRENCY:-2}" -P threads -Q knowledge_pdf_celery -n knowledge_pdf@%h
|
||||
}
|
||||
|
||||
if [ "$start_mode" = "api" ]; then
|
||||
echo "Running database migrations..."
|
||||
alembic upgrade head || echo "WARNING: alembic migration failed, continuing startup..."
|
||||
if [ $start_mode = "api" ]; then
|
||||
echo "Starting API server..."
|
||||
uvicorn bisheng.main:app --host 0.0.0.0 --port 7860 --no-access-log --workers 1 --reload
|
||||
elif [ "$start_mode" = "knowledge" ]; then
|
||||
echo "Starting Knowledge Celery worker..."
|
||||
start_knowledge
|
||||
elif [ "$start_mode" = "workflow" ]; then
|
||||
echo "Starting Workflow Celery worker..."
|
||||
start_workflow
|
||||
elif [ "$start_mode" = "beat" ]; then
|
||||
echo "Starting Celery beat..."
|
||||
start_beat
|
||||
elif [ "$start_mode" = "default" ]; then
|
||||
echo "Starting default celery worker..."
|
||||
start_default
|
||||
elif [ "$start_mode" = "pdf" ]; then
|
||||
echo "Starting Knowledge PDF Celery worker..."
|
||||
start_pdf
|
||||
elif [ "$start_mode" = "linsight" ]; then
|
||||
echo "Starting LinSight worker..."
|
||||
start_linsight
|
||||
elif [ "$start_mode" = "worker" ]; then
|
||||
echo "Starting All worker..."
|
||||
uvicorn bisheng.main:app --host 0.0.0.0 --port 7860 --no-access-log --workers 8
|
||||
elif [ $start_mode = "worker" ]; then
|
||||
echo "Starting Celery worker..."
|
||||
# 处理知识库相关任务的worker
|
||||
start_knowledge &
|
||||
# 处理工作流相关任务的worker
|
||||
start_workflow &
|
||||
# 处理linsight相关任务的worker
|
||||
start_linsight &
|
||||
# 默认其他任务的执行worker,目前是定时统计埋点数据
|
||||
start_default &
|
||||
start_beat
|
||||
|
||||
echo "All workers started successfully."
|
||||
nohup celery -A bisheng.worker.main worker -l info -c 20 -P threads -Q knowledge_celery &
|
||||
# 工作流执行worker,只能启动一个进程来处理工作流的执行,暂不支持多进程
|
||||
celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q workflow_celery
|
||||
else
|
||||
echo "Invalid start mode. Use api、worker、knowledge、workflow、beat、default、pdf、linsight."
|
||||
echo "Invalid start mode. Use 'api' or 'celery'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================
|
||||
# BiSheng 运维管理脚本
|
||||
# 用法: ./bisheng.sh <命令> [参数]
|
||||
# =============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml"
|
||||
COMPOSE_CMD="docker compose -f ${COMPOSE_FILE}"
|
||||
|
||||
# 容器名常量(与 docker-compose.yml 对应)
|
||||
BACKEND_CONTAINER="bisheng-backend"
|
||||
WORKER_CONTAINER="bisheng-backend-worker"
|
||||
|
||||
# 所有可管理的 compose service 名称
|
||||
ALL_SERVICES=(backend backend_worker frontend mysql redis elasticsearch minio milvus etcd)
|
||||
|
||||
# ─── 颜色输出 ────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
header() { echo -e "${CYAN}${BOLD}$*${RESET}"; }
|
||||
|
||||
# ─── 帮助 ────────────────────────────────────────────────────
|
||||
usage() {
|
||||
header "═══════════════════════════════════════════════"
|
||||
header " BiSheng 运维管理脚本"
|
||||
header "═══════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo -e "${BOLD}查看日志:${RESET}"
|
||||
echo " $0 logs backend 实时跟踪 backend 日志(默认最近 200 行)"
|
||||
echo " $0 logs worker 实时跟踪 backend_worker 日志(默认最近 200 行)"
|
||||
echo " $0 logs backend -n 0 实时跟踪 backend 所有历史日志"
|
||||
echo " $0 logs worker -n 500 查看 worker 最近 500 行日志"
|
||||
echo ""
|
||||
echo -e "${BOLD}镜像版本管理:${RESET}"
|
||||
echo " $0 version 查看当前配置的镜像版本"
|
||||
echo " $0 version v3.0.0 修改 backend、worker、frontend 的版本为 v3.0.0"
|
||||
echo ""
|
||||
echo -e "${BOLD}进入容器 Shell:${RESET}"
|
||||
echo " $0 exec backend 进入 backend 容器"
|
||||
echo " $0 exec worker 进入 backend_worker 容器"
|
||||
echo ""
|
||||
echo -e "${BOLD}更新镜像并重启:${RESET}"
|
||||
echo " $0 update 拉取最新镜像并重启 backend + worker"
|
||||
echo " $0 update backend 只更新并重启 backend"
|
||||
echo " $0 update worker 只更新并重启 worker"
|
||||
echo ""
|
||||
echo -e "${BOLD}重启容器:${RESET}"
|
||||
echo " $0 restart 重启 backend + worker"
|
||||
echo " $0 restart backend 重启 backend"
|
||||
echo " $0 restart worker 重启 worker"
|
||||
echo " $0 restart frontend 重启 frontend"
|
||||
echo " $0 restart <service...> 重启任意多个 service"
|
||||
echo ""
|
||||
echo -e "${BOLD}可用 service 名称:${RESET}"
|
||||
echo " ${ALL_SERVICES[*]}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ─── service 别名解析 ─────────────────────────────────────────
|
||||
resolve_service() {
|
||||
case "$1" in
|
||||
backend) echo "backend" ;;
|
||||
worker|backend_worker) echo "backend_worker" ;;
|
||||
frontend) echo "frontend" ;;
|
||||
mysql) echo "mysql" ;;
|
||||
redis) echo "redis" ;;
|
||||
es|elasticsearch) echo "elasticsearch" ;;
|
||||
minio) echo "minio" ;;
|
||||
milvus) echo "milvus" ;;
|
||||
etcd) echo "etcd" ;;
|
||||
*) echo "$1" ;; # 原样传入,让 docker compose 自行报错
|
||||
esac
|
||||
}
|
||||
|
||||
# ─── 查看日志 ─────────────────────────────────────────────────
|
||||
cmd_logs() {
|
||||
local target="${1:-}"
|
||||
shift || true
|
||||
|
||||
local lines=200
|
||||
|
||||
# 解析可选 -n <行数>
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n)
|
||||
lines="${2:-200}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local service
|
||||
case "$target" in
|
||||
backend) service="backend" ;;
|
||||
worker|backend_worker) service="backend_worker" ;;
|
||||
*)
|
||||
error "未知目标 '${target}',请使用 backend 或 worker"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$lines" -eq 0 ]; then
|
||||
info "实时跟踪 ${service} 所有历史日志(Ctrl+C 退出)..."
|
||||
${COMPOSE_CMD} logs -f "${service}"
|
||||
else
|
||||
info "实时跟踪 ${service} 最近 ${lines} 行日志(Ctrl+C 退出)..."
|
||||
${COMPOSE_CMD} logs -f --tail="${lines}" "${service}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── 修改版本号 ───────────────────────────────────────────────
|
||||
cmd_version() {
|
||||
local new_version="${1:-}"
|
||||
|
||||
if [[ -z "$new_version" ]]; then
|
||||
info "当前 docker-compose.yml 配置的版本:"
|
||||
grep -E "image:.*dataelement/bisheng-(backend|frontend):" "$COMPOSE_FILE" | awk '{$1=$1};1'
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "正在将 backend, backend_worker, frontend 版本修改为: ${new_version}"
|
||||
|
||||
# 兼容 macOS 和 Linux 的 sed -i 用法
|
||||
if sed --version 2>/dev/null | grep -q GNU; then
|
||||
sed -i -E "s|(image: dataelement/bisheng-backend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
|
||||
sed -i -E "s|(image: dataelement/bisheng-frontend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
|
||||
else
|
||||
# macOS/BSD sed
|
||||
sed -i '' -E "s|(image: dataelement/bisheng-backend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
|
||||
sed -i '' -E "s|(image: dataelement/bisheng-frontend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
|
||||
fi
|
||||
|
||||
info "✅ 版本修改完成:"
|
||||
grep -E "image:.*dataelement/bisheng-(backend|frontend):" "$COMPOSE_FILE" | awk '{$1=$1};1'
|
||||
warn "注意:只是修改了配置文件,若要生效请执行 '$0 update'"
|
||||
}
|
||||
|
||||
# ─── 进入容器 ─────────────────────────────────────────────────
|
||||
cmd_exec() {
|
||||
local target="${1:-}"
|
||||
local container
|
||||
|
||||
case "$target" in
|
||||
backend) container="${BACKEND_CONTAINER}" ;;
|
||||
worker|backend_worker) container="${WORKER_CONTAINER}" ;;
|
||||
*)
|
||||
error "未知目标 '${target}',请使用 backend 或 worker"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
info "进入容器 ${container} ..."
|
||||
docker exec -it "${container}" /bin/bash 2>/dev/null \
|
||||
|| docker exec -it "${container}" /bin/sh
|
||||
}
|
||||
|
||||
# ─── 更新镜像并重启 ───────────────────────────────────────────
|
||||
cmd_update() {
|
||||
local targets=()
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
targets=("backend" "backend_worker" "frontend")
|
||||
else
|
||||
for t in "$@"; do
|
||||
targets+=("$(resolve_service "$t")")
|
||||
done
|
||||
fi
|
||||
|
||||
info "拉取最新镜像:${targets[*]}"
|
||||
${COMPOSE_CMD} pull "${targets[@]}"
|
||||
|
||||
info "重启服务(不重建依赖):${targets[*]}"
|
||||
${COMPOSE_CMD} up -d --no-deps "${targets[@]}"
|
||||
|
||||
info "✅ 更新完成"
|
||||
${COMPOSE_CMD} ps "${targets[@]}"
|
||||
}
|
||||
|
||||
# ─── 重启容器 ─────────────────────────────────────────────────
|
||||
cmd_restart() {
|
||||
local targets=()
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
targets=("backend" "backend_worker" "frontend")
|
||||
else
|
||||
for t in "$@"; do
|
||||
targets+=("$(resolve_service "$t")")
|
||||
done
|
||||
fi
|
||||
|
||||
info "重启服务:${targets[*]}"
|
||||
${COMPOSE_CMD} restart "${targets[@]}"
|
||||
|
||||
info "✅ 重启完成"
|
||||
${COMPOSE_CMD} ps "${targets[@]}"
|
||||
}
|
||||
|
||||
# ─── 入口 ────────────────────────────────────────────────────
|
||||
main() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local cmd="$1"; shift
|
||||
|
||||
case "$cmd" in
|
||||
logs) cmd_logs "$@" ;;
|
||||
version) cmd_version "$@" ;;
|
||||
exec) cmd_exec "$@" ;;
|
||||
update) cmd_update "$@" ;;
|
||||
restart) cmd_restart "$@" ;;
|
||||
help|-h|--help) usage ;;
|
||||
*)
|
||||
error "未知命令: ${cmd}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,59 +0,0 @@
|
||||
services:
|
||||
|
||||
backend:
|
||||
container_name: bisheng-backend
|
||||
#image: cr.dataelem.com/dataelement/bisheng-backend:2.5.0-sg
|
||||
build:
|
||||
context: ../src/backend
|
||||
dockerfile: Dockerfile
|
||||
image: bisheng-backend:local
|
||||
ports:
|
||||
- "7860:7860"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
BS_SSO_SYNC__GATEWAY_HMAC_SECRET: "bisheng-local-hmac-20260422"
|
||||
BS_MILVUS_CONNECTION_ARGS: '{"host":"milvus","port":"19530","user":"","password":"","secure":false}'
|
||||
BS_MILVUS_IS_PARTITION: 'true'
|
||||
BS_MILVUS_PARTITION_SUFFIX: '1'
|
||||
BS_ELASTICSEARCH_URL: 'http://elasticsearch:9200'
|
||||
BS_ELASTICSEARCH_SSL_VERIFY: '{}' # 可根据自己部署的密码进行配置 '{"basic_auth": ("elastic", "elastic")}'
|
||||
BS_MINIO_SCHEMA: 'false'
|
||||
BS_MINIO_CERT_CHECK: 'false'
|
||||
BS_MINIO_ENDPOINT: 'minio:9000'
|
||||
BS_MINIO_SHAREPOINT: 'minio:9000'
|
||||
BS_MINIO_ACCESS_KEY: 'minioadmin'
|
||||
BS_MINIO_SECRET_KEY: 'minioadmin'
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config_dev.yaml:/app/bisheng/config.yaml
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/entrypoint.sh:/app/entrypoint.sh
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/bisheng:/app/data
|
||||
- ../src/backend:/app
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
command: sh entrypoint.sh api # 启动api服务
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
|
||||
start_period: 30s
|
||||
interval: 90s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
container_name: bisheng-frontend
|
||||
# 注意:此处为 Hub 的 latest,未必与自建镜像 backend:v2.5-local 同版本;界面偏旧请 pull 最新或按源码 build(见 AGENTS / 部署文档)
|
||||
#image: cr.dataelem.com/dataelement/bisheng-frontend:2.5.0-sg
|
||||
build:
|
||||
context: ../src/frontend
|
||||
dockerfile: Dockerfile
|
||||
image: bisheng-frontend:local
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/nginx/conf.d:/etc/nginx/conf.d
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- backend
|
||||
@@ -1,8 +1,7 @@
|
||||
services:
|
||||
ft_server:
|
||||
container_name: bisheng-ft-server
|
||||
image: dataelement/bisheng-ft:v0.5.0
|
||||
shm_size: "4g"
|
||||
image: dataelement/bisheng-ft:v0.2.0
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
services:
|
||||
office:
|
||||
container_name: bisheng-office
|
||||
image: onlyoffice/documentserver:7.1.1
|
||||
ports:
|
||||
- "8701:80"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
JWT_ENABLED: "false"
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/office/bisheng:/var/www/onlyoffice/documentserver/sdkjs-plugins/bisheng
|
||||
command: bash -c "supervisorctl restart all"
|
||||
restart: on-failure
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
mysql:
|
||||
container_name: bisheng-mysql
|
||||
image: mysql:8.0
|
||||
|
||||
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
@@ -13,54 +13,13 @@ services:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/mysql/conf/my.cnf:/etc/mysql/my.cnf
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/mysql/data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysql -u root -p$$MYSQL_ROOT_PASSWORD -e 'CREATE DATABASE IF NOT EXISTS openfga;'"]
|
||||
test: ["CMD-SHELL", "exit | mysql -u root -p$$MYSQL_ROOT_PASSWORD"]
|
||||
start_period: 30s
|
||||
interval: 20s
|
||||
timeout: 10s
|
||||
retries: 4
|
||||
restart: on-failure
|
||||
|
||||
openfga-migrate:
|
||||
container_name: bisheng-openfga-migrate
|
||||
image: openfga/openfga:latest
|
||||
command: migrate
|
||||
environment:
|
||||
OPENFGA_DATASTORE_ENGINE: mysql
|
||||
OPENFGA_DATASTORE_URI: "root:1234@tcp(mysql:3306)/openfga?parseTime=true"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
openfga:
|
||||
container_name: bisheng-openfga
|
||||
image: openfga/openfga:latest
|
||||
command: run
|
||||
environment:
|
||||
OPENFGA_DATASTORE_ENGINE: mysql
|
||||
OPENFGA_DATASTORE_URI: "root:1234@tcp(mysql:3306)/openfga?parseTime=true"
|
||||
OPENFGA_LOG_FORMAT: json
|
||||
OPENFGA_PLAYGROUND_ENABLED: "true"
|
||||
OPENFGA_CHECK_QUERY_CACHE_ENABLED: "true"
|
||||
OPENFGA_CHECK_QUERY_CACHE_TTL: 30s
|
||||
OPENFGA_CHECK_ITERATOR_CACHE_ENABLED: "true"
|
||||
OPENFGA_CHECK_ITERATOR_CACHE_TTL: 30s
|
||||
OPENFGA_CHECK_ITERATOR_CACHE_MAX_RESULTS: 10000
|
||||
OPENFGA_CACHE_CONTROLLER_ENABLED: "true"
|
||||
OPENFGA_CACHE_CONTROLLER_TTL: 10s
|
||||
OPENFGA_DATASTORE_MAX_OPEN_CONNS: 80
|
||||
OPENFGA_DATASTORE_MAX_IDLE_CONNS: 40
|
||||
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8081:8081"
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
openfga-migrate:
|
||||
condition: service_completed_successfully
|
||||
# 不在 compose 里写 HEALTHCHECK:openfga 官方镜像为 distroless,无 /bin/sh、无 wget,
|
||||
# CMD-SHELL 会永久 unhealthy。未定义时 Compose 将 ``service_healthy`` 视为已启动即可。
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
container_name: bisheng-redis
|
||||
image: redis:7.0.4
|
||||
@@ -79,15 +38,26 @@ services:
|
||||
retries: 3
|
||||
restart: on-failure
|
||||
|
||||
office:
|
||||
container_name: bisheng-office
|
||||
image: onlyoffice/documentserver:7.1.1
|
||||
ports:
|
||||
- "8701:80"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
JWT_ENABLED: "false"
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/office/bisheng:/var/www/onlyoffice/documentserver/sdkjs-plugins/bisheng
|
||||
command: bash -c "supervisorctl restart all"
|
||||
restart: on-failure
|
||||
|
||||
backend:
|
||||
container_name: bisheng-backend
|
||||
image: cr.dataelem.com/dataelement/bisheng-backend:2.5.0-sg
|
||||
image: dataelement/bisheng-backend:v1.3.1
|
||||
ports:
|
||||
- "7860:7860"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
BISHENG_DASHBOARD_PRO: "true"
|
||||
BS_SSO_SYNC__GATEWAY_HMAC_SECRET: "bisheng-local-hmac-20260422"
|
||||
BS_MILVUS_CONNECTION_ARGS: '{"host":"milvus","port":"19530","user":"","password":"","secure":false}'
|
||||
BS_MILVUS_IS_PARTITION: 'true'
|
||||
BS_MILVUS_PARTITION_SUFFIX: '1'
|
||||
@@ -99,8 +69,6 @@ services:
|
||||
BS_MINIO_SHAREPOINT: 'minio:9000'
|
||||
BS_MINIO_ACCESS_KEY: 'minioadmin'
|
||||
BS_MINIO_SECRET_KEY: 'minioadmin'
|
||||
BS_MINIO_PUBLIC_BUCKET: 'bisheng'
|
||||
BS_MINIO_TMP_BUCKET: 'tmp-dir'
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config.yaml:/app/bisheng/config.yaml
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/entrypoint.sh:/app/entrypoint.sh
|
||||
@@ -120,16 +88,14 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
# openfga 官方镜像无 HEALTHCHECK,Compose v5 无法用 service_healthy
|
||||
openfga:
|
||||
office:
|
||||
condition: service_started
|
||||
|
||||
|
||||
backend_worker:
|
||||
container_name: bisheng-backend-worker
|
||||
image: cr.dataelem.com/dataelement/bisheng-backend:2.5.0-sg
|
||||
image: dataelement/bisheng-backend:v1.3.1
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
BS_SSO_SYNC__GATEWAY_HMAC_SECRET: "bisheng-local-hmac-20260422"
|
||||
BS_MILVUS_CONNECTION_ARGS: '{"host":"milvus","port":"19530","user":"","password":"","secure":false}'
|
||||
BS_MILVUS_IS_PARTITION: 'true'
|
||||
BS_MILVUS_PARTITION_SUFFIX: '1'
|
||||
@@ -141,8 +107,6 @@ services:
|
||||
BS_MINIO_SHAREPOINT: 'minio:9000'
|
||||
BS_MINIO_ACCESS_KEY: 'minioadmin'
|
||||
BS_MINIO_SECRET_KEY: 'minioadmin'
|
||||
BS_MINIO_PUBLIC_BUCKET: 'bisheng'
|
||||
BS_MINIO_TMP_BUCKET: 'tmp-dir'
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config.yaml:/app/bisheng/config.yaml
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/entrypoint.sh:/app/entrypoint.sh
|
||||
@@ -156,12 +120,12 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
openfga:
|
||||
office:
|
||||
condition: service_started
|
||||
|
||||
frontend:
|
||||
container_name: bisheng-frontend
|
||||
image: cr.dataelem.com/dataelement/bisheng-frontend:2.5.0-sg
|
||||
image: dataelement/bisheng-frontend:v1.3.1
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
@@ -175,7 +139,7 @@ services:
|
||||
|
||||
elasticsearch:
|
||||
container_name: bisheng-es
|
||||
image: docker.io/bitnamilegacy/elasticsearch:8.12.0
|
||||
image: docker.io/bitnami/elasticsearch:8.12.0
|
||||
user: root
|
||||
ports:
|
||||
- "9200:9200"
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
# 本地 bisheng + Gateway,Docker 仅中间件
|
||||
|
||||
## 1. 启动中间件
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File docker/local-dev/start-middleware.ps1
|
||||
```
|
||||
|
||||
Linux/macOS:
|
||||
|
||||
```bash
|
||||
bash docker/local-dev/start-middleware.sh
|
||||
```
|
||||
|
||||
脚本会:停止 `bisheng-backend` / `bisheng-backend-worker` / `bisheng-frontend` 容器(若存在),再启动 MySQL、OpenFGA、Redis、ES、Milvus 依赖栈,并初始化 `bisheng_gateway` 库表。
|
||||
|
||||
## 1b. 清空 bisheng 业务库并同步清空 OpenFGA / Redis(避免 FGA 与 user_id 不一致)
|
||||
|
||||
仅执行 `reset-full-db-keep-superadmin.sql` **不会**清空 OpenFGA 使用的独立 MySQL 库 `openfga`,历史上会出现「新用户占用旧 user_id,继承旧权限元组」的问题。
|
||||
|
||||
**推荐一键顺序**(先停本机 uvicorn / Celery,再执行):
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File docker/local-dev/reset-full-local-stack.ps1
|
||||
```
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
bash docker/local-dev/reset-full-local-stack.sh
|
||||
```
|
||||
|
||||
脚本会:`FLUSHDB` Redis `1,2,3`(与默认 `config.yaml` 的 bisheng/celery 及 Gateway 常用库一致)→ `DROP/CREATE` MySQL `openfga` → `docker compose … openfga-migrate` → 导入 `reset-full-db-keep-superadmin.sql`。
|
||||
|
||||
**未包含**:Elasticsearch 索引、Milvus 集合、MinIO 桶内对象;若需「知识库也干净」,请另行删除对应索引/集合/桶,或使用 compose 卷重建(会丢全部 Docker 数据)。
|
||||
|
||||
## 2. 对齐关系
|
||||
|
||||
| 组件 | 地址 | 说明 |
|
||||
|------|------|------|
|
||||
| bisheng 后端 | `http://127.0.0.1:7860` | 源码启动,读 `src/backend/bisheng/config.yaml`(已指向本机中间件) |
|
||||
| Gateway | `http://127.0.0.1:8180` | `bisheng-gateway` 仓库,`application-local.yml` |
|
||||
| Gateway ↔ bisheng | `bisheng.bisheng-api-url` | 已设为 `7860` |
|
||||
| Gateway ↔ Redis | `spring.data.redis` | `127.0.0.1:6379`,**database: 3**(与 bisheng 用的 db 1/2 区分) |
|
||||
| Gateway ↔ MySQL | `bisheng_gateway` | root 密码与 compose 一致 `1234` |
|
||||
| 组织同步 HMAC | `sso_sync.gateway_hmac_secret` / Gateway `bisheng.gateway-hmac-secret` | 两处必须一致(示例:`bisheng-local-hmac-20260422`);Gateway 使用 F014 `POST /api/v1/departments/sync` 推部门(bisheng 已无 `/api/v2/group/sync`) |
|
||||
| 管理端 Vite | `VITE_PROXY_TARGET` | 走 Gateway 时设为 `http://127.0.0.1:8180` |
|
||||
| 工作台 Vite | `VITE_DEV_API_TARGET` | 走 Gateway 时设为 `http://127.0.0.1:8180` |
|
||||
|
||||
## 3. 启动顺序建议
|
||||
|
||||
1. 中间件脚本(含 OpenFGA migrate)
|
||||
2. bisheng API:`cd src/backend`,`$env:config = "config.yaml"`(勿用 `bisheng\config.yaml`)
|
||||
3. Celery worker / beat(与 `AGENTS.md` 一致,否则异步任务不跑)
|
||||
4. Gateway:`cd bisheng-gateway`,`mvn package -DskipTests` 后 `java -jar ... --spring.profiles.active=local --server.port=8180`
|
||||
5. Platform:`src/frontend/platform`,配置好 `.env.development.local` 后 `npm start`
|
||||
6. Client:`src/frontend/client`,将 `src/frontend/env.development.gateway.example` 复制为 `src/frontend/.env.development.local` 后 `npm start`
|
||||
|
||||
示例环境变量见 `src/frontend/platform/env.development.gateway.example` 与 `src/frontend/env.development.gateway.example`。
|
||||
|
||||
## 4. Gateway 仓库
|
||||
|
||||
默认与 bisheng 同级目录:`../bisheng-gateway`(已由脚本/说明克隆时放在该路径)。若路径不同,自行调整启动命令中的目录。
|
||||
|
||||
构建需要 **JDK 17** 与 **Maven**(`mvn` 在 PATH 中)。示例:
|
||||
|
||||
```bash
|
||||
cd ../bisheng-gateway
|
||||
mvn -DskipTests package
|
||||
java -jar target/gateway-0.0.1-SNAPSHOT.jar --spring.profiles.active=local --server.port=8180
|
||||
```
|
||||
|
||||
`application-local.yml` 在 gateway 仓库内,覆盖路由、MySQL、Redis 与 `bisheng-api-url`,与 Docker 中间件及本机 bisheng 端口一致。
|
||||
|
||||
启用商业版 SSO 等需在后端启动前设置 `BISHENG_PRO=true`,见 `AGENTS.md`。
|
||||
|
||||
## 5. bisheng 后端(Windows,已开启 SSO)
|
||||
|
||||
一键启动(需已 `uv sync`、中间件已起):
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File docker/local-dev/start-backend-sso.ps1
|
||||
```
|
||||
|
||||
或手动:
|
||||
|
||||
```powershell
|
||||
cd src/backend
|
||||
# 须为「相对 bisheng 包目录」的文件名,勿写成 bisheng\config.yaml(会拼成双 bisheng 路径)
|
||||
$env:config = "config.yaml"
|
||||
$env:BISHENG_PRO = "true"
|
||||
$env:BS_SSO_SYNC__GATEWAY_HMAC_SECRET = "bisheng-local-hmac-20260422"
|
||||
.\.venv\Scripts\python.exe -m uvicorn bisheng.main:app --host 0.0.0.0 --port 7860 --workers 1 --no-access-log
|
||||
```
|
||||
|
||||
Linux/macOS:`export config=config.yaml`(不要用 `bisheng/config.yaml`)。
|
||||
|
||||
## 6. Gateway → bisheng 组织 / 部门同步(可测)
|
||||
|
||||
bisheng 侧为 **F014**:`POST /api/v1/departments/sync` + 请求头 `X-Signature`(算法见 `bisheng/sso_sync/domain/services/hmac_auth.py`)。
|
||||
|
||||
### 一键拉起(中间件 + API + Celery + Gateway + 冒烟)
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File docker/local-dev/start-full-stack.ps1
|
||||
```
|
||||
|
||||
### 方式 A:Gateway 代签推送(推荐)
|
||||
|
||||
- **企业微信拉树并推送(部门 + 已激活成员)**(需 `application.yml` / `application-local.yml` 里企业微信 `wxoauth` 为真实可用凭证):
|
||||
`GET http://127.0.0.1:8180/api/group/test`
|
||||
Gateway 将部门树 + 成员打成一个请求,经 HMAC 调用 bisheng **`POST /api/v1/internal/sso/gateway-wecom-org-sync`**(内部先 `departments/sync` 再逐人 `login-sync`,**`org_sync_log` 只落一行**;企微「部门负责人」依赖 **`user/get` 返回的 `is_leader_in_dept`**,Gateway 已对每位激活成员补拉详情后再组 `department_admin_external_ids`,并始终下发该数组(可为空)以便 bisheng 对账 FGA)。
|
||||
- **自定义 JSON**(与 bisheng `DepartmentsSyncRequest` 同结构):
|
||||
`POST http://127.0.0.1:8180/api/group/sso-departments-raw`
|
||||
Body 示例:`{"upsert":[{"external_id":"d1","name":"研发","parent_external_id":null,"sort":0,"ts":1710000000}],"remove":[],"source_ts":1710000000}`
|
||||
|
||||
### 方式 B:本机脚本自签(不经过 Gateway 亦可)
|
||||
|
||||
```powershell
|
||||
cd src/backend
|
||||
.\.venv\Scripts\python.exe ..\..\scripts\dev\gateway_hmac_org_sync_smoke.py --base http://127.0.0.1:8180
|
||||
.\.venv\Scripts\python.exe ..\..\scripts\dev\gateway_hmac_org_sync_smoke.py --base http://127.0.0.1:7860
|
||||
```
|
||||
|
||||
管理端查看同步日志(需登录):`GET /api/v1/org-sync/gateway-logs`(经 Gateway 代理为 `/api/v1/...`)。
|
||||
|
||||
**成员 / 日志**:`GET /api/group/test` 走合并接口;单点登录仍可用 `POST /api/v1/internal/sso/login-sync`(HMAC,每条单独写 `org_sync_log`)。
|
||||
@@ -1,69 +0,0 @@
|
||||
CREATE DATABASE IF NOT EXISTS `bisheng_gateway` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
USE `bisheng_gateway`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gt_group_resource`
|
||||
(
|
||||
`id` int auto_increment primary key,
|
||||
`group_id` int not null,
|
||||
`resource_id` varchar(256) not null,
|
||||
`resource_limit` int default 0 not null,
|
||||
`resource_type` tinyint not null
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gt_sensitive_words`
|
||||
(
|
||||
`id` int auto_increment primary key,
|
||||
`resource_id` varchar(256) not null,
|
||||
`resource_type` tinyint null,
|
||||
`auto_words` text null,
|
||||
`words` text null,
|
||||
`words_types` varchar(32) null,
|
||||
`is_check` tinyint default 0 not null,
|
||||
`create_time` datetime not null,
|
||||
`update_time` datetime not null,
|
||||
`logic_delete` tinyint default 0 not null,
|
||||
`auto_reply` varchar(128) null
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gt_user_group`
|
||||
(
|
||||
`id` int auto_increment primary key,
|
||||
`group_name` varchar(256) not null,
|
||||
`admin_user` varchar(512) null,
|
||||
`admin_user_id` varchar(512) null,
|
||||
`group_limit` int default 0 not null,
|
||||
`create_time` datetime not null,
|
||||
`update_time` datetime not null,
|
||||
`logic_delete` tinyint default 0 not null
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gt_block_record`
|
||||
(
|
||||
`id` int auto_increment primary key,
|
||||
`chat_id` varchar(64) null,
|
||||
`user_input` text null,
|
||||
`system_out` text null,
|
||||
`block_words` text null,
|
||||
`create_time` datetime not null,
|
||||
`update_time` datetime not null,
|
||||
`resource_id` varchar(64) null
|
||||
);
|
||||
|
||||
-- v2.5 部门维度限流(与 bisheng-gateway db/update_v_2.5.sql 对齐)
|
||||
CREATE TABLE IF NOT EXISTS `gt_department` (
|
||||
`id` INT NOT NULL PRIMARY KEY COMMENT 'Matches department.id in bisheng',
|
||||
`dept_name` VARCHAR(256) NOT NULL,
|
||||
`dept_limit` INT DEFAULT 0 NOT NULL COMMENT '0 = unlimited',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT '部门流量控制';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gt_department_resource` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`department_id` INT NOT NULL,
|
||||
`resource_id` VARCHAR(256) NOT NULL COMMENT 'assistant/skill/workflow ID',
|
||||
`resource_limit` INT DEFAULT 0 NOT NULL COMMENT '0 = unlimited',
|
||||
`resource_type` TINYINT NOT NULL COMMENT '2=skill, 3=assistant, 5=workflow',
|
||||
INDEX `idx_dept_resource` (`department_id`, `resource_id`)
|
||||
) COMMENT '部门-资源流量限制';
|
||||
@@ -1,32 +0,0 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
在 Docker 容器 bisheng-mysql 中对库 bisheng 执行 reset-org-to-superadmin.sql。
|
||||
|
||||
.PARAMETER Container
|
||||
MySQL 容器名,默认 bisheng-mysql。
|
||||
|
||||
.PARAMETER Password
|
||||
root 密码,默认 1234(与本地 docker-compose 一致)。
|
||||
#>
|
||||
param(
|
||||
[string] $Container = "bisheng-mysql",
|
||||
[string] $Password = "1234"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$sql = Join-Path $PSScriptRoot "reset-org-to-superadmin.sql"
|
||||
|
||||
if (-not (Test-Path $sql)) {
|
||||
Write-Error "Missing SQL file: $sql"
|
||||
}
|
||||
|
||||
Write-Host "Running reset on container '$Container' database bisheng ..." -ForegroundColor Cyan
|
||||
Get-Content -LiteralPath $sql -Encoding UTF8 | docker exec -i $Container mysql -uroot "-p$Password" bisheng
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "mysql exited with code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
Write-Host "Done. Check kept_user_id / remaining_users in mysql output above." -ForegroundColor Green
|
||||
@@ -1,69 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- bisheng 库:清空「组织同步 + 部门 + 成员」相关数据,仅保留一名超级管理员
|
||||
--
|
||||
-- 保留用户规则(与产品约定一致):
|
||||
-- 1) 优先保留「已绑定 role_id = 1(AdminRole)」中 user_id 最小的一名用户;
|
||||
-- 2) 若无人拥有 role_id=1,则保留 user 表中 user_id 最小的一行(兜底)。
|
||||
--
|
||||
-- 会删除 / 清空:
|
||||
-- org_sync_log / org_sync_config、department*、user_department、
|
||||
-- 除保留用户外的 user 及依赖外键的子表(channel、linsight_*、usergroup、userrole)、
|
||||
-- user_link(非保留用户)、user_tenant(全表后仅为保留用户重建一条根租户记录)、invitecode 全表。
|
||||
--
|
||||
-- 不会动:tenant、role、知识库 flow 等业务主数据(仅去掉部门挂载关系)。
|
||||
--
|
||||
-- 执行前务必备份:
|
||||
-- docker exec bisheng-mysql mysqldump -uroot -p1234 bisheng > backup-bisheng.sql
|
||||
--
|
||||
-- 执行示例(仓库根目录):
|
||||
-- docker exec -i bisheng-mysql mysql -uroot -p1234 bisheng < docker/local-dev/reset-org-to-superadmin.sql
|
||||
--
|
||||
-- OpenFGA / Redis 中的权限元组或 org_sync 锁不会由此脚本清理;若你启用了 FGA,
|
||||
-- 清理后请在文档环境执行一次全量同步或按运维流程重建元组。
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET @keep := (
|
||||
SELECT COALESCE(
|
||||
(SELECT MIN(ur.user_id) FROM userrole ur WHERE ur.role_id = 1),
|
||||
(SELECT MIN(u.user_id) FROM `user` u)
|
||||
)
|
||||
);
|
||||
|
||||
-- 空库保护:没有用户则只清组织表,不删 user
|
||||
DELETE FROM org_sync_log;
|
||||
DELETE FROM org_sync_config;
|
||||
|
||||
DELETE FROM department_knowledge_space;
|
||||
DELETE FROM user_department;
|
||||
DELETE FROM `department`;
|
||||
|
||||
DELETE FROM invitecode;
|
||||
|
||||
DELETE FROM user_link WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
DELETE FROM user_tenant WHERE @keep IS NOT NULL;
|
||||
|
||||
DELETE FROM channel WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
DELETE FROM linsight_session_version WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
DELETE FROM linsight_sop WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
DELETE FROM linsight_sop_record WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
|
||||
DELETE FROM usergroup WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
DELETE FROM userrole WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
|
||||
DELETE FROM `user` WHERE @keep IS NOT NULL AND user_id <> @keep;
|
||||
|
||||
INSERT INTO user_tenant (user_id, tenant_id, is_default, status, is_active)
|
||||
SELECT @keep, 1, 1, 'active', 1
|
||||
FROM DUAL
|
||||
WHERE @keep IS NOT NULL;
|
||||
|
||||
DELETE FROM userrole WHERE @keep IS NOT NULL AND user_id = @keep AND role_id <> 1;
|
||||
|
||||
INSERT INTO userrole (user_id, role_id, tenant_id)
|
||||
SELECT @keep, 1, 1
|
||||
FROM DUAL
|
||||
WHERE @keep IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM userrole ur WHERE ur.user_id = @keep AND ur.role_id = 1);
|
||||
|
||||
SELECT @keep AS kept_user_id, (SELECT COUNT(*) FROM `user`) AS remaining_users;
|
||||
@@ -1,8 +0,0 @@
|
||||
# 在 bisheng 仓库根目录执行:本机 bisheng API + 开启 SSO(BISHENG_PRO)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$backend = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) "src\backend"
|
||||
Set-Location $backend
|
||||
$env:config = "config.yaml"
|
||||
$env:BISHENG_PRO = "true"
|
||||
$env:BS_SSO_SYNC__GATEWAY_HMAC_SECRET = "bisheng-local-hmac-20260422"
|
||||
& ".\.venv\Scripts\python.exe" -m uvicorn bisheng.main:app --host 0.0.0.0 --port 7860 --workers 1 --no-access-log
|
||||
@@ -1,79 +0,0 @@
|
||||
# 一键:Docker 中间件 + bisheng API (BISHENG_PRO) + Celery worker + Gateway(各开新窗口)
|
||||
# 在 bisheng 仓库根目录执行: powershell -ExecutionPolicy Bypass -File docker/local-dev/start-full-stack.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
$mvnBin = "$env:USERPROFILE\tools\apache-maven-3.9.9\bin"
|
||||
if (Test-Path $mvnBin) { $env:Path = "$mvnBin;$env:Path" }
|
||||
|
||||
$backend = Join-Path $repoRoot "src\backend"
|
||||
$gateway = Join-Path (Split-Path $repoRoot -Parent) "bisheng-gateway"
|
||||
if (-not (Test-Path (Join-Path $gateway "pom.xml"))) {
|
||||
$gateway = Join-Path $repoRoot "..\bisheng-gateway"
|
||||
}
|
||||
# Common Windows layout: %USERPROFILE%\Desktop\gateway\bisheng-gateway
|
||||
$gwDesktop = Join-Path $env:USERPROFILE "Desktop\gateway\bisheng-gateway"
|
||||
if (-not (Test-Path (Join-Path $gateway "pom.xml")) -and (Test-Path (Join-Path $gwDesktop "pom.xml"))) {
|
||||
$gateway = $gwDesktop
|
||||
}
|
||||
|
||||
$javaExe = "java"
|
||||
try { $javaExe = (Get-Command java -ErrorAction Stop).Source } catch { }
|
||||
|
||||
Write-Host "==> Middleware"
|
||||
& "$PSScriptRoot\start-middleware.ps1"
|
||||
|
||||
if (Test-Path (Join-Path $gateway "pom.xml")) {
|
||||
Write-Host "==> Maven package gateway"
|
||||
Push-Location $gateway
|
||||
mvn -q -DskipTests package
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$py = Join-Path $backend ".venv\Scripts\python.exe"
|
||||
if (-not (Test-Path $py)) {
|
||||
Write-Error "Missing $py — run: cd src/backend ; uv sync"
|
||||
}
|
||||
|
||||
# 子进程继承当前会话环境(避免 Start-Process -Command 长字符串在部分环境下 ArgumentList 校验失败)
|
||||
$env:config = "config.yaml"
|
||||
$env:BISHENG_PRO = "true"
|
||||
$env:BS_SSO_SYNC__GATEWAY_HMAC_SECRET = "bisheng-local-hmac-20260422"
|
||||
|
||||
Write-Host "==> Start bisheng API (new window)"
|
||||
Start-Process -FilePath $py -WorkingDirectory $backend -ArgumentList @(
|
||||
"-m", "uvicorn", "bisheng.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--no-access-log"
|
||||
) -WindowStyle Normal
|
||||
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
$celeryNode = "dev@{0}" -f $env:COMPUTERNAME
|
||||
Write-Host "==> Start Celery worker (new window)"
|
||||
Start-Process -FilePath $py -WorkingDirectory $backend -ArgumentList @(
|
||||
"-m", "celery", "-A", "bisheng.worker.main", "worker", "-l", "info", "-c", "4", "-P", "threads",
|
||||
"-Q", "knowledge_celery,workflow_celery,celery", "-n", $celeryNode
|
||||
) -WindowStyle Normal
|
||||
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$jar = Join-Path $gateway "target\gateway-0.0.1-SNAPSHOT.jar"
|
||||
if (Test-Path $jar) {
|
||||
Write-Host "==> Start Gateway (new window)"
|
||||
Start-Process -FilePath $javaExe -WorkingDirectory $gateway -ArgumentList @(
|
||||
"-jar", ".\target\gateway-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=local", "--server.port=8180"
|
||||
) -WindowStyle Normal
|
||||
} else {
|
||||
Write-Warning "Gateway jar not found: $jar"
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 15
|
||||
|
||||
Write-Host "==> HMAC smoke (via Gateway :8180)"
|
||||
& $py (Join-Path $repoRoot "scripts\dev\gateway_hmac_org_sync_smoke.py") --base http://127.0.0.1:8180
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "bisheng: http://127.0.0.1:7860/health"
|
||||
Write-Host "Gateway: http://127.0.0.1:8180/api/oauth2/list"
|
||||
Write-Host "企业微信部门树 -> bisheng (F014): GET http://127.0.0.1:8180/api/group/test"
|
||||
Write-Host "自定义 JSON -> bisheng: POST http://127.0.0.1:8180/api/group/sso-departments-raw"
|
||||
Write-Host "Python 自签: & '$py' '$repoRoot\scripts\dev\gateway_hmac_org_sync_smoke.py' --base http://127.0.0.1:8180"
|
||||
@@ -1,20 +0,0 @@
|
||||
# 仅启动 Docker 中间件(不启 bisheng-backend / worker / frontend 容器)
|
||||
# 在仓库根目录执行: powershell -File docker/local-dev/start-middleware.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$dockerDir = Join-Path $root "docker"
|
||||
Set-Location $dockerDir
|
||||
|
||||
Write-Host "Stopping bisheng app containers if present..."
|
||||
docker stop bisheng-backend bisheng-backend-worker bisheng-frontend 2>$null
|
||||
|
||||
Write-Host "Starting middleware (mysql -> openfga -> redis -> es -> milvus stack)..."
|
||||
docker compose -f docker-compose.yml -p bisheng up -d mysql openfga-migrate openfga redis elasticsearch etcd minio milvus
|
||||
|
||||
Write-Host "Initializing bisheng_gateway schema (idempotent)..."
|
||||
$sqlPath = Join-Path $PSScriptRoot "init-gateway-db.sql"
|
||||
# PowerShell 管道会损坏 UTF-8/中文注释,用 docker cp 再执行
|
||||
docker cp $sqlPath bisheng-mysql:/tmp/init-gateway-db.sql
|
||||
docker exec bisheng-mysql sh -c "mysql -uroot -p1234 < /tmp/init-gateway-db.sql"
|
||||
|
||||
Write-Host "Done. MySQL :3306, Redis :6379, OpenFGA :8080, ES :9200, MinIO :9100, Milvus :19530"
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT/docker"
|
||||
|
||||
echo "Stopping bisheng app containers if present..."
|
||||
docker stop bisheng-backend bisheng-backend-worker bisheng-frontend 2>/dev/null || true
|
||||
|
||||
echo "Starting middleware..."
|
||||
docker compose -f docker-compose.yml -p bisheng up -d mysql openfga-migrate openfga redis elasticsearch etcd minio milvus
|
||||
|
||||
echo "Initializing bisheng_gateway schema (idempotent)..."
|
||||
docker exec -i bisheng-mysql mysql -uroot -p1234 < "$ROOT/docker/local-dev/init-gateway-db.sql"
|
||||
|
||||
echo "Done."
|
||||
@@ -5,7 +5,6 @@ default-character-set=utf8mb4
|
||||
default-character-set=utf8mb4
|
||||
|
||||
[mysqld]
|
||||
max_connections=1000
|
||||
init_connect='SET collation_connection = utf8mb4_unicode_ci, NAMES utf8mb4'
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
|
||||
@@ -6,10 +6,6 @@ map $http_upgrade $connection_upgrade {
|
||||
}
|
||||
|
||||
|
||||
upstream backend_server {
|
||||
server bisheng-backend:7860; # backend api
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
gzip on;
|
||||
@@ -25,31 +21,12 @@ server {
|
||||
location / {
|
||||
root /usr/share/nginx/html/platform;
|
||||
index index.html index.htm;
|
||||
# 禁止浏览器缓存 index.html
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
}
|
||||
try_files $uri $uri/ /index.html;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
}
|
||||
|
||||
location /workspace/ {
|
||||
alias /usr/share/nginx/html/client/;
|
||||
index index.html index.htm;
|
||||
# 禁止浏览器缓存 /workspace/index.html
|
||||
location = /workspace/index.html {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
}
|
||||
try_files $uri $uri/ /workspace/index.html;
|
||||
}
|
||||
|
||||
location ~ ^(/workspace)?/api(/|$) {
|
||||
rewrite ^/workspace(/.*)$ $1 break;
|
||||
proxy_pass http://backend_server;
|
||||
location /api {
|
||||
proxy_pass http://backend:7860;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -57,14 +34,33 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
client_max_body_size 1024m;
|
||||
client_max_body_size 50m;
|
||||
add_header Access-Control-Allow-Origin $host;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
}
|
||||
|
||||
location ~ ^(/workspace)?/bisheng|/tmp-dir {
|
||||
rewrite ^/workspace(/.*)$ $1 break;
|
||||
#proxy_pass http://minio:9000;
|
||||
proxy_pass http://192.168.106.171:9000;
|
||||
location /workspace/ {
|
||||
alias /usr/share/nginx/html/client/;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /workspace/index.html;
|
||||
}
|
||||
}
|
||||
|
||||
location /workspace/api {
|
||||
rewrite ^/workspace(/.*)$ $1 break;
|
||||
proxy_pass http://backend:7860;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
client_max_body_size 50m;
|
||||
add_header Access-Control-Allow-Origin $host;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
}
|
||||
|
||||
location ~ ^/(workspace/bisheng|bisheng|tmp-dir)/ {
|
||||
rewrite ^/workspace(/.*)$ $1 break;
|
||||
proxy_pass http://minio:9000;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ server {
|
||||
|
||||
listen 8443;
|
||||
location /api {
|
||||
proxy_pass http://bisheng-backend:7860;
|
||||
proxy_pass http://backend:7860;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -26,4 +26,4 @@ server {
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
client_max_body_size 50m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
ALTER TABLE knowledge_space_tag_library
|
||||
ADD COLUMN ai_tags JSON COMMENT 'AI生成的标签',
|
||||
ADD COLUMN ai_tag_count INT DEFAULT 0 COMMENT 'AI标签数量';
|
||||
|
||||
|
||||
ALTER TABLE tag
|
||||
ADD COLUMN resource_type VARCHAR(60) NOT NULL DEFAULT 'manual_tag' COMMENT '资源类型';
|
||||
|
||||
update config set value = '{"system_prompt": "# \u89d2\u8272\n\u4f60\u662f\u4e00\u4e2a\u4e25\u8c28\u7684AI\u95ee\u7b54\u52a9\u624b\uff0c\u4f60\u7684\u4efb\u52a1\u662f\u6839\u636e\u7528\u6237\u95ee\u9898\u4ee5\u53ca\u76f8\u5173\u8d44\u6599\u8fdb\u884c\u56de\u7b54\u3002\n\u5728\u56de\u7b54\u65f6\uff0c\u8bf7\u6ce8\u610f\u4ee5\u4e0b\u51e0\u70b9\uff1a\n1. \u8bf7\u4f7f\u7528\u7528\u6237\u6240\u4f7f\u7528\u7684\u8bed\u8a00\u8fdb\u884c\u56de\u7b54\u3002\n2. \u5f53\u3010\u53c2\u8003\u8d44\u6599\u3011\u4e2d\u6709\u660e\u786e\u4e0e\u95ee\u9898\u76f8\u5173\u7684\u4fe1\u606f\u65f6\u624d\u8fdb\u884c\u56de\u7b54\uff0c\u4fdd\u6301\u7b54\u6848\u4e25\u8c28\u3001\u4e13\u4e1a\uff0c\u4e0d\u5141\u8bb8\u81ea\u884c\u63a8\u6d4b\uff0c\u5728\u56de\u7b54\u4e2d\u6807\u6ce8\u53c2\u8003\u8d44\u6599\u51fa\u5904\u3002\u5982\u679c\u4e0d\u540c\u7684\u53c2\u8003\u6765\u6e90\u6709\u5dee\u5f02\u751a\u81f3\u51b2\u7a81\uff0c\u5219\u5e94\u90fd\u5217\u4e3e\u51fa\u6765\u3002\n3. \u5982\u679c\u3010\u53c2\u8003\u8d44\u6599\u3011\u4e0e\u7528\u6237\u95ee\u9898\u65e0\u5173\uff0c\u5219\u56de\u590d\u201c\u6ca1\u6709\u627e\u5230\u76f8\u5173\u5185\u5bb9\u201d\u6216\u662f\u201cno content found\"\u3002\n4. \u82e5\u6587\u7ae0\u5185\u5bb9\u4e2d\u5305\u542b\u56fe\u7247\u5f15\u7528\uff08\u4f8b\u5982\uff1a\uff09\uff0c\u8bf7\u4ecd\u7136\u4f7f\u7528 Markdown \u683c\u5f0f\u6e32\u67d3\u56fe\u7247\uff0c\u4e0d\u8981\u4fee\u6539\u6216\u5220\u9664\u3002\n5. \u5f53\u524d\u65f6\u95f4\u662f{cur_date}\u3002", "user_prompt": "# \u53c2\u8003\u8d44\u6599\n```\n{retrieved_file_content}\n```\n# \u7528\u6237\u95ee\u9898\n{question}", "max_chunk_size": 15000, "auto_tag_visible": true, "review_tag_visible": true}'
|
||||
where key = "workstation_knowledge_space";
|
||||
|
||||
update tag set resource_type = 'system_tag'
|
||||
where name in ("政策制度", "产品资料", "技术文档", "项目资料", "财务资料", "人力资源", "市场销售", "客户案例", "培训资料", "其他");
|
||||
|
||||
DROP TABLE IF EXISTS `review_tag_link`;
|
||||
CREATE TABLE `review_tag_link` (
|
||||
`tag_id` int NOT NULL,
|
||||
`resource_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`resource_type` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`tenant_id` int NOT NULL DEFAULT '1' COMMENT 'Tenant ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`is_deleted` tinyint DEFAULT '0',
|
||||
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `resource_tag_uniq` (`resource_id`,`resource_type`,`tag_id`),
|
||||
KEY `ix_taglink_tenant_id` (`tenant_id`),
|
||||
KEY `ix_taglink_tag_id` (`tag_id`),
|
||||
KEY `ix_taglink_id` (`id`),
|
||||
KEY `ix_taglink_create_time` (`create_time`),
|
||||
KEY `idx_taglink_tenant_id` (`tenant_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `review_tag`;
|
||||
CREATE TABLE `review_tag` (
|
||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`business_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`business_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`resource_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`tenant_id` int NOT NULL DEFAULT '1' COMMENT 'Tenant ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`is_deleted` tinyint DEFAULT '0',
|
||||
`review_status` tinyint DEFAULT '0',
|
||||
`reject_reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`review_time` datetime DEFAULT NULL,
|
||||
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ix_tag_id` (`id`),
|
||||
KEY `ix_tag_create_time` (`create_time`),
|
||||
KEY `ix_tag_tenant_id` (`tenant_id`),
|
||||
KEY `ix_tag_name` (`name`),
|
||||
KEY `idx_tag_tenant_id` (`tenant_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -1,270 +0,0 @@
|
||||
# 2.5 全面 Review 报告(非 ReBAC 部分)
|
||||
|
||||
日期: 2026-04-23
|
||||
分支: feat/2.5.0
|
||||
|
||||
> ReBAC 权限系统的 bug 见 `rebac-permission-bugs-2026-04-23.md`,本文档覆盖其余模块。
|
||||
|
||||
---
|
||||
|
||||
## 一、Sync 端点 + `_run_async_safe` 系统性风险
|
||||
|
||||
### 问题
|
||||
|
||||
大量业务端点仍然是 sync(`def` 而非 `async def`),内部通过 `_run_async_safe` 调用 async 权限检查。FastAPI 把 sync 端点放到 threadpool 执行,`_run_async_safe` 在 threadpool 里可能 fallback 到 `asyncio.run()` 创建临时 event loop,污染全局连接(详见 rebac-permission-bugs Bug 1)。
|
||||
|
||||
### 受影响端点(不完全列表)
|
||||
|
||||
| 文件 | 端点 | 调用链 |
|
||||
|------|------|--------|
|
||||
| `api/v1/assistant.py:29` | `GET /assistant` (sync) | `get_user_access_resource_ids` → `_run_async_safe` |
|
||||
| `api/v1/assistant.py:50` | `POST /assistant/delete` (sync) | `delete_assistant` → `has_any_permission_sync` → `_run_async_safe` |
|
||||
| `api/v1/flows.py:29` | `DELETE /flows/{id}` (sync) | `has_any_permission_sync` → `_run_async_safe` |
|
||||
| `api/v1/workflow.py` | 多个 sync 端点 | `filter_object_ids_by_permission_sync` → `_run_async_safe` |
|
||||
| `workstation/api/endpoints/apps.py:45` | `GET /app/recommended` (async 但调 sync DAO) | `FlowDao.get_all_apps` (sync) 在 async 端点里 |
|
||||
|
||||
### 修复方向
|
||||
|
||||
把所有涉及权限检查的端点改成 `async def`,直接 `await` async 权限方法,消除 `_run_async_safe` 调用。
|
||||
|
||||
---
|
||||
|
||||
## 二、认证/中间件安全问题
|
||||
|
||||
### 2.1(P0)WebSocket 中间件缺少 token_version 校验
|
||||
|
||||
`http_middleware.py:320-335` — `WebSocketLoggingMiddleware` 只调 `_set_tenant_context(token)`,不调 `_apply_token_version_and_visible()`。HTTP 请求会校验 token_version,但 WebSocket 连接完全跳过。被禁用的用户可以通过 WebSocket 保持连接。
|
||||
|
||||
### 2.2(P0)SSO 端点缺少 tenant_id 同步
|
||||
|
||||
`user/api/user.py:55-97` — SSO 登录端点不调 `UserTenantSyncService.sync_user()`。普通登录流程会调(`UserService.user_login` → `sync_user`),但 SSO 完全绕过。导致:
|
||||
- JWT 里的 `tenant_id` 可能过期(用户主部门已变更)
|
||||
- `token_version` 不会在首次 SSO 登录时 bump
|
||||
|
||||
### 2.3(P1)SSO 端点 JWT 缺少 token_version
|
||||
|
||||
`user/api/user.py:81` — `LoginUser.create_access_token(user_exist, auth_jwt=auth_jwt)` 没传 `token_version`。新创建的 SSO 用户可能 DB 里 `token_version` 和 JWT 里的不一致。
|
||||
|
||||
### 2.4(P1)中间件 exempt path 用 startswith() 过于宽泛
|
||||
|
||||
`http_middleware.py:280` — `is_exempt = req_path.startswith(TENANT_CHECK_EXEMPT_PATHS)`。`/api/v1/user/login-admin` 会匹配 `/api/v1/user/login`,跳过 token_version 校验。
|
||||
|
||||
### 2.5(P1)WebSocket 不支持 Bearer token 认证
|
||||
|
||||
`http_middleware.py:332-346` — WebSocket 中间件只从 Cookie 提取 token,不检查 `Authorization: Bearer`。使用 Bearer token 的 WebSocket 客户端会得到 `token=None`,tenant context 被设为 `DEFAULT_TENANT_ID`,破坏多租户隔离。
|
||||
|
||||
### 2.6(P1)WebSocket 缺少禁用用户检查
|
||||
|
||||
`http_middleware.py:235-244` — 禁用用户检查只在 `_apply_token_version_and_visible()` 里,WebSocket 不调这个方法。禁用用户可以通过 WebSocket 继续操作。
|
||||
|
||||
### 2.7(P2)SSO 端点 JWT 默认 tenant_id=1
|
||||
|
||||
`user/api/user.py:55-97` — SSO 端点调 `create_access_token` 不传 `tenant_id`,默认 `DEFAULT_TENANT_ID=1`。多租户环境下所有 SSO 用户都拿到 `tenant_id=1`。
|
||||
|
||||
### 2.8(P2)token_version 缓存 TTL 300s 导致延迟失效
|
||||
|
||||
`http_middleware.py:42` 和 `user.py:468` — `is_super` 和 `token_version` 都缓存 300s。用户被禁用后,旧 JWT 最多还能用 5 分钟。
|
||||
|
||||
### 2.9(P3)登出不 bump token_version
|
||||
|
||||
`user/api/user.py:199-214` — 登出只清 cookie,不 bump `token_version`。旧 JWT 如果被提取出来仍然有效。
|
||||
|
||||
---
|
||||
|
||||
## 三、前端权限/租户 UI 问题
|
||||
|
||||
### 3.1(P1)PermissionGrantTab 硬编码中文 fallback model 名称
|
||||
|
||||
`permission/PermissionGrantTab.tsx:15-20` — `DEFAULT_MODELS` 硬编码了中文名称(`所有者`、`可查看`等),没有走 i18n。当 API 获取 grantable models 失败时 fallback 到这些硬编码值。
|
||||
|
||||
### 3.2(P2)PermissionListTab 缺少 include_children 展示
|
||||
|
||||
权限列表 tab 没有展示 department 授权的 `include_children` 状态。用户无法区分"工程部"和"工程部(含子部门)"的授权。
|
||||
|
||||
### 3.3(P2)TenantSelect 页面缺少错误处理
|
||||
|
||||
`LoginPage/TenantSelect.tsx` — 租户选择页面在 API 调用失败时没有明确的错误提示,用户可能卡在选择页面。
|
||||
|
||||
### 3.4(P2)MainLayout 多租户 header 缺少租户切换确认
|
||||
|
||||
`layout/MainLayout.tsx` — 租户切换没有确认对话框。误点可能导致上下文切换,丢失未保存的工作。
|
||||
|
||||
---
|
||||
|
||||
## 四、Alembic Migration 多头冲突
|
||||
|
||||
### 4.1(P0)三个 f026 migration 形成分叉头
|
||||
|
||||
三个 migration 文件都以 `f025_merge_f024_heads` 为 `down_revision`:
|
||||
- `f026_chatmessage_files_longtext`
|
||||
- `f026_department_admin_grant`
|
||||
- `f026_role_scope_name_unique`
|
||||
|
||||
`f027_role_scope_nullsafe_unique` 只依赖 `f026_role_scope_name_unique`。
|
||||
|
||||
`alembic upgrade head` 会报 "Multiple heads detected" 错误。需要一个 merge migration 把三个 f026 合并。
|
||||
|
||||
---
|
||||
|
||||
## 五、Async 端点调用 Sync 方法阻塞 Event Loop
|
||||
|
||||
### 5.1(P1)`create_knowledge` async 端点调用 sync 服务
|
||||
|
||||
`knowledge/api/endpoints/knowledge.py:234` — `async def create_knowledge` 调用 `KnowledgeService.create_knowledge()`(sync 方法)。sync 方法内部做了:
|
||||
- `KnowledgeDao.insert_one()` — sync DB 写入
|
||||
- `KnowledgeRag.init_knowledge_milvus_vectorstore_sync()` — sync Milvus 操作
|
||||
- `OwnerService.write_owner_tuple_sync()` — `_run_async_safe`
|
||||
|
||||
这些 sync 操作在 async 端点里直接执行,会阻塞 event loop。FastAPI 对 `async def` 端点不会自动把 sync 调用放到 threadpool。
|
||||
|
||||
### 5.2(P2)多个 async 端点混用 sync DAO
|
||||
|
||||
`workstation/api/endpoints/apps.py:45` — `async def get_recommended_apps` 调用 `FlowDao.get_all_apps()`(sync DAO)。应该用 `FlowDao.aget_all_apps()`。
|
||||
|
||||
类似问题在多个 workstation 端点中存在。
|
||||
|
||||
---
|
||||
|
||||
## 六、资源生命周期 FGA 元组清理缺失
|
||||
|
||||
### 6.1(P1)Workflow 删除不清理 FGA tuples
|
||||
|
||||
`api/services/flow.py:328` — `delete_flow_hook` 没有调用 `OwnerService.delete_resource_tuples`。对比:
|
||||
- assistant 删除:`assistant.py:264` 有 `delete_resource_tuples_sync('assistant', ...)`
|
||||
- knowledge 删除:`knowledge_service.py:489` 有 `delete_resource_tuples_sync('knowledge_library', ...)`
|
||||
- tool 删除:`tool.py:416` 有 `delete_resource_tuples_sync('tool', ...)`
|
||||
- channel 删除:`channel_service.py:928` 有 `delete_resource_tuples('channel', ...)`
|
||||
- **workflow 删除:缺失**
|
||||
|
||||
导致 FGA 里残留孤立元组,`list_objects` 会返回已删除的 workflow ID,前端查不到对应资源。
|
||||
|
||||
---
|
||||
|
||||
## 八、`require_quota` 装饰器把 sync 端点变成 async
|
||||
|
||||
### 8.1(P1)sync 端点被 async wrapper 包装后阻塞 event loop
|
||||
|
||||
`quota_service.py:489` — `require_quota` 装饰器的 wrapper 是 `async def`。当装饰 sync 端点时(如 `workflow.py:175 def create_flow`),FastAPI 看到 async 函数,在 event loop 上直接执行。但 wrapper 内部调用 sync 的 `func(*args, **kwargs)`,阻塞 event loop。
|
||||
|
||||
受影响端点:
|
||||
- `workflow.py:175` — `def create_flow` (sync) + `@require_quota`
|
||||
- 其他被 `@require_quota` 装饰的 sync 端点
|
||||
|
||||
修复方向:`require_quota` 的 wrapper 应该检测被装饰函数是否 sync,如果是则用 `asyncio.to_thread` 包装。
|
||||
|
||||
---
|
||||
|
||||
## 九、部门/组织同步问题
|
||||
|
||||
### 9.1(P0)SSO 同步路径计算错误 — 用 parent_id 代替 self id
|
||||
|
||||
`sso_sync/domain/services/dept_upsert_service.py:122-123` — 路径计算 `computed_path = f'{base}{parent_id}/'` 把 parent_id 追加到路径末尾,而不是部门自身的 id。产生 `/1/1/` 而不是 `/1/2/`。
|
||||
|
||||
影响:部门树结构损坏,path-based 子树查询全部失效。
|
||||
|
||||
### 9.2(P0)SSO 顶层部门路径设为 `/` 而非 `/{id}/`
|
||||
|
||||
`dept_upsert_service.py:119-123` — `parent_external_id=None` 时路径设为 `'/'`,违反 F002 路径约定(应为 `'/{dept.id}/'`)。本地创建的根部门用 `f'/{dept.id}/'`,SSO 同步的不一致。
|
||||
|
||||
### 9.3(P1)SSO 登录同步缺少附属部门 FGA member tuple 写入
|
||||
|
||||
`login_sync_service.py:124-127` — `_ensure_secondaries()` 把用户加入附属部门的 DB 记录,但不触发 `DepartmentChangeHandler.on_members_added()` 写 FGA tuple。用户在附属部门的权限检查会失败。
|
||||
|
||||
### 9.4(P1)SSO 主部门变更不清理旧部门 FGA member tuple
|
||||
|
||||
`login_sync_service.py:248-268` — `_ensure_primary()` 变更主部门时不触发 change handler 删除旧部门的 member tuple。FGA 里用户仍然是旧部门的 member。
|
||||
|
||||
### 9.5(P2)部门归档不级联子部门
|
||||
|
||||
`department_service.py:456-490` — 归档部门时子部门不自动归档,产生孤儿节点(parent_id 指向已归档部门)。
|
||||
|
||||
### 9.6(P2)部门 purge 不清理 parent FGA tuple
|
||||
|
||||
`department_service.py:503-580` — `apurge_department()` 不清理 `department#parent` tuple,FGA 图残留脏数据。
|
||||
|
||||
### 9.7(P2)部门移动路径更新非原子
|
||||
|
||||
`department_service.py:625-637` — `func.replace()` 更新路径和 `parent_id` 更新不是原子的,并发查询可能看到不一致状态。
|
||||
|
||||
### 9.8(P2)SSO 同步 `_ensure_secondaries` TOCTOU 竞态
|
||||
|
||||
`login_sync_service.py:374-382` — 检查已有成员关系和添加新关系之间有竞态窗口,并发 SSO 登录可能触发唯一约束冲突。
|
||||
|
||||
---
|
||||
|
||||
## 十、多租户实现问题
|
||||
|
||||
### 10.1(P0)AdminScope 不重置 visible_tenant_ids — 跨租户数据泄漏
|
||||
|
||||
`common/middleware/admin_scope.py:73-131` — 全局超管切换到 Child 管理视图时,`AdminScopeMiddleware` 设置 `_admin_scope_tenant_id` 但不重置 `visible_tenant_ids`。ORM tenant_filter 仍然用 JWT 里的旧 `visible_tenant_ids`(如 `{child_1, 1}`),导致超管切到 Child 2 管理视图时仍能看到 Child 1 的数据。
|
||||
|
||||
### 10.2(P0)ContextVar 请求间泄漏 — 跨租户数据泄漏
|
||||
|
||||
`core/context/tenant.py:45-71` — 中间件设置 `current_tenant_id`、`visible_tenant_ids` 等 ContextVar,但请求结束时不重置。async 环境下如果请求异常中断,这些值会泄漏到下一个复用同一 async task 的请求。
|
||||
|
||||
修复:中间件的 `call_next(request)` 应包在 try/finally 里,finally 重置所有 ContextVar。
|
||||
|
||||
### 10.3(P1)tenant_filter 不使用 visible_tenant_ids,共享资源不可见
|
||||
|
||||
`core/database/tenant_filter.py:87` — ORM event listener 只用 `WHERE tenant_id = get_current_tenant_id()`(严格等于)。`visible_tenant_ids` ContextVar(存 `{leaf_id, 1}`)完全没被使用。
|
||||
|
||||
Child 用户的 `current_tenant_id = leaf_id`,Root 共享的资源(`tenant_id=1`)被过滤掉。即使 ReBAC 层允许访问,ORM 查询也查不到。
|
||||
|
||||
### 10.4(P1)WebSocket 中间件缺少 visible_tenant_ids 注入
|
||||
|
||||
`http_middleware.py:320-346` — WebSocket 中间件调 `_set_tenant_context(token)` 但不调 `_apply_token_version_and_visible()`。WebSocket 连接的 `visible_tenant_ids = None`,ORM 回退到严格等于过滤。WebSocket 客户端(聊天、实时更新)看不到 Root 共享资源。
|
||||
|
||||
### 10.5(P1)AdminScope 不检查 management API 标志
|
||||
|
||||
`core/database/tenant_filter.py:81-105` — tenant_filter 通过 `get_current_tenant_id()` 间接使用 `_admin_scope_tenant_id`,但不检查 `_is_management_api` 标志。如果 admin scope 泄漏到业务 API(中间件顺序 bug),业务 API 会按错误的 tenant 过滤。
|
||||
|
||||
### 10.6(P2)UserTenantSyncService 不验证目标 tenant 是否 active
|
||||
|
||||
`tenant/domain/services/user_tenant_sync_service.py:114-119` — `aactivate_user_tenant()` 不验证目标 tenant 是否仍然 active。如果 tenant 在 resolve 和 activate 之间被禁用,用户会被分配到已禁用的 tenant。
|
||||
|
||||
### 10.7(P2)knowledge_service 没有 bypass_tenant_filter
|
||||
|
||||
`knowledge/domain/services/knowledge_service.py` — 整个文件没有使用 `bypass_tenant_filter`。`aget_user_knowledge`、`aget_knowledge_files` 等查询都受 tenant_filter 影响,即使 ReBAC 层已经做了 tenant 过滤。
|
||||
|
||||
---
|
||||
|
||||
## 问题汇总
|
||||
|
||||
| 编号 | 严重度 | 模块 | 问题 |
|
||||
| ---- | ------ | ---- | ---- |
|
||||
| 1.1 | P1 | 全局 | sync 端点 + `_run_async_safe` 系统性风险 |
|
||||
| 2.1 | P0 | 认证 | WebSocket 缺少 token_version 校验 |
|
||||
| 2.2 | P0 | 认证 | SSO 端点缺少 tenant_id 同步 |
|
||||
| 2.3 | P1 | 认证 | SSO JWT 缺少 token_version |
|
||||
| 2.4 | P1 | 认证 | exempt path startswith() 过于宽泛 |
|
||||
| 2.5 | P1 | 认证 | WebSocket 不支持 Bearer token |
|
||||
| 2.6 | P1 | 认证 | WebSocket 缺少禁用用户检查 |
|
||||
| 2.7 | P2 | 认证 | SSO JWT 默认 tenant_id=1 |
|
||||
| 2.8 | P2 | 认证 | token_version 缓存 300s 延迟失效 |
|
||||
| 2.9 | P3 | 认证 | 登出不 bump token_version |
|
||||
| 3.1 | P1 | 前端 | PermissionGrantTab 硬编码中文 |
|
||||
| 3.2 | P2 | 前端 | 权限列表缺少 include_children 展示 |
|
||||
| 3.3 | P2 | 前端 | TenantSelect 缺少错误处理 |
|
||||
| 3.4 | P2 | 前端 | 租户切换无确认对话框 |
|
||||
| 4.1 | P0 | DB | 三个 f026 migration 分叉头 |
|
||||
| 5.1 | P1 | 性能 | async 端点调 sync 服务阻塞 event loop |
|
||||
| 5.2 | P2 | 性能 | async 端点混用 sync DAO |
|
||||
| 6.1 | P1 | 生命周期 | Workflow 删除不清理 FGA tuples |
|
||||
| 8.1 | P1 | Quota | require_quota 把 sync 端点变 async |
|
||||
| 9.1 | P0 | SSO同步 | 路径计算用 parent_id 代替 self id |
|
||||
| 9.2 | P0 | SSO同步 | 顶层部门路径设为 `/` |
|
||||
| 9.3 | P1 | SSO同步 | 附属部门缺少 FGA member tuple |
|
||||
| 9.4 | P1 | SSO同步 | 主部门变更不清理旧 FGA tuple |
|
||||
| 9.5 | P2 | 部门 | 归档不级联子部门 |
|
||||
| 9.6 | P2 | 部门 | purge 不清理 parent FGA tuple |
|
||||
| 9.7 | P2 | 部门 | 移动路径更新非原子 |
|
||||
| 9.8 | P2 | SSO同步 | _ensure_secondaries TOCTOU 竞态 |
|
||||
| 10.1 | P0 | 多租户 | AdminScope 不重置 visible_tenant_ids(跨租户泄漏) |
|
||||
| 10.2 | P0 | 多租户 | ContextVar 请求间泄漏(跨租户泄漏) |
|
||||
| 10.3 | P1 | 多租户 | tenant_filter 不用 visible_tenant_ids |
|
||||
| 10.4 | P1 | 多租户 | WebSocket 缺少 visible_tenant_ids 注入 |
|
||||
| 10.5 | P1 | 多租户 | AdminScope 不检查 management API 标志 |
|
||||
| 10.6 | P2 | 多租户 | UserTenantSync 不验证目标 tenant active |
|
||||
| 10.7 | P2 | 多租户 | knowledge_service 没有 bypass_tenant_filter |
|
||||
|
||||
**统计:P0 = 7, P1 = 14, P2 = 11, P3 = 1, 总计 33 个问题**
|
||||
@@ -1,256 +0,0 @@
|
||||
# 知识空间文件版本管理(2.6.0 beta2)
|
||||
|
||||
> 面向 **后续开发 / AI 代码代理**:快速了解功能、数据模型与实现脉络。
|
||||
> 部署 / 升级流程见 `2.6.0-beta2-升级说明.md`。
|
||||
> PRD 原文:https://dataelem.feishu.cn/wiki/Rv4LwMBswiZzrvk25IEc04XZnlg
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能一句话
|
||||
|
||||
> 同一份知识文件的多次上传,自动归并为一份「逻辑文档」,挂在主版本/历史版本链上。检索/对话/列表只走主版本;SimHash 相似度推荐用户合并相似上传,杜绝重复检索 chunk。
|
||||
|
||||
**范围**:仅 `Knowledge.type=3 SPACE`(知识空间)。老的 NORMAL / QA / PRIVATE 知识库零影响。
|
||||
|
||||
**前端落地**:client app(`src/frontend/client`),`SpaceDetail/` 目录。platform 不涉及。
|
||||
|
||||
**总开关**:`config.yaml` → `knowledges.version_management.enabled`(默认 `false`)。关闭时:
|
||||
- 写 API(关联/设主/删除/dismiss/合并)返 403
|
||||
- 列表仍正常,字段 `version_no` / `has_similar` 仍返回,前端 UI 整体隐藏
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念
|
||||
|
||||
| 概念 | 表 / 字段 | 说明 |
|
||||
|---|---|---|
|
||||
| 逻辑文档 | `knowledge_document` | 一份内容,可挂多个版本。`primary_version_id` 指向主版本 |
|
||||
| 版本 | `knowledge_document_version` | `(document_id, version_no)` 唯一。`is_primary` 标志主版本(同时间只一个) |
|
||||
| 物理文件 | `knowledgefile` | 实际上传的文件;增列 `simhash`(varchar16)、`similar_status`(0/1/2) |
|
||||
| 主版本 | `is_primary=true` | 列表/预览/搜索/RAG 检索/智能体问答**只走主版本** |
|
||||
| 文档编码 | `knowledgefile.file_encoding` | 复用首钢功能;由 `FileEncodingTransformer` 自动生成 |
|
||||
| SimHash | 64-bit hex 字符串 | `similarity = 1 - hamming/64`,默认 ≥ 0.85 触发推荐 |
|
||||
|
||||
**关键不变量**:
|
||||
- 上传文件(D3 决策,与开关无关)→ 同事务建 `knowledge_document` + V1 主版本。
|
||||
- 历史 V1 一律默认主版本(无论解析状态)。
|
||||
- 相似 ≠ 版本:SimHash 只推荐,关联永远人工。
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端结构
|
||||
|
||||
### 3.1 模块布局
|
||||
|
||||
```
|
||||
bisheng/knowledge/
|
||||
├── api/
|
||||
│ ├── endpoints/
|
||||
│ │ ├── knowledge_space.py # 既有,改造支持版本
|
||||
│ │ └── knowledge_version.py # 新,11 个版本相关 endpoint
|
||||
│ └── dependencies.py # get_knowledge_version_service 注入
|
||||
├── domain/
|
||||
│ ├── models/
|
||||
│ │ ├── knowledge_document.py # 新表
|
||||
│ │ └── knowledge_document_version.py
|
||||
│ ├── repositories/ # 接口 + impl(Plan 1)
|
||||
│ ├── schemas/
|
||||
│ │ ├── knowledge_space_schema.py # 文件列表项加 version_no/is_multi_version/has_similar
|
||||
│ │ └── knowledge_version_schema.py # 版本管理 API 入出参
|
||||
│ └── services/
|
||||
│ └── knowledge_version_service.py # 业务核心
|
||||
└── rag/
|
||||
├── version_filter.py # build_primary_only_filter — RAG 主版本过滤
|
||||
└── pipeline/transformer/simhash.py # SimHashTransformer
|
||||
```
|
||||
|
||||
### 3.2 `KnowledgeVersionService`(核心)
|
||||
|
||||
承载 11 个方法,分 4 类:
|
||||
|
||||
**读**:
|
||||
- `list_versions_for_file(knowledge_file_id)` → 版本链表 + 主版本号
|
||||
- `search_associable_documents(space_id, keyword, current_file_id)` → 全部文档(用于「处理相似文档」批量流程)
|
||||
- `search_version_sources(space_id, keyword, current_file_id)` → **只单版本**文档(用于「版本管理」入口)
|
||||
- `get_similar_candidates_for_file(file_id)` → top-N 相似主版本(用于批量流程)
|
||||
- `get_version_recommendations(file_id)` → **只单版本** top-N(用于版本管理入口)
|
||||
- `list_pending_similar_files(space_id)` → 列出 `similar_status=1` 待处理
|
||||
|
||||
**写(关联)** — 两个方向:
|
||||
- `link_file_to_document(knowledge_file_id, target_document_id)`
|
||||
→ 把当前文件**搬进**目标文档的链做新主(批量处理流程,旧语义)
|
||||
- `merge_source_document_into_current(current_knowledge_file_id, source_document_id)`
|
||||
→ 把源单版本文档**吃进**当前文件的链做新主(版本管理入口,反向)
|
||||
|
||||
**写(其他)**:
|
||||
- `set_primary_version(version_id)` — 切主版本(`is_primary` 翻转,索引不重建)
|
||||
- `delete_version(version_id)` — 删历史版本(主版本不可删)+ 清 Milvus/ES/MinIO
|
||||
- `dismiss_similar(knowledge_file_id)` — 用户选「不关联」,`similar_status=2`
|
||||
|
||||
**约束**:所有写方法先调 `_require_version_management_enabled()` 守门;关闭则 403。
|
||||
|
||||
### 3.3 API 端点(都挂 `/api/v1/knowledge/space`)
|
||||
|
||||
| 方法 | 路径 | 用途 | 入口 |
|
||||
|---|---|---|---|
|
||||
| GET | `/file/{id}/versions` | 版本链 | 通用 |
|
||||
| POST | `/document/link` | 把文件搬进文档(旧) | SimilarDocumentDialog(批量) |
|
||||
| POST | `/version/merge` | 把单版本文档吃进当前(新,反向) | RelateDocumentPanel(版本管理) |
|
||||
| POST | `/version/{id}/set-primary` | 切主版本 | VersionHistoryDialog |
|
||||
| DELETE | `/version/{id}` | 删历史版本 | VersionHistoryDialog |
|
||||
| GET | `/{space_id}/document/search` | 全部文档关键词搜 | 批量流程 |
|
||||
| GET | `/{space_id}/document/version-search` | **只单版本**搜 | 版本管理入口 |
|
||||
| GET | `/file/{id}/similar` | top-N 相似(全部) | 批量流程 |
|
||||
| GET | `/file/{id}/version-recommendations` | top-N 相似(单版本) | 版本管理入口 |
|
||||
| GET | `/{space_id}/similar-pending` | 待处理列表 + N | 顶部按钮计数 |
|
||||
| POST | `/file/{id}/dismiss-similar` | 不关联,清相似标 | 两个入口共用 |
|
||||
|
||||
### 3.4 `KnowledgeSpaceService` 改造
|
||||
|
||||
`add_file` 同事务建 `knowledge_document` + V1 主版本(注入 `version_repo` + `doc_repo`)。
|
||||
|
||||
`list_space_children` / `search_space_children`:
|
||||
- DAO 加 `exclude_file_ids` 参数 → 排除非主版本
|
||||
- 返回项加 `version_no` / `is_multi_version` / `has_similar`
|
||||
|
||||
`get_file_download` 不变 — 历史版本物理文件仍在 `knowledgefile` 表里,按 `file_id` 走标准 download 流即可。前端版本历史下载按钮就是直接复用这个 endpoint。
|
||||
|
||||
### 3.5 SimHash 集成
|
||||
|
||||
- `bisheng/common/utils/simhash_utils.py` — 64-bit hex 计算 + hamming + similarity
|
||||
- `bisheng/knowledge/rag/pipeline/transformer/simhash.py::SimHashTransformer` — 挂在 `_init_common_transformers` 和 `_init_excel_transformers`,位置:`FileEncodingTransformer` 之后,`ExtraFileTransformer` 之前
|
||||
- `bisheng.api.services.knowledge_imp.addEmbedding` 解析成功后**同步**调用 `scan_similar_for_file`(原异步 `asyncio.run` 已改为 sync,避免 celery 线程池 event-loop 冲突)
|
||||
- 历史 init 脚本 `src/backend/scripts/init_knowledge_document_versions.py` **不算 simhash**(节流)
|
||||
|
||||
### 3.6 RAG 主版本过滤
|
||||
|
||||
- `KnowledgeDocumentVersionRepository.find_non_primary_file_ids_by_knowledge_ids(knowledge_ids)` — JOIN `knowledge_document` 按空间聚合
|
||||
- `bisheng/knowledge/rag/version_filter.py::build_primary_only_filter` — 字符串/字典拼装,排序去重 + 不可变
|
||||
- 注入点:
|
||||
- `KnowledgeSpaceChatService.chat_folder` — 整空间或指定文件 减集兜底空集
|
||||
- `RagUtils.init_knowledge_retriever` — sync 上下文走 `asyncio.run` best-effort,失败仅 warn
|
||||
- 索引不重建,仅靠过滤实现「切主版本立即生效」
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端结构(`src/frontend/client`)
|
||||
|
||||
### 4.1 组件
|
||||
|
||||
```
|
||||
pages/knowledge/SpaceDetail/
|
||||
├── index.tsx # 总装:queries / dialogs / 顶部 banner
|
||||
├── KnowledgeSpaceHeader.tsx # 顶部「处理相似文档(N)」橙色 pill 按钮
|
||||
├── FileTable.tsx + FileCard.tsx # 文件列表两种视图;Vn 角标 + 相似 pill
|
||||
├── VersionManagementDialog.tsx # 文件三点菜单「版本管理」入口
|
||||
├── RelateDocumentPanel.tsx # 嵌在 VersionManagementDialog 里,新方向 API
|
||||
├── VersionHistorySheet.tsx # 「版本历史」对话框(Dialog,不是 Sheet,虽名字保留)
|
||||
└── SimilarDocumentDialog.tsx # 顶部按钮触发的「处理相似文档」批量弹窗(旧方向)
|
||||
```
|
||||
|
||||
### 4.2 两个入口的 mental model
|
||||
|
||||
| 入口 | 用户视角 | 推荐范围 | 关联方向 | 用的 API |
|
||||
|---|---|---|---|---|
|
||||
| 顶部「处理相似文档(N)」 | 看 N 个 *待处理* 文件,逐个挑选目标 | 全部主版本 | 当前文件 → 目标文档(原文档被删) | `linkAsNewVersionApi` |
|
||||
| 文件三点菜单「版本管理」 | 给 *当前文件* 选个被吃的文档 | **只单版本**文档 | 选中文档 → 当前文档(被吃文档删) | `mergeIntoCurrentApi` |
|
||||
|
||||
两个入口共用 `dismiss_similar`(不关联)。两套接口并存,**不要把它们合并**。
|
||||
|
||||
### 4.3 UI 渲染规则(`FileTable` / `FileCard`)
|
||||
|
||||
- `is_multi_version=true` → 显示蓝色 `Vn` 角标
|
||||
- `has_similar=true && !is_multi_version` → 显示橙色「相似文档」pill(可点 → 打开版本管理对话)
|
||||
- **两者互斥**:多版本文件即使后端误标 `has_similar` 也只显示 Vn,前端兜底
|
||||
|
||||
### 4.4 API 函数(`src/api/knowledge.ts` 末尾)
|
||||
|
||||
11 个版本管理函数,字段名称与后端 schema 严格对齐(`knowledge_file_id` / `target_document_id` / `original_file_name` / `file_code` / `uploader_name` 等)。
|
||||
|
||||
`KnowledgeFile.id` 是 string,API 接 int,在调用边界用 `Number(file.id)` 转换。
|
||||
|
||||
### 4.5 i18n
|
||||
|
||||
三语 (`en` / `zh-Hans` / `ja`),`com_knowledge.version.*` 嵌套 50+ key。主要前缀:`history_*`(版本历史)、`similar_dialog_*`(批量弹窗)、`section_*` / `pill_*` / `btn_*`。
|
||||
|
||||
### 4.6 配置开关 hook
|
||||
|
||||
`src/hooks/useVersionManagementEnabled.ts` 读 `bishengConfState.knowledges.version_management.enabled`。配置经 `/api/v1/env` 暴露给前端。**整个版本管理 UI 都用这个 hook 守门**,关闭时连菜单项都不渲染。
|
||||
|
||||
### 4.7 列表刷新机制
|
||||
|
||||
文件上传后异步 polling(`useFileManager.ts` 5s 间隔),解析完成 + SimHash 扫描完成后 `has_similar` 才置位。SpaceDetail `index.tsx` 监听 `displayFiles` 里 `has_similar && !is_multi_version` 的 id 集合变化,变化即 `invalidateQueries(["pending-similar"])`,顶部按钮 N 实时更新。
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置 + 启动
|
||||
|
||||
`config.yaml`:
|
||||
|
||||
```yaml
|
||||
knowledges:
|
||||
version_management:
|
||||
enabled: true # 总开关
|
||||
simhash_similarity_threshold: 0.85
|
||||
```
|
||||
|
||||
后端服务起后,前端 hook 自动读到。无需重启前端。
|
||||
|
||||
---
|
||||
|
||||
## 6. 已知遗留(不挡发布)
|
||||
|
||||
| 问题 | 影响 | 出路 |
|
||||
|---|---|---|
|
||||
| `audit_log` 表不存在 | 版本管理事件审计静默失败 | 数据库 DDL 问题,需 migration |
|
||||
| `similar_status` 残留 | V2 删除后 V1 主版本 `similar_status=1` 不清,UI 持续显示相似 pill 直到用户主动 dismiss | Plan 3 只标新上传,删历史不清。可选改:`delete_version` 后重扫主版本 |
|
||||
| 写 API 缺 owner/manager 权限校验 | 只校验开关,未做角色守门 | Plan 2 reviewer 已标 |
|
||||
| 深层文件夹路径未实测 | D1 决策(`file_level_path` 跟移)的多层 folder 场景未压测 | 用户基本在 root 测试 |
|
||||
| `addEmbedding._scan` 失败仅 warn | 相似扫描静默失败时不感知 | 改 Celery 子任务正经监控 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试
|
||||
|
||||
```bash
|
||||
# 全量回归(约 ~170 passed,5 pre-existing 失败:mineru / paddleocr 路径硬编码 + docx_string 3 个,跟版本管理无关)
|
||||
cd src/backend && uv run pytest test/knowledge test/workflow -q
|
||||
```
|
||||
|
||||
主要测试模块:
|
||||
- `test/knowledge/test_knowledge_version_service_*.py`(单元)
|
||||
- `test/knowledge/test_knowledge_version_endpoint.py`(API)
|
||||
- `test/knowledge/test_simhash_*.py` / `test_version_filter.py`
|
||||
- `test/workflow/test_rag_utils.py`
|
||||
|
||||
---
|
||||
|
||||
## 8. 关键文件索引
|
||||
|
||||
**后端**:
|
||||
- `bisheng/knowledge/domain/services/knowledge_version_service.py` — 业务核心(11 方法)
|
||||
- `bisheng/knowledge/api/endpoints/knowledge_version.py` — 11 个 endpoint
|
||||
- `bisheng/knowledge/domain/schemas/knowledge_version_schema.py` — 权威 schema
|
||||
- `bisheng/knowledge/rag/version_filter.py` — RAG 主版本过滤
|
||||
- `bisheng/api/services/knowledge_imp.py` — `addEmbedding` sync scan + `delete_vector_files`
|
||||
- `bisheng/database/models/audit_log.py` — 4 个 EventType
|
||||
- `scripts/init_knowledge_document_versions.py` — 历史数据初始化
|
||||
|
||||
**前端**:
|
||||
- `src/api/knowledge.ts` 末尾 — 11 个 API 函数 + 类型
|
||||
- `src/pages/knowledge/SpaceDetail/` — 所有 UI
|
||||
- `src/locales/{en,zh-Hans,ja}/translation.json` `com_knowledge.version.*`
|
||||
- `src/hooks/useVersionManagementEnabled.ts` — 开关 hook
|
||||
|
||||
**Alembic**:`v2_6_0_f039_knowledge_document_tables.py`(注意是 f039,不是计划里的 f038)
|
||||
|
||||
---
|
||||
|
||||
## 9. 排错速查
|
||||
|
||||
- 前端报 404 → 检查 URL 前缀。client app 走 vite proxy,前缀是 `/workspace/api/v1/...`
|
||||
- 后端 422 但前端没感知 → bisheng `ORJSONResponse` 把 422 包成 HTTP 200,前端 axios interceptor 检查的是 `body.status_code`
|
||||
- 改后端代码无效果 → 必须重启 uvicorn / worker(无 hot reload)
|
||||
- 版本管理 UI 不出现 → 看 `/api/v1/env` 返回的 `knowledges.version_management.enabled` 是不是 `true`,或 config.yaml 是否生效
|
||||
- 相似 pill 没显示 → 后端 `addEmbedding._scan` 是否跑过(看日志),`similar_status` 是否落到 `1`
|
||||
- Tooltip 在 Dialog 里看不见 → DialogContent 是 `z-[100]`,Tooltip 要 `z-[200]` 才能盖过去
|
||||
@@ -1,721 +0,0 @@
|
||||
# BiSheng 2.5 多租户切换运维手册
|
||||
|
||||
> 适用版本:BiSheng v2.5.1+
|
||||
> 适用部署形态:Docker Compose 容器化部署
|
||||
> 读者:运维 / SRE / DBA
|
||||
> 文档目的:为「单租户 ↔ 多租户」模式切换提供可复制粘贴的操作步骤、验证方法、回滚方案与故障排查。
|
||||
|
||||
---
|
||||
|
||||
## 0. 30 秒速查
|
||||
|
||||
| 场景 | 复杂度 | 耗时 | 关键动作 |
|
||||
|------|--------|------|---------|
|
||||
| **单租户 → 多租户** | ⭐ | 5–15 min | 改 1 行 YAML + 重启容器 + 验证 API |
|
||||
| **多租户 → 单租户(软回退,未挂载过 Child)** | ⭐⭐ | 10 min | 改 1 行 YAML + 重启 + 让用户重新登录 |
|
||||
| **多租户 → 单租户(硬回退,已挂载 Child 并写入数据)** | ⭐⭐⭐⭐ | 数小时 | 数据迁移 + 存储重命名 + FGA 清理 + flag 切换 |
|
||||
|
||||
> ⚠️ **核心心智模型**:`multi_tenant.enabled` 是**代码层"忽略开关"**,**不是数据层"清空开关"**。开启后产生的 Tenant / 业务数据 / 外部存储前缀**不会因为关闭 flag 而自动清理**。
|
||||
|
||||
> 💡 **已在 v2.5 系列运行的环境(快速路径)**:如果 `SELECT version_num FROM alembic_version` 已是 `v2_5_*` 系列、`tenant` 表已有 Root、`user_tenant` 已全员绑定(`(SELECT COUNT(*) FROM user_tenant WHERE status='active') = (SELECT COUNT(*) FROM user)`),说明 DDL 已跑完、数据已就位。此时单租户→多租户实际仅需 **1-2 min**(改 yaml + 仅重启 backend + worker,无需停 mysql / milvus / es / minio)。本手册主流程默认按"全新启用"描述以最大兼容性。
|
||||
|
||||
---
|
||||
|
||||
## 1. 切换前必读
|
||||
|
||||
### 1.1 关键事实(基于代码验证)
|
||||
|
||||
| 事实 | 说明 |
|
||||
|------|------|
|
||||
| **DDL 永远会跑** | Alembic 迁移(创建 `tenant` / `user_tenant` 表、给 `auditlog` 加 `operator_tenant_id` 等扩展列、给 48 张业务表加 `tenant_id` 列)在 `alembic upgrade head` 时**无条件执行**,不依赖 `multi_tenant.enabled` |
|
||||
| **Root Tenant 永远存在** | id=1 的 Root Tenant 在迁移和 `init_default_data` 中**幂等创建**,无论 flag 真假都会有 |
|
||||
| **flag 仅控代码分支** | 关闭时跳过:JWT 派生叶子 Tenant、`user_tenant` 读取、Tenant 管理 API、前端菜单显示等 |
|
||||
| **不支持热更新** | 改 `config.yaml` 必须**重启容器**才生效(`ConfigService.load_settings_from_yaml()` 仅在 lifespan 启动时执行一次) |
|
||||
| **不支持环境变量** | 只能改 YAML,没有 `BS_MULTI_TENANT_ENABLED` |
|
||||
| **不支持灰度** | 单 DB 共用,无法只在部分节点启用 |
|
||||
|
||||
### 1.2 切换前检查清单
|
||||
|
||||
```text
|
||||
□ 确认变更窗口(建议业务低峰期,预留 30 min 弹性)
|
||||
□ 完成 MySQL 全量备份(mysqldump)
|
||||
□ 完成 MinIO 桶元数据备份(mc mirror 或 OSS-side snapshot)
|
||||
□ 确认 Milvus / ES 状态健康
|
||||
□ 通知用户:切换瞬间会有 1–3 分钟服务不可用 + 已登录用户 JWT 失效需重新登录
|
||||
□ 准备回滚预案(数据库快照恢复点)
|
||||
□ 通知 Child Tenant Admin(仅适用于回退场景)
|
||||
```
|
||||
|
||||
### 1.3 v2.5 重要决策(影响运维边界)
|
||||
|
||||
以下是 2026-04-20 之后的几条关键决策,会影响验证维度和故障排查思路:
|
||||
|
||||
| 决策日期 | 决策 | 运维影响 |
|
||||
|---------|------|---------|
|
||||
| 2026-04-20/21 | **Tenant 树形数据模型收窄**:废弃 `tenant_path` / `level` / `tenant_kind`,仅保留 `parent_tenant_id` + `share_default_to_children` | 不要在 `tenant` 表查询/排查中假设这些列存在;旧 spec 引用作废 |
|
||||
| 2026-04-20/21 | **Root Tenant 不可删/禁**(INV-T11,强制由迁移脚本创建) | 任何尝试 `DELETE FROM tenant WHERE id=1` 会破坏系统;禁用 API 也会拒绝 |
|
||||
| 2026-04-20/21 | **仅私有化部署**,一个实例只服务一个集团 | 不支持多客户共存;不要在同一 DB 上混合多个组织 |
|
||||
| 2026-04-19 | **F019 admin-scope** 恢复"管理视图切换"语义(不切换用户归属,仅写 Redis);详见 §2.4 | 关闭多租户前必须清 `admin_scope:*` Redis key |
|
||||
| 2026-04-19 | **F020 LLM 多租户**:Root 共享只读、Child Admin 自主、错误码 19801~19804;存量模型默认归 Root + 共享(F029) | 故障排查时遇到 19801(共享只读)/ 19802(不可访问)/ 19803(系统配置受限)/ 19804(端点白名单)查 §6 |
|
||||
| 2026-04-25 | **v2.5 配额收窄**:`tenant.quota_config` 仅保留 `storage_gb` 一个维度,其他 9 项(resource 数 / token / user_count 等)推到 v2.6 | 验证配额时只看 storage;其他维度未实现是**预期行为**而非缺口 |
|
||||
|
||||
> 完整决策上下文:见 `docs/PRD/2.5 权限管理体系改造 PRD/2.5 多租户需求文档.md` §6(配额)/ §7.1(LLM)/ 附录 C(决策表)/ 附录 G.1(演进路线)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 流程 A:单租户 → 多租户
|
||||
|
||||
### 2.1 流程总览
|
||||
|
||||
```
|
||||
[Step 1] 备份 →
|
||||
[Step 2] 改 config.yaml →
|
||||
[Step 3] 重启容器(DDL 自动 upgrade)→
|
||||
[Step 4] API/SQL 验证 →
|
||||
[Step 5] 灰度观察 →
|
||||
完成
|
||||
```
|
||||
|
||||
### 2.2 详细步骤
|
||||
|
||||
#### Step 1 · 数据库与存储备份(强制)
|
||||
|
||||
```bash
|
||||
# 1. MySQL 备份
|
||||
docker exec bisheng-mysql sh -c \
|
||||
'mysqldump -uroot -p"${MYSQL_ROOT_PASSWORD}" --all-databases --single-transaction --routines --triggers' \
|
||||
> /backup/bisheng_mysql_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 2. MinIO 元数据快照(如使用 mc 工具)
|
||||
mc mirror --preserve --overwrite local/bisheng /backup/bisheng_minio_$(date +%Y%m%d_%H%M%S)/
|
||||
|
||||
# 3. 容器配置备份
|
||||
cp -r ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config /backup/bisheng_config_$(date +%Y%m%d_%H%M%S)/
|
||||
```
|
||||
|
||||
**验证**:备份文件大小非 0,能 `head -n 100` 读取。
|
||||
|
||||
> ⚠️ **大库备份的健康检查震荡陷阱**:如果 backend 已在运行且 DB > 几 GB,`mysqldump --all-databases` 期间 backend 容器可能因健康检查超时被 docker 自动重启。这会导致 Step 3 真正想重启时,容器 STIME 已经在你**修改 yaml 之前**,进程加载的反而是旧 yaml(不含 `multi_tenant` 键)。
|
||||
>
|
||||
> **避坑做法**(任选其一):
|
||||
> - 备份完成后用 `docker ps --format 'table {{.Names}}\t{{.Status}}'` 记录 backend 当前 STIME,Step 3 重启后**必须**看到 STIME 跳到新值才算真重启
|
||||
> - 备份和改 yaml 之间 `sleep 30s` 让健康检查稳定
|
||||
> - 备份分库执行(`--databases bisheng` 而非 `--all-databases`),减小锁/IO 压力
|
||||
|
||||
#### Step 2 · 修改配置
|
||||
|
||||
**文件位置**:`${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config.yaml`
|
||||
(容器内挂载点:`/app/bisheng/config.yaml`)
|
||||
|
||||
**最小化改动**:
|
||||
```yaml
|
||||
multi_tenant:
|
||||
enabled: true
|
||||
default_tenant_code: "default"
|
||||
admin_scope_ttl_seconds: 14400 # 超管 admin-scope Redis TTL(秒),默认 4h,滑动续期
|
||||
```
|
||||
|
||||
> ⚠️ **当前 `MultiTenantConf` 仅支持上述三个字段**(见 `src/backend/bisheng/core/config/multi_tenant.py`)。早期文档/讨论中出现的 `group_shared_by_default` 字段已不存在,请勿照抄。
|
||||
|
||||
**保存后切勿热重载** —— 必须走 Step 3 重启。
|
||||
|
||||
#### Step 3 · 重启容器
|
||||
|
||||
```bash
|
||||
cd /path/to/bisheng/docker
|
||||
|
||||
# 优雅重启(推荐,影响所有容器)
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
|
||||
# 跟踪启动日志(关注 alembic 是否成功)
|
||||
docker-compose logs -f bisheng-backend | grep -E "alembic|multi_tenant|Default tenant|Loading"
|
||||
```
|
||||
|
||||
> 💡 **只重 backend + worker(更快,不动 mysql / milvus / es / minio)**:
|
||||
> ```bash
|
||||
> docker restart bisheng-backend bisheng-backend-worker
|
||||
> ```
|
||||
> 用 container 名直接走 `docker restart`,绕开 `docker-compose restart` 的 service 名差异(不同部署里 service 名可能是 `backend` 而不是 `bisheng-backend`)。停机时间从 1-3 min 缩到 30s。**但**:此路径不会重跑 entrypoint 里的 `alembic upgrade head` 之外的初始化步骤,仅适用于「DDL 已跑完、只需 reload 配置」的场景(即 §0 速查的"快速路径")。
|
||||
|
||||
**预期日志关键字**:
|
||||
```
|
||||
DEBUG bisheng.common.services.config_service Loading multi_tenant from /app/bisheng/config.yaml
|
||||
INFO alembic.runtime.migration Running upgrade ... -> v2_5_1_f013, ...
|
||||
INFO bisheng.core.database.tenant_filter Tenant filter events registered for 17 tables
|
||||
INFO bisheng.common.init_data Default tenant ready (id=1); ...
|
||||
INFO uvicorn Application startup complete.
|
||||
```
|
||||
|
||||
> ⚠️ **`Loading multi_tenant ...` 这行 DEBUG 是关键证据**:它是进程启动时打的,代表本次启动确实读到了带 `multi_tenant` 键的 yaml。
|
||||
> - 缺这行 + 后续 `/api/v1/env` 返回 `false` → yaml 修改时间晚于进程启动时间,**重新 `docker restart bisheng-backend bisheng-backend-worker`**
|
||||
> - 有这行 + `/api/v1/env` 仍返回 `false` → 真异常,看完整 ERROR 日志
|
||||
|
||||
**启动顺序**(参考 `docker/bisheng/entrypoint.sh:32-36`):
|
||||
1. `alembic upgrade head` → 跑全部迁移(含 F001 / F011 / F012 / F013)
|
||||
2. `uvicorn bisheng.main:app` → lifespan 触发 `init_default_data()`
|
||||
3. backend-worker、celery-beat 各自启动并监听队列
|
||||
|
||||
> ⚠️ 如果 alembic 失败,entrypoint 会打印 WARNING 但**继续启动应用**。务必检查日志确认迁移真的过了。
|
||||
|
||||
#### Step 4 · 启用验证
|
||||
|
||||
**SQL 层**:
|
||||
```sql
|
||||
-- ① Root Tenant 已建
|
||||
SELECT id, tenant_code, tenant_name, status, parent_tenant_id
|
||||
FROM tenant WHERE id = 1;
|
||||
-- 预期:id=1, tenant_code='default', status='active', parent_tenant_id=NULL
|
||||
|
||||
-- ② 业务表已加 tenant_id 列(举例 chatmessage)
|
||||
SHOW COLUMNS FROM chatmessage LIKE 'tenant_id';
|
||||
-- 预期:Field=tenant_id, Type=int, Null=NO, Default=1
|
||||
|
||||
-- ③ 全量用户已写入 user_tenant
|
||||
SELECT COUNT(*) FROM user_tenant WHERE status='active';
|
||||
-- 预期:等于 user 表存量行数
|
||||
|
||||
-- ④ user.token_version 字段已加
|
||||
SHOW COLUMNS FROM user LIKE 'token_version';
|
||||
-- 预期:Field=token_version, Type=int, Null=NO, Default=0
|
||||
```
|
||||
|
||||
**API 层**:
|
||||
```bash
|
||||
# 健康检查
|
||||
curl -s http://<host>:7860/health
|
||||
# 预期:HTTP 200
|
||||
|
||||
# 多租户开关
|
||||
curl -s http://<host>:7860/api/v1/env | jq '.multi_tenant_enabled'
|
||||
# 预期:true
|
||||
```
|
||||
|
||||
**前端层**:
|
||||
1. 浏览器打开管理控制台
|
||||
2. 用现有超管账号登录(首次登录会自动授予 `system:global#super_admin`)
|
||||
3. 系统管理菜单应出现 **Tenant 管理** 入口
|
||||
4. 进入 Tenant 列表,应看到 1 行 Root Tenant(不可删除)
|
||||
|
||||
#### Step 5 · 灰度观察(建议 24 小时)
|
||||
|
||||
| 观察项 | 怎么看 | 异常处理 |
|
||||
|-------|--------|---------|
|
||||
| 后端错误率 | `docker-compose logs bisheng-backend \| grep ERROR` | 单租户业务路径应零变化;若大量出错排查 §6 故障排查 |
|
||||
| 用户登录正常 | 让 5–10 个用户尝试登录 | 检查 JWT 是否携带 `tenant_id=1`、`token_version=0` |
|
||||
| 知识库 / 工作流 / 对话功能正常 | 用户实测 | 检查 SQLAlchemy event 是否注入了 `WHERE tenant_id IN (...)` |
|
||||
| Celery 任务正常 | `docker-compose logs bisheng-backend-worker` | 异步任务需从 task headers 恢复 tenant 上下文 |
|
||||
|
||||
> **此阶段重要原则**:先**不挂载任何 Child Tenant**。让系统在"启用 multi-tenant flag 但仅有 Root"的形态下稳定运行 24 小时,确认无回归。挂载 Child 是后续业务动作,不属于本手册范围。
|
||||
|
||||
### 2.3 启用后的产物清单
|
||||
|
||||
| 产物 | 位置 | 是否可逆 |
|
||||
|------|------|---------|
|
||||
| `tenant` 表 + Root 行 (id=1) | MySQL | ❌ 数据永久 |
|
||||
| `user_tenant` 全量绑定记录 | MySQL | ❌ 数据永久 |
|
||||
| `auditlog` 表的 `operator_tenant_id` 等扩展列(F011) | MySQL | ❌ 列永久 |
|
||||
| 48 张业务表的 `tenant_id` 列(F001 TENANT_TABLES) | MySQL | ❌ 列永久 |
|
||||
| `user.token_version` 列 | MySQL | ❌ 列永久 |
|
||||
| `department.is_tenant_root / mounted_tenant_id` | MySQL | ❌ 列永久 |
|
||||
| OpenFGA `tenant` 类型 schema | FGA store | ❌ schema 永久 |
|
||||
| 前端 Tenant 管理菜单(仅显示) | runtime | ✅ flag 关闭即隐藏 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 admin-scope 管理视图切换(v2.5.1 F019 必读)
|
||||
|
||||
启用多租户后,超管仅能在「整树视图」与「单 Tenant 视图」之间切换管理上下文,**不切换用户归属**。这是运维和 QA 必须理解的运行时机制。
|
||||
|
||||
#### 2.4.1 工作机制
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **API 入口** | `POST /api/v1/admin/tenant-scope`(设置 / 清除);路由位于 `src/backend/bisheng/admin/api/endpoints/tenant_scope.py` |
|
||||
| **持久化** | Redis key `admin_scope:{user_id}`,TTL = `multi_tenant.admin_scope_ttl_seconds`(默认 14400 秒 / 4 小时),每次命中管理 API 时**滑动续期** |
|
||||
| **不重签 JWT** | 仅写 Redis,不动 token,登出/超时自然失效 |
|
||||
| **作用范围** | 仅管理类 API(`/llm`、`/roles`、`/tenants/{id}/quota`、`/audit_log` 等)读 Redis 把 IN 列表覆盖为 `{scope_tenant_id, root_id}`;普通业务 API 不受影响 |
|
||||
| **权限要求** | 仅全局超管(`system:global#super_admin`)可调用,否则返 403 + 错误码 19701 (`admin_scope_forbidden`) |
|
||||
|
||||
#### 2.4.2 失效时机(6 条)
|
||||
|
||||
为避免越权或脏视图,admin-scope 在以下任一情况会被清除:
|
||||
|
||||
1. 用户主动调 `POST /api/v1/admin/tenant-scope` 传空值
|
||||
2. 用户登出 / JWT 失效
|
||||
3. Redis TTL 到期
|
||||
4. 目标 Child Tenant 被禁用 / 归档 / 删除
|
||||
5. 用户失去超管角色
|
||||
6. 后端重启(Redis 持久化策略决定,依赖部署配置)
|
||||
|
||||
#### 2.4.3 给 QA 的测试要点
|
||||
|
||||
- **入口**:模型管理(`/model`)页面顶部 ScopeBar(仅超管可见,见 `src/frontend/platform/src/pages/ModelPage/manage/index.tsx:189`)
|
||||
- **切换后行为**:模型列表、配额面板、审计日志按所选 scope 过滤;前端右上角应显示当前 scope 标识
|
||||
- **典型 BUG 排查**:QA 反馈"切了 scope 但列表没变" → 检查浏览器 DevTools 的 API 请求是否带了 scope cookie / header,以及 Redis 是否真的写入
|
||||
|
||||
```bash
|
||||
# 排查指令
|
||||
docker exec bisheng-redis redis-cli GET admin_scope:<user_id>
|
||||
docker exec bisheng-redis redis-cli TTL admin_scope:<user_id>
|
||||
```
|
||||
|
||||
#### 2.4.4 关闭多租户前必须清理
|
||||
|
||||
走 §3.2 / §3.3 回退流程之前,**先清空 admin_scope Redis key**(手册 §3.2 Step 4 已包含此步骤)。否则极少数情况下短暂保留的 key 会让超管"切换到一个不存在的 scope",触发 fallback 报错。
|
||||
|
||||
---
|
||||
|
||||
## 3. 流程 B:多租户 → 单租户
|
||||
|
||||
### 3.1 决策树(先回答这个问题)
|
||||
|
||||
```
|
||||
是否在多租户模式下挂载过 Child Tenant?
|
||||
├─ 否 → 走 §3.2 软回退(10 min)
|
||||
└─ 是
|
||||
├─ Child 下产生过业务数据(知识库、对话、文件、工作流)?
|
||||
│ ├─ 否 → 走 §3.2 软回退(先在管理后台手工解绑/删除空 Child;解绑请使用策略 A "迁移到 Root",详见《多租户需求文档》§5.2.2)
|
||||
│ └─ 是
|
||||
│ ├─ 数据可丢弃 → 走 §3.3 数据迁回 + §3.2 软回退
|
||||
│ └─ 数据不可丢弃 → ⚠️ **强烈建议保持多租户模式**,或考虑数据库快照回滚到启用前
|
||||
```
|
||||
|
||||
> 💡 **判断依据 SQL**:
|
||||
> ```sql
|
||||
> -- 是否挂载过 Child?
|
||||
> SELECT COUNT(*) FROM tenant WHERE id != 1;
|
||||
>
|
||||
> -- Child 下是否有业务数据?
|
||||
> SELECT tenant_id, COUNT(*) FROM chatmessage WHERE tenant_id != 1 GROUP BY tenant_id;
|
||||
> SELECT tenant_id, COUNT(*) FROM knowledge WHERE tenant_id != 1 GROUP BY tenant_id;
|
||||
> SELECT tenant_id, COUNT(*) FROM flow WHERE tenant_id != 1 GROUP BY tenant_id;
|
||||
> ```
|
||||
|
||||
### 3.2 软回退(无 Child 数据,推荐路径)
|
||||
|
||||
#### Step 1 · 备份(同 §2.2 Step 1)
|
||||
|
||||
#### Step 2 · 清理 Child Tenant(如果有空 Child)
|
||||
|
||||
```sql
|
||||
-- 在管理后台或直接 SQL 删除(仅当 user_count=0 才允许)
|
||||
-- 推荐走 API(自动清理 user_tenant、department、FGA tuple)
|
||||
DELETE FROM /api/v1/tenants/{child_id}
|
||||
```
|
||||
|
||||
或者用 SQL 强制删除(**仅在确认 Child 无关联数据时**):
|
||||
```sql
|
||||
DELETE FROM user_tenant WHERE tenant_id != 1;
|
||||
UPDATE department SET is_tenant_root=0, mounted_tenant_id=NULL WHERE is_tenant_root=1 AND mounted_tenant_id != 1;
|
||||
DELETE FROM tenant WHERE id != 1;
|
||||
```
|
||||
|
||||
#### Step 3 · 修改配置
|
||||
|
||||
```yaml
|
||||
multi_tenant:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
#### Step 4 · 清理 Redis 残留
|
||||
|
||||
```bash
|
||||
docker exec bisheng-redis redis-cli --scan --pattern 'admin_scope:*' | \
|
||||
xargs -r docker exec bisheng-redis redis-cli DEL
|
||||
|
||||
docker exec bisheng-redis redis-cli --scan --pattern 'dept:*' | \
|
||||
xargs -r docker exec bisheng-redis redis-cli DEL
|
||||
|
||||
docker exec bisheng-redis redis-cli --scan --pattern 'user:*:leaf_tenant' | \
|
||||
xargs -r docker exec bisheng-redis redis-cli DEL
|
||||
```
|
||||
|
||||
#### Step 5 · 重启容器
|
||||
|
||||
```bash
|
||||
cd /path/to/bisheng/docker
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
docker-compose logs -f bisheng-backend | grep -E "multi_tenant|startup"
|
||||
```
|
||||
|
||||
#### Step 6 · 验证
|
||||
|
||||
```bash
|
||||
curl -s http://<host>:7860/api/v1/env | jq '.multi_tenant_enabled'
|
||||
# 预期:false
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Tenant 表只剩 Root
|
||||
SELECT COUNT(*) FROM tenant; -- 预期:1
|
||||
-- user_tenant 全部指向 Root
|
||||
SELECT DISTINCT tenant_id FROM user_tenant; -- 预期:仅 1
|
||||
```
|
||||
|
||||
#### Step 7 · 通知用户重新登录
|
||||
|
||||
软回退**不强制 token_version 自增**,旧 JWT 仍有效。但建议清理一次让用户体验一致:
|
||||
```sql
|
||||
UPDATE user SET token_version = token_version + 1;
|
||||
```
|
||||
所有用户下次请求 401,重新登录后获得 `tenant_id=1` 的 JWT。
|
||||
|
||||
### 3.3 硬回退(已挂载 Child + 业务数据)
|
||||
|
||||
> ⚠️ **此流程含数据破坏性操作。三选一,由业务方决策**:
|
||||
> - **方案 A:保留多租户模式**(推荐,无需操作)
|
||||
> - **方案 B:用切换前的数据库快照整体回滚**(最干净,但丢失启用期间所有变更)
|
||||
> - **方案 C:本节流程,逐项把 Child 数据迁回 Root**(保留启用期间的业务变更,但需手工迁移)
|
||||
|
||||
下面给出方案 C 的步骤。**执行前务必演练在 staging。**
|
||||
|
||||
#### Step 1 · 进入维护模式(停止业务写入)
|
||||
|
||||
```bash
|
||||
# 停止前端容器,仅保留后端用于迁移
|
||||
docker-compose stop bisheng-frontend
|
||||
|
||||
# 停止 worker,避免任务执行中干扰
|
||||
docker-compose stop bisheng-backend-worker
|
||||
```
|
||||
|
||||
#### Step 2 · 业务数据 tenant_id 改回 Root
|
||||
|
||||
```sql
|
||||
-- 通用模板(48 张表逐个执行;以 chatmessage 举例)
|
||||
UPDATE chatmessage SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
UPDATE flow SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
UPDATE flowversion SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
UPDATE knowledge SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
UPDATE knowledgefile SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
UPDATE assistant SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
UPDATE auditlog SET tenant_id = 1 WHERE tenant_id != 1;
|
||||
-- ... 完整清单见 src/backend/bisheng/core/database/alembic/versions/v2_5_0_f001_multi_tenant.py 的 TENANT_TABLES (48 张)
|
||||
```
|
||||
|
||||
> ⚠️ **F020 唯一约束陷阱**:`llm_server` 在 v2.5.1 F020 中把 `UNIQUE(name)` 改为 `UNIQUE(tenant_id, name)`。若 Child 与 Root 存在同名 LLM,直接 `UPDATE llm_server SET tenant_id=1` 会触发约束冲突。**先去重**:
|
||||
> ```sql
|
||||
> -- 找出冲突项
|
||||
> SELECT name, GROUP_CONCAT(id), GROUP_CONCAT(tenant_id)
|
||||
> FROM llm_server WHERE tenant_id != 1
|
||||
> AND name IN (SELECT name FROM llm_server WHERE tenant_id = 1)
|
||||
> GROUP BY name;
|
||||
> -- 改名 / 删除 / 决策保留方
|
||||
> ```
|
||||
> `group` / `role` 表也有同类约束(F003 / F005),同样需要预清理。
|
||||
|
||||
#### Step 3 · MinIO 路径合并(把 Child 文件搬出 `tenant_{code}/` 到根目录)
|
||||
|
||||
> 🔴 **执行顺序硬约束(必读)**:
|
||||
> `_should_fallback_to_root()` (`src/backend/bisheng/core/storage/minio/minio_storage.py:50-58`) 在 `multi_tenant.enabled=false` 时**直接返回 false**,不再回退到 tenant 前缀路径。
|
||||
>
|
||||
> **正确顺序**:迁 MinIO 文件 → 业务表 UPDATE tenant_id → 改 flag → 重启
|
||||
> **错误顺序**:先关 flag → 立即所有 `tenant_*/` 路径下的文件 NoSuchKey,知识库下载/预览全部 404
|
||||
>
|
||||
> 因此本节(Step 3)必须在 Step 8 切 flag **之前**完成。
|
||||
|
||||
bucket 名:默认 `bisheng`(见 `docker/bisheng/config/config.yaml:64` 的 `public_bucket`)。如有自定义请替换。
|
||||
|
||||
```bash
|
||||
# 列出所有 Child 路径
|
||||
mc ls local/bisheng/ | grep '^.*tenant_'
|
||||
|
||||
# 对每个 Child 子目录搬迁(会与 Root 文件冲突时需先重命名!)
|
||||
for prefix in $(mc ls local/bisheng/ | awk '/tenant_/ {print $NF}'); do
|
||||
echo "Moving $prefix -> root"
|
||||
mc mv --recursive local/bisheng/$prefix local/bisheng/
|
||||
done
|
||||
```
|
||||
|
||||
> ⚠️ 如果 Child 与 Root 有同名 key(例如 `knowledge/file_xxx.pdf`),合并会冲突。**必须先备份再操作,并人工解决冲突。**
|
||||
|
||||
#### Step 4 · Milvus Collection 数据迁移
|
||||
|
||||
> Milvus 不支持改名 collection。流程:从 `t{child_id}_xxx` 集合 dump 出向量 → 再 insert 到 Root 的对应 collection。
|
||||
|
||||
```python
|
||||
# 大致 Python 伪代码(具体执行需根据知识库 model 维度调整)
|
||||
from pymilvus import Collection, connections
|
||||
connections.connect(host='bisheng-milvus-standalone', port=19530)
|
||||
|
||||
child_col = Collection('t2_col_xxx') # 子租户 collection
|
||||
root_col = Collection('col_xxx') # Root collection
|
||||
# 分批导出并写入 Root collection
|
||||
# 完成后删除 child_col
|
||||
child_col.drop()
|
||||
```
|
||||
|
||||
> 💡 如果 Child 知识库不重要,可以直接 drop 不迁移(业务上等于"知识库被清空")。
|
||||
|
||||
#### Step 5 · Elasticsearch / Redis 清理
|
||||
|
||||
```bash
|
||||
# ES 索引:先确认无业务依赖再删除
|
||||
curl -X DELETE http://bisheng-elasticsearch:9200/t*_*
|
||||
|
||||
# Redis 子租户 key
|
||||
docker exec bisheng-redis redis-cli --scan --pattern 't:*:*' | \
|
||||
xargs -r docker exec bisheng-redis redis-cli DEL
|
||||
```
|
||||
|
||||
#### Step 6 · OpenFGA Tuple 清理(可选,技术债收敛)
|
||||
|
||||
```bash
|
||||
# 列出所有 tenant#admin、shared_to 关系(参考 OpenFGA Playground 或 CLI)
|
||||
fga tuple read --store-id $STORE_ID --type tenant
|
||||
# 逐个 fga tuple delete ...
|
||||
```
|
||||
|
||||
> 不清理也不影响 enabled=false 运行(FGA 不参与单租户路径),但会留 orphan 数据。
|
||||
|
||||
#### Step 7 · 删除 Child Tenant 行 + 复原 department 标记
|
||||
|
||||
```sql
|
||||
DELETE FROM user_tenant WHERE tenant_id != 1;
|
||||
INSERT IGNORE INTO user_tenant(user_id, tenant_id, is_default, status)
|
||||
SELECT id, 1, 1, 'active' FROM user;
|
||||
|
||||
UPDATE department SET is_tenant_root=0, mounted_tenant_id=NULL
|
||||
WHERE is_tenant_root=1 AND mounted_tenant_id != 1;
|
||||
|
||||
UPDATE department SET tenant_id=1 WHERE tenant_id != 1;
|
||||
|
||||
DELETE FROM tenant WHERE id != 1;
|
||||
```
|
||||
|
||||
#### Step 8 · 切 flag + 强制全员重新登录
|
||||
|
||||
```yaml
|
||||
multi_tenant:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
```sql
|
||||
UPDATE user SET token_version = token_version + 1;
|
||||
```
|
||||
|
||||
#### Step 9 · 重启全部容器
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
#### Step 10 · 验证
|
||||
|
||||
参考 §3.2 Step 6 的验证项 + 抽查若干原 Child 用户的:
|
||||
- 历史对话记录可见
|
||||
- 历史知识库文件可读(PDF 可下载)
|
||||
- 工作流可正常运行
|
||||
|
||||
---
|
||||
|
||||
## 4. 验证清单(双向通用)
|
||||
|
||||
### 4.1 数据一致性
|
||||
|
||||
```sql
|
||||
-- 用户与 user_tenant 一一对应(多租户开启时)
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM user) AS user_count,
|
||||
(SELECT COUNT(DISTINCT user_id) FROM user_tenant WHERE status='active') AS bound_count;
|
||||
|
||||
-- 业务表 tenant_id 分布
|
||||
SELECT 'chatmessage' tbl, tenant_id, COUNT(*) cnt FROM chatmessage GROUP BY tenant_id
|
||||
UNION ALL
|
||||
SELECT 'flow', tenant_id, COUNT(*) FROM flow GROUP BY tenant_id
|
||||
UNION ALL
|
||||
SELECT 'knowledge', tenant_id, COUNT(*) FROM knowledge GROUP BY tenant_id;
|
||||
-- 单租户期望:tenant_id 全为 1
|
||||
-- 多租户期望:tenant_id 分布在 {1, child_1, child_2, ...}
|
||||
```
|
||||
|
||||
### 4.2 服务健康
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
# 全部应 State=Up,bisheng-backend healthcheck=healthy
|
||||
|
||||
curl -s http://<host>:7860/health
|
||||
# HTTP 200
|
||||
|
||||
curl -s http://<host>:7860/api/v1/env | jq
|
||||
# 字段齐全,multi_tenant_enabled 正确
|
||||
```
|
||||
|
||||
### 4.3 端到端
|
||||
|
||||
| 场景 | 检查 |
|
||||
|------|------|
|
||||
| 用户登录 | JWT 解码后 `tenant_id` 字段正确 |
|
||||
| 知识库上传 | 文件落到正确的 MinIO 路径(Root=无前缀;Child=`tenant_{code}/`) |
|
||||
| 知识库召回 | Milvus 查询能命中(看后端日志的 collection 名) |
|
||||
| 对话历史 | 切换前后用户能看到自己的对话(注意 tenant_id 必须匹配) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 回滚预案
|
||||
|
||||
| 触发条件 | 回滚动作 | 数据损失 |
|
||||
|---------|---------|----------|
|
||||
| Step 3 alembic 失败 | 立即停容器,从 §2.2 Step 1 备份恢复 MySQL,回滚 config.yaml | 备份点之后的所有变更 |
|
||||
| Step 4 验证失败 | 改 `enabled: false`,重启 | 列已加但被忽略,无业务影响 |
|
||||
| 启用 24h 内业务异常但已挂 Child | 走 §3.3 硬回退 或 整库快照恢复 | 看选择 |
|
||||
| 启用 7 天后发现兼容问题 | 优先排查代码 bug,**避免回退**(数据迁移成本极高) | — |
|
||||
|
||||
---
|
||||
|
||||
## 6. 故障排查
|
||||
|
||||
| 症状 | 可能原因 | 排查 |
|
||||
|------|---------|------|
|
||||
| 启用后用户登录 401 | JWT token_version 不匹配 | 让用户清缓存重新登录;检查 `user.token_version` 是否被异常自增 |
|
||||
| 启用后业务表查询返回空 | SQLAlchemy event IN 列表注入异常 | 检查 `current_tenant_id` ContextVar 是否设置;看后端 SQL 日志 |
|
||||
| 知识库列表为空 | tenant_id 不匹配 | `SELECT tenant_id, COUNT(*) FROM knowledge GROUP BY tenant_id` 看分布 |
|
||||
| MinIO 文件 404 | 路径前缀错乱 | 检查 `tenant_storage.py` 当前用的前缀逻辑;对照 `tenant.tenant_code` |
|
||||
| OpenFGA 报错 | tuple 数量爆炸或 schema 不一致 | 检查 OpenFGA 容器日志;可考虑切到 PostgreSQL 后端 |
|
||||
| 容器启动卡在 alembic | 大表加列锁表 | 等待(chatmessage 行数大时可能 5–30 min);勿强杀 |
|
||||
| Celery 任务执行失败 | 异步任务未恢复 tenant 上下文 | 检查 `task headers` 透传逻辑、worker 日志 |
|
||||
| 超管切了 admin-scope 但视图没变 | Redis 写入失败 / 前端 cache | `redis-cli GET admin_scope:<user_id>` + `TTL`;检查前端 `/api/v1/llm` 等管理 API 的请求是否带 scope 上下文 |
|
||||
| 19701 `admin_scope_forbidden` | 非超管尝试调 `/admin/tenant-scope` | 确认调用者具备 `system:global#super_admin` 关系(OpenFGA 检查) |
|
||||
| 19801 `llm_model_shared_readonly` | Child Admin 试图编辑 Root 共享模型 | 预期行为;走 F020 设计,UI 应禁用编辑按钮 |
|
||||
| 19802 `llm_model_not_accessible` | 跨 tenant 取不到模型 | 检查 `llm_server.tenant_id` 与调用者上下文,必要时排查 IN 列表注入 |
|
||||
| 19803 `llm_system_config_forbidden` | **F022 修订**:跨 tenant 写入系统级 LLM 配置(target tenant_id ∉ caller's manageable set)。Child Admin 写自己 Child 的配置返 200,仅构造 PUT 试图写 Root 或其他 Child 才返 19803 | 预期;自然路径下 admin-scope 已确保 target = 自己 leaf,不会触发 |
|
||||
| 19804 `llm_endpoint_not_whitelisted` | Child 配置的 LLM endpoint 不在白名单 | 检查 `llm.endpoint_whitelist` 配置 |
|
||||
| 关闭 flag 后 MinIO 文件 NoSuchKey | 先关 flag、后迁文件(顺序错) | 立即从备份恢复 MinIO;按 §3.3 Step 3 警告框中的正确顺序重做 |
|
||||
|
||||
---
|
||||
|
||||
## 7. FAQ
|
||||
|
||||
**Q1:启用多租户后能立即给客户演示吗?**
|
||||
A:不建议。先在 Root 形态下稳定运行 24 小时,确认无回归;再按业务节奏挂载 Child。
|
||||
|
||||
**Q2:alembic 跑了一半失败了怎么办?**
|
||||
A:先看日志确定失败的迁移版本号。如果是非破坏性失败(如索引创建超时),可以手工补完。如果是破坏性失败,从备份恢复 MySQL,回滚 config,再排查迁移脚本。
|
||||
|
||||
**Q3:能否只对部分用户启用多租户?**
|
||||
A:不能。`multi_tenant.enabled` 是全局 flag,无灰度能力。
|
||||
|
||||
**Q4:升级到 v2.5 但暂时不想用多租户,可以保持 false 吗?**
|
||||
A:可以。DDL 会跑(业务表加 `tenant_id` 列),但代码层完全走单租户路径,零行为变化。
|
||||
|
||||
**Q5:启用期间 backend-worker 容器忘了重启会怎样?**
|
||||
A:worker 用旧 config,可能不会按多租户语义透传 tenant 上下文,导致异步任务的 `tenant_id` 错乱。**两个后端容器必须同步重启**。
|
||||
|
||||
**Q6:能不能跳过备份直接切换?**
|
||||
A:测试环境可以;生产环境**绝对不行**。alembic 给大表加列没有原子回滚保证,备份是唯一可靠的兜底。
|
||||
|
||||
**Q7:硬回退后,原 Child Admin 还是管理员吗?**
|
||||
A:硬回退会清理 OpenFGA 的 `tenant#admin` tuple。原 Child Admin 在单租户模式下仅保留其在 RBAC 中的 AdminRole(如果有);如果没有,会变成普通用户。需提前做角色规划。
|
||||
|
||||
**Q8:MinIO 数据是否要在切换前迁移?**
|
||||
A:从单租户切到多租户,**Root 数据零迁移**(继续用无前缀路径)。从多租户切回单租户,**必须迁移 Child 路径数据**(参考 §3.3 Step 3)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 附录
|
||||
|
||||
### 8.1 关键路径速查
|
||||
|
||||
| 类别 | 路径 |
|
||||
|------|------|
|
||||
| 后端配置文件 | `${DOCKER_VOLUME_DIRECTORY}/bisheng/config/config.yaml` |
|
||||
| 容器内配置路径 | `/app/bisheng/config.yaml` |
|
||||
| Compose 文件 | `docker/docker-compose.yml` |
|
||||
| 启动脚本 | `docker/bisheng/entrypoint.sh` |
|
||||
| Alembic 版本目录 | `src/backend/bisheng/core/database/alembic/versions/` |
|
||||
| 多租户配置模型 | `src/backend/bisheng/core/config/multi_tenant.py` |
|
||||
| Root Tenant 初始化 | `src/backend/bisheng/common/init_data.py` (`_init_default_tenant`) |
|
||||
| 前端开关消费 | `/api/v1/env` 返回的 `multi_tenant_enabled` |
|
||||
| Tenant 树 spec | `features/v2.5.1/011-tenant-tree-model/spec.md` |
|
||||
|
||||
### 8.2 容器服务一览(默认端口)
|
||||
|
||||
| 容器 | 端口 | 角色 |
|
||||
|------|------|------|
|
||||
| bisheng-mysql | 3306 | 业务数据库 |
|
||||
| bisheng-openfga | 8080 / 8081 / 3000 | ReBAC 引擎 |
|
||||
| bisheng-redis | 6379 | 缓存 + admin-scope |
|
||||
| bisheng-backend | 7860 | 主 API(含 alembic upgrade) |
|
||||
| bisheng-backend-worker | — | Celery worker(必须与 backend 同步重启) |
|
||||
| bisheng-frontend | 3001 | 静态前端 |
|
||||
| bisheng-elasticsearch | 9200 | 检索 |
|
||||
| bisheng-milvus-standalone | 19530 | 向量库 |
|
||||
| bisheng-milvus-minio | 9000 / 9001 | 对象存储 |
|
||||
|
||||
### 8.3 多租户相关迁移版本
|
||||
|
||||
> 截至 2026-05-07,v2.5 系列共 22 个迁移文件涉及多租户语义;硬回退时必须按依赖顺序处理。最新追加:F034 `tenant_system_model_config`(F022 系统级 LLM 配置按租户隔离)。
|
||||
|
||||
| revision | 文件 | 主要动作 |
|
||||
|----------|------|---------|
|
||||
| `v2_5_0_f001_multi_tenant` | `v2_5_0_f001_multi_tenant.py` | 创建 `tenant` / `user_tenant` 表,给 **48 张**业务表加 `tenant_id`(TENANT_TABLES 清单见文件 L25-82) |
|
||||
| `v2_5_0_f003_user_group` | `v2_5_0_f003_user_group.py` | `group` 表 UNIQUE 改 `(tenant_id, group_name)`、加 `visibility` |
|
||||
| `v2_5_0_f004_rebac` | `v2_5_0_f004_rebac.py` | 创建 `failed_tuple` 表(FGA 补偿队列) |
|
||||
| `v2_5_0_f005_role_menu_quota` | `v2_5_0_f005_role_menu_quota.py` | `role` UNIQUE 改 `(tenant_id, role_type, role_name)`、冲突角色改名 `<name>-dup-<id>` |
|
||||
| `v2_5_1_f011_tenant_tree` | `v2_5_1_f011_tenant_tree.py` | 树形字段 + `department.is_tenant_root/mounted_tenant_id` + `auditlog` 扩展列 |
|
||||
| `v2_5_1_f012_user_token_version` | `v2_5_1_f012_user_token_version.py` | `user.token_version` |
|
||||
| `v2_5_1_f013_auditlog_tenant_id_nullable` | `v2_5_1_f013_auditlog_tenant_id_nullable.py` | `auditlog.tenant_id` 改可空(修登录回归) |
|
||||
| `v2_5_1_f014_sso_sync_fields` | `v2_5_1_f014_sso_sync_fields.py` | SSO 同步字段(与多租户用户归属交互) |
|
||||
| `v2_5_1_f017_is_shared` | `v2_5_1_f017_is_shared.py` | F017 资源共享:`knowledge.is_shared` 等共享标志位 |
|
||||
| `v2_5_1_f017_llm_token_log` | `v2_5_1_f017_llm_token_log.py` | LLM token 日志按 tenant 计量基础设施 |
|
||||
| `v2_5_1_f020_llm_tenant` | `v2_5_1_f020_llm_tenant.py` | ⚠️ **`llm_server` UNIQUE 改 `(tenant_id, name)`** — 硬回退要预清理 |
|
||||
| `v2_5_1_f021_department_knowledge_space` | `v2_5_1_f021_department_knowledge_space.py` | 部门知识空间维度 |
|
||||
| `v2_5_1_f022_approval_request` | `v2_5_1_f022_approval_request.py` | 审批请求(含 tenant_id) |
|
||||
| `v2_5_1_f023_department_admin_membership_overlay` | (文件存在的话) | 部门管理员成员资格覆盖 |
|
||||
| `v2_5_1_f024_department_space_per_space_approval_settings` | (同上) | 知识空间审批设置 |
|
||||
| `v2_5_1_f024_role_creator` | `v2_5_1_f024_role_creator.py` | role.creator 字段 |
|
||||
| `v2_5_1_f026_role_scope_name_unique` | `v2_5_1_f026_role_scope_name_unique.py` | role 范围名唯一约束 |
|
||||
| `v2_5_1_f026_department_admin_grant` | `v2_5_1_f026_department_admin_grant.py` | 部门管理员授权 |
|
||||
| `v2_5_1_f026_chatmessage_files_longtext` | `v2_5_1_f026_chatmessage_files_longtext.py` | chatmessage 字段扩容 |
|
||||
| `v2_5_1_f027_role_scope_nullsafe_unique` | `v2_5_1_f027_role_scope_nullsafe_unique.py` | role 范围 nullsafe 唯一 |
|
||||
| `v2_5_1_f028_workbench_menu_keys_backfill` | `v2_5_1_f028_workbench_menu_keys_backfill.py` | 工作台菜单 keys 回填(DML) |
|
||||
| `v2_5_1_f029_llm_shared_backfill` | `v2_5_1_f029_llm_shared_backfill.py` | F029 存量 LLM 归 Root + 默认共享(DML) |
|
||||
| `v2_5_1_f030_tenant_root_dept_id_backfill` | `v2_5_1_f030_tenant_root_dept_id_backfill.py` | ⚠️ **`tenant.root_dept_id` 回填,无 downgrade** — 硬回退残留点 |
|
||||
| `v2_5_1_f034_tenant_system_model_config` | `v2_5_1_f034_tenant_system_model_config.py` | F022 系统级 LLM 配置按租户隔离:建表 + 从 `config` backfill 5 个 key 到 `tenant_id=1`(INSERT IGNORE 幂等);downgrade 仅 DROP 新表,旧 `config` 5 行作为 rollback 锚点保留 |
|
||||
|
||||
### 8.4 命令速查
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
docker exec bisheng-mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases --single-transaction' > backup.sql
|
||||
|
||||
# 配置编辑(vim 示例)
|
||||
vim ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config.yaml
|
||||
|
||||
# 重启
|
||||
docker-compose down && docker-compose up -d
|
||||
|
||||
# 跟启动日志
|
||||
docker-compose logs -f bisheng-backend | grep -E "alembic|multi_tenant|Default tenant|startup"
|
||||
|
||||
# 验证 flag
|
||||
curl -s http://localhost:7860/api/v1/env | jq '.multi_tenant_enabled'
|
||||
|
||||
# 验证 Root Tenant
|
||||
docker exec bisheng-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" bisheng \
|
||||
-e 'SELECT id, tenant_code, status, parent_tenant_id FROM tenant'
|
||||
|
||||
# 强制全员重新登录
|
||||
docker exec bisheng-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" bisheng \
|
||||
-e 'UPDATE user SET token_version = token_version + 1'
|
||||
|
||||
# 清理 admin-scope Redis
|
||||
docker exec bisheng-redis redis-cli --scan --pattern 'admin_scope:*' | \
|
||||
xargs -r docker exec bisheng-redis redis-cli DEL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 文档版本
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2026-04-25 | 首版:覆盖单 → 多、多 → 单(软/硬两种)、验证、回滚、故障排查 |
|
||||
| v1.1 | 2026-04-26 | 修正:①TENANT_TABLES 数量 45→48;②删除不存在的 `group_shared_by_default` 配置;③`audit_log` → `auditlog`(且非新建是加列)。新增:①§1.3 v2.5 关键决策(树形收窄、Root 不可删、配额收窄、F020 错误码);②§2.4 admin-scope 整章;③§3.3 Step 3 MinIO 路径分裂硬约束(先迁文件后关 flag);④§3.3 Step 2 F020 UNIQUE 约束陷阱预清理;⑤§6 故障排查补 admin-scope/19701/19801~19804/MinIO 顺序错;⑥§8.3 迁移版本表从 4 个补到 21 个 |
|
||||
|
||||
> 维护提示:每次添加多租户相关 alembic 迁移、改动 `minio_storage.py` 前缀逻辑(特别是 `_should_fallback_to_root`)、新增 admin-scope 行为或错误码,都需要回看 §1.3 决策表、§2.4 admin-scope、§3.3 硬回退步骤、§6 故障排查、§8.3 迁移列表是否仍然完整。
|
||||
@@ -1,374 +0,0 @@
|
||||
# BiSheng 2.5 多租户需求文档
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 1. 这是什么(30 秒读懂)
|
||||
|
||||
**一句话**:把一个 BiSheng 实例变成「集团总部 + 多个独立子公司」的协作平台,公司之间数据隔离,部分资源(模型)总部统一发放。
|
||||
|
||||
**核心三件事**:
|
||||
|
||||
| # | 能力 | 用大白话讲 |
|
||||
|---|---|---|
|
||||
| 1 | 部门挂载为子租户 | 选中某个部门 → 把它整体变成一个独立子公司,下面所有员工都自动归属过去 |
|
||||
| 2 | 存储容量配额 | 管理员给每个子公司设一个知识库存储上限(GB),用满就不能再传 |
|
||||
| 3 | 模型租户隔离 + Root 共享 | 每家子公司可以自己注册模型;总部注册的模型可以选择共享给所有子公司只读使用 |
|
||||
|
||||
**不做什么**:
|
||||
- ❌ 不支持多 Root(一个实例 = 一个集团)
|
||||
- ❌ 不支持多层嵌套子公司(仅 Root + Child 两层)
|
||||
- ❌ 不做 SaaS 多客户共享部署
|
||||
- ❌ 不做模板下发、集团共享知识库等高级共享场景(v2.5 仅支持模型共享)
|
||||
- ❌ 不做用户多租户切换(每个用户在任何时刻只属于一个租户)
|
||||
|
||||
---
|
||||
|
||||
## 2. 谁是谁(角色与作用域)
|
||||
|
||||
| 角色 | 能看到 | 能操作 |
|
||||
|---|---|---|
|
||||
| **全局超管** | 整个集团:Root + 所有子租户 | 挂载/解绑子租户、改任何租户配额、跨租户查审计、临时切到任何子租户视角 |
|
||||
| **子租户管理员(Child Admin)** | 仅本子租户内的资源、成员、模型、角色 | 本子租户内成员管理、模型注册、角色配置、查本租户审计;**不能** 改配额、**不能** 挂载/解绑 |
|
||||
| **普通用户** | 本人所属租户内的资源 + 该租户能看到的 Root 共享资源 | 在本租户内创建、使用、协作 |
|
||||
|
||||
**用户与租户的关系约束**:
|
||||
- 一个用户在任何时刻**最多归属一个**子租户(叶子租户)
|
||||
- 用户归属由「用户主部门 → 沿部门树往上找最近的挂载点」自动派生,不需要也不能手工切换
|
||||
- 主部门变更会自动重算归属,旧登录态立即失效
|
||||
|
||||
**Root 租户的特殊性**:
|
||||
- 系统启动时自动创建,编号固定为 1
|
||||
- 不可禁用、不可归档、不可删除
|
||||
- 没有「Root 管理员」这个角色,Root 由全局超管直接管理
|
||||
|
||||
---
|
||||
|
||||
## 3. 单租户 vs 多租户(部署模式)
|
||||
|
||||
多租户能力有总开关,默认关闭。
|
||||
|
||||
| 状态 | 行为 |
|
||||
|---|---|
|
||||
| **关闭(默认)** | 行为等同传统单租户。系统只有一个 Root,所有数据归 Root,没有「租户管理」入口 |
|
||||
| **开启** | 管理后台出现「租户管理」入口;部门设置出现「标记为子租户」按钮;模型管理出现「管理视图切换」 |
|
||||
|
||||
**切换规则**:
|
||||
- 关 → 开:现有数据自动全部归 Root,零迁移成本
|
||||
- 开 → 关:仅当所有子租户都已解绑/归档时允许;否则拒绝
|
||||
|
||||
---
|
||||
|
||||
## 4. 子租户管理(核心工作流)
|
||||
|
||||
### 4.1 创建子租户:通过部门挂载
|
||||
|
||||
**入口**:管理后台 → 组织与成员 → 部门管理 → 选定一个部门 → 「设置」标签 → 「标记为子租户」按钮。
|
||||
|
||||
**操作流程**:
|
||||
1. 全局超管在部门树选定要变成子公司的部门
|
||||
2. 弹出对话框,填写:
|
||||
- 子租户名称(默认带入部门名,可改)
|
||||
- 初始管理员(必选,从该部门子树成员中多选)
|
||||
3. 确认后,该部门即被标记为子租户根;该部门所有成员的归属自动切到这个子租户
|
||||
|
||||
**约束**:
|
||||
- 一个部门最多挂一个子租户
|
||||
- 不允许在已挂载子租户的部门子树内再次挂载(即不允许嵌套)
|
||||
- 不允许把根部门标记为子租户(根部门永远等于 Root)
|
||||
- 仅全局超管可执行
|
||||
|
||||
**挂载发生后**:
|
||||
- 涉及成员的旧登录态立即失效,下次登录自动以子租户身份进入
|
||||
- 子租户列表立刻可见,初始管理员可登录管理本租户
|
||||
|
||||
### 4.2 编辑子租户
|
||||
|
||||
| 字段 | 是否可改 |
|
||||
|---|---|
|
||||
| 名称 | ✅ |
|
||||
| Logo | ✅ |
|
||||
| 联系信息 | ✅ |
|
||||
| 状态(启用/禁用) | ✅(仅子租户可禁用,Root 不可) |
|
||||
| 编码(tenant_code) | ❌ 创建时定,永久不变 |
|
||||
| 父级关系 | ❌ 仅 2 层,不存在改父级 |
|
||||
|
||||
**禁用子租户的后果**:
|
||||
- 该子租户内所有成员立即吊销登录态
|
||||
- 业务 API 全部拦截(拒绝读写)
|
||||
- 异步任务挂起、组织同步暂停
|
||||
- 二次确认后才生效
|
||||
|
||||
### 4.3 取消挂载(资源迁回 Root + 子租户归档)
|
||||
|
||||
**入口**:部门管理 → 已挂载的部门 → 「设置」 → 「取消挂载」。
|
||||
|
||||
**发生的事**:
|
||||
1. 该子租户的所有业务资源(知识库、应用、工具、消息等)批量改归 Root
|
||||
2. 该子租户成员归属重派生,回到 Root
|
||||
3. 子租户状态变为 **archived**(归档),编码后缀加 `#archived#时间戳` 避免冲突
|
||||
4. 写审计日志
|
||||
|
||||
**用户体验**:
|
||||
- 用户不需要重新登录,下一次请求自然落在 Root
|
||||
- 资源不会丢失,只是「换了房东」
|
||||
|
||||
**archived 是终态**:归档后的子租户不能重新启用,只能物理删除。
|
||||
|
||||
### 4.4 删除子租户
|
||||
|
||||
仅 **archived** 状态的子租户可删除。删除时要求输入子租户编码作为二次确认,避免误删。
|
||||
|
||||
### 4.5 子租户成员管理
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 加入 | 成员主部门进入挂载子树 → 自动成为该子租户成员 |
|
||||
| 离开 | 主部门移出挂载子树 → 自动从该子租户移除,归属重派生 |
|
||||
| 任命 Child Admin | 全局超管或现有 Child Admin 在「租户管理 → 用户」中授予 |
|
||||
| 撤销 Child Admin | 同上,且支持自助辞任 |
|
||||
|
||||
**Child Admin 与部门管理员是两套体系**:
|
||||
- Child Admin 管的是「这个子公司」的资源、成员、角色
|
||||
- 部门管理员管的是「这个部门」的人员
|
||||
- 两者互不级联,可以各自独立任免
|
||||
|
||||
---
|
||||
|
||||
## 5. 存储容量配额
|
||||
|
||||
### 5.1 配额维度
|
||||
|
||||
v2.5 仅支持**一项配额**:每个租户的知识库存储上限(GB)。
|
||||
|
||||
- 留空 = 无限制
|
||||
- 单位是 GB,最小粒度 1 GB
|
||||
- 用量统计:知识库内所有文件原始大小之和(不含向量化产物)
|
||||
|
||||
> 用户数、Token 配额暂不开放给管理员配置(v2.6 规划)。
|
||||
|
||||
### 5.2 两级硬盖
|
||||
|
||||
子租户的实际可用存储 = **min(子租户剩余配额, Root 剩余配额)**。
|
||||
|
||||
- Root 是整个集团的存储天花板
|
||||
- Root 用量 = Root 自己直接挂的资源 + 所有子租户用量之和
|
||||
- 即使子租户自己还有余量,只要 Root 满了一样会被阻断
|
||||
|
||||
**示例**:
|
||||
| 设定 | 实际行为 |
|
||||
|---|---|
|
||||
| Root = 1 TB,子公司 A = 100 GB | A 最多用 100 GB |
|
||||
| Root = 100 GB,子公司 A = 1 TB | A 实际可用 100 GB(受 Root 硬盖) |
|
||||
| Root 当前已用 950 GB,配额 1 TB;子公司 B 还剩 200 GB | B 实际只能再传 50 GB |
|
||||
|
||||
### 5.3 超限行为
|
||||
|
||||
- 上传/创建知识库文件时检查配额
|
||||
- 任意一级(子租户或 Root)满 → 拒绝创建,返回明确错误
|
||||
- 删除文件即时释放配额,可立即重试
|
||||
|
||||
### 5.4 监控与调整
|
||||
|
||||
- 租户列表页显示用量条(已用/总额)
|
||||
- 全局超管可在「配额」按钮中手工调整任意租户上限
|
||||
- Child Admin 可查看本租户用量,**不能修改**
|
||||
- 不做计费对接、不做主动告警(v2.6 规划)
|
||||
|
||||
---
|
||||
|
||||
## 6. 模型租户隔离与 Root 共享
|
||||
|
||||
### 6.1 模型归属
|
||||
|
||||
每个 LLM 模型在注册时归属一个租户:
|
||||
|
||||
| 模型来源 | 由谁注册 | 默认可见性 |
|
||||
|---|---|---|
|
||||
| Root 模型 | 全局超管在 Root 视角下注册 | Root 用户可见;Child 用户取决于「共享给所有子租户」开关 |
|
||||
| Child 模型 | Child Admin 在本子租户内注册 | 仅本子租户可见,其他租户看不到 |
|
||||
|
||||
**跨租户调用一律拒绝**:哪怕拿到模型 ID,不属于自己可见范围一律返回 404。
|
||||
|
||||
### 6.2 Root 共享开关
|
||||
|
||||
Root 注册或编辑模型时有一个**「共享给所有子租户」**勾选框,默认勾选。
|
||||
|
||||
| 共享状态 | Child 用户表现 |
|
||||
|---|---|
|
||||
| 共享开 | 模型出现在 Child 的可用模型列表中,标注「Root 共享(只读)」徽标,可调用,但**不能编辑/删除** |
|
||||
| 共享关 | Child 看不到该模型;Child 中已有引用按 fallback 处理(具体由各业务模块定义) |
|
||||
|
||||
**约束**:
|
||||
- Child Admin 看到 Root 共享模型时,编辑/删除按钮禁用
|
||||
- 试图通过 API 改 Root 共享模型会被拒绝(错误码 19801)
|
||||
- 共享开关变更会立即生效,无需重启
|
||||
|
||||
### 6.3 全局超管管理子租户模型
|
||||
|
||||
全局超管要给某个子租户单独配模型时:通过**管理视图切换**临时进入该子租户视角,按 Child Admin 身份操作(详见 §7)。
|
||||
|
||||
不存在「跨租户直接管理某个子租户模型」的入口,统一走视图切换。
|
||||
|
||||
---
|
||||
|
||||
## 7. 管理视图切换(Admin-Scope)
|
||||
|
||||
### 7.1 用途
|
||||
|
||||
全局超管想以「某个子租户管理员的视角」看东西时使用。比如:
|
||||
- 全局超管要帮子公司 A 配置模型 → 切到 A 的视角,操作就和 A 的管理员一样
|
||||
- 全局超管要看子公司 A 的角色配置 → 切到 A 的视角看清单
|
||||
|
||||
### 7.2 入口与作用范围
|
||||
|
||||
**入口**:管理后台 → 模型管理页面顶部胶囊按钮「管理视图:[租户名] ▼」。点击展开下拉,列出 Root + 所有活跃子租户。
|
||||
|
||||
**作用范围**:
|
||||
- ✅ 仅影响**管理类页面**:模型管理、角色与权限、配额、审计日志
|
||||
- ❌ **不影响**业务页面:聊天、知识库、应用调用——业务侧用户始终是「全局超管」身份
|
||||
|
||||
设计意图:超管在管理视图里像 Child Admin 一样工作,但回到聊天/工作流场景仍是超管,避免误打误撞改坏业务数据。
|
||||
|
||||
### 7.3 失效条件
|
||||
|
||||
任一条件触发,视图自动切回 Root:
|
||||
- 用户主动切回 Root 或主动清除
|
||||
- 4 小时无活动(滑动 TTL)
|
||||
- 用户登出
|
||||
- 用户的全局超管角色被撤销
|
||||
- 当前选中的子租户被禁用、归档或删除
|
||||
|
||||
### 7.4 审计
|
||||
|
||||
每次切换都写一条审计日志,记录「谁、什么时间、从哪个视图切到哪个视图」。
|
||||
|
||||
> **2026-05-07 更新**:视图切换审计仅作为合规留痕写入 `audit_log` 表,**不在「系统操作」页展示**。如需查询切换历史,由超管/合规人员通过 SQL(`WHERE action='admin.scope_switch'`)直查。原因见 §9「展示范围」。
|
||||
|
||||
---
|
||||
|
||||
## 8. 用户归属
|
||||
|
||||
用户归属哪个子租户,由系统**自动**算出,不需要也不能手工切换。
|
||||
|
||||
**派生规则**:
|
||||
```
|
||||
用户主部门 → 沿部门树往根找 → 遇到的第一个挂载点对应的子租户 = 用户归属
|
||||
→ 一直到根都没遇到挂载点 → 归属 Root
|
||||
```
|
||||
|
||||
**触发重算**的场景:
|
||||
- 用户主部门变更
|
||||
- 部门移动(导致挂载关系变化)
|
||||
- 子租户挂载/解绑
|
||||
|
||||
**重算副作用**:旧登录态立即失效,用户下次请求需重新登录,刷新身份后进入新租户。
|
||||
|
||||
**关于已离职员工的资源归属**:员工归属切走后,本人此前创建的资源仍归原所有者,不会自动迁移。v2.5 暂未提供「所有者交接」管理界面(后端能力已具备,待后续版本暴露 UI)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 审计日志
|
||||
|
||||
**记录什么**:所有改变集团结构、权限、配额、所有权的关键动作(写入 `audit_log` 表)。
|
||||
|
||||
| 动作类别 | 示例 | 是否在 UI 展示 |
|
||||
|---|---|---|
|
||||
| 租户结构 | 挂载子租户、解绑子租户、禁用子租户 | ✅ 展示 |
|
||||
| 租户结构(异常) | 子租户因部门删除进入孤儿状态(`tenant.orphaned`) | ❌ 仅留痕 |
|
||||
| 模型供应方 | 新建/更新/删除模型供应方 | ✅ 展示 |
|
||||
| 模型其他 | 模型共享开关、模型上下线、模型状态切换 | ❌ 仅留痕 |
|
||||
| 视图切换 | 全局超管切到/切出某子租户视角(`admin.scope_switch`) | ❌ 仅留痕 |
|
||||
| 资源管理 | 资源跨租户迁移、所有者交接、Root 资源共享开关 | ❌ 仅留痕 |
|
||||
| 用户归属变更 | SSO 部门变更引发的用户租户切换、用户来源被 SSO 接管 | ❌ 仅留痕 |
|
||||
| 部门同步异常 | 部门同步时间戳冲突、部门重链接 | ❌ 仅留痕 |
|
||||
|
||||
> **2026-05-07 决策(展示范围收窄)**:UI「系统操作」页只展示「**租户管理**」(挂载/解绑/禁用)和「**模型管理**」(模型供应方 CRUD)两类共 6 项操作行为。其余审计事件继续写入 `audit_log` 表用于合规追溯,但不出现在 UI 列表与筛选下拉中——需要时由超管/合规人员通过 SQL 直查(按 `action` 列过滤)。
|
||||
>
|
||||
> 收窄理由:日常运维只关心「人为可控的结构性变更」,自动触发的归属变更/孤儿事件/视图切换信噪比低,单独留作合规底料更合适。
|
||||
|
||||
**谁能看**(UI 展示部分):
|
||||
- 全局超管:可看全集团所有展示项
|
||||
- Child Admin:只能看本子租户范围内的事件 + 全局超管对本子租户的操作
|
||||
|
||||
**入口**:管理后台 → 系统操作日志 → 支持按时间、用户、操作类型筛选(仅展示 6 项操作行为)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 部门同步与外部对接
|
||||
|
||||
### 10.1 同步源
|
||||
|
||||
v2.5 已支持以下三种部门数据来源:
|
||||
|
||||
| 同步源 | 状态 |
|
||||
|---|---|
|
||||
| 企业微信 | ✅ 已支持(全量 + 定时 + 手动触发) |
|
||||
| 飞书 | ✅ 已支持 |
|
||||
| 通用 REST API | ✅ 已支持(客户自有 HR 系统对接) |
|
||||
| 钉钉 | 🕐 v2.5.2 |
|
||||
|
||||
### 10.2 同步范围
|
||||
|
||||
- 同步**部门树 + 成员**
|
||||
- 不会触碰子租户挂载标记(挂载是手工动作,永远不被外部同步覆盖)
|
||||
- 同步只更新 Root 视角下的整棵部门树;子租户归属不需要同步——它由「用户主部门 + 挂载点」自动派生
|
||||
|
||||
**冲突原则**:本地手工修改 vs 外部同步推送,**以外部同步源为准**(同步源是事实之源)。
|
||||
|
||||
### 10.3 配置与执行
|
||||
|
||||
**配置入口**:管理后台 → 组织同步 → 选择同步源 → 填凭证 → 配置定时计划。
|
||||
|
||||
**执行方式**:
|
||||
- 定时执行(cron 表达式)
|
||||
- 手动触发一次性全量同步
|
||||
- 每次执行结果写入同步日志,失败原因可查
|
||||
|
||||
**安全约束**:
|
||||
- 凭证(如企微 corpsecret)加密存储,前端读取时脱敏
|
||||
- 集成失败连续 N 次自动暂停该同步源,并触发告警
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:状态机
|
||||
|
||||
### A.1 子租户状态
|
||||
|
||||
```
|
||||
[创建] → active ──(禁用)──→ disabled ──(启用)──→ active
|
||||
└──(取消挂载)──→ archived ──(删除)──→ [已删除]
|
||||
```
|
||||
|
||||
| 状态 | 含义 | 允许的操作 |
|
||||
|---|---|---|
|
||||
| active | 正常使用中 | 编辑、改配额、禁用、取消挂载、改成员/管理员 |
|
||||
| disabled | 临时禁用 | 启用、取消挂载、删除 |
|
||||
| archived | 已解绑,终态 | 仅物理删除 |
|
||||
|
||||
**Root 永远 active,不进入这个状态机**。
|
||||
|
||||
### A.2 用户归属变化
|
||||
|
||||
```
|
||||
主部门变更 → 重算叶子租户 → 旧登录态失效 → 用户下次请求触发重新登录 → 进入新租户
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:业务错误码
|
||||
|
||||
| 错误码 | 场景 |
|
||||
|---|---|
|
||||
| 19701 | 非全局超管尝试切换管理视图 |
|
||||
| 19801 | Child 用户尝试编辑/删除 Root 共享模型 |
|
||||
| 19802 | 跨租户访问模型(不在可见范围) |
|
||||
| 22008 | 尝试禁用/归档/删除 Root 租户 |
|
||||
|
||||
---
|
||||
|
||||
## 修订记录
|
||||
|
||||
| 日期 | 修订内容 |
|
||||
|---|---|
|
||||
| 2026-04-26 | 反向对齐 v2.5 当前代码事实,删除已废弃字段(tenant_kind/path/level)、未实现章节(首次登录初始化、集团共享资源、模板下发、SSO tenant_mapping、所有者交接 UI)与全部技术方案描述(DDL、DSL、伪代码、迁移脚本、Celery 配置、Redis Key、JWT 结构)。按金字塔结构重写,从 2204 行精简到约 360 行 |
|
||||
@@ -1,222 +0,0 @@
|
||||
# 2.5 工作台配置多租户化 PRD
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前 `/build/client` 的工作台配置包含 4 个 tab:
|
||||
|
||||
- 日常
|
||||
- 灵思
|
||||
- 订阅
|
||||
- 知识空间
|
||||
|
||||
现状中,工作台配置直接存放在全局 `config` 表固定 key 上,不带 `tenant_id`。在多租户启用后,会产生以下问题:
|
||||
|
||||
1. 租户串扰
|
||||
任一租户管理员保存配置后,其他租户会立即看到相同结果,并可能相互覆盖。
|
||||
|
||||
2. 资源 ID 失效
|
||||
配置中内嵌了租户级资源 ID,例如 `tools[]`、`orgKbs[]`、`recommendedApps[]`。子租户继承根租户配置后,这些资源 ID 在当前租户下未必可解析。
|
||||
|
||||
3. 子租户管理员无法配置
|
||||
当前工作台配置更偏向全局配置语义,无法满足子租户管理员独立配置与独立生效的要求。
|
||||
|
||||
系统模型配置已经具备较成熟的“根租户共享 + 子租户覆盖”模式。工作台配置需要完成同类多租户化改造,但由于工作台配置中包含更多租户资源引用,资源处理策略必须比系统模型配置更严格。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
本需求目标如下:
|
||||
|
||||
1. 单租户切换为多租户后,现有工作台配置自动迁移为根租户配置。
|
||||
2. 子租户未手动配置前,默认继承根租户的工作台配置。
|
||||
3. 子租户一旦修改某个 tab,该 tab 配置转为本租户独立配置,与其他租户隔离。
|
||||
4. 子租户管理员可管理本租户的工作台配置。
|
||||
5. 内置工具在子租户下可用,并能在继承根租户配置时正确映射为子租户自身资源。
|
||||
6. 非工具类租户资源不做跨租户复制,统一按当前租户可解析规则过滤。
|
||||
7. 知识空间 tab 纳入多租户配置框架,但其中“部门知识空间”管理区维持超管统一配置,不做租户隔离。
|
||||
8. 超管可在 Platform 界面切换目标租户,查看并配置不同租户的工作台配置。
|
||||
|
||||
## 3. 范围
|
||||
|
||||
### 3.1 本期范围
|
||||
|
||||
- 日常 tab 多租户配置
|
||||
- 灵思 tab 多租户配置
|
||||
- 订阅 tab 多租户配置
|
||||
- 知识空间 tab 多租户配置
|
||||
- 工作台配置读取、保存、继承、覆盖、权限、迁移方案
|
||||
- 子租户内置工具复制与工具映射规则
|
||||
|
||||
### 3.2 特殊范围说明
|
||||
|
||||
知识空间 tab 中包含“部门知识空间”管理区。该区域仅超管可配置,因此:
|
||||
|
||||
- 该区域继续沿用统一配置逻辑
|
||||
- 不区分多租户
|
||||
- 不参与子租户继承 / 覆盖语义
|
||||
|
||||
### 3.3 本期不做
|
||||
|
||||
- 组织知识库跨租户复制
|
||||
- 推荐应用跨租户复制
|
||||
- root 资源直接以 root id 在 child 运行时复用
|
||||
- 字段级 merge 继承
|
||||
|
||||
## 4. 用户与场景
|
||||
|
||||
### 4.1 用户角色
|
||||
|
||||
- 系统管理员
|
||||
- 根租户管理员
|
||||
- 子租户管理员
|
||||
|
||||
### 4.2 典型场景
|
||||
|
||||
1. 单租户升级为多租户后,历史工作台配置成为根租户默认配置。
|
||||
2. 新建子租户后,子租户未修改工作台配置时,默认看到根租户配置效果。
|
||||
3. 子租户管理员首次保存日常 / 灵思 / 订阅 / 知识空间 tab 后,仅影响本租户。
|
||||
4. 根租户更新某个 tab 配置后,所有未覆盖该 tab 的子租户会立即看到更新。
|
||||
5. 子租户在继承根租户日常 / 灵思配置时,可正常使用复制到本租户的内置工具。
|
||||
6. 子租户继承根租户配置时,`orgKbs[]`、`recommendedApps[]` 中不可解析资源被自动过滤。
|
||||
7. 知识空间 tab 下“部门知识空间”区始终由超管统一管理,不随租户配置切换。
|
||||
8. 超管可在工作台配置页切换目标租户,分别查看和修改不同租户的配置视图。
|
||||
|
||||
## 5. 功能需求
|
||||
|
||||
### 5.1 配置继承与覆盖规则
|
||||
|
||||
工作台配置采用“tab 级整份覆盖”策略:
|
||||
|
||||
1. 根租户始终读取自己的配置。
|
||||
2. 子租户读取某个 tab 时:
|
||||
- 若当前租户该 tab 没有本地配置,继承根租户该 tab 配置。
|
||||
- 若当前租户该 tab 已有本地配置,仅使用本租户配置。
|
||||
3. 子租户首次保存某个 tab 时,为该 tab 创建独立配置。
|
||||
4. 子租户保存后,该 tab 不再跟随根租户变更。
|
||||
|
||||
本期不做字段级 merge。
|
||||
|
||||
### 5.2 日常 tab 资源规则
|
||||
|
||||
涉及资源:
|
||||
|
||||
- `tools[]`
|
||||
- `orgKbs[]`
|
||||
- `recommendedApps[]`
|
||||
|
||||
规则如下:
|
||||
|
||||
1. `tools[]`
|
||||
- 允许继承根租户配置意图
|
||||
- 返回前按子租户工具资源映射
|
||||
- 不直接透传根租户工具 ID
|
||||
|
||||
2. `orgKbs[]`
|
||||
- 不做资源复制
|
||||
- 仅保留当前子租户下可解析、可访问的项
|
||||
- 解析不到的项自动过滤
|
||||
|
||||
3. `recommendedApps[]`
|
||||
- 不做资源复制
|
||||
- 仅保留当前子租户下可解析、可访问的项
|
||||
- 解析不到的项自动过滤
|
||||
|
||||
### 5.3 灵思 tab 资源规则
|
||||
|
||||
涉及资源:
|
||||
|
||||
- `tools[]`
|
||||
- `orgKbs[]`
|
||||
|
||||
规则如下:
|
||||
|
||||
1. `tools[]`
|
||||
- 允许继承根租户配置意图
|
||||
- 返回前按子租户工具资源映射
|
||||
|
||||
2. `orgKbs[]`
|
||||
- 不做资源复制
|
||||
- 仅保留当前子租户下可解析、可访问的项
|
||||
- 解析不到的项自动过滤
|
||||
|
||||
### 5.4 订阅 tab 资源规则
|
||||
|
||||
若订阅配置中存在租户级资源字段,则统一遵循以下规则:
|
||||
|
||||
- 不复制资源
|
||||
- 仅保留当前租户可解析、可访问的项
|
||||
- 不可解析项自动过滤
|
||||
|
||||
若订阅配置不包含租户级资源字段,则可原样继承配置结构。
|
||||
|
||||
### 5.5 知识空间 tab 规则
|
||||
|
||||
知识空间 tab 本身纳入多租户配置框架,支持:
|
||||
|
||||
- 根租户共享
|
||||
- 子租户覆盖
|
||||
|
||||
但 tab 内“部门知识空间”管理区保持独立规则:
|
||||
|
||||
- 仅超管可配置
|
||||
- 不区分多租户
|
||||
- 不参与租户继承 / 覆盖
|
||||
|
||||
### 5.6 内置工具规则
|
||||
|
||||
为保证子租户继承根租户配置时工具仍可使用,本期采用“复制 + 映射”策略。
|
||||
|
||||
1. 创建子租户时,复制根租户内置工具到子租户。
|
||||
2. 复制后的工具保留与根租户一致的 `tool_key`。
|
||||
3. 子租户继承根租户 `tools[]` 配置时:
|
||||
- 按叶子工具 `tool_key` 查找子租户下对应工具
|
||||
- 将根租户工具 ID 映射为子租户工具 ID
|
||||
- 按子租户工具分类重新组装工具分组结构
|
||||
|
||||
目标语义是:
|
||||
|
||||
- 继承的是根租户“选择了哪些内置工具”
|
||||
- 执行的是子租户“自己的工具资源”
|
||||
|
||||
### 5.7 权限要求
|
||||
|
||||
1. 根租户工作台配置可由系统管理员或具备根租户管理权限的角色修改。
|
||||
2. 子租户工作台配置可由当前子租户管理员修改。
|
||||
3. 子租户管理员只能修改本租户配置,不得跨租户操作。
|
||||
4. 超管可在 Platform 构建页主动切换“当前配置租户”,对 root 或指定 child tenant 的工作台配置进行查看和保存。
|
||||
|
||||
### 5.8 前端展示要求
|
||||
|
||||
Platform 构建页需展示当前配置状态:
|
||||
|
||||
- 当前使用根租户配置
|
||||
- 当前租户已覆盖
|
||||
|
||||
Platform 构建页需支持以下交互:
|
||||
|
||||
- 超管可切换目标租户,按当前选中租户查看和配置 4 个 tab
|
||||
- 非超管不展示跨租户切换入口
|
||||
- 知识空间 tab 下“部门知识空间”管理区仅超管可见
|
||||
- 非超管进入知识空间配置页时,前端直接隐藏“部门知识空间”管理区
|
||||
|
||||
Client 侧仅消费后端投影后的租户视图,不在前端执行跨租户资源映射。
|
||||
|
||||
## 6. 非功能要求
|
||||
|
||||
1. 多租户关闭时,系统行为与当前单租户模式保持一致。
|
||||
2. 迁移逻辑需支持幂等执行。
|
||||
3. 资源无法映射时,系统优先过滤并降级,不得导致整个配置读取失败。
|
||||
4. 工具复制与配置过滤过程需可观测,便于排查。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 单租户升级后,历史工作台配置被迁移为根租户配置。
|
||||
2. 子租户未覆盖时,可继承根租户 4 个 tab 的配置。
|
||||
3. 子租户覆盖某个 tab 后,仅该租户该 tab 生效,其他租户不受影响。
|
||||
4. 根租户修改某个 tab 后,所有未覆盖该 tab 的子租户立即看到更新。
|
||||
5. 子租户继承根租户 `tools[]` 时,可正常使用复制到本租户的内置工具。
|
||||
6. 子租户继承根租户 `orgKbs[]`、`recommendedApps[]` 时,不可解析资源被安全过滤。
|
||||
7. 子租户管理员可独立配置本租户工作台。
|
||||
8. 知识空间 tab 的普通配置可继承 / 覆盖。
|
||||
9. 知识空间 tab 下“部门知识空间”区始终由超管统一配置,不受租户覆盖影响。
|
||||
10. 超管可通过界面切换不同租户并分别配置对应租户的工作台配置。
|
||||
11. 非超管在知识空间 tab 中看不到“部门知识空间”管理区。
|
||||
@@ -1,315 +0,0 @@
|
||||
# 2.5 工作台配置多租户化实施计划
|
||||
|
||||
## 1. 实施目标
|
||||
|
||||
将工作台配置从全局配置改造为租户维度配置,覆盖 4 个 tab:
|
||||
|
||||
- `workstation`(日常)
|
||||
- `workstation_linsight`(灵思)
|
||||
- `workstation_subscription`(订阅)
|
||||
- `workstation_knowledge_space`(知识空间)
|
||||
|
||||
实现能力包括:
|
||||
|
||||
- root 配置共享
|
||||
- child 默认继承
|
||||
- child tab 级覆盖
|
||||
- 子租户管理员可配置
|
||||
- 内置工具复制与继承映射
|
||||
- 非工具资源按当前租户过滤
|
||||
|
||||
特别说明:
|
||||
|
||||
- 知识空间 tab 纳入多租户配置框架
|
||||
- 但 tab 内“部门知识空间”管理区继续走超管统一配置逻辑,不参与租户 override
|
||||
- 超管需要在 Platform 界面支持切换目标租户,以分别查看和配置不同租户的工作台配置
|
||||
|
||||
## 2. 数据模型与结构迁移
|
||||
|
||||
### 2.1 新增配置表
|
||||
|
||||
新增 `tenant_workstation_config` 表,建议字段如下:
|
||||
|
||||
- `id`
|
||||
- `tenant_id`
|
||||
- `key`
|
||||
- `value`
|
||||
- `create_time`
|
||||
- `update_time`
|
||||
|
||||
唯一键:
|
||||
|
||||
- `(tenant_id, key)`
|
||||
|
||||
支持的 `key`:
|
||||
|
||||
- `workstation`
|
||||
- `workstation_linsight`
|
||||
- `workstation_subscription`
|
||||
- `workstation_knowledge_space`
|
||||
|
||||
### 2.2 Alembic 职责
|
||||
|
||||
`alembic` 仅负责结构变更:
|
||||
|
||||
- 新建 `tenant_workstation_config`
|
||||
- 创建唯一索引 `(tenant_id, key)`
|
||||
- 按需要补充基础索引和约束
|
||||
|
||||
不在 alembic 中写复杂业务数据迁移逻辑。
|
||||
|
||||
## 3. 业务迁移与补齐脚本
|
||||
|
||||
### 3.1 脚本组织方式
|
||||
|
||||
迁移逻辑放在 `scripts/`,底层拆成两个 Python 脚本,并提供一个 shell 总入口:
|
||||
|
||||
- `src/backend/scripts/migrate_workstation_config_to_root.py`
|
||||
- `src/backend/scripts/backfill_child_builtin_tools.py`
|
||||
- `src/backend/scripts/run_workstation_multitenant_migration.sh`
|
||||
|
||||
### 3.2 脚本职责
|
||||
|
||||
#### `migrate_workstation_config_to_root.py`
|
||||
|
||||
职责:
|
||||
|
||||
- 将旧全局 `config` 中的工作台 4 个 key 迁移到 root tenant
|
||||
- 迁移 key 包含:
|
||||
- `workstation`
|
||||
- `workstation_linsight`
|
||||
- `workstation_subscription`
|
||||
- `workstation_knowledge_space`
|
||||
- 幂等执行
|
||||
- 不删除旧 `config` 数据
|
||||
- 输出迁移统计、跳过原因、失败项
|
||||
|
||||
#### `backfill_child_builtin_tools.py`
|
||||
|
||||
职责:
|
||||
|
||||
- 为所有现有 child tenant 补齐 root 的内置工具
|
||||
- 按 `tool_key` 幂等补齐
|
||||
- 输出 tenant 维度统计和失败项
|
||||
|
||||
#### `run_workstation_multitenant_migration.sh`
|
||||
|
||||
职责:
|
||||
|
||||
- 作为总入口顺序执行两个 Python 脚本
|
||||
- 先做结构迁移前置检查
|
||||
- 支持 dry-run / real-run 参数透传
|
||||
- 汇总输出执行结果
|
||||
- 失败时返回明确退出码,便于运维执行
|
||||
|
||||
## 4. 配置读取与写入流程
|
||||
|
||||
### 4.1 读取流程
|
||||
|
||||
建议为 4 个 tab 分别提供 resolve 逻辑:
|
||||
|
||||
- `resolve_daily_config(tenant_id)`
|
||||
- `resolve_linsight_config(tenant_id)`
|
||||
- `resolve_subscription_config(tenant_id)`
|
||||
- `resolve_knowledge_space_config(tenant_id)`
|
||||
|
||||
统一规则:
|
||||
|
||||
1. 优先读取当前租户该 tab 配置
|
||||
2. 未命中时 fallback 到 root 租户该 tab 配置
|
||||
3. 若配置来自 root,则根据当前租户执行资源投影
|
||||
4. 返回带元信息的结果
|
||||
|
||||
返回结构建议统一为:
|
||||
|
||||
- `data`
|
||||
- `inherited_from_root`
|
||||
- `source_tenant_id`
|
||||
- `has_override`
|
||||
|
||||
### 4.2 写入流程
|
||||
|
||||
统一采用 tab 级整份覆盖:
|
||||
|
||||
1. root 保存:更新 root 行
|
||||
2. child 首次保存:创建 child 该 tab 行
|
||||
3. child 后续保存:更新 child 该 tab 行
|
||||
|
||||
不做字段级 merge。
|
||||
|
||||
## 5. 资源投影规则
|
||||
|
||||
### 5.1 工具投影
|
||||
|
||||
建议抽出统一逻辑:
|
||||
|
||||
- `project_tools_for_tenant(raw_tools, tenant_id)`
|
||||
|
||||
规则:
|
||||
|
||||
1. 仅以叶子工具 `tool_key` 作为映射锚点
|
||||
2. 不直接使用 root 的 `tool id`
|
||||
3. 不直接使用 root 的 `tool type id`
|
||||
4. 查找子租户下同 `tool_key` 的复制后工具
|
||||
5. 找到后替换为子租户工具 `id`
|
||||
6. 按子租户当前工具分类重新组装父分组
|
||||
7. 找不到映射的工具自动过滤并记录 warning
|
||||
|
||||
### 5.2 日常配置投影
|
||||
|
||||
建议抽出:
|
||||
|
||||
- `project_daily_config_for_tenant(config, tenant_id)`
|
||||
|
||||
内部处理:
|
||||
|
||||
- `tools[]` 走 `project_tools_for_tenant`
|
||||
- `orgKbs[]` 走 `filter_org_kbs_for_tenant`
|
||||
- `recommendedApps[]` 走 `filter_recommended_apps_for_tenant`
|
||||
|
||||
### 5.3 灵思配置投影
|
||||
|
||||
建议抽出:
|
||||
|
||||
- `project_linsight_config_for_tenant(config, tenant_id)`
|
||||
|
||||
内部处理:
|
||||
|
||||
- `tools[]` 走 `project_tools_for_tenant`
|
||||
- `orgKbs[]` 走 `filter_org_kbs_for_tenant`
|
||||
|
||||
### 5.4 订阅配置投影
|
||||
|
||||
建议抽出:
|
||||
|
||||
- `project_subscription_config_for_tenant(config, tenant_id)`
|
||||
|
||||
若存在租户资源字段,则统一按“当前租户可解析项保留,其余过滤”的原则处理。
|
||||
|
||||
### 5.5 知识空间配置投影
|
||||
|
||||
建议抽出:
|
||||
|
||||
- `project_knowledge_space_config_for_tenant(config, tenant_id)`
|
||||
|
||||
规则:
|
||||
|
||||
- 知识空间 tab 的普通配置项可继承 / 覆盖
|
||||
- “部门知识空间”管理区不走租户 override,不走租户投影,继续使用超管统一配置链路
|
||||
|
||||
## 6. 内置工具复制方案
|
||||
|
||||
### 6.1 复制对象
|
||||
|
||||
需要复制的对象:
|
||||
|
||||
- `t_gpts_tools_type`
|
||||
- `t_gpts_tools`
|
||||
|
||||
仅复制内置工具,不复制租户自建工具。
|
||||
|
||||
### 6.2 复制约束
|
||||
|
||||
1. 复制后的工具必须保留与 root 相同的 `tool_key`
|
||||
2. 复制动作需幂等
|
||||
3. 不重复插入已存在的 child 内置工具
|
||||
|
||||
### 6.3 执行时机
|
||||
|
||||
需覆盖两条子租户创建链路:
|
||||
|
||||
- `TenantService.acreate_tenant`
|
||||
- `TenantMountService.mount_child`
|
||||
|
||||
同时通过 `backfill_child_builtin_tools.py` 补齐历史 child tenant。
|
||||
|
||||
## 7. 接口与前端改造
|
||||
|
||||
### 7.1 后端接口
|
||||
|
||||
建议保持现有工作台配置接口路径不变,但增强返回结构和租户语义。
|
||||
|
||||
读接口返回:
|
||||
|
||||
- `data`
|
||||
- `inherited_from_root`
|
||||
- `source_tenant_id`
|
||||
- `has_override`
|
||||
|
||||
写接口规则:
|
||||
|
||||
- root 修改 root 行
|
||||
- child 修改 child 行
|
||||
|
||||
### 7.2 Platform 前端
|
||||
|
||||
工作台配置页需支持:
|
||||
|
||||
- 展示“继承 root / 当前租户覆盖”
|
||||
- 子租户保存后展示已覆盖状态
|
||||
- 当资源被过滤时,可提示“部分 root 资源未映射到当前租户”
|
||||
- 超管展示租户切换入口,切换后以所选租户视角读取和保存 4 个 tab 配置
|
||||
- 非超管不展示租户切换入口
|
||||
- 知识空间 tab 下“部门知识空间”管理区仅超管可见,非超管前端直接隐藏
|
||||
|
||||
### 7.3 Client 前端
|
||||
|
||||
Client 侧仅消费后端投影后的租户视图:
|
||||
|
||||
- 不在前端做跨租户资源 remap
|
||||
- 不依赖 root id 在 child 下直接可用
|
||||
|
||||
## 8. 测试计划
|
||||
|
||||
### 8.1 单元测试
|
||||
|
||||
- root 配置读写
|
||||
- child 无 override 时继承 root
|
||||
- child 有 override 后与 root 隔离
|
||||
- root 更新后未覆盖 child 立即生效
|
||||
- child 工具按 `tool_key` 正确映射
|
||||
- `orgKbs` 过滤正确
|
||||
- `recommendedApps` 过滤正确
|
||||
- child admin 仅能修改本租户配置
|
||||
|
||||
### 8.2 迁移测试
|
||||
|
||||
- 旧全局工作台配置正确迁移到 root
|
||||
- 重复执行脚本不产生重复脏数据
|
||||
- 历史 child tenant 可补齐内置工具
|
||||
- shell 总入口可顺序串联执行并正确返回退出码
|
||||
|
||||
### 8.3 集成测试
|
||||
|
||||
- Platform 构建页 4 个 tab 联调
|
||||
- Platform 超管租户切换联调
|
||||
- Client 日常配置消费
|
||||
- 灵思配置消费
|
||||
- 订阅配置消费
|
||||
- 知识空间 tab 普通配置继承 / 覆盖
|
||||
- 知识空间 tab 下“部门知识空间”区不受租户 override 影响
|
||||
- 非超管访问知识空间 tab 时“部门知识空间”区隐藏
|
||||
- 单租户模式回归
|
||||
|
||||
## 9. 验收场景
|
||||
|
||||
1. 单租户升级后,旧工作台配置被迁移为 root 配置。
|
||||
2. child 未覆盖时继承 root 的 4 个 tab 配置。
|
||||
3. child 覆盖某 tab 后,仅该 tab 不再跟随 root。
|
||||
4. root 配置中的内置工具在 child 继承视图下可正常使用。
|
||||
5. `orgKbs`、`recommendedApps` 在 child 下仅保留可解析项。
|
||||
6. 知识空间 tab 的普通配置可继承 / 覆盖。
|
||||
7. 知识空间 tab 下“部门知识空间”区始终保持超管统一管理。
|
||||
8. 总入口脚本可重复执行,不产生重复脏数据。
|
||||
9. 超管可在界面切换不同租户并分别配置对应租户的工作台配置。
|
||||
10. 非超管在知识空间 tab 下看不到“部门知识空间”管理区。
|
||||
|
||||
## 10. 默认假设
|
||||
|
||||
1. 采用“两份文档”而非单文档合并。
|
||||
2. 文档目录固定为 `docs/PRD/2.5 权限管理体系改造 PRD/`。
|
||||
3. 继承策略采用“tab 级整份覆盖”。
|
||||
4. 非工具资源不做复制,仅做过滤。
|
||||
5. 内置工具通过“复制 + `tool_key` 映射”支持继承。
|
||||
6. 业务数据迁移放在 `scripts/`,`alembic` 仅负责结构变更。
|
||||
7. 迁移执行采用“两个 Python 脚本 + 一个 shell 总入口”的组织方式。
|
||||
@@ -1,375 +0,0 @@
|
||||
# 2.5 权限管理体系改造技术方案 Review
|
||||
|
||||
## 总体评价
|
||||
|
||||
技术方案整体结构清晰,选型论证充分(OpenFGA),迁移策略分阶段设计合理。但存在 **1 个致命设计缺陷** 和多个需要修正的问题。以下按严重程度排列。
|
||||
|
||||
---
|
||||
|
||||
## 一、致命缺陷:部门层级授权模型错误
|
||||
|
||||
### 问题描述
|
||||
|
||||
技术方案第七节「场景 1:部门层级权限传递」声称:
|
||||
|
||||
> Alice 在 API 小组(dept:3),工作流 wf-001 授权给「工程部(dept:1)#member」查看。
|
||||
> `check(user="user:alice", relation="can_read", object="workflow:wf-001") → True`
|
||||
|
||||
**这是错误的。在当前 DSL 定义下,这个 Check 会返回 False。**
|
||||
|
||||
原因:DSL 中 `department` 的 `member` 关系定义为 `define member: [user]`,**没有继承逻辑**。`department:1#member` 在 OpenFGA 中只会展开为 **直接隶属于 department:1 的 member 用户**,不会递归遍历子部门。
|
||||
|
||||
技术方案自己在「核心设计要点」表中也写了:
|
||||
|
||||
> "部门 member 不继承 — member 仅限直接隶属,避免'加入总公司=加入所有部门'"
|
||||
|
||||
这与场景 1 的验证结论直接矛盾。方案作者误以为 OpenFGA 会自动通过 `parent` 关系遍历子部门的 member,但 OpenFGA 的 userset 解析 (`type#relation`) 只解析**该对象自身**的 relation,不会沿着 parent/child 关系图遍历。
|
||||
|
||||
`admin from parent` 能工作是因为它是**从子到父**的方向查找("我的 parent 的 admin 也是我的 admin"),这是每个子部门自身定义的 rewrite rule。但 `department:1#member` 是从**资源侧**引用一个 userset,OpenFGA 只会展开 department:1 这一个对象的 member 集合。
|
||||
|
||||
### 影响范围
|
||||
|
||||
这是整个方案的**核心假设**。所有资源类型(workflow, assistant, tool, knowledge_space, channel, dashboard)都使用 `department#member` 作为授权主体。如果部门层级授权不生效,**整个 ReBAC 体系的部门授权能力形同虚设**。
|
||||
|
||||
### 修复建议
|
||||
|
||||
有三种方案,推荐方案 A:
|
||||
|
||||
**方案 A:业务层展开子部门(推荐)**
|
||||
|
||||
授权时不只写一条 `(workflow:wf-001, viewer, department:1#member)`,而是**业务层查出 department:1 及其所有子部门**,写入多条元组:
|
||||
|
||||
```python
|
||||
# 授权给「工程部及其所有子部门」
|
||||
sub_depts = DepartmentDao.get_subtree(dept_id=1) # [1, 2, 3]
|
||||
for dept_id in sub_depts:
|
||||
fga.write_tuple(object="workflow:wf-001", relation="viewer", user=f"department:{dept_id}#member")
|
||||
```
|
||||
|
||||
- 优点:OpenFGA 模型保持简洁,语义明确
|
||||
- 缺点:子部门新增时需要补写元组;授权管理页需要识别并展示"部门(含子部门)"的聚合关系
|
||||
- 需要额外维护:部门树结构变更(新增/移动/删除子部门)时,需要级联更新所有引用该部门的资源元组
|
||||
|
||||
**方案 B:反转 parent 方向,建立 children 关系**
|
||||
|
||||
在 department 上同时维护 `parent` 和 `children` 双向关系,通过 `member_all: member or member_all from children` 实现递归展开。
|
||||
|
||||
- 优点:OpenFGA 原生支持递归查询
|
||||
- 缺点:需要维护双向关系元组,部门调整时写入量翻倍;深层嵌套时 Check 性能下降
|
||||
|
||||
**方案 C:只在 department 上用 parent 构建 ancestor 关系**
|
||||
|
||||
```dsl
|
||||
type department
|
||||
relations
|
||||
define parent: [department]
|
||||
define admin: [user] or admin from parent
|
||||
define member: [user]
|
||||
define ancestor: [department] or ancestor from parent # 所有祖先
|
||||
```
|
||||
|
||||
然后在资源类型中改为:
|
||||
```dsl
|
||||
type workflow
|
||||
relations
|
||||
define viewer: [user, department#member, user_group#member] or editor or viewer_via_dept
|
||||
define viewer_via_dept: [department]
|
||||
# 业务层写入时:(workflow:wf-001, viewer_via_dept, department:1)
|
||||
# Check 时需要自定义逻辑判断 user 是否属于 dept:1 的子树
|
||||
```
|
||||
|
||||
这个方案过于复杂,不推荐。
|
||||
|
||||
**最终建议:采用方案 A(业务层展开)**,并在技术方案中明确:
|
||||
1. `PermissionService.authorize()` 中,当 `subject_type == 'department'` 时,自动查出子部门树并批量写入
|
||||
2. 新增 `DepartmentChangeHandler`,监听部门新增/移动/删除事件,级联维护相关元组
|
||||
3. 授权查询 API 返回时,将多个子部门元组合并展示为一条"工程部(含子部门)"的记录
|
||||
|
||||
---
|
||||
|
||||
## 二、严重问题
|
||||
|
||||
### 2.1 read_tuples 无法获取完整权限列表
|
||||
|
||||
**问题**:API 4.3「查询资源权限列表」使用 `read_tuples` 获取资源的授权情况。但 OpenFGA 的 `Read` API 只返回**直接写入的元组**,不包含通过继承、computed relation 推导出的关系。
|
||||
|
||||
例如:用户 A 是知识空间 ks-10 的 viewer,文件夹 folder:100 的 parent 是 ks-10。`read_tuples(object="folder:100")` **不会返回用户 A**,因为 A 的权限是通过 `can_read from parent` 继承来的,不是直接写入的元组。
|
||||
|
||||
**影响**:前端「管理权限」弹窗无法正确展示继承的权限关系。
|
||||
|
||||
**修复建议**:
|
||||
- 对于需要展示继承权限的场景(如文件夹权限管理),前端需要**分层展示**:本级直接授权 + 从父级继承的授权
|
||||
- 继承的授权需要业务层向上遍历 parent 链,逐级 `read_tuples` 拼装
|
||||
- 或者使用 OpenFGA 的 `ListUsers` API(v1.4+),它可以列出对某个对象有某种关系的所有用户(含计算关系)
|
||||
|
||||
### 2.2 资源创建与 OpenFGA 写入的事务一致性
|
||||
|
||||
**问题**:创建资源(如知识空间)时需要同时写 MySQL 和 OpenFGA。技术方案没有讨论**原子性保证**。如果 MySQL 写入成功但 OpenFGA 写入失败,会出现资源已创建但无人有权限的状态。
|
||||
|
||||
**修复建议**:
|
||||
- 方案一(推荐):**先写 MySQL,再写 OpenFGA**。OpenFGA 写入失败时记入补偿队列(`failed_tuples` 表),由定时任务重试。同时在 `PermissionService.check` 中增加 owner 回退逻辑:如果 OpenFGA check 返回 False 且用户是资源 DB 记录的 `user_id`(创建者),返回 True
|
||||
- 方案二:引入 Outbox 模式,通过 Celery 异步写入 OpenFGA,但这会引入权限生效延迟
|
||||
|
||||
### 2.3 前端权限标记获取方式缺失
|
||||
|
||||
**问题**:PRD 要求根据用户对资源的权限级别控制按钮显隐(如 viewer 不能看到"编辑"按钮)。但技术方案没有定义前端如何获取这些权限标记。
|
||||
|
||||
当前 `GET /api/v1/permissions/objects` 只返回 `object_ids`,不返回**权限级别**。列表页需要知道每个资源用户具有什么权限。
|
||||
|
||||
**修复建议**:
|
||||
- 方案一:列表 API 返回时,为每个资源附带 `permission_level: "viewer" | "editor" | "manager" | "owner"`。后端批量调用 `BatchCheck`,从高到低依次检查
|
||||
- 方案二:新增 `POST /api/v1/permissions/batch-check` 批量检查 API,前端在列表加载后调用
|
||||
- 推荐方案一,减少前端请求数
|
||||
|
||||
### 2.4 PermissionService 每次实例化的性能问题
|
||||
|
||||
**问题**:`LoginUser.async_access_check` 中每次调用都 `PermissionService()` 新建实例。虽然 `FGAClient` 是单例,但 `PermissionService` 本身的创建和 GC 是不必要的开销。
|
||||
|
||||
**修复建议**:将 `PermissionService` 改为单例或作为 FastAPI 依赖注入的实例。
|
||||
|
||||
---
|
||||
|
||||
## 三、中等问题
|
||||
|
||||
### 3.1 super_admin 检查导致双倍延迟
|
||||
|
||||
**问题**:每次权限检查都先 `_is_super_admin()` 调 OpenFGA,再做实际 Check。对于非管理员用户,每次请求会产生**两次** OpenFGA 调用。
|
||||
|
||||
**修复建议**:
|
||||
- super_admin 判定不应走 OpenFGA Check,而应在 `LoginUser` 初始化时(JWT 解析阶段)从 `userrole` 表判断 `role_id == AdminRole`(旧文档常写 `user_role`,当前逻辑已有此能力),结果缓存到 `LoginUser.is_admin()` 标记
|
||||
- OpenFGA 中的 `system:global / super_admin` 元组仅作为数据迁移桥梁和审计用途,不参与热路径 Check
|
||||
|
||||
### 3.2 list_accessible_ids 返回 None 的设计
|
||||
|
||||
**问题**:管理员时返回 `None` 表示"不过滤"。这要求**所有调用方**都处理 None vs list 两种分支,增加了出错概率。
|
||||
|
||||
**修复建议**:返回一个特殊的 sentinel 对象(如 `ALL_ACCESSIBLE`)或使用 `Optional[list]` 的类型标注并在 DAO 层统一处理。更好的方案是将"是否管理员"的判断提前到调用方,管理员直接走无过滤查询分支。
|
||||
|
||||
### 3.3 OpenFGA 与 BiSheng 共享 MySQL 实例
|
||||
|
||||
**问题**:docker-compose 中 OpenFGA 连接 `bisheng-mysql`,与业务数据库共用一个 MySQL 实例。OpenFGA 有自己的 DDL 迁移,可能在版本升级时影响 BiSheng 的数据库。
|
||||
|
||||
**修复建议**:OpenFGA 使用**独立的 database**(同一 MySQL 实例但不同 schema),如 `openfga` 库。docker-compose 中配置已经是 `openfga?parseTime=true`,确认这是独立 schema 即可。但建议在文档中明确这一点。
|
||||
|
||||
### 3.4 缓存失效的级联问题
|
||||
|
||||
**问题**:L2 缓存(list_objects 30s TTL)只在 `PermissionService.authorize()` 调用后主动清除。但以下场景不会触发清除:
|
||||
- 用户被加入/移出部门(通过 org_sync 或部门管理)
|
||||
- 用户被加入/移出用户组
|
||||
- 文件夹 parent 关系变更
|
||||
|
||||
**修复建议**:
|
||||
- 识别所有会改变权限的操作点,统一通过事件机制触发缓存清除
|
||||
- 或者降低 TTL 到 10s,接受短暂的最终一致
|
||||
- 在 UI 上对权限变更操作给出"变更最多需要 X 秒生效"的提示
|
||||
|
||||
### 3.5 relation_definition 动态 Model 生成风险
|
||||
|
||||
**问题**:自定义关系层级(P2 优先级)的实现方案是运行时动态生成 OpenFGA authorization model。这有几个风险:
|
||||
1. 新 model 生成后,旧版本 model 下写入的元组如果引用了被删除的 relation,会导致 Check 报错
|
||||
2. 并发生成 model 可能产生竞态条件
|
||||
3. model 生成逻辑包含自动构建继承链,生成的 DSL 正确性难以保证
|
||||
|
||||
**修复建议**:
|
||||
- P0 阶段使用**静态固定的** authorization model,不实现动态生成
|
||||
- P2 阶段实现动态生成时:删除关系前必须先清理所有引用该关系的元组;加分布式锁防止并发生成;生成后用 OpenFGA 的 model validation API 校验
|
||||
|
||||
### 3.6 迁移脚本的幂等性和错误处理
|
||||
|
||||
**问题**:迁移脚本 `migrate_rbac_to_rebac()` 没有讨论:
|
||||
- **幂等性**:如果中途失败重跑,已写入的元组会重复写入(OpenFGA 对重复元组写入会报错)
|
||||
- **部分失败处理**:批量写入 100 条,如果第 50 批失败,前 49 批已经写入,状态不一致
|
||||
|
||||
**修复建议**:
|
||||
- 每条元组写入前先 Read 检查是否存在,或使用 `try/except` 捕获"already exists"错误跳过
|
||||
- 记录迁移进度(checkpoint),支持断点续传
|
||||
- 增加 `--verify` 模式:迁移完成后,遍历所有资源和用户,对比新旧系统的 Check 结果
|
||||
|
||||
---
|
||||
|
||||
## 四、建议改进项
|
||||
|
||||
### 4.1 缺少:资源删除时的元组清理机制
|
||||
|
||||
方案提到"定期清理已删除资源的孤立元组"但没有具体设计。建议:
|
||||
- 资源删除时同步调用 `fga.read_tuples(object=f"{type}:{id}")` 获取所有关联元组并删除
|
||||
- 作为防御性措施,增加定时任务扫描孤立元组
|
||||
|
||||
### 4.2 缺少:文件夹移动场景
|
||||
|
||||
PRD 提到"支持知识空间内的文件移动"。文件夹从 parent_A 移动到 parent_B 时需要:
|
||||
1. 删除旧 parent 元组
|
||||
2. 写入新 parent 元组
|
||||
3. 如果子文件夹/文件有基于继承的权限,移动后权限链会自动调整(OpenFGA 特性),但可能导致用户权限意外丢失
|
||||
|
||||
建议在技术方案中增加文件夹移动的权限影响说明和用户确认交互。
|
||||
|
||||
### 4.3 权限检查 API 的安全问题
|
||||
|
||||
`POST /api/v1/permissions/check` 接受 `user_id` 参数,意味着任何登录用户可以查询其他用户的权限。应限制为只能检查当前登录用户的权限(从 JWT 提取),或仅允许管理员使用此 API。
|
||||
|
||||
### 4.4 缺少:OpenFGA 高可用方案
|
||||
|
||||
方案提到"多实例部署"但没有细节。生产环境建议:
|
||||
- OpenFGA 至少 2 个实例 + 负载均衡
|
||||
- 健康检查端点接入监控
|
||||
- BiSheng 侧实现 Circuit Breaker,OpenFGA 不可用时快速失败而非阻塞
|
||||
|
||||
### 4.5 OpenFGA Docker 镜像版本未锁定
|
||||
|
||||
`image: openfga/openfga:latest` 使用 latest 标签,可能在部署时因版本变化引入兼容性问题。建议锁定具体版本,如 `openfga/openfga:v1.8.0`。
|
||||
|
||||
### 4.6 缺少性能基准测试计划
|
||||
|
||||
建议在验证方案中增加:
|
||||
- Check 单次延迟基准(预期 < 10ms)
|
||||
- ListObjects 在不同元组量下的延迟(100/1000/10000 元组)
|
||||
- 文件夹继承深度对 Check 延迟的影响(3 层/5 层/10 层)
|
||||
|
||||
---
|
||||
|
||||
## 五、值得肯定的部分
|
||||
|
||||
1. **OpenFGA 选型论证充分**:Apache 2.0 协议兼容、CNCF 生态、Zanzibar 模型验证,选型理由站得住
|
||||
2. **新旧体系职责分工明确**:OpenFGA 管资源访问,role 表管菜单和配额,边界清晰
|
||||
3. **迁移策略分阶段设计**:Schema → 数据迁移 → 双写 → 切换 → 清理,渐进式低风险
|
||||
4. **权限金字塔设计合理**:owner > manager > editor > viewer 的继承链符合直觉
|
||||
5. **配额解析逻辑**:多角色取最大值的策略合理
|
||||
6. **第三方同步模块的 Provider 抽象**:四种数据源统一接口,可扩展性好
|
||||
7. **迁移附录的详细案例**:旧数据链路 → 迁移步骤 → 前后对比,展示清晰
|
||||
|
||||
---
|
||||
|
||||
## 六、修改优先级总结
|
||||
|
||||
| 优先级 | 问题 | 章节 |
|
||||
|--------|------|------|
|
||||
| **P0 必须修复** | 部门层级授权模型错误(department#member 不递归) | 一 |
|
||||
| **P0 必须修复** | read_tuples 无法获取继承权限,权限列表 API 设计不成立 | 二.1 |
|
||||
| **P1 应当修复** | 资源创建与 OpenFGA 写入的事务一致性 | 二.2 |
|
||||
| **P1 应当修复** | 前端权限标记获取方式缺失 | 二.3 |
|
||||
| **P1 应当修复** | super_admin 双倍检查延迟 | 三.1 |
|
||||
| **P2 建议修复** | 缓存失效级联、迁移幂等性、文件夹移动等 | 三.4-三.6, 四 |
|
||||
|
||||
---
|
||||
|
||||
## 七、2026-04-18 v2.5 Tenant 树架构 Review 更新(2026-04-20/21 收窄同步)
|
||||
|
||||
> 本章节为技术方案 Review 的增量补充,反映《2.5 多租户需求文档》重写(2026-04-18)+ 两轮收窄(2026-04-20 / 2026-04-21)后的决策对技术实现的影响。原一~六章针对 v2.4→v2.5 的 ReBAC 基础改造;本章针对多租户 Tenant 树架构与 9 项 PRD Review 决策的落地要点。
|
||||
>
|
||||
> **2026-04-20 收窄**:删除 SaaS 多客户场景(仅私有化单 Root);数据模型精简为 `parent_tenant_id` + `share_default_to_children`(废弃 `tenant_path` / `level` / `tenant_kind`);Root 自动创建且不可删除/禁用;新增 `audit_log` 表 + `user.token_version` 字段 + `Tenant.status` 的 `orphaned` 枚举 + Gateway 实时 vs Celery 校对 ts 冲突规则。
|
||||
>
|
||||
> **2026-04-21 Round 2 Review**:FGA DSL 删除 `tenant#parent` 关系;资源 manager/editor/viewer 彻底移除 `tenant#member`(Tenant 仅作配额/存储/归属边界,不进资源授权);所有者交接 to_user 校验改为"叶子 ∈ {tenant_id, Root}";F011 新增 `DepartmentDeletionHandler` 集中处理孤儿 Tenant;新增 `POST /departments/sync` 和 `POST /tenants/{child_id}/resources/migrate-from-root` 两个端点;废弃 API 语义 410 Gone(`POST /tenants`、`POST /user/switch-tenant`)。
|
||||
|
||||
### 7.1 架构变动概述
|
||||
|
||||
原多租户模型(扁平 Tenant + 用户可属多租户)在本次改版中被重构为:
|
||||
|
||||
- **Tenant 树形**:`tenant` 表新增 `parent_tenant_id` + `share_default_to_children` 字段(2026-04-20 精简:废弃 `tenant_path` / `level` / `tenant_kind`),表达"集团总部 + 子公司"的私有化部署拓扑
|
||||
- **仅私有化部署**:2026-04-20 收窄,不再支持 SaaS 多客户共存;一个 bisheng 实例固定单 Root Tenant(不可删除/禁用)
|
||||
- **两层管理员模型**:原独立的 "Root Admin" 角色合并到"全局超管";OpenFGA `tenant.admin` 不再沿 `parent` 继承;Root Tenant 不写 `tenant:1#admin` 元组
|
||||
- **用户归属唯一叶子**:放弃 `user_tenant` 多对多,用户主部门变更即归属切换
|
||||
- **数据留原处 + 所有者交接**:调岗时资源 `tenant_id` 不迁移,提供 `transfer-owner` API 批量转交
|
||||
- **Phase 2 演进路线**:配额转移/计费对接/审批工作流/多层嵌套等推迟到 v2.6+
|
||||
|
||||
### 7.2 Review 观察与技术实施要点
|
||||
|
||||
#### 7.2.1 OpenFGA DSL 简化 ✓ 降低复杂度
|
||||
|
||||
合并管理员模型后,DSL 变化:
|
||||
|
||||
```yaml
|
||||
# 2026-04-21 最终 DSL(删除 parent 关系 + 资源列不含 tenant#member)
|
||||
type tenant
|
||||
relations
|
||||
define admin: [user] # 原: [user] or admin from parent;Root 不写此元组
|
||||
define member: [user] # 归属反查;不进资源授权
|
||||
define shared_to: [tenant] # Root → Child 显式共享
|
||||
```
|
||||
|
||||
**实施要点**:
|
||||
|
||||
- 全局超管通过 `system:global#super_admin` 在权限检查 ① 阶段短路,**不走 tenant#admin 继承链**
|
||||
- `PermissionService.check` 中的五级短路保持不变,但 ② 阶段的 `tenant#admin` FGA check 仅用于 Child Tenant 内部授权
|
||||
- DSL 升级需同步 OpenFGA authorization model 版本;存量 `tenant:{root_id}#admin` 元组迁移脚本应保留(不清除)作为审计依据,但不再参与运行时判定
|
||||
- **2026-04-21 收窄**:`tenant#parent` 关系删除(FGA 冗余,依赖 MySQL `parent_tenant_id`);资源 manager/editor/viewer 移除 `tenant#member`,viewer 仅保留 `tenant#shared_to#member`——资源授权回归 owner + user + department#member + user_group#member 四源
|
||||
|
||||
**与原技术方案的差异**:第三节 `PermissionService._is_super_admin()` 保持不变;但 tenant admin 继承逻辑需重写测试用例(原"Root admin 继承 Child admin"断言全部失效),**且新增"普通 Tenant 成员默认不可编辑/读他人资源"断言**(2026-04-21 收窄)。
|
||||
|
||||
#### 7.2.2 所有者交接 API(新增)⚠ 需追加设计
|
||||
|
||||
PRD §5.6.4.1 引入 `POST /api/v1/tenants/{tenant_id}/resources/transfer-owner`。技术方案需补充:
|
||||
|
||||
- **事务一致性**:MySQL(资源表 `user_id` 改写)+ OpenFGA(owner 元组撤销重写)双写。复用已有的 `failed_tuples` 补偿表(见原技术方案 §2.7)
|
||||
- **批量上限**:单次交接不超过 N 条资源(建议 N=500),超过分批;避免 OpenFGA 大量 Write 阻塞
|
||||
- **权限校验**:调用者需同时满足(原 owner 本人 OR tenant admin)AND (接收人是 tenant 成员)
|
||||
- **审计日志**:写 `audit_log` 表,字段含 from_user/to_user/resource_list/operator/reason
|
||||
|
||||
**新增服务**:`ResourceOwnershipService.transfer_owner(tenant_id, from_user, to_user, resource_types, resource_ids)`,接口放在 `user/domain/services/` 或新建 `tenant/domain/services/`。
|
||||
|
||||
#### 7.2.3 SSO 换型 relink 接口(新增)⚠ 实施注意
|
||||
|
||||
PRD §9.6 要求 `POST /api/v1/internal/departments/relink` 在 SSO 换 HR 系统时按 `Department.path` 回落匹配。技术要点:
|
||||
|
||||
- 匹配策略:优先 `external_id` 全匹配 → fallback `path` + 部门名称匹配
|
||||
- 若 `path` 匹配出现多候选,返回冲突列表供管理员确认,不自动决策
|
||||
- 写入前不删除旧 `external_id` 记录,写入新映射;留 audit 便于回滚
|
||||
- 幂等:同一 old→new 映射重复调用结果一致
|
||||
|
||||
#### 7.2.4 用户归属与数据留存 ✓ 原方案适配
|
||||
|
||||
PRD §5.6.4 决策"调岗不迁移资源"。对技术方案影响:
|
||||
|
||||
- `UserTenantSyncService.sync_user` 中新增第 5/6 步逻辑:**不**迁移资源 `tenant_id`;检查 `user.resources_count`,若 >0 则告警
|
||||
- 新增配置 `user_tenant_sync.enforce_transfer_before_relocate`(2026-04-21 config key 修正:原 `user.enforce_*`),默认 `false`;`true` 时 SSO 同步前阻断
|
||||
- `UserTenant` 仍保留但语义变为"归属快照"(单条 active 记录),不再是多对多
|
||||
- 同步新增 `user.token_version` 字段:主部门跨 Tenant 变更时 +1,旧 JWT 立即失效
|
||||
|
||||
#### 7.2.5 两层管理员的权限检查链路 ✓ 简化
|
||||
|
||||
原技术方案 §3.3 `LoginUser.async_access_check` 的五级短路中,② 阶段需微调:
|
||||
|
||||
```python
|
||||
# 原(三层):② FGA check(user, admin, tenant) 命中则放行(含 Root admin 对 Child 资源的继承)
|
||||
# 新(两层 + 2026-04-20 字段精简):② 仅用于 Child Tenant 级 admin;Root/全局超管已在 ① 短路
|
||||
async def check_child_tenant_admin(user_id, resource_tenant_id):
|
||||
tenant = await Tenant.aget(resource_tenant_id)
|
||||
if tenant.parent_tenant_id is None: # Root 级资源:不触发 ②(已在 ① 短路或走 ③ 归属)
|
||||
# 2026-04-20 字段精简:原 tenant.level == 0 判定改为 parent_tenant_id IS NULL
|
||||
return False
|
||||
return await fga.check(f"user:{user_id}", "admin", f"tenant:{resource_tenant_id}")
|
||||
```
|
||||
|
||||
### 7.3 新增实施风险(P0/P1 级)
|
||||
|
||||
| 级别 | 风险 | 建议 |
|
||||
|------|------|------|
|
||||
| **P0** | 所有者交接未覆盖资源类型遗漏 | 在 feature 011/013 spec 中列出 23 张业务表,逐项标注"是否需要写入 owner 元组"。漏项会导致批量交接后部分资源仍归旧 user |
|
||||
| **P0** | DSL 升级灰度策略 | 生产环境从 "admin or admin from parent" 切到 "admin" 需要滚动升级;新旧 model 共存期需兼容读取两种语义 |
|
||||
| **P1** | SSO 换型 relink 的 path 冲突处理 | 方案需补充 UI 手工裁决流;否则大规模换型时管理员工作量大 |
|
||||
| **P1** | `user_tenant_sync.enforce_transfer_before_relocate=true` 配置下的用户体验 | 需在前端出具"待交接列表"页面,否则用户会发现登录后无法切租户且无提示 |
|
||||
| ~~**P2**~~ | ~~两层管理员模型后 SaaS 多客户场景的 platform super_admin 分离~~ | **2026-04-20 收窄后移除**:bisheng 仅支持私有化单实例,不再有 SaaS 多客户场景;全局超管即本实例最高权限,无需拆分"客户超管/平台超管" |
|
||||
| **P0** | DSL 收窄(2026-04-21)影响存量资源可见性 | 原"同 Tenant 成员通过 `tenant#member` 隐式可编辑"的行为被移除;升级后若客户依赖此隐式授权,需在升级指引中提示管理员补显式 user/department/user_group 授权 |
|
||||
| **P0** | Root Tenant 保护(2026-04-20) | 升级后存量管理员不可再禁用/归档/删除 `tenant:1`;相关 API 返回 403 + 19008;需在运维手册显式说明 |
|
||||
| **P1** | Gateway 实时 vs Celery 校对 ts 冲突守卫(2026-04-20) | `OrgSyncTsGuard` 和 `org_sync_log.event_type` + 索引缺失会导致冲突无法识别;升级脚本必须补齐 DDL(见 §3.6) |
|
||||
|
||||
### 7.4 可观测性与测试
|
||||
|
||||
- **新增测试用例**:
|
||||
- 两层管理员链路的边界测试(原 Root admin 对 Child 资源的访问必须走 ① 短路,不再走 ② FGA 继承)
|
||||
- 所有者交接事务回滚测试(MySQL 成功、OpenFGA 失败时的补偿)
|
||||
- SSO 换型 relink 的多候选冲突场景
|
||||
- **压测补充**(对应 PRD §9.6):
|
||||
- 所有者交接批量 500 条的 P99 延迟 < 2s
|
||||
- 两层管理员的权限检查 ① 阶段短路命中率(预期集团场景 90%+)
|
||||
|
||||
### 7.5 Review 结论
|
||||
|
||||
本次 v2.5 Tenant 树改版在**简化管理员模型**、**明确合规边界**、**推迟商业化能力**三方面做了务实权衡,整体复杂度**低于**原设计(OpenFGA DSL 简化、无需多对多 user_tenant 维护)。主要新增工作量集中在:
|
||||
|
||||
1. 所有者交接 API 设计与实现(P0)
|
||||
2. DSL 升级的灰度策略(P0)
|
||||
3. SSO 换型 relink 接口(P1)
|
||||
4. Phase 2 能力的明确归档(附录 G,文档工作)
|
||||
|
||||
**建议开发节奏**:Feature 011/012/013(tenant-tree-model、tenant-resolver、tenant-fga-tree)作为 P0 阻塞,先行启动;所有者交接纳入 tenant-resolver 或单独 feature;SSO 换型 relink 可作为 P1 跟随 F015 LDAP reconcile feature 一并实现。
|
||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 899 KiB |
|
Before Width: | Height: | Size: 479 B |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 122 KiB |