# 数据源导入开发文档 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 | |---|---|---| | 开放平台 | | | | 连接器类型 | 「飞书」 | 「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::(去重) ``` ### 任务去重(两层保护) 1. **DB 层**:`HasRunningSync` 检查该数据源是否已有运行中的同步任务,防止同步耗时超过调度间隔时产生重叠 2. **队列层**:基于 `TaskID`(格式 `dssync::`)去重,同一分钟内多实例触发时只有第一个成功入队,其余标记为 `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//` 下创建连接器: ```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/`) 作为实现参考。