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

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+297
View File
@@ -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 |