重新整理处理过程逻辑,避免相互占用
parent
f0add35030
commit
e0608e5459
|
|
@ -1,49 +1,59 @@
|
|||
{
|
||||
"sub_topics": [
|
||||
{
|
||||
"title": "宽带装维进度与关键指标汇报",
|
||||
"time_interval": "00:00:01 - 00:02:06",
|
||||
"overview": "会议首先汇报了宽带装维上门量与安装进度,受天气影响当周上门量约580户,累计进度35%,距离3000户目标仍存差距。同时通报弱光指数为0.51(目标0.5)、三代终端目标压降至5.5,FPTR指标已达标,主动过境指标相对靠后。"
|
||||
"title": "宽带业务上门量、转化率与退单管控",
|
||||
"time_interval": "00:00:01 - 00:03:51",
|
||||
"overview": "会议通报了宽带安装与上门维护的整体进度,累计上门量达标但距3000户目标仍有差距。关键指标方面,弱光率降至0.51接近目标,三代终端压降改善,月度转化率87.35%接近90%目标,退单率控制在6.53%。主要退单因素为用户原因、天气影响及审核流程待优化,需加强B2C协调与一线审批意识。"
|
||||
},
|
||||
{
|
||||
"title": "九零工程转化与退单率分析",
|
||||
"time_interval": "00:02:07 - 00:03:51",
|
||||
"overview": "针对九零工程转化率(当前87.35%,目标90%)与退单率(当前6.53%,目标低于7.5%)进行拆解,指出天气、用户原因及覆盖问题是影响转化与退单的主要因素。会议提出需优化B to C协调流程,加强退单审核意识,并将该指标纳入综合运维考核。"
|
||||
"title": "PCDN专线质量恶化与学校出口限速机制",
|
||||
"time_interval": "00:03:53 - 00:05:31",
|
||||
"overview": "会议重点讨论了PCDN专线流量持续恶化问题,主要集中于两所学校开学后的出口带宽占用。目前市公司已介入分析涉及学校的IP地址,正协同校方建立发现超限直接限速的管控机制。该问题因学校互联网带宽免费导致管控难度大,需加快限速策略落地以保障网络质量。"
|
||||
},
|
||||
{
|
||||
"title": "专线护航、PCDN治理与基站运维",
|
||||
"time_interval": "00:03:53 - 00:08:04",
|
||||
"overview": "重点汇报118条专线巡检进度(已完成56条,剩余17条月底清零),并指出PCDN在学校端持续恶化,正协同学校进行IP限速分析。同时通报超频基站故障已恢复,当周收到报障33件(网络相关9件,以光缆问题为主)。"
|
||||
"title": "无线网络质量优化与专线护航巡检进展",
|
||||
"time_interval": "00:05:33 - 00:07:56",
|
||||
"overview": "网络质量方面,物业点检测因天线问题延期至4月初,唱厅工单需待智能体质检结束,5G基站正通过参数调整试点解决漏话问题。超频站点故障已及时恢复,旁站故障率低。专线护航巡检方面,118条存量专线已巡检56条,未验收工程45条全量巡检,预计月底可按时完成全量巡检任务。"
|
||||
},
|
||||
{
|
||||
"title": "二级基站拆除与网络质量评估体系落地",
|
||||
"time_interval": "00:08:07 - 00:10:00",
|
||||
"overview": "通报二级基站拆除进展,剩余121站中已拆除46站,预计4月15日前完成剩余75站拆除及电费调整。会议提及网络质量综合评估体系已内部下发,下周将分解制定目标,并同步推进花果山项目与微托无人机验收等2025年剩余工作。"
|
||||
"title": "当周故障通报与二级站拆除及网络评估体系",
|
||||
"time_interval": "00:07:57 - 00:09:35",
|
||||
"overview": "当周共收报障33件,其中9件网络相关故障以光缆问题为主。重点推进金沙管家二级站拆除工作,剩余75站预计两周内完成下电及电费调整,目标4月15日前全量完成。此外,内部已收到网络质量综合评估体系文件,下周将分解制定具体目标。"
|
||||
},
|
||||
{
|
||||
"title": "综合部重点工作与后勤保障安排",
|
||||
"time_interval": "00:10:01 - 00:14:53",
|
||||
"overview": "汇报上周完成5项工作(含框架清单、投资评估、收入地图等)及7项推进中工作,重点说明因内外部打印价格差异大,拟采用外派打印保障招投标。同时通报工会经费压减背景下,推进食堂改造(约29.8万元)与自饮机引入,并筹备第四届体育文化节。"
|
||||
"title": "综合部重点工作推进与跨部门协同事项",
|
||||
"time_interval": "00:09:46 - 00:12:39",
|
||||
"overview": "综合部汇报了25年剩余两项工作进展,建委框架清单与投资计划已汇报完成,终端占比问题已制定五项措施。会议同步推进数据中心包装、基建宣传、渠道产能评估及资产优化等7项工作,部分事项因兼职或流程原因仍在梳理方案,预计本周内完成专题汇报。"
|
||||
},
|
||||
{
|
||||
"title": "主题教育开展与招待费管控通报",
|
||||
"time_interval": "00:14:54 - 00:22:49",
|
||||
"overview": "通报主题教育第一期简报发布及基层党组织学习情况,完成年度招待费信息公开报备。分析2025年招待费超年初预算14%,其中综合部去年超支107%,2026年已调整预算。会议要求政企部统筹对外招待,避免过度依赖单一客户经理,并加快历史费用报销。"
|
||||
"title": "工会经费管理、食堂改造与员工文体活动筹备",
|
||||
"time_interval": "00:12:40 - 00:18:54",
|
||||
"overview": "工会经费因上级压减需严考严用,已公示并上报软性工程,食堂改造预算约29.8万元正进行审计与方案优化。同时规划下半年引入自饮机节约成本,并全年组织六场大型体育活动。针对周五第四届体育文化节,需各部门抽调人员补充方阵缺额,确定人员后将统一购衣并于周中排练。"
|
||||
},
|
||||
{
|
||||
"title": "区级评优申报推进与商客市场经营汇报",
|
||||
"time_interval": "00:22:50 - 00:31:43",
|
||||
"overview": "跟进河川区担当作为评优申报流程,明确需对接区委办与人力社保局确认资格,截止3月31日。商客市场方面,2月营收88.5万(环比增1万),价值拓展完成115,但个别商客经理周基础业务仅2笔,需加强签约落实与价值拓展。"
|
||||
"title": "党建主题教育、招待费管控与区级荣誉申报",
|
||||
"time_interval": "00:18:55 - 00:27:30",
|
||||
"overview": "主题教育一期简报已发布,基层党组织学习已按期完成,招待费已按年度向管理层公示并报备纪检。25年招待费整体超预算14%,综合部超支严重,政企部需统筹对外招待避免客户经理个人承担。此外,正对接河川区担当作为集体/个人评选,因评选偏向政府机构且流程尚不明确,建议先摸清内部意向再决定是否申报。"
|
||||
},
|
||||
{
|
||||
"title": "考核指标督办与市场满意度整改要求",
|
||||
"time_interval": "00:31:44 - 00:42:25",
|
||||
"overview": "强调工作回复需量化且日清日结,要求下周内提交“上课”指标具体保障措施。针对满意度测评结果批评相关部门思想松懈,要求市场部本周内汇报招聘、农村渠道及营销方案进度,并严格落实“80/20两率”3月底目标,每日微信报送日报。"
|
||||
"title": "商客市场收入拓展与社区/单位拆迁营销进展",
|
||||
"time_interval": "00:30:23 - 00:32:04",
|
||||
"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",
|
||||
"overview": "确认体育文化节方阵人员调配与排练安排,强调执行力与结果导向。领导着重部署四季度年度考核工作,要求各部门提前摸排考核规则(如阿普提升等指标),主动与市公司沟通对齐,避免起跑落后,统筹资源确保年度考核公平达标。"
|
||||
"overview": "运动会筹备工作已明确时间节点,分管领导出差期间需提前视频汇报。会议重点强调四公司年度考核的提前摸排,要求各部门主动了解可能影响考核的指标变化,避免被动。领导指出当前部分工作存在知道怎么做却不去做的作风问题,要求各级人员强化执行力,确保年度考核起跑线公平,争取通过努力改善成绩。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2,56 +2,45 @@
|
|||
|
||||
# 会议记录
|
||||
|
||||
议 题:合川分公司周例会(2026年第19期)
|
||||
议 题:合川分公司周例会(2026年第X期)
|
||||
|
||||
时 间:2026年5月6日13:37—14:23
|
||||
|
||||
地 点:分公司会议室
|
||||
时 间:2026年5月6日 13:37
|
||||
|
||||
主持人:AlanPaine
|
||||
|
||||
参加人:分公司领导、各部门经理及相关人员
|
||||
|
||||
议程:
|
||||
|
||||
一、各部门汇报
|
||||
|
||||
二、分公司领导指示部署
|
||||
议程:各部门工作汇报、重点工作部署与执行纪律要求
|
||||
|
||||
---
|
||||
|
||||
## 会议内容
|
||||
|
||||
### 一、市场部、政企部、建维部、综合部︱党群纪检部按议程现场按顺序做汇报。综合部按照领导部署通报周例会领导部署工作推进完成情况:2026年重点工作跟踪本周完结5项(综合部牵头完成5项),待跟进20项,详见汇报材料。
|
||||
### 一、各部门汇报
|
||||
|
||||
**建维部与网络质量管控:**
|
||||
- **宽带与上门量:** 受天气影响上周上门量580户,累计进度距3000户目标略靠后;弱光值改善至0.51(逼近0.5目标),三代终端压降至5.5,FPTR达标,主动过境率0.3需提升。
|
||||
- **九零工程转化与退单:** 月度转化率87.35%(目标90%),退单率6.53%,主因用户原因(29单)及天气。需优化B2C协调流程,建议引入一线审批提单机制。
|
||||
- **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考核将挂钩工信部有责及离网率,需每日跟进并制定管控动作。
|
||||
- **网络/建维方面:** 宽带安装累计上门量达标但距3000户目标仍有差距;弱光率降至0.51,三代终端压降改善,月度转化率87.35%(目标90%),退单率6.53%;PCDN专线流量恶化集中于两所学校,正协同市公司分析IP并建立超限限速机制;5G基站通过参数调整试点解决漏话,超频站点及旁站故障已及时恢复;118条存量专线及45条未验收工程已全量巡检,预计月底按时完成;当周报障33件(9件网络相关,以光缆为主);金沙管家二级站剩余75站预计两周内完成下电及电费调整;网络质量综合评估体系已下发,下周分解目标。
|
||||
- **综合部方面:** 建委框架清单、投资计划及终端占比措施已汇报完成;数据中心包装、基建宣传、渠道产能评估等7项工作正梳理方案;工会经费压减需严考严用,食堂改造预算29.8万元正审计,下半年拟引入自饮机;第四届体育文化节方阵缺9人,需各部门抽调,周中排练;主题教育一期简报已发,基层党组织学习完成;招待费已按年度公示报备,整体超预算14%,综合部超支明显;河川区担当作为集体/个人评选正对接,建议摸清意向后再申报。
|
||||
- **市场/商客方面:** 商客市场2月收入88.5万元,较上期增1万元,价值拓展完成115户;拆迁营销聚焦通货场景价值提升,已完成1145户,本周开展5场社区/单位活动但签约转化率未达预期;满意度测评结果不佳,主管存在思想松懈问题;商务部需本周内汇报招聘、农村渠道进度及西消队伍建设方案。
|
||||
|
||||
---
|
||||
|
||||
### 二、分公司领导指示部署
|
||||
### 二、部署强调
|
||||
|
||||
#### 会议领导强调:
|
||||
#### 建维及综合分管领导强调:
|
||||
|
||||
1. **作风建设与执行力:**
|
||||
- 严抓工作拖延现象,全员须养成“日清日结”习惯,工作回复必须量化、有结果、有措施、有成效。
|
||||
- 杜绝“有方法无动作”的作风,部门经理及一线人员需强化执行力,事项不落实不罢休。
|
||||
1. **工作纪律与执行反馈:**
|
||||
- 各项回复工作必须做到日清日结、量化结果,严禁拖延或敷衍,未完成任务需主动沟通困难并推进。
|
||||
- 针对“上课”指标,需结合专线助账客单位业务发展拿出具体可行方案,明确责任人确保达成。
|
||||
2. **资源统筹与合规管理:**
|
||||
- 招待费使用需统筹规范,政企部对外招待应合理分摊,避免由个别客户经理个人承担,严禁历史费用拖欠。
|
||||
- 河川区荣誉申报需先摸清内部意向与流程,确认有推荐名额后再行申报,避免资源浪费。
|
||||
|
||||
2. **业务指标整改与保障:**
|
||||
- 上课指标与专线助账客业务责任人须于当日提交具体可行的保障方案,结合专线助账客单位发展确保指标达成。
|
||||
- 3月底为满意度测评关键节点,须严格按市公司规定打造样板。针对不满客户需利用5:30等时间节点技巧管控。KPI明确挂钩工信部有责及离网率,市场部需每日微信日报跟进。
|
||||
---
|
||||
|
||||
3. **预算统筹与年度考核预警:**
|
||||
- 政企部须统筹对外招待安排,避免费用过度集中于个别客户经理,真实发生费用需及时结算报销。
|
||||
- 各部门需提前细查四公司年度考核指标,对可能产生重大影响的考核偏差或风险点必须提前预警上报。政企部与市场部需加强沟通力度,必要时向上协调,确保考核公平合理,避免起跑线吃亏。
|
||||
#### 市场及考核分管领导强调:
|
||||
|
||||
1. **市场收官与满意度整改:**
|
||||
- 市场部需提前谋划二季度业务活动,克服淡季疲软思想,全面提振签约、商客及AI军团等业务活动声势。
|
||||
- 满意度工作必须落实到位,主管需亲自抓,严格按四公司规定打造满意客户样板;对不满客户需利用时间节点和技巧进行干预,商务部每日需发送日报。
|
||||
- 摸细KPI考核规则(工信部有责指标及网率考核),利用可控的操作动作平衡指标,确保考核成绩。
|
||||
2. **年度考核与执行力建设:**
|
||||
- 提前摸排四公司年度考核指标变化,主动了解可能影响考核的病毒、卡位点或政策调整,提前沟通争取有利条件,避免被动。
|
||||
- 严厉批评“知道怎么做却不去做”的作风,要求各级管理人员及一线人员强化执行力,确保年度考核起跑线公平,通过努力改善成绩。
|
||||
|
|
@ -7,6 +7,7 @@ const state = {
|
|||
templateName: "template1.md",
|
||||
templates: [],
|
||||
processing: false,
|
||||
guideBusy: false,
|
||||
resultEditMode: false,
|
||||
rightEditMode: false,
|
||||
selectedTreeKey: "",
|
||||
|
|
@ -91,10 +92,22 @@ function meetingById(meetingId) {
|
|||
return state.meetings.find((item) => item.id === meetingId) || null;
|
||||
}
|
||||
|
||||
function templateMetaByName(name) {
|
||||
return state.templates.find((item) => item.name === name) || null;
|
||||
}
|
||||
|
||||
function isMarkdownFile(name = "") {
|
||||
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) {
|
||||
const name = $("#sidebar-meeting-name");
|
||||
const meta = $("#sidebar-meeting-meta");
|
||||
|
|
@ -124,39 +137,36 @@ function renderMeetingStatus(meeting) {
|
|||
topicsBadge.className = meeting.has_topics ? "badge" : "badge muted";
|
||||
}
|
||||
|
||||
function setRightStatus(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 refreshActionButtons() {
|
||||
const canProcess = Boolean(state.meetingId) && !state.processing && !state.guideBusy;
|
||||
$("#btn-process").disabled = !canProcess;
|
||||
$("#btn-process").textContent = state.processing ? "总结中" : state.guideBusy ? "等待" : "总结";
|
||||
|
||||
function resetRightStream() {
|
||||
$("#side-status-stream").textContent = "";
|
||||
}
|
||||
const canEditResult = Boolean(state.meetingId) && !state.processing && !state.guideBusy;
|
||||
$("#btn-toggle-result-edit").disabled = !canEditResult;
|
||||
|
||||
function updateRightStream(text) {
|
||||
const lines = text.replace(/\r\n/g, "\n").split("\n").filter((line) => line.trim() !== "");
|
||||
$("#side-status-stream").textContent = lines.slice(-2).join("\n");
|
||||
const resource = state.rightResource;
|
||||
const canEditSide = Boolean(resource?.editable) && !state.processing && !state.guideBusy;
|
||||
$("#btn-toggle-side-edit").disabled = !canEditSide;
|
||||
|
||||
updateGuideButton(resource);
|
||||
}
|
||||
|
||||
function resetProcessingStream() {
|
||||
$("#stream-box").style.display = "none";
|
||||
$("#stream-title").textContent = "";
|
||||
$("#stream-content").textContent = "";
|
||||
setRightStatus(false, "空闲");
|
||||
resetRightStream();
|
||||
setStatus("left", false, "空闲");
|
||||
}
|
||||
|
||||
function showResultEmpty() {
|
||||
state.resultEditMode = false;
|
||||
$("#btn-toggle-result-edit").disabled = true;
|
||||
$("#btn-toggle-result-edit").textContent = "编辑";
|
||||
$("#result-editor").style.display = "none";
|
||||
$("#result-md").style.display = "none";
|
||||
$("#processing-indicator").hidden = true;
|
||||
$("#result-empty").hidden = false;
|
||||
refreshActionButtons();
|
||||
}
|
||||
|
||||
function setResultEditMode(editMode) {
|
||||
|
|
@ -173,8 +183,8 @@ function showResult(markdown) {
|
|||
$("#result-editor").value = markdown;
|
||||
$("#result-md").innerHTML = marked.parse(markdown || "");
|
||||
$("#result-md").scrollTop = 0;
|
||||
$("#btn-toggle-result-edit").disabled = !state.meetingId;
|
||||
setResultEditMode(false);
|
||||
refreshActionButtons();
|
||||
}
|
||||
|
||||
function showProcessingView() {
|
||||
|
|
@ -182,7 +192,6 @@ function showProcessingView() {
|
|||
$("#result-editor").style.display = "none";
|
||||
$("#result-md").style.display = "none";
|
||||
$("#processing-indicator").hidden = false;
|
||||
$("#btn-toggle-result-edit").disabled = true;
|
||||
}
|
||||
|
||||
function setSelectedTreeKey(key) {
|
||||
|
|
@ -201,8 +210,8 @@ async function setCurrentMeeting(meetingId) {
|
|||
});
|
||||
state.meetingId = data.active_meeting_id;
|
||||
renderMeetingStatus(data.meeting);
|
||||
$("#btn-process").disabled = !state.meetingId || state.processing;
|
||||
setSelectedTreeKey(state.selectedTreeKey);
|
||||
refreshActionButtons();
|
||||
}
|
||||
|
||||
function renderNode(node, parent, depth) {
|
||||
|
|
@ -341,7 +350,7 @@ async function loadMeetingSummary(meetingId) {
|
|||
}
|
||||
|
||||
async function selectMeeting(meetingId) {
|
||||
if (state.processing) {
|
||||
if (state.processing || state.guideBusy) {
|
||||
return;
|
||||
}
|
||||
await setCurrentMeeting(meetingId);
|
||||
|
|
@ -383,30 +392,32 @@ function syncTemplateSelection(name) {
|
|||
|
||||
function updateGuideButton(resource) {
|
||||
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.textContent = "解析";
|
||||
return;
|
||||
}
|
||||
|
||||
const hasGuide = resource.type === "template-guide" || Boolean(resource.hasGuide);
|
||||
button.disabled = false;
|
||||
button.textContent = hasGuide ? "重解析" : "解析";
|
||||
button.textContent = state.guideBusy ? (hasGuide ? "重解析中" : "解析中") : hasGuide ? "重解析" : "解析";
|
||||
button.disabled = state.guideBusy || state.processing;
|
||||
}
|
||||
|
||||
function applyRightResource(resource) {
|
||||
state.rightResource = resource;
|
||||
$("#editor-resource-label").textContent = `当前资源:${resource.label}`;
|
||||
$("#side-editor").value = resource.content || "";
|
||||
$("#btn-toggle-side-edit").disabled = !resource.editable;
|
||||
$("#btn-toggle-side-edit").textContent = resource.editable ? "编辑" : "只读";
|
||||
updateGuideButton(resource);
|
||||
state.rightEditMode = false;
|
||||
renderSidePreview(resource);
|
||||
|
||||
if (resource.type === "template" || resource.type === "template-guide") {
|
||||
syncTemplateSelection(resource.templateName || resource.name);
|
||||
}
|
||||
|
||||
refreshActionButtons();
|
||||
}
|
||||
|
||||
function setRightEditMode(editMode) {
|
||||
|
|
@ -502,7 +513,7 @@ async function openResultFile(meetingId, filename, treeKey) {
|
|||
}
|
||||
|
||||
async function openTreeResource(path) {
|
||||
if (state.processing) {
|
||||
if (state.processing || state.guideBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -685,7 +696,6 @@ async function refresh() {
|
|||
|
||||
await loadTree();
|
||||
renderMeetingStatus(meetingById(state.meetingId));
|
||||
$("#btn-process").disabled = !state.meetingId || state.processing;
|
||||
|
||||
if (state.meetingId) {
|
||||
try {
|
||||
|
|
@ -703,37 +713,60 @@ async function refresh() {
|
|||
templateData.some((item) => item.name === state.rightResource.name)
|
||||
) {
|
||||
await openTemplate(state.rightResource.name, state.rightResource.treeKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
} else if (
|
||||
state.rightResource &&
|
||||
state.rightResource.type === "template-guide" &&
|
||||
templateData.some((item) => item.name === state.rightResource.templateName)
|
||||
) {
|
||||
await openTemplateGuide(state.rightResource.name, state.rightResource.treeKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.rightResource && state.templateName) {
|
||||
} else if (!state.rightResource && state.templateName) {
|
||||
await openTemplate(state.templateName);
|
||||
}
|
||||
|
||||
refreshActionButtons();
|
||||
}
|
||||
|
||||
$("#btn-process").addEventListener("click", () => {
|
||||
if (!state.meetingId || state.processing) {
|
||||
async function ensureTemplateGuideBeforeProcess() {
|
||||
const templateMeta = templateMetaByName(state.templateName);
|
||||
if (templateMeta?.has_guide) {
|
||||
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;
|
||||
$("#btn-process").disabled = true;
|
||||
$("#btn-process").textContent = "处理中";
|
||||
setStatus("left", true, "总结中");
|
||||
refreshActionButtons();
|
||||
showProcessingView();
|
||||
$("#stream-box").style.display = "block";
|
||||
$("#stream-title").textContent = "第一阶段:结构化主题...";
|
||||
$("#stream-content").textContent = "";
|
||||
setRightStatus(true, "工作中");
|
||||
resetRightStream();
|
||||
|
||||
const source = new EventSource(
|
||||
`/api/meetings/${state.meetingId}/process?template_name=${encodeURIComponent(state.templateName)}`,
|
||||
|
|
@ -758,7 +791,6 @@ $("#btn-process").addEventListener("click", () => {
|
|||
$("#stream-title").textContent = "第二阶段:生成会议总结...";
|
||||
streamAcc = "";
|
||||
$("#stream-content").textContent = "";
|
||||
resetRightStream();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -767,7 +799,6 @@ $("#btn-process").addEventListener("click", () => {
|
|||
const { data } = payload;
|
||||
streamAcc += data.text || "";
|
||||
$("#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") {
|
||||
resultAcc += data.text || "";
|
||||
}
|
||||
|
|
@ -777,21 +808,18 @@ $("#btn-process").addEventListener("click", () => {
|
|||
if (payload.type === "done") {
|
||||
source.close();
|
||||
state.processing = false;
|
||||
$("#btn-process").disabled = false;
|
||||
$("#btn-process").textContent = "处理";
|
||||
showResult(payload.data?.result || resultAcc || "");
|
||||
await refresh();
|
||||
toast("会议处理完成");
|
||||
toast("会议总结完成");
|
||||
refreshActionButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === "error") {
|
||||
source.close();
|
||||
state.processing = false;
|
||||
$("#btn-process").disabled = false;
|
||||
$("#btn-process").textContent = "处理";
|
||||
resetProcessingStream();
|
||||
$("#processing-indicator").hidden = true;
|
||||
refreshActionButtons();
|
||||
toast(`处理失败:${payload.data}`, "err");
|
||||
}
|
||||
};
|
||||
|
|
@ -799,16 +827,32 @@ $("#btn-process").addEventListener("click", () => {
|
|||
source.onerror = () => {
|
||||
source.close();
|
||||
state.processing = false;
|
||||
$("#btn-process").disabled = false;
|
||||
$("#btn-process").textContent = "处理";
|
||||
resetProcessingStream();
|
||||
$("#processing-indicator").hidden = true;
|
||||
refreshActionButtons();
|
||||
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 () => {
|
||||
if (!state.meetingId) {
|
||||
if (!state.meetingId || state.processing || state.guideBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -825,13 +869,12 @@ $("#btn-toggle-result-edit").addEventListener("click", async () => {
|
|||
body: JSON.stringify({ content }),
|
||||
});
|
||||
showResult(content);
|
||||
$("#btn-toggle-result-edit").disabled = false;
|
||||
toast("结果已保存");
|
||||
});
|
||||
|
||||
$("#btn-toggle-side-edit").addEventListener("click", async () => {
|
||||
const resource = state.rightResource;
|
||||
if (!resource || !resource.editable) {
|
||||
if (!resource || !resource.editable || state.processing || state.guideBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -865,8 +908,8 @@ $("#btn-toggle-side-edit").addEventListener("click", async () => {
|
|||
|
||||
resource.content = content;
|
||||
setRightEditMode(false);
|
||||
$("#btn-toggle-side-edit").disabled = false;
|
||||
toast("资源已保存");
|
||||
refreshActionButtons();
|
||||
});
|
||||
|
||||
$("#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")) {
|
||||
return;
|
||||
}
|
||||
if (state.processing || state.guideBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const templateName = resource.templateName || resource.name;
|
||||
$("#btn-reparse-guide").disabled = true;
|
||||
setRightStatus(true, "工作中");
|
||||
resetRightStream();
|
||||
state.guideBusy = true;
|
||||
setStatus("right", true, "解析中");
|
||||
refreshActionButtons();
|
||||
|
||||
try {
|
||||
const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
setRightStatus(false, "空闲");
|
||||
$("#btn-reparse-guide").disabled = false;
|
||||
state.templates = state.templates.map((item) => (
|
||||
item.name === templateName ? { ...item, has_guide: true } : item
|
||||
));
|
||||
toast(`说明已更新:${templateName}`);
|
||||
await refresh();
|
||||
await openTemplateGuide(result.name);
|
||||
} finally {
|
||||
state.guideBusy = false;
|
||||
setStatus("right", false, "空闲");
|
||||
refreshActionButtons();
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn-import").addEventListener("click", () => {
|
||||
|
|
@ -978,8 +1029,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||
applySavedLayout();
|
||||
initResize("gutter-1", "sidebar", "result-panel");
|
||||
initResize("gutter-2", "result-panel", "template-panel");
|
||||
setRightStatus(false, "空闲");
|
||||
resetRightStream();
|
||||
setStatus("left", false, "空闲");
|
||||
setStatus("right", false, "空闲");
|
||||
|
||||
try {
|
||||
await refresh();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
--panel: rgba(255, 255, 255, 0.94);
|
||||
--panel-strong: #ffffff;
|
||||
--line: #c7dcf8;
|
||||
--line-strong: #9cc3f5;
|
||||
--text: #16324f;
|
||||
--muted: #5f7f9f;
|
||||
--accent: #2f80ed;
|
||||
|
|
@ -12,8 +11,6 @@
|
|||
--accent-soft: #e8f2ff;
|
||||
--ok-bg: #e9f7f1;
|
||||
--ok-text: #19744e;
|
||||
--danger-bg: #fdecec;
|
||||
--danger-text: #b53f4e;
|
||||
--shadow: 0 18px 48px rgba(47, 128, 237, 0.12);
|
||||
--radius: 18px;
|
||||
--font: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
|
|
@ -51,69 +48,6 @@ textarea {
|
|||
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 {
|
||||
height: var(--app-h);
|
||||
padding: 24px;
|
||||
|
|
@ -231,6 +165,54 @@ textarea {
|
|||
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 {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
|
@ -362,12 +344,8 @@ select.btn {
|
|||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: var(--ok-bg);
|
||||
color: var(--ok-text);
|
||||
border-color: rgba(25, 116, 78, 0.12);
|
||||
}
|
||||
|
||||
.badge.muted {
|
||||
|
|
@ -481,21 +459,6 @@ select.btn {
|
|||
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 {
|
||||
position: relative;
|
||||
width: min(620px, calc(100% - 40px));
|
||||
|
|
@ -525,29 +488,6 @@ select.btn {
|
|||
font-weight: 700;
|
||||
color: var(--accent-strong);
|
||||
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,
|
||||
|
|
@ -569,49 +509,12 @@ select.btn {
|
|||
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,
|
||||
#side-preview,
|
||||
#side-plain-preview,
|
||||
#result-editor,
|
||||
#side-editor {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -735,6 +638,24 @@ select.btn {
|
|||
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) {
|
||||
html,
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -44,17 +44,24 @@
|
|||
<div class="panel-header">
|
||||
<div class="panel-heading">
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body panel-scroll" id="result-body">
|
||||
<div id="result-empty" class="empty-state">
|
||||
<div class="empty-icon">M</div>
|
||||
<p>选择会议后即可查看结果,或直接开始处理。</p>
|
||||
<p>选择会议后即可查看结果,或直接开始总结。</p>
|
||||
</div>
|
||||
<div id="processing-indicator" class="processing" hidden>
|
||||
<div class="stream-box" id="stream-box">
|
||||
|
|
@ -73,14 +80,13 @@
|
|||
<div class="panel-header">
|
||||
<div class="panel-heading">
|
||||
<span>右侧编辑区</span>
|
||||
<div class="side-status" id="side-status">
|
||||
<div class="side-status-row">
|
||||
<span class="side-status-label">当前状态</span>
|
||||
<span class="status-light idle" id="side-status-light"></span>
|
||||
<span class="side-status-text" id="side-status-text">空闲</span>
|
||||
<div class="status-block">
|
||||
<div class="status-row">
|
||||
<span class="status-label">当前状态</span>
|
||||
<span class="status-light idle" id="right-status-light"></span>
|
||||
<span class="status-text" id="right-status-text">空闲</span>
|
||||
</div>
|
||||
<div class="side-status-meta" id="editor-resource-label">当前资源:模板</div>
|
||||
<pre class="side-status-stream" id="side-status-stream"></pre>
|
||||
<div class="status-meta" id="editor-resource-label">当前资源:模板</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
- 严格保留模板的Markdown标题层级结构(`#`→`##`→`###`→`####`),不得跨级或打乱顺序。
|
||||
- 模板头部的基础信息字段(议题、时间、地点、主持人、参加人)必须保留,其中的 `X`、`XX`、`XXX` 及示例年份/公司名称需替换为真实会议信息。
|
||||
- “议程”部分保留固定条目结构,内容替换为实际讨论顺序,若无议程变动可维持原结构。
|
||||
- “会议内容”下的部门汇报部分需保留进度通报与数据跟踪的句式结构,替换其中的部门名称与量化数据占位符,无实际汇报内容时省略该小节。
|
||||
- “部署强调”部分必须保留 `#### [发言人/领导]强调:` 的标题格式,严禁保留示例中的“X总”。
|
||||
- 严格保留领导发言下的列表嵌套格式:使用数字编号(`1.`)引导业务方向,方向名称加粗(`**XX方面:**`),其下使用短横线(`- `)缩进列出具体的发言要点或行动项。
|
||||
- 所有占位符(如 `XXX`)及模板示例文本在生成时必须替换为真实会议内容,严禁原样输出占位或演示文字。
|
||||
- “部署强调”板块数量及每个领导下的业务方向子项数量应随实际会议内容动态增减,无依据支撑的空白项、示例分组必须省略,不强行补齐。
|
||||
- 保留各主要章节间的 `---` 分隔线,用于清晰划分汇报环节与领导部署区块。
|
||||
- 严格保留模板的Markdown标题层级与宏观结构(# 会议记录 -> ## 会议内容 -> ### 各部门汇报/部署强调),不得增删或调整主标题顺序。
|
||||
- 模板中的“X”、“XX”、“XXX”及具体年份、部门名均为占位示例,生成时必须替换为会议真实数据与名称;若实际会议未涉及某字段,则直接省略该条目,不强行补齐。
|
||||
- “各部门汇报”小节需按实际汇报顺序提炼核心内容与进度数据,替换原长句模板;若无汇报内容,可省略该小节。
|
||||
- “部署强调”部分采用“#### [领导职务/姓名]强调:”作为独立标题块,多个领导发言时按实际顺序并列生成,替换示例中的“X总”。
|
||||
- 领导强调下按实际业务领域划分子项(格式为“数字. **领域:**”),子项数量与领域名称严格按实际讲话内容动态增减,不得照搬模板示例的固定领域列表。
|
||||
- 各领域下的具体指示需使用无序列表(`- `)逐条列出,替换示例“XXX”;保持原有缩进层级,确保要点清晰。
|
||||
- 所有仅用于演示结构的说明性文字(如“按议程现场按顺序做汇报”“详见汇报材料”等)必须删除,仅输出实际会议提炼的实质内容。
|
||||
- 严格遵循模板原有的排版分隔逻辑,各主要模块间保留“---”分隔线,维持整体版式节奏。
|
||||
Binary file not shown.
|
|
@ -37,6 +37,8 @@ TEMPLATE_GUIDE_DIR.mkdir(parents=True, exist_ok=True)
|
|||
|
||||
app = FastAPI(title="Meeting Summary Web")
|
||||
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:
|
||||
|
|
@ -118,6 +120,13 @@ def _guide_path(template_name: str) -> Path:
|
|||
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:
|
||||
content = []
|
||||
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():
|
||||
raise HTTPException(404, f"Template not found: {template_name}")
|
||||
|
||||
lock = _get_template_lock(template_name)
|
||||
with lock:
|
||||
guide_path = _guide_path(template_name)
|
||||
if guide_path.exists() and not force:
|
||||
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)
|
||||
if not template_path.exists():
|
||||
raise HTTPException(404, f"Template not found: {name}")
|
||||
with _get_template_lock(name):
|
||||
_guide_path(name).write_text(content, encoding="utf-8")
|
||||
return {"ok": True}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue