重新整理处理过程逻辑,避免相互占用

new_test
Bifang 2026-05-09 17:26:00 +08:00
parent f0add35030
commit e0608e5459
8 changed files with 295 additions and 307 deletions

View File

@ -1,49 +1,59 @@
{ {
"sub_topics": [ "sub_topics": [
{ {
"title": "宽带装维进度与关键指标汇报", "title": "宽带业务上门量、转化率与退单管控",
"time_interval": "00:00:01 - 00:02:06", "time_interval": "00:00:01 - 00:03:51",
"overview": "会议首先汇报了宽带装维上门量与安装进度受天气影响当周上门量约580户累计进度35%距离3000户目标仍存差距。同时通报弱光指数为0.51目标0.5、三代终端目标压降至5.5FPTR指标已达标主动过境指标相对靠后。" "overview": "会议通报了宽带安装与上门维护的整体进度累计上门量达标但距3000户目标仍有差距。关键指标方面弱光率降至0.51接近目标三代终端压降改善月度转化率87.35%接近90%目标退单率控制在6.53%。主要退单因素为用户原因、天气影响及审核流程待优化需加强B2C协调与一线审批意识。"
}, },
{ {
"title": "九零工程转化与退单率分析", "title": "PCDN专线质量恶化与学校出口限速机制",
"time_interval": "00:02:07 - 00:03:51", "time_interval": "00:03:53 - 00:05:31",
"overview": "针对九零工程转化率当前87.35%目标90%与退单率当前6.53%目标低于7.5%进行拆解指出天气、用户原因及覆盖问题是影响转化与退单的主要因素。会议提出需优化B to C协调流程加强退单审核意识并将该指标纳入综合运维考核。" "overview": "会议重点讨论了PCDN专线流量持续恶化问题主要集中于两所学校开学后的出口带宽占用。目前市公司已介入分析涉及学校的IP地址正协同校方建立发现超限直接限速的管控机制。该问题因学校互联网带宽免费导致管控难度大需加快限速策略落地以保障网络质量。"
}, },
{ {
"title": "专线护航、PCDN治理与基站运维", "title": "无线网络质量优化与专线护航巡检进展",
"time_interval": "00:03:53 - 00:08:04", "time_interval": "00:05:33 - 00:07:56",
"overview": "重点汇报118条专线巡检进度已完成56条剩余17条月底清零并指出PCDN在学校端持续恶化正协同学校进行IP限速分析。同时通报超频基站故障已恢复当周收到报障33件网络相关9件以光缆问题为主。" "overview": "网络质量方面物业点检测因天线问题延期至4月初唱厅工单需待智能体质检结束5G基站正通过参数调整试点解决漏话问题。超频站点故障已及时恢复旁站故障率低。专线护航巡检方面118条存量专线已巡检56条未验收工程45条全量巡检预计月底可按时完成全量巡检任务。"
}, },
{ {
"title": "二级基站拆除与网络质量评估体系落地", "title": "当周故障通报与二级站拆除及网络评估体系",
"time_interval": "00:08:07 - 00:10:00", "time_interval": "00:07:57 - 00:09:35",
"overview": "通报二级基站拆除进展剩余121站中已拆除46站预计4月15日前完成剩余75站拆除及电费调整。会议提及网络质量综合评估体系已内部下发下周将分解制定目标并同步推进花果山项目与微托无人机验收等2025年剩余工作。" "overview": "当周共收报障33件其中9件网络相关故障以光缆问题为主。重点推进金沙管家二级站拆除工作剩余75站预计两周内完成下电及电费调整目标4月15日前全量完成。此外内部已收到网络质量综合评估体系文件下周将分解制定具体目标。"
}, },
{ {
"title": "综合部重点工作与后勤保障安排", "title": "综合部重点工作推进与跨部门协同事项",
"time_interval": "00:10:01 - 00:14:53", "time_interval": "00:09:46 - 00:12:39",
"overview": "汇报上周完成5项工作含框架清单、投资评估、收入地图等及7项推进中工作重点说明因内外部打印价格差异大拟采用外派打印保障招投标。同时通报工会经费压减背景下推进食堂改造约29.8万元)与自饮机引入,并筹备第四届体育文化节。" "overview": "综合部汇报了25年剩余两项工作进展建委框架清单与投资计划已汇报完成终端占比问题已制定五项措施。会议同步推进数据中心包装、基建宣传、渠道产能评估及资产优化等7项工作部分事项因兼职或流程原因仍在梳理方案预计本周内完成专题汇报。"
}, },
{ {
"title": "主题教育开展与招待费管控通报", "title": "工会经费管理、食堂改造与员工文体活动筹备",
"time_interval": "00:14:54 - 00:22:49", "time_interval": "00:12:40 - 00:18:54",
"overview": "通报主题教育第一期简报发布及基层党组织学习情况完成年度招待费信息公开报备。分析2025年招待费超年初预算14%其中综合部去年超支107%2026年已调整预算。会议要求政企部统筹对外招待避免过度依赖单一客户经理并加快历史费用报销。" "overview": "工会经费因上级压减需严考严用已公示并上报软性工程食堂改造预算约29.8万元正进行审计与方案优化。同时规划下半年引入自饮机节约成本,并全年组织六场大型体育活动。针对周五第四届体育文化节,需各部门抽调人员补充方阵缺额,确定人员后将统一购衣并于周中排练。"
}, },
{ {
"title": "区级评优申报推进与商客市场经营汇报", "title": "党建主题教育、招待费管控与区级荣誉申报",
"time_interval": "00:22:50 - 00:31:43", "time_interval": "00:18:55 - 00:27:30",
"overview": "跟进河川区担当作为评优申报流程明确需对接区委办与人力社保局确认资格截止3月31日。商客市场方面2月营收88.5万环比增1万价值拓展完成115但个别商客经理周基础业务仅2笔需加强签约落实与价值拓展。" "overview": "主题教育一期简报已发布基层党组织学习已按期完成招待费已按年度向管理层公示并报备纪检。25年招待费整体超预算14%,综合部超支严重,政企部需统筹对外招待避免客户经理个人承担。此外,正对接河川区担当作为集体/个人评选,因评选偏向政府机构且流程尚不明确,建议先摸清内部意向再决定是否申报。"
}, },
{ {
"title": "考核指标督办与市场满意度整改要求", "title": "商客市场收入拓展与社区/单位拆迁营销进展",
"time_interval": "00:31:44 - 00:42:25", "time_interval": "00:30:23 - 00:32:04",
"overview": "强调工作回复需量化且日清日结要求下周内提交“上课”指标具体保障措施。针对满意度测评结果批评相关部门思想松懈要求市场部本周内汇报招聘、农村渠道及营销方案进度并严格落实“80/20两率”3月底目标每日微信报送日报。" "overview": "商客市场2月收入88.5万元较上期增加1万元超改、城北和南京改增幅超2万元价值拓展完成115户。拆迁营销主要聚焦通货场景价值提升已完成1145户改造通过社区清洗与单位集中服务相结合推进本周开展5场活动但签约转化率未达预期需重点加强落实签约板块。"
}, },
{ {
"title": "运动会筹备与年度考核工作部署", "title": "建委评估体系落实与工作执行纪律要求",
"time_interval": "00:32:15 - 00:35:22",
"overview": "会议强调需严格落实建委今年综合评估体系,要求各项回复工作做到日清日结、量化结果。针对当前部分经理回复滞后、工作拖延的问题,会议严肃指出必须养成及时汇报与跟进的习惯,未完成任务需主动沟通困难。同时要求针对上课指标拿出具体可行方案,结合专线助账客单位业务推进,确保指标达成。"
},
{
"title": "市场部季度收官、满意度测评整改与指标管控",
"time_interval": "00:35:23 - 00:42:25",
"overview": "市场部需提前谋划二季度业务活动避免淡季疲软并要求本周内汇报招聘、农村渠道进度及西消队伍建设方案。针对满意度测评结果不佳会议批评了主管思想松懈问题强调需按四公司规定打造满意客户样板对不满客户利用时间节点和技巧进行干预。要求商务部每日微信发送日报并摸细KPI考核规则利用可控操作动作平衡指标。"
},
{
"title": "运动会安排、年度考核摸排与执行力强调",
"time_interval": "00:42:26 - 00:46:16", "time_interval": "00:42:26 - 00:46:16",
"overview": "确认体育文化节方阵人员调配与排练安排,强调执行力与结果导向。领导着重部署四季度年度考核工作,要求各部门提前摸排考核规则(如阿普提升等指标),主动与市公司沟通对齐,避免起跑落后,统筹资源确保年度考核公平达标。" "overview": "运动会筹备工作已明确时间节点,分管领导出差期间需提前视频汇报。会议重点强调四公司年度考核的提前摸排,要求各部门主动了解可能影响考核的指标变化,避免被动。领导指出当前部分工作存在知道怎么做却不去做的作风问题,要求各级人员强化执行力,确保年度考核起跑线公平,争取通过努力改善成绩。"
} }
] ]
} }

