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 分钟」并允许一键失败重置(前端已部分具备「无活跃任务则推进失败」逻辑)。
相关推荐
七夜zippoe2 分钟前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
举个栗子。4 分钟前
SwarmForge:AI 智能体协同编程框架,让多个 Agent 在隔离工作区并行协作
人工智能·开源·ai编程
AIGC小尼15 分钟前
Windows 本地 AI 漫剧全自动生产线部署完整教程(零基础、全指令、带源码、模型配置、排错方案)
人工智能·windows·ai漫剧
合米AI SOP系统19 分钟前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo20 分钟前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
ShallWeL38 分钟前
Orin 上多模型常驻与显存预算
人工智能·嵌入式硬件·nvidia·orin
xiaohaiAIgeo40 分钟前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
IT_陈寒41 分钟前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
集和诚JHCTECH1 小时前
案例分享 | BRAV-7721助力印刷电路板(PCB)智能缺陷复判系统
人工智能·嵌入式硬件·边缘计算