fix(paper): 确认大纲后即时 seed 进度卡并逐节刷新

- 后端写稿循环不再落空 paper_progress 块(重载后出现空卡),改落开始撰写文本
- 前端 confirmPaper 时用大纲节标题本地 seed 进度卡,2s 轮询 patch 逐节状态
- 当前节按 current_section_id 标 writing(…动画)
This commit is contained in:
2026-10-08 17:10:50 +08:00
parent c923419d2d
commit d37e0d9c3a
3 changed files with 44 additions and 7 deletions
+3 -3
View File
@@ -232,9 +232,9 @@ async def run_writing_loop(db, paper_id: str, session_id: str) -> None:
from ...routers.docgen import insert_artifact # 惰性导入,避免服务层→路由层常驻耦合 from ...routers.docgen import insert_artifact # 惰性导入,避免服务层→路由层常驻耦合
client = _new_client() client = _new_client()
await _write_message(db, session_id, blocks=[ # 进度卡由前端确认时本地 seed + 2s 轮询 get_paper_progress 驱动(不落库,避免重载后出现
{"type": "paper_progress", "paper_id": paper_id, "status": "writing", "sections": [], "current_section_id": ""}, # 空的「撰写中」卡);后端只落「开始撰写」文本 + 逐节完成小结 + 最终 artifact。
], text="开始撰写") await _write_message(db, session_id, blocks=[_text_block("开始逐节撰写……")], text="开始撰写")
try: try:
while True: while True:
prog = await client.call_tool("get_paper_progress", {"paper_id": paper_id}) prog = await client.call_tool("get_paper_progress", {"paper_id": paper_id})
+2 -1
View File
@@ -118,6 +118,7 @@ def test_writing_loop_completes_to_artifact(bff_database, monkeypatch, fake_inse
assert svc.s1_done and svc.s2_done assert svc.s1_done and svc.s2_done
rows = _run(bff_database.fetchall("SELECT * FROM messages WHERE session_id = 's1' ORDER BY rowid")) rows = _run(bff_database.fetchall("SELECT * FROM messages WHERE session_id = 's1' ORDER BY rowid"))
assert "paper_progress" in rows[0]["blocks"] # 进度卡由前端确认时本地 seed + 轮询驱动,后端只落「开始撰写」文本 + 逐节小结 + artifact
assert "开始逐节撰写" in rows[0]["blocks"]
assert "artifact" in rows[-1]["blocks"] assert "artifact" in rows[-1]["blocks"]
assert "art-fake" in rows[-1]["blocks"] assert "art-fake" in rows[-1]["blocks"]
+39 -3
View File
@@ -1658,16 +1658,47 @@ export const useChatStore = defineStore('chat', {
this.scrollSeq++; this.scrollSeq++;
}, },
/** 确认大纲 → POST confirm 启动后台写节循环 → 2s 轮询进度 */ /** 确认大纲 → 本地先 seed 进度卡 → POST confirm 启动后台写节循环 → 2s 轮询进度 */
async confirmPaper(paperId) { async confirmPaper(paperId) {
const sessionId = this.sessionId; const sessionId = this.sessionId;
if (!paperId || !sessionId) return; if (!paperId || !sessionId) return;
this._seedPaperProgress(sessionId, paperId);
try { try {
await confirmPaperApi(paperId, sessionId); await confirmPaperApi(paperId, sessionId);
} catch { /* 启动失败:卡片可见错误 */ } } catch { /* 启动失败:卡片可见错误 */ }
this._startPaperPoll(paperId, sessionId); this._startPaperPoll(paperId, sessionId);
}, },
/** 确认后立即在消息流上一条 paper_progress 块(用大纲节标题做种子,全部 pending),
* 使 _startPaperPoll 的 _patchPaperProgress 有块可 patch,进度卡在撰写期间逐节刷新,
* 而不是等 complete 重载时一次性出现。 */
_seedPaperProgress(sessionId, paperId) {
const list = this.sessionMsgs[sessionId] || this.messages || [];
for (const m of list) {
if (!m || m.role !== 'assistant' || !Array.isArray(m.blocks)) continue;
if (m.blocks.some((b) => b && b.type === 'paper_progress' && b.paper_id === paperId)) return;
}
let sections = [];
for (let i = list.length - 1; i >= 0; i--) {
const m = list[i];
if (!m || m.role !== 'assistant' || !Array.isArray(m.blocks)) continue;
const ob = m.blocks.find((b) => b && b.type === 'paper_outline' && b.paper_id === paperId);
if (ob) {
sections = (Array.isArray(ob.sections) ? ob.sections : []).map((sec) => ({
section_id: sec.section_id, title: sec.title || '', status: 'pending',
}));
break;
}
}
const raw = {
id: null, role: 'assistant', kind: 'paper', status: 'done', text: null,
blocks: [{ type: 'paper_progress', paper_id: paperId, status: 'writing', sections, current_section_id: '' }],
raw: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(),
};
list.push(raw);
this.scrollSeq++;
},
_startPaperPoll(paperId, sessionId) { _startPaperPoll(paperId, sessionId) {
if (paperPolls.has(paperId)) return; if (paperPolls.has(paperId)) return;
const entry = { timer: null, sessionId }; const entry = { timer: null, sessionId };
@@ -1706,13 +1737,18 @@ export const useChatStore = defineStore('chat', {
/** 把进度同步进消息流里的 paper_progress 块 */ /** 把进度同步进消息流里的 paper_progress 块 */
_patchPaperProgress(sessionId, prog) { _patchPaperProgress(sessionId, prog) {
const list = this.sessionMsgs[sessionId] || []; const list = this.sessionMsgs[sessionId] || [];
const current = prog.current_section_id || '';
for (const m of list) { for (const m of list) {
if (!m || m.role !== 'assistant' || !Array.isArray(m.blocks)) continue; if (!m || m.role !== 'assistant' || !Array.isArray(m.blocks)) continue;
const pb = m.blocks.find((b) => b && b.type === 'paper_progress'); const pb = m.blocks.find((b) => b && b.type === 'paper_progress');
if (pb) { if (pb) {
pb.status = prog.status; pb.status = prog.status;
pb.current_section_id = prog.current_section_id || ''; pb.current_section_id = current;
pb.sections = (prog.sections || []).map((s) => ({ section_id: s.section_id, title: s.title, status: s.status })); // 后端节状态只有 pending/done;当前节用 current_section_id 标为 writing(…动画)
pb.sections = (prog.sections || []).map((sec) => ({
section_id: sec.section_id, title: sec.title,
status: (current && sec.section_id === current) ? 'writing' : sec.status,
}));
} }
} }
this.scrollSeq++; this.scrollSeq++;