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 干净
This commit is contained in:
2026-09-13 12:02:58 +08:00
committed by 姚章浩
parent ed48c4ceda
commit dcdc8d1ddd
410 changed files with 56661 additions and 1 deletions
+110
View File
@@ -0,0 +1,110 @@
"""Read-only UI acceptance; no review, upload, retry or production writes."""
import json
from pathlib import Path
import urllib.request
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.support.ui import WebDriverWait, Select
BASE = 'http://127.0.0.1:8088'
ROOT = Path(__file__).resolve().parents[1]
items = []
while True:
with urllib.request.urlopen(f'{BASE}/api/v1/source-items?limit=100&offset={len(items)}', timeout=10) as response:
page = json.load(response)
items.extend(page['data'])
if len(items) >= page['summary']['total']:
break
assert page['data']
assert len(items) == len({x['id'] for x in items}) == page['summary']['total']
assert all(x['title'] for x in items)
assert not any(x['directory_path'].startswith('/') or '://' in x['directory_path'] for x in items)
(ROOT/'tmp/document-ui-catalog.json').write_text(json.dumps(items,ensure_ascii=False))
options = Options()
options.add_argument('--headless')
options.binary_location = '/snap/firefox/current/usr/lib/firefox/firefox'
driver = webdriver.Firefox(options=options, service=Service('/snap/bin/geckodriver'))
wait = WebDriverWait(driver,30,poll_frequency=.15)
driver.set_window_size(1440,1100)
def text():
return driver.find_element(By.TAG_NAME,'body').text
def nav(label):
driver.find_element(By.XPATH,f"//nav//button[contains(.,'{label}')]").click()
def loaded():
wait.until(lambda d: not d.find_element(By.XPATH,"//header//button[contains(.,'刷新')]").get_attribute('disabled'))
def root_fits():
assert driver.execute_script('return document.documentElement.scrollWidth <= window.innerWidth'), 'root overflow'
try:
driver.get(BASE)
wait.until(lambda d: d.find_element(By.TAG_NAME,'h1').text == '来源与目录')
loaded()
assert not driver.find_elements(By.CSS_SELECTOR,'[role="alert"]')
assert '未接通目录' in text()
assert '只走这 8 步' not in text()
expected_external = sum(x['visibility']=='external' for x in items)
counts = driver.find_element(By.CSS_SELECTOR,'.document-counts').text
assert str(len(items)) in counts and str(expected_external) in counts
first = next(x for x in items if x['directory_path'] and x['source_type']=='pp')
folder = next(e for e in driver.find_elements(By.CSS_SELECTOR,'.folder.child') if first['directory_path'] in e.text)
folder.click()
wait.until(lambda d: first['directory_path'] in d.find_element(By.CSS_SELECTOR,'.document-location').text)
rows = driver.find_elements(By.CSS_SELECTOR,'.document-table tbody tr')
assert rows and all(first['directory_path'] in r.text for r in rows)
driver.find_element(By.CSS_SELECTOR,'.folder').click()
search = driver.find_element(By.CSS_SELECTOR,'input[type="search"]')
search.send_keys(first['title'])
wait.until(lambda d: first['title'] in d.find_element(By.CSS_SELECTOR,'.document-table').text)
driver.find_element(By.CSS_SELECTOR,'.file-link').click()
detail = wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.document-inspector'))
assert detail.is_displayed() and first['title'] in detail.text
assert '原文预览尚未接通' in detail.text
driver.find_element(By.XPATH,"//button[.='关闭详情']").click()
search.clear()
search.send_keys(' ')
selects = driver.find_elements(By.CSS_SELECTOR,'.document-filters select')
Select(selects[0]).select_by_value('internal')
wait.until(lambda d: '筛选结果 5 份' in d.find_element(By.CSS_SELECTOR,'.document-location').text)
assert len(driver.find_elements(By.CSS_SELECTOR,'.document-table tbody tr')) == 5
Select(selects[0]).select_by_value('')
root_fits()
assert driver.execute_script('return document.documentElement.scrollHeight < window.innerHeight + 300'), 'document table should scroll inside its panel'
driver.get_full_page_screenshot_as_file(str(ROOT/'tmp/DOCUMENT_WORKBENCH_FIREFOX.png'))
nav('解析监控')
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='解析监控')
loaded()
before = driver.execute_script("return performance.getEntriesByType('resource').filter(x=>x.name.includes('/source-items?')).length")
WebDriverWait(driver,25).until(lambda d:d.execute_script("return performance.getEntriesByType('resource').filter(x=>x.name.includes('/source-items?')).length") > before)
loaded()
assert '解析完成不代表已上线' in text()
nav('待处理文件')
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='待处理文件')
expected_attention = sum(x['content_state']=='over_limit' or x['processing_state'] in ('failed','stalled') for x in items)
assert f'筛选结果 {expected_attention} 份' in text()
nav('来源连接')
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='来源连接')
assert not driver.find_element(By.CSS_SELECTOR,'.scan-audit-panel').get_attribute('open')
nav('发布管理')
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='从审核到安全发布')
assert not driver.find_element(By.CSS_SELECTOR,'.release-help').get_attribute('open')
assert '只走这 8 步' not in text()
assert not driver.find_elements(By.XPATH,"//button[.='授权生产发布']")
nav('来源与目录')
loaded()
for width in (1440,1024,768):
driver.set_window_size(width,1000)
root_fits()
driver.refresh()
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='来源与目录')
assert driver.find_element(By.CSS_SELECTOR,'.tab-list').text.count('解析监控')==1
result = {'catalog':len(items),'titles':sum(bool(x['title']) for x in items),'categories':sum(bool(x['directory_path']) for x in items),'external':expected_external,'attention':expected_attention,'search':'pass','directory_filter':'pass','scope_filter':'pass','file_drawer':'pass','automatic_refresh':'pass','tabs_restore':'pass','root_overflow':False,'write_requests':0}
(ROOT/'tmp/document-ui-acceptance.json').write_text(json.dumps(result,ensure_ascii=False,indent=2))
print(json.dumps(result,ensure_ascii=False))
finally:
driver.quit()