View File

@ -2,56 +2,45 @@
# 会议记录 # 会议记录
议 题合川分公司周例会2026年第19期) 议 题合川分公司周例会2026年第X期)
时 间2026年5月6日13:37—14:23 时 间2026年5月6日 13:37
地 点:分公司会议室
主持人AlanPaine 主持人AlanPaine
参加人:分公司领导、各部门经理及相关人员 议程:各部门工作汇报、重点工作部署与执行纪律要求
议程:
一、各部门汇报
二、分公司领导指示部署
--- ---
## 会议内容 ## 会议内容
### 一、市场部、政企部、建维部、综合部︱党群纪检部按议程现场按顺序做汇报。综合部按照领导部署通报周例会领导部署工作推进完成情况2026年重点工作跟踪本周完结5项综合部牵头完成5项待跟进20项详见汇报材料。 ### 一、各部门汇报
**建维部与网络质量管控:** - **网络/建维方面:** 宽带安装累计上门量达标但距3000户目标仍有差距弱光率降至0.51三代终端压降改善月度转化率87.35%目标90%退单率6.53%PCDN专线流量恶化集中于两所学校正协同市公司分析IP并建立超限限速机制5G基站通过参数调整试点解决漏话超频站点及旁站故障已及时恢复118条存量专线及45条未验收工程已全量巡检预计月底按时完成当周报障33件9件网络相关以光缆为主金沙管家二级站剩余75站预计两周内完成下电及电费调整网络质量综合评估体系已下发下周分解目标。
- **宽带与上门量:** 受天气影响上周上门量580户累计进度距3000户目标略靠后弱光值改善至0.51逼近0.5目标三代终端压降至5.5FPTR达标主动过境率0.3需提升。 - **综合部方面:** 建委框架清单、投资计划及终端占比措施已汇报完成数据中心包装、基建宣传、渠道产能评估等7项工作正梳理方案工会经费压减需严考严用食堂改造预算29.8万元正审计下半年拟引入自饮机第四届体育文化节方阵缺9人需各部门抽调周中排练主题教育一期简报已发基层党组织学习完成招待费已按年度公示报备整体超预算14%,综合部超支明显;河川区担当作为集体/个人评选正对接,建议摸清意向后再申报。
- **九零工程转化与退单:** 月度转化率87.35%目标90%退单率6.53%主因用户原因29单及天气。需优化B2C协调流程建议引入一线审批提单机制。 - **市场/商客方面:** 商客市场2月收入88.5万元较上期增1万元价值拓展完成115户拆迁营销聚焦通货场景价值提升已完成1145户本周开展5场社区/单位活动但签约转化率未达预期;满意度测评结果不佳,主管存在思想松懈问题;商务部需本周内汇报招聘、农村渠道进度及西消队伍建设方案。
- **PCDN专线与基站维护** 专线指标恶化集中于两所学校已上报市公司分析出口IP拟建立快速发现与限速机制。超频基站3个已查明原因并恢复。专线护航118条月底完成全量巡检。当周报障33件网络相关9件主要为光缆。金沙管家二级站点剩余75站2周内拆除下电4月15日前完成服务费调整。
**综合部与行政后勤:**
- **项目推进与后勤:** 25项剩余工作已完成5项含施工司管清单、投资计划评估等。解决打印机价格偏差问题对接外部保障招投标打印。工会经费压减食堂改造约29.8万元待审计。
- **招待费通报:** 2025年整体招待费超年初预算14%综合部去年超支107%已调整2026年预算。
- **区级评选申报:** 区委办与人社局联合开展担当作为集体/个人评选,需确认申报资格与内部推荐名额,建议先与委办及人社局经办领导沟通,避免盲目申报。
**市场部与政企部:**
- **拆迁与商客市场:** 三期拆迁已完成1145户通过二次扩容提升低价值用户转化。商客2月收入88.5万元环比增1万价值拓展完成115但个别经理基础业务薄弱需督导。
- **满意度与考核准备:** 满意度测评近期为拉分项负责人需亲自抓。KPI考核将挂钩工信部有责及离网率需每日跟进并制定管控动作。
--- ---
### 二、分公司领导指示部署 ### 二、部署强调
#### 会议领导强调: #### 建维及综合分管领导强调:
1. **作风建设与执行力:** 1. **工作纪律与执行反馈:**
- 严抓工作拖延现象,全员须养成“日清日结”习惯,工作回复必须量化、有结果、有措施、有成效。 - 各项回复工作必须做到日清日结、量化结果,严禁拖延或敷衍,未完成任务需主动沟通困难并推进。
- 杜绝“有方法无动作”的作风,部门经理及一线人员需强化执行力,事项不落实不罢休。 - 针对“上课”指标,需结合专线助账客单位业务发展拿出具体可行方案,明确责任人确保达成。
2. **资源统筹与合规管理:**
- 招待费使用需统筹规范,政企部对外招待应合理分摊,避免由个别客户经理个人承担,严禁历史费用拖欠。
- 河川区荣誉申报需先摸清内部意向与流程,确认有推荐名额后再行申报,避免资源浪费。
2. **业务指标整改与保障:** ---
- 上课指标与专线助账客业务责任人须于当日提交具体可行的保障方案,结合专线助账客单位发展确保指标达成。
- 3月底为满意度测评关键节点须严格按市公司规定打造样板。针对不满客户需利用5:30等时间节点技巧管控。KPI明确挂钩工信部有责及离网率市场部需每日微信日报跟进。
3. **预算统筹与年度考核预警:** #### 市场及考核分管领导强调:
- 政企部须统筹对外招待安排,避免费用过度集中于个别客户经理,真实发生费用需及时结算报销。
- 各部门需提前细查四公司年度考核指标,对可能产生重大影响的考核偏差或风险点必须提前预警上报。政企部与市场部需加强沟通力度,必要时向上协调,确保考核公平合理,避免起跑线吃亏。 1. **市场收官与满意度整改:**
- 市场部需提前谋划二季度业务活动克服淡季疲软思想全面提振签约、商客及AI军团等业务活动声势。
- 满意度工作必须落实到位,主管需亲自抓,严格按四公司规定打造满意客户样板;对不满客户需利用时间节点和技巧进行干预,商务部每日需发送日报。
- 摸细KPI考核规则工信部有责指标及网率考核利用可控的操作动作平衡指标确保考核成绩。
2. **年度考核与执行力建设:**
- 提前摸排四公司年度考核指标变化,主动了解可能影响考核的病毒、卡位点或政策调整,提前沟通争取有利条件,避免被动。
- 严厉批评“知道怎么做却不去做”的作风,要求各级管理人员及一线人员强化执行力,确保年度考核起跑线公平,通过努力改善成绩。

