e00cab2f70
用户对 系统设置 → 数据源配置 的四条要求: ① 这里不要做文件格式筛选(格式归目录级标注决定); ② 列出我账号下能看到的知识库;③ 列出钉盘能看到哪些;④ 勾选那些作为来源。 ① 来源卡片不再渲染 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 已完成。
106 lines
5.1 KiB
TypeScript
106 lines
5.1 KiB
TypeScript
// 「系统设置 → 数据源配置」的数据源发现客户端:列出当前 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]
|
||
}
|