Make the governed RAG evidence path executable end to end
Some checks failed
verify / verify (push) Has been cancelled

Separate local parsing from model indexing, bind review decisions to immutable manifests, persist vectors behind active profiles, and expose retrieval, chat, evaluation, and document workflows through the React workbench.

Constraint: Live Bailian authentication currently fails for all three configured capabilities

Rejected: Direct upload-to-embedding flow | bypasses local review and manifest binding

Confidence: high

Scope-risk: broad

Directive: Keep private-data deployment blocked until authentication, RBAC, and separate database roles land

Tested: make verify; fresh and replay Docker document smoke; worker recovery smoke; frozen synthetic evaluation; migration 0003-0004 roundtrip

Not-tested: Successful live Bailian calls, OCR, real multi-user authorization
This commit is contained in:
2026-07-13 05:58:11 +08:00
parent 75592af33a
commit ecdb10c37a
111 changed files with 25457 additions and 152 deletions

View File

@@ -0,0 +1,162 @@
import { useRef, useState } from "react";
import { Icon } from "../../../components/Icon";
import { DOCUMENT_ACCEPT, formatBytes, validateDocumentFile } from "../file";
import type { DocumentJob, UploadPhase, UploadWorkflowState } from "../types";
interface DocumentUploadPanelProps {
workflow: UploadWorkflowState;
job: DocumentJob | undefined;
onUpload: (file: File) => void;
onCancel: () => void;
}
const ACTIVE_PHASES: UploadPhase[] = [
"hashing",
"declaring",
"uploading",
"completing",
"parsing",
"indexing",
];
const STEPS = [
{ phases: ["hashing", "declaring", "uploading", "completing"], label: "校验并隔离上传" },
{ phases: ["parsing"], label: "本地安全解析" },
{ phases: ["review"], label: "人工复核出域清单" },
{ phases: ["indexing", "indexed"], label: "向量化与激活" },
] as const;
function stepState(phase: UploadPhase, index: number): "pending" | "active" | "done" {
if (phase === "indexed") return "done";
const activeIndex = STEPS.findIndex((step) => (step.phases as readonly string[]).includes(phase));
if (activeIndex === -1) return "pending";
if (index < activeIndex) return "done";
return index === activeIndex ? "active" : "pending";
}
export function DocumentUploadPanel({
workflow,
job,
onUpload,
onCancel,
}: DocumentUploadPanelProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [selectionError, setSelectionError] = useState<string | null>(null);
const busy = ACTIVE_PHASES.includes(workflow.phase);
function chooseFile(next: File | undefined) {
if (next === undefined) return;
const error = validateDocumentFile(next);
setSelectionError(error);
setFile(error === null ? next : null);
}
return (
<section
className="document-panel document-upload-panel"
aria-labelledby="document-upload-title"
>
<div className="section-heading">
<div>
<span className="eyebrow">INGESTION GATE</span>
<h2 id="document-upload-title"></h2>
</div>
<span className="section-heading__meta"> 100 MiB</span>
</div>
<div
className={`document-dropzone${selectionError === null ? "" : " document-dropzone--error"}`}
onDragOver={(event) => event.preventDefault()}
onDrop={(event) => {
event.preventDefault();
if (!busy) chooseFile(event.dataTransfer.files[0]);
}}
>
<Icon name="document" size={27} />
<strong>{file === null ? "选择或拖入地质资料" : file.name}</strong>
<span>
{file === null
? "支持 TXT、Markdown、DOCX、PDF"
: `${formatBytes(file.size)} · 等待本地校验`}
</span>
<button
className="secondary-button document-file-button"
disabled={busy}
onClick={() => inputRef.current?.click()}
type="button"
>
</button>
<input
ref={inputRef}
accept={DOCUMENT_ACCEPT}
aria-label="选择待上传文档"
className="visually-hidden"
disabled={busy}
onChange={(event) => chooseFile(event.target.files?.[0])}
type="file"
/>
</div>
{selectionError !== null && (
<p className="field-error" role="alert">
{selectionError}
</p>
)}
<div className="document-workflow" aria-label="文档处理阶段">
{STEPS.map((step, index) => {
const state = stepState(workflow.phase, index);
return (
<div
className={`document-workflow__step document-workflow__step--${state}`}
key={step.label}
>
<span aria-hidden="true">{state === "done" ? "✓" : index + 1}</span>
<small>{step.label}</small>
</div>
);
})}
</div>
<div className="document-live-status" aria-live="polite">
<div>
<strong>{workflow.filename ?? "尚未启动上传"}</strong>
<span>{workflow.message ?? "文件只在浏览器计算摘要,模型密钥不会进入页面。"}</span>
</div>
{job !== undefined && (
<div className="document-job-progress">
<span>{job.stage}</span>
<strong>{job.progress}%</strong>
</div>
)}
</div>
<div className="document-upload-actions">
<button
className="primary-button"
disabled={file === null || busy}
onClick={() => {
if (file !== null) onUpload(file);
}}
type="button"
>
{busy && <span className="button-spinner" aria-hidden="true" />}
{busy ? "处理中" : "校验并上传"}
</button>
{busy && (
<button className="tertiary-button" onClick={onCancel} type="button">
</button>
)}
</div>
<p className="disabled-note">
<Icon name="shield" size={15} />
PDF OCR_REQUIRED
</p>
</section>
);
}