diff --git a/frontend/assets/app.js b/frontend/assets/app.js index 878557f..4ca5365 100644 --- a/frontend/assets/app.js +++ b/frontend/assets/app.js @@ -160,8 +160,15 @@ function renderMeetingStatus(meeting) { function refreshActionButtons() { const canProcess = Boolean(state.meetingId) && !state.processing && !state.guideBusy; + const currentMeeting = meetingById(state.meetingId); $("#btn-process").disabled = !canProcess; - $("#btn-process").textContent = state.processing ? "总结中" : state.guideBusy ? "等待" : "总结"; + $("#btn-process").textContent = state.processing + ? "总结中" + : state.guideBusy + ? "等待" + : currentMeeting?.has_summary + ? "重总结" + : "总结"; const canEditResult = Boolean(state.meetingId) && !state.processing && !state.guideBusy; $("#btn-toggle-result-edit").disabled = !canEditResult; @@ -268,6 +275,64 @@ function pushStandaloneReparseLine(text) { box.textContent = lines.slice(-6).join("\n"); } +async function streamTemplateGuideReparse(templateName, userNotes = "", handlers = {}) { + const params = new URLSearchParams(); + if (userNotes.trim()) { + params.set("user_notes", userNotes.trim()); + } + const suffix = params.toString() ? `?${params.toString()}` : ""; + const source = new EventSource( + `/api/templates/${encodeURIComponent(templateName)}/guide/reparse/stream${suffix}`, + ); + + try { + const result = await new Promise((resolve, reject) => { + let contentAcc = ""; + + source.onmessage = (event) => { + if (!event.data) { + return; + } + const payload = JSON.parse(event.data); + if (payload.type === "status") { + handlers.onStatus?.(payload.data); + return; + } + if (payload.type === "chunk") { + const chunk = payload.data?.text || ""; + if (chunk) { + contentAcc += chunk; + handlers.onChunk?.(chunk, contentAcc, payload.data); + } + return; + } + if (payload.type === "done") { + resolve(payload.data || { name: templateName, content: contentAcc }); + return; + } + if (payload.type === "error") { + reject(new Error(payload.data || "解析失败")); + } + }; + + source.onerror = () => { + reject(new Error("解析连接中断")); + }; + }); + + state.templates = state.templates.map((item) => ( + item.name === templateName ? { ...item, has_guide: true } : item + )); + if (state.rightResource?.name === templateName && state.rightResource.type === "template") { + state.rightResource.hasGuide = true; + } + await refreshRightResourceAfterGuideReparse(templateName, result.content || ""); + return result; + } finally { + source.close(); + } +} + async function runStandaloneGuideReparseFlow() { const templateName = state.templateName; const userNotes = $("#reparse-modal-notes").value || ""; @@ -1084,24 +1149,14 @@ async function reparseTemplateGuideFromModal(userNotes = "") { } async function runTemplateGuideReparse(templateName, userNotes = "") { - const params = new URLSearchParams(); - if (userNotes.trim()) { - params.set("user_notes", userNotes.trim()); - } - const suffix = params.toString() ? `?${params.toString()}` : ""; - const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse${suffix}`, { - method: "POST", + return streamTemplateGuideReparse(templateName, userNotes, { + onStatus() { + $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; + }, + onChunk(_chunk, contentAcc) { + $("#process-guide-editor").value = contentAcc; + }, }); - - state.templates = state.templates.map((item) => ( - item.name === templateName ? { ...item, has_guide: true } : item - )); - - if (state.rightResource?.name === templateName && state.rightResource.type === "template") { - state.rightResource.hasGuide = true; - } - await refreshRightResourceAfterGuideReparse(templateName, result.content || ""); - return result; } function startMeetingProcess(userNotes = "") { @@ -1225,12 +1280,24 @@ $("#btn-process-guide-edit").addEventListener("click", async () => { } }); -$("#btn-process-guide-reparse").addEventListener("click", () => { +$("#btn-process-guide-reparse").addEventListener("click", async () => { if (state.processing || state.guideBusy) { return; } - showReparseGuidePrompt(); + $("#btn-process-guide-reparse").disabled = true; + try { + if (state.processGuideEditMode) { + await saveProcessGuideFromModal(); + } + hideReparseGuidePrompt(); + closeModal("modal-process-guide"); + openStandaloneReparseModal(); + } catch (error) { + toast(error.message, "err"); + } finally { + $("#btn-process-guide-reparse").disabled = false; + } }); $("#btn-reparse-guide-cancel").addEventListener("click", () => { diff --git a/template_guides/template2.md b/template_guides/template2.md index 0538f63..e6a6c06 100644 --- a/template_guides/template2.md +++ b/template_guides/template2.md @@ -1,9 +1,10 @@ -- 严格保留模板的Markdown标题层级体系(# 总标题 > ## 内容主体 > ### 一级板块 > #### 发言人/细分板块),后续生成时不得随意更改层级关系。 -- 顶部元数据(议题、时间、地点、主持人、参加人)必须基于实际会议信息填充,所有占位符(X、XX、XXX等)需替换为真实数据,严禁原样输出。 -- “议程”区块需保留标题,其下列表项数量与内容严格按实际会议流程动态调整,无实际议程时省略该区块。 -- “汇报通报”区块需替换具体部门名称与量化数据,若会议记录中无汇报环节,则整体省略该小节,不强行补齐。 -- “领导部署/强调”区块需保留结构框架,将占位姓名替换为实际发言人;该区块下的发言人数量、业务分类标题(**XX方面:**)及下级详情列表需与真实讲话内容严格对应,动态增减,无依据时直接省略。 -- 所有示例性文字、演示性标题及占位内容必须在生成前全部替换为实际业务内容,禁止保留模板中的示例字符。 -- 使用水平分割线(---)作为区块分隔符,依次分隔头部信息、议程、汇报板块与部署板块,保持文档结构清晰。 -- 严格保留格式规范:领导发言使用有序列表(1. 2. 3.),业务分类使用加粗文本(**分类名称:**),具体指示或要点使用无序列表(- ),有内容时必须完整套用此层级格式。 -- 遵循“无依据不填充”原则,模板中未被实际会议内容支撑的空白项、占位项或示例项必须直接删除,不得虚构或强行对齐模板结构。 \ No newline at end of file +- 标题层级固定为 `# 会议记录` → `## 会议内容` → `### 章节标题` → `#### 发言人强调` → `1. **方面名称**` → `- 具体条目`,共六级层级,后续生成须严格保持该层级结构 +- 会议基本信息(议题、时间、地点、主持人、参加人、议程)为固定区块,信息必须完整保留,不可省略 +- 议程列表项为固定结构(各部门汇报、领导指示部署),顺序不可调整,但具体部门名称可按实际参会情况替换 +- 所有 `X`、`XX`、`XXX` 形式占位符须替换为真实内容,禁止原样输出 +- 所有数字占位符(如 `X项`、`XX项`)须替换为具体统计数字,禁止原样输出 +- 领导强调部分的结构(多个 `#### X总强调` 依次排列,每个下面分多个 `1. **方面**` 条目)为固定格式,每个领导强调至少包含一个方面,最多不超过四个 +- 方面名称(如“政企方面”“市场方面”“云数方面”等)使用 `**加粗**` 格式,后续生成须保持加粗 +- 具体部署内容使用 `-` 列表项,每方面至少一条,至多三条 +- 若某位领导未对某些方面作指示,该方面标题及条目应整体省略,不保留空结构 +- 模板中“详见汇报材料”等提示性文字在无对应材料时应删除,不强行保留 \ No newline at end of file diff --git a/web/__pycache__/server.cpython-314.pyc b/web/__pycache__/server.cpython-314.pyc index f487111..0d53700 100644 Binary files a/web/__pycache__/server.cpython-314.pyc and b/web/__pycache__/server.cpython-314.pyc differ diff --git a/web/server.py b/web/server.py index f52dbc8..175cb37 100644 --- a/web/server.py +++ b/web/server.py @@ -621,7 +621,12 @@ async def save_template_guide(name: str, payload: dict): @app.post("/api/templates/{name}/guide/reparse") async def reparse_template_guide(name: str, user_notes: str = ""): - content = _ensure_template_guide(name, force=True, user_notes=user_notes) + try: + content = _ensure_template_guide(name, force=True, user_notes=user_notes) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(500, str(exc) or "Template guide reparse failed") from exc return {"name": name, "content": content}