Files
knowledge-governance/web/src/dingtalkSources.ts
T
姚章浩 e00cab2f70 feat(settings): 数据源配置改成「列出来、勾选、变成来源」,并去掉文件格式筛选
用户对 系统设置 → 数据源配置 的四条要求:
① 这里不要做文件格式筛选(格式归目录级标注决定);
② 列出我账号下能看到的知识库;③ 列出钉盘能看到哪些;④ 勾选那些作为来源。

① 来源卡片不再渲染 FilenameFilterEditor(目录级筛选仍在「来源与目录」里,按目录就近覆盖)。
②③ 新端点 GET /api/v1/dingtalk-spaces/available(管理员,只读):用 dws 列举
   - 知识库:wiki +space-list --type orgWikiSpace/myWikiSpace(实测 17 + 1 个)
   - 钉盘:wiki space list --type mySpace/orgSpace(实测 我的文件 + 6 个组织空间)
   实测约束写进命令形态:--limit 上限是 50;翻页必须 --page-all(--max-items 兜底),
   否则超过一页的知识库会被静默截断(被截断时页面显示"可能不全"而不是假装列全)。
   列不出来 ≠ 空列表:dws 未登录/权限不足 → 503 + 原因;某一类失败只丢那一类。
④ 面板勾选后走既有登记路径(/api/v1/dingtalk-spaces),首次目录扫描自动排队;
   已登记的按空间名标「已是来源」并禁用勾选;扫码会话失效时直接弹二维码,扫完自动重试。

基础设施:api 与 worker 共享同一份 dws 状态(bind mount 同一份,refresh token 是轮换制,
不能各留一份)。子进程 HOME 必须指向 DWS_HOME —— 这条是实测踩出来的:漏了会报"未登录",
已有回归测试钉住。dws 子进程沿用白名单环境,库口令等变量不进子进程。

测试:新包 internal/dingtalkdiscovery(fixture 用真实 dws 输出形状):paging 约束、
截断提示、一类失败不连带、全部失败→ErrUnavailable、子进程环境;httpapi 侧 401/501/503/500
与不泄漏内部错误;OpenAPI 契约(400/503 组件)通过。前端 189 条 + tsc 干净。

面板实测:知识库(18)/钉盘(7)都列出来,已有三条钉钉来源自动标「已是来源」;
勾选「视觉文档」→ 加入为来源 → 新来源 sc-8a4be5b8293d1c9a048c3c7e9b194155(dingtalk,enabled)
+ source.catalog.refresh 已完成。
2026-10-10 16:46:39 +08:00

