Compare commits
10 Commits
cb76bdefba
...
2a89c83bc8
| Author | SHA1 | Date |
|---|---|---|
|
|
2a89c83bc8 | |
|
|
ed2b3afab8 | |
|
|
4c2629a0bf | |
|
|
0560f56a90 | |
|
|
30fb09f11d | |
|
|
4a921f27b7 | |
|
|
56c7e227af | |
|
|
bdbdc674c6 | |
|
|
c8cdd3325c | |
|
|
4b9960a6bf |
|
|
@ -5,4 +5,5 @@ node_modules/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
.env
|
.env
|
||||||
data
|
data/
|
||||||
|
test.ipynb
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
# meeting
|
||||||
|
|
||||||
|
一个用于生成会议纪要的简单项目,支持:
|
||||||
|
|
||||||
|
- 命令行处理会议转写文本
|
||||||
|
- Web 页面导入会议原文并生成纪要
|
||||||
|
- 使用模板控制纪要输出格式
|
||||||
|
- 输出结构化主题和 Markdown 纪要
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
项目会基于会议原文完成两步处理:
|
||||||
|
|
||||||
|
1. 提取会议子主题,输出 `sub_topic.json`
|
||||||
|
2. 按模板生成会议纪要,输出 `meeting_summary.md`
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
建议使用 Python 3.10+
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置模型
|
||||||
|
|
||||||
|
Web 端会读取根目录下的 `config.json`,其中包含:
|
||||||
|
|
||||||
|
- `api_base_url`
|
||||||
|
- `api_key`
|
||||||
|
- `model_name`
|
||||||
|
- `max_tokens`
|
||||||
|
|
||||||
|
如果你使用自己的模型服务,先修改这个文件。
|
||||||
|
|
||||||
|
说明:命令行脚本当前通过 `agents/chat.py` 内的固定配置调用模型;Web 端使用 `config.json`。
|
||||||
|
|
||||||
|
## 命令行用法
|
||||||
|
|
||||||
|
按会议 ID 处理:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python meeting_summary.py --meeting-id 你的会议ID
|
||||||
|
```
|
||||||
|
|
||||||
|
按文件路径处理:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python meeting_summary.py --input examples/huiyi.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
指定模板:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python meeting_summary.py --input examples/huiyi.txt --template template1.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动 Web
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn web.server:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
meeting/
|
||||||
|
├─ agents/ # 模型调用
|
||||||
|
├─ data/
|
||||||
|
│ ├─ meetings/ # 会议原文
|
||||||
|
│ └─ results/
|
||||||
|
│ ├─ json/ # 子主题结果
|
||||||
|
│ └─ md/ # 会议纪要结果
|
||||||
|
├─ examples/ # 示例输入输出
|
||||||
|
├─ frontend/ # 前端页面
|
||||||
|
├─ prompt/ # Prompt 配置
|
||||||
|
├─ template/ # 纪要模板
|
||||||
|
├─ template_guides/ # 模板说明
|
||||||
|
├─ web/ # FastAPI 服务
|
||||||
|
└─ meeting_summary.py # 命令行入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输入与输出
|
||||||
|
|
||||||
|
输入支持:
|
||||||
|
|
||||||
|
- `txt`
|
||||||
|
- `md`
|
||||||
|
|
||||||
|
输出位置:
|
||||||
|
|
||||||
|
- 命令行传 `--meeting-id`:
|
||||||
|
- `data/results/json/<meeting_id>/sub_topic.json`
|
||||||
|
- `data/results/md/<meeting_id>/meeting_summary.md`
|
||||||
|
- 命令行传 `--input`:
|
||||||
|
- `examples/sub_topic.json`
|
||||||
|
- `examples/meeting_summary.md`
|
||||||
|
- Web 端处理:
|
||||||
|
- `data/results/json/<meeting_id>/sub_topic.json`
|
||||||
|
- `data/results/md/<meeting_id>/meeting_summary.md`
|
||||||
|
|
||||||
|
## 模板相关
|
||||||
|
|
||||||
|
- 模板文件放在 `template/`
|
||||||
|
- 模板说明放在 `template_guides/`
|
||||||
|
- Web 端支持导入模板、编辑模板、重新解析模板说明
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
项目内提供了示例文件:
|
||||||
|
|
||||||
|
- `examples/huiyi.txt`
|
||||||
|
- `examples/sub_topic.json`
|
||||||
|
- `examples/meeting_summary.md`
|
||||||
|
|
||||||
|
适合先用它们验证流程是否正常。
|
||||||
15
config.json
15
config.json
|
|
@ -1,5 +1,16 @@
|
||||||
{
|
{
|
||||||
"api_base_url": "https://api.llm.unissense.tech/v1",
|
"api_profiles": [
|
||||||
|
{
|
||||||
|
"name": "默认接口",
|
||||||
|
"api_base_url": "http://10.100.53.199:9527/v1",
|
||||||
|
"api_key": "unis123",
|
||||||
|
"model_name": "Qwen3.6-35B",
|
||||||
|
"max_tokens": 64000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active_api_profile_name": "默认接口",
|
||||||
|
"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": 64000
|
||||||
}
|
}
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"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提升考核),避免起跑线落后。领导指出考核争取公平得分需靠主动沟通与前期铺垫,各部门需联动协同,周会至此结束。"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +1,49 @@
|
||||||
{
|
{
|
||||||
"sub_topics": [
|
"sub_topics": [
|
||||||
{
|
{
|
||||||
"title": "宽带业务上门量、转化率与退单管控",
|
"title": "宽带安装维护进度与关键指标",
|
||||||
"time_interval": "00:00:01 - 00:03:51",
|
"time_interval": "00:00:01 - 00:02:06",
|
||||||
"overview": "会议通报了宽带安装与上门维护的整体进度,累计上门量达标但距3000户目标仍有差距。关键指标方面,弱光率降至0.51接近目标,三代终端压降改善,月度转化率87.35%接近90%目标,退单率控制在6.53%。主要退单因素为用户原因、天气影响及审核流程待优化,需加强B2C协调与一线审批意识。"
|
"overview": "受降雨影响安装上门量波动,周均上门近1000户、月均约4000户,累计进度距3000户目标仍有差距。弱光指标降至0.51逼近0.5目标,三代终端年度目标压降至5.5,FPTR已达标但主动过境仅0.3,需持续平衡安装与维护资源。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "PCDN专线质量恶化与学校出口限速机制",
|
"title": "安装转化率退单分析及PCDN专线问题",
|
||||||
"time_interval": "00:03:53 - 00:05:31",
|
"time_interval": "00:02:07 - 00:04:21",
|
||||||
"overview": "会议重点讨论了PCDN专线流量持续恶化问题,主要集中于两所学校开学后的出口带宽占用。目前市公司已介入分析涉及学校的IP地址,正协同校方建立发现超限直接限速的管控机制。该问题因学校互联网带宽免费导致管控难度大,需加快限速策略落地以保障网络质量。"
|
"overview": "月度安装转化率87.35%距90%目标略差,退单率6.53%主要受用户原因(29单)及天气影响。B2C协调流程待优化,PCDN专线质量持续恶化,已定位人文等两所学校IP集中问题并上报市公司分析限速。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "无线网络质量优化与专线护航巡检进展",
|
"title": "网络质量攻坚与专线护航进展",
|
||||||
"time_interval": "00:05:33 - 00:07:56",
|
"time_interval": "00:04:22 - 00:08:13",
|
||||||
"overview": "网络质量方面,物业点检测因天线问题延期至4月初,唱厅工单需待智能体质检结束,5G基站正通过参数调整试点解决漏话问题。超频站点故障已及时恢复,旁站故障率低。专线护航巡检方面,118条存量专线已巡检56条,未验收工程45条全量巡检,预计月底可按时完成全量巡检任务。"
|
"overview": "针对5G漏话开展基站参数调整试点,成功处置3个超频基站故障。专线护航方面118条存量及新建专线已巡检101条,剩余17条月底完成;当周报障33件其中9件为光缆故障,整体网络运行平稳。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "当周故障通报与二级站拆除及网络评估体系",
|
"title": "二级基站拆除与工程评估体系",
|
||||||
"time_interval": "00:07:57 - 00:09:35",
|
"time_interval": "00:08:14 - 00:09:35",
|
||||||
"overview": "当周共收报障33件,其中9件网络相关故障以光缆问题为主。重点推进金沙管家二级站拆除工作,剩余75站预计两周内完成下电及电费调整,目标4月15日前全量完成。此外,内部已收到网络质量综合评估体系文件,下周将分解制定具体目标。"
|
"overview": "剩余基站中已完成拆除46站,计划4月15日前完成剩余75站下电及电费调整。网络质量综合评估体系已下发,下周将启动内部分解与目标制定,其他工程进展正常。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "综合部重点工作推进与跨部门协同事项",
|
"title": "综合部行政后勤、工会活动与区级表彰申报",
|
||||||
"time_interval": "00:09:46 - 00:12:39",
|
"time_interval": "00:09:46 - 00:30:22",
|
||||||
"overview": "综合部汇报了25年剩余两项工作进展,建委框架清单与投资计划已汇报完成,终端占比问题已制定五项措施。会议同步推进数据中心包装、基建宣传、渠道产能评估及资产优化等7项工作,部分事项因兼职或流程原因仍在梳理方案,预计本周内完成专题汇报。"
|
"overview": "完成建委清单整理、终端占比汇报及打印机采购洽谈,持续推进数据中心包装与渠道产能评估。工会经费压减背景下推进食堂改造与饮水设备引入,运动会方阵缺9人需部门协调;25年招待费超预算14%(综合部超107%),区级担当作为表彰申报因标准模糊需高层对接。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "工会经费管理、食堂改造与员工文体活动筹备",
|
"title": "三期拆迁商客市场业务汇报",
|
||||||
"time_interval": "00:12:40 - 00:18:54",
|
"time_interval": "00:30:23 - 00:32:08",
|
||||||
"overview": "工会经费因上级压减需严考严用,已公示并上报软性工程,食堂改造预算约29.8万元正进行审计与方案优化。同时规划下半年引入自饮机节约成本,并全年组织六场大型体育活动。针对周五第四届体育文化节,需各部门抽调人员补充方阵缺额,确定人员后将统一购衣并于周中排练。"
|
"overview": "三期拆迁完成1145户并提升价值至50元以上,本周开展5场社区活动但签约转化未达预期。商客市场2月收入88.5万元环比增1万元,超改等区域增幅超2万元,价值拓展完成115户,但商客经理基础业务量分化明显。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "党建主题教育、招待费管控与区级荣誉申报",
|
"title": "工作执行力整顿与满意度考核要求",
|
||||||
"time_interval": "00:18:55 - 00:27:30",
|
"time_interval": "00:32:09 - 00:35:21",
|
||||||
"overview": "主题教育一期简报已发布,基层党组织学习已按期完成,招待费已按年度向管理层公示并报备纪检。25年招待费整体超预算14%,综合部超支严重,政企部需统筹对外招待避免客户经理个人承担。此外,正对接河川区担当作为集体/个人评选,因评选偏向政府机构且流程尚不明确,建议先摸清内部意向再决定是否申报。"
|
"overview": "严厉批评部分工作推进缓慢、缺乏量化回复的现象,要求养成“日清日结”习惯。针对满意度指标未达标问题,指出管理层存在松懈,要求本周内提交具体可行的保障方案,并明确KPI将挂钩满意度与离网率。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "商客市场收入拓展与社区/单位拆迁营销进展",
|
"title": "市场部季度收官与满意度提升举措",
|
||||||
"time_interval": "00:30:23 - 00:32:04",
|
"time_interval": "00:35:22 - 00:42:25",
|
||||||
"overview": "商客市场2月收入88.5万元,较上期增加1万元,超改、城北和南京改增幅超2万元,价值拓展完成115户。拆迁营销主要聚焦通货场景价值提升,已完成1145户改造,通过社区清洗与单位集中服务相结合推进,本周开展5场活动但签约转化率未达预期,需重点加强落实签约板块。"
|
"overview": "部署一季度收官与二季度营销提前谋划,要求商务部本周内汇报招聘、渠道及营销方案进度。明确满意度测评按老办法执行,80/20000指标需每日微信日报通报,利用考核规则中的操作空间平衡数据,确保KPI达标。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "建委评估体系落实与工作执行纪律要求",
|
"title": "年度考核部署与会议总结",
|
||||||
"time_interval": "00:32:15 - 00:35:22",
|
|
||||||
"overview": "会议强调需严格落实建委今年综合评估体系,要求各项回复工作做到日清日结、量化结果。针对当前部分经理回复滞后、工作拖延的问题,会议严肃指出必须养成及时汇报与跟进的习惯,未完成任务需主动沟通困难。同时要求针对上课指标拿出具体可行方案,结合专线助账客单位业务推进,确保指标达成。"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "市场部季度收官、满意度测评整改与指标管控",
|
|
||||||
"time_interval": "00:35:23 - 00:42:25",
|
|
||||||
"overview": "市场部需提前谋划二季度业务活动,避免淡季疲软,并要求本周内汇报招聘、农村渠道进度及西消队伍建设方案。针对满意度测评结果不佳,会议批评了主管思想松懈问题,强调需按四公司规定打造满意客户样板,对不满客户利用时间节点和技巧进行干预。要求商务部每日微信发送日报,并摸细KPI考核规则,利用可控操作动作平衡指标。"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "运动会安排、年度考核摸排与执行力强调",
|
|
||||||
"time_interval": "00:42:26 - 00:46:16",
|
"time_interval": "00:42:26 - 00:46:16",
|
||||||
"overview": "运动会筹备工作已明确时间节点,分管领导出差期间需提前视频汇报。会议重点强调四公司年度考核的提前摸排,要求各部门主动了解可能影响考核的指标变化,避免被动。领导指出当前部分工作存在知道怎么做却不去做的作风问题,要求各级人员强化执行力,确保年度考核起跑线公平,争取通过努力改善成绩。"
|
"overview": "强调全员需提升执行力,杜绝“知道怎么做却不行动”的作风。要求各部门提前摸排年度考核潜在风险指标,加强与市公司沟通争取公平考核环境,确保工作前置与资源统筹,会议至此结束。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
# 会议记录
|
|
||||||
|
|
||||||
议 题:合川分公司周例会(2026年第X期)
|
|
||||||
|
|
||||||
时 间:2026年5月8日 10:38—10:50
|
|
||||||
|
|
||||||
地 点:分公司会议室
|
|
||||||
|
|
||||||
主持人:管理员
|
|
||||||
|
|
||||||
参加人:分公司领导、各部门经理及AI云数中心经理
|
|
||||||
|
|
||||||
议程:
|
|
||||||
|
|
||||||
一、各部门汇报
|
|
||||||
|
|
||||||
二、分公司领导指示部署
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 会议内容
|
|
||||||
|
|
||||||
### 一、各部门汇报
|
|
||||||
|
|
||||||
建维部、综合部、市场部及政企部按议程依次汇报。建维部通报宽带装维进度:当前累计上门量突破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笔进度滞后。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 二、部署强调
|
|
||||||
|
|
||||||
#### 建维部负责人强调:
|
|
||||||
|
|
||||||
1. **装维与质量指标方面:**
|
|
||||||
- 兼顾安装与上门量,加快追赶3000户目标进度
|
|
||||||
- 保持弱光率与三类终端改善势头,专项提升主动跟进指标
|
|
||||||
- 建议将退单率纳入综合运维考核,推动剔除分局长已审批同意的退单以保公平
|
|
||||||
2. **工程与网络攻坚方面:**
|
|
||||||
- 2周内完成二级基站剩余75站拆除,确保4月15日前全量完成下电与资费调整
|
|
||||||
- 按期完成专线护航剩余17条巡检,存量光缆路由抢盘行动及时上报
|
|
||||||
- 下周内分解网络质量综合评估体系指标并制定阶段性目标
|
|
||||||
3. **故障处理与专项测试方面:**
|
|
||||||
- 持续压降超频站点及旁站故障,当周33件故障以光缆问题为主需重点排查
|
|
||||||
- 跟进三创空间天线检测及5G弱化基站参数试点调整效果
|
|
||||||
- 协同全资方建立专线自动限速机制,规范学校带宽使用并第一时间系统上线管控
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 分公司领导强调:
|
|
||||||
|
|
||||||
1. **市场与政企拓展方面:**
|
|
||||||
- 深度复盘商客市场与“查企”数据,加强低ARPU价值提升与龙狮业务转化
|
|
||||||
- 严抓部分经理周基础业务仅2笔的滞后问题,以周为单位严格跟踪并加快分局推进节奏
|
|
||||||
- 扭转满意度测评垫底局面,政企部负责人须亲自抓客户会工作,杜绝思想松懈
|
|
||||||
2. **行政后勤与费用管控方面:**
|
|
||||||
- 统筹政企部对外招待,加快报销结算,严控招待费超支风险(25年已超预算14%)
|
|
||||||
- 摸底区级“担当作为”评优申报决策领导意向,避免形式主义消耗
|
|
||||||
- 落实招投标打印机外协保障方案,确保不影响招投标业务开展
|
|
||||||
3. **作风建设与考核部署方面:**
|
|
||||||
- 严厉批评工作回复滞后、缺乏量化措施,要求全员养成“日事日清、日结日高”习惯,今日提交专线与商客指标具体保障方案
|
|
||||||
- 市场部需本周内汇报招聘进度、农村渠道建设及行销队伍转型方案
|
|
||||||
- 吃透市公司考核技巧,确保3月底“八零两绿”达标,今年KPI将纳入工信部有责与离网率
|
|
||||||
- 强化全员执行力,严禁“知而不做”作风,各部门需提前对接市公司年度考核口径(如APP提升考核),主动沟通争取公平得分,避免起跑线落后
|
|
||||||
|
|
@ -2,13 +2,21 @@
|
||||||
|
|
||||||
# 会议记录
|
# 会议记录
|
||||||
|
|
||||||
议 题:合川分公司周例会(2026年第X期)
|
议 题:合川分公司周例会(2026年第18期)
|
||||||
|
|
||||||
时 间:2026年5月6日 13:37
|
时 间:2026年5月6日13:37—15:00
|
||||||
|
|
||||||
|
地 点:分公司会议室
|
||||||
|
|
||||||
主持人:AlanPaine
|
主持人:AlanPaine
|
||||||
|
|
||||||
议程:各部门工作汇报、重点工作部署与执行纪律要求
|
参加人:分公司领导、各部门经理、AI云数中心经理
|
||||||
|
|
||||||
|
议程:
|
||||||
|
|
||||||
|
一、各部门汇报
|
||||||
|
|
||||||
|
二、分公司领导指示部署
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -16,31 +24,26 @@
|
||||||
|
|
||||||
### 一、各部门汇报
|
### 一、各部门汇报
|
||||||
|
|
||||||
- **网络/建维方面:** 宽带安装累计上门量达标但距3000户目标仍有差距;弱光率降至0.51,三代终端压降改善,月度转化率87.35%(目标90%),退单率6.53%;PCDN专线流量恶化集中于两所学校,正协同市公司分析IP并建立超限限速机制;5G基站通过参数调整试点解决漏话,超频站点及旁站故障已及时恢复;118条存量专线及45条未验收工程已全量巡检,预计月底按时完成;当周报障33件(9件网络相关,以光缆为主);金沙管家二级站剩余75站预计两周内完成下电及电费调整;网络质量综合评估体系已下发,下周分解目标。
|
建维部汇报宽带安装维护进度,受降雨影响周均上门近1000户,累计进度距3000户目标有差距;弱光指标降至0.51逼近0.5目标,月度安装转化率87.35%、退单率6.53%,主要受用户原因及B2C流程协调影响;PCDN专线质量恶化已定位两所学校IP问题并上报市公司分析限速;网络质量攻坚完成3个超频基站处置,118条专线已巡检101条,剩余17条月底完成;剩余121座二级基站已完成拆除46座,计划4月15日前完成下电及电费调整,网络质量综合评估体系已下发。综合部汇报完成建委清单整理、终端占比汇报及打印机采购洽谈,持续推进数据中心包装与渠道产能评估;工会经费压减背景下推进食堂改造与饮水设备引入,运动会方阵缺9人需部门协调;2025年招待费超预算14%(综合部超107%),区级担当作为表彰申报因标准模糊需高层对接。市场部/政企部汇报三期拆迁完成1145户并提升价值至50元以上,本周开展5场社区活动但签约转化未达预期;商客市场2月收入88.5万元环比增1万元,价值拓展完成115户,但商客经理基础业务量分化明显。
|
||||||
- **综合部方面:** 建委框架清单、投资计划及终端占比措施已汇报完成;数据中心包装、基建宣传、渠道产能评估等7项工作正梳理方案;工会经费压减需严考严用,食堂改造预算29.8万元正审计,下半年拟引入自饮机;第四届体育文化节方阵缺9人,需各部门抽调,周中排练;主题教育一期简报已发,基层党组织学习完成;招待费已按年度公示报备,整体超预算14%,综合部超支明显;河川区担当作为集体/个人评选正对接,建议摸清意向后再申报。
|
|
||||||
- **市场/商客方面:** 商客市场2月收入88.5万元,较上期增1万元,价值拓展完成115户;拆迁营销聚焦通货场景价值提升,已完成1145户,本周开展5场社区/单位活动但签约转化率未达预期;满意度测评结果不佳,主管存在思想松懈问题;商务部需本周内汇报招聘、农村渠道进度及西消队伍建设方案。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 二、部署强调
|
### 二、部署强调
|
||||||
|
|
||||||
#### 建维及综合分管领导强调:
|
#### 分公司领导强调:
|
||||||
|
|
||||||
1. **工作纪律与执行反馈:**
|
1. **建维与网络保障方面:**
|
||||||
- 各项回复工作必须做到日清日结、量化结果,严禁拖延或敷衍,未完成任务需主动沟通困难并推进。
|
- 平衡安装与维护资源,持续压降三代终端指标,针对主动过境率偏低(0.3)需专项提升。
|
||||||
- 针对“上课”指标,需结合专线助账客单位业务发展拿出具体可行方案,明确责任人确保达成。
|
- 优化B2C退单审核流程,将退单率纳入综合运维考核,减少一线人为随意退单。
|
||||||
2. **资源统筹与合规管理:**
|
- 针对PCDN专线恶化问题,加快与市公司及学校协同,落实IP地址限速机制。
|
||||||
- 招待费使用需统筹规范,政企部对外招待应合理分摊,避免由个别客户经理个人承担,严禁历史费用拖欠。
|
- 做好5G漏话基站参数调整试点观察,持续管控超频站点,加快剩余17条专线巡检及存量光缆路由问题上报。
|
||||||
- 河川区荣誉申报需先摸清内部意向与流程,确认有推荐名额后再行申报,避免资源浪费。
|
|
||||||
|
|
||||||
---
|
2. **市场与政企拓展方面:**
|
||||||
|
- 加快二期拆迁价值提升与社区活动转化,针对商客经理业绩分化需加强过程管控与帮扶。
|
||||||
|
- 提前谋划二季度营销,商务部本周内汇报招聘、农村渠道及营销方案进度,杜绝按部就班思想。
|
||||||
|
- 满意度测评暂按老办法执行,市场部需每日微信通报80/20000指标进展,利用考核规则操作空间平衡数据,确保KPI挂钩满意度与离网率达标。
|
||||||
|
|
||||||
#### 市场及考核分管领导强调:
|
3. **综合管理与考核部署方面:**
|
||||||
|
- 强化工作执行力,严禁“知道怎么做却不行动”的作风,要求养成“日清日结”习惯,工作回复必须量化并附措施成效。
|
||||||
1. **市场收官与满意度整改:**
|
- 严格招待费预算管理,各部门需按时间进度合理统筹使用,避免超支;工会经费压减下需优化食堂改造与饮水设备引入方案。
|
||||||
- 市场部需提前谋划二季度业务活动,克服淡季疲软思想,全面提振签约、商客及AI军团等业务活动声势。
|
- 提前摸排年度考核潜在风险指标,加强与市公司沟通争取公平考核环境,确保工作前置与资源统筹,全力备战四公司年度评估。
|
||||||
- 满意度工作必须落实到位,主管需亲自抓,严格按四公司规定打造满意客户样板;对不满客户需利用时间节点和技巧进行干预,商务部每日需发送日报。
|
|
||||||
- 摸细KPI考核规则(工信部有责指标及网率考核),利用可控的操作动作平衡指标,确保考核成绩。
|
|
||||||
2. **年度考核与执行力建设:**
|
|
||||||
- 提前摸排四公司年度考核指标变化,主动了解可能影响考核的病毒、卡位点或政策调整,提前沟通争取有利条件,避免被动。
|
|
||||||
- 严厉批评“知道怎么做却不去做”的作风,要求各级管理人员及一线人员强化执行力,确保年度考核起跑线公平,通过努力改善成绩。
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -149,7 +149,7 @@ textarea {
|
||||||
|
|
||||||
#template-panel {
|
#template-panel {
|
||||||
flex: 0 0 42%;
|
flex: 0 0 42%;
|
||||||
min-width: 360px;
|
min-width: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
|
|
@ -157,6 +157,7 @@ textarea {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
min-height: 96px;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 249, 255, 0.92));
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 249, 255, 0.92));
|
||||||
|
|
@ -190,6 +191,11 @@ textarea {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#result-panel .panel-heading,
|
||||||
|
#template-panel .panel-heading {
|
||||||
|
min-height: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-row {
|
.status-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -203,6 +209,13 @@ textarea {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-meta {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
@ -608,6 +621,7 @@ select.btn {
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgba(17, 47, 84, 0.28);
|
background: rgba(17, 47, 84, 0.28);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-mask.show {
|
.modal-mask.show {
|
||||||
|
|
@ -647,6 +661,192 @@ 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 {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f9fcff;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field textarea:focus,
|
||||||
|
.form-field input:focus {
|
||||||
|
border-color: rgba(47, 128, 237, 0.55);
|
||||||
|
box-shadow: 0 0 0 4px rgba(47, 128, 237, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-modal {
|
||||||
|
width: min(760px, 100%);
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-modal-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-modal-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-modal-head p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-modal-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-editor {
|
||||||
|
min-height: 260px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-editor:not(.is-editing) {
|
||||||
|
background: #f3f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-notes {
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-inline-prompt {
|
||||||
|
position: absolute;
|
||||||
|
inset: 62px 52px 164px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(238, 245, 255, 0.72);
|
||||||
|
border-radius: 18px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-inline-prompt-box {
|
||||||
|
width: min(620px, 100%);
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 18px 42px rgba(47, 128, 237, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-inline-prompt-title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-inline-prompt-subtitle {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-inline-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 140px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f9fcff;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-inline-actions {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reparse-modal {
|
||||||
|
width: min(720px, 100%);
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reparse-modal-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reparse-modal-textarea {
|
||||||
|
min-height: 176px;
|
||||||
|
height: 176px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reparse-stream-box {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reparse-stream-content {
|
||||||
|
min-height: 152px;
|
||||||
|
max-height: 152px;
|
||||||
|
overflow: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reparse-stream-content::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-process-guide .modal-actions,
|
||||||
|
#modal-reparse-guide .modal-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid rgba(199, 220, 248, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(248, 252, 255, 0.98);
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
border-color: rgba(47, 128, 237, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 22px;
|
top: 22px;
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<link rel="stylesheet" href="/assets/styles.css?v=20260511m">
|
||||||
<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>
|
||||||
|
|
@ -12,11 +12,12 @@
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">FRONTEND + BACKEND</p>
|
<p class="eyebrow">FRONTEND + BACKEND</p>
|
||||||
<h1>Meeting Summary Workspace</h1>
|
<h1>Agent test space</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>
|
||||||
|
|
||||||
|
|
@ -24,13 +25,13 @@
|
||||||
<aside class="panel" id="sidebar">
|
<aside class="panel" id="sidebar">
|
||||||
<div class="panel-header sidebar-header">
|
<div class="panel-header sidebar-header">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span>资源管理器</span>
|
<span>资源管理器</span>
|
||||||
<small id="sidebar-meeting-name">当前会议:未选择</small>
|
<small id="sidebar-meeting-name">当前会议:未选择</small>
|
||||||
<small id="sidebar-meeting-meta">请从左侧选择一个会议开始处理。</small>
|
<small id="sidebar-meeting-meta">请从左侧选择一个会议开始处理。</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-badges">
|
<div class="header-badges">
|
||||||
<span class="badge muted" id="badge-summary">未生成总结</span>
|
<span class="badge muted" id="badge-summary">未生成总结</span>
|
||||||
<span class="badge muted" id="badge-topics">未生成主题 JSON</span>
|
<span class="badge muted" id="badge-topics">未生成主题 JSON</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
@ -43,25 +44,25 @@
|
||||||
<section class="panel panel-main" id="result-panel">
|
<section class="panel panel-main" id="result-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span>会议结果</span>
|
<span>会议结果</span>
|
||||||
<div class="status-block">
|
<div class="status-block">
|
||||||
<div class="status-row">
|
<div class="status-row">
|
||||||
<span class="status-label">当前状态</span>
|
<span class="status-label">当前状态</span>
|
||||||
<span class="status-light idle" id="left-status-light"></span>
|
<span class="status-light idle" id="left-status-light"></span>
|
||||||
<span class="status-text" id="left-status-text">空闲</span>
|
<span class="status-text" id="left-status-text">空闲</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-meta" id="selected-meeting-tip">未选择会议</div>
|
<div class="status-meta" id="selected-meeting-tip">未选择会议</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="btn primary sm" id="btn-process" disabled>总结</button>
|
<button class="btn primary sm" id="btn-process" disabled>总结</button>
|
||||||
<button class="btn sm" id="btn-toggle-result-edit" disabled>编辑</button>
|
<button class="btn sm" id="btn-toggle-result-edit" disabled>编辑</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body panel-scroll" id="result-body">
|
<div class="panel-body panel-scroll" id="result-body">
|
||||||
<div id="result-empty" class="empty-state">
|
<div id="result-empty" class="empty-state">
|
||||||
<div class="empty-icon">M</div>
|
<div class="empty-icon">M</div>
|
||||||
<p>选择会议后即可查看结果,或直接开始总结。</p>
|
<p>选择会议后即可查看结果,或直接开始总结。</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="processing-indicator" class="processing" hidden>
|
<div id="processing-indicator" class="processing" hidden>
|
||||||
<div class="stream-box" id="stream-box">
|
<div class="stream-box" id="stream-box">
|
||||||
|
|
@ -79,21 +80,21 @@
|
||||||
<section class="panel" id="template-panel">
|
<section class="panel" id="template-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span>文档编辑</span>
|
<span>编辑区</span>
|
||||||
<div class="status-block">
|
<div class="status-block">
|
||||||
<div class="status-row">
|
<div class="status-row">
|
||||||
<span class="status-label">当前状态</span>
|
<span class="status-label">当前状态</span>
|
||||||
<span class="status-light idle" id="right-status-light"></span>
|
<span class="status-light idle" id="right-status-light"></span>
|
||||||
<span class="status-text" id="right-status-text">空闲</span>
|
<span class="status-text" id="right-status-text">空闲</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-meta" id="editor-resource-label">当前资源:模板</div>
|
<div class="status-meta" id="editor-resource-label">当前资源:模板</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<label class="inline-label" for="tpl-select">模板</label>
|
<label class="inline-label" for="tpl-select">模板</label>
|
||||||
<select class="btn sm" id="tpl-select"></select>
|
<select class="btn sm" id="tpl-select"></select>
|
||||||
<button class="btn sm" id="btn-reparse-guide" disabled>解析</button>
|
<button class="btn sm" id="btn-reparse-guide" disabled>解析</button>
|
||||||
<button class="btn sm" id="btn-toggle-side-edit" disabled>编辑</button>
|
<button class="btn sm" id="btn-toggle-side-edit" disabled>编辑</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body panel-scroll" id="template-body">
|
<div class="panel-body panel-scroll" id="template-body">
|
||||||
|
|
@ -105,47 +106,156 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-mask" id="modal-process-guide">
|
||||||
|
<div class="modal-box guide-modal">
|
||||||
|
<div class="guide-modal-head">
|
||||||
|
<div>
|
||||||
|
<h3 id="guide-modal-title">模板使用说明</h3>
|
||||||
|
<p id="guide-modal-subtitle">确认后开始总结,可先补充一些关注要点</p>
|
||||||
|
</div>
|
||||||
|
<button class="icon-btn" id="btn-close-process-guide" aria-label="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="guide-modal-body">
|
||||||
|
<label class="form-field">
|
||||||
|
<span>模板说明</span>
|
||||||
|
<textarea id="process-guide-editor" class="guide-editor" spellcheck="false"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="guide-inline-prompt" id="reparse-guide-prompt" hidden>
|
||||||
|
<div class="guide-inline-prompt-box">
|
||||||
|
<div class="guide-inline-prompt-title">模板说明补充</div>
|
||||||
|
<div class="guide-inline-prompt-subtitle">可为空;会和系统解析模板一起用于重新解析</div>
|
||||||
|
<textarea id="reparse-guide-notes" class="guide-inline-textarea" spellcheck="false" placeholder="例如:强调哪些结构必须保留、哪些段落需要重点解释、哪些措辞需要避免"></textarea>
|
||||||
|
<div class="guide-inline-actions">
|
||||||
|
<button class="btn" id="btn-reparse-guide-cancel">取消</button>
|
||||||
|
<button class="btn primary" id="btn-reparse-guide-confirm">确认重解析</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>补充说明(可空)</span>
|
||||||
|
<textarea id="process-extra-notes" class="guide-notes" spellcheck="false" placeholder="例如:强调哪些结构要保留、哪些内容要重点解释、哪些措辞需要避免"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn" id="btn-process-guide-edit">编辑</button>
|
||||||
|
<button class="btn" id="btn-process-guide-reparse">重解析</button>
|
||||||
|
<button class="btn primary" id="btn-process-guide-confirm">确认</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-mask" id="modal-reparse-guide">
|
||||||
|
<div class="modal-box reparse-modal">
|
||||||
|
<div class="guide-modal-head">
|
||||||
|
<div>
|
||||||
|
<h3 id="reparse-modal-title">模板使用说明补充</h3>
|
||||||
|
<p id="reparse-modal-subtitle">用于固定模板使用规格,优化最终输出结果(可不填写)</p>
|
||||||
|
</div>
|
||||||
|
<button class="icon-btn" id="btn-close-reparse-guide" aria-label="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="reparse-modal-body" id="reparse-modal-form">
|
||||||
|
<textarea id="reparse-modal-notes" class="guide-inline-textarea reparse-modal-textarea" spellcheck="false" placeholder="例如:强调哪些结构必须保留、哪些段落需要重点解释、哪些措辞需要避免"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="reparse-modal-body" id="reparse-modal-progress" hidden>
|
||||||
|
<div class="stream-box reparse-stream-box">
|
||||||
|
<div class="stream-title" id="reparse-stream-title">正在重新解析模板说明...</div>
|
||||||
|
<pre class="stream-content reparse-stream-content" id="reparse-stream-content"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn" id="btn-reparse-modal-cancel">取消</button>
|
||||||
|
<button class="btn primary" id="btn-reparse-modal-confirm">确认解析</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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"></script>
|
<script type="module" src="/assets/app.js?v=20260511m"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,6 @@ mode_contracts:
|
||||||
data_summary: |
|
data_summary: |
|
||||||
请严格参考下方模板的“大结构和层级关系”输出会议总结,最终结果必须为 Markdown。
|
请严格参考下方模板的“大结构和层级关系”输出会议总结,最终结果必须为 Markdown。
|
||||||
|
|
||||||
结构要求:
|
|
||||||
- 保留模板中的一级/二级主结构,不要随意改写整体框架顺序。
|
|
||||||
- 模板中的占位词、示例词、说明文字不能原样输出,必须替换或删除。
|
|
||||||
- 会议实际没有涉及的“小节、子项、占位条目”可以直接省略,不要为了凑模板保留“无相关内容”“XXX”“待补充”等字样。
|
|
||||||
- 如果某个一级模块完全没有可靠信息支撑,则该一级模块整体可以省略。
|
|
||||||
- 如果某个模块下只有部分子项有内容,则只保留有内容的子项,不必把其他子项补齐。
|
|
||||||
|
|
||||||
内容要求:
|
内容要求:
|
||||||
- 总结必须完全依据会议原文和话题拆分结果,不得杜撰。
|
- 总结必须完全依据会议原文和话题拆分结果,不得杜撰。
|
||||||
- 会议讨论方向每次都可能不同,标题、分组、关注点要根据本次会议内容动态生成,不能机械套用固定业务分类。
|
- 会议讨论方向每次都可能不同,标题、分组、关注点要根据本次会议内容动态生成,不能机械套用固定业务分类。
|
||||||
|
|
@ -62,10 +55,8 @@ user_template:
|
||||||
{sub_topices}
|
{sub_topices}
|
||||||
|
|
||||||
输出要求:
|
输出要求:
|
||||||
|
- 结果需要保证具体指向的书面化表达,避免空泛的口头话表达
|
||||||
- 严格遵循模板的大结构,但不要机械保留无意义占位内容
|
- 严格遵循模板的大结构,但不要机械保留无意义占位内容
|
||||||
- 模板中没有被会议内容支撑的小节、子项、占位条目,直接删除,不要输出“无相关内容”
|
|
||||||
- 每次会议讨论方向都可能不同,需根据本次会议实际内容动态组织主题和小标题
|
|
||||||
- 只保留有明确信息依据的内容,避免泛化扩写
|
- 只保留有明确信息依据的内容,避免泛化扩写
|
||||||
- 重点保留关键数据、问题、决策、动作、责任分工和后续安排
|
|
||||||
- 语言保持客观、准确、简洁
|
- 语言保持客观、准确、简洁
|
||||||
- 按会议实际讨论顺序组织内容,但不要照搬时间区间
|
- 按会议实际讨论顺序组织内容,但不要照搬时间区间
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ mode_contracts:
|
||||||
- 问答、决策、行动项、引用、复选框等格式是否需要保留
|
- 问答、决策、行动项、引用、复选框等格式是否需要保留
|
||||||
|
|
||||||
尤其注意以下常见占位/示例模式,并在输出中转成通用规则:
|
尤其注意以下常见占位/示例模式,并在输出中转成通用规则:
|
||||||
|
- 占位符替换需要根据实际内容,若文本没有对应内容则保留占位符
|
||||||
- `X`、`XX`、`XXX`
|
- `X`、`XX`、`XXX`
|
||||||
- `[占位内容]`
|
- `[占位内容]`
|
||||||
- `Q:`、`A:`
|
- `Q:`、`A:`
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
- 严格保留模板的Markdown标题层级(## 主模块、### 子模块),主模块标题可保留原模板的Emoji标识。
|
|
||||||
- 所有方括号 `[]` 内的文本、示例标题(如“议题1/2/3”“关于产品定位”)及括号提示语均为结构占位符,生成时必须替换为真实会议内容,严禁原样输出。
|
|
||||||
- 若某模块或子项在会议实际内容中无对应信息,应直接省略该部分,不得留空或强行补全占位符。
|
- 整体结构需严格保留六大核心模块层级(概述、核心议题、关键问答、重要观点、开发决策、后续行动),标题使用 Markdown `##` 与 `###` 规范。
|
||||||
- 各模块下的子条目数量(如议题数量、观点子项、行动项等)允许根据实际讨论内容动态增减,不强制匹配模板示例数量。
|
- 所有 `[占位提示]`、示例性文字及括号内的说明语必须在生成前替换为真实会议内容;若无对应信息,直接省略该占位项,严禁原样输出或保留空括号。
|
||||||
- 保留问答对的固定排版:使用 `Q: ` 与 `A: ` 标识问答内容,不同问答对之间以 `---` 分隔。
|
- “核心议题”与“重要观点”下的子标题为动态结构,须根据实际讨论内容灵活增减数量与命名,禁止强制凑齐模板示例的固定数量。
|
||||||
- 保留观点陈述的引用块格式:统一使用 `> ` 开头包裹具体观点或共识内容。
|
- “关键问答”模块需严格保留 `Q:` 与 `A:` 的问答对格式;若包含多个问答对,每组之间必须使用 `---` 水平线分隔。
|
||||||
- 保留行动项的复选框格式:统一使用 `- [ ] ` 开头列出后续待办任务。
|
- “重要观点”模块下的每条内容必须使用 Markdown 引用块 `> ` 格式包裹,确保观点与决策的视觉层级独立。
|
||||||
- 保留分析类与决策类内容的键值对列表格式:使用 `- 标签: 内容` 的结构呈现各项分析或决议。
|
- “后续行动”模块必须使用 Markdown 复选框 `- [ ] ` 格式列出任务项;任务数量与具体内容按实际分配动态生成,不可沿用模板示例条数。
|
||||||
- 子模块标题可根据实际议题语义进行重命名,但需维持原模板的模块分类逻辑与层级归属。
|
- 各模块下的细分条目需保留列表与子项结构,但具体标签名称可根据实际业务语境进行合理替换或泛化,确保语义准确。
|
||||||
- 生成内容需严格遵循“有则保留对应格式,无则直接省略该条目”的原则,禁止输出任何未经验证的结构化空壳或示例文字。
|
- 模板中未提及或会议内容未覆盖的空白小节、占位示例,一律按“无依据不强行补齐”原则直接删除,不输出空标题、空列表或残留占位符。
|
||||||
|
- 标题中的 Emoji 装饰符号需保留以维持模板风格一致性;若实际场景不适用,可替换为通用符号或仅保留纯文本,但需全篇保持统一。
|
||||||
|
- 占位符替换完成后需进行逻辑校验,确保每个标题/标签与其下方的具体内容严格对应,禁止出现格式错位或内容张冠李戴。
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
- 严格保留模板的Markdown标题层级与宏观结构(# 会议记录 -> ## 会议内容 -> ### 各部门汇报/部署强调),不得增删或调整主标题顺序。
|
- 会议记录头部信息(议题、时间、地点、主持人、参加人、议程)必须完整填写,占位符替换为实际内容,无对应内容时保留占位符。
|
||||||
- 模板中的“X”、“XX”、“XXX”及具体年份、部门名均为占位示例,生成时必须替换为会议真实数据与名称;若实际会议未涉及某字段,则直接省略该条目,不强行补齐。
|
- 各部门汇报部分必须使用一段话简明扼要总结,不展开列表或分点,避免长篇大论。
|
||||||
- “各部门汇报”小节需按实际汇报顺序提炼核心内容与进度数据,替换原长句模板;若无汇报内容,可省略该小节。
|
- 部署强调部分保留“X总强调”标题层级(####),每个领导下的方面和要点使用无序列表(-),每个方面至少一个要点,无内容时省略该方面或该要点。
|
||||||
- “部署强调”部分采用“#### [领导职务/姓名]强调:”作为独立标题块,多个领导发言时按实际顺序并列生成,替换示例中的“X总”。
|
- 所有占位符(如X、XXX、XX、[占位内容])必须替换为真实内容(数字、名称、描述等),不能原样输出。
|
||||||
- 领导强调下按实际业务领域划分子项(格式为“数字. **领域:**”),子项数量与领域名称严格按实际讲话内容动态增减,不得照搬模板示例的固定领域列表。
|
- 示例文本(如“XXX”、“XX项”、“2026第X期”)需替换为实际数据或描述,不可保留示例样式。
|
||||||
- 各领域下的具体指示需使用无序列表(`- `)逐条列出,替换示例“XXX”;保持原有缩进层级,确保要点清晰。
|
- 空白条目、小节、子项:如果某领导未发言或某方面无要点,则相应省略该领导小节或该方面列表,不强行补齐。
|
||||||
- 所有仅用于演示结构的说明性文字(如“按议程现场按顺序做汇报”“详见汇报材料”等)必须删除,仅输出实际会议提炼的实质内容。
|
- 标题层级和整体结构(# 会议记录 → ## 会议内容 → ### 一、各部门汇报 / ### 二、部署强调)必须保留,不可合并或删除。
|
||||||
- 严格遵循模板原有的排版分隔逻辑,各主要模块间保留“---”分隔线,维持整体版式节奏。
|
- 议程内容(一、各部门汇报;二、分公司领导指示部署)作为固定框架必须保留,不可省略。
|
||||||
|
- 允许按实际参会领导数量动态增减“X总强调”小节,每个小节内方面名称和数量也可根据实际内容调整,不限于模板列举。
|
||||||
|
- 模板末尾的说明性文字(如使用说明及注释)不应出现在最终输出中,仅作为规则参考。
|
||||||
Binary file not shown.
285
web/server.py
285
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 []
|
||||||
|
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 {
|
return {
|
||||||
"api_base_url": "http://10.100.53.199:9527/v1",
|
"api_profiles": normalized_profiles,
|
||||||
"api_key": "unis123",
|
"active_api_profile_name": active_profile_name,
|
||||||
"model_name": "Qwen3.6-35B",
|
"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:
|
||||||
|
|
@ -135,7 +228,12 @@ def _collect_llm_content(client, model, system_prompt: str, user_prompt: str, ma
|
||||||
return "".join(content).strip()
|
return "".join(content).strip()
|
||||||
|
|
||||||
|
|
||||||
def _parse_template_guide(template_name: str, template_content: str, cfg: dict | None = None) -> str:
|
def _parse_template_guide(
|
||||||
|
template_name: str,
|
||||||
|
template_content: str,
|
||||||
|
cfg: dict | None = None,
|
||||||
|
user_notes: str = "",
|
||||||
|
) -> str:
|
||||||
prompt = load_prompt("templatet_parser", "zh")
|
prompt = load_prompt("templatet_parser", "zh")
|
||||||
config = cfg or _load_config()
|
config = cfg or _load_config()
|
||||||
client = _get_llm_client(config)
|
client = _get_llm_client(config)
|
||||||
|
|
@ -144,10 +242,35 @@ def _parse_template_guide(template_name: str, template_content: str, cfg: dict |
|
||||||
template_name=template_name,
|
template_name=template_name,
|
||||||
template_content=template_content,
|
template_content=template_content,
|
||||||
)
|
)
|
||||||
|
if user_notes.strip():
|
||||||
|
user_prompt += f"\n\n用户补充的模板使用说明(优先参考,可为空):\n{user_notes.strip()}"
|
||||||
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 _ensure_template_guide(template_name: str, *, force: bool = False, cfg: dict | None = None) -> str:
|
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,
|
||||||
|
*,
|
||||||
|
force: bool = False,
|
||||||
|
cfg: dict | None = None,
|
||||||
|
user_notes: str = "",
|
||||||
|
) -> str:
|
||||||
template_path = _resolve_child(TEMPLATE_DIR, template_name)
|
template_path = _resolve_child(TEMPLATE_DIR, template_name)
|
||||||
if not template_path.exists():
|
if not template_path.exists():
|
||||||
raise HTTPException(404, f"Template not found: {template_name}")
|
raise HTTPException(404, f"Template not found: {template_name}")
|
||||||
|
|
@ -162,6 +285,7 @@ def _ensure_template_guide(template_name: str, *, force: bool = False, cfg: dict
|
||||||
template_name=template_name,
|
template_name=template_name,
|
||||||
template_content=template_path.read_text(encoding="utf-8"),
|
template_content=template_path.read_text(encoding="utf-8"),
|
||||||
cfg=cfg,
|
cfg=cfg,
|
||||||
|
user_notes=user_notes,
|
||||||
)
|
)
|
||||||
guide_path.write_text(guide_content, encoding="utf-8")
|
guide_path.write_text(guide_content, encoding="utf-8")
|
||||||
return guide_content
|
return guide_content
|
||||||
|
|
@ -215,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}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -345,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():
|
||||||
|
|
@ -462,11 +620,79 @@ async def save_template_guide(name: str, payload: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/templates/{name}/guide/reparse")
|
@app.post("/api/templates/{name}/guide/reparse")
|
||||||
async def reparse_template_guide(name: str):
|
async def reparse_template_guide(name: str, user_notes: str = ""):
|
||||||
content = _ensure_template_guide(name, force=True)
|
try:
|
||||||
|
content = _ensure_template_guide(name, force=True, user_notes=user_notes)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(500, str(exc) or "Template guide reparse failed") from exc
|
||||||
return {"name": name, "content": content}
|
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 = []
|
||||||
|
|
@ -517,7 +743,12 @@ async def save_meeting_summary(meeting_id: str, payload: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/meetings/{meeting_id}/process")
|
@app.get("/api/meetings/{meeting_id}/process")
|
||||||
async def process_meeting(meeting_id: str, request: Request, template_name: str = "template1.md"):
|
async def process_meeting(
|
||||||
|
meeting_id: str,
|
||||||
|
request: Request,
|
||||||
|
template_name: str = "template1.md",
|
||||||
|
user_notes: str = "",
|
||||||
|
):
|
||||||
meeting_dir = DATA_DIR / meeting_id
|
meeting_dir = DATA_DIR / meeting_id
|
||||||
if not meeting_dir.exists():
|
if not meeting_dir.exists():
|
||||||
raise HTTPException(404, "Meeting not found")
|
raise HTTPException(404, "Meeting not found")
|
||||||
|
|
@ -548,7 +779,13 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
|
||||||
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}})
|
||||||
|
|
@ -578,9 +815,17 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
|
||||||
article=transcript,
|
article=transcript,
|
||||||
sub_topices=sub_topics,
|
sub_topices=sub_topics,
|
||||||
)
|
)
|
||||||
|
if 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