diff --git a/sdk/app/services/paper/agent.py b/sdk/app/services/paper/agent.py index 316012c..4ab241e 100644 --- a/sdk/app/services/paper/agent.py +++ b/sdk/app/services/paper/agent.py @@ -232,9 +232,9 @@ async def run_writing_loop(db, paper_id: str, session_id: str) -> None: from ...routers.docgen import insert_artifact # 惰性导入,避免服务层→路由层常驻耦合 client = _new_client() - await _write_message(db, session_id, blocks=[ - {"type": "paper_progress", "paper_id": paper_id, "status": "writing", "sections": [], "current_section_id": ""}, - ], text="开始撰写") + # 进度卡由前端确认时本地 seed + 2s 轮询 get_paper_progress 驱动(不落库,避免重载后出现 + # 空的「撰写中」卡);后端只落「开始撰写」文本 + 逐节完成小结 + 最终 artifact。 + await _write_message(db, session_id, blocks=[_text_block("开始逐节撰写……")], text="开始撰写") try: while True: prog = await client.call_tool("get_paper_progress", {"paper_id": paper_id}) diff --git a/tests/paper/test_paper_bff_agent.py b/tests/paper/test_paper_bff_agent.py index f567881..60170dc 100644 --- a/tests/paper/test_paper_bff_agent.py +++ b/tests/paper/test_paper_bff_agent.py @@ -118,6 +118,7 @@ def test_writing_loop_completes_to_artifact(bff_database, monkeypatch, fake_inse assert svc.s1_done and svc.s2_done 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 "art-fake" in rows[-1]["blocks"] diff --git a/ui/src/stores/chat.js b/ui/src/stores/chat.js index 3a2815c..2e9d76e 100644 --- a/ui/src/stores/chat.js +++ b/ui/src/stores/chat.js @@ -1658,16 +1658,47 @@ export const useChatStore = defineStore('chat', { this.scrollSeq++; }, - /** 确认大纲 → POST confirm 启动后台写节循环 → 2s 轮询进度 */ + /** 确认大纲 → 本地先 seed 进度卡 → POST confirm 启动后台写节循环 → 2s 轮询进度 */ async confirmPaper(paperId) { const sessionId = this.sessionId; if (!paperId || !sessionId) return; + this._seedPaperProgress(sessionId, paperId); try { await confirmPaperApi(paperId, sessionId); } catch { /* 启动失败:卡片可见错误 */ } 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) { if (paperPolls.has(paperId)) return; const entry = { timer: null, sessionId }; @@ -1706,13 +1737,18 @@ export const useChatStore = defineStore('chat', { /** 把进度同步进消息流里的 paper_progress 块 */ _patchPaperProgress(sessionId, prog) { const list = this.sessionMsgs[sessionId] || []; + const current = prog.current_section_id || ''; for (const m of list) { if (!m || m.role !== 'assistant' || !Array.isArray(m.blocks)) continue; const pb = m.blocks.find((b) => b && b.type === 'paper_progress'); if (pb) { pb.status = prog.status; - pb.current_section_id = prog.current_section_id || ''; - pb.sections = (prog.sections || []).map((s) => ({ section_id: s.section_id, title: s.title, status: s.status })); + pb.current_section_id = current; + // 后端节状态只有 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++;