6440e3f679
- 迁移 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 集成
79 lines
3.8 KiB
TypeScript
79 lines
3.8 KiB
TypeScript
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)
|
||
}
|