From bdbdc674c64ddc18bb24041cb96d19f5300c60aa Mon Sep 17 00:00:00 2001 From: Bifang <915779419@qq.com> Date: Mon, 11 May 2026 11:00:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=85=A8=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.json | 24 +- .../results/json/1778221747643/sub_topic.json | 128 ++++---- .../md/1778221747643/meeting_summary.md | 82 +++--- frontend/assets/app.js | 277 +++++++++++++++--- frontend/assets/styles.css | 12 + frontend/index.html | 81 +++-- template/test.md | 61 ++++ template_guides/template1.md | 20 +- template_guides/test.md | 9 + web/__pycache__/server.cpython-314.pyc | Bin 40301 -> 51292 bytes web/server.py | 249 +++++++++++++++- 11 files changed, 771 insertions(+), 172 deletions(-) create mode 100644 template/test.md create mode 100644 template_guides/test.md diff --git a/config.json b/config.json index 9eaf306..d6c6300 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,23 @@ { - "api_base_url": "https://api.llm.unissense.tech/v1", - "api_key": "unis123", - "model_name": "Qwen3.6-35B" + "api_profiles": [ + { + "name": "默认接口", + "api_base_url": "https://api.llm.unissense.tech/v1", + "api_key": "unis123", + "model_name": "Qwen3.6-35B", + "max_tokens": 64000 + }, + { + "name": "MiniMax-M2.5", + "api_base_url": "https://coding.dashscope.aliyuncs.com/v1", + "api_key": "sk-sp-575f1f4c70804854a46b16018a478f5d", + "model_name": "MiniMax-M2.5", + "max_tokens": 32768 + } + ], + "active_api_profile_name": "MiniMax-M2.5", + "api_base_url": "https://coding.dashscope.aliyuncs.com/v1", + "api_key": "sk-sp-575f1f4c70804854a46b16018a478f5d", + "model_name": "MiniMax-M2.5", + "max_tokens": 32768 } \ No newline at end of file diff --git a/data/results/json/1778221747643/sub_topic.json b/data/results/json/1778221747643/sub_topic.json index abe17fc..a07cdcb 100644 --- a/data/results/json/1778221747643/sub_topic.json +++ b/data/results/json/1778221747643/sub_topic.json @@ -1,54 +1,76 @@ +```json { - "sub_topics": [ - { - "title": "宽带装维上门进度与质量指标通报", - "time_interval": "00:00:01 - 00:02:06", - "overview": "会议回顾宽带装维上门量与安装进度的平衡情况,当前累计上门量超1000户,距3000户目标进度偏后,周均上门量约1000次,月均约4000次。关键质量指标方面,弱光率当前为0.51即将达到0.5目标,三类终端指标较上周下降0.1个百分点,FTDR已达标,但主动跟进指标相对靠后。" - }, - { - "title": "九零工程转化率与退单率分析", - "time_interval": "00:02:07 - 00:03:51", - "overview": "汇报“九零工程”月度转化率87.35%(目标90%)及退单率6.53%(目标低于7.5%),退单主因包括改约、用户原因29单及天气影响2单。针对蓝单转化率仅75.29%及退单率偏高问题,提出将退单率纳入综合运维考核,并建议剔除分局长审批同意的退单以优化考核公平性。" - }, - { - "title": "专线质量恶化与PCDN治理探讨", - "time_interval": "00:03:53 - 00:05:31", - "overview": "专线指标持续恶化,主要问题集中在人文与海师两所学校,已报市公司分析出口IP地址。会议探讨了建立后台自动限速机制的可行性,明确学校互联网带宽虽免费但需规范使用,后续发现问题将第一时间通过系统上线处理并协同全资方落实管控。" - }, - { - "title": "网络质量专项攻坚与故障处理通报", - "time_interval": "00:05:33 - 00:08:04", - "overview": "通报三创空间天线缺货延至4月初检测,畅听工单需智能体逐项核查,5G弱化问题正试点调整基站参数。超频站点攻坚发现3处故障(铁塔开关电源及传输光衰)均已恢复,专线护航118条中已巡检56条,剩余17条月底完成。当周共收故障33件,9件网络相关以光缆问题为主。" - }, - { - "title": "二级基站清理与工程下沉推进", - "time_interval": "00:08:07 - 00:09:35", - "overview": "二级基站清理工作进展顺利,2026年剩余121站已全部下电,累计拆除46站,预计两周内完成剩余75站拆除及资费调整,确保4月15日前全量完成。工程下沉项目正常开展,已接收网络质量综合评估体系,下周将针对该体系进行指标分解与目标制定。" - }, - { - "title": "综合部行政后勤与工会活动筹备", - "time_interval": "00:09:46 - 00:12:39", - "overview": "综合部汇报无人机验收待推动、招投标打印机外协洽谈(内外价差超十倍)及气象/渠道/专线发展计划。工会经费压降背景下,推进食堂改造(预算29.8万元)及直升机引水项目获批。筹备第四届体育文化节,确定“河川佳绩,穿越巅峰”主题,正协调各部门补充方阵人员并安排周二至周四排练。" - }, - { - "title": "招待费管控与区级评优申报研判", - "time_interval": "00:12:40 - 00:27:30", - "overview": "通报2025年招待费超年初预算14%,综合部2024年曾超支107%,2026年仅综合部调整预算,要求统筹正气部对外招待并加快报销结算。针对河川区“担当作为”评优申报,因往届多为政府机构且评选流程未明,领导指示需提前摸底决策领导意向后再决定是否投入精力,避免形式主义消耗。" - }, - { - "title": "商客市场拓展与“查企”数据复盘", - "time_interval": "00:27:32 - 00:34:04", - "overview": "“查企”工作累计完成690万收入与1110户发展,主要采取低ARPU价值提升与融合套餐策略,本周开展5场社区/单位营销但龙狮业务转化不佳。商客市场1月收入88.5万元(同比增1万),价值拓展排名全市第6,个别分局增幅超2万,但部分经理周基础业务仅2笔,进度滞后需加强。" - }, - { - "title": "工作作风整顿与满意度考核部署", - "time_interval": "00:34:04 - 00:42:32", - "overview": "领导严厉批评部分工作回复滞后、缺乏量化措施,要求养成“日事日清”习惯并今日提交专线与商客指标保障方案。市场部满意度测评垫底,指出正气部负责人缺席客户会导致工作松懈,要求本周内汇报招聘、农村渠道及行销转型进度。明确今年KPI考核将纳入工信部有责与离网率,要求吃透市公司技巧确保3月底“八零两绿”达标。" - }, - { - "title": "年度考核指标预沟通与会议总结", - "time_interval": "00:42:33 - 00:46:16", - "overview": "会议最后强调全员需强化执行力,杜绝“知而不做”的作风,要求各部门提前与市公司对接年度考核口径与潜在指标(如APP提升考核),避免起跑线落后。领导指出考核争取公平得分需靠主动沟通与前期铺垫,各部门需联动协同,周会至此结束。" - } - ] -} \ No newline at end of file + "sub_topics": [ + { + "title": "网络运维指标汇报", + "time_interval": "00:00:01 - 00:03:56", + "overview": "汇报宽带上门安装及故障处理情况,上周上门量580户,累计超1000户,目标3000户进度滞后。弱光指标0.51%接近0.5%目标,三类终端年度目标从7.5%压降至5.5%。九零工程转化率月度87.35%接近90%目标,退单率6.53%,退单主要原因包括用户原因29单、覆盖6单、天气2单。" + }, + { + "title": "PCDN专线及三创空间指标", + "time_interval": "00:03:53 - 00:05:52", + "overview": "PCDN专线指标持续恶化,3月份达4.84%,主要涉及人文和海师两所学校,已报市公司分析IP地址并协调学校限速。三创空间物业点本月未达标,因天线缺货预计4月初检测,其他指标达标。" + }, + { + "title": "畅听、超频站点及专线巡检", + "time_interval": "00:05:33 - 00:08:13", + "overview": "畅听一项未达标需智能判断和质检;5G基站参数调整试点待观察。超频站点发现3个问题基站,其中2个为铁塔开关电源故障,1个为传输光衰过大,均已处理。专线巡检方面,118条存量专线已完成56条,45条未验收工程全部巡检,3月底前完成全部巡检。" + }, + { + "title": "金属管家及工程项目进展", + "time_interval": "00:08:14 - 00:09:35", + "overview": "金属管家二级站还剩121站,上周已全部下电,累计拆除46站,剩余75站预计2周内完成拆除和资管转出,4月15日前全量完成。网络质量综合评估体系已内部收到,下周进行分解和目标制定。" + }, + { + "title": "综合部25年工作汇报", + "time_interval": "00:09:46 - 00:12:39", + "overview": "汇报25年剩余2项工作:花果山无变化,无人机项目待拜访唐书记推动。上周完成5项工作:建委施工资管框架整理、投资计划专项评估汇报、终端占比问题汇报、收入地图完成、打印机问题已与极客洽谈并预留招投标打印机。" + }, + { + "title": "工会及后勤工作", + "time_interval": "00:12:40 - 00:15:20", + "overview": "工会经费因市公司压价需节电耗用,本周上报暖心工程包括根石和食堂改造,食堂改造预算29.8万元计划5月更换修补。拟采用直升机方式解决用水问题,已报市公司批准。幸福一加一活动全年安排6场大型活动,近期筹备六一儿童节活动。" + }, + { + "title": "体育文化节筹备", + "time_interval": "00:15:21 - 00:19:34", + "overview": "第五届体育文化节主题为河川佳绩、穿越巅峰、东亚赛场,需25人组成方阵,目前差9人,各部门需再安排2人参加。方阵人员周二至周四排练,周五参加活动,参加人员有绩效加分。会上讨论部分主力选手因伤无法参加。" + }, + { + "title": "招待费公开及主题教育", + "time_interval": "00:19:36 - 00:20:34", + "overview": "主题教育市公司已发布第1期简报,基层党组织截至3月15日已开展学习。招待费信息公开已通过邮件发布并向纪检组织报备,公开对象为本单位管理层班子成员。" + }, + { + "title": "招待费使用情况及区级评选", + "time_interval": "00:20:38 - 00:24:53", + "overview": "25年招待费使用超预算14%,综合部增长明显超107%,市场部因渠道大会使用较快。河川区担当作为评选通知已收到,建议争取参评以差异化竞争,报名截止3月31日,需网上链接报名。" + }, + { + "title": "查企及商客市场工作", + "time_interval": "00:27:32 - 00:32:25", + "overview": "查企工作通过单位集中方式开展,完成5100户价值提升,新年查企2次通话15.1元/月,一季度主要完成通富未达50的价值提升。商客市场1月收入88.5万,草街、城北、南京街增幅超2万,一家农家乐负200元。价值拓展完成115同比第6,重点工作在三经理方面,综合楼基础业务仅2笔较差。" + }, + { + "title": "建委评估体系及工作要求", + "time_interval": "00:32:33 - 00:34:29", + "overview": "建委综合评估体系已出台,要求按时间节点推进工作。会议强调工作习惯养成,要求日事日清、日清日结、日结日高,工作回复需量化、有结果、有措施、有成效,工作进展需及时向安排工作的领导汇报。" + }, + { + "title": "市场部工作问题及满意度", + "time_interval": "00:34:33 - 00:38:19", + "overview": "市场部工作存在三个方面问题:季度收官不足、业务推进疲软、传统淡季需造势。要求本周内就招聘情况、农村渠道进度、行销方案三个事项做专题汇报。满意度测评结果不理想,正气部是拉分项,廖经理参加客户会较少,要求其亲自抓满意度工作。" + }, + { + "title": "客服及满意度提升", + "time_interval": "00:38:23 - 00:42:32", + "overview": "关于80两绿指标,3月底四公司有统一要求,需按汇通运营的满意样板操作。不满客户处理有技巧空间,5点半和6点半有不同操作方式。目前河川区满意度属于一类倒数第二,要求市场部每天在红星出日报,KPI考核主要是工信部有责和离网率。" + }, + { + "title": "领导总结和工作部署", + "time_interval": "00:42:33 - 00:46:16", + "overview": "强调执行力,要求知道怎么做就要去做,不认可知道但不去做的工作风格。市公司年度考核涉及多个方面,部分考核可能影响较大,需提前沟通了解考核口径,不要等定好了再说。建议与分管领导加强联动,确保在同一起跑线上。" + } + ] +} +``` \ No newline at end of file diff --git a/data/results/md/1778221747643/meeting_summary.md b/data/results/md/1778221747643/meeting_summary.md index a72d419..7b811b7 100644 --- a/data/results/md/1778221747643/meeting_summary.md +++ b/data/results/md/1778221747643/meeting_summary.md @@ -1,22 +1,14 @@ - - # 会议记录 -议 题:合川分公司周例会(2026年第X期) +议 题:合川分公司周例会 -时 间:2026年5月8日 10:38—10:50 +时 间:2026年5月8日 10:38—11:00 地 点:分公司会议室 主持人:管理员 -参加人:分公司领导、各部门经理及AI云数中心经理 - -议程: - - 一、各部门汇报 - - 二、分公司领导指示部署 +参加人:分公司领导 部门经理人员 AI云数中心经理 --- @@ -24,41 +16,53 @@ ### 一、各部门汇报 -建维部、综合部、市场部及政企部按议程依次汇报。建维部通报宽带装维进度:当前累计上门量突破1000户,距3000户目标进度偏后,周均上门约1000次;弱光率0.51逼近0.5目标,三类终端指标改善0.1个百分点,FTDR达标但主动跟进指标靠后(0.3)。“九零工程”月度转化率87.35%(目标90%),退单率6.53%(目标<7.5%),退单主因改约及用户原因;蓝单转化率75.29%,建议将退单率纳入运维考核并优化考核口径。专线质量受两所学校IP地址影响持续恶化,已报市公司分析并探讨后台自动限速机制;三创空间天线缺货延至4月初检测,5G弱化问题试点调整基站参数;超频基站3处故障已恢复,专线护航118条中56条已巡检,剩余17条月底完成。综合部汇报无人机验收待推动、招投标打印机外协洽谈(内外价差超十倍)、食堂改造(预算29.8万元)及直升机引水项目获批;第四届体育文化节正协调各部门补充8名方阵人员,周二至周四排练。市场部通报“查企”累计完成690万收入与1110户发展,商客市场1月收入88.5万元(同比增1万),价值拓展全市第6,但部分经理周基础业务仅2笔进度滞后。 +**网络运维方面:** 宽带上门安装及故障处理方面,上周上门量580户,累计超1000户,目标3000户进度滞后。弱光指标0.51%接近0.5%目标,三类终端年度目标从7.5%压降至5.5%。九零工程转化率月度87.35%接近90%目标,退单率6.53%,退单主要原因包括用户原因29单、覆盖6单、天气2单。PCDN专线指标持续恶化,3月份达4.84%,主要涉及人文和海师两所学校,已报市公司分析IP地址并协调学校限速。三创空间物业点本月未达标因天线缺货预计4月初检测。专线巡检118条存量专线已完成56条,45条未验收工程全部巡检,3月底前可完成全部巡检。金属管家二级站还剩121站,累计拆除46站,剩余75站预计2周内完成,4月15日前全量完成。 + +**综合部方面:** 25年剩余2项工作:花果山无变化,无人机项目待拜访唐书记推动。上周完成5项工作:建委施工资管框架整理、投资计划专项评估汇报、终端占比问题汇报、收入地图完成、打印机问题已与极客洽谈并预留招投标打印机。工会经费因市公司压价需节电耗用,本周上报暖心工程包括根石和食堂改造(预算29.8万元计划5月更换修补)。幸福一加一活动全年安排6场大型活动,近期筹备六一儿童节活动。第五届体育文化节主题为"河川佳绩、穿越巅峰、东亚赛场",需25人组成方阵,目前差9人,各部门需再安排2人参加。招待费25年使用超预算14%,综合部超107%。 + +**查企及商客方面:** 查企通过单位集中方式完成5100户价值提升。商客市场1月收入88.5万,草街、城北、南京街增幅超2万。价值拓展完成115同比第6,综合楼基础业务仅2笔较差。 --- ### 二、部署强调 -#### 建维部负责人强调: +#### 建委领导强调: -1. **装维与质量指标方面:** - - 兼顾安装与上门量,加快追赶3000户目标进度 - - 保持弱光率与三类终端改善势头,专项提升主动跟进指标 - - 建议将退单率纳入综合运维考核,推动剔除分局长已审批同意的退单以保公平 -2. **工程与网络攻坚方面:** - - 2周内完成二级基站剩余75站拆除,确保4月15日前全量完成下电与资费调整 - - 按期完成专线护航剩余17条巡检,存量光缆路由抢盘行动及时上报 - - 下周内分解网络质量综合评估体系指标并制定阶段性目标 -3. **故障处理与专项测试方面:** - - 持续压降超频站点及旁站故障,当周33件故障以光缆问题为主需重点排查 - - 跟进三创空间天线检测及5G弱化基站参数试点调整效果 - - 协同全资方建立专线自动限速机制,规范学校带宽使用并第一时间系统上线管控 +1. **工作习惯要求:** + - 养成日事日清、日清日结、日结日高的好习惯 + - 工作回复需量化、有结果、有措施、有成效 + - 工作进展需及时向安排工作的领导汇报,不能只在周报时反馈综合部 + +2. **评估体系推进:** + - 建委综合评估体系已出台,要求按时间节点推进工作 + - 涉及吉卡尔号绕轨运输等工作需当天处理、及时回复 --- -#### 分公司领导强调: +#### 市场部分管领导强调: -1. **市场与政企拓展方面:** - - 深度复盘商客市场与“查企”数据,加强低ARPU价值提升与龙狮业务转化 - - 严抓部分经理周基础业务仅2笔的滞后问题,以周为单位严格跟踪并加快分局推进节奏 - - 扭转满意度测评垫底局面,政企部负责人须亲自抓客户会工作,杜绝思想松懈 -2. **行政后勤与费用管控方面:** - - 统筹政企部对外招待,加快报销结算,严控招待费超支风险(25年已超预算14%) - - 摸底区级“担当作为”评优申报决策领导意向,避免形式主义消耗 - - 落实招投标打印机外协保障方案,确保不影响招投标业务开展 -3. **作风建设与考核部署方面:** - - 严厉批评工作回复滞后、缺乏量化措施,要求全员养成“日事日清、日结日高”习惯,今日提交专线与商客指标具体保障方案 - - 市场部需本周内汇报招聘进度、农村渠道建设及行销队伍转型方案 - - 吃透市公司考核技巧,确保3月底“八零两绿”达标,今年KPI将纳入工信部有责与离网率 - - 强化全员执行力,严禁“知而不做”作风,各部门需提前对接市公司年度考核口径(如APP提升考核),主动沟通争取公平得分,避免起跑线落后 \ No newline at end of file +1. **满意度工作:** + - 满意度测评结果不理想,正气部是拉分项,廖经理参加客户会较少 + - 要求廖经理亲自抓满意度工作,不能只是开学 + - 80两绿指标3月底四公司有统一要求,需按汇通运营的满意样板操作 + - 不满客户处理有技巧空间,5点半和6点半有不同操作方式 + - 目前河川区满意度属于一类倒数第二,要求市场部每天在红星出日报 + +2. **市场业务推进:** + - 季度收官不足,业务推进疲软,需提前谋划二季度业务活动 + - 不能因传统淡季放松,要造起签约、3人、H、AI终端等业务声势 + - 本周内就招聘情况、农村渠道进度、行销方案三个事项做专题汇报 + +--- + +#### 分公司领导总结: + +1. **执行力要求:** + - 不认可知道怎么做但不去做的工作风格 + - 事情要做好,要知道怎么做也要去做 + +2. **考核沟通:** + - 市公司年度考核涉及多个方面,部分考核可能影响较大 + - 需提前沟通了解考核口径,不要等定好了再说 + - 两个分管领导(政企部跟相关负责部门)要加强联动 + - 确保在同一起跑线上,不要输在起跑线 \ No newline at end of file diff --git a/frontend/assets/app.js b/frontend/assets/app.js index 489a056..878557f 100644 --- a/frontend/assets/app.js +++ b/frontend/assets/app.js @@ -13,6 +13,7 @@ const state = { processGuideEditMode: false, selectedTreeKey: "", rightResource: null, + settingsDraft: null, }; const STORAGE_KEY = "meeting-workspace-preferences"; @@ -80,6 +81,19 @@ function closeModal(id) { document.getElementById(id).classList.remove("show"); } +function cloneSettingsDraft(draft) { + return { + active_api_profile_name: draft.active_api_profile_name || "", + api_profiles: (draft.api_profiles || []).map((item) => ({ + name: item.name, + api_base_url: item.api_base_url, + api_key: item.api_key, + model_name: item.model_name, + max_tokens: item.max_tokens, + })), + }; +} + async function api(url, options) { const res = await fetch(url, options || {}); if (!res.ok) { @@ -173,6 +187,45 @@ function syncProcessGuideConfirmLabel() { $("#btn-process-guide-confirm").textContent = meeting?.has_summary ? "重总结" : "确认"; } +function renderSettingsKeyOptions() { + const draft = state.settingsDraft; + const select = $("#cfg-key-select"); + select.innerHTML = ""; + + (draft?.api_profiles || []).forEach((item) => { + const option = document.createElement("option"); + option.value = item.name; + option.textContent = item.model_name || item.name; + select.appendChild(option); + }); + + if (draft?.active_api_profile_name) { + select.value = draft.active_api_profile_name; + } + + const current = draft?.api_profiles?.find((item) => item.name === draft.active_api_profile_name); + $("#cfg-current-model").value = current?.model_name || ""; + $("#cfg-max-tokens").value = current?.max_tokens || 64000; + $("#btn-key-delete").disabled = (draft?.api_profiles?.length || 0) <= 1; +} + +function loadSettingsDraft(cfg) { + state.settingsDraft = cloneSettingsDraft(cfg); + renderSettingsKeyOptions(); +} + +async function persistSettingsDraft() { + const payload = cloneSettingsDraft(state.settingsDraft || {}); + if (!payload.api_profiles.length) { + throw new Error("??????????"); + } + await api("/api/settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); +} + function showReparseGuidePrompt() { $("#reparse-guide-notes").value = ""; $("#reparse-guide-prompt").hidden = false; @@ -227,27 +280,64 @@ async function runStandaloneGuideReparseFlow() { pushStandaloneReparseLine("已提交重解析请求,正在准备说明..."); refreshActionButtons(); - const hints = [ - "正在结合你的补充说明整理解析重点...", - "正在按解析模板抽取可执行的使用说明...", - "即将刷新右侧资源并打开最新说明...", - ]; - let hintIndex = 0; - const timer = window.setInterval(() => { - if (hintIndex < hints.length) { - pushStandaloneReparseLine(hints[hintIndex]); - hintIndex += 1; - } - }, 900); - try { - const result = await runTemplateGuideReparse(templateName, userNotes); - pushStandaloneReparseLine("模板说明已更新。"); + const params = new URLSearchParams(); + if (userNotes.trim()) { + params.set("user_notes", userNotes.trim()); + } + const source = new EventSource( + `/api/templates/${encodeURIComponent(templateName)}/guide/reparse/stream?${params.toString()}`, + ); + + 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") { + $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; + pushStandaloneReparseLine("已连接模型,开始解析..."); + return; + } + if (payload.type === "chunk") { + const chunk = payload.data?.text || ""; + if (chunk) { + contentAcc += chunk; + $("#reparse-stream-content").textContent = contentAcc.replace(/\r\n/g, "\n").split("\n").slice(-8).join("\n"); + } + return; + } + if (payload.type === "done") { + source.close(); + resolve(payload.data || { name: templateName, content: contentAcc }); + return; + } + if (payload.type === "error") { + source.close(); + reject(new Error(payload.data || "解析失败")); + } + }; + + source.onerror = () => { + source.close(); + 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 || ""); closeModal("modal-reparse-guide"); await openProcessGuideModal(); $("#process-guide-editor").value = result.content || ""; } finally { - window.clearInterval(timer); state.guideBusy = false; setStatus("right", false, "空闲"); setStandaloneReparseProcessing(false); @@ -1299,15 +1389,21 @@ $("#btn-import").addEventListener("click", () => { openModal("modal-import"); }); +$("#btn-import-template").addEventListener("click", () => { + $("#import-template-name").value = ""; + $("#import-template-file").value = ""; + openModal("modal-import-template"); +}); + $("#btn-confirm-import").addEventListener("click", async () => { const name = $("#import-name").value.trim(); const file = $("#import-file").files[0]; if (!name) { - toast("请输入会议名称", "err"); + toast("???????", "err"); return; } if (!file) { - toast("请选择转录文件", "err"); + toast("???????", "err"); return; } @@ -1318,48 +1414,159 @@ $("#btn-confirm-import").addEventListener("click", async () => { const result = await fetch("/api/meetings/import", { method: "POST", body: formData }); if (!result.ok) { const detail = await result.json().catch(() => ({ detail: "Import failed" })); - toast(`导入失败:${detail.detail}`, "err"); + toast(`?????${detail.detail}`, "err"); return; } const payload = await result.json(); closeModal("modal-import"); - toast(`导入成功:${name}`); + toast(`?????${name}`); await refresh(); await selectMeeting(payload.id); }); +$("#btn-confirm-import-template").addEventListener("click", async () => { + const name = $("#import-template-name").value.trim(); + const file = $("#import-template-file").files[0]; + if (!name) { + toast("???????", "err"); + return; + } + if (!file) { + toast("???????", "err"); + return; + } + + const formData = new FormData(); + formData.append("name", name); + formData.append("file", file); + + const result = await fetch("/api/templates/import", { method: "POST", body: formData }); + if (!result.ok) { + const detail = await result.json().catch(() => ({ detail: "Import failed" })); + toast(`?????${detail.detail}`, "err"); + return; + } + + const payload = await result.json(); + closeModal("modal-import-template"); + toast(`??????${payload.name}`); + await refresh(); + syncTemplateSelection(payload.name); + await openTemplate(payload.name); +}); + $("#btn-settings").addEventListener("click", async () => { try { const cfg = await api("/api/settings"); - $("#cfg-url").value = cfg.api_base_url || ""; - $("#cfg-key").value = cfg.api_key || ""; - $("#cfg-model").value = cfg.model_name || ""; + loadSettingsDraft(cfg); } finally { openModal("modal-settings"); } }); +$("#cfg-key-select").addEventListener("change", async (event) => { + if (!state.settingsDraft) { + return; + } + state.settingsDraft.active_api_profile_name = event.target.value; + renderSettingsKeyOptions(); + try { + await persistSettingsDraft(); + toast(`??????${$("#cfg-current-model").value}`); + } catch (error) { + toast(error.message, "err"); + } +}); + $("#btn-save-settings").addEventListener("click", async () => { - const payload = { - api_base_url: $("#cfg-url").value.trim(), - api_key: $("#cfg-key").value.trim(), - model_name: $("#cfg-model").value.trim(), - }; - await api("/api/settings", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), + if (!state.settingsDraft) { + return; + } + const currentName = $("#cfg-key-select").value; + const maxTokens = Number($("#cfg-max-tokens").value || 64000); + if (!Number.isFinite(maxTokens) || maxTokens < 1) { + toast("max_tokens ????? 0 ???", "err"); + return; + } + state.settingsDraft.api_profiles = state.settingsDraft.api_profiles.map((item) => ( + item.name === currentName ? { ...item, max_tokens: Math.floor(maxTokens) } : item + )); + try { + await persistSettingsDraft(); + renderSettingsKeyOptions(); + toast(`??????${$("#cfg-current-model").value}`); + } catch (error) { + toast(error.message, "err"); + } +}); + +$("#btn-key-add").addEventListener("click", () => { + $("#cfg-add-model-name").value = ""; + $("#cfg-add-base-url").value = ""; + $("#cfg-add-api-key").value = ""; + $("#cfg-add-max-tokens").value = "64000"; + openModal("modal-add-model"); +}); + +$("#btn-confirm-add-model").addEventListener("click", async () => { + if (!state.settingsDraft) { + return; + } + const modelName = $("#cfg-add-model-name").value.trim(); + const apiBaseUrl = $("#cfg-add-base-url").value.trim(); + const apiKey = $("#cfg-add-api-key").value.trim(); + const maxTokens = Number($("#cfg-add-max-tokens").value || 64000); + if (!modelName || !apiBaseUrl || !apiKey || !Number.isFinite(maxTokens) || maxTokens < 1) { + toast("??????????", "err"); + return; + } + if (state.settingsDraft.api_profiles.some((item) => item.name === modelName)) { + toast("???????", "err"); + return; + } + + state.settingsDraft.api_profiles.push({ + name: modelName, + model_name: modelName, + api_base_url: apiBaseUrl, + api_key: apiKey, + max_tokens: Math.floor(maxTokens), }); - closeModal("modal-settings"); - toast("配置已保存"); + state.settingsDraft.active_api_profile_name = modelName; + renderSettingsKeyOptions(); + + try { + await persistSettingsDraft(); + closeModal("modal-add-model"); + toast(`??????${modelName}`); + } catch (error) { + toast(error.message, "err"); + } +}); + +$("#btn-key-delete").addEventListener("click", async () => { + if (!state.settingsDraft || state.settingsDraft.api_profiles.length <= 1) { + return; + } + const targetName = $("#cfg-key-select").value; + state.settingsDraft.api_profiles = state.settingsDraft.api_profiles.filter((item) => item.name !== targetName); + state.settingsDraft.active_api_profile_name = state.settingsDraft.api_profiles[0]?.name || ""; + renderSettingsKeyOptions(); + + try { + await persistSettingsDraft(); + toast(`??????${targetName}`); + } catch (error) { + toast(error.message, "err"); + } }); $$("[data-close]").forEach((button) => { button.addEventListener("click", () => closeModal(button.dataset.close)); }); -["modal-import", "modal-settings", "modal-process-guide", "modal-reparse-guide"].forEach((id) => { +["modal-import", "modal-import-template", "modal-settings", "modal-add-model", "modal-process-guide", "modal-reparse-guide"].forEach((id) => { document.getElementById(id).addEventListener("click", (event) => { if (event.target.id === id) { if (id === "modal-reparse-guide" && state.guideBusy) { diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 46b4e8c..3267b89 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -661,6 +661,18 @@ select.btn { background: #f9fcff; } +.form-field select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: #f9fcff; +} + +.settings-key-actions { + justify-content: flex-start; +} + .form-field textarea { width: 100%; padding: 12px 14px; diff --git a/frontend/index.html b/frontend/index.html index 36f7af1..0d5f7f8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ Meeting Summary - + @@ -15,8 +15,9 @@

