PlantAI-rag-new 首次提交:文献知识工作台

- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化
- Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab
- 四档响应式(桌面/笔记本/平板/手机)、深浅主题
- 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测
- 真实凭据已脱敏,运行时配置在 backend/.env(不入 git)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jerry
2026-08-25 20:47:52 +08:00
co-authored by Claude Fable 5
commit 1e94de7464
171 changed files with 26291 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
// deploy/m2_probe5.js —— PipelineCard props 与渲染链探针(PM 使用,在远端运行)
// 读组件实例 props(vnode.props.block)并比较与 pinia store 的引用/状态。
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await page.goto('http://127.0.0.1:5180', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.app-shell', { timeout: 15000 });
await page.click('.new-task-btn');
await page.waitForSelector('.composer textarea', { timeout: 15000 });
await page.fill('.composer textarea', 'SPL gene family in rice');
await page.click('.send-btn');
const t0 = Date.now();
let last = '';
while (Date.now() - t0 < 150000) {
const snap = await page.evaluate(() => {
const cardEl = document.querySelector('.pipeline-card');
if (!cardEl) return { noCard: true };
// 沿 vnode 链找组件实例
let vnode = cardEl.__vueParentComponent;
const found = [];
for (let i = 0; i < 6 && vnode; i++) {
const p = vnode.props;
if (p && p.block) {
const b = p.block;
found.push({
comp: vnode.type?.name || vnode.type?.__name || 'anon',
blockStatus: b.status,
steps: (b.steps || []).map((s) => `${s.phase}:${s.state}`).join(','),
isReactiveProxy: !!b.__v_raw || typeof b.status !== 'string' || false,
});
// 读原始 store 对象比较(raw)
const raw = b.__v_raw || b;
found[found.length - 1].rawStatus = raw.status;
found[found.length - 1].rawSteps = (raw.steps || []).map((s) => s.state).join(',');
break;
}
vnode = vnode.parent;
}
// pinia 直接读
let storeBlock = null;
const rootEl = document.querySelector('#app') || document.querySelector('.app-shell');
let n = rootEl;
let app = null;
while (n && !app) { if (n.__vue_app__) app = n.__vue_app__; n = n.__vueParentComponent?.parent || null; }
if (app) {
const pinia = app.config.globalProperties.$pinia;
const chat = pinia._s.get('chat');
const aMsg = [...chat.messages].reverse().find((m) => m.role === 'assistant');
const sb = aMsg?.blocks?.[0];
if (sb) storeBlock = { status: sb.status, steps: sb.steps.map((s) => s.state).join(',') };
}
return { found, storeBlock, domText: cardEl.textContent.slice(0, 60) };
});
const key = JSON.stringify(snap);
if (key !== last) {
last = key;
console.log(`T+${Math.round((Date.now() - t0) / 1000)}s`, JSON.stringify(snap, null, 1));
}
// 流结束后看最终状态
const doneNow = await page.evaluate(() => {
const rootEl = document.querySelector('#app');
let n = rootEl; let app = null;
while (n && !app) { if (n.__vue_app__) app = n.__vue_app__; n = n.__vueParentComponent?.parent || null; }
if (!app) return false;
return !app.config.globalProperties.$pinia._s.get('chat').busy;
});
if (doneNow && Date.now() - t0 > 30000) break;
await new Promise((r) => setTimeout(r, 2500));
}
await browser.close();
})();