106 lines
5.1 KiB
TypeScript
Raw 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.
// 「系统设置 → 数据源配置」的数据源发现客户端:列出当前 dws 账号能看到的钉钉知识库与钉盘空间。
// 只读;勾选后的登记仍走既有的 /api/v1/dingtalk-spaces(由扫码会话解析空间根目录)。
export interface AvailableKnowledgeBase {
name: string
space_id: string
space_type?: string
url: string
description?: string
}
export interface AvailableDriveSpace {
name: string
space_id: string
space_type: string
root_folder_id?: string
}
export interface AvailableDingtalkSources {
knowledge_bases: AvailableKnowledgeBase[]
drive_spaces: AvailableDriveSpace[]
/** 知识库这一类为什么不全(某类无权限、被上限截断)。 */
knowledge_notice?: string
/** 钉盘这一类为什么列不出来(租户权限差异)。 */
drive_notice?: string
fetched_at: string
}
// 钉盘空间的链接形态与知识库一致(空间 id 决定地址),勾选后按同一条登记路径提交。
export function driveSpaceURL(spaceID: string): string {
return `https://alidocs.dingtalk.com/i/spaces/${spaceID}/overview`
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
export async function fetchAvailableDingtalkSources(signal?: AbortSignal): Promise<AvailableDingtalkSources> {
const response = await fetch('/api/v1/dingtalk-spaces/available', { headers: { Accept: 'application/json' }, signal })
if (!response.ok) {
// 503 是环境问题(dws 未登录/权限不足),501 是后端没接线:都要说清楚"不是没有来源"。
if (response.status === 503) throw new Error('暂时列不出来:dws 未登录或缺少租户目录读权限。先刷新钉钉会话,或让管理员检查 dws 凭据。')
if (response.status === 501) throw new Error('后端未接线:数据源发现接口不可用。')
if (response.status === 401 || response.status === 403) throw new Error('需要管理员身份才能列举数据源。')
throw new Error(`列举数据源失败(HTTP ${response.status})`)
}
const payload = await response.json() as unknown
const data = isRecord(payload) && isRecord(payload.data) ? payload.data : {}
const knowledgeBases = Array.isArray(data.knowledge_bases) ? data.knowledge_bases : []
const driveSpaces = Array.isArray(data.drive_spaces) ? data.drive_spaces : []
return {
knowledge_bases: knowledgeBases.flatMap((item) => {
if (!isRecord(item)) return []
const name = typeof item.name === 'string' ? item.name : ''
const spaceID = typeof item.space_id === 'string' ? item.space_id : ''
const url = typeof item.url === 'string' && item.url ? item.url : driveSpaceURL(spaceID)
const spaceType = typeof item.space_type === 'string' ? item.space_type : ''
return name && spaceID ? [{ name, space_id: spaceID, space_type: spaceType, url, description: typeof item.description === 'string' ? item.description : '' }] : []
}),
drive_spaces: driveSpaces.flatMap((item) => {
if (!isRecord(item)) return []
const name = typeof item.name === 'string' ? item.name : ''
const spaceID = typeof item.space_id === 'string' ? item.space_id : ''
const spaceType = typeof item.space_type === 'string' ? item.space_type : ''
return name && spaceID ? [{ name, space_id: spaceID, space_type: spaceType, root_folder_id: typeof item.root_folder_id === 'string' ? item.root_folder_id : '' }] : []
}),
knowledge_notice: typeof data.knowledge_notice === 'string' ? data.knowledge_notice : '',
drive_notice: typeof data.drive_notice === 'string' ? data.drive_notice : '',
fetched_at: typeof data.fetched_at === 'string' ? data.fetched_at : '',
}
}
export interface DiscoveryCandidate {
key: string
kind: 'knowledge_base' | 'drive_space'
name: string
spaceID: string
url: string
detail: string
alreadyRegistered: boolean
}
// 把发现结果拍平成可勾选列表:已登记(按空间名比对来源连接名)的条目打标,页面据此禁用勾选,
// 避免重复登记。
export function discoveryCandidates(list: AvailableDingtalkSources | null, registeredNames: string[]): DiscoveryCandidate[] {
const registered = new Set(registeredNames.map((name) => name.trim()).filter(Boolean))
const knowledgeBases = (list?.knowledge_bases ?? []).map((item): DiscoveryCandidate => ({
key: `kb:${item.space_id}`,
kind: 'knowledge_base',
name: item.name,
spaceID: item.space_id,
url: item.url || driveSpaceURL(item.space_id),
detail: item.space_type === 'myWikiSpace' ? '钉钉知识库 · 我的知识库' : '钉钉知识库 · 组织知识库',
alreadyRegistered: registered.has(item.name.trim()),
}))
const driveSpaces = (list?.drive_spaces ?? []).map((item): DiscoveryCandidate => ({
key: `drive:${item.space_id}`,
kind: 'drive_space',
name: item.name,
spaceID: item.space_id,
url: driveSpaceURL(item.space_id),
detail: item.space_type === 'mySpace' ? '钉盘 · 我的文件' : '钉盘 · 组织空间',
alreadyRegistered: registered.has(item.name.trim()),
}))
return [...knowledgeBases, ...driveSpaces]
}