Meeting Summary Workspace

- - + + +
@@ -170,45 +171,91 @@ + + + +
- + diff --git a/template/test.md b/template/test.md new file mode 100644 index 0000000..9896bc5 --- /dev/null +++ b/template/test.md @@ -0,0 +1,61 @@ +## 📝 会议概述 +[简述产品开发背景、当前阶段、主要议题和预期交付成果] + +## 🚀 核心议题 + +### 议题1:[主题标题] +- 需求分析: [用户需求、市场需求、功能需求定义与优先级] +- 技术评估: [技术可行性、架构选择、开发难度评估] +- 资源规划: [人力配置、时间安排、预算分配] + +### 议题2:[主题标题] +- 设计方案: [产品设计、UI/UX设计、交互逻辑设计] +- 原型验证: [原型测试结果、用户反馈、迭代方向] +- 标准制定: [开发规范、质量标准、验收标准] + +### 议题3:[主题标题] +- 开发进度: [当前开发状态、里程碑达成、延期风险] +- 技术难点: [技术挑战、解决方案、备选方案] +- 测试策略: [测试计划、测试用例、Bug修复优先级] + +## ❓ 关键问答 + +Q: [技术实现问题1] +A: [技术解决方案1] + +--- + +Q: [产品设计问题2] +A: [设计决策说明2] + +## 💡 重要观点 + +### 关于产品定位 +> "[对产品市场定位、核心价值的关键判断]" + +### 关于技术选型 +> "[对技术架构、开发工具选择的专业建议]" + +### 关于用户体验 +> "[对用户需求、交互设计的深度洞察]" + +### 关于商业价值 +> "[对盈利模式、市场前景的战略思考]" + +### 核心共识 +> "[团队达成的最重要产品开发共识]" + +## 📊 开发决策 + +- 技术决策: [确定的技术方案和架构选择] +- 功能决策: [优先开发的功能和延后的功能] +- 资源决策: [人员调配和预算分配调整] + +## 📌 后续行动 + +- [ ] 具体行动项目1 +- [ ] 具体行动项目2 +- [ ] 具体行动项目3 +- [ ] 具体行动项目4 +- [ ] 具体行动项目5 +- [ ] 具体行动项目6 \ No newline at end of file diff --git a/template_guides/template1.md b/template_guides/template1.md index 13a70d7..de9683a 100644 --- a/template_guides/template1.md +++ b/template_guides/template1.md @@ -1,10 +1,10 @@ -- 严格保留模板的Markdown标题层级(## 主模块、### 子模块),主模块标题可保留原模板的Emoji标识。 -- 所有方括号 `[]` 内的文本、示例标题(如“议题1/2/3”“关于产品定位”)及括号提示语均为结构占位符,生成时必须替换为真实会议内容,严禁原样输出。 -- 若某模块或子项在会议实际内容中无对应信息,应直接省略该部分,不得留空或强行补全占位符。 -- 各模块下的子条目数量(如议题数量、观点子项、行动项等)允许根据实际讨论内容动态增减,不强制匹配模板示例数量。 -- 保留问答对的固定排版:使用 `Q: ` 与 `A: ` 标识问答内容,不同问答对之间以 `---` 分隔。 -- 保留观点陈述的引用块格式:统一使用 `> ` 开头包裹具体观点或共识内容。 -- 保留行动项的复选框格式:统一使用 `- [ ] ` 开头列出后续待办任务。 -- 保留分析类与决策类内容的键值对列表格式:使用 `- 标签: 内容` 的结构呈现各项分析或决议。 -- 子模块标题可根据实际议题语义进行重命名,但需维持原模板的模块分类逻辑与层级归属。 -- 生成内容需严格遵循“有则保留对应格式,无则直接省略该条目”的原则,禁止输出任何未经验证的结构化空壳或示例文字。 \ No newline at end of file + + +- 宏观结构可保留,但二级标题模块及三级子项允许按实际内容动态增删,严禁为填充模板强行保留无依据的空白示例项。 +- 所有 `[...]` 占位符、示例性标题(如议题1/2/3、关于产品定位等)及示例描述必须替换为会议真实信息,禁止原样输出。 +- 示例中的固定子标签(如需求分析、技术评估、开发进度等)仅为参考模板,需根据实际讨论维度灵活调整或替换为通用分类。 +- 问答内容必须严格遵循 `Q: [问题] \n A: [回答]` 的配对结构,支持多条问答独立成组,问答对之间可保留分隔线。 +- 重要观点需统一使用 Markdown 引用块(`> `)格式输出,每个观点独立成段,小标题应随实际议题动态生成。 +- 后续行动项必须使用 Markdown 复选框列表(`- [ ]`)格式,条目数量严格对应实际待办任务,无明确行动项时该整节应省略。 +- 决策项、观点项等列表内容允许数量增减,需保持缩进层级与符号一致性,不强制限定固定条目数。 +- 输出需严格遵循 Markdown 排版规范,确保标题层级、列表、引用块、复选框等具有结构意义的格式在有内容时完整保留。 \ No newline at end of file diff --git a/template_guides/test.md b/template_guides/test.md new file mode 100644 index 0000000..de484c5 --- /dev/null +++ b/template_guides/test.md @@ -0,0 +1,9 @@ +- 标题层级必须保留:使用 `##` 作为一级标题,`###` 作为二级标题 +- 所有带方括号的占位符 `[...]` 需替换为真实内容,不能保留原占位格式 +- "核心议题"下的子议题数量不固定,可根据实际会议内容增删,但每个议题的结构(需求分析/技术评估/资源规划 或 设计方案/原型验证/标准制定 或 开发进度/技术难点/测试策略)需保留 +- "关键问答"部分采用固定的 `Q:` 和 `A:` 格式,问答对数量可根据实际内容增减 +- "重要观点"下的五个分类(产品定位、技术选型、用户体验、商业价值、核心共识)为固定结构,不可删除或合并,但无内容时可省略该观点 +- "开发决策"和"后续行动"部分的列表项数量可根据实际情况调整 +- "后续行动"必须保留复选框格式 `- [ ]`,有具体行动时勾选或保持空白 +- 引用块格式 `> "..."` 必须保留,用于呈现重要观点和核心共识 +- 空白章节或无实质内容的子项应省略,不强行补齐 \ No newline at end of file diff --git a/web/__pycache__/server.cpython-314.pyc b/web/__pycache__/server.cpython-314.pyc index aaf7669dfc8e2e38e0f966d33bc2ce1ea743af04..f48711100ca239ec369a8248b0fd33d8609f43fa 100644 GIT binary patch delta 17070 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 delta 9011 zcmahu4R}=5vG?rmy}S8ONFcwEZ2n*gNd!U?B7`Im{sQ^A#2_SL*<^3Xl4LiW-S88! zi69E12#)=#s3_Ii;`^%5RiCsf)++U@{Q+tPZ*4_C``*{SYQqC+AFtXsb8dDw;q$c% znVoaaoS8Xu=FFKh`{7~pXFoK>7sba#OZd6Fgl!FU9Eneh|1?UHT+*nd9!Qk}?o(wS znP8#$dr?`bk~%$A>Q98v%}cWHQ)eO00u`zQ`)5;spi(VDe2$=5W0~o@Po0nLGHN5` zV)dX}j_r!^cC}imE>x>jrrxS9C^f5#aBQ&{GX<8Y)!14>tw4=hgLtjLYt=f$mkPX2 zU50qQz?XK;X+8j~J~?xF8Kc!mlDZ;QDln8DBgYzCEt>L$FRApz1T~cog zG3ZR5*qU!r+i?1Nn%>XCN;M*bCy9am_Ru_a1JZ4rB&In`)gGqmK&sA3R4t)Jz!@bf zO?4u{=1Bx}7jC9ey&R#@ZW!y2`Qx)XZBvbG?rUTCux*du)&?AtnteH+dcYhgWb z(wfc0b|w#mxp;Agk7mG_)ozdU4`TneNt`wyCt=nC9-e)_>PNZ&(M_=5#*ns|Lt)D8 z6WpRbG(LStczh@F*)>UChj5+<#V}IbF^Qrx#3y-on9rR^^$nteIZ<}|NVtC#`|p}G z+ZmdD>z**x1L|I+yPN1HtY>qlOWjv_yCk`@Bq>YUB&lpwSD>r2tNAR{=A>kG&Qb4C z?^X9#n2m#xjRbk(&v7XDd2~O6W0~{iKbmk9eh^z%IR_DcGJ;X*3(3lqD+C^Pi?6H1 zqMJbIqaZ?zuO5Z!8F_Ha?TJvDv&pVnR9RJ2YpFR;a@cOjLxG;+$`eetY2I$1+vD8= zdP;eob;#@S&z(14j~?*3wSK49HJ}a0wCvEl^GnK$=P#&p$naIxw2>4Ppv(O}m)oyL zan0p+2DF`l6B2je0G~;tu}&IxcKf_No-MoryTm$HA~Gs{Xq|G=nttAze$kqF!J7GD zLh^&d_YFT@bTOm!d`9Ujspm3Ef1OeL>x8;~du7pCH-%?OJFJbvPho)zPY_x?hps zaJ`qg^4z+O=PViTTiQuG6Su=t+4-yr-p>A&Vn~IfIkTtEM;@Ha$m!Jb1q90BgPd%6 z1)^2(WzM}ZxrphqKi~>zFqm&=HE_!Q)Qm;gi%Ku288rk1ms;v&d=>a|bL0kW)WE~J zBWZ#~^nj)XaEqNDcO!1O*&pC=>dq{-3|8jt%BV-;Mu&-$Tuw6)UrYEZ_-$T++=A#b zkn^{&g<>VE;i3Eo;!262#2_w(IR$BPtww`P$XlVMz%H*xbT#ZKaOBB|g+4Y!$beUK z(a^b4Hd?YxYL?)5Z!)Z#*~XgTxtZ0bHY+4Gr9}6j+h<9t4V!QHQWEEzc1ojiw$xCA zH_JFpMz~Xzq$p{Iq+0W&1!fO(M8O)zid=MM*G^}^*Qa^?x`IpBTmy~5Y5hJg8pUCS z=Axo$yd8DoT?iaDUFq)kXx;$tgyThvO1EIApVTg-Pfkvb;T$o}{{8`{!H|1#xS8m_ zB|+MpzJ-@!5+7WAVDW{RoJ&)tKQi>>$fG0Yrj)!FTXK0y<|DT~+4*Saxhb>Xi=BNn zMw*iGKYlv2d+Orq5*Q3_S({3`X6yl33LU(OItt*3J)3pItHoc|g#DLin#@6 z=tr_vo`G;VUyP84g*Q4Bz8!mXWzfaZV7ffe=l1ZM-O_BVWtisffDLmCOm74DvU ziseH2ylmDa-8#mUjU;gc z%v#(Oh07sE{3#e++_-KoqPV|t)I;H$(|P8F1Rf(m3OS<2XHW-81@*zQtEgwl80;yo8fbpR>!Cu*g9YmTu$+$gxeYNKmm-Pk#joZC4mc`o#haz^l+SJ%u-?&VeoG#7@ z*@5iwy0xABGWLEbL6yqm1Yc%&S~;9nvy@M8@M8&nt9BSy&#M2#j&LF*v$QJ`g2!9M zm*CxvS4#L3h1g#FyZRmiEmMtXfJj4HGwq zBCa;R2PY0EuJ!o+cu(Z?c(i`EzbZ#J`TAO`%{a!zElB6YxC$}uBJEI@Ukm7#K5ds@ zgfYBtagXYd3b&p7S+{X*+mMHAZodheg*hBrZc9n|6XO4Ye}2pyr9a}e_(?(~jNG;) z%aV9F`8~_jORGX9d5!t(~#@;D+_t=EBc#BLAGgUkFg#)-64r zeoeQKyYk;r?^|$eW7dec7JftR_X%7iApA;PCsvQ&N%Q!l)cd za+BIuX!?H>5c-mcMu*k7$RZkF$8M6?Lu!!HLRUQQe#WuGPMn{#$LxMYBPD0XBsC54l zFJG%x%&LP6v~<=+IbS1x9^s9owMN32gO^55k8cQXtUq9XtFe@EQ4wPZDI%PPMWL%F zqZkYg&-4_?(-2(@zwG(*R9tE3J&M8<~Rj{_IU>$#(DBdFQLj;Z_ z-9~OruNrYQxj4oDz#)3f>_FZWx%juJBi0h|Aek`Zo$#B!!o@W-MwtsbRe<-1VOlWW z72)OZ*NBKrsmFw}ZaNd~P}x6EE<`)54*L4JRvD@&!DR;WJ>k*=%A|L_jD({y=vdiMHZf zQp5C=&!OQwxUg%sG7nL*!|+#$!Xt=~XVJK!%mEghd&gAPDRQVH8Wh_tD1?(HF2}8m zXjPRno{ZkC*8-Y5K^sGe(ByS^dUw83jzae{_{Hw`jUAbw#6t{zb>|25Vn-H`ZeOE~ z!=lI5-(HL9Mg6*)vI`Q84lWqJ4xfD^SE)iKM_6#`$e-C-5p2kF^r&8!-??STz5Ky)%?Lobfb+guCA~woz~uN8iX_PU05t z`#=$L_Q1#cGAko{X*jlQII$2C5nMx(%!VYvt{uwm$x&9JxEB~~yyr1x38Le^a+(FR z?yX`qk;NITzjh_MGUV;|c>6@!hw(^ge2P{a1lXUeG$P+u82o7e?VZ#e7 zc>Mk+*a#W1QDh~21&v0Q8&Snrxb16gDxB(@BvmL>gIGu*!^AX1mhf8;&{NlV=_ZRR zNB~|t6tA?R;PWha{!lrau;IsPrr~HGF{nJebJlazBf2BTVf-cR4VhdFw!@{v4rM(u z{XGNgk@>+*$5yft8@7d^swqL@QPiY9jI=R?|LfC%r_M^0bk&P2E3}qaagPpi-@stN z&o>~=r}*c0BOT>jm*W#hS3tp2XD9CcUl>d|{?RHj_wbZvF?C|xF}8H_Nx}!8@a2fQ zkwa7qRo~81I#BHA3_8C3y@^}>1%sleFV-)|L1ZDKd*omUW;LhAEv&4E#+J zGuff>vWb`?lGum~=uRT9Azh5i#>hKpZF-D-SaG%QK{mV#wV1F}3d?#zC~X*5o~IGzT=??)>88KQ zkoNqimi1H0EKv1>nJfxwUMRN5swOpV6I0{cENViVQfe+Wg(}q9680s+p%<1WBu(t? zh=$8AWP)i^@)VW+C0F3m=#wpFN?-^3t>zGmK`g$hDRW|sae~ds5j=!-#D}ov6u=^P zN}Elcy2+wW3l+K5R5cB1f_XANxzu#{`N>;Z2E6*bT~1OnD;S)rNru`}DN$MLro>9~ zOmpJk*z?obbl82WAYn#Gxk1yql;(8!xFZirY89(V%^q)Oz_~?95c7lNuQ!e9pP1$( znlS^7k!p@^iw3qSCHH27v_-2qla~+YA4ry?LMxhX#}(zaMU}FN7FP4~q#UVZ8jJ>~ zF8!D0Yc&(M&^oiF=~6vgv)3ctYMw56nBa3MSS07Erj97Ey=d>uABd02?n zQNTaLt)NP$D=_5eiPVV0=Ng8FZcV&o`9|#H8wfl@qw#K6z~u~+@MmG~i@8}tG(JG% zRDs^2@yDqnpFFl5&b~M!=_lBpR1k#gFSfC5u;!&TBX80?i`(bbbj8RYby;NYjgELx zN7BuMLjj|hnt7BH6Z=Qxx$IISvP3&XsjDAM!%>tC>^qdoG0vGe+pYjY?yta=83*Z}KbC z-pF{T^Znx9^NEhb(H|w}99axWkEFrO)Asn>-=xhx8hs^3D)F$f_+aqgE7(7wpJ^3@ z*W#h|^g31nCr%f!N_hYD+>zSq23dMjnN4s(EyJ&$RMc8mr%P{D*Ct`>Y+|hmzkZrL zAK^POME6cr?R5P5S$aW($^5fwGveS9W^dJtbUQjXUP3zZaDhbcbOmR z9$#BLoJ7Azm5SNL7|^liKODEz)!nNVFU8s@_w^6kyuRXY>KTStAI}{bj_%X6!D3gx zXS;@lGW^NJ?QuB+y9PDg+KWGMY25Efy7>v#f|^gb79-g!`x_D0@_NDoA2Ai~K>;&49!kv%OW4dzi({C-BkMmJ+o9)G#OGDpBD{{xLb zOrU~59f1`D8VHC@Ur(5bT%u}5RRJU68#zSiHwjRIoQsOGa0ih-oufYSKBqd5k=0x# z>=Ocakam9}jOs+9$it~x!>NFRMLZw6GSv|99xe^J;UjN!6|p=lxvH;ms18RQbkUQ8>f_rlf#hxsYK#*>eiv{o^Bua8g4fWHNy>m z0v>oXJ^BTt#Q&1NB6#x6l?-CuYMgal!C$7Yr_ish*KKAt^Li{}E`erVk2SNE>{@Ih zE4p3?_rH~A8Y_gC-x^K867E&AWXc#bAvTr>^>2SKn0B^_!972HGKn%M8Z8|&5jxfZ I dict: - if CONFIG_FILE.exists(): - return json.loads(CONFIG_FILE.read_text(encoding="utf-8")) +def _normalize_config(raw: dict | None = None) -> dict: + data = dict(raw or {}) + profiles = data.get("api_profiles") or [] + default_max_tokens = int(data.get("max_tokens", 64000) or 64000) + + # Backward compatibility: migrate older single-config or multi-key formats. + if not profiles: + if data.get("api_keys"): + profiles = [ + { + "name": item.get("name", f"?? {index}"), + "api_base_url": data.get("api_base_url", "http://10.100.53.199:9527/v1"), + "api_key": item.get("key", ""), + "model_name": data.get("model_name", "Qwen3.6-35B"), + "max_tokens": int(item.get("max_tokens", default_max_tokens) or default_max_tokens), + } + for index, item in enumerate(data.get("api_keys") or [], start=1) + ] + elif data.get("api_key"): + profiles = [ + { + "name": "????", + "api_base_url": data.get("api_base_url", "http://10.100.53.199:9527/v1"), + "api_key": data.get("api_key", ""), + "model_name": data.get("model_name", "Qwen3.6-35B"), + "max_tokens": default_max_tokens, + } + ] + + normalized_profiles = [] + for index, item in enumerate(profiles, start=1): + if not isinstance(item, dict): + continue + api_key = str(item.get("api_key", "")).strip() + api_base_url = str(item.get("api_base_url", "")).strip() + model_name = str(item.get("model_name", "")).strip() + max_tokens = int(item.get("max_tokens", default_max_tokens) or default_max_tokens) + if not api_key or not api_base_url or not model_name: + continue + profile_name = str(item.get("name", "")).strip() or f"?? {index}" + normalized_profiles.append( + { + "name": profile_name, + "api_base_url": api_base_url, + "api_key": api_key, + "model_name": model_name, + "max_tokens": max_tokens, + } + ) + + if not normalized_profiles: + normalized_profiles = [ + { + "name": "????", + "api_base_url": "http://10.100.53.199:9527/v1", + "api_key": "unis123", + "model_name": "Qwen3.6-35B", + "max_tokens": default_max_tokens, + } + ] + + active_profile_name = str(data.get("active_api_profile_name") or data.get("active_api_key_name") or "").strip() + if not any(item["name"] == active_profile_name for item in normalized_profiles): + active_profile_name = normalized_profiles[0]["name"] + + active_profile = next( + (item for item in normalized_profiles if item["name"] == active_profile_name), + normalized_profiles[0], + ) return { - "api_base_url": "http://10.100.53.199:9527/v1", - "api_key": "unis123", - "model_name": "Qwen3.6-35B", + "api_profiles": normalized_profiles, + "active_api_profile_name": active_profile_name, + "api_base_url": active_profile["api_base_url"], + "api_key": active_profile["api_key"], + "model_name": active_profile["model_name"], + "max_tokens": active_profile["max_tokens"], } +def _load_config() -> dict: + if CONFIG_FILE.exists(): + return _normalize_config(json.loads(CONFIG_FILE.read_text(encoding="utf-8"))) + return _normalize_config() + + def _save_config(cfg: dict): - CONFIG_FILE.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8") + CONFIG_FILE.write_text( + json.dumps(_normalize_config(cfg), ensure_ascii=False, indent=2), + encoding="utf-8", + ) def _load_app_state() -> dict: @@ -77,6 +156,10 @@ def _get_llm_client(cfg: dict): return OpenAI(api_key=cfg["api_key"], base_url=cfg["api_base_url"]) +def _cfg_max_tokens(cfg: dict) -> int: + return int(cfg.get("max_tokens", 64000) or 64000) + + def _llm_stream(client, model, system_prompt, user_prompt, max_token=64000): response = client.chat.completions.create( model=model, @@ -90,10 +173,20 @@ def _llm_stream(client, model, system_prompt, user_prompt, max_token=64000): ) for chunk in response: delta = chunk.choices[0].delta - if delta.content is None: - yield "reasoning", delta.reasoning - else: - yield "content", delta.content + content = getattr(delta, "content", None) + reasoning = ( + getattr(delta, "reasoning", None) + or getattr(delta, "reasoning_content", None) + ) + if isinstance(content, list): + content = "".join(str(item) for item in content if item) + if isinstance(reasoning, list): + reasoning = "".join(str(item) for item in reasoning if item) + + if reasoning: + yield "reasoning", reasoning + if content: + yield "content", content def _read_meeting_meta(meeting_id: str) -> dict: @@ -154,6 +247,23 @@ def _parse_template_guide( return _collect_llm_content(client, config["model_name"], system_prompt, user_prompt) +def _build_template_guide_prompts( + template_name: str, + template_content: str, + user_notes: str = "", +) -> tuple[dict, str, str]: + prompt = load_prompt("templatet_parser", "zh") + cfg = _load_config() + system_prompt = prompt["system"]["role"] + prompt["mode_contracts"]["parse_template_requirements"] + user_prompt = prompt["user_template"]["template_input"].format( + template_name=template_name, + template_content=template_content, + ) + if user_notes.strip(): + user_prompt += f"\n\n??????????????????????\n{user_notes.strip()}" + return cfg, system_prompt, user_prompt + + def _ensure_template_guide( template_name: str, *, @@ -229,10 +339,13 @@ async def get_settings(): @app.put("/api/settings") async def save_settings(cfg: dict): - required = {"api_base_url", "api_key", "model_name"} + required = {"api_profiles", "active_api_profile_name"} if not required.issubset(cfg.keys()): raise HTTPException(400, f"Missing fields: {required - set(cfg.keys())}") - _save_config(cfg) + normalized = _normalize_config(cfg) + if not normalized["api_profiles"]: + raise HTTPException(400, "At least one API profile is required") + _save_config(normalized) return {"ok": True} @@ -359,6 +472,37 @@ async def import_meeting(name: str = Form(...), file: UploadFile = File(...)): return {"id": meeting_id, "name": name} +@app.post("/api/templates/import") +async def import_template(name: str = Form(...), file: UploadFile = File(...)): + if not file.filename: + raise HTTPException(400, "No file selected") + + template_name = name.strip() + if not template_name: + raise HTTPException(400, "Template name is required") + if not template_name.lower().endswith(".md"): + template_name += ".md" + + ext = Path(file.filename).suffix.lower() + if ext != ".md": + raise HTTPException(400, "Only .md template files are supported") + + content = await file.read() + try: + text = content.decode("utf-8") + except UnicodeDecodeError: + text = content.decode("gbk", errors="replace") + + target = _resolve_child(TEMPLATE_DIR, template_name) + target.write_text(text, encoding="utf-8") + + guide_path = _guide_path(template_name) + if guide_path.exists(): + guide_path.unlink() + + return {"name": template_name} + + @app.delete("/api/meetings/{meeting_id}") async def delete_meeting(meeting_id: str): if not (DATA_DIR / meeting_id).exists(): @@ -481,6 +625,69 @@ async def reparse_template_guide(name: str, user_notes: str = ""): return {"name": name, "content": content} +@app.get("/api/templates/{name}/guide/reparse/stream") +async def reparse_template_guide_stream(name: str, request: Request, user_notes: str = ""): + template_path = _resolve_child(TEMPLATE_DIR, name) + if not template_path.exists(): + raise HTTPException(404, f"Template not found: {name}") + + template_content = template_path.read_text(encoding="utf-8") + events = queue.Queue() + + def run(): + try: + lock = _get_template_lock(name) + with lock: + cfg, system_prompt, user_prompt = _build_template_guide_prompts(name, template_content, user_notes) + client = _get_llm_client(cfg) + events.put({"type": "status", "data": "parsing"}) + + result = "" + for chunk_type, chunk_content in _llm_stream( + client, + cfg["model_name"], + system_prompt, + user_prompt, + max_token=_cfg_max_tokens(cfg), + ): + if not chunk_content: + continue + text = str(chunk_content) + events.put({"type": "chunk", "data": {"chunk_type": chunk_type, "text": text}}) + if chunk_type == "content": + result += text + + _guide_path(name).write_text(result, encoding="utf-8") + events.put({"type": "done", "data": {"name": name, "content": result}}) + except Exception as exc: + events.put({"type": "error", "data": str(exc)}) + + threading.Thread(target=run, daemon=True).start() + + async def gen(): + loop = asyncio.get_running_loop() + while True: + if await request.is_disconnected(): + break + try: + event = await loop.run_in_executor(None, events.get, True, 0.5) + yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n" + if event["type"] in {"done", "error"}: + break + except queue.Empty: + yield ": heartbeat\n\n" + + return StreamingResponse( + gen(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + "Connection": "keep-alive", + }, + ) + + @app.get("/api/prompts") async def list_prompts(): prompts = [] @@ -567,7 +774,13 @@ async def process_meeting( user_prompt = prompt["user_template"]["article_preproces"].format(article=transcript) sub_topics = "" - for chunk_type, chunk_content in _llm_stream(client, model_name, system_prompt, user_prompt): + for chunk_type, chunk_content in _llm_stream( + client, + model_name, + system_prompt, + user_prompt, + max_token=_cfg_max_tokens(cfg), + ): if chunk_content: text = str(chunk_content) events.put({"type": "chunk", "data": {"stage": 1, "chunk_type": chunk_type, "text": text}}) @@ -601,7 +814,13 @@ async def process_meeting( user_prompt += f"\n\n用户补充要点(优先参考,可为空):\n{user_notes.strip()}" result = "" - for chunk_type, chunk_content in _llm_stream(client, model_name, system_prompt, user_prompt): + for chunk_type, chunk_content in _llm_stream( + client, + model_name, + system_prompt, + user_prompt, + max_token=_cfg_max_tokens(cfg), + ): if chunk_content: text = str(chunk_content) events.put({"type": "chunk", "data": {"stage": 2, "chunk_type": chunk_type, "text": text}})