Files
knowledge-governance/web/src/routingBindings.ts
T
姚章浩 6440e3f679 feat(release): 发布目标知识库可选(源库 → 目标库映射)
- 迁移 000044:production_kb_bindings + release_item_executions.target_knowledge_base_id
- 系统设置新增「生产知识库绑定(对内/对外)」:从生产 WeKnora 实际列表选库,
  保存时真校验(不存在拒收;生产端不可达只提示并如实回报),仅管理员可改
- 执行计划按绑定解析目标库并冻结进 release_item_executions;空目标直接拒收;
  已发布过的条目保留原目标(重试/回滚回到同一处)
- DocumentBundle.RemapKnowledgeBase:knowledges/chunks/knowledge_tags/wiki_pages/
  wiki_folders 的 knowledge_base_id 与 ES 文档字段改写为目标库,重算表/文档摘要并重新封装
- 契约比对改为「声明的映射对」(KnowledgeBaseMapping),其余字段继续 fail-closed
- 契约检查器 / 目标身份 / 验证器全部走映射后的目标库;对象通道无需改写
- Compose 拆分:数据面接线 compose.release-sim.yaml + 两道门禁 compose.release-gates.yaml
- 测试:RemapKnowledgeBase 全通道/无操作/fail-closed、映射契约、绑定接口、RBAC、真实 PG 集成
2026-10-08 15:41:13 +08:00

79 lines
3.8 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.
import { newOperationID } from './operationKey'
export interface RoutingBindings {
internal_kb_id: string
external_kb_id: string
internal_env_fallback: boolean
external_env_fallback: boolean
internal_bound_by_ui: boolean
external_bound_by_ui: boolean
internal_kb_name: string
external_kb_name: string
updated_by: string
weknora_connected: boolean
// 本次保存把多少条「还没上传」的待入库项重新指向了新库(读取时为 0)。
replanned: number
retired_duplicates: number
}
export interface KnowledgeBaseOption {
id: string
name: string
knowledge_count: number
}
const record = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
function bindingProjection(item: Record<string, unknown>): RoutingBindings {
return {
internal_kb_id: typeof item.internal_kb_id === 'string' ? item.internal_kb_id : '',
external_kb_id: typeof item.external_kb_id === 'string' ? item.external_kb_id : '',
internal_env_fallback: item.internal_env_fallback === true,
external_env_fallback: item.external_env_fallback === true,
internal_bound_by_ui: item.internal_bound_by_ui === true,
external_bound_by_ui: item.external_bound_by_ui === true,
internal_kb_name: typeof item.internal_kb_name === 'string' ? item.internal_kb_name : '',
external_kb_name: typeof item.external_kb_name === 'string' ? item.external_kb_name : '',
updated_by: typeof item.updated_by === 'string' ? item.updated_by : '',
weknora_connected: item.weknora_connected === true,
replanned: typeof item.replanned === 'number' ? item.replanned : 0,
retired_duplicates: typeof item.retired_duplicates === 'number' ? item.retired_duplicates : 0,
}
}
export async function fetchRoutingBindings(signal?: AbortSignal): Promise<RoutingBindings> {
const response = await fetch('/api/v1/routing-bindings', { signal, headers: { Accept: 'application/json' } })
if (!response.ok) throw new Error(`读取知识库绑定失败(HTTP ${response.status})`)
const value = await response.json() as unknown
if (!record(value) || !record(value.data)) throw new Error('知识库绑定响应无效')
return bindingProjection(value.data)
}
export async function fetchRoutingBindingOptions(signal?: AbortSignal): Promise<KnowledgeBaseOption[]> {
const response = await fetch('/api/v1/routing-bindings/options', { signal, headers: { Accept: 'application/json' } })
if (!response.ok) throw new Error(`读取可绑定知识库失败(HTTP ${response.status})`)
const value = await response.json() as unknown
if (!record(value) || !Array.isArray(value.data)) throw new Error('知识库列表响应无效')
return value.data.filter(record).map((item) => ({
id: typeof item.id === 'string' ? item.id : '',
name: typeof item.name === 'string' ? item.name : '',
knowledge_count: typeof item.knowledge_count === 'number' ? item.knowledge_count : 0,
})).filter((item) => item.id)
}
export async function saveRoutingBindings(input: { internal_kb_id: string; external_kb_id: string }, signal?: AbortSignal): Promise<RoutingBindings> {
const response = await fetch('/api/v1/routing-bindings', {
method: 'PUT', signal,
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'Idempotency-Key': newOperationID() },
body: JSON.stringify(input),
})
if (!response.ok) {
let reason = `保存知识库绑定失败(HTTP ${response.status})`
try { const body = await response.json() as unknown; if (record(body) && typeof body.error === 'string') reason = body.error } catch { /* keep status text */ }
throw new Error(reason)
}
const value = await response.json() as unknown
if (!record(value) || !record(value.data)) throw new Error('知识库绑定响应无效')
return bindingProjection(value.data)
}