feat(v2): 纠偏第二批——接通历史经验加工闭环
覆盖 AC-50/51/52(BR-48/49、E-28)。审计确认的 P1-3 缺口"历史提炼与卡片 不在实时 API 调用图上"由此关闭:提炼、候选治理、卡片进方案全部有真实调用方。 后端: - contracts:新增 E-28 EXTRACTION_UNAVAILABLE(运行级,不进 HTTP 信封)与 KnowledgeExtraction 记录类型。契约层强制"失败不得带候选", AC-50「模型不可用⇒不产生候选」是类型约束而不是文档约定。 - services/experience_extraction.py(新):真实提炼入口。模型不可用或调用失败 → 记 E-28 且 candidate_id 为空;不产半成品候选。传输纪律同 assistant_model (禁进程内代理、零重试),且 tools=[] 使"越权读材料"在结构上不可能。 自然语言修订同样必须真实调用模型,没有"把中文猜成条件"的回退路径。 - services/knowledge_cards.py:新增 current_draft/revise/list_cards 与 registry_conditions。修订只产草稿、保留 origin 与来源、升版本记 parent_version; test_cases 纳入可改字段——它是 component_evaluation 对自定义 Skill 的发布门禁, 不开放则提炼出的候选永远卡在评测门外。 - services/project_assistant.py:enqueue 改用 review_plan.assemble(), 让 T06-b 的依赖预检(E-19 可定位文案)与维度解析不再被绕过。 - api/prescreen.py:case-timeline、knowledge-extractions、knowledge-candidates (创建/修订/状态机)、review-plans(装配/状态机)。角色门禁与既有 /components 入口一致;id 与数据域由服务端生成;按 id 的路由一律在 run() 内 查归属,否则 KeyError 会变成 500 而不是 404。 - component_registry/review_plan:下沉 current_draft/version_key 与 list_plans。 前端: - 新增 /experience-center 独立入口(BR-50)与 ExperienceCenter 组件; AgentNav 增加「历史经验加工」。 - originBadge 此前零调用方、.origin-* 无任何 CSS——守卫只 grep 库文件字符串, 于是"测试通过但功能不存在"。本批补齐消费方与样式,并用左边框样式区分四种来源, 保证去色仍可区分(AC-42/BR-48)。 - 守卫语义按 AC-55 调整:由"禁止新建路由"改为"禁止重复页面" (面板不得搬到独立路由、已合并页必须保持重定向),并新增 experience-center-ui 守卫(已登记 verify.py,共 12 项检查)。 测试:新增 36 项(提炼 15 / 候选治理 15 / 卡片到运行 6),全量 unittest 无回归; 两次同项目运行对照证明卡片确实改变了核查结果,且 basis_ids 指向 卡片id@版本。 未闭合(记录在 17 的剩余缺口):真实模型提炼尚未实测(需端点); 界面未经浏览器点击验证;例外边界未进入可求值语言;规则类候选不在本批。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -50,3 +50,8 @@
|
||||
.xb-history-counts button,.xb-event-list a{font-size:14px}.xb-history-counts small,.xb-event-list small{font-size:12px}
|
||||
.xb-returned-summary small{font-size:12px}.xb-returned-summary strong{font-size:14px}
|
||||
@media(max-width:760px){.ra-button{min-height:42px;font-size:13px}.xb-project-card h2{font-size:18px}.xb-tabs button,.xb-tabs a{font-size:13px}.xb-review-heading h2{font-size:22px}}
|
||||
.xb-origin{display:inline-flex;align-items:center;gap:5px;font-size:12px;line-height:1.6;padding:3px 9px;border:1px solid #d6dade;border-left-width:4px;border-radius:4px;background:#f4f5f6;color:#5b6672;white-space:nowrap}
|
||||
.origin-history{background:#eaf2ef;color:#1f5c46;border-color:#bcd6c9;border-left-style:dashed}
|
||||
.origin-manual{background:#eef1f7;color:#3f5185;border-color:#c6cee3;border-left-style:solid}
|
||||
.origin-policy{background:#f6f0e5;color:#8a6524;border-color:#e0d0b0;border-left-style:double}
|
||||
.origin-unknown{background:#f4f5f6;color:#6b7480;border-color:#d6dade;border-left-style:dotted}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
import {useEffect, useState} from "react";
|
||||
import AgentNav from "@/components/xingbang/AgentNav";
|
||||
import ExperienceCenter from "@/components/xingbang/ExperienceCenter";
|
||||
import {messageOf} from "@/lib/prescreen";
|
||||
import "../data-access/workspace.css";
|
||||
|
||||
/** 历史经验加工中心(纠偏第二批 · BR-50 的"三个模块各有独立入口"之一)。
|
||||
*
|
||||
* BR-50 明确:不得只在旧页下叠加面板。因此这里是**独立路由**,不是工作台的第 7 个页签;
|
||||
* 但沿用同一套深色导航组件与 workspace.css,避免再造一套视觉。
|
||||
*
|
||||
* 本页只负责选项目与传请求助手;加工流程在 ExperienceCenter 内。
|
||||
*/
|
||||
export default function ExperienceCenterPage() {
|
||||
const [identity, setIdentity] = useState<any>(null);
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
const [projectId, setProjectId] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const headers = (json = true) => {
|
||||
const token = sessionStorage.getItem("xingbang-access-token");
|
||||
const role = sessionStorage.getItem("xingbang-demo-role");
|
||||
return {...(json ? {"Content-Type": "application/json"} : {}),
|
||||
...(token ? {Authorization: `Bearer ${token}`} : {}),
|
||||
...(role ? {"X-Xingbang-Demo-Role": role} : {})};
|
||||
};
|
||||
|
||||
async function send(method: string, path: string, body?: any) {
|
||||
const response = await fetch(`/api/risk-agent/project-assistant${path}`,
|
||||
{method, headers: headers(), body: body === undefined ? undefined : JSON.stringify(body)});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw Error(messageOf(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [context, list] = await Promise.all([
|
||||
fetch("/api/risk-agent/access/context", {headers: headers(false)}).then((r) => r.ok ? r.json() : null).catch(() => null),
|
||||
send("GET", "/projects"),
|
||||
]);
|
||||
setIdentity(context);
|
||||
setProjects(list.items || list || []);
|
||||
const requested = new URLSearchParams(location.search).get("project");
|
||||
const first = (list.items || list || [])[0];
|
||||
if (requested) setProjectId(requested);
|
||||
else if (first) setProjectId(first.id);
|
||||
} catch (e: any) {setError(e.message);} finally {setLoaded(true);}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const project = projects.find((item) => item.id === projectId) || null;
|
||||
return <div className="ra-app">
|
||||
<AgentNav active="experience" project={projectId || undefined} identity={identity || undefined}/>
|
||||
<main className="ra-main">
|
||||
<div className="ra-heading">
|
||||
<div>
|
||||
<div className="ra-eyebrow">EXPERIENCE / 历史经验加工</div>
|
||||
<h1>历史经验加工中心</h1>
|
||||
<p>从历史材料恢复时间线,用真实模型提炼核查方法,经专家修订与独立审核后成为可复用的知识卡片。提炼失败会如实显示原因,不会给出一个"看起来成功"的草稿。</p>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div role="alert" className="ra-alert error">{error}</div>}
|
||||
|
||||
{!loaded && <p>正在读取项目…</p>}
|
||||
{loaded && !projects.length && <div className="ra-empty">
|
||||
<h2>当前身份没有可访问的项目</h2>
|
||||
<p>历史经验加工以项目为上下文;请先在项目工作台确认身份与项目范围。</p>
|
||||
</div>}
|
||||
|
||||
{projects.length > 0 && <>
|
||||
<label className="ra-field"><span>加工哪个项目的历史过程</span>
|
||||
<select aria-label="选择项目" value={projectId}
|
||||
onChange={(e) => {setProjectId(e.target.value); history.replaceState({}, "", `?project=${encodeURIComponent(e.target.value)}`);}}>
|
||||
{projects.map((item) => <option key={item.id} value={item.id}>{item.id} · {item.customer || ""}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
{project && <ExperienceCenter key={project.id} project={project} identity={identity} send={send}/>}
|
||||
</>}
|
||||
</main>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user