View File

@ -7,6 +7,7 @@ const state = {
templateName: "template1.md", templateName: "template1.md",
templates: [], templates: [],
processing: false, processing: false,
guideBusy: false,
resultEditMode: false, resultEditMode: false,
rightEditMode: false, rightEditMode: false,
selectedTreeKey: "", selectedTreeKey: "",
@ -91,10 +92,22 @@ function meetingById(meetingId) {
return state.meetings.find((item) => item.id === meetingId) || null; return state.meetings.find((item) => item.id === meetingId) || null;
} }
function templateMetaByName(name) {
return state.templates.find((item) => item.name === name) || null;
}
function isMarkdownFile(name = "") { function isMarkdownFile(name = "") {
return name.toLowerCase().endsWith(".md"); return name.toLowerCase().endsWith(".md");
} }
function setStatus(side, isBusy, text = "空闲") {
const light = $(`#${side}-status-light`);
const textEl = $(`#${side}-status-text`);
light.classList.toggle("idle", !isBusy);
light.classList.toggle("busy", isBusy);
textEl.textContent = text;
}
function renderMeetingStatus(meeting) { function renderMeetingStatus(meeting) {
const name = $("#sidebar-meeting-name"); const name = $("#sidebar-meeting-name");
const meta = $("#sidebar-meeting-meta"); const meta = $("#sidebar-meeting-meta");
@ -124,39 +137,36 @@ function renderMeetingStatus(meeting) {
topicsBadge.className = meeting.has_topics ? "badge" : "badge muted"; topicsBadge.className = meeting.has_topics ? "badge" : "badge muted";
} }
function setRightStatus(isBusy, text = "空闲") { function refreshActionButtons() {
const light = $("#side-status-light"); const canProcess = Boolean(state.meetingId) && !state.processing && !state.guideBusy;
const textEl = $("#side-status-text"); $("#btn-process").disabled = !canProcess;
light.classList.toggle("idle", !isBusy); $("#btn-process").textContent = state.processing ? "总结中" : state.guideBusy ? "等待" : "总结";
light.classList.toggle("busy", isBusy);
textEl.textContent = text;
}
function resetRightStream() { const canEditResult = Boolean(state.meetingId) && !state.processing && !state.guideBusy;
$("#side-status-stream").textContent = ""; $("#btn-toggle-result-edit").disabled = !canEditResult;
}
function updateRightStream(text) { const resource = state.rightResource;
const lines = text.replace(/\r\n/g, "\n").split("\n").filter((line) => line.trim() !== ""); const canEditSide = Boolean(resource?.editable) && !state.processing && !state.guideBusy;
$("#side-status-stream").textContent = lines.slice(-2).join("\n"); $("#btn-toggle-side-edit").disabled = !canEditSide;
updateGuideButton(resource);
} }
function resetProcessingStream() { function resetProcessingStream() {
$("#stream-box").style.display = "none"; $("#stream-box").style.display = "none";
$("#stream-title").textContent = ""; $("#stream-title").textContent = "";
$("#stream-content").textContent = ""; $("#stream-content").textContent = "";
setRightStatus(false, "空闲"); setStatus("left", false, "空闲");
resetRightStream();
} }
function showResultEmpty() { function showResultEmpty() {
state.resultEditMode = false; state.resultEditMode = false;
$("#btn-toggle-result-edit").disabled = true;
$("#btn-toggle-result-edit").textContent = "编辑"; $("#btn-toggle-result-edit").textContent = "编辑";
$("#result-editor").style.display = "none"; $("#result-editor").style.display = "none";
$("#result-md").style.display = "none"; $("#result-md").style.display = "none";
$("#processing-indicator").hidden = true; $("#processing-indicator").hidden = true;
$("#result-empty").hidden = false; $("#result-empty").hidden = false;
refreshActionButtons();
} }
function setResultEditMode(editMode) { function setResultEditMode(editMode) {
@ -173,8 +183,8 @@ function showResult(markdown) {
$("#result-editor").value = markdown; $("#result-editor").value = markdown;
$("#result-md").innerHTML = marked.parse(markdown || ""); $("#result-md").innerHTML = marked.parse(markdown || "");
$("#result-md").scrollTop = 0; $("#result-md").scrollTop = 0;
$("#btn-toggle-result-edit").disabled = !state.meetingId;
setResultEditMode(false); setResultEditMode(false);
refreshActionButtons();
} }
function showProcessingView() { function showProcessingView() {
@ -182,7 +192,6 @@ function showProcessingView() {
$("#result-editor").style.display = "none"; $("#result-editor").style.display = "none";
$("#result-md").style.display = "none"; $("#result-md").style.display = "none";
$("#processing-indicator").hidden = false; $("#processing-indicator").hidden = false;
$("#btn-toggle-result-edit").disabled = true;
} }
function setSelectedTreeKey(key) { function setSelectedTreeKey(key) {
@ -201,8 +210,8 @@ async function setCurrentMeeting(meetingId) {
}); });
state.meetingId = data.active_meeting_id; state.meetingId = data.active_meeting_id;
renderMeetingStatus(data.meeting); renderMeetingStatus(data.meeting);
$("#btn-process").disabled = !state.meetingId || state.processing;
setSelectedTreeKey(state.selectedTreeKey); setSelectedTreeKey(state.selectedTreeKey);
refreshActionButtons();
} }
function renderNode(node, parent, depth) { function renderNode(node, parent, depth) {
@ -341,7 +350,7 @@ async function loadMeetingSummary(meetingId) {
} }
async function selectMeeting(meetingId) { async function selectMeeting(meetingId) {
if (state.processing) { if (state.processing || state.guideBusy) {
return; return;
} }
await setCurrentMeeting(meetingId); await setCurrentMeeting(meetingId);
@ -383,30 +392,32 @@ function syncTemplateSelection(name) {
function updateGuideButton(resource) { function updateGuideButton(resource) {
const button = $("#btn-reparse-guide"); const button = $("#btn-reparse-guide");
if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) { const isTemplateResource = resource && (resource.type === "template" || resource.type === "template-guide");
if (!isTemplateResource) {
button.disabled = true; button.disabled = true;
button.textContent = "解析"; button.textContent = "解析";
return; return;
} }
const hasGuide = resource.type === "template-guide" || Boolean(resource.hasGuide); const hasGuide = resource.type === "template-guide" || Boolean(resource.hasGuide);
button.disabled = false; button.textContent = state.guideBusy ? (hasGuide ? "重解析中" : "解析中") : hasGuide ? "重解析" : "解析";
button.textContent = hasGuide ? "重解析" : "解析"; button.disabled = state.guideBusy || state.processing;
} }
function applyRightResource(resource) { function applyRightResource(resource) {
state.rightResource = resource; state.rightResource = resource;
$("#editor-resource-label").textContent = `当前资源:${resource.label}`; $("#editor-resource-label").textContent = `当前资源:${resource.label}`;
$("#side-editor").value = resource.content || ""; $("#side-editor").value = resource.content || "";
$("#btn-toggle-side-edit").disabled = !resource.editable;
$("#btn-toggle-side-edit").textContent = resource.editable ? "编辑" : "只读"; $("#btn-toggle-side-edit").textContent = resource.editable ? "编辑" : "只读";
updateGuideButton(resource);
state.rightEditMode = false; state.rightEditMode = false;
renderSidePreview(resource); renderSidePreview(resource);
if (resource.type === "template" || resource.type === "template-guide") { if (resource.type === "template" || resource.type === "template-guide") {
syncTemplateSelection(resource.templateName || resource.name); syncTemplateSelection(resource.templateName || resource.name);
} }
refreshActionButtons();
} }
function setRightEditMode(editMode) { function setRightEditMode(editMode) {
@ -502,7 +513,7 @@ async function openResultFile(meetingId, filename, treeKey) {
} }
async function openTreeResource(path) { async function openTreeResource(path) {
if (state.processing) { if (state.processing || state.guideBusy) {
return; return;
} }
@ -685,7 +696,6 @@ async function refresh() {
await loadTree(); await loadTree();
renderMeetingStatus(meetingById(state.meetingId)); renderMeetingStatus(meetingById(state.meetingId));
$("#btn-process").disabled = !state.meetingId || state.processing;
if (state.meetingId) { if (state.meetingId) {
try { try {
@ -703,37 +713,60 @@ async function refresh() {
templateData.some((item) => item.name === state.rightResource.name) templateData.some((item) => item.name === state.rightResource.name)
) { ) {
await openTemplate(state.rightResource.name, state.rightResource.treeKey); await openTemplate(state.rightResource.name, state.rightResource.treeKey);
return; } else if (
}
if (
state.rightResource && state.rightResource &&
state.rightResource.type === "template-guide" && state.rightResource.type === "template-guide" &&
templateData.some((item) => item.name === state.rightResource.templateName) templateData.some((item) => item.name === state.rightResource.templateName)
) { ) {
await openTemplateGuide(state.rightResource.name, state.rightResource.treeKey); await openTemplateGuide(state.rightResource.name, state.rightResource.treeKey);
return; } else if (!state.rightResource && state.templateName) {
}
if (!state.rightResource && state.templateName) {
await openTemplate(state.templateName); await openTemplate(state.templateName);
} }
refreshActionButtons();
} }
$("#btn-process").addEventListener("click", () => { async function ensureTemplateGuideBeforeProcess() {
if (!state.meetingId || state.processing) { const templateMeta = templateMetaByName(state.templateName);
if (templateMeta?.has_guide) {
return; return;
} }
toast("当前模板还没有解析说明,先为你解析模板。");
state.guideBusy = true;
setStatus("left", true, "解析中");
refreshActionButtons();
try {
const result = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide/reparse`, {
method: "POST",
});
state.templates = state.templates.map((item) => (
item.name === state.templateName ? { ...item, has_guide: true } : item
));
if (
state.rightResource &&
state.rightResource.templateName === result.name &&
state.rightResource.type === "template"
) {
state.rightResource.hasGuide = true;
refreshActionButtons();
}
} finally {
state.guideBusy = false;
setStatus("left", false, "空闲");
refreshActionButtons();
}
}
function startMeetingProcess() {
state.processing = true; state.processing = true;
$("#btn-process").disabled = true; setStatus("left", true, "总结中");
$("#btn-process").textContent = "处理中"; refreshActionButtons();
showProcessingView(); showProcessingView();
$("#stream-box").style.display = "block"; $("#stream-box").style.display = "block";
$("#stream-title").textContent = "第一阶段:结构化主题..."; $("#stream-title").textContent = "第一阶段:结构化主题...";
$("#stream-content").textContent = ""; $("#stream-content").textContent = "";
setRightStatus(true, "工作中");
resetRightStream();
const source = new EventSource( const source = new EventSource(
`/api/meetings/${state.meetingId}/process?template_name=${encodeURIComponent(state.templateName)}`, `/api/meetings/${state.meetingId}/process?template_name=${encodeURIComponent(state.templateName)}`,
@ -758,7 +791,6 @@ $("#btn-process").addEventListener("click", () => {
$("#stream-title").textContent = "第二阶段:生成会议总结..."; $("#stream-title").textContent = "第二阶段:生成会议总结...";
streamAcc = ""; streamAcc = "";
$("#stream-content").textContent = ""; $("#stream-content").textContent = "";
resetRightStream();
} }
return; return;
} }
@ -767,7 +799,6 @@ $("#btn-process").addEventListener("click", () => {
const { data } = payload; const { data } = payload;
streamAcc += data.text || ""; streamAcc += data.text || "";
$("#stream-content").textContent = streamAcc.replace(/\r\n/g, "\n").split("\n").slice(-4).join("\n"); $("#stream-content").textContent = streamAcc.replace(/\r\n/g, "\n").split("\n").slice(-4).join("\n");
updateRightStream(streamAcc);
if (data.stage === 2 && data.chunk_type === "content") { if (data.stage === 2 && data.chunk_type === "content") {
resultAcc += data.text || ""; resultAcc += data.text || "";
} }
@ -777,21 +808,18 @@ $("#btn-process").addEventListener("click", () => {
if (payload.type === "done") { if (payload.type === "done") {
source.close(); source.close();
state.processing = false; state.processing = false;
$("#btn-process").disabled = false;
$("#btn-process").textContent = "处理";
showResult(payload.data?.result || resultAcc || ""); showResult(payload.data?.result || resultAcc || "");
await refresh(); toast("会议总结完成");
toast("会议处理完成"); refreshActionButtons();
return; return;
} }
if (payload.type === "error") { if (payload.type === "error") {
source.close(); source.close();
state.processing = false; state.processing = false;
$("#btn-process").disabled = false;
$("#btn-process").textContent = "处理";
resetProcessingStream(); resetProcessingStream();
$("#processing-indicator").hidden = true; $("#processing-indicator").hidden = true;
refreshActionButtons();
toast(`处理失败:${payload.data}`, "err"); toast(`处理失败:${payload.data}`, "err");
} }
}; };
@ -799,16 +827,32 @@ $("#btn-process").addEventListener("click", () => {
source.onerror = () => { source.onerror = () => {
source.close(); source.close();
state.processing = false; state.processing = false;
$("#btn-process").disabled = false;
$("#btn-process").textContent = "处理";
resetProcessingStream(); resetProcessingStream();
$("#processing-indicator").hidden = true; $("#processing-indicator").hidden = true;
refreshActionButtons();
toast("处理连接中断", "err"); toast("处理连接中断", "err");
}; };
}
$("#btn-process").addEventListener("click", async () => {
if (!state.meetingId || state.processing || state.guideBusy) {
return;
}
try {
await ensureTemplateGuideBeforeProcess();
startMeetingProcess();
} catch (error) {
state.processing = false;
state.guideBusy = false;
setStatus("left", false, "空闲");
refreshActionButtons();
toast(error.message, "err");
}
}); });
$("#btn-toggle-result-edit").addEventListener("click", async () => { $("#btn-toggle-result-edit").addEventListener("click", async () => {
if (!state.meetingId) { if (!state.meetingId || state.processing || state.guideBusy) {
return; return;
} }
@ -825,13 +869,12 @@ $("#btn-toggle-result-edit").addEventListener("click", async () => {
body: JSON.stringify({ content }), body: JSON.stringify({ content }),
}); });
showResult(content); showResult(content);
$("#btn-toggle-result-edit").disabled = false;
toast("结果已保存"); toast("结果已保存");
}); });
$("#btn-toggle-side-edit").addEventListener("click", async () => { $("#btn-toggle-side-edit").addEventListener("click", async () => {
const resource = state.rightResource; const resource = state.rightResource;
if (!resource || !resource.editable) { if (!resource || !resource.editable || state.processing || state.guideBusy) {
return; return;
} }
@ -865,8 +908,8 @@ $("#btn-toggle-side-edit").addEventListener("click", async () => {
resource.content = content; resource.content = content;
setRightEditMode(false); setRightEditMode(false);
$("#btn-toggle-side-edit").disabled = false;
toast("资源已保存"); toast("资源已保存");
refreshActionButtons();
}); });
$("#tpl-select").addEventListener("change", async (event) => { $("#tpl-select").addEventListener("change", async (event) => {
@ -879,21 +922,29 @@ $("#btn-reparse-guide").addEventListener("click", async () => {
if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) { if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) {
return; return;
} }
if (state.processing || state.guideBusy) {
return;
}
const templateName = resource.templateName || resource.name; const templateName = resource.templateName || resource.name;
$("#btn-reparse-guide").disabled = true; state.guideBusy = true;
setRightStatus(true, "工作中"); setStatus("right", true, "解析中");
resetRightStream(); refreshActionButtons();
try {
const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse`, { const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse`, {
method: "POST", method: "POST",
}); });
state.templates = state.templates.map((item) => (
setRightStatus(false, "空闲"); item.name === templateName ? { ...item, has_guide: true } : item
$("#btn-reparse-guide").disabled = false; ));
toast(`说明已更新:${templateName}`); toast(`说明已更新:${templateName}`);
await refresh();
await openTemplateGuide(result.name); await openTemplateGuide(result.name);
} finally {
state.guideBusy = false;
setStatus("right", false, "空闲");
refreshActionButtons();
}
}); });
$("#btn-import").addEventListener("click", () => { $("#btn-import").addEventListener("click", () => {
@ -978,8 +1029,8 @@ document.addEventListener("DOMContentLoaded", async () => {
applySavedLayout(); applySavedLayout();
initResize("gutter-1", "sidebar", "result-panel"); initResize("gutter-1", "sidebar", "result-panel");
initResize("gutter-2", "result-panel", "template-panel"); initResize("gutter-2", "result-panel", "template-panel");
setRightStatus(false, "空闲"); setStatus("left", false, "空闲");
resetRightStream(); setStatus("right", false, "空闲");
try { try {
await refresh(); await refresh();

View File

@ -4,7 +4,6 @@
--panel: rgba(255, 255, 255, 0.94); --panel: rgba(255, 255, 255, 0.94);
--panel-strong: #ffffff; --panel-strong: #ffffff;
--line: #c7dcf8; --line: #c7dcf8;
--line-strong: #9cc3f5;
--text: #16324f; --text: #16324f;
--muted: #5f7f9f; --muted: #5f7f9f;
--accent: #2f80ed; --accent: #2f80ed;
@ -12,8 +11,6 @@
--accent-soft: #e8f2ff; --accent-soft: #e8f2ff;
--ok-bg: #e9f7f1; --ok-bg: #e9f7f1;
--ok-text: #19744e; --ok-text: #19744e;
--danger-bg: #fdecec;
--danger-text: #b53f4e;
--shadow: 0 18px 48px rgba(47, 128, 237, 0.12); --shadow: 0 18px 48px rgba(47, 128, 237, 0.12);
--radius: 18px; --radius: 18px;
--font: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; --font: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
@ -51,69 +48,6 @@ textarea {
font: inherit; font: inherit;
} }
.inline-label {
color: var(--muted);
font-size: 13px;
}
.side-status {
margin-top: 6px;
display: grid;
gap: 4px;
}
.side-status-row {
display: flex;
align-items: center;
gap: 8px;
}
.side-status-label,
.side-status-meta {
color: var(--muted);
font-size: 13px;
}
.side-status-text {
font-size: 13px;
color: var(--text);
}
.status-light {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
flex: 0 0 auto;
}
.status-light.idle {
background: #2db36c;
box-shadow: 0 0 0 4px rgba(45, 179, 108, 0.14);
}
.status-light.busy {
width: 52px;
height: 10px;
border-radius: 999px;
background:
linear-gradient(90deg, rgba(47, 128, 237, 0.18), rgba(47, 128, 237, 0.55), rgba(47, 128, 237, 0.18));
background-size: 200% 100%;
box-shadow: inset 0 0 0 1px rgba(47, 128, 237, 0.12);
animation: status-marquee 1.2s linear infinite;
}
.side-status-stream {
margin: 2px 0 0;
min-height: 2.9em;
max-width: 100%;
color: #557cad;
white-space: pre-wrap;
line-height: 1.45;
font-size: 12px;
font-family: var(--mono);
}
.app-shell { .app-shell {
height: var(--app-h); height: var(--app-h);
padding: 24px; padding: 24px;
@ -231,6 +165,54 @@ textarea {
flex-wrap: wrap; flex-wrap: wrap;
} }
.status-block {
margin-top: 6px;
display: grid;
gap: 4px;
}
.status-row {
display: flex;
align-items: center;
gap: 8px;
}
.status-label,
.status-meta,
.inline-label {
color: var(--muted);
font-size: 13px;
}
.status-text {
font-size: 13px;
color: var(--text);
}
.status-light {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
flex: 0 0 auto;
}
.status-light.idle {
background: #2db36c;
box-shadow: 0 0 0 4px rgba(45, 179, 108, 0.14);
}
.status-light.busy {
width: 52px;
height: 10px;
border-radius: 999px;
background:
linear-gradient(90deg, rgba(47, 128, 237, 0.18), rgba(47, 128, 237, 0.55), rgba(47, 128, 237, 0.18));
background-size: 200% 100%;
box-shadow: inset 0 0 0 1px rgba(47, 128, 237, 0.12);
animation: status-marquee 1.2s linear infinite;
}
.panel-body { .panel-body {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
@ -362,12 +344,8 @@ select.btn {
border-radius: 999px; border-radius: 999px;
font-size: 12px; font-size: 12px;
border: 1px solid transparent; border: 1px solid transparent;
}
.badge {
background: var(--ok-bg); background: var(--ok-bg);
color: var(--ok-text); color: var(--ok-text);
border-color: rgba(25, 116, 78, 0.12);
} }
.badge.muted { .badge.muted {
@ -481,21 +459,6 @@ select.btn {
font-weight: 700; font-weight: 700;
} }
.spinner {
width: 38px;
height: 38px;
border: 3px solid rgba(47, 128, 237, 0.16);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.stream-box { .stream-box {
position: relative; position: relative;
width: min(620px, calc(100% - 40px)); width: min(620px, calc(100% - 40px));
@ -525,29 +488,6 @@ select.btn {
font-weight: 700; font-weight: 700;
color: var(--accent-strong); color: var(--accent-strong);
background: linear-gradient(180deg, rgba(232, 242, 255, 0.92), rgba(248, 252, 255, 0.98)); background: linear-gradient(180deg, rgba(232, 242, 255, 0.92), rgba(248, 252, 255, 0.98));
overflow: hidden;
}
.stream-title::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: -100%;
width: 100%;
background: linear-gradient(
90deg,
rgba(47, 128, 237, 0) 0%,
rgba(125, 182, 255, 0.12) 16%,
rgba(125, 182, 255, 0.28) 38%,
rgba(125, 182, 255, 0.36) 50%,
rgba(125, 182, 255, 0.28) 62%,
rgba(125, 182, 255, 0.12) 84%,
rgba(47, 128, 237, 0) 100%
);
transform: none;
animation: title-marquee 2.2s linear infinite;
pointer-events: none;
} }
.stream-content, .stream-content,
@ -569,49 +509,12 @@ select.btn {
background: rgba(251, 253, 255, 0.96); background: rgba(251, 253, 255, 0.96);
} }
@keyframes marquee-glow {
from {
left: -35%;
}
to {
left: 100%;
}
}
@keyframes title-marquee {
0% {
left: -100%;
}
62% {
left: 100%;
}
100% {
left: 100%;
}
}
#result-md, #result-md,
#side-preview, #side-preview,
#side-plain-preview, #side-plain-preview,
#result-editor, #result-editor,
#side-editor { #side-editor {
padding: 18px 20px; padding: 18px 20px;
}
@keyframes status-marquee {
from {
background-position: 200% 0;
}
to {
background-position: 0 0;
}
}
#result-md,
#side-preview,
#side-plain-preview,
#result-editor,
#side-editor {
display: none; display: none;
} }
@ -735,6 +638,24 @@ select.btn {
transform: translateY(0); transform: translateY(0);
} }
@keyframes marquee-glow {
from {
left: -35%;
}
to {
left: 100%;
}
}
@keyframes status-marquee {
from {
background-position: 200% 0;
}
to {
background-position: 0 0;
}
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
html, html,
body { body {

View File

@ -44,17 +44,24 @@
<div class="panel-header"> <div class="panel-header">
<div class="panel-heading"> <div class="panel-heading">
<span>会议结果</span> <span>会议结果</span>
<small id="selected-meeting-tip">未选择会议</small> <div class="status-block">
<div class="status-row">
<span class="status-label">当前状态</span>
<span class="status-light idle" id="left-status-light"></span>
<span class="status-text" id="left-status-text">空闲</span>
</div>
<div class="status-meta" id="selected-meeting-tip">未选择会议</div>
</div>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<button class="btn primary sm" id="btn-process" disabled>处理</button> <button class="btn primary sm" id="btn-process" disabled>总结</button>
<button class="btn sm" id="btn-toggle-result-edit" disabled>编辑</button> <button class="btn sm" id="btn-toggle-result-edit" disabled>编辑</button>
</div> </div>
</div> </div>
<div class="panel-body panel-scroll" id="result-body"> <div class="panel-body panel-scroll" id="result-body">
<div id="result-empty" class="empty-state"> <div id="result-empty" class="empty-state">
<div class="empty-icon">M</div> <div class="empty-icon">M</div>
<p>选择会议后即可查看结果,或直接开始处理</p> <p>选择会议后即可查看结果,或直接开始总结</p>
</div> </div>
<div id="processing-indicator" class="processing" hidden> <div id="processing-indicator" class="processing" hidden>
<div class="stream-box" id="stream-box"> <div class="stream-box" id="stream-box">
@ -73,14 +80,13 @@
<div class="panel-header"> <div class="panel-header">
<div class="panel-heading"> <div class="panel-heading">
<span>右侧编辑区</span> <span>右侧编辑区</span>
<div class="side-status" id="side-status"> <div class="status-block">
<div class="side-status-row"> <div class="status-row">
<span class="side-status-label">当前状态</span> <span class="status-label">当前状态</span>
<span class="status-light idle" id="side-status-light"></span> <span class="status-light idle" id="right-status-light"></span>
<span class="side-status-text" id="side-status-text">空闲</span> <span class="status-text" id="right-status-text">空闲</span>
</div> </div>
<div class="side-status-meta" id="editor-resource-label">当前资源:模板</div> <div class="status-meta" id="editor-resource-label">当前资源:模板</div>
<pre class="side-status-stream" id="side-status-stream"></pre>
</div> </div>
</div> </div>
<div class="toolbar"> <div class="toolbar">

View File

@ -1,9 +1,8 @@
- 严格保留模板的Markdown标题层级结构`#`→`##`→`###`→`####`),不得跨级或打乱顺序。 - 严格保留模板的Markdown标题层级与宏观结构# 会议记录 -> ## 会议内容 -> ### 各部门汇报/部署强调),不得增删或调整主标题顺序。
- 模板头部的基础信息字段(议题、时间、地点、主持人、参加人)必须保留,其中的 `X`、`XX`、`XXX` 及示例年份/公司名称需替换为真实会议信息。 - 模板中的“X”、“XX”、“XXX”及具体年份、部门名均为占位示例生成时必须替换为会议真实数据与名称若实际会议未涉及某字段则直接省略该条目不强行补齐。
- “议程”部分保留固定条目结构,内容替换为实际讨论顺序,若无议程变动可维持原结构。 - “各部门汇报”小节需按实际汇报顺序提炼核心内容与进度数据,替换原长句模板;若无汇报内容,可省略该小节。
- “会议内容”下的部门汇报部分需保留进度通报与数据跟踪的句式结构,替换其中的部门名称与量化数据占位符,无实际汇报内容时省略该小节。 - “部署强调”部分采用“#### [领导职务/姓名]强调”作为独立标题块多个领导发言时按实际顺序并列生成替换示例中的“X总”。
- “部署强调”部分必须保留 `#### [发言人/领导]强调:` 的标题格式严禁保留示例中的“X总”。 - 领导强调下按实际业务领域划分子项(格式为“数字. **领域:**”),子项数量与领域名称严格按实际讲话内容动态增减,不得照搬模板示例的固定领域列表。
- 严格保留领导发言下的列表嵌套格式:使用数字编号(`1.`)引导业务方向,方向名称加粗(`**XX方面**`),其下使用短横线(`- `)缩进列出具体的发言要点或行动项。 - 各领域下的具体指示需使用无序列表(`- `逐条列出替换示例“XXX”保持原有缩进层级确保要点清晰。
- 所有占位符(如 `XXX`)及模板示例文本在生成时必须替换为真实会议内容,严禁原样输出占位或演示文字。 - 所有仅用于演示结构的说明性文字(如“按议程现场按顺序做汇报”“详见汇报材料”等)必须删除,仅输出实际会议提炼的实质内容。
- “部署强调”板块数量及每个领导下的业务方向子项数量应随实际会议内容动态增减,无依据支撑的空白项、示例分组必须省略,不强行补齐。 - 严格遵循模板原有的排版分隔逻辑,各主要模块间保留“---”分隔线,维持整体版式节奏。
- 保留各主要章节间的 `---` 分隔线,用于清晰划分汇报环节与领导部署区块。

View File

@ -37,6 +37,8 @@ TEMPLATE_GUIDE_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="Meeting Summary Web") app = FastAPI(title="Meeting Summary Web")
app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets") app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets")
_template_lock_guard = threading.Lock()
_template_locks: dict[str, threading.Lock] = {}
def _load_config() -> dict: def _load_config() -> dict:
@ -118,6 +120,13 @@ def _guide_path(template_name: str) -> Path:
return _resolve_child(TEMPLATE_GUIDE_DIR, template_name) return _resolve_child(TEMPLATE_GUIDE_DIR, template_name)
def _get_template_lock(template_name: str) -> threading.Lock:
with _template_lock_guard:
if template_name not in _template_locks:
_template_locks[template_name] = threading.Lock()
return _template_locks[template_name]
def _collect_llm_content(client, model, system_prompt: str, user_prompt: str, max_token: int = 64000) -> str: def _collect_llm_content(client, model, system_prompt: str, user_prompt: str, max_token: int = 64000) -> str:
content = [] content = []
for chunk_type, chunk_content in _llm_stream(client, model, system_prompt, user_prompt, max_token=max_token): for chunk_type, chunk_content in _llm_stream(client, model, system_prompt, user_prompt, max_token=max_token):
@ -143,6 +152,8 @@ def _ensure_template_guide(template_name: str, *, force: bool = False, cfg: dict
if not template_path.exists(): if not template_path.exists():
raise HTTPException(404, f"Template not found: {template_name}") raise HTTPException(404, f"Template not found: {template_name}")
lock = _get_template_lock(template_name)
with lock:
guide_path = _guide_path(template_name) guide_path = _guide_path(template_name)
if guide_path.exists() and not force: if guide_path.exists() and not force:
return guide_path.read_text(encoding="utf-8") return guide_path.read_text(encoding="utf-8")
@ -445,6 +456,7 @@ async def save_template_guide(name: str, payload: dict):
template_path = _resolve_child(TEMPLATE_DIR, name) template_path = _resolve_child(TEMPLATE_DIR, name)
if not template_path.exists(): if not template_path.exists():
raise HTTPException(404, f"Template not found: {name}") raise HTTPException(404, f"Template not found: {name}")
with _get_template_lock(name):
_guide_path(name).write_text(content, encoding="utf-8") _guide_path(name).write_text(content, encoding="utf-8")
return {"ok": True} return {"ok": True}