流程全通过版本
parent
c8cdd3325c
commit
bdbdc674c6
20
config.json
20
config.json
|
|
@ -1,5 +1,23 @@
|
||||||
{
|
{
|
||||||
|
"api_profiles": [
|
||||||
|
{
|
||||||
|
"name": "默认接口",
|
||||||
"api_base_url": "https://api.llm.unissense.tech/v1",
|
"api_base_url": "https://api.llm.unissense.tech/v1",
|
||||||
"api_key": "unis123",
|
"api_key": "unis123",
|
||||||
"model_name": "Qwen3.6-35B"
|
"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
|
||||||
}
|
}
|
||||||
|
|
@ -1,54 +1,76 @@
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"sub_topics": [
|
"sub_topics": [
|
||||||
{
|
{
|
||||||
"title": "宽带装维上门进度与质量指标通报",
|
"title": "网络运维指标汇报",
|
||||||
"time_interval": "00:00:01 - 00:02:06",
|
"time_interval": "00:00:01 - 00:03:56",
|
||||||
"overview": "会议回顾宽带装维上门量与安装进度的平衡情况,当前累计上门量超1000户,距3000户目标进度偏后,周均上门量约1000次,月均约4000次。关键质量指标方面,弱光率当前为0.51即将达到0.5目标,三类终端指标较上周下降0.1个百分点,FTDR已达标,但主动跟进指标相对靠后。"
|
"overview": "汇报宽带上门安装及故障处理情况,上周上门量580户,累计超1000户,目标3000户进度滞后。弱光指标0.51%接近0.5%目标,三类终端年度目标从7.5%压降至5.5%。九零工程转化率月度87.35%接近90%目标,退单率6.53%,退单主要原因包括用户原因29单、覆盖6单、天气2单。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "九零工程转化率与退单率分析",
|
"title": "PCDN专线及三创空间指标",
|
||||||
"time_interval": "00:02:07 - 00:03:51",
|
"time_interval": "00:03:53 - 00:05:52",
|
||||||
"overview": "汇报“九零工程”月度转化率87.35%(目标90%)及退单率6.53%(目标低于7.5%),退单主因包括改约、用户原因29单及天气影响2单。针对蓝单转化率仅75.29%及退单率偏高问题,提出将退单率纳入综合运维考核,并建议剔除分局长审批同意的退单以优化考核公平性。"
|
"overview": "PCDN专线指标持续恶化,3月份达4.84%,主要涉及人文和海师两所学校,已报市公司分析IP地址并协调学校限速。三创空间物业点本月未达标,因天线缺货预计4月初检测,其他指标达标。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "专线质量恶化与PCDN治理探讨",
|
"title": "畅听、超频站点及专线巡检",
|
||||||
"time_interval": "00:03:53 - 00:05:31",
|
"time_interval": "00:05:33 - 00:08:13",
|
||||||
"overview": "专线指标持续恶化,主要问题集中在人文与海师两所学校,已报市公司分析出口IP地址。会议探讨了建立后台自动限速机制的可行性,明确学校互联网带宽虽免费但需规范使用,后续发现问题将第一时间通过系统上线处理并协同全资方落实管控。"
|
"overview": "畅听一项未达标需智能判断和质检;5G基站参数调整试点待观察。超频站点发现3个问题基站,其中2个为铁塔开关电源故障,1个为传输光衰过大,均已处理。专线巡检方面,118条存量专线已完成56条,45条未验收工程全部巡检,3月底前完成全部巡检。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "网络质量专项攻坚与故障处理通报",
|
"title": "金属管家及工程项目进展",
|
||||||
"time_interval": "00:05:33 - 00:08:04",
|
"time_interval": "00:08:14 - 00:09:35",
|
||||||
"overview": "通报三创空间天线缺货延至4月初检测,畅听工单需智能体逐项核查,5G弱化问题正试点调整基站参数。超频站点攻坚发现3处故障(铁塔开关电源及传输光衰)均已恢复,专线护航118条中已巡检56条,剩余17条月底完成。当周共收故障33件,9件网络相关以光缆问题为主。"
|
"overview": "金属管家二级站还剩121站,上周已全部下电,累计拆除46站,剩余75站预计2周内完成拆除和资管转出,4月15日前全量完成。网络质量综合评估体系已内部收到,下周进行分解和目标制定。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "二级基站清理与工程下沉推进",
|
"title": "综合部25年工作汇报",
|
||||||
"time_interval": "00:08:07 - 00:09:35",
|
|
||||||
"overview": "二级基站清理工作进展顺利,2026年剩余121站已全部下电,累计拆除46站,预计两周内完成剩余75站拆除及资费调整,确保4月15日前全量完成。工程下沉项目正常开展,已接收网络质量综合评估体系,下周将针对该体系进行指标分解与目标制定。"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "综合部行政后勤与工会活动筹备",
|
|
||||||
"time_interval": "00:09:46 - 00:12:39",
|
"time_interval": "00:09:46 - 00:12:39",
|
||||||
"overview": "综合部汇报无人机验收待推动、招投标打印机外协洽谈(内外价差超十倍)及气象/渠道/专线发展计划。工会经费压降背景下,推进食堂改造(预算29.8万元)及直升机引水项目获批。筹备第四届体育文化节,确定“河川佳绩,穿越巅峰”主题,正协调各部门补充方阵人员并安排周二至周四排练。"
|
"overview": "汇报25年剩余2项工作:花果山无变化,无人机项目待拜访唐书记推动。上周完成5项工作:建委施工资管框架整理、投资计划专项评估汇报、终端占比问题汇报、收入地图完成、打印机问题已与极客洽谈并预留招投标打印机。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "招待费管控与区级评优申报研判",
|
"title": "工会及后勤工作",
|
||||||
"time_interval": "00:12:40 - 00:27:30",
|
"time_interval": "00:12:40 - 00:15:20",
|
||||||
"overview": "通报2025年招待费超年初预算14%,综合部2024年曾超支107%,2026年仅综合部调整预算,要求统筹正气部对外招待并加快报销结算。针对河川区“担当作为”评优申报,因往届多为政府机构且评选流程未明,领导指示需提前摸底决策领导意向后再决定是否投入精力,避免形式主义消耗。"
|
"overview": "工会经费因市公司压价需节电耗用,本周上报暖心工程包括根石和食堂改造,食堂改造预算29.8万元计划5月更换修补。拟采用直升机方式解决用水问题,已报市公司批准。幸福一加一活动全年安排6场大型活动,近期筹备六一儿童节活动。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "商客市场拓展与“查企”数据复盘",
|
"title": "体育文化节筹备",
|
||||||
"time_interval": "00:27:32 - 00:34:04",
|
"time_interval": "00:15:21 - 00:19:34",
|
||||||
"overview": "“查企”工作累计完成690万收入与1110户发展,主要采取低ARPU价值提升与融合套餐策略,本周开展5场社区/单位营销但龙狮业务转化不佳。商客市场1月收入88.5万元(同比增1万),价值拓展排名全市第6,个别分局增幅超2万,但部分经理周基础业务仅2笔,进度滞后需加强。"
|
"overview": "第五届体育文化节主题为河川佳绩、穿越巅峰、东亚赛场,需25人组成方阵,目前差9人,各部门需再安排2人参加。方阵人员周二至周四排练,周五参加活动,参加人员有绩效加分。会上讨论部分主力选手因伤无法参加。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "工作作风整顿与满意度考核部署",
|
"title": "招待费公开及主题教育",
|
||||||
"time_interval": "00:34:04 - 00:42:32",
|
"time_interval": "00:19:36 - 00:20:34",
|
||||||
"overview": "领导严厉批评部分工作回复滞后、缺乏量化措施,要求养成“日事日清”习惯并今日提交专线与商客指标保障方案。市场部满意度测评垫底,指出正气部负责人缺席客户会导致工作松懈,要求本周内汇报招聘、农村渠道及行销转型进度。明确今年KPI考核将纳入工信部有责与离网率,要求吃透市公司技巧确保3月底“八零两绿”达标。"
|
"overview": "主题教育市公司已发布第1期简报,基层党组织截至3月15日已开展学习。招待费信息公开已通过邮件发布并向纪检组织报备,公开对象为本单位管理层班子成员。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "年度考核指标预沟通与会议总结",
|
"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",
|
"time_interval": "00:42:33 - 00:46:16",
|
||||||
"overview": "会议最后强调全员需强化执行力,杜绝“知而不做”的作风,要求各部门提前与市公司对接年度考核口径与潜在指标(如APP提升考核),避免起跑线落后。领导指出考核争取公平得分需靠主动沟通与前期铺垫,各部门需联动协同,周会至此结束。"
|
"overview": "强调执行力,要求知道怎么做就要去做,不认可知道但不去做的工作风格。市公司年度考核涉及多个方面,部分考核可能影响较大,需提前沟通了解考核口径,不要等定好了再说。建议与分管领导加强联动,确保在同一起跑线上。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
@ -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. **装维与质量指标方面:**
|
1. **工作习惯要求:**
|
||||||
- 兼顾安装与上门量,加快追赶3000户目标进度
|
- 养成日事日清、日清日结、日结日高的好习惯
|
||||||
- 保持弱光率与三类终端改善势头,专项提升主动跟进指标
|
- 工作回复需量化、有结果、有措施、有成效
|
||||||
- 建议将退单率纳入综合运维考核,推动剔除分局长已审批同意的退单以保公平
|
- 工作进展需及时向安排工作的领导汇报,不能只在周报时反馈综合部
|
||||||
2. **工程与网络攻坚方面:**
|
|
||||||
- 2周内完成二级基站剩余75站拆除,确保4月15日前全量完成下电与资费调整
|
2. **评估体系推进:**
|
||||||
- 按期完成专线护航剩余17条巡检,存量光缆路由抢盘行动及时上报
|
- 建委综合评估体系已出台,要求按时间节点推进工作
|
||||||
- 下周内分解网络质量综合评估体系指标并制定阶段性目标
|
- 涉及吉卡尔号绕轨运输等工作需当天处理、及时回复
|
||||||
3. **故障处理与专项测试方面:**
|
|
||||||
- 持续压降超频站点及旁站故障,当周33件故障以光缆问题为主需重点排查
|
|
||||||
- 跟进三创空间天线检测及5G弱化基站参数试点调整效果
|
|
||||||
- 协同全资方建立专线自动限速机制,规范学校带宽使用并第一时间系统上线管控
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 分公司领导强调:
|
#### 市场部分管领导强调:
|
||||||
|
|
||||||
1. **市场与政企拓展方面:**
|
1. **满意度工作:**
|
||||||
- 深度复盘商客市场与“查企”数据,加强低ARPU价值提升与龙狮业务转化
|
- 满意度测评结果不理想,正气部是拉分项,廖经理参加客户会较少
|
||||||
- 严抓部分经理周基础业务仅2笔的滞后问题,以周为单位严格跟踪并加快分局推进节奏
|
- 要求廖经理亲自抓满意度工作,不能只是开学
|
||||||
- 扭转满意度测评垫底局面,政企部负责人须亲自抓客户会工作,杜绝思想松懈
|
- 80两绿指标3月底四公司有统一要求,需按汇通运营的满意样板操作
|
||||||
2. **行政后勤与费用管控方面:**
|
- 不满客户处理有技巧空间,5点半和6点半有不同操作方式
|
||||||
- 统筹政企部对外招待,加快报销结算,严控招待费超支风险(25年已超预算14%)
|
- 目前河川区满意度属于一类倒数第二,要求市场部每天在红星出日报
|
||||||
- 摸底区级“担当作为”评优申报决策领导意向,避免形式主义消耗
|
|
||||||
- 落实招投标打印机外协保障方案,确保不影响招投标业务开展
|
2. **市场业务推进:**
|
||||||
3. **作风建设与考核部署方面:**
|
- 季度收官不足,业务推进疲软,需提前谋划二季度业务活动
|
||||||
- 严厉批评工作回复滞后、缺乏量化措施,要求全员养成“日事日清、日结日高”习惯,今日提交专线与商客指标具体保障方案
|
- 不能因传统淡季放松,要造起签约、3人、H、AI终端等业务声势
|
||||||
- 市场部需本周内汇报招聘进度、农村渠道建设及行销队伍转型方案
|
- 本周内就招聘情况、农村渠道进度、行销方案三个事项做专题汇报
|
||||||
- 吃透市公司考核技巧,确保3月底“八零两绿”达标,今年KPI将纳入工信部有责与离网率
|
|
||||||
- 强化全员执行力,严禁“知而不做”作风,各部门需提前对接市公司年度考核口径(如APP提升考核),主动沟通争取公平得分,避免起跑线落后
|
---
|
||||||
|
|
||||||
|
#### 分公司领导总结:
|
||||||
|
|
||||||
|
1. **执行力要求:**
|
||||||
|
- 不认可知道怎么做但不去做的工作风格
|
||||||
|
- 事情要做好,要知道怎么做也要去做
|
||||||
|
|
||||||
|
2. **考核沟通:**
|
||||||
|
- 市公司年度考核涉及多个方面,部分考核可能影响较大
|
||||||
|
- 需提前沟通了解考核口径,不要等定好了再说
|
||||||
|
- 两个分管领导(政企部跟相关负责部门)要加强联动
|
||||||
|
- 确保在同一起跑线上,不要输在起跑线
|
||||||
|
|
@ -13,6 +13,7 @@ const state = {
|
||||||
processGuideEditMode: false,
|
processGuideEditMode: false,
|
||||||
selectedTreeKey: "",
|
selectedTreeKey: "",
|
||||||
rightResource: null,
|
rightResource: null,
|
||||||
|
settingsDraft: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = "meeting-workspace-preferences";
|
const STORAGE_KEY = "meeting-workspace-preferences";
|
||||||
|
|
@ -80,6 +81,19 @@ function closeModal(id) {
|
||||||
document.getElementById(id).classList.remove("show");
|
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) {
|
async function api(url, options) {
|
||||||
const res = await fetch(url, options || {});
|
const res = await fetch(url, options || {});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -173,6 +187,45 @@ function syncProcessGuideConfirmLabel() {
|
||||||
$("#btn-process-guide-confirm").textContent = meeting?.has_summary ? "重总结" : "确认";
|
$("#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() {
|
function showReparseGuidePrompt() {
|
||||||
$("#reparse-guide-notes").value = "";
|
$("#reparse-guide-notes").value = "";
|
||||||
$("#reparse-guide-prompt").hidden = false;
|
$("#reparse-guide-prompt").hidden = false;
|
||||||
|
|
@ -227,27 +280,64 @@ async function runStandaloneGuideReparseFlow() {
|
||||||
pushStandaloneReparseLine("已提交重解析请求,正在准备说明...");
|
pushStandaloneReparseLine("已提交重解析请求,正在准备说明...");
|
||||||
refreshActionButtons();
|
refreshActionButtons();
|
||||||
|
|
||||||
const hints = [
|
|
||||||
"正在结合你的补充说明整理解析重点...",
|
|
||||||
"正在按解析模板抽取可执行的使用说明...",
|
|
||||||
"即将刷新右侧资源并打开最新说明...",
|
|
||||||
];
|
|
||||||
let hintIndex = 0;
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
if (hintIndex < hints.length) {
|
|
||||||
pushStandaloneReparseLine(hints[hintIndex]);
|
|
||||||
hintIndex += 1;
|
|
||||||
}
|
|
||||||
}, 900);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runTemplateGuideReparse(templateName, userNotes);
|
const params = new URLSearchParams();
|
||||||
pushStandaloneReparseLine("模板说明已更新。");
|
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");
|
closeModal("modal-reparse-guide");
|
||||||
await openProcessGuideModal();
|
await openProcessGuideModal();
|
||||||
$("#process-guide-editor").value = result.content || "";
|
$("#process-guide-editor").value = result.content || "";
|
||||||
} finally {
|
} finally {
|
||||||
window.clearInterval(timer);
|
|
||||||
state.guideBusy = false;
|
state.guideBusy = false;
|
||||||
setStatus("right", false, "空闲");
|
setStatus("right", false, "空闲");
|
||||||
setStandaloneReparseProcessing(false);
|
setStandaloneReparseProcessing(false);
|
||||||
|
|
@ -1299,15 +1389,21 @@ $("#btn-import").addEventListener("click", () => {
|
||||||
openModal("modal-import");
|
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 () => {
|
$("#btn-confirm-import").addEventListener("click", async () => {
|
||||||
const name = $("#import-name").value.trim();
|
const name = $("#import-name").value.trim();
|
||||||
const file = $("#import-file").files[0];
|
const file = $("#import-file").files[0];
|
||||||
if (!name) {
|
if (!name) {
|
||||||
toast("请输入会议名称", "err");
|
toast("???????", "err");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!file) {
|
if (!file) {
|
||||||
toast("请选择转录文件", "err");
|
toast("???????", "err");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1318,48 +1414,159 @@ $("#btn-confirm-import").addEventListener("click", async () => {
|
||||||
const result = await fetch("/api/meetings/import", { method: "POST", body: formData });
|
const result = await fetch("/api/meetings/import", { method: "POST", body: formData });
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
const detail = await result.json().catch(() => ({ detail: "Import failed" }));
|
const detail = await result.json().catch(() => ({ detail: "Import failed" }));
|
||||||
toast(`导入失败:${detail.detail}`, "err");
|
toast(`?????${detail.detail}`, "err");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await result.json();
|
const payload = await result.json();
|
||||||
closeModal("modal-import");
|
closeModal("modal-import");
|
||||||
toast(`导入成功:${name}`);
|
toast(`?????${name}`);
|
||||||
await refresh();
|
await refresh();
|
||||||
await selectMeeting(payload.id);
|
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 () => {
|
$("#btn-settings").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const cfg = await api("/api/settings");
|
const cfg = await api("/api/settings");
|
||||||
$("#cfg-url").value = cfg.api_base_url || "";
|
loadSettingsDraft(cfg);
|
||||||
$("#cfg-key").value = cfg.api_key || "";
|
|
||||||
$("#cfg-model").value = cfg.model_name || "";
|
|
||||||
} finally {
|
} finally {
|
||||||
openModal("modal-settings");
|
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 () => {
|
$("#btn-save-settings").addEventListener("click", async () => {
|
||||||
const payload = {
|
if (!state.settingsDraft) {
|
||||||
api_base_url: $("#cfg-url").value.trim(),
|
return;
|
||||||
api_key: $("#cfg-key").value.trim(),
|
}
|
||||||
model_name: $("#cfg-model").value.trim(),
|
const currentName = $("#cfg-key-select").value;
|
||||||
};
|
const maxTokens = Number($("#cfg-max-tokens").value || 64000);
|
||||||
await api("/api/settings", {
|
if (!Number.isFinite(maxTokens) || maxTokens < 1) {
|
||||||
method: "PUT",
|
toast("max_tokens ????? 0 ???", "err");
|
||||||
headers: { "Content-Type": "application/json" },
|
return;
|
||||||
body: JSON.stringify(payload),
|
}
|
||||||
|
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");
|
state.settingsDraft.active_api_profile_name = modelName;
|
||||||
toast("配置已保存");
|
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) => {
|
$$("[data-close]").forEach((button) => {
|
||||||
button.addEventListener("click", () => closeModal(button.dataset.close));
|
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) => {
|
document.getElementById(id).addEventListener("click", (event) => {
|
||||||
if (event.target.id === id) {
|
if (event.target.id === id) {
|
||||||
if (id === "modal-reparse-guide" && state.guideBusy) {
|
if (id === "modal-reparse-guide" && state.guideBusy) {
|
||||||
|
|
|
||||||
|
|
@ -661,6 +661,18 @@ select.btn {
|
||||||
background: #f9fcff;
|
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 {
|
.form-field textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Meeting Summary</title>
|
<title>Meeting Summary</title>
|
||||||
<link rel="stylesheet" href="/assets/styles.css?v=20260511e">
|
<link rel="stylesheet" href="/assets/styles.css?v=20260511l">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/13.0.3/marked.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/13.0.3/marked.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -15,8 +15,9 @@
|
||||||
<h1>Meeting Summary Workspace</h1>
|
<h1>Meeting Summary Workspace</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
<button class="btn primary" id="btn-import">导入会议</button>
|
<button class="btn primary" id="btn-import">导入会议</button>
|
||||||
<button class="btn" id="btn-settings">设置</button>
|
<button class="btn" id="btn-import-template">导入模板</button>
|
||||||
|
<button class="btn" id="btn-settings">设置</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -170,45 +171,91 @@
|
||||||
|
|
||||||
<div class="modal-mask" id="modal-import">
|
<div class="modal-mask" id="modal-import">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3>导入会议转录</h3>
|
<h3>导入会议转录</h3>
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span>会议名称</span>
|
<span>会议名称</span>
|
||||||
<input type="text" id="import-name" placeholder="例如:2026-05-08 周例会">
|
<input type="text" id="import-name" placeholder="例如:2026-05-08 周例会">
|
||||||
</label>
|
</label>
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span>转录文件(.txt / .md)</span>
|
<span>转录文件(.txt / .md)</span>
|
||||||
<input type="file" id="import-file" accept=".txt,.md">
|
<input type="file" id="import-file" accept=".txt,.md">
|
||||||
</label>
|
</label>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn" data-close="modal-import">取消</button>
|
<button class="btn" data-close="modal-import">取消</button>
|
||||||
<button class="btn primary" id="btn-confirm-import">导入</button>
|
<button class="btn primary" id="btn-confirm-import">导入</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-mask" id="modal-import-template">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3>导入模板</h3>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>模板名称</span>
|
||||||
|
<input type="text" id="import-template-name" placeholder="例如:template3.md">
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>模板文件(.md)</span>
|
||||||
|
<input type="file" id="import-template-file" accept=".md">
|
||||||
|
</label>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn" data-close="modal-import-template">取消</button>
|
||||||
|
<button class="btn primary" id="btn-confirm-import-template">导入</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-mask" id="modal-settings">
|
<div class="modal-mask" id="modal-settings">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3>API 配置</h3>
|
<h3>API 配置</h3>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>当前模型</span>
|
||||||
|
<input type="text" id="cfg-current-model" readonly>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>模型列表</span>
|
||||||
|
<select id="cfg-key-select"></select>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Max Tokens</span>
|
||||||
|
<input type="number" id="cfg-max-tokens" min="1" step="1">
|
||||||
|
</label>
|
||||||
|
<div class="modal-actions settings-key-actions">
|
||||||
|
<button class="btn" id="btn-key-add">新增模型</button>
|
||||||
|
<button class="btn" id="btn-save-settings">保存当前</button>
|
||||||
|
<button class="btn" id="btn-key-delete">删除模型</button>
|
||||||
|
<button class="btn" data-close="modal-settings">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-mask" id="modal-add-model">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3>新增模型</h3>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Model Name</span>
|
||||||
|
<input type="text" id="cfg-add-model-name" placeholder="Qwen3.6-35B">
|
||||||
|
</label>
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span>Base URL</span>
|
<span>Base URL</span>
|
||||||
<input type="text" id="cfg-url" placeholder="http://host:port/v1">
|
<input type="text" id="cfg-add-base-url" placeholder="http://host:port/v1">
|
||||||
</label>
|
</label>
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span>API Key</span>
|
<span>API Key</span>
|
||||||
<input type="text" id="cfg-key" placeholder="your-api-key">
|
<input type="text" id="cfg-add-api-key" placeholder="your-api-key">
|
||||||
</label>
|
</label>
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span>Model Name</span>
|
<span>Max Tokens</span>
|
||||||
<input type="text" id="cfg-model" placeholder="Qwen3.6-35B">
|
<input type="number" id="cfg-add-max-tokens" min="1" step="1" placeholder="64000">
|
||||||
</label>
|
</label>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn" data-close="modal-settings">取消</button>
|
<button class="btn" data-close="modal-add-model">取消</button>
|
||||||
<button class="btn primary" id="btn-save-settings">保存</button>
|
<button class="btn primary" id="btn-confirm-add-model">保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
<script type="module" src="/assets/app.js?v=20260511e"></script>
|
<script type="module" src="/assets/app.js?v=20260511l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
## 📝 会议概述
|
||||||
|
[简述产品开发背景、当前阶段、主要议题和预期交付成果]
|
||||||
|
|
||||||
|
## 🚀 核心议题
|
||||||
|
|
||||||
|
### 议题1:[主题标题]
|
||||||
|
- 需求分析: [用户需求、市场需求、功能需求定义与优先级]
|
||||||
|
- 技术评估: [技术可行性、架构选择、开发难度评估]
|
||||||
|
- 资源规划: [人力配置、时间安排、预算分配]
|
||||||
|
|
||||||
|
### 议题2:[主题标题]
|
||||||
|
- 设计方案: [产品设计、UI/UX设计、交互逻辑设计]
|
||||||
|
- 原型验证: [原型测试结果、用户反馈、迭代方向]
|
||||||
|
- 标准制定: [开发规范、质量标准、验收标准]
|
||||||
|
|
||||||
|
### 议题3:[主题标题]
|
||||||
|
- 开发进度: [当前开发状态、里程碑达成、延期风险]
|
||||||
|
- 技术难点: [技术挑战、解决方案、备选方案]
|
||||||
|
- 测试策略: [测试计划、测试用例、Bug修复优先级]
|
||||||
|
|
||||||
|
## ❓ 关键问答
|
||||||
|
|
||||||
|
Q: [技术实现问题1]
|
||||||
|
A: [技术解决方案1]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Q: [产品设计问题2]
|
||||||
|
A: [设计决策说明2]
|
||||||
|
|
||||||
|
## 💡 重要观点
|
||||||
|
|
||||||
|
### 关于产品定位
|
||||||
|
> "[对产品市场定位、核心价值的关键判断]"
|
||||||
|
|
||||||
|
### 关于技术选型
|
||||||
|
> "[对技术架构、开发工具选择的专业建议]"
|
||||||
|
|
||||||
|
### 关于用户体验
|
||||||
|
> "[对用户需求、交互设计的深度洞察]"
|
||||||
|
|
||||||
|
### 关于商业价值
|
||||||
|
> "[对盈利模式、市场前景的战略思考]"
|
||||||
|
|
||||||
|
### 核心共识
|
||||||
|
> "[团队达成的最重要产品开发共识]"
|
||||||
|
|
||||||
|
## 📊 开发决策
|
||||||
|
|
||||||
|
- 技术决策: [确定的技术方案和架构选择]
|
||||||
|
- 功能决策: [优先开发的功能和延后的功能]
|
||||||
|
- 资源决策: [人员调配和预算分配调整]
|
||||||
|
|
||||||
|
## 📌 后续行动
|
||||||
|
|
||||||
|
- [ ] 具体行动项目1
|
||||||
|
- [ ] 具体行动项目2
|
||||||
|
- [ ] 具体行动项目3
|
||||||
|
- [ ] 具体行动项目4
|
||||||
|
- [ ] 具体行动项目5
|
||||||
|
- [ ] 具体行动项目6
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
- 严格保留模板的Markdown标题层级(## 主模块、### 子模块),主模块标题可保留原模板的Emoji标识。
|
|
||||||
- 所有方括号 `[]` 内的文本、示例标题(如“议题1/2/3”“关于产品定位”)及括号提示语均为结构占位符,生成时必须替换为真实会议内容,严禁原样输出。
|
|
||||||
- 若某模块或子项在会议实际内容中无对应信息,应直接省略该部分,不得留空或强行补全占位符。
|
- 宏观结构可保留,但二级标题模块及三级子项允许按实际内容动态增删,严禁为填充模板强行保留无依据的空白示例项。
|
||||||
- 各模块下的子条目数量(如议题数量、观点子项、行动项等)允许根据实际讨论内容动态增减,不强制匹配模板示例数量。
|
- 所有 `[...]` 占位符、示例性标题(如议题1/2/3、关于产品定位等)及示例描述必须替换为会议真实信息,禁止原样输出。
|
||||||
- 保留问答对的固定排版:使用 `Q: ` 与 `A: ` 标识问答内容,不同问答对之间以 `---` 分隔。
|
- 示例中的固定子标签(如需求分析、技术评估、开发进度等)仅为参考模板,需根据实际讨论维度灵活调整或替换为通用分类。
|
||||||
- 保留观点陈述的引用块格式:统一使用 `> ` 开头包裹具体观点或共识内容。
|
- 问答内容必须严格遵循 `Q: [问题] \n A: [回答]` 的配对结构,支持多条问答独立成组,问答对之间可保留分隔线。
|
||||||
- 保留行动项的复选框格式:统一使用 `- [ ] ` 开头列出后续待办任务。
|
- 重要观点需统一使用 Markdown 引用块(`> `)格式输出,每个观点独立成段,小标题应随实际议题动态生成。
|
||||||
- 保留分析类与决策类内容的键值对列表格式:使用 `- 标签: 内容` 的结构呈现各项分析或决议。
|
- 后续行动项必须使用 Markdown 复选框列表(`- [ ]`)格式,条目数量严格对应实际待办任务,无明确行动项时该整节应省略。
|
||||||
- 子模块标题可根据实际议题语义进行重命名,但需维持原模板的模块分类逻辑与层级归属。
|
- 决策项、观点项等列表内容允许数量增减,需保持缩进层级与符号一致性,不强制限定固定条目数。
|
||||||
- 生成内容需严格遵循“有则保留对应格式,无则直接省略该条目”的原则,禁止输出任何未经验证的结构化空壳或示例文字。
|
- 输出需严格遵循 Markdown 排版规范,确保标题层级、列表、引用块、复选框等具有结构意义的格式在有内容时完整保留。
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
- 标题层级必须保留:使用 `##` 作为一级标题,`###` 作为二级标题
|
||||||
|
- 所有带方括号的占位符 `[...]` 需替换为真实内容,不能保留原占位格式
|
||||||
|
- "核心议题"下的子议题数量不固定,可根据实际会议内容增删,但每个议题的结构(需求分析/技术评估/资源规划 或 设计方案/原型验证/标准制定 或 开发进度/技术难点/测试策略)需保留
|
||||||
|
- "关键问答"部分采用固定的 `Q:` 和 `A:` 格式,问答对数量可根据实际内容增减
|
||||||
|
- "重要观点"下的五个分类(产品定位、技术选型、用户体验、商业价值、核心共识)为固定结构,不可删除或合并,但无内容时可省略该观点
|
||||||
|
- "开发决策"和"后续行动"部分的列表项数量可根据实际情况调整
|
||||||
|
- "后续行动"必须保留复选框格式 `- [ ]`,有具体行动时勾选或保持空白
|
||||||
|
- 引用块格式 `> "..."` 必须保留,用于呈现重要观点和核心共识
|
||||||
|
- 空白章节或无实质内容的子项应省略,不强行补齐
|
||||||
Binary file not shown.
245
web/server.py
245
web/server.py
|
|
@ -33,6 +33,7 @@ DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
RESULTS_MD_DIR.mkdir(parents=True, exist_ok=True)
|
RESULTS_MD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
RESULTS_JSON_DIR.mkdir(parents=True, exist_ok=True)
|
RESULTS_JSON_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
FRONTEND_ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
FRONTEND_ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
TEMPLATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
TEMPLATE_GUIDE_DIR.mkdir(parents=True, exist_ok=True)
|
TEMPLATE_GUIDE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
app = FastAPI(title="Meeting Summary Web")
|
app = FastAPI(title="Meeting Summary Web")
|
||||||
|
|
@ -41,18 +42,96 @@ _template_lock_guard = threading.Lock()
|
||||||
_template_locks: dict[str, threading.Lock] = {}
|
_template_locks: dict[str, threading.Lock] = {}
|
||||||
|
|
||||||
|
|
||||||
def _load_config() -> dict:
|
def _normalize_config(raw: dict | None = None) -> dict:
|
||||||
if CONFIG_FILE.exists():
|
data = dict(raw or {})
|
||||||
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
profiles = data.get("api_profiles") or []
|
||||||
return {
|
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_base_url": "http://10.100.53.199:9527/v1",
|
||||||
"api_key": "unis123",
|
"api_key": "unis123",
|
||||||
"model_name": "Qwen3.6-35B",
|
"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_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):
|
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:
|
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"])
|
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):
|
def _llm_stream(client, model, system_prompt, user_prompt, max_token=64000):
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=model,
|
model=model,
|
||||||
|
|
@ -90,10 +173,20 @@ def _llm_stream(client, model, system_prompt, user_prompt, max_token=64000):
|
||||||
)
|
)
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
delta = chunk.choices[0].delta
|
delta = chunk.choices[0].delta
|
||||||
if delta.content is None:
|
content = getattr(delta, "content", None)
|
||||||
yield "reasoning", delta.reasoning
|
reasoning = (
|
||||||
else:
|
getattr(delta, "reasoning", None)
|
||||||
yield "content", delta.content
|
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:
|
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)
|
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(
|
def _ensure_template_guide(
|
||||||
template_name: str,
|
template_name: str,
|
||||||
*,
|
*,
|
||||||
|
|
@ -229,10 +339,13 @@ async def get_settings():
|
||||||
|
|
||||||
@app.put("/api/settings")
|
@app.put("/api/settings")
|
||||||
async def save_settings(cfg: dict):
|
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()):
|
if not required.issubset(cfg.keys()):
|
||||||
raise HTTPException(400, f"Missing fields: {required - set(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}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -359,6 +472,37 @@ async def import_meeting(name: str = Form(...), file: UploadFile = File(...)):
|
||||||
return {"id": meeting_id, "name": name}
|
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}")
|
@app.delete("/api/meetings/{meeting_id}")
|
||||||
async def delete_meeting(meeting_id: str):
|
async def delete_meeting(meeting_id: str):
|
||||||
if not (DATA_DIR / meeting_id).exists():
|
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}
|
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")
|
@app.get("/api/prompts")
|
||||||
async def list_prompts():
|
async def list_prompts():
|
||||||
prompts = []
|
prompts = []
|
||||||
|
|
@ -567,7 +774,13 @@ async def process_meeting(
|
||||||
user_prompt = prompt["user_template"]["article_preproces"].format(article=transcript)
|
user_prompt = prompt["user_template"]["article_preproces"].format(article=transcript)
|
||||||
|
|
||||||
sub_topics = ""
|
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:
|
if chunk_content:
|
||||||
text = str(chunk_content)
|
text = str(chunk_content)
|
||||||
events.put({"type": "chunk", "data": {"stage": 1, "chunk_type": chunk_type, "text": text}})
|
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()}"
|
user_prompt += f"\n\n用户补充要点(优先参考,可为空):\n{user_notes.strip()}"
|
||||||
|
|
||||||
result = ""
|
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:
|
if chunk_content:
|
||||||
text = str(chunk_content)
|
text = str(chunk_content)
|
||||||
events.put({"type": "chunk", "data": {"stage": 2, "chunk_type": chunk_type, "text": text}})
|
events.put({"type": "chunk", "data": {"stage": 2, "chunk_type": chunk_type, "text": text}})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue