python webview打包版卡死、开发版正常

现象:源码/测试版(python -m src.main)语音播报流程流畅;PyInstaller 发行包卡在「播报文稿生成 ~38%」,进度与超时均失效。 结论:不是业务算法差异,而是冻结进程内引擎的启动方式触发了 Windows 默认 ProactorEventLoop + 日志锁竞争,并叠加 GUI 进程代理继承等问题。 文档日期:2026‑08‑19


1. 现象对照

维度 开发 / 测试版 打包发行版(修复前)
启动入口 python -m src.main 桌面客户端程序.exesys.frozen=True
播报引擎进程模型 独立子进程engine/.venv/.../python -m uvicorn app.main:app 同进程守护线程 :桌面壳内 threading.Thread + uvicorn
事件循环 子进程默认策略,外网断开路径相对独立 线程内 asyncio.run → 默认 ProactorEventLoop
典型卡住点 任务中心显示「播报文稿生成 · ...」约 38%,可挂死十余分钟
超时是否生效 正常 不生效(阶段 420s 超时、httpx 超时也无法打断)
日志 engine/engine.out.log engine-data/engine.log;挂死时常停在某条 [vo.normalize] / 大模型调用前后

前端 38% 来自播报阶段序号映射:script_storyboard 在有效步骤序列中约对应 3/8,即「播报文稿 Agent 生成中」阶段,不是业务写死的 38 这个魔法数


2. 架构分叉(问题入口)

桌面通过 ProduceEngine.ensure_started() 拉起引擎,分支如下:

text 复制代码
ensure_started()
├── frozen == False  →  子进程 uvicorn(开发路径)
└── frozen == True   →  _ensure_started_inprocess()(打包路径)

对应代码:src/produce_engine.py

选择进程内线程的初衷合理:发行包无 .venv,依赖已打进 _internal,避免再附带解释器。副作用是:引擎与无控制台 GUI、WebView2、同步日志写文件共享同一进程地址空间,异步运行时行为与「独立 uvicorn」不再等价。


3. 问题定位过程

3.1 业务侧排除

  1. 同一数据库/缓存服务、大模型密钥配置下,开发版可完成同类型播报,说明 大模型、对象存储、库表本身可用
  2. 卡住任务 DB 状态长期为 RUNNINGprogress_message 停在「播报文稿 Agent 生成中...」,completed_at 为空。
  3. 阶段超时常量 STAGE_TIMEOUT_SCRIPT_STORYBOARD = 420 存在,但挂死后 远超 420s 仍不失败 → 更像是 事件循环本身不再调度,而非单纯「模型慢」。

3.2 卡住热路径

播报文稿阶段主链:

text 复制代码
pipeline._run_script_storyboard
  → agent_plan.generate_script_and_storyboard
      → search(httpx)
      → llm_client.chat_json(httpx)
      → finalize_voiceover_storyboard_sync(to_thread)
  → 进度心跳 _with_progress_heartbeat

日志中常见停顿点:[vo.script] step=llm_generate / [vo.normalize] dedupe begin 等。停在日志语句附近,提示 写日志与 asyncio 回调交叉,而不一定是 normalize 算法复杂度。

3.3 uvicorn 在 Windows 上的关键行为

uvicorn.Server.run() 大致为:

text 复制代码
config.setup_event_loop()
asyncio.run(self.serve(...))

uvicorn.loops.asyncio.asyncio_setup 源码(uvicorn 0.34):

python 复制代码
def asyncio_setup(use_subprocess: bool = False) -> None:
    if sys.platform == "win32" and use_subprocess:
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

Config.use_subprocess 仅在 reloadworkers > 1 时为真。 桌面进程内单 worker、无 reload:setup_event_loop() 等于空操作。

Python 3.12 on Windows 默认策略 → asyncio.run() 创建 ProactorEventLoop

开发版:独立 python -m uvicorn 子进程,即使也是 Proactor,也 不与 GUI 主线程、同一套 RotatingFileHandler 高频争用;连接关闭异常路径的危害面更小,表现为「测试 OK」。

3.4 修复后的验证证据

新包启动后 engine-data/engine.log 出现:

text 复制代码
冻结引擎 asyncio.run loop_factory -> _WindowsSelectorEventLoop
引擎事件循环: _WindowsSelectorEventLoop (selector=SelectSelector frozen=True)

说明发行路径已强制 Selector;此前仅 set_event_loop_policy 再调用 Server.run(),仍可能被 asyncio.run() 按默认工厂建出 Proactor。


4. 根本原因

4.1 主因:Proactor + 冻结 GUI 线程内引擎 → 异步死锁式挂起

因果链:

  1. 打包版引擎跑在桌面进程的守护线程里。
  2. uvicorn 未因 use_subprocess 切换 Selector,asyncio.run 使用 ProactorEventLoop
  3. 播报阶段大量短连接(大模型对话、搜索、对象存储 HEAD/GET 等);远端关闭连接时,Proactor 管道回调 _call_connection_lost 可能在回调里抛出 OSError(如 WinError 10038)。
  4. asyncio 默认异常处理器走 logging;同时业务 worker / 心跳线程高频写 engine.log(文件 handler)。
  5. logging handler 锁与异常再入写日志叠加,可导致 业务协程永久卡在 logging 调用上 ;事件循环无法继续调度 → httpx 超时、asyncio.wait_for 超时全部失效
  6. UI 仍按 stage 显示「播报文稿生成 ~38%」,表现为「打包就卡、测试不卡」。

