Files
knowledge-governance/web/src/SourceLinkManager.tsx
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

36 lines
5.5 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 { useContext,useEffect,useState } from 'react'
import { fetchCatalogSources } from './sourceCatalog'
import type { CatalogSource } from './sourceCatalog'
import { WorkspaceContextTabsSetter } from './workspaceContext'
import './sourceLinkManager.css'
const typeLabel={dingtalk:'钉钉知识库',pp:'PP资料下载中心',alist:'AList文件源'}
const authLabel={dingtalk:'专用浏览器扫码登录',pp:'公开访问,无需Key',alist:'只读Token'}
const actionLabel={dingtalk:'模拟登录并验证目录',pp:'模拟验证公开目录',alist:'模拟验证只读Token'}
const placeholder={dingtalk:'粘贴钉钉知识库网页链接(模拟)',pp:'粘贴PP资料下载中心链接(模拟)',alist:'例如:https://alist.example.com(模拟)'}
const linkLabel={dingtalk:'知识库链接',pp:'资料中心地址',alist:'AList站点地址'}
interface Props{sources:CatalogSource[];selected:CatalogSource|null;validated:boolean;loading:boolean;error:string|null;onSelect:(source:CatalogSource)=>void;onValidate:()=>void;onRefresh:()=>void}
export function SourceLinkManagerView({sources,selected,validated,loading,error,onSelect,onValidate,onRefresh}:Props){
const [link,setLink]=useState('')
return <main className="page-shell source-link-manager">
<header className="hero compact"><div><p className="eyebrow">连接配置</p><h1>来源链接</h1><p className="subtitle">配置钉钉、PP和AList的入口、认证方式及目录验证。</p></div><button onClick={onRefresh} disabled={loading}>{loading?'刷新中…':'刷新来源'}</button></header>
{error?<div className="error-banner" role="alert">{error}</div>:null}
<div className="simulation-banner"><strong>本地模拟配置</strong><span>使用真实来源名称和快照数量,但不连接登录服务、不保存真实Key。</span></div>
<div className="source-link-layout">
<aside className="source-link-list"><h2>内容来源</h2>{sources.map(source=><button className={selected?.connection_id===source.connection_id?'active':''} key={source.connection_id} onClick={()=>{setLink('');onSelect(source)}}><span><b>{source.name}</b><small>{typeLabel[source.source_type]}</small></span><strong>{source.file_count}份</strong></button>)}{!loading&&!error&&!sources.length?<div className="empty-state">尚无来源链接。</div>:null}</aside>
<section className="source-link-editor">{selected?<><div className="source-link-title"><div><h2>{selected.name}</h2><span>{typeLabel[selected.source_type]} · 本地只读快照</span></div><span className="connection-unverified">模拟验证待执行</span></div>
<div className="source-link-fields"><label>{linkLabel[selected.source_type]}<input value={link} onChange={event=>setLink(event.target.value)} placeholder={placeholder[selected.source_type]} /></label><label>认证方式<input value={authLabel[selected.source_type]} readOnly /></label><label>Key或Token<input value="" disabled placeholder="本地模拟阶段不接收真实Key" /></label><label>起始目录<input value={selected.source_type==='alist'?`/${selected.name}`:selected.name} readOnly /></label></div>
<div className="source-link-boundary"><b>当前快照</b><span>文件 {selected.file_count}</span><span>目录 {selected.folder_count}</span><span>不下载文件内容,只读取目录和元数据</span><span>不执行上传、移动、重命名或删除</span></div>
<button className="validate-source-link" onClick={onValidate}>{actionLabel[selected.source_type]}</button>
{validated?<div className="validation-results" aria-label="模拟验证结果"><div><span>网络连接</span><b>模拟通过</b></div><div><span>登录或Token</span><b>模拟通过</b></div><div><span>根目录</span><b>模拟通过</b></div><div><span>读取第一层目录</span><b>模拟通过</b></div><div><span>只读边界</span><b>模拟通过</b></div><small>以上只是本地交互模拟;迁移到测试服务器后才执行真实验证。</small></div>:null}
<p className="credential-note">真实模式下,新凭据先验证,成功后才替换旧凭据;网页永远不能读取已保存Key。</p>
</>:loading?<div className="empty-state">正在加载来源…</div>:null}</section>
</div>
</main>
}
export function SourceLinkManager(){
const [sources,setSources]=useState<CatalogSource[]>([]);const [selected,setSelected]=useState<CatalogSource|null>(null);const [validated,setValidated]=useState(false);const [loading,setLoading]=useState(true);const [error,setError]=useState<string|null>(null);const [revision,setRevision]=useState(0);const setContextTabs=useContext(WorkspaceContextTabsSetter)
useEffect(()=>{const controller=new AbortController();setLoading(true);void fetchCatalogSources(controller.signal).then(result=>{if(!controller.signal.aborted){setSources(result);setSelected(current=>current&&result.find(item=>item.connection_id===current.connection_id)||result[0]||null);setError(null)}}).catch(cause=>{if(!controller.signal.aborted)setError(cause instanceof Error?cause.message:'加载来源链接失败')}).finally(()=>{if(!controller.signal.aborted)setLoading(false)});return()=>controller.abort()},[revision])
useEffect(()=>{setContextTabs(selected?[{key:selected.connection_id,label:selected.name,onActivate:()=>undefined}]:[]);return()=>setContextTabs([])},[selected,setContextTabs])
return <SourceLinkManagerView sources={sources} selected={selected} validated={validated} loading={loading} error={error} onSelect={source=>{setSelected(source);setValidated(false)}} onValidate={()=>setValidated(true)} onRefresh={()=>{setValidated(false);setRevision(value=>value+1)}} />
}