Files
xinmi/docs/数据源导入开发文档.md
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

961 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据源导入开发文档
WeKnora 的数据源导入模块支持从外部平台(飞书、企业微信、Notion、Confluence 等)自动导入和同步内容到知识库。用户可配置数据源连接,选择需要同步的资源,并通过手动触发或定时调度自动完成内容的增量/全量同步。
数据源绑定到知识库,一个知识库可接入多个数据源。所有配置通过前端知识库设置页面管理,凭证使用 AES-256-GCM 加密存储在数据库中。
## 目录
- [快速接入指南](#快速接入指南)
- [飞书知识库接入](#飞书知识库接入)
- [前端管理](#前端管理)
- [架构总览](#架构总览)
- [数据模型](#数据模型)
- [API 端点](#api-端点)
- [核心概念](#核心概念)
- [同步处理流程](#同步处理流程)
- [接口定义](#接口定义)
- [连接器详解](#连接器详解)
- [飞书 (Feishu)](#飞书-feishu)
- [定时调度](#定时调度)
- [关键参数与阈值](#关键参数与阈值)
- [错误处理](#错误处理)
- [扩展新连接器](#扩展新连接器)
---
## 快速接入指南
### 前置条件
- WeKnora 已部署并运行
- 已创建至少一个知识库
- 拥有外部平台(如飞书、企业微信)的管理权限,可创建应用并授权
### 飞书知识库接入
从飞书知识库(Wiki)同步文档到 WeKnora。支持 docx、doc、sheet、bitable、file 类型文档的自动导入。
#### 第一步:创建飞书应用
1. 登录 [飞书开放平台](https://open.feishu.cn/) → **开发者后台** → **创建企业自建应用**
2. 在 **凭证与基础信息** 页获取:
- **App ID**
- **App Secret**
#### 第二步:开通权限
在 **权限管理** 中搜索并开通以下权限:
| 权限 | 说明 |
|------|------|
| `wiki:wiki:readonly` | 读取知识库空间列表和节点树 |
| `drive:drive:readonly` | 读取云文档基础信息 |
| `drive:export:readonly` | 导出文档内容(docx/xlsx) |
| `docx:document:readonly` | 读取新版文档内容 |
> **docx 内嵌内容下钻权限(可选)**
>
> 若需同步 docx 文档中内嵌的附件和图片,还需额外开通以下权限(缺失时自动回退到导出 docx,不会同步失败,仅无法获取内嵌表格/附件):
>
> - `docx:document:readonly` — 读取文档块(上表已含,此处仅供核对)
> - `drive:drive:readonly` / `docs:document.media:download` — 下载附件、图片
> - `sheets:spreadsheet:readonly` — 读取内嵌电子表格
> - `bitable:app:readonly` — 读取内嵌多维表格
#### 第三步:发布应用
在 **版本管理与发布** 中创建版本并提交审核。审核通过后方可正常调用 API。
#### 第四步:在 WeKnora 中添加数据源
1. 进入知识库设置页 → **数据源** 标签页
2. 点击 **添加数据源**
3. 选择连接器类型:**飞书**
4. 填写凭证:
- **App ID**:填入从飞书获取的 App ID
- **App Secret**:填入从飞书获取的 App Secret
5. 点击 **测试连接**,验证凭证有效
6. 选择要同步的知识库空间(Wiki Space)
7. 配置同步策略:
- **同步模式**:增量同步(推荐)或全量同步
- **同步频率**:选择预设的 Cron 表达式(如每 30 分钟、每小时、每天凌晨 2 点)
- **冲突策略**:覆盖(推荐)或跳过
- **同步删除**:是否同步源端的删除操作
8. 点击保存
#### 第五步:验证
保存后可点击 **立即同步** 触发首次同步。在同步日志中查看同步进度和结果:
- **成功**:显示已创建/更新/跳过/失败的文档数量
- **失败**:显示错误信息,常见原因为权限不足或 Token 过期
---
### Lark 知识库接入
Lark 是飞书的国际版。Wiki / docx / drive 接口与飞书一致,**共用同一套连接器代码**,接入步骤
与[飞书知识库接入](#飞书知识库接入)相同,差别只有三处:
| | 飞书 | Lark |
|---|---|---|
| 开放平台 | <https://open.feishu.cn/> | <https://open.larksuite.com/> |
| 连接器类型 | 「飞书」 | 「Lark」 |
| API 地址 | `https://open.feishu.cn` | `https://open.larksuite.com`(选中 Lark 类型后自动使用) |
#### Lark 权限
权限标识与飞书相同,在 Lark 开放平台的 **权限管理** 中开通:
| 权限 | 说明 | Lark 官方文档核对 |
|------|------|------|
| `wiki:wiki:readonly` | 读取知识库空间列表和节点树 | ✅ 已核对 |
| `drive:export:readonly` | 导出文档内容(docx/xlsx) | ✅ 已核对 |
| `drive:drive:readonly` | 读取云文档基础信息、下载文件 | ✅ 已核对 |
| `docx:document:readonly` | 读取新版文档内容 | ⚠️ 未能核对(Lark 文档页渲染受限),沿用飞书侧标识 |
> **不要套用 [IM 集成文档](./IM集成开发文档.md#飞书接入)里的那份权限 JSON**:那份清单混入了
> `aily:file:*`、`corehr:file:download` 等在 Lark 并不存在的权限(整份导入会失败),
> 且反而**缺少**上表中数据源真正需要的 `drive:*` / `docx:*`。
>
> 若同一个 Lark 应用既做 IM 机器人又做知识库同步,请使用
> [IM 集成文档 — Lark 权限配置](./IM集成开发文档.md#lark-权限配置)中的合并清单。
#### 应用互不通用
在 open.feishu.cn 创建的应用无法读取 Lark 知识库,反之亦然——凭证只在创建它的那朵云上有效。
若你此前用「飞书」连接器加 `base_url=https://open.larksuite.com` 的方式接入过 Lark,该配置
仍然有效(`base_url` 保留为显式覆盖项),但新建数据源请直接选「Lark」类型。
---
## 前端管理
数据源在知识库设置页的 **数据源** 标签页中管理。
### 创建向导
添加数据源使用多步向导流程:
1. **选择类型**:从可用连接器列表中选择平台
2. **配置凭证**:填写平台凭证,支持即时「测试连接」验证
3. **选择资源**:浏览并勾选要同步的资源(如飞书知识库空间)
4. **同步策略**:配置 Cron 调度、同步模式、冲突策略等
### 数据源列表
每个数据源以卡片形式展示,包含:
- **连接器类型标识**:飞书(蓝色)等
- **数据源名称**:用户自定义
- **同步状态**:活跃 / 暂停 / 错误
- **最近同步时间**:上次成功同步的时间
- **最近同步结果**:已同步的文档数量摘要
- **操作按钮**:立即同步、暂停/恢复、编辑、删除
### 同步日志
每个数据源可查看历史同步日志,包含:
- 同步状态:运行中 / 成功 / 部分成功 / 失败 / 已取消
- 开始时间 / 结束时间
- 文档计数:总数、已创建、已更新、已删除、已跳过、失败
- 错误信息(如有)
---
## 架构总览
```
┌───────────────────────────────────────────────────────────────────────────┐
│ 数据源导入架构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 飞书 │ │ Notion │ │Confluence│ │ GitHub │ │ 其他... │ │
│ │ (Wiki) │ │ │ │ │ │ │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ API │ API │ API │ API │ │
│ ─────┼─────────────┼────────────┼─────────────┼─────────────┼────── │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Feishu │ │ Notion │ │Confluence│ │ GitHub │ │ ... │ │
│ │Connector │ │Connector │ │Connector │ │Connector │ │Connector │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │ 连接 │
│ ─────┴─────────────┴─────────────┴──────────────┴─────────────┘ 器层 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ ConnectorRegistry │ 连接器注册表 │
│ │ · 按类型查找连接器实例 │ │
│ │ · 连接器元数据(名称、认证方式、能力) │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ───────────────────┼──────────────────────────────────────────────── │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ DataSourceService (业务逻辑层) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ · 数据源 CRUD │ │ │
│ │ │ · 连接验证 (ValidateConnection) │ │ │
│ │ │ · 资源发现 (ListAvailableResources) │ │ │
│ │ │ · 手动同步 (ManualSync) │ │ │
│ │ │ · 暂停/恢复 │ │ │
│ │ │ · 同步执行 (ProcessSync) │ │ │
│ │ │ · 内容入库 (ingestItem) │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ───────────────────┼──────────────────────────────────────────────── │
│ ▼ │
│ ┌──────────────────────────────────┐ ┌────────────────────────────┐ │
│ │ Scheduler (Cron 调度器) │ │ Task Queue (asynq) │ │
│ │ · robfig/cron(含秒) │ │ · 异步同步任务 │ │
│ │ · DB 加载活跃数据源 │ │ · Redis / Lite 模式 │ │
│ │ · 任务去重 (TaskID + Running) │ │ · TypeDataSourceSync │ │
│ └──────────────────────────────────┘ └────────────────────────────┘ │
│ │ │
│ ───────────────────┼──────────────────────────────────────────────── │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ WeKnora Core (知识入库管道) │ │
│ │ KnowledgeService · KnowledgeBaseService │ │
│ │ 文档解析 → 分块 → 向量化 → 索引 │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
```
**设计模式:**
| 模式 | 用途 |
|------|------|
| Adapter Pattern | 统一不同平台的差异,每个平台实现 `Connector` 接口 |
| Registry Pattern | 通过 `ConnectorRegistry` 按类型动态查找连接器实例 |
| Strategy Pattern | `SyncMode`(增量/全量)、`ConflictStrategy`(覆盖/跳过)可选策略 |
| Producer-Consumer | 手动/定时触发 → asynq 任务队列 → Worker 异步执行同步 |
| Cursor-based Pagination | 增量同步基于 `SyncCursor` 状态跟踪变更 |
---
## 数据模型
### data_sources 表
数据源配置存储在 `data_sources` 表中,绑定到知识库:
```sql
CREATE TABLE data_sources (
id VARCHAR(36) PRIMARY KEY,
tenant_id BIGINT NOT NULL,
knowledge_base_id VARCHAR(36) NOT NULL,
name VARCHAR(255) NOT NULL DEFAULT '',
type VARCHAR(50) NOT NULL, -- 连接器类型
config JSONB NOT NULL DEFAULT '{}', -- 加密的凭证和配置
sync_schedule VARCHAR(100) DEFAULT '', -- Cron 表达式(6 段,含秒)
sync_mode VARCHAR(20) DEFAULT 'incremental', -- 'incremental' | 'full'
status VARCHAR(20) DEFAULT 'active', -- 'active' | 'paused' | 'error' | 'deleted'
conflict_strategy VARCHAR(20) DEFAULT 'overwrite', -- 'overwrite' | 'skip'
sync_deletions BOOLEAN DEFAULT true,
last_sync_at TIMESTAMPTZ,
last_sync_cursor JSONB,
last_sync_result JSONB,
error_message TEXT DEFAULT '',
sync_log_retention_days INTEGER DEFAULT 30,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ
);
```
**config 字段结构(解密后):**
```json
{
"type": "feishu",
"credentials": {
"app_id": "cli_xxx",
"app_secret": "xxx"
},
"resource_ids": ["space_id_1", "space_id_2"],
"settings": {}
}
```
**各连接器凭证字段:**
| 连接器类型 | 凭证字段 | 认证方式 |
|-----------|---------|---------|
| 飞书 (feishu) | `app_id`, `app_secret` | OAuth2 (Tenant Access Token) |
### sync_logs 表
同步日志记录每次同步操作的执行情况:
```sql
CREATE TABLE sync_logs (
id VARCHAR(36) PRIMARY KEY,
data_source_id VARCHAR(36) NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
tenant_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL, -- 'running' | 'success' | 'partial' | 'failed' | 'canceled'
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ,
items_total INTEGER DEFAULT 0,
items_created INTEGER DEFAULT 0,
items_updated INTEGER DEFAULT 0,
items_deleted INTEGER DEFAULT 0,
items_skipped INTEGER DEFAULT 0,
items_failed INTEGER DEFAULT 0,
error_message TEXT DEFAULT '',
result JSONB, -- 详细同步结果
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
---
## API 端点
所有端点位于 `/api/v1/datasource` 路径下,需认证。
### 连接器元数据
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/datasource/types` | 获取可用连接器类型列表 |
**响应示例:**
```json
[
{
"type": "feishu",
"name": "飞书",
"description": "Import documents from Feishu/Lark Wiki spaces",
"icon": "feishu",
"priority": 1,
"auth_type": "oauth2",
"capabilities": ["incremental", "deletion_sync"]
}
]
```
### 凭证验证(不落库)
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/datasource/validate-credentials` | 验证凭证(创建前的测试连接) |
**请求体:**
```json
{
"type": "feishu",
"credentials": {
"app_id": "cli_xxx",
"app_secret": "xxx"
}
}
```
### 数据源管理
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/datasource` | 创建数据源 |
| GET | `/api/v1/datasource?kb_id=xxx` | 列出知识库的所有数据源 |
| GET | `/api/v1/datasource/:id` | 获取数据源详情 |
| PUT | `/api/v1/datasource/:id` | 更新数据源配置 |
| DELETE | `/api/v1/datasource/:id` | 删除数据源(软删除) |
### 操作
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/datasource/:id/validate` | 测试已有数据源的连接(会更新状态) |
| GET | `/api/v1/datasource/:id/resources` | 浏览外部系统的可用资源 |
| POST | `/api/v1/datasource/:id/sync` | 手动触发同步 |
| POST | `/api/v1/datasource/:id/pause` | 暂停数据源(停止定时同步) |
| POST | `/api/v1/datasource/:id/resume` | 恢复数据源 |
### 同步日志
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/datasource/:id/logs?limit=10&offset=0` | 获取同步日志列表 |
| GET | `/api/v1/datasource/logs/:log_id` | 获取单条同步日志详情 |
---
## 核心概念
### DataSource — 数据源
每个数据源代表一个外部平台连接与 WeKnora 知识库的绑定关系。一个知识库可以添加多个数据源,实现从多个平台汇聚内容。
数据源的核心属性:
- **类型 (Type)**:绑定的连接器类型,如 `feishu`、`notion` 等
- **配置 (Config)**:加密存储的凭证、选中的资源 ID 列表和额外设置
- **同步模式 (SyncMode)**:`incremental`(增量同步,只同步变更)或 `full`(全量同步,每次拉取所有)
- **同步调度 (SyncSchedule)**:6 段 Cron 表达式(含秒),为空则仅支持手动触发
- **冲突策略 (ConflictStrategy)**:源端与本地均有变更时的处理策略
- **同步删除 (SyncDeletions)**:源端删除文档时是否同步删除本地知识
### Connector — 连接器
连接器是与外部平台交互的适配层,每个平台实现一个连接器。所有连接器通过 `ConnectorRegistry` 注册和管理。
连接器提供四个核心能力:
| 能力 | 说明 |
|------|------|
| `Validate` | 验证凭证和连接是否可用 |
| `ListResources` | 列出外部系统中可选的资源(如知识库空间、文件夹) |
| `FetchAll` | 全量拉取指定资源中的所有文档 |
| `FetchIncremental` | 基于游标增量拉取变更的文档 |
### SyncCursor — 同步游标
增量同步的状态标记,记录上次同步的位点。游标内容因连接器类型而异,由连接器自行管理。如飞书连接器的游标包含每个节点的最后编辑时间,用于判断哪些文档发生了变更。
```go
type SyncCursor struct {
LastSyncTime *time.Time // 上次同步时间
ConnectorCursor map[string]interface{} // 连接器自定义状态
LastSchemaHash string // Schema 变更检测
}
```
### FetchedItem — 拉取到的文档条目
连接器从外部平台拉取文档后,统一封装为 `FetchedItem`:
```go
type FetchedItem struct {
ExternalID string // 外部平台的文档唯一标识
Title string // 文档标题
Content []byte // 文档内容(二进制)
ContentType string // MIME 类型
FileName string // 文件名(含扩展名)
URL string // 原始 URL
UpdatedAt *time.Time // 最后更新时间
Metadata map[string]string // 元数据(来源、作者等)
IsDeleted bool // 标记为已删除(增量同步)
SourceResourceID string // 所属资源 ID
}
```
### Resource — 可选资源
外部系统中可选择同步的资源节点,支持层级结构:
```go
type Resource struct {
ExternalID string // 外部 ID
Name string // 名称
Type string // 类型(如 wiki_space)
Description string // 描述
URL string // 外部链接
ModifiedAt *time.Time // 最后修改时间
ParentID string // 父节点 ID(支持树形结构)
Metadata map[string]string // 附加信息
}
```
---
## 同步处理流程
### 完整同步流程
```
手动触发 / Cron 定时触发
│
▼
┌─ 创建 SyncLog (status: running) ──────────────────┐
│ 入队异步任务 (asynq: TypeDataSourceSync) │
│ · 手动同步 → 队列: default │
│ · 定时同步 → 队列: low, TaskID 去重 │
└──────────────────────────┬────────────────────────┘
│
▼
┌─ ProcessSync (Worker 异步执行) ───────────────────┐
│ 1. 解析任务体 (DataSourceSyncPayload) │
│ 2. 加载数据源配置 │
│ 3. 解密凭证 → 获取 Connector │
│ 4. 设置空间上下文 │
│ 5. 创建/获取自动标签 (连接器名·数据源名) │
│ 6. 判断同步模式: │
│ ├─ ForceFull 或 SyncModeFull → FetchAll │
│ └─ SyncModeIncremental → FetchIncremental │
│ 7. 遍历 FetchedItem 列表: │
│ └─ ingestItem → KnowledgeService │
│ 8. 更新 SyncLog (计数、状态、耗时) │
│ 9. 更新 DataSource (游标、最后同步时间、结果) │
└──────────────────────────────────────────────────┘
```
### ingestItem — 单文档入库
```
FetchedItem
│
├─ IsDeleted = true?
│ └─ 按 external_id 查找已有知识 → 软删除
│
├─ 有文件内容 (Content)?
│ └─ CreateKnowledgeFromFile
│ · 按 external_id 查找已有知识
│ · 已存在 → 删除旧版本 → 重建
│ · 不存在 → 新建
│ · metadata: external_id, source_resource_id, datasource_id
│ · channel: 连接器类型 (如 "feishu")
│ · 自动关联标签
│
└─ 仅有 URL (无 Content)?
└─ CreateKnowledgeFromURL
· 同上查重逻辑
```
### 数据源生命周期
```
创建数据源 (前端向导)
│
▼
┌─ DataSourceService.Create ────────────────────┐
│ 1. 验证知识库存在 │
│ 2. 获取对应 Connector │
│ 3. 验证配置和凭证 (connector.Validate) │
│ 4. 加密凭证写库 │
│ 5. 若有 SyncSchedule → 注册 Cron 任务 │
└───────────────────────────────────────────────┘
暂停数据源:
status → paused, 移除 Cron 任务
恢复数据源:
status → active, 重新注册 Cron 任务
删除数据源:
deleted_at = NOW(), 移除 Cron 任务, 级联删除 sync_logs
连接错误:
ValidateConnection 失败 → status → error, 记录 error_message
ValidateConnection 成功且原为 error → status → active, 清空 error_message
```
---
## 接口定义
### Connector — 连接器接口(必须实现)
```go
type Connector interface {
Type() string
Validate(ctx context.Context, config *types.DataSourceConfig) error
ListResources(ctx context.Context, config *types.DataSourceConfig) ([]types.Resource, error)
FetchAll(ctx context.Context, config *types.DataSourceConfig, resourceIDs []string) ([]types.FetchedItem, error)
FetchIncremental(ctx context.Context, config *types.DataSourceConfig, cursor *types.SyncCursor) ([]types.FetchedItem, *types.SyncCursor, error)
}
```
| 方法 | 职责 |
|------|------|
| `Type()` | 返回连接器类型标识,用于注册和路由 |
| `Validate()` | 验证凭证和连接可用性,如尝试获取 Access Token |
| `ListResources()` | 列出外部系统中可选的顶层资源(如知识库空间、工作区) |
| `FetchAll()` | 全量拉取指定资源下的所有文档,返回 `FetchedItem` 列表 |
| `FetchIncremental()` | 基于上次游标增量拉取变更,返回变更列表和新游标 |
### DataSourceService — 业务服务接口
```go
type DataSourceService interface {
CreateDataSource(ctx context.Context, ds *types.DataSource) (*types.DataSource, error)
GetDataSource(ctx context.Context, id string) (*types.DataSource, error)
ListDataSources(ctx context.Context, knowledgeBaseID string) ([]*types.DataSource, error)
UpdateDataSource(ctx context.Context, ds *types.DataSource) (*types.DataSource, error)
DeleteDataSource(ctx context.Context, id string) error
ValidateConnection(ctx context.Context, dataSourceID string) error
ValidateCredentials(ctx context.Context, connectorType string, credentials map[string]interface{}) error
ListAvailableResources(ctx context.Context, dataSourceID string) ([]types.Resource, error)
ManualSync(ctx context.Context, dataSourceID string) (*types.SyncLog, error)
PauseDataSource(ctx context.Context, dataSourceID string) error
ResumeDataSource(ctx context.Context, dataSourceID string) error
GetSyncLogs(ctx context.Context, dataSourceID string, limit, offset int) ([]*types.SyncLog, error)
GetSyncLog(ctx context.Context, syncLogID string) (*types.SyncLog, error)
ProcessSync(ctx context.Context, task *asynq.Task) error
}
```
### ConnectorMetadata — 连接器元数据
```go
type ConnectorMetadata struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Priority int `json:"priority"`
AuthType string `json:"auth_type"`
Capabilities []string `json:"capabilities"`
}
```
| 字段 | 说明 |
|------|------|
| `Type` | 连接器类型标识(如 `feishu`) |
| `Name` | 显示名称(如 `飞书`) |
| `Priority` | 排序优先级,数值越小越靠前 |
| `AuthType` | 认证方式:`oauth2` / `api_key` / `token` / `password` / `none` |
| `Capabilities` | 支持的能力:`incremental`(增量同步)、`webhook`、`deletion_sync`(删除同步) |
---
## 连接器详解
### 飞书 (Feishu)
飞书连接器支持从飞书知识库(Wiki)同步文档到 WeKnora。
#### 认证机制
```
App ID + App Secret
│
▼
POST /open-apis/auth/v3/tenant_access_token/internal
│
▼
获取 Tenant Access Token(有效期 2 小时)
│
▼
Token 缓存,提前 5 分钟刷新
```
- **认证类型**:企业自建应用,Tenant Access Token
- **Token 有效期**:2 小时,内置缓存,提前 5 分钟自动刷新
- **API 地址**:默认 `https://open.feishu.cn`,国际版 Lark 使用 `https://open.larksuite.com`
#### 资源发现
`ListResources` 调用飞书 Wiki Space API 列出所有知识库空间:
```
GET /open-apis/wiki/v2/spaces (分页, page_size=50)
```
每个知识库空间映射为一个 `Resource`:
| 字段 | 值 |
|------|------|
| `Type` | `wiki_space` |
| `ExternalID` | 空间 ID (space_id) |
| `URL` | `https://feishu.cn/wiki/{space_id}` |
| `Metadata.visibility` | 空间可见性 |
#### 全量同步 (FetchAll)
```
对每个选中的 Wiki Space:
1. 递归列举空间下所有节点 (ListAllWikiNodesRecursive)
└─ GET /open-apis/wiki/v2/spaces/{space_id}/nodes (parent 为空 → 顶层)
└─ 对 has_child=true 的节点递归
2. 对每个节点:
└─ fetchNodeContent → 判断文档类型 → 导出/下载
```
#### 增量同步 (FetchIncremental)
增量同步基于节点编辑时间对比,而非飞书事件订阅:
```
1. 加载上次游标 (feishuCursor)
└─ SpaceNodeTimes[space_id][node_token] = 上次记录的编辑时间
2. 对每个 Space 递归列举所有节点(全树遍历)
3. 对比变更:
├─ 新节点(游标中不存在)→ 拉取内容
├─ obj_edit_time 变更(优先)或 node_edit_time 变更 → 拉取内容
├─ 编辑时间未变 → 跳过
└─ 游标中存在但本次遍历不存在 → 标记为 IsDeleted
4. 返回新游标(含本轮每个节点的最新编辑时间)
```
> **注意**:增量同步的 API 调用量与知识库空间的节点树大小相关,因为需要遍历完整节点树来检测变更。对于大型空间,建议适当降低同步频率。
#### 支持的文档类型
| `obj_type` | 支持 | 获取方式 | 导出格式 |
|------------|------|---------|---------|
| `docx` | 是 | 导出 .docx(默认)/ blocks API 转 Markdown(blocks 模式) | `.docx` / Markdown |
| `doc` | 是 | 导出任务 (Export API) | `.docx` |
| `sheet` | 是 | 导出任务 (Export API) | `.xlsx` |
| `bitable` | 是 | 导出任务 (Export API) | `.xlsx` |
| `file` | 是 | 直接下载 (Drive Download API) | 原格式 |
| `mindnote` | 否 | — | — |
| `slides` | 否 | — | — |
#### 内容获取流程
**docx(新版云文档):** 由环境变量 `FEISHU_DOCX_PARSE_MODE` 控制解析路径(作用于 app 服务,飞书知识库和飞书云盘连接器同时生效)。
- **export 模式(默认,留空或 `export`)**:走异步导出 API
```
1. CreateExportTask -> 创建导出任务
2. 轮询 GetExportTaskStatus(间隔 2 秒,最长约 60 秒)
3. DownloadExportFile -> 下载 .docx 二进制
4. .docx 交 docreader 解析(图片 inline 进父文档,与普通 docx 上传一致)
```
- **blocks 模式(`FEISHU_DOCX_PARSE_MODE=blocks`)**:走 blocks API
```
1. GET /open-apis/docx/v1/documents/{obj_token}/blocks (分页 500)
2. blocksToMarkdown -> 转换为 Markdown 正文
├─ 文本/标题/列表/表格/代码块 -> Markdown
├─ image block -> ![图片]() 空占位符(图片单独下载为独立知识条目)
└─ file block -> 附件,作为独立知识条目
3. blocks API 失败或渲染空 -> 回退 export
```
**doc / sheet / bitable:** 走异步导出 API(同 export 模式流程),导出为 .docx / .xlsx 后交 docreader 解析。
**文件 (file):**
```
DownloadDriveFile -> GET /drive/v1/files/{token}/download
```
##### blocks vs export 优缺点
| | export(默认) | blocks |
|---|---|---|
| 解析路径 | 导出 .docx -> docreader | blocks API -> Markdown |
| 图片与文档关联 | ✅ 图片 inline 进父文档,`parent_chunk_id` 关联 | ❌ 图片作为独立知识条目,与文档割裂 |
| 检索 / Wiki / 智能体关联图片 | 是 | 否 |
| 同步速度 | 慢(异步导出 + docx 解析) | 快 |
| docx 内附件(file block) | 丢失(.docx 导出不含) | 保留 |
| 所需权限 | 不需要(但建议保留以便切换) | 需 `docx:document:readonly` |
- **export**:图片 inline 进父文档(同普通 docx 上传),通过 `parent_chunk_id` 建立同知识条目父子关联,三个场景都能关联图片内容;代价是同步慢、docx 内附件丢失、图片 OCR/caption 依赖多模态配置。
- **blocks**:快、保留附件,但图片 block 渲染成空占位符、图片独立入库,与父文档只有元数据级弱关联,检索 / Wiki / 智能体都无法把图片内容关联回文档。
> 环境变量配置见 `.env.example` 的 E9 段。
#### 源码文件
| 文件 | 职责 |
|------|------|
| `internal/datasource/connector/feishu/core/types.go` | 飞书 API 类型定义、配置结构(Config)、Region 常量 |
| `internal/datasource/connector/feishu/core/client.go` | API 客户端:Token 管理、Wiki/Drive API 调用、导出/下载 |
| `internal/datasource/connector/feishu/core/blocks.go` | docx blocks API 类型(DocxBlock 等)、listDocumentBlocks |
| `internal/datasource/connector/feishu/core/markdown.go` | blocksToMarkdown:block 数组转 Markdown |
| `internal/datasource/connector/feishu/core/shared.go` | 共享逻辑:FetchDocxWithBlocks、ParseFeishuConfig、exportDocxFallback |
| `internal/datasource/connector/feishu/core/engine.go` | 通用同步引擎:NodeOps 接口、FetchStreamEngine / FetchAllEngine |
| `internal/datasource/connector/feishu/core/region.go` | Region(飞书 / Lark 云区分)、URL 构造 |
| `internal/datasource/connector/feishu/wiki/connector.go` | 飞书知识库 Connector 实现(wikiOps) |
| `internal/datasource/connector/feishu/drive/connector.go` | 飞书云盘 Connector 实现(driveOps) |
| `internal/datasource/connector/feishu/{core,wiki,drive}/*_test.go` | 单元测试:使用 HTTP Mock 模拟飞书开放平台 |
## 定时调度
### Cron 调度器
数据源同步调度基于 `robfig/cron/v3`,使用 **6 段 Cron 表达式(含秒)**:
```
秒 分 时 日 月 星期
```
**常用预设:**
| 表达式 | 说明 |
|--------|------|
| `0 */30 * * * *` | 每 30 分钟 |
| `0 0 * * * *` | 每小时整点 |
| `0 0 */6 * * *` | 每 6 小时 |
| `0 0 0 * * *` | 每天午夜 |
| `0 0 2 * * *` | 每天凌晨 2 点 |
### 调度生命周期
```
服务启动
│
▼
Scheduler.Start(ctx)
│
├─ 1. 从 DB 加载所有活跃数据源 (FindActive)
│ 条件: status = active, sync_schedule != '', deleted_at IS NULL
│
├─ 2. 为每个数据源注册 Cron Entry
│
└─ 3. 启动 Cron Runner
Cron 触发时:
├─ 检查是否有运行中的同步 (HasRunningSync)
│ 是 → 跳过本次触发
│
├─ 创建 SyncLog (status: running)
│
└─ 入队 asynq 任务
队列: low
TaskID: dssync:<dsID>:<UTC minute>(去重)
```
### 任务去重(两层保护)
1. **DB 层**:`HasRunningSync` 检查该数据源是否已有运行中的同步任务,防止同步耗时超过调度间隔时产生重叠
2. **队列层**:基于 `TaskID`(格式 `dssync:<dataSourceID>:<UTC minute>`)去重,同一分钟内多实例触发时只有第一个成功入队,其余标记为 `canceled`
### 动态管理
| 操作 | 行为 |
|------|------|
| 创建数据源(含 schedule) | 注册 Cron Entry |
| 更新数据源 schedule | 移除旧 Entry → 注册新 Entry |
| 暂停数据源 | 移除 Cron Entry |
| 恢复数据源 | 重新注册 Cron Entry |
| 删除数据源 | 移除 Cron Entry |
---
## 关键参数与阈值
| 参数 | 值 | 说明 |
|------|------|------|
| Token 缓存安全余量 | 5 min | Token 过期前提前刷新(飞书/企微通用) |
| 飞书 Wiki Space 分页大小 | 50 | ListResources 每页返回数 |
| 飞书导出任务轮询间隔 | 2s | 飞书 Export API 状态检查间隔 |
| 飞书导出任务最大等待 | ~60s | 导出任务超时时间 |
| 企微文档列表分页大小 | 50 | wedoc/doc_list 每页返回数 |
| 企微微盘文件列表分页大小 | 1000 | wedrive/file_list 每次拉取最大数 |
| 企微 access_token 有效期 | 7200s | 2 小时,缓存提前 5 分钟刷新 |
| 同步日志保留天数 | 30 天 | 默认值,可配置 |
| 同步日志默认分页 | 10 | GetSyncLogs 默认 limit |
| Cron 表达式格式 | 6 段含秒 | robfig/cron WithSeconds() |
| 定时同步队列 | `low` | asynq 低优先级队列 |
| 手动同步队列 | `default` | asynq 默认队列 |
---
## 错误处理
| 场景 | 处理策略 |
|------|---------|
| 凭证验证失败 | 返回错误信息,数据源状态标记为 `error`,记录 `error_message` |
| 凭证验证成功(原为 error) | 恢复状态为 `active`,清空 `error_message` |
| 连接器未找到 | 返回 `ErrConnectorNotFound` |
| 知识库不存在 | 创建/更新时校验,返回 `ErrKnowledgeBaseNotFound` |
| 数据源不存在 | 返回 `ErrDataSourceNotFound` |
| 数据源非活跃状态触发同步 | 仅允许 `active` 或 `error` 状态手动同步 |
| 同步任务入队失败 | SyncLog 标记为 `failed`,记录错误 |
| 定时同步任务去重 | SyncLog 标记为 `canceled`,错误信息 `deduplicated: another instance enqueued first` |
| 单文档拉取失败 | 记录错误到 FetchedItem.Metadata,不中断整体同步 |
| 单文档入库失败 | 计入 `items_failed`,不中断整体同步 |
| 重复文档 (DuplicateKnowledgeError) | 计入 `items_skipped` |
| 不支持的文档类型 | 静默跳过(如飞书的 mindnote、slides) |
---
## 扩展新连接器
接入新的外部平台只需 3 步:
### 1. 实现 Connector 接口
在 `internal/datasource/connector/<type>/` 下创建连接器:
```go
package myplatform
type Connector struct {
// 连接器配置
}
func NewConnector() *Connector {
return &Connector{}
}
func (c *Connector) Type() string {
return types.ConnectorTypeMyPlatform
}
func (c *Connector) Validate(ctx context.Context, config *types.DataSourceConfig) error {
// 验证凭证:尝试获取 Token 或调用 API
}
func (c *Connector) ListResources(ctx context.Context, config *types.DataSourceConfig) ([]types.Resource, error) {
// 列出可选资源(工作区、空间、文件夹等)
}
func (c *Connector) FetchAll(ctx context.Context, config *types.DataSourceConfig, resourceIDs []string) ([]types.FetchedItem, error) {
// 全量拉取所有文档
}
func (c *Connector) FetchIncremental(ctx context.Context, config *types.DataSourceConfig, cursor *types.SyncCursor) ([]types.FetchedItem, *types.SyncCursor, error) {
// 增量拉取变更(可复用 FetchAll 逻辑 + 编辑时间对比)
}
```
建议的文件结构:
```
internal/datasource/connector/myplatform/
├── types.go # 平台 API 类型定义
├── client.go # API 客户端(Token 管理、HTTP 调用)
└── connector.go # Connector 接口实现
```
### 2. 注册连接器
在 `internal/container/container.go` 的 `initConnectorRegistry` 中注册:
```go
func initConnectorRegistry() *datasource.ConnectorRegistry {
registry := datasource.NewConnectorRegistry()
registry.Register(feishuConnector.NewConnector())
registry.Register(myplatform.NewConnector()) // 新增
return registry
}
```
在 `internal/datasource/connector.go` 的 `ConnectorMetadataRegistry` 中添加元数据:
```go
types.ConnectorTypeMyPlatform: {
Type: types.ConnectorTypeMyPlatform,
Name: "My Platform",
Description: "Import documents from My Platform",
Icon: "myplatform",
Priority: 10,
AuthType: "api_key",
Capabilities: []string{"incremental"},
},
```
在 `internal/types/datasource.go` 中添加类型常量:
```go
const ConnectorTypeMyPlatform = "myplatform"
```
### 3. 前端添加配置
在 `frontend/src/views/knowledge/settings/DataSourceEditorDialog.vue` 中:
- 添加连接器类型选项
- 添加该平台的凭证表单字段
- 标记 `available: true`
在 i18n 文件中添加平台名称翻译。
> Service 层(`DataSourceService`)不需要修改 — 数据源管理、同步调度、内容入库、日志记录全部由 Service 统一处理。可参考飞书连接器 (`internal/datasource/connector/feishu/`) 作为实现参考。