WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
# 开发指南
|
||||
|
||||
本章面向准备对 WeKnora 做二次开发的工程师,介绍本地开发环境搭建、Makefile 命令、开发模式(`docker-compose.dev.yml`)、测试体系、代码规范与调试技巧。
|
||||
|
||||
## 1. 技术栈与环境要求
|
||||
|
||||
WeKnora 由三个可独立开发的进程组成:
|
||||
|
||||
| 组件 | 目录 | 语言 / 运行时 | 版本要求(来源) |
|
||||
| --- | --- | --- | --- |
|
||||
| 主后端 `app` | `cmd/server` + `internal/` | Go | **Go 1.26.0**(`go.mod` 中 `go 1.26.0`),需 CGO(DuckDB、sqlite-vec 绑定) |
|
||||
| 文档解析服务 `docreader` | `docreader/` | Python + gRPC | **Python >= 3.10.18**(`docreader/pyproject.toml` 中 `requires-python`),依赖用 **uv** 管理(仓库含 `uv.lock`,Docker 内 `uv sync --locked`) |
|
||||
| 前端 `frontend` | `frontend/` | Node.js + Vue 3 | Node 22 系(`devDependencies` 含 `@tsconfig/node22`、`@types/node ^22`),Vite 7 + TypeScript ~6.0 + Vue 3.5 + TDesign,版本号 `0.8.0` |
|
||||
| CLI | `cli/`(独立 Go module) | Go | Go 1.26(`.github/workflows/cli.yml` 矩阵 `go: ['1.26']`) |
|
||||
|
||||
推荐额外安装的开发工具:
|
||||
|
||||
```bash
|
||||
# 数据库迁移 CLI(scripts/migrate.sh 依赖)
|
||||
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||||
|
||||
# 代码检查(make lint 调用)
|
||||
# 安装方式见 https://golangci-lint.run;仓库根有 .golangci.yml 配置
|
||||
|
||||
# Swagger 文档生成(make docs 调用)
|
||||
make install-swagger # go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
# Python 依赖管理
|
||||
pip install uv # docreader 使用 uv sync 安装依赖
|
||||
|
||||
# Docker + Docker Compose(v2 插件或独立 docker-compose 均可,scripts/dev.sh 自动探测)
|
||||
```
|
||||
|
||||
## 2. 快速开始:开发模式(推荐)
|
||||
|
||||
开发模式的核心思想:**基础设施跑在 Docker 里,`app` 与 `frontend` 跑在本地**,改代码即时重启,无需反复构建镜像。入口是 `scripts/dev.sh`(Makefile 的 `dev-*` 目标是它的包装)。
|
||||
|
||||
```bash
|
||||
# 1. 准备环境变量:dev.sh 会加载 .env(必须存在),再用 .env.local 覆盖(可选)
|
||||
cp .env.example .env
|
||||
|
||||
# 2. 启动基础设施(ParadeDB/Postgres + Redis + docreader,默认还带 Langfuse)
|
||||
make dev-start # 等价 ./scripts/dev.sh start
|
||||
make dev-start DEV_ARGS=--qdrant # 附加可选 profile
|
||||
|
||||
# 3. 另开终端:本地跑后端(内部执行 go run -ldflags=... ./cmd/server)
|
||||
make dev-app # 等价 ./scripts/dev.sh app
|
||||
|
||||
# 4. 再开终端:本地跑前端(cd frontend && npm install && npm run dev)
|
||||
make dev-frontend # 等价 ./scripts/dev.sh frontend
|
||||
|
||||
# 其他
|
||||
make dev-status # 查看容器状态
|
||||
make dev-logs # 查看日志
|
||||
make dev-stop # 停止
|
||||
make dev-restart # 重启
|
||||
```
|
||||
|
||||
前端 dev server 监听 `5173`(`frontend/vite.config.ts` 中 `server.port: 5173`),并把 `/api` 与 `/files` 代理到本地后端(`DEV_PROXY_TARGET`)。`vite preview`(端口 `4173`)用生产构建产物起服务,是最接近 release 镜像的验证环境。
|
||||
|
||||
### 2.1 docker-compose.dev.yml 服务清单
|
||||
|
||||
`docker-compose.dev.yml` 只包含依赖服务,不含 `app`/`frontend`。默认启动与 profile 可选服务如下(profile 通过 `dev.sh start` 的参数开启):
|
||||
|
||||
| 服务 | 镜像 | 端口(默认) | 启动条件 |
|
||||
| --- | --- | --- | --- |
|
||||
| `postgres` | `paradedb/paradedb:v0.22.2-pg17`(自带 pg_search/BM25) | `5432` | 默认启动 |
|
||||
| `redis` | `redis:7.0-alpine`(`--requirepass`) | `6379` | 默认启动 |
|
||||
| `docreader` | 本地构建 `docker/Dockerfile.docreader` | `50051`(gRPC) | 默认启动 |
|
||||
| `searxng`(+`searxng-init`) | `searxng/searxng:latest` | `127.0.0.1:8888` | `--searxng` / `--full`(compose profile `searxng`) |
|
||||
| `minio` | `minio/minio:latest` | `9000` / 控制台 `9001` | `--minio` / `--full` |
|
||||
| `qdrant` | `qdrant/qdrant:v1.16.2` | `6333` / `6334` | `--qdrant` / `--full` |
|
||||
| `opensearch` | `opensearchproject/opensearch:3.3.2`(关闭 security,纯 HTTP) | `9200` | profile `opensearch` / `full` |
|
||||
| `opensearch-dashboards` | `opensearchproject/opensearch-dashboards:3.3.0` | `5601` | profile `opensearch-ui`(按需单独启动) |
|
||||
| `milvus` | `milvusdb/milvus:v2.6.11`(standalone,内嵌 etcd) | `19530` / `9091` | profile `milvus` / `full` |
|
||||
| `neo4j` | `neo4j:latest`(APOC 插件) | `7474` / `7687` | `--neo4j` / `--full` |
|
||||
| `dex` | `dexidp/dex:latest`(OIDC 测试身份源,配置 `misc/dex-config.yaml`) | `5556` | `--dex` / `--full` |
|
||||
| `langfuse-web` / `langfuse-worker` / `langfuse-clickhouse` / `langfuse-minio` / `langfuse-db-init` | Langfuse v3 自建栈,复用 dev 的 postgres(独立 `langfuse` 库)与 redis(DB 1) | web `3000`、minio `9100/9101` | `--langfuse`(`dev.sh` 默认开启,`--no-langfuse` 关闭) |
|
||||
| `odl-hybrid` | 本地构建 `docker/Dockerfile.odl-hybrid`(Docling PDF 后端) | `5002` | `--odl-hybrid`(镜像较大,按需) |
|
||||
| `sandbox` | `wechatopenai/weknora-sandbox`(Skills 脚本执行沙箱,仅 build/pull,非常驻) | - | profile `full` |
|
||||
|
||||
`dev.sh start` 的可选参数:`--minio`、`--qdrant`、`--neo4j`、`--dex`、`--langfuse`(默认开)、`--no-langfuse`、`--odl-hybrid`、`--full`(全部可选服务,不含 odl-hybrid)。通过 Makefile 传参:`make dev-start DEV_ARGS=--odl-hybrid`。
|
||||
|
||||
### 2.2 本地单独跑 docreader
|
||||
|
||||
`dev-start` 默认把 docreader 跑在容器里;如需本地调试 Python 代码:
|
||||
|
||||
```bash
|
||||
cd docreader
|
||||
uv sync # 按 uv.lock 安装依赖(容器内为 uv sync --locked --no-dev)
|
||||
uv run -m docreader.main # 启动 gRPC 服务(与 Dockerfile CMD 一致),监听 DOCREADER_GRPC_PORT(默认 50051)
|
||||
```
|
||||
|
||||
docreader 的大量调优参数(PDF 渲染 DPI、扫描件判定、SSRF 白名单、gRPC TLS 等)以 `DOCREADER_*` 环境变量注入,完整清单见 `docker-compose.dev.yml` 的 `docreader.environment` 段。
|
||||
|
||||
### 2.3 Lite 模式(零外部依赖)
|
||||
|
||||
Lite 模式把 SQLite(+sqlite-vec)与内存队列编译进单个二进制,适合快速体验与桌面端:
|
||||
|
||||
```bash
|
||||
make build-lite # 先构建前端到 web/,再 CGO 构建 Go(tags: sqlite_fts5);SKIP_FRONTEND=1 跳过前端
|
||||
make run-lite # 依赖 .env.lite,构建并启动 WeKnora-lite
|
||||
make package-lite # 打 tarball 发行包(scripts/package-lite.sh)
|
||||
make package-mac-app # 打 macOS .app(scripts/package-mac-app.sh)
|
||||
```
|
||||
|
||||
## 3. Makefile 目标全览
|
||||
|
||||
以下目标定义在根目录 `Makefile`,`make help` 也有一份中文帮助。
|
||||
|
||||
### 3.1 基础构建与运行
|
||||
|
||||
| 目标 | 作用 |
|
||||
| --- | --- |
|
||||
| `build` | `go build -o WeKnora ./cmd/server` |
|
||||
| `run` | 先 `build` 再运行 `./WeKnora` |
|
||||
| `test` | `go test -v ./...` |
|
||||
| `clean` | `go clean` 并删除二进制 |
|
||||
| `build-prod` | 生产构建:CGO_ENABLED=1,`-ldflags "-w -s"` 注入 Version/CommitID/BuildTime/GoVersion(`internal/handler` 包变量),并设置 protobuf `conflictPolicy=warn`(规避 qdrant/milvus proto 冲突) |
|
||||
| `fmt` | `go fmt ./...` |
|
||||
| `lint` | `golangci-lint run` |
|
||||
| `deps` | `go mod download` |
|
||||
| `docs` | `swag init -g ./cmd/server/main.go -o ./docs --parseDependency --parseInternal` 生成 Swagger 文档 |
|
||||
| `install-swagger` | 安装 `swag` CLI |
|
||||
|
||||
### 3.2 Docker 镜像与服务管理
|
||||
|
||||
| 目标 | 作用 |
|
||||
| --- | --- |
|
||||
| `docker-build-app` | 构建 `wechatopenai/weknora-app`(`docker/Dockerfile.app`,注入 `scripts/get_version.sh` 的版本信息) |
|
||||
| `docker-build-docreader` | 构建 `wechatopenai/weknora-docreader`(`docker/Dockerfile.docreader`) |
|
||||
| `docker-build-frontend` | 先 `scripts/build_frontend_dist.sh`,再构建 `wechatopenai/weknora-ui` |
|
||||
| `docker-build-all` | 以上三个镜像 |
|
||||
| `docker-run` | 确保 `.env` 存在(缺失时从 `.env.example` 复制或 touch)后 `docker-compose up` |
|
||||
| `docker-stop` / `docker-restart` | `docker-compose down` / `stop -t 60` + `up` |
|
||||
| `start-all` / `stop-all` | `scripts/start_all.sh`(一键启动/停止全部服务) |
|
||||
| `start-ollama` / `start-docker` | `start_all.sh --ollama` / `--docker` |
|
||||
| `build-images` / `build-images-app` / `build-images-docreader` / `build-images-frontend` / `clean-images` | `scripts/build_images.sh` 从源码构建/清理镜像 |
|
||||
| `check-env` / `list-containers` / `pull-images` | `start_all.sh --check / --list / --pull` |
|
||||
| `show-platform` | 显示 `uname -m` 与 Docker 构建平台(amd64/arm64 自动探测) |
|
||||
| `clean-db` | 删除 `weknora_postgres-data` / `weknora_minio_data` / `weknora_redis_data` 三个 Docker volume(**清空数据**) |
|
||||
|
||||
### 3.3 数据库迁移(详见《数据库与迁移》一章)
|
||||
|
||||
| 目标 | 作用 |
|
||||
| --- | --- |
|
||||
| `migrate-up` / `migrate-down` | `scripts/migrate.sh up / down` |
|
||||
| `migrate-version` | 查看当前迁移版本 |
|
||||
| `migrate-create name=xxx` | 创建一对新迁移文件 |
|
||||
| `migrate-force version=N` | 强制设置版本(dirty state 恢复) |
|
||||
| `migrate-goto version=N` | 迁移到指定版本 |
|
||||
|
||||
### 3.4 开发模式与 Lite
|
||||
|
||||
| 目标 | 作用 |
|
||||
| --- | --- |
|
||||
| `dev-start` / `dev-stop` / `dev-restart` / `dev-logs` / `dev-status` | `scripts/dev.sh start/stop/restart/logs/status`(支持 `DEV_ARGS` 传 profile 参数) |
|
||||
| `dev-app` | 本地 `go run ./cmd/server`(带版本 ldflags) |
|
||||
| `dev-frontend` | 本地 `npm run dev` |
|
||||
| `build-lite` / `run-lite` / `package-lite` / `package-mac-app` | Lite 模式构建/运行/打包(见 2.3) |
|
||||
| `download_spatial` | `go run cmd/download/duckdb/duckdb.go` 下载 DuckDB spatial 扩展(数据分析工具用) |
|
||||
|
||||
## 4. 测试体系
|
||||
|
||||
### 4.1 Go 单元测试(主模块)
|
||||
|
||||
```bash
|
||||
make test # go test -v ./...
|
||||
# 或按包运行:
|
||||
go test ./internal/infrastructure/chunker/...
|
||||
go test -run TestXxx ./internal/application/service/...
|
||||
```
|
||||
|
||||
主模块测试广泛使用 `go-sqlmock`、`miniredis` 等内存替身(见 `go.mod`),大部分无需真实数据库即可运行。部分包依赖 CGO(DuckDB/sqlite-vec)。
|
||||
|
||||
### 4.2 docreader 测试(Python)
|
||||
|
||||
测试位于 `docreader/tests/`,使用标准库 `unittest` 编写(文件内 `unittest.main()`),覆盖解析路由、并发、EPUB/Excel/MHTML/PDF 解析、SSRF 防护等:
|
||||
|
||||
```bash
|
||||
cd docreader
|
||||
uv sync
|
||||
uv run python -m unittest discover -s tests -v # 全部
|
||||
uv run python -m unittest tests.test_parser_routing # 单个
|
||||
```
|
||||
|
||||
### 4.3 CLI 测试与验收测试
|
||||
|
||||
`cli/` 是独立 Go module,自带 `cli/Makefile`:
|
||||
|
||||
```bash
|
||||
cd cli
|
||||
make test # go test ./...
|
||||
make test-coverage # 带覆盖率
|
||||
make lint # go vet
|
||||
```
|
||||
|
||||
跨切面的契约/集成测试集中在 `cli/acceptance/`(见 `cli/acceptance/doc.go`):
|
||||
|
||||
- `cli/acceptance/contract/` — envelope JSON 输出形状 golden 测试 + error.code 注册表一致性;
|
||||
- `cli/acceptance/e2e/` — 对真实 WeKnora server 的黑盒测试(testscript 风格),需要环境变量指向测试服务器;CI 侧由 `.github/workflows/cli-e2e.yml` 承载,**按需触发**(`workflow_dispatch` 手动,或给 PR 打 `acceptance-e2e` 标签),使用 secrets `WEKNORA_E2E_HOST` / `WEKNORA_E2E_TOKEN`。
|
||||
|
||||
### 4.4 tests/ 目录与前端测试
|
||||
|
||||
- `tests/miniprogram/miniprogram.test.js` — 小程序客户端的集成测试(Node 测试脚本),是 `tests/` 目前唯一内容;
|
||||
- 前端:`cd frontend && npm run type-check`(vue-tsc)与 `npm test`(`tsx --test`,Node test runner)。
|
||||
|
||||
## 5. 代码规范与提交流程
|
||||
|
||||
### 5.1 Go 代码规范
|
||||
|
||||
仓库根 `.golangci.yml`(golangci-lint v2 配置格式):
|
||||
|
||||
```yaml
|
||||
version: 2
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 120
|
||||
tab-width: 4
|
||||
linters:
|
||||
enable:
|
||||
- lll # 控制行宽(120 列)
|
||||
- govet
|
||||
- revive
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
```
|
||||
|
||||
提交前建议执行:
|
||||
|
||||
```bash
|
||||
make fmt && make lint && make test
|
||||
```
|
||||
|
||||
注意格式化标准是 **gofumpt**(比 gofmt 更严格),行宽上限 120。
|
||||
|
||||
### 5.2 CI 与提交流程
|
||||
|
||||
`.github/` 下的实际配置:
|
||||
|
||||
| 文件 | 触发路径 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `workflows/app.yml` | 根模块 Go 代码、`go.mod`、`config/`、`migrations/`、`scripts/`、`skills/preloaded/`、`docker/Dockerfile.app` | 主模块检查:gofmt 格式校验(只针对 PR 内的提交)、`go vet`、`go test`、`go build ./cmd/server` |
|
||||
| `workflows/frontend.yml` | `frontend/`、`scripts/build_frontend_dist.sh` | Node 24:`npm test` + `npm run type-check` + `npm run build` |
|
||||
| `workflows/docreader.yml` | `docreader/`、`testdata/`、`packages/`、相关 Dockerfile | uv 装依赖 → `compileall` → `unittest discover docreader/tests`;再拉起 docreader gRPC 服务跑 `go test ./docreader/client ./docreader/proto` |
|
||||
| `workflows/mcp-server.yml` | `mcp-server/` | Python 3.10-3.13 矩阵测试;合入 main 后按 `pyproject.toml` 里的版本号用 PyPI Trusted Publishing 自动发布(版本已存在则跳过上传,不依赖打 tag) |
|
||||
| `workflows/cli.yml` | `cli/` | ubuntu/macos/windows 三平台矩阵,Go 1.26,`go build` + `go test -race -coverprofile` + `go vet` + skill wire 词表检查 |
|
||||
| `workflows/cli-e2e.yml` | 手动 / label | CLI 端到端验收(label `acceptance-e2e` 或手动触发,见 4.3) |
|
||||
| `workflows/docker-image.yml` | — | Docker 镜像构建发布 |
|
||||
| `workflows/release-lite.yml` | — | Lite 版本发布 |
|
||||
| `pull_request_template.md` | — | PR 模板 |
|
||||
| `ISSUE_TEMPLATE/` | — | Issue 模板 |
|
||||
| `dependabot.yml` | — | 依赖升级机器人 |
|
||||
|
||||
四条按路径触发的检查(app / frontend / docreader / mcp-server)覆盖了主要模块,但本地先跑一遍仍然更省时间。前端可以直接用 `scripts/verify_frontend_pr.sh`,它按 CI 同样的顺序执行 `npm test` → `npm run type-check` → `npm run build`。
|
||||
|
||||
提交流程:fork / 分支 → 本地 `fmt + lint + test` → PR(按模板填写)→ 相关路径触发 CI。
|
||||
|
||||
## 6. 调试技巧
|
||||
|
||||
### 6.1 日志级别
|
||||
|
||||
日志实现在 `internal/logger/logger.go`(logrus)。级别由环境变量 `LOG_LEVEL` 控制,取值 `debug` / `info` / `warn`(`warning`)/ `error` / `fatal`,未设置或非法时默认 **debug**(`getLogLevelFromEnv()`)。`LOG_PATH` 控制输出路径;两者在 `main()` 加载 `.env` 后即时生效。docreader 侧同样读取 `LOG_LEVEL`(compose 中透传)。
|
||||
|
||||
每个请求带 `X-Request-ID` 贯穿 app 与 docreader 日志(docreader 的 `init_logging_request_id`),排查问题时先抓 request id。
|
||||
|
||||
### 6.2 GIN_MODE 与 Swagger
|
||||
|
||||
- `GIN_MODE=release` 时禁用 Swagger UI(`internal/router/router.go`)、并影响 embed channel 的安全行为;开发时不要设置或设为 `debug`。
|
||||
- `make docs` 生成 Swagger 后,启动服务访问 `http://localhost:8080/swagger/index.html`。
|
||||
|
||||
### 6.3 数据库与迁移调试
|
||||
|
||||
- `AUTO_MIGRATE=false` 可关闭启动时自动迁移;`AUTO_RECOVER_DIRTY`(默认开启,设为 `false` 关闭)控制 dirty state 自动恢复(`internal/container/container.go`)。迁移失败只告警不阻断启动,注意看启动日志里的 `Database migration failed`。
|
||||
- `make migrate-version` 快速确认 schema 版本。
|
||||
|
||||
### 6.4 LLM 链路观测(Langfuse)
|
||||
|
||||
`dev.sh start` 默认拉起自建 Langfuse(`http://localhost:3000`)。本地 `go run` 的 app 需要导出:
|
||||
|
||||
```bash
|
||||
export LANGFUSE_HOST=http://localhost:3000
|
||||
export LANGFUSE_PUBLIC_KEY=pk-lf-xxx
|
||||
export LANGFUSE_SECRET_KEY=sk-lf-xxx
|
||||
```
|
||||
|
||||
即可在 Langfuse UI 中查看每次会话的模型调用 trace(文档处理 span 亦落库到 `knowledge_processing_spans` 表,前端可视化)。
|
||||
|
||||
### 6.5 pprof
|
||||
|
||||
当前代码中**未内置** `net/http/pprof` 端点(`internal/`、`cmd/` 下无 pprof 引用)。如需性能剖析,可临时在 `cmd/server/main.go` 中 `import _ "net/http/pprof"` 并起一个独立 `http.ListenAndServe("localhost:6060", nil)`,或使用 `go test -bench . -cpuprofile` 针对具体包剖析。
|
||||
|
||||
### 6.6 分块策略诊断
|
||||
|
||||
chunker 提供 `SplitWithDiagnostics()`(`internal/infrastructure/chunker/strategy.go`),返回策略链选择、各 tier 被拒原因与文档画像,配合 `LOG_LEVEL=debug`(`chunker: tier %s rejected` 日志)可排查分块效果问题。
|
||||
@@ -0,0 +1,313 @@
|
||||
# 数据库与迁移
|
||||
|
||||
本章梳理 WeKnora 的数据库支持矩阵、`migrations/` 目录全部迁移叠加后的最终表结构、表间关系(ER 图)、golang-migrate 迁移机制,以及新增迁移与常见问题排查。
|
||||
|
||||
## 1. 支持的数据库
|
||||
|
||||
主应用通过 GORM 连接数据库,驱动由环境变量 `DB_DRIVER` 决定。`internal/container/container.go` 的 `initDatabase()` 中的 switch **只接受两个值**:
|
||||
|
||||
| `DB_DRIVER` | 说明 |
|
||||
| --- | --- |
|
||||
| `postgres` | 标准模式。既支持原生 PostgreSQL(+pgvector),也支持 **ParadeDB**(PostgreSQL 分支,内置 `pg_search`/BM25,官方 compose 默认镜像 `paradedb/paradedb:v0.22.2-pg17`)。GORM DSN 由 `DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME` 拼装,强制 `sslmode=disable`、`TimeZone=UTC` |
|
||||
| `sqlite` | Lite 模式。路径取 `DB_PATH`(默认 `./data/weknora.db`),DSN 附加 `_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on`,并加载 `sqlite-vec` 扩展(`sqlite_vec.Auto()`)做向量检索 |
|
||||
| 其他值 | 直接报错 `unsupported database driver` |
|
||||
|
||||
**MySQL 不是主库选项**:`go.mod` 里的 `go-sql-driver/mysql` 是给 Doris 检索引擎(MySQL 协议、`database/sql`)注册协议驱动用的(见 `container.go` import 注释)。`migrations/mysql/00-init-db.sql` 是一份仅含 7 张核心表(tenants/models/knowledge_bases/knowledges/sessions/messages/chunks)的一次性 MySQL 建表脚本,**没有任何 Go 代码或脚本引用它**,未接入应用启动流程,可视为遗留/外部初始化用途。
|
||||
|
||||
检索引擎(向量/关键词索引的存储)与主库解耦,由 `RETRIEVE_DRIVER` 控制(postgres / elasticsearch / qdrant / milvus / sqlite 等,详见《扩展点指南》)。当 `RETRIEVE_DRIVER` 不含 `postgres` 时,迁移 DSN 会带上 `options=-c app.skip_embedding=true`,`embeddings` 表相关迁移通过该 GUC 条件跳过。
|
||||
|
||||
## 2. 迁移目录结构
|
||||
|
||||
```text
|
||||
migrations/
|
||||
├── versioned/ # PostgreSQL/ParadeDB 版本化迁移:000000-000079 共 80 版(160 个 .up/.down.sql 文件)
|
||||
├── sqlite/ # SQLite 迁移:000000_init(压平的全量 schema)+ 其后的增量版本
|
||||
├── paradedb/ # ParadeDB 附加脚本:00-init-db.sql(扩展初始化)、01-migrate-to-paradedb.sql(存量库切换)
|
||||
└── mysql/ # 00-init-db.sql,遗留的一次性 MySQL 建表脚本(未接入代码)
|
||||
```
|
||||
|
||||
- `versioned/` 是唯一的"增量历史",从 `000000_init` 到 `000079_knowledge_folder_path`;
|
||||
- `sqlite/` 以 `000000_init` 作为压平后的全量初始化(JSONB→TEXT、SERIAL→AUTOINCREMENT 等方言差异已适配),其后按需追加增量版本(当前有 `000001_remove_wiki_log`、`000002_knowledge_folder_path`),同样由 golang-migrate 顺序执行;
|
||||
- `paradedb/00-init-db.sql` 创建 `pg_search` 等扩展;BM25 索引使用中文 Lindera 分词器建在 `embeddings.content` 上。
|
||||
|
||||
### 2.1 versioned/ 迁移史概览(按主题)
|
||||
|
||||
| 版本段 | 主题 | 引入的关键表/列 |
|
||||
| --- | --- | --- |
|
||||
| 000000 | 核心初始化 | `tenants`、`models`、`knowledge_bases`、`knowledges`、`chunks`、`sessions`、`messages` |
|
||||
| 000001 | 用户认证 + Agent + MCP | `users`、`auth_tokens`、`custom_agents`、`mcp_services`、`knowledge_tags` |
|
||||
| 000002-000011 | 向量/检索 | `embeddings`(HNSW + BM25,受 `app.skip_embedding` 门控)、`chunks.flags`、`seq_id`、ParadeDB BM25 索引 |
|
||||
| 000012-000018 | 跨租户协作 | `organizations`、`organization_members`、`kb_shares`、`agent_shares`、`organization_join_requests` |
|
||||
| 000019-000028 | 消息/IM 增强 | `messages` 扩列(images、rendered_content、agent_duration_ms)、`im_channels`、`im_channel_sessions` |
|
||||
| 000029-000036 | 数据源与向量库抽象 | `data_sources`、`sync_logs`、`web_search_providers`、`vector_stores`、KB 的 asr_config/vector_store_id |
|
||||
| 000037-000041 | Wiki 与任务队列 | `wiki_pages`、`wiki_folders`、`wiki_page_issues`、`wiki_log_entries`(已于 000077 移除)、`task_pending_ops`、`task_dead_letters` |
|
||||
| 000042-000054 | RBAC / 审计 / 邀请 | `mcp_tool_approvals`、`tenant_members`、`audit_logs`、`organization_tenant_members`、`user_resource_favorites`、`tenant_invitations`、`user_kb_pins`、`invitation_tokens` |
|
||||
| 000055-000060 | 处理管道与嵌入渠道 | `knowledge_processing_spans`、`knowledge_pending_subtasks`、`embed_channels`、HNSW 1024 维索引 |
|
||||
| 000061-000067 | Wiki 层级 / OAuth / 文档多标签 / 建议问题 | `wiki_pages` 层级列、`mcp_oauth_clients`、`mcp_oauth_tokens`、`knowledge_tag_relations`、`principals`、`principal_models`、`tenant_api_keys`、`message_suggestion_sets`、`message_suggestion_events` |
|
||||
| 000068-000074 | 存储/资源/临时文档 | `storage_backends`、`resources`、`resource_bindings`、`resource_access_grants`、`temporary_documents`、平台级 API key、OAuth 刷新租期 |
|
||||
| 000075-000076 | Wiki 版本历史与索引 | `wiki_page_revisions`、`wiki_pages.last_edit_source`/`last_editor_id`、`knowledges.metadata->>'external_id'` 前缀索引 |
|
||||
| 000077 | 移除 Wiki 操作日志 | DROP `wiki_log_entries`,并删除历史遗留的 `page_type = 'log'` 页面;Wiki 变更统一记入知识库活动流 |
|
||||
| 000078 | 分块编辑与自定义元数据 | `chunks` 增加 `source_content`/`content_revision`/`index_status`/`last_editor_id`/`context_header`,新增 `chunk_revisions` 表,`knowledges` 增加 `custom_metadata` |
|
||||
| 000079 | 知识库文件夹树 | `knowledges` 增加 `folder_path` 列并回填历史目录上传(原先路径塞在 `file_name` 里),新增 `(tenant_id, knowledge_base_id, folder_path)` 索引 |
|
||||
|
||||
## 3. 最终表结构
|
||||
|
||||
以下为全部 up 迁移叠加后的**最终生效结构**(后续迁移对早期表的 ALTER 已合并)。所有业务表统一带 `created_at` / `updated_at`,多数带 `deleted_at`(GORM 软删除),不再逐一列出。
|
||||
|
||||
### 3.1 租户与用户
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `tenants` | 租户(工作空间),多租户体系根 | `id`(SERIAL,起始 10000)、`name`、`api_key`(唯一索引)、`retriever_engines`(JSONB)、`status`、`storage_quota`/`storage_used`、`agent_config`/`context_config`/`conversation_config`/`web_search_config`/`credentials`(JSONB)、`default_storage_backend_id` |
|
||||
| `users` | 登录用户 | `id`(UUID)、`username`(唯一)、`email`(唯一)、`password_hash`、`tenant_id`(FK→tenants,ON DELETE SET NULL)、`is_active`、`can_access_all_tenants`(系统管理员)、`preferences`(JSON) |
|
||||
| `auth_tokens` | 登录令牌 | `id`、`user_id`(FK→users,CASCADE)、`token`、`token_type`(access/refresh)、`expires_at`(TIMESTAMPTZ,000072 起)、`is_revoked` |
|
||||
| `tenant_members` | 租户级 RBAC 成员关系 | `user_id`+`tenant_id`(软删除下唯一)、`role`(owner/admin/contributor/viewer)、`status`、`invited_by`、`joined_at` |
|
||||
| `tenant_invitations` | 站内邀请 | `tenant_id`、`invitee_user_id`、`role`、`status`(pending/accepted/rejected)、`expires_at`;pending 唯一约束 |
|
||||
| `invitation_tokens` | 邀请链接令牌(000054) | token 与租户/角色绑定 |
|
||||
| `tenant_api_keys` | 租户/平台 API Key | `tenant_id`(platform 作用域时为 NULL)、`scope_type`(tenant/platform,CHECK 约束)、`key_hash`(唯一)、`full_access`、`knowledge_base_ids`、`capabilities`、`expires_at`/`revoked_at` |
|
||||
| `user_kb_pins` | 用户级知识库置顶 | PK(`tenant_id`,`user_id`,`kb_id`)+ `pinned_at` |
|
||||
| `user_resource_favorites` | 用户收藏 | PK(`user_id`,`tenant_id`,`resource_type`,`resource_id`) |
|
||||
| `audit_logs` | 审计日志(000044) | `tenant_id`、`actor_user_id`/`actor_role`、`action`、`target_type`/`target_id`/`target_user_id`、`request_path`/`request_method`、`outcome`(success/denied)、`scope_type`/`scope_id`、`details`(JSONB) |
|
||||
|
||||
### 3.2 模型与知识库
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `models` | AI 模型配置(LLM/embedding/rerank 等) | `id`、`tenant_id`(FK→tenants,CASCADE)、`name`/`display_name`、`type`(embedding/summary/rerank/llm…)、`source`、`parameters`(JSONB)、`is_default`、`is_builtin`、`managed_by`、`status` |
|
||||
| `knowledge_bases` | 知识库 | `id`(UUID)、`tenant_id`、`name`、`type`(document/faq)、`chunking_config`/`image_processing_config`/`vlm_config`/`faq_config`/`asr_config`/`wiki_config`/`indexing_strategy`(JSONB)、`embedding_model_id`/`summary_model_id`(FK→models)、`vector_store_id`(FK→vector_stores)、`storage_backend_id`(FK→storage_backends)、`creator_id`(FK→users)、`is_temporary`、`activity_scope` |
|
||||
| `knowledges` | 知识条目(文档/网页/FAQ 等) | `id`、`tenant_id`、`knowledge_base_id`(FK)、`type`、`title`、`source`(VARCHAR(2048))、`parse_status`(unprocessed/processing/completed/failed)、`enable_status`、`file_name`/`file_type`/`file_size`/`file_path`/`file_hash`、`metadata`(内部入库状态)、`custom_metadata`(JSONB,用户自填元数据,000078)、`folder_path`(目录树路径,000079)、`summary_status`、`channel`、`processed_at`/`error_message`。**没有 `tag_id` 列**——000063 起标签走 `knowledge_tag_relations` 关联表 |
|
||||
| `chunks` | 分块(检索最小单元) | `id`、`tenant_id`、`knowledge_base_id`、`knowledge_id`(FK)、`content`、`source_content`(解析器原始输出,不可变)、`content_revision`、`index_status`(ready/processing/failed)、`last_editor_id`、`context_header`(索引用标题面包屑)、`chunk_index`、`start_at`/`end_at`、`pre_chunk_id`/`next_chunk_id`(链表)、`parent_chunk_id`(父子分块自引用)、`chunk_type`(text/image/…)、`image_info`/`video_info`、`relation_chunks`/`indirect_relation_chunks`(JSONB)、`is_enabled`、`flags`、`status`、`content_hash`、`seq_id`、`tag_id` |
|
||||
| `chunk_revisions` | 分块历史版本(000078) | `id`、`tenant_id`、`knowledge_base_id`、`knowledge_id`、`chunk_id`+`revision`(唯一索引)、`content`、`is_enabled`、`editor_id`、`edit_source`、`edited_at` |
|
||||
| `embeddings` | 向量 + BM25 索引(Postgres/ParadeDB 检索引擎专用,受 `app.skip_embedding` 门控) | `id`、`source_id`+`source_type`(唯一,chunk/wiki 页等来源)、`chunk_id`/`knowledge_id`/`knowledge_base_id`、`content`(BM25 全文)、`dimension`、`embedding`(halfvec,HNSW 索引按 768/1024/3584 维分建)、`is_enabled`、`tag_id` |
|
||||
| `knowledge_tags` | 知识标签(FAQ 分类等) | `id`、`tenant_id`、`knowledge_base_id`、`name`、`seq_id` |
|
||||
| `knowledge_tag_relations` | 文档 ↔ 标签多对多(000063) | 复合主键(`knowledge_id`,`tag_id`)+ `created_at`;两侧各建索引。**同时删掉了 `knowledges.tag_id` 列**(存量单标签数据已迁入本表)。FAQ 条目的标签不在这里,仍是 `chunks.tag_id` 单标签 |
|
||||
| `vector_stores` | 外接向量库连接配置(000032) | `id`、`tenant_id`、`name`(租户内唯一)、`engine_type`、`connection_config`/`index_config`(JSONB) |
|
||||
|
||||
### 3.3 会话与消息
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `sessions` | 会话(对话上下文与检索参数快照) | `id`、`tenant_id`、`title`、`knowledge_base_id`、`agent_id`(FK→custom_agents)、`user_id`、`max_rounds`、`enable_rewrite`、`fallback_strategy`/`fallback_response`、`keyword_threshold`/`vector_threshold`、`embedding_top_k`/`rerank_top_k`/`rerank_threshold`、`rerank_model_id`/`summary_model_id`、`agent_config`/`context_config`(JSONB) |
|
||||
| `messages` | 消息 | `id`、`request_id`、`session_id`(FK)、`role`、`content`/`rendered_content`、`knowledge_references`(JSONB 引用)、`agent_steps`(JSONB,Agent 推理轨迹)、`mentioned_items`/`images`(JSONB)、`is_completed`/`is_fallback`、`channel`(web/IM 渠道)、`agent_id`+`agent_tenant_id`、`model_id`、`knowledge_id`、`agent_duration_ms`、`execution_context` |
|
||||
| `message_suggestion_sets` | 建议问题集(000067) | `tenant_id`、`session_id`、`assistant_message_id`、`placement`(starter/follow_up)、`config_hash`+`locale`(缓存键,唯一)、`status`、`questions`(JSONB)、token/延迟统计、`lease_until` |
|
||||
| `message_suggestion_events` | 建议问题曝光/点击事件 | `suggestion_set_id`(FK,CASCADE)、`question_id`、`event_type`、`actor_id` |
|
||||
| `temporary_documents` | 会话内临时文档(000070) | `tenant_id`、`session_id`、`resource_ref`、`file_name`/`file_type`/`file_size`、`status`(uploaded/processing/ready/expired)、`content`、`chunks`(JSONB)、`expires_at` |
|
||||
|
||||
### 3.4 Agent 与 MCP
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `custom_agents` | 自定义 Agent | **复合主键 (`id`,`tenant_id`)**、`name`、`is_builtin`、`created_by`(FK→users)、`runnable_by_viewer`、`config`(JSONB:模式/模型/工具/知识范围) |
|
||||
| `mcp_services` | MCP 服务配置 | `id`、`tenant_id`、`name`、`enabled`、`transport_type`(stdio/sse/…)、`url`/`headers`/`auth_config`/`stdio_config`/`env_vars`(JSONB)、`is_builtin` |
|
||||
| `mcp_tool_approvals` | MCP 工具审批策略(000042) | (`tenant_id`,`service_id`,`tool_name`) 唯一、`require_approval` |
|
||||
| `mcp_oauth_clients` | MCP OAuth 客户端(000062) | (`tenant_id`,`service_id`) 唯一、`client_id`/`client_secret`/`redirect_uri` |
|
||||
| `mcp_oauth_tokens` | MCP OAuth 令牌 | (`tenant_id`,`user_id`,`service_id`) 唯一、`access_token`/`refresh_token`、`expires_at`、`refresh_lease_id`/`refresh_lease_until`(000074,防并发刷新) |
|
||||
| `principals` / `principal_models` | 主体—模型授权(000064) | 主体(用户/租户)可用模型映射 |
|
||||
|
||||
### 3.5 跨租户协作(组织)
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `organizations` | 组织(跨租户协作单元,000012) | `id`、`name`、`owner_id`(FK→users)、`owner_tenant_id`、`invite_code`(唯一)+ 过期控制、`require_approval`、`searchable`、`member_limit` |
|
||||
| `organization_members` | 组织的用户成员 | `organization_id`(FK,CASCADE)、`user_id`、`tenant_id`、`role` |
|
||||
| `organization_tenant_members` | 组织的租户成员(000045) | (`organization_id`,`tenant_id`) 唯一、`role`(admin/editor/viewer)、`representative_user_id` |
|
||||
| `organization_join_requests` | 加入/升级申请 | `organization_id`、`user_id`、`status`(pending 唯一)、`requested_role`、`request_type`(join/upgrade)、审批字段 |
|
||||
| `kb_shares` | 知识库共享到组织 | (`knowledge_base_id`,`organization_id`) 软删除下唯一、`source_tenant_id`、`permission` |
|
||||
| `agent_shares` | Agent 共享到组织 | FK (`agent_id`,`source_tenant_id`)→custom_agents 复合主键、`organization_id`、`permission` |
|
||||
| `tenant_disabled_shared_agents` | 租户禁用某共享 Agent | PK(`tenant_id`,`agent_id`,`source_tenant_id`) |
|
||||
|
||||
### 3.6 Wiki
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `wiki_pages` | AI 生成的 Wiki 页面(000037) | `id`、`tenant_id`、`knowledge_base_id`、`slug`(KB 内唯一)、`title`、`page_type`(summary/index/…)、`status`、`content`/`summary`、层级列(000061:`parent_slug`、`folder_id`、`category_path`、`wiki_path`、`depth`、`sort_order`)、`source_refs`/`chunk_refs`/`in_links`/`out_links`(JSONB)、`version`;全文 GIN/tsvector + trigram 索引 |
|
||||
| `wiki_folders` | Wiki 文件夹树 | `knowledge_base_id`、`parent_id`(邻接表)、`name`(同父下唯一)、`path`(物化路径)、`depth`、`sort_order` |
|
||||
| `wiki_page_issues` | 页面问题上报 | `knowledge_base_id`、`slug`、`issue_type`、`description`、`suspected_knowledge_ids`、`status`、`reported_by` |
|
||||
| `wiki_page_revisions` | Wiki 页面历史版本(000075) | `page_id`+`version`(唯一索引)、标题/正文/摘要/类型/状态/别名快照、`edit_source`(pipeline/agent/user/revert)、`editor_id`、`edited_at`;两级保留上限:软 50 版(只裁 pipeline 与空来源)/ 硬 200 版 |
|
||||
|
||||
### 3.7 数据源 / 渠道 / 搜索
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `data_sources` | 外部数据源连接(Feishu/Notion/语雀/RSS,000029) | `id`、`tenant_id`、`knowledge_base_id`、`type`、`config`(JSONB 凭证)、`sync_schedule`(cron)、`sync_mode`(incremental/full)、`conflict_strategy`、`sync_deletions`、`last_sync_at`/`last_sync_cursor`/`last_sync_result` |
|
||||
| `sync_logs` | 每次同步的执行记录 | `data_source_id`(FK,CASCADE)、`status`、`started_at`/`finished_at`、`items_total/created/updated/deleted/skipped/failed`、`error_message` |
|
||||
| `im_channels` | IM 渠道接入配置(企业微信/飞书/Slack 等) | `tenant_id`、`platform`、`agent_id`、`knowledge_base_id`、凭证配置 |
|
||||
| `im_channel_sessions` | IM 用户/线程 ↔ session 映射 | `im_channel_id`、`session_id`、`agent_id`、平台用户/会话标识 |
|
||||
| `embed_channels` | 网页嵌入聊天组件渠道(000060) | `tenant_id`、`agent_id`、公开 token/域名配置 |
|
||||
| `web_search_providers` | 联网搜索引擎配置(000030) | `id`、`tenant_id`、`name`、`provider`(bing/google/tavily/searxng…)、`parameters`(JSONB API key)、`is_default` |
|
||||
|
||||
### 3.8 存储 / 资源 / 任务 / 可观测
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `storage_backends` | 对象存储后端配置(000068) | `id`、`tenant_id`、`name`(租户内唯一)、`provider`(local/minio/cos/oss/s3/obs/tos/ks3)、`config`(JSONB)、`source`(user/system)、`legacy_alias` |
|
||||
| `resources` | 统一资源注册表(000069) | `id`、`handle`(22 位短句柄,唯一)、`tenant_id`、`storage_backend_id`、`provider`、`physical_path`、`location_hash`(租户内唯一)、`mime_type`/`original_name`/`size`/`content_hash`、`lifecycle`(persistent/temporary)+`expires_at`、`state` |
|
||||
| `resource_bindings` | 资源 ↔ 属主(消息/知识/会话)多态绑定 | (`resource_id`,`owner_type`,`owner_id`,`relation`) 唯一 |
|
||||
| `resource_access_grants` | 资源临时访问令牌 | `token_hash`(唯一)、`resource_id`、`access_scope`、`expires_at`/`revoked_at` |
|
||||
| `task_pending_ops` | 通用待处理任务队列(000041) | `tenant_id`、`task_type`、`scope`+`scope_id`、`op`、`dedup_key`、`payload`(JSONB)、`fail_count`、`enqueued_at`/`claimed_at`(并发领取) |
|
||||
| `task_dead_letters` | 失败任务死信归档 | `task_type`、`scope`/`scope_id`/`related_id`、`payload`、`last_error`、`fail_count`、`failed_at` |
|
||||
| `knowledge_pending_subtasks` | 知识处理子任务队列(000056) | `knowledge_id`、`attempt`、`task_type`、payload |
|
||||
| `knowledge_processing_spans` | 文档处理管道 trace(000055) | (`knowledge_id`,`attempt`,`span_id`) 唯一、`parent_span_id`、`name`(DocReader/Chunking/Embedding…)、`kind`、`status`、`input`/`output`/`metadata`(JSONB)、`error_code`/`error_message`、`duration_ms` |
|
||||
| `schema_migrations` | golang-migrate 状态表(自动维护) | `version`、`dirty` |
|
||||
|
||||
## 4. ER 图(核心表)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
tenants ||--o{ users : "tenant_id (SET NULL)"
|
||||
tenants ||--o{ tenant_members : "租户成员"
|
||||
users ||--o{ tenant_members : "user_id"
|
||||
users ||--o{ auth_tokens : "登录令牌"
|
||||
tenants ||--o{ models : "模型配置"
|
||||
tenants ||--o{ knowledge_bases : "知识库"
|
||||
tenants ||--o{ tenant_api_keys : "API Key"
|
||||
tenants ||--o{ audit_logs : "审计"
|
||||
users ||--o{ audit_logs : "actor_user_id"
|
||||
|
||||
knowledge_bases ||--o{ knowledges : "文档"
|
||||
knowledge_bases }o--|| models : "embedding_model_id"
|
||||
knowledge_bases }o--o| vector_stores : "vector_store_id"
|
||||
knowledge_bases }o--o| storage_backends : "storage_backend_id"
|
||||
knowledge_bases }o--o| users : "creator_id"
|
||||
knowledges ||--o{ chunks : "分块"
|
||||
chunks ||--o| chunks : "parent_chunk_id (父子分块)"
|
||||
chunks ||--o| embeddings : "source_id (向量/BM25)"
|
||||
knowledge_bases ||--o{ knowledge_tags : "标签"
|
||||
knowledge_tags ||--o{ knowledge_tag_relations : "多标签关联"
|
||||
knowledges ||--o{ knowledge_tag_relations : "knowledge_id"
|
||||
|
||||
tenants ||--o{ sessions : "会话"
|
||||
sessions ||--o{ messages : "消息"
|
||||
sessions }o--o| custom_agents : "agent_id"
|
||||
sessions }o--o| knowledge_bases : "knowledge_base_id"
|
||||
messages }o--o| knowledges : "knowledge_id"
|
||||
messages ||--o{ message_suggestion_sets : "建议问题"
|
||||
message_suggestion_sets ||--o{ message_suggestion_events : "事件"
|
||||
sessions ||--o{ temporary_documents : "临时文档"
|
||||
|
||||
tenants ||--o{ custom_agents : "自定义 Agent"
|
||||
tenants ||--o{ mcp_services : "MCP 服务"
|
||||
mcp_services ||--o{ mcp_tool_approvals : "工具审批"
|
||||
mcp_services ||--o{ mcp_oauth_clients : "OAuth 客户端"
|
||||
mcp_services ||--o{ mcp_oauth_tokens : "OAuth 令牌"
|
||||
|
||||
users ||--o{ organizations : "owner_id"
|
||||
organizations ||--o{ organization_tenant_members : "租户成员"
|
||||
organizations ||--o{ kb_shares : "知识库共享"
|
||||
organizations ||--o{ agent_shares : "Agent 共享"
|
||||
organizations ||--o{ organization_join_requests : "加入申请"
|
||||
knowledge_bases ||--o{ kb_shares : "被共享"
|
||||
custom_agents ||--o{ agent_shares : "被共享 (id, tenant_id)"
|
||||
|
||||
knowledge_bases ||--o{ wiki_pages : "Wiki 页面"
|
||||
wiki_pages }o--o| wiki_folders : "folder_id"
|
||||
wiki_folders ||--o{ wiki_folders : "parent_id (树)"
|
||||
knowledge_bases ||--o{ wiki_page_issues : "问题上报"
|
||||
|
||||
knowledge_bases ||--o{ data_sources : "数据源"
|
||||
data_sources ||--o{ sync_logs : "同步日志"
|
||||
tenants ||--o{ web_search_providers : "联网搜索配置"
|
||||
tenants ||--o{ im_channels : "IM 渠道"
|
||||
im_channels ||--o{ im_channel_sessions : "渠道会话映射"
|
||||
im_channel_sessions }o--|| sessions : "session_id"
|
||||
tenants ||--o{ embed_channels : "嵌入渠道"
|
||||
|
||||
tenants ||--o{ storage_backends : "存储后端"
|
||||
tenants ||--o{ resources : "资源"
|
||||
resources }o--o| storage_backends : "storage_backend_id"
|
||||
resources ||--o{ resource_bindings : "多态绑定 (message/knowledge/session)"
|
||||
resources ||--o{ resource_access_grants : "访问授权"
|
||||
|
||||
tenants ||--o{ task_pending_ops : "任务队列"
|
||||
tenants ||--o{ task_dead_letters : "死信"
|
||||
knowledges ||--o{ knowledge_processing_spans : "处理 trace"
|
||||
```
|
||||
|
||||
## 5. 迁移机制(golang-migrate)
|
||||
|
||||
迁移工具是 **golang-migrate/migrate v4**(`go.mod`:`github.com/golang-migrate/migrate/v4 v4.19.1`),状态记录在 `schema_migrations` 表(`version` + `dirty`)。有两条执行路径:
|
||||
|
||||
### 5.1 应用启动时自动迁移(默认)
|
||||
|
||||
`internal/container/container.go` 的 `initDatabase()`:
|
||||
|
||||
- `AUTO_MIGRATE != "false"` 时(**默认开启**),调用 `database.RunMigrationsWithOptions(migrateDSN, opts)`;
|
||||
- `AUTO_RECOVER_DIRTY != "false"` 时(**默认开启**)设置 `MigrationOptions.AutoRecoverDirty = true`,遇到 dirty state 自动尝试恢复;
|
||||
- 迁移失败**只打 Warn 日志不阻断启动**(假设迁移可能由外部管理),排查问题时务必看启动日志;
|
||||
- postgres 的 migrate DSN 会拼上 `options=-c app.skip_embedding=<true|false>`(取决于 `RETRIEVE_DRIVER` 是否包含 `postgres`),控制 `embeddings` 相关迁移是否实际建表建索引。
|
||||
|
||||
`internal/database/migration.go` 中的路径选择逻辑:
|
||||
|
||||
```go
|
||||
// internal/database/migration.go
|
||||
migrationsPath := "file://migrations/versioned"
|
||||
if strings.HasPrefix(dsn, "sqlite3://") {
|
||||
migrationsPath = "file://migrations/sqlite"
|
||||
}
|
||||
```
|
||||
|
||||
即 postgres/ParadeDB 走 `migrations/versioned/`,SQLite 走 `migrations/sqlite/`。
|
||||
|
||||
### 5.2 手工执行:scripts/migrate.sh
|
||||
|
||||
`scripts/migrate.sh` 是 `migrate` CLI 的包装(Makefile 的 `migrate-*` 目标调用它):
|
||||
|
||||
- 自动加载根目录 `.env`;
|
||||
- DSN 优先取 `DB_URL`(并把 `sslmode=require/prefer` 强制替换为 `disable`),否则由 `DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME` 拼装(默认 `localhost:5432/postgres/WeKnora`),密码用 Python `urllib.parse.quote` URL 编码以兼容特殊字符;
|
||||
- 迁移目录默认 `MIGRATIONS_DIR=migrations/versioned`;
|
||||
- 未安装 `migrate` 时提示:`go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest`。
|
||||
|
||||
```bash
|
||||
make migrate-up # 应用全部待执行迁移
|
||||
make migrate-down # 回滚
|
||||
make migrate-version # 查看当前版本与 dirty 标志
|
||||
make migrate-create name=add_xxx # 创建 000080_add_xxx.up.sql / .down.sql
|
||||
make migrate-force version=74 # 强制标记版本(恢复 dirty)
|
||||
make migrate-goto version=60 # 迁移/回滚到指定版本
|
||||
```
|
||||
|
||||
## 6. 如何新增一个迁移
|
||||
|
||||
1. **创建文件**:`make migrate-create name=add_my_feature`,在 `migrations/versioned/` 下生成下一个版本号(当前最大为 `000079`,新迁移将是 `000080_add_my_feature.up.sql` / `.down.sql`);
|
||||
2. **编写 up SQL**:注意 PostgreSQL 方言(JSONB、部分索引、`TIMESTAMP WITH TIME ZONE`);若涉及 `embeddings` 表,参考既有迁移用 `app.skip_embedding` GUC 做条件门控(`SELECT current_setting('app.skip_embedding', true)`),保证非 postgres 检索引擎部署也能通过迁移;
|
||||
3. **编写 down SQL**:必须可逆(drop column/table/index),否则回滚链会断;
|
||||
4. **同步 SQLite**:`migrations/sqlite/000000_init.up.sql` 是压平的全量 schema,**新增列/表必须合并进去**(注意方言转换:JSONB→TEXT、SERIAL→INTEGER AUTOINCREMENT、无部分索引语法差异等)。若变更需要在已有 Lite 库上生效(例如删表、删数据),还要在 `migrations/sqlite/` 追加一个增量版本;
|
||||
5. **同步 GORM 模型**:在 `internal/types/` 对应 struct 增加字段(GORM 只做 ORM 映射,生产库**不使用 AutoMigrate** 建表,schema 完全由 SQL 迁移驱动);
|
||||
6. **验证**:`make migrate-up` → `make migrate-down` → `make migrate-up` 三连确认可逆;SQLite 侧用 `DB_DRIVER=sqlite` 启动一次 Lite 版验证初始化脚本。
|
||||
|
||||
## 7. 常见迁移问题排查
|
||||
|
||||
### 7.1 dirty state(最常见)
|
||||
|
||||
迁移中途失败/进程被杀后,`schema_migrations.dirty = true`,后续迁移拒绝执行。
|
||||
|
||||
```bash
|
||||
# 1. 确认状态
|
||||
make migrate-version # 输出形如 "74 (dirty)"
|
||||
# 或直接查表
|
||||
# SELECT version, dirty FROM schema_migrations;
|
||||
|
||||
# 2. 人工检查该版本的 up SQL 实际执行到哪,把残留补齐或清理
|
||||
|
||||
# 3. 强制回到上一个干净版本后重试
|
||||
make migrate-force version=73
|
||||
make migrate-up
|
||||
```
|
||||
|
||||
应用默认 `AUTO_RECOVER_DIRTY` 开启(`container.go`),启动时会自动尝试恢复;若关闭(设为 `false`),日志会提示手工使用 force。
|
||||
|
||||
### 7.2 迁移"成功"但表没建出来
|
||||
|
||||
检查启动日志:自动迁移失败只是 Warn(`Database migration failed ... Continuing with application startup`),不会让进程退出。另外 `embeddings` 相关对象受 `app.skip_embedding` 门控——若 `RETRIEVE_DRIVER` 不含 `postgres`,不建 `embeddings` 索引属预期行为。
|
||||
|
||||
### 7.3 密码特殊字符导致连接失败
|
||||
|
||||
`migrate` CLI 要求 URL 形式 DSN,密码含 `@ # !` 等字符必须 URL 编码。`scripts/migrate.sh` 和 `container.go` 都已处理(分别用 Python `quote` 与 Go `url.QueryEscape`);自己手拼 `DB_URL` 时需自行编码。
|
||||
|
||||
### 7.4 ParadeDB / 原生 Postgres 差异
|
||||
|
||||
BM25 索引(`USING bm25`、Lindera 中文分词)只在 ParadeDB 可用;原生 Postgres 部署需保证相应迁移的条件分支生效或改用 Elasticsearch 等外部检索引擎。存量原生 Postgres 库切到 ParadeDB 可参考 `migrations/paradedb/01-migrate-to-paradedb.sql`。
|
||||
|
||||
### 7.5 版本文件冲突
|
||||
|
||||
多个分支同时新增同一个版本号(如两个 `000080_*`)会冲突:golang-migrate 按数字排序且版本号唯一。合并时后合入者需要把自己的迁移改成下一个空闲版本号(up/down 两个文件都要改名)。
|
||||
@@ -0,0 +1,704 @@
|
||||
# 扩展点指南
|
||||
|
||||
WeKnora 在文档解析、分块、检索、模型接入、联网搜索、数据源、IM 渠道、Agent 工具、对象存储九个层面都预留了清晰的扩展点。本章逐个给出:**核心接口定义(真实源码)→ 现有实现列表 → 新增实现步骤(含注册点文件)**。所有接口代码均摘自当前仓库源码。
|
||||
|
||||
## 0. 扩展点总览
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph DR["docreader (Python)"]
|
||||
P1["文档解析器<br/>(parser/registry.py)"]
|
||||
end
|
||||
subgraph APP["app (Go, internal/)"]
|
||||
P2["分块策略<br/>(infrastructure/chunker)"]
|
||||
P3["检索引擎<br/>(application/repository/retriever)"]
|
||||
P4["模型 Provider<br/>(models/provider)"]
|
||||
P5["联网搜索引擎<br/>(infrastructure/web_search)"]
|
||||
P6["数据源连接器<br/>(datasource/connector)"]
|
||||
P7["IM 平台适配器<br/>(im/adapter.go)"]
|
||||
P8["Agent 工具<br/>(agent/tools)"]
|
||||
P9["存储后端<br/>(application/service/file)"]
|
||||
end
|
||||
DOC["原始文档"] --> P1
|
||||
P1 -->|"markdown + 图片"| P2
|
||||
P2 -->|"chunks"| P3
|
||||
P6 -->|"外部内容同步"| P1
|
||||
P7 -->|"IM 消息"| AG["Agent 引擎"]
|
||||
AG --> P8
|
||||
P8 --> P3
|
||||
P8 --> P5
|
||||
AG --> P4
|
||||
P1 -.->|"文件读写"| P9
|
||||
P2 -.-> P9
|
||||
CT["container.go<br/>(依赖注入 / 注册中枢)"] -.->|"注册"| P3
|
||||
CT -.->|"注册"| P5
|
||||
CT -.->|"注册"| P6
|
||||
CT -.->|"注册"| P7
|
||||
```
|
||||
|
||||
Go 侧绝大多数扩展点的**注册中枢**是 `internal/container/container.go`(依赖注入容器):检索引擎 `initRetrieveEngineRegistry()`、联网搜索 `registerWebSearchProviders()`、IM 适配器 `registerIMAdapterFactories()`、数据源连接器 `initConnectorRegistry()`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 新增文档解析器(docreader,Python)
|
||||
|
||||
### 接口定义
|
||||
|
||||
基类在 `docreader/parser/base_parser.py`。轻量化重构后 BaseParser 只负责把文档转成 markdown 文本 + 原始图片引用(分块、图片存储、OCR、VLM caption 均在 Go 侧完成):
|
||||
|
||||
```python
|
||||
# docreader/parser/base_parser.py
|
||||
class BaseParser(ABC):
|
||||
"""Base parser interface."""
|
||||
|
||||
def __init__(self, file_name: str = "", file_type: Optional[str] = None, **kwargs):
|
||||
self.file_name = file_name
|
||||
self.file_type = file_type or os.path.splitext(file_name)[1].lstrip(".")
|
||||
|
||||
@abstractmethod
|
||||
def parse_into_text(self, content: bytes) -> Document:
|
||||
"""Parse document content into markdown text.
|
||||
|
||||
Returns:
|
||||
Document with ``content`` (markdown string) and optional
|
||||
``images`` dict mapping storage-relative paths to base64 data.
|
||||
"""
|
||||
```
|
||||
|
||||
返回值 `Document`(`docreader/models/document.py`,pydantic 模型)核心字段是 `content: str`(markdown)与 `images: Dict[str, str]`(路径 → base64)。
|
||||
|
||||
### 注册机制
|
||||
|
||||
`docreader/parser/registry.py` 的 `ParserEngineRegistry` 以"引擎名 → {文件扩展名 → Parser 类}"两级映射管理解析器;当请求的引擎不支持该文件类型时自动回落到 `builtin` 引擎。默认注册表由 `_build_default_registry()` 构建,模块级单例 `registry = _build_default_registry()`。
|
||||
|
||||
```python
|
||||
# docreader/parser/registry.py(节选)
|
||||
class ParserEngineRegistry:
|
||||
def register(self, name: str, file_types: Dict[str, Type[BaseParser]],
|
||||
description: str = "", check_available: Callable = None,
|
||||
unavailable_hint: str = ""): ...
|
||||
def get_parser_class(self, engine: str, file_type: str) -> Type[BaseParser]: ...
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
| 引擎 | Parser | 文件 |
|
||||
| --- | --- | --- |
|
||||
| `builtin` | `Docx2Parser` / `DocParser` / `PDFParser` / `MarkdownParser` / `ExcelParser` / `EPUBParser` / `HTMLParser` / `MHTMLParser` / `ImageParser`(jpg/png/gif/bmp/tiff/webp 等) | `docreader/parser/docx2_parser.py`、`doc_parser.py`、`pdf_parser.py`、`markdown_parser.py`、`excel_parser.py`、`epub_parser.py`、`html_parser.py`、`mhtml_parser.py`、`image_parser.py` |
|
||||
| `markitdown` | `MarkitdownParser`(微软 MarkItDown,多格式) | `docreader/parser/markitdown_parser.py` |
|
||||
| `opendataloader` | `OpenDataLoaderParser`(PDF 版面分析,需 Java 11+,带 `check_available` 探测) | `docreader/parser/opendataloader_parser.py` |
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `docreader/parser/` 新建 `my_parser.py`,继承 `BaseParser`,实现 `parse_into_text(content: bytes) -> Document`;
|
||||
2. **注册点:`docreader/parser/registry.py`** — 在 `_build_default_registry()` 中追加:
|
||||
|
||||
```python
|
||||
reg.register(
|
||||
"my_engine",
|
||||
{"myext": MyParser},
|
||||
description="我的解析引擎",
|
||||
check_available=lambda overrides: (True, ""), # 可选:依赖可用性探测
|
||||
unavailable_hint="缺依赖时给用户的提示", # 可选
|
||||
)
|
||||
```
|
||||
|
||||
3. 若是给已有扩展名换实现,也可只往 `builtin` 的映射里加一行 `"ext": MyParser`;
|
||||
4. 在 `docreader/tests/` 增加 unittest(参考 `test_parser_routing.py`),`uv run python -m unittest` 验证。
|
||||
|
||||
---
|
||||
|
||||
## 2. 新增分块策略(internal/infrastructure/chunker)
|
||||
|
||||
### 接口定义
|
||||
|
||||
分块没有 interface,而是**策略分层(tier)+ 包级函数变量覆盖**的模式。公共入口在 `internal/infrastructure/chunker/strategy.go`:
|
||||
|
||||
```go
|
||||
// internal/infrastructure/chunker/strategy.go
|
||||
// Strategy values for SplitterConfig.Strategy.
|
||||
const (
|
||||
StrategyAuto = "auto"
|
||||
StrategyHeading = "heading"
|
||||
StrategyHeuristic = "heuristic"
|
||||
StrategyRecursive = "recursive"
|
||||
StrategyLegacy = "legacy"
|
||||
)
|
||||
|
||||
func Split(text string, cfg SplitterConfig) []Chunk
|
||||
func SplitWithDiagnostics(text string, cfg SplitterConfig) ([]Chunk, *Diagnostics)
|
||||
func SplitParentChild(text string, parentCfg, childCfg SplitterConfig) ParentChildResult
|
||||
```
|
||||
|
||||
配置与结果类型在 `internal/infrastructure/chunker/splitter.go`:
|
||||
|
||||
```go
|
||||
// internal/infrastructure/chunker/splitter.go
|
||||
type Chunk struct {
|
||||
Content string
|
||||
ContextHeader string
|
||||
Seq int
|
||||
Start int
|
||||
End int
|
||||
}
|
||||
|
||||
type SplitterConfig struct {
|
||||
ChunkSize int
|
||||
ChunkOverlap int
|
||||
Separators []string
|
||||
Strategy string // 空 = legacy(向后兼容)
|
||||
TokenLimit int // 以近似 token 数限制块大小,0 = 用 ChunkSize 字符数
|
||||
Languages []string // 多语言启发式提示,空 = 自动检测
|
||||
}
|
||||
```
|
||||
|
||||
策略分发在 `runTier()`;heading / heuristic 两个实现通过包级函数变量在各自文件的 `init()` 中覆盖:
|
||||
|
||||
```go
|
||||
// internal/infrastructure/chunker/strategy.go
|
||||
func runTier(tier StrategyTier, text string, cfg SplitterConfig, profile *DocProfile) []Chunk {
|
||||
switch tier {
|
||||
case TierHeading:
|
||||
return splitByHeadings(text, cfg, profile)
|
||||
case TierHeuristic:
|
||||
return splitByHeuristics(text, cfg, profile)
|
||||
case TierLegacy:
|
||||
return SplitText(text, cfg)
|
||||
}
|
||||
return SplitText(text, cfg)
|
||||
}
|
||||
|
||||
var splitByHeadings = func(text string, cfg SplitterConfig, _ *DocProfile) []Chunk {
|
||||
return SplitText(text, cfg) // 被 heading_splitter.go 的 init() 覆盖
|
||||
}
|
||||
var splitByHeuristics = func(text string, cfg SplitterConfig, _ *DocProfile) []Chunk {
|
||||
return SplitText(text, cfg) // 被 heuristic_splitter.go 的 init() 覆盖
|
||||
}
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
| 策略 tier | 说明 | 文件 |
|
||||
| --- | --- | --- |
|
||||
| `TierHeading` | 按 Markdown 标题层级分块 | `internal/infrastructure/chunker/heading_hierarchy.go` 等 |
|
||||
| `TierHeuristic` | 多语言启发式分块 | `internal/infrastructure/chunker/heuristic_splitter.go` |
|
||||
| `TierLegacy`(=`recursive`) | 递归分隔符分块(原始实现) | `internal/infrastructure/chunker/splitter.go` 的 `SplitText()` |
|
||||
| 校验器 | 每个 tier 输出经 `ValidateChunks` 验收,失败则沿链回落 | `internal/infrastructure/chunker/validator.go` |
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/infrastructure/chunker/` 新建 `my_splitter.go`,实现 `func(text string, cfg SplitterConfig, profile *DocProfile) []Chunk`;
|
||||
2. **注册点:`internal/infrastructure/chunker/strategy.go`** —
|
||||
- 增加策略常量(如 `StrategyMine = "mine"`)与新的 `StrategyTier`;
|
||||
- 在 `resolveChain`/`resolveChainWithProfile` 的 switch 中为新策略返回 tier 链(建议以 `TierLegacy` 兜底);
|
||||
- 在 `runTier()` 中新增 case;
|
||||
3. 调用方无需改动:知识库的 `chunking_config.strategy`(JSONB)经 `internal/application/service/knowledge.go` 的 `buildSplitterConfig` 传入;
|
||||
4. 用 `SplitWithDiagnostics` 写单测验证 tier 选择与 `ValidateChunks` 验收行为。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新增检索引擎(Retriever Engine)
|
||||
|
||||
### 接口定义
|
||||
|
||||
接口在 `internal/types/interfaces/retriever.go`(三层:引擎 → 仓储 → 服务 + 注册表):
|
||||
|
||||
```go
|
||||
// internal/types/interfaces/retriever.go
|
||||
type RetrieveEngine interface {
|
||||
EngineType() types.RetrieverEngineType
|
||||
Retrieve(ctx context.Context, params types.RetrieveParams) ([]*types.RetrieveResult, error)
|
||||
Support() []types.RetrieverType // 支持的检索类型(向量/关键词)
|
||||
}
|
||||
|
||||
type RetrieveEngineRepository interface {
|
||||
Save(ctx context.Context, indexInfo *types.IndexInfo, params map[string]any) error
|
||||
BatchSave(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) error
|
||||
EstimateStorageSize(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) int64
|
||||
DeleteByChunkIDList(ctx context.Context, indexIDList []string, dimension int, knowledgeType string) error
|
||||
DeleteBySourceIDList(ctx context.Context, sourceIDList []string, dimension int, knowledgeType string) error
|
||||
CopyIndices(ctx context.Context, sourceKnowledgeBaseID string,
|
||||
sourceToTargetKBIDMap map[string]string,
|
||||
sourceToTargetChunkIDMap map[string]string,
|
||||
targetKnowledgeBaseID string, dimension int, knowledgeType string) error
|
||||
DeleteByKnowledgeIDList(ctx context.Context, knowledgeIDList []string, dimension int, knowledgeType string) error
|
||||
BatchUpdateChunkEnabledStatus(ctx context.Context, chunkStatusMap map[string]bool) error
|
||||
BatchUpdateChunkTagID(ctx context.Context, chunkTagMap map[string]string) error
|
||||
RetrieveEngine
|
||||
}
|
||||
|
||||
type RetrieveEngineRegistry interface {
|
||||
Register(indexService RetrieveEngineService) error
|
||||
GetRetrieveEngineService(engineType types.RetrieverEngineType) (RetrieveEngineService, error)
|
||||
GetAllRetrieveEngineServices() []RetrieveEngineService
|
||||
GetByStoreID(storeID string) (RetrieveEngineService, error)
|
||||
}
|
||||
```
|
||||
|
||||
引擎类型枚举在 `internal/types/retriever.go`:
|
||||
|
||||
```go
|
||||
// internal/types/retriever.go
|
||||
const (
|
||||
PostgresRetrieverEngineType RetrieverEngineType = "postgres"
|
||||
ElasticsearchRetrieverEngineType RetrieverEngineType = "elasticsearch"
|
||||
InfinityRetrieverEngineType RetrieverEngineType = "infinity"
|
||||
ElasticFaissRetrieverEngineType RetrieverEngineType = "elasticfaiss"
|
||||
QdrantRetrieverEngineType RetrieverEngineType = "qdrant"
|
||||
MilvusRetrieverEngineType RetrieverEngineType = "milvus"
|
||||
WeaviateRetrieverEngineType RetrieverEngineType = "weaviate"
|
||||
DorisRetrieverEngineType RetrieverEngineType = "doris"
|
||||
SQLiteRetrieverEngineType RetrieverEngineType = "sqlite"
|
||||
TencentVectorDBRetrieverEngineType RetrieverEngineType = "tencent_vectordb"
|
||||
OpenSearchRetrieverEngineType RetrieverEngineType = "opensearch"
|
||||
)
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
均在 `internal/application/repository/retriever/` 下:`postgres/`(pgvector + BM25/ParadeDB)、`elasticsearch/v7/`、`elasticsearch/v8/`、`qdrant/`、`milvus/`、`weaviate/`、`doris/`、`sqlite/`(sqlite-vec + FTS5)、`tencentvectordb/`、`opensearch/`。
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/types/retriever.go` 增加 `RetrieverEngineType` 常量;
|
||||
2. 在 `internal/application/repository/retriever/myengine/` 新建包,实现 `RetrieveEngineRepository` 接口(可参考 `qdrant/` 或 `sqlite/`);
|
||||
3. **注册点:`internal/container/container.go` 的 `initRetrieveEngineRegistry()`** — 按 `RETRIEVE_DRIVER` 环境变量(逗号分隔)条件注册:
|
||||
|
||||
```go
|
||||
// internal/container/container.go(节选)
|
||||
retrieveDriver := strings.Split(os.Getenv("RETRIEVE_DRIVER"), ",")
|
||||
if slices.Contains(retrieveDriver, "postgres") {
|
||||
postgresRepo := postgresRepo.NewPostgresRetrieveEngineRepository(db)
|
||||
if err := registry.Register(
|
||||
retriever.NewKVHybridRetrieveEngine(postgresRepo, types.PostgresRetrieverEngineType),
|
||||
); err != nil { ... }
|
||||
}
|
||||
```
|
||||
|
||||
仿照上例为新引擎加分支,用 `retriever.NewKVHybridRetrieveEngine(repo, 引擎类型)` 包装后注册;
|
||||
4. 若引擎需要独立部署,在 `docker-compose.dev.yml` 加一个带 profile 的服务(参考 `qdrant`/`opensearch`),并在 `.env.example` 补连接变量。
|
||||
|
||||
---
|
||||
|
||||
## 4. 新增模型 Provider(internal/models/provider)
|
||||
|
||||
### 接口定义
|
||||
|
||||
Provider 元数据接口 + 全局注册表在 `internal/models/provider/provider.go`:
|
||||
|
||||
```go
|
||||
// internal/models/provider/provider.go
|
||||
type ProviderName string // "openai" / "anthropic" / "aliyun" / "zhipu" / "deepseek" / ...
|
||||
|
||||
type Provider interface {
|
||||
// Info 返回服务商的元数据
|
||||
Info() ProviderInfo
|
||||
// ValidateConfig 验证服务商的配置
|
||||
ValidateConfig(config *Config) error
|
||||
}
|
||||
|
||||
// Register 添加一个提供者到全局注册表
|
||||
func Register(p Provider)
|
||||
```
|
||||
|
||||
`ProviderInfo` 描述展示名、各模型类型(chat/embedding/rerank)的默认 BaseURL、是否需要鉴权、额外配置字段等。Chat 请求的差异化适配(endpoint 拼接、thinking 参数、鉴权头、工具调用元数据)由 `internal/models/chat/provider.go` 的内部适配器接口承担:
|
||||
|
||||
```go
|
||||
// internal/models/chat/provider.go
|
||||
type providerAdapter interface {
|
||||
Name() provider.ProviderName
|
||||
Matches(model string) bool
|
||||
Thinking() ThinkingStrategy
|
||||
ShapeRequest(req *openai.ChatCompletionRequest, opts *ChatOptions, isStream bool)
|
||||
TransformMessages(msgs []openai.ChatCompletionMessage) []openai.ChatCompletionMessage
|
||||
Endpoint(baseURL, modelID string, isStream bool) string
|
||||
Auth(req *http.Request, creds authCreds, body []byte)
|
||||
ForceRawHTTP() bool
|
||||
ExtractToolCallMetadata(raw json.RawMessage) types.ToolCallMetadata
|
||||
InjectToolCallMetadata(toolCall map[string]any, metadata types.ToolCallMetadata)
|
||||
}
|
||||
```
|
||||
|
||||
Embedding 与 Rerank 各自有独立接口:
|
||||
|
||||
```go
|
||||
// internal/models/embedding/embedder.go
|
||||
type Embedder interface {
|
||||
Embed(ctx context.Context, text string) ([]float32, error)
|
||||
BatchEmbed(ctx context.Context, texts []string) ([][]float32, error)
|
||||
GetModelName() string
|
||||
GetDimensions() int
|
||||
GetModelID() string
|
||||
EmbedderPooler
|
||||
}
|
||||
|
||||
// internal/models/rerank/reranker.go
|
||||
type Reranker interface {
|
||||
Rerank(ctx context.Context, query string, documents []string) ([]RankResult, error)
|
||||
GetModelName() string
|
||||
GetModelID() string
|
||||
}
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
`internal/models/provider/provider.go` 中已定义 27 个 `ProviderName` 常量:openai、anthropic、aliyun、zhipu、openrouter、litellm、requesty、siliconflow、jina、generic、deepseek、gemini、volcengine、hunyuan、minimax、mimo、gpustack、moonshot、modelscope、qianfan、qiniu、longcat、lkeap、nvidia 等。具体 Provider 实现分布在 `internal/models/provider/` 下的各文件(如 `zhipu.go`、`gemini.go`、`hunyuan.go`、`generic.go`);特殊 embedding 实现如 `internal/models/embedding/jina.go`、`volcengine.go`、`nvidia.go`。
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. **注册点一:`internal/models/provider/provider.go`** — 增加 `ProviderName` 常量;
|
||||
2. 在 `internal/models/provider/` 新建 `myprovider.go`,实现 `Provider` 接口(`Info()` 给出默认 URL/支持的模型类型),并通过 `provider.Register(...)`(通常在 `init()` 或集中初始化处)挂入全局注册表——OpenAI 兼容协议的服务商到这一步即可用,chat 侧默认走通用 OpenAI 适配;
|
||||
3. **注册点二(可选):`internal/models/chat/provider.go`** — 若 API 协议有差异(非标 endpoint、特殊鉴权、thinking 字段),实现并注册一个 `providerAdapter`;
|
||||
4. **注册点三(可选)**:需要专有 Embedding/Rerank 协议时,在 `internal/models/embedding/`、`internal/models/rerank/` 各加实现并接入其构造工厂;
|
||||
5. 如需开箱即用的内置模型,补充 `config/builtin_models.yaml` 声明(启动时会同步进 `models` 表)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 新增联网搜索引擎(internal/infrastructure/web_search)
|
||||
|
||||
### 接口定义
|
||||
|
||||
```go
|
||||
// internal/types/interfaces/web_search.go
|
||||
type WebSearchProvider interface {
|
||||
// Name returns the name of the provider
|
||||
Name() string
|
||||
// Search performs a web search
|
||||
Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error)
|
||||
}
|
||||
```
|
||||
|
||||
注册表是工厂映射(按需用租户参数实例化):
|
||||
|
||||
```go
|
||||
// internal/infrastructure/web_search/registry.go
|
||||
type ProviderFactory func(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)
|
||||
|
||||
type Registry struct {
|
||||
factories map[string]ProviderFactory
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *Registry) Register(id string, factory ProviderFactory)
|
||||
func (r *Registry) CreateProvider(providerType string, params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
`internal/infrastructure/web_search/` 目录:`duckduckgo.go`、`google.go`、`bing.go`、`tavily.go`、`ollama.go`、`baidu.go`、`searxng.go`、`keenable.go`、`zhipu.go`(另有 `proxy.go` 出站代理支持)。类型常量在 `internal/types/web_search_provider.go`(`WebSearchProviderTypeBing/Google/DuckDuckGo/Tavily/Ollama/Baidu/Searxng/Keenable/Zhipu`)。
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/types/web_search_provider.go` 增加 `WebSearchProviderType` 常量;
|
||||
2. 在 `internal/infrastructure/web_search/` 新建 `mysearch.go`,实现 `WebSearchProvider` 并暴露工厂 `func NewMySearchProvider(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)`;
|
||||
3. **注册点:`internal/container/container.go` 的 `registerWebSearchProviders()`**:
|
||||
|
||||
```go
|
||||
func registerWebSearchProviders(registry *infra_web_search.Registry) {
|
||||
registry.Register("duckduckgo", infra_web_search.NewDuckDuckGoProvider)
|
||||
registry.Register("google", infra_web_search.NewGoogleProvider)
|
||||
// ... 在此追加:
|
||||
registry.Register("mysearch", infra_web_search.NewMySearchProvider)
|
||||
}
|
||||
```
|
||||
|
||||
4. 前端的 provider 下拉与参数表单如需展示新引擎,同步 `frontend/` 相应配置页组件;租户配置持久化在 `web_search_providers` 表。
|
||||
|
||||
---
|
||||
|
||||
## 6. 新增数据源连接器(internal/datasource/connector)
|
||||
|
||||
> 目录内附有实现指南 `internal/datasource/CONNECTOR_IMPLEMENTATION_GUIDE.md`,可对照阅读。
|
||||
|
||||
### 接口定义
|
||||
|
||||
```go
|
||||
// internal/datasource/connector.go
|
||||
type Connector interface {
|
||||
// Type returns the connector type identifier (e.g., "feishu", "notion")
|
||||
Type() string
|
||||
|
||||
// Validate verifies that the provided configuration is valid by testing
|
||||
// connectivity and checking credentials.
|
||||
Validate(ctx context.Context, config *types.DataSourceConfig) error
|
||||
|
||||
// ListResources lists available resources that can be synced.
|
||||
// parentID 支持层级资源的懒加载:"" 返回顶层,非空返回该资源的直接子节点。
|
||||
ListResources(ctx context.Context, config *types.DataSourceConfig, parentID string) ([]types.Resource, error)
|
||||
|
||||
// ResolveResourceAncestors 为懒加载树的既有选中项解析祖先链(O(depth))。
|
||||
ResolveResourceAncestors(
|
||||
ctx context.Context, config *types.DataSourceConfig, resourceIDs []string,
|
||||
) ([]string, error)
|
||||
|
||||
// FetchAll performs a full sync of the specified resources.
|
||||
FetchAll(ctx context.Context, config *types.DataSourceConfig, resourceIDs []string) ([]types.FetchedItem, error)
|
||||
|
||||
// FetchIncremental performs an incremental sync based on the provided cursor.
|
||||
FetchIncremental(ctx context.Context, config *types.DataSourceConfig, cursor *types.SyncCursor) ([]types.FetchedItem, *types.SyncCursor, error)
|
||||
}
|
||||
```
|
||||
|
||||
可选的流式接口(大数据量分页 checkpoint,内存只驻留单条 item):
|
||||
|
||||
```go
|
||||
// internal/datasource/connector.go
|
||||
type StreamHandler interface {
|
||||
Emit(ctx context.Context, item types.FetchedItem) error
|
||||
Checkpoint(ctx context.Context, cursor *types.SyncCursor) error
|
||||
}
|
||||
|
||||
type StreamingConnector interface {
|
||||
Connector
|
||||
FetchStream(ctx context.Context, config *types.DataSourceConfig,
|
||||
cursor *types.SyncCursor, h StreamHandler) (*types.SyncCursor, error)
|
||||
}
|
||||
```
|
||||
|
||||
注册表同文件:`ConnectorRegistry`(`NewConnectorRegistry()` / `Register(connector)` / `Get(type)` / `List()`);连接器的 UI 元数据(名称、AuthType、capabilities)在同文件的 `ConnectorMetadataRegistry` map 中。
|
||||
|
||||
### 现有实现
|
||||
|
||||
| 类型 | 目录 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `feishu` / `lark` | `internal/datasource/connector/feishu/` | 同一实现,`NewConnector(RegionFeishu / RegionLark)` 区分区域 |
|
||||
| `notion` | `internal/datasource/connector/notion/` | 页面与数据库 |
|
||||
| `yuque` | `internal/datasource/connector/yuque/` | 语雀 |
|
||||
| `rss` | `internal/datasource/connector/rss/` | RSS 订阅 |
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/datasource/connector/mysource/` 新建包,实现 `Connector`(大数据量建议同时实现 `StreamingConnector`),提供 `NewConnector()`;
|
||||
2. **注册点一:`internal/container/container.go` 的 `initConnectorRegistry()`**:
|
||||
|
||||
```go
|
||||
if err := registry.Register(mysourceConnector.NewConnector()); err != nil {
|
||||
errs = errors.Join(errs, fmt.Errorf("register mysource connector: %w", err))
|
||||
}
|
||||
```
|
||||
|
||||
3. **注册点二:`internal/datasource/connector.go` 的 `ConnectorMetadataRegistry`** — 增加类型常量(`internal/types` 的 `ConnectorTypeXxx`)与元数据条目(Name/Description/AuthType/Capabilities);
|
||||
4. 同步配置结构:`types.DataSourceConfig` 若需新增凭证字段,注意加密存储约定;前端数据源接入页按元数据渲染。
|
||||
|
||||
---
|
||||
|
||||
## 7. 新增 IM 平台适配器(internal/im)
|
||||
|
||||
### 接口定义
|
||||
|
||||
```go
|
||||
// internal/im/adapter.go
|
||||
type Platform string // "wecom" / "feishu" / "lark" / "slack" / "telegram" / "dingtalk" /
|
||||
// "mattermost" / "wechat" / "qqbot" / "yunzhijia"
|
||||
|
||||
// Adapter is the interface every IM platform must implement.
|
||||
type Adapter interface {
|
||||
// Platform returns the platform identifier.
|
||||
Platform() Platform
|
||||
|
||||
// VerifyCallback verifies the signature/token of an incoming callback request.
|
||||
VerifyCallback(c *gin.Context) error
|
||||
|
||||
// ParseCallback parses the raw IM callback request into a unified IncomingMessage.
|
||||
// Returns nil message for non-message events (e.g., URL verification).
|
||||
ParseCallback(c *gin.Context) (*IncomingMessage, error)
|
||||
|
||||
// SendReply sends a reply back to the IM platform.
|
||||
SendReply(ctx context.Context, incoming *IncomingMessage, reply *ReplyMessage) error
|
||||
|
||||
// HandleURLVerification handles the initial URL verification challenge.
|
||||
HandleURLVerification(c *gin.Context) bool
|
||||
}
|
||||
```
|
||||
|
||||
两个可选能力接口:
|
||||
|
||||
```go
|
||||
// internal/im/adapter.go
|
||||
// StreamSender:实现后 IM 服务将实时推送流式回答(如飞书流式卡片、Telegram 编辑消息)
|
||||
type StreamSender interface {
|
||||
StartStream(ctx context.Context, incoming *IncomingMessage) (string, error)
|
||||
UpdateStreamContent(ctx context.Context, incoming *IncomingMessage, streamID string, fullContent string) error
|
||||
FinalizeStream(ctx context.Context, incoming *IncomingMessage, streamID string, finalContent string) error
|
||||
EndStream(ctx context.Context, incoming *IncomingMessage, streamID string) error
|
||||
}
|
||||
|
||||
// FileDownloader:实现后,配置了 knowledge_base_id 的渠道会把文件消息入库
|
||||
type FileDownloader interface {
|
||||
DownloadFile(ctx context.Context, msg *IncomingMessage) (io.ReadCloser, string, error)
|
||||
}
|
||||
```
|
||||
|
||||
适配器由工厂按渠道实例化(`internal/im/service.go`):
|
||||
|
||||
```go
|
||||
// internal/im/service.go
|
||||
type AdapterFactory func(ctx context.Context, channel *IMChannel,
|
||||
msgHandler func(ctx context.Context, msg *IncomingMessage) error,
|
||||
) (Adapter, context.CancelFunc, error)
|
||||
|
||||
func (s *Service) RegisterAdapterFactory(platform string, factory AdapterFactory)
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
`internal/im/` 下每个平台一个子包:`wecom/`、`feishu/`(lark 复用,`feishu.NewFactory(RegionLark)`)、`slack/`、`telegram/`、`dingtalk/`、`mattermost/`、`wechat/`、`qqbot/`、`yunzhijia/`。
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/im/adapter.go` 增加 `Platform` 常量;
|
||||
2. 新建 `internal/im/myplatform/`,实现 `Adapter`(按需加 `StreamSender`/`FileDownloader`)与 `NewFactory() im.AdapterFactory`;
|
||||
3. **注册点:`internal/container/container.go` 的 `registerIMAdapterFactories()`**:
|
||||
|
||||
```go
|
||||
func registerIMAdapterFactories(imService *imPkg.Service) {
|
||||
imService.RegisterAdapterFactory("wecom", wecom.NewFactory())
|
||||
// ... 在此追加:
|
||||
imService.RegisterAdapterFactory("myplatform", myplatform.NewFactory())
|
||||
if err := imService.LoadAndStartChannels(); err != nil { ... }
|
||||
}
|
||||
```
|
||||
|
||||
4. 渠道配置持久化在 `im_channels` 表,会话映射在 `im_channel_sessions`;前端渠道管理页需增加对应平台的配置表单。
|
||||
|
||||
---
|
||||
|
||||
## 8. 新增 Agent 工具(internal/agent/tools)
|
||||
|
||||
### 接口定义
|
||||
|
||||
工具接口定义在 `internal/types/agent.go`:
|
||||
|
||||
```go
|
||||
// internal/types/agent.go
|
||||
type Tool interface {
|
||||
// Name returns the unique identifier for this tool
|
||||
Name() string
|
||||
|
||||
// Description returns a human-readable description of what the tool does
|
||||
Description() string
|
||||
|
||||
// Parameters returns the JSON Schema for the tool's parameters
|
||||
Parameters() json.RawMessage
|
||||
|
||||
// Execute runs the tool with the given arguments
|
||||
Execute(ctx context.Context, args json.RawMessage) (*ToolResult, error)
|
||||
}
|
||||
```
|
||||
|
||||
运行时注册表在 `internal/agent/tools/registry.go`:
|
||||
|
||||
```go
|
||||
// internal/agent/tools/registry.go
|
||||
type ToolRegistry struct {
|
||||
tools map[string]types.Tool
|
||||
maxToolOutputSize int
|
||||
}
|
||||
|
||||
// RegisterTool adds a tool to the registry.
|
||||
// 同名工具 first-wins,防止名称碰撞劫持(GHSA-67q9-58vj-32qx)。
|
||||
func (r *ToolRegistry) RegisterTool(tool types.Tool)
|
||||
func (r *ToolRegistry) GetTool(name string) (types.Tool, error)
|
||||
func (r *ToolRegistry) ListTools() []string
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
工具名常量集中在 `internal/agent/tools/definitions.go`:`thinking`、`todo_write`、`grep_chunks`、`knowledge_search`、`list_knowledge_chunks`、`query_knowledge_graph`、`get_document_info`、`database_query`、`data_analysis`、`data_schema`、`web_search`、`web_fetch`、skills 工具(`execute_skill_script`、`read_skill`)、wiki 工具(`wiki_read_page`、`wiki_write_page`、`wiki_replace_text`、`wiki_rename_page`、`wiki_delete_page`、`wiki_search`、`wiki_read_source_doc`、`wiki_flag_issue`、`wiki_read_issue`、`wiki_update_issue`)。实现文件与工具同名(如 `grep_chunks.go`、`knowledge_search.go`、`data_analysis.go`、`mcp_tool.go`——后者把 MCP 服务的远程工具包装成 `types.Tool`)。
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/agent/tools/` 新建 `my_tool.go`,实现 `types.Tool` 四个方法(`Parameters()` 返回 JSON Schema;注意工具名 ≤ 64 字符的 OpenAI 限制,见 `definitions.go` 的 `maxFunctionNameLength`);
|
||||
2. **注册点一:`internal/agent/tools/definitions.go`** — 增加 `ToolMyTool = "my_tool"` 常量,并把工具加进 `AvailableToolDefinitions()`(UI 的可选工具列表,注释明确要求与已注册工具保持同步);
|
||||
3. **注册点二:Agent 引擎的工具装配处** — 在构建 `ToolRegistry` 的服务逻辑(Agent 会话初始化,按 Agent 配置的允许工具列表实例化并 `RegisterTool`)中加入新工具的构造;带资源清理需求时实现 `Cleanup`(`types.Cleanable`);
|
||||
4. 输出体量大的工具注意 `ToolRegistry` 的 `maxToolOutputSize` 截断行为;为工具编写 `_test.go`(同目录有大量参考,如 `grep_chunks_scope_test.go`)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 新增存储后端(对象存储)
|
||||
|
||||
### 接口定义
|
||||
|
||||
文件服务接口在 `internal/types/interfaces/file.go`:
|
||||
|
||||
```go
|
||||
// internal/types/interfaces/file.go
|
||||
type FileService interface {
|
||||
CheckConnectivity(ctx context.Context) error
|
||||
SaveFile(ctx context.Context, file *multipart.FileHeader, tenantID uint64, knowledgeID string) (string, error)
|
||||
SaveBytes(ctx context.Context, data []byte, tenantID uint64, fileName string, temp bool) (string, error)
|
||||
GetFile(ctx context.Context, filePath string) (io.ReadCloser, error)
|
||||
GetFileURL(ctx context.Context, filePath string) (string, error)
|
||||
DeleteFile(ctx context.Context, filePath string) error
|
||||
CopyFile(ctx context.Context, srcPath string, tenantID uint64, knowledgeID string) (string, error)
|
||||
}
|
||||
```
|
||||
|
||||
多后端解析(租户级 `storage_backends` 表配置 → FileService 实例)经 `internal/types/interfaces/storagebackend.go`:
|
||||
|
||||
```go
|
||||
// internal/types/interfaces/storagebackend.go
|
||||
type StorageBackendService interface {
|
||||
Create(ctx context.Context, backend *types.StorageBackend) error
|
||||
Update(ctx context.Context, backend *types.StorageBackend) error
|
||||
Delete(ctx context.Context, tenantID uint64, id string) error
|
||||
SetDefault(ctx context.Context, tenantID uint64, id string) error
|
||||
Test(ctx context.Context, backend *types.StorageBackend) error
|
||||
}
|
||||
|
||||
type StorageBackendResolver interface {
|
||||
ResolveFileService(ctx context.Context, tenant *types.Tenant, backendID, provider, localBaseDir string) (FileService, string, error)
|
||||
ResolveBackend(ctx context.Context, tenant *types.Tenant, backendID, provider string) (*types.StorageBackend, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 现有实现
|
||||
|
||||
均在 `internal/application/service/file/`:
|
||||
|
||||
| provider | 文件 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `local` | `local.go` | 本地文件系统 |
|
||||
| `minio` | `minio.go` | MinIO / S3 兼容 |
|
||||
| `cos` | `cos.go` | 腾讯云 COS |
|
||||
| `tos` | `tos.go` | 火山引擎 TOS |
|
||||
| `s3` | `s3.go` | AWS S3 及兼容服务 |
|
||||
| `obs` | `obs.go` | 华为云 OBS |
|
||||
| `oss` | `oss.go` | 阿里云 OSS |
|
||||
| `ks3` | `ks3.go` | 金山云 KS3 |
|
||||
|
||||
### 新增步骤
|
||||
|
||||
1. 在 `internal/application/service/file/` 新建 `mystore.go`,实现 `FileService` 全部方法(`CheckConnectivity` 用于前端"测试连接"按钮,即 `StorageBackendService.Test`);
|
||||
2. **注册点:`internal/application/service/file/factory.go` 的 `NewFileServiceFromStorageConfig()`** — 在 provider switch 中加 case:
|
||||
|
||||
```go
|
||||
switch p {
|
||||
case "local": // NewLocalFileService(...)
|
||||
case "minio": // NewMinioFileService(...)
|
||||
// ... 在此追加:
|
||||
case "mystore":
|
||||
return NewMyStoreFileService(cfg), p, nil
|
||||
default:
|
||||
return nil, p, fmt.Errorf("unsupported storage provider: %s", p)
|
||||
}
|
||||
```
|
||||
|
||||
3. 若新 provider 需要新的配置字段(endpoint/bucket/region 等),扩展 `internal/types` 中的 `StorageEngineConfig` / `StorageBackend.config`(JSONB);
|
||||
4. 前端存储后端管理页增加对应 provider 的表单项;租户配置落在 `storage_backends` 表(`provider` 列即 switch 的 key)。
|
||||
|
||||
---
|
||||
|
||||
## 附:扩展点速查表
|
||||
|
||||
| 扩展点 | 核心接口 | 接口文件 | 注册点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文档解析器 | `BaseParser.parse_into_text` | `docreader/parser/base_parser.py` | `docreader/parser/registry.py` `_build_default_registry()` |
|
||||
| 分块策略 | tier 函数 `func(text, cfg, profile) []Chunk` | `internal/infrastructure/chunker/strategy.go` | 同文件 `runTier()` + 策略常量 |
|
||||
| 检索引擎 | `RetrieveEngineRepository` | `internal/types/interfaces/retriever.go` | `container.go` `initRetrieveEngineRegistry()`(`RETRIEVE_DRIVER` 门控) |
|
||||
| 模型 Provider | `Provider` / `providerAdapter` / `Embedder` / `Reranker` | `internal/models/provider/provider.go` 等 | `provider.Register()` + `internal/models/chat/provider.go` |
|
||||
| 联网搜索 | `WebSearchProvider` | `internal/types/interfaces/web_search.go` | `container.go` `registerWebSearchProviders()` |
|
||||
| 数据源连接器 | `Connector` / `StreamingConnector` | `internal/datasource/connector.go` | `container.go` `initConnectorRegistry()` + `ConnectorMetadataRegistry` |
|
||||
| IM 适配器 | `Adapter`(+`StreamSender`/`FileDownloader`) | `internal/im/adapter.go` | `container.go` `registerIMAdapterFactories()` |
|
||||
| Agent 工具 | `types.Tool` | `internal/types/agent.go` | `internal/agent/tools/definitions.go` + `ToolRegistry.RegisterTool` |
|
||||
| 存储后端 | `FileService` | `internal/types/interfaces/file.go` | `internal/application/service/file/factory.go` switch |
|
||||
Reference in New Issue
Block a user