这与「算法在打包后变慢」无关,是 运行时模型差异

4.2 次因:GUI 进程继承系统代理

无控制台 PyInstaller 进程常带上用户/机器级 HTTP(S)_PROXY。 httpx 默认 trust_env=True 时会走异常代理,外网调用表现为无限挂起,同样容易停在播报文稿阶段。

开发子进程环境变量集合往往不同,故不易复现。 对策(已落地):HTTPX_CLIENT_KWARGS = {trust_env: False, proxy: None},并在冻结引擎线程入口清理代理环境变量、设置 NO_PROXY=*

4.3 伴随问题(放大「卡」的体感,但非 38% 主因)

问题 表现 说明
删除项目未释放内存队列 新任务显示「排队等待(第 2 位)」 只删 DB 行,未 cancel 同类型 asyncio.Lock 等待者;计数泄漏
发行包漏打 aiomysql 「启动播报引擎失败: No module named 'aiomysql'」 壳与引擎共用冻结解释器,根 requirements.txt 未对齐引擎依赖
httpx 未进 collect_all 偶发导入/运行异常 清单已补齐

5. 修复方案(已实现)

5.1 强制 Selector 事件循环(主修复)

_ensure_started_inprocess不再依赖 Server.run() / uvicorn 的 setup_event_loop,改为:

  1. 设置 WindowsSelectorEventLoopPolicy
  2. asyncio.run(serve(), loop_factory=policy.new_event_loop),保证真正创建的是 Selector 循环;
  3. lifespan 日志打印 frozen 与 loop 类型;若仍为 Proactor 则打 error,便于回归。

5.2 代理隔离

  • 全局 httpx:trust_env=False + proxy=None
  • 冻结线程:剥离 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY

5.3 删除与队列

  • delete_series 前调用 task_queue.abandon_series_tasks,取消内存 worker,避免假排队。

5.4 打包依赖

  • requirements.txt 对齐引擎(含 aiomysqlhttpx 等);
  • package_manifest.py 将相关包列入 COLLECT_ALL / REQUIRED_INTERNAL

6. 为何「测试 OK、打包就卡」可一句话概括

测试版引擎是独立进程;打包版引擎挤进 GUI 同进程线程,且误用 Windows Proactor 事件循环。连接关闭与日志争用把 asyncio 卡死,于是播报文稿阶段永远停在约 38%,超时也救不回来。

次要因素(代理、假排队、缺依赖)会叠加失败模式,但与「流畅 vs 卡死」二分最对齐的,是 进程模型 + 事件循环类型 这一条。


7. 回归检查清单

打包后启动,确认:

  1. dist/.../engine-data/engine.log_WindowsSelectorEventLoopfrozen=True
  2. 执行播报任务后,progress_message 能在数分钟内越过「播报文稿 Agent 生成中」,或在超时后变为失败而非永久 RUNNING。
  3. 删除进行中项目后,新建任务不应无故「第 2 位」排队。
  4. verify_package.py 通过,且 _internalaiomysqlhttpx

8. 相关代码与文档索引

路径 作用
src/produce_engine.py frozen 分支、Selector loop_factory、清代理、安全日志 handler
engine/app/main.py 启动时打印事件循环类型;Proactor 告警
engine/app/services/volc/http_utils.py httpx 禁用环境代理
engine/app/services/task_queue.py 同类型串行锁、abandon_series_tasks
engine/app/services/pipeline.py delete_series 先放弃队列再删库
scripts/package_manifest.py / requirements.txt 发行依赖对齐
docs/packaging-notes.md 打包结构与分发注意

9. 后续建议(可选)

  1. 长期:评估打包版也改为「附带嵌入式 Python / sidecar uvicorn」,与开发模型一致,降低同进程耦合。
  2. CI :冻结冒烟用例------启动 exe 后读 engine.log,断言 Selector + /api/health
  3. 可观测性:卡住时在任务中心展示「无心跳超过 N 分钟」并允许一键失败重置(前端已部分具备「无活跃任务则推进失败」逻辑)。
相关推荐
大山佬1 小时前
抛弃 115200bps 的串口调试:J-Link RTT 实时日志系统配置、性能评测与多通道架构全面指南
人工智能·j-link
LaughingZhu1 小时前
Product Hunt 每日热榜 | 2026-08-18
人工智能·经验分享·深度学习·神经网络·产品运营
碧海银沙音频科技研究院1 小时前
基于杰理AC7016C的GTCRN门控卷积神经网络语音增强方法
人工智能·嵌入式硬件·语音识别
科技快报1 小时前
CANN全面开源开放 共赢AI技术生态
人工智能
阿里云大数据AI技术1 小时前
阿里云PAI发布通用蒸馏框架EasyDistill2.0,提供主流大模型蒸馏场景最佳实践
人工智能·agent
ZJU_统一阿萨姆2 小时前
【推理优化进阶】图编译与运行时:动态形状、CUDA Graph 与内存规划
开发语言·人工智能·语言模型·架构·开源
乐之者v2 小时前
AI人工智能--DeepSeek Harness 选择哪种模式?
人工智能·ai
大力财经2 小时前
百度Q2财报:AI业务占比过半 GPU云强劲增长283%
人工智能
桃西西呀2 小时前
Cordis 内核到底在管什么?五要素我带你看源码
人工智能