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

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


1. 现象对照

维度 开发 / 测试版 打包发行版(修复前)
启动入口 python -m src.main 桌面客户端程序.exe(sys.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 状态长期为 RUNNING,progress_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 仅在 reload 或 workers > 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 对齐引擎(含 aiomysql、httpx 等);
  • package_manifest.py 将相关包列入 COLLECT_ALL / REQUIRED_INTERNAL。

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

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

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


7. 回归检查清单

打包后启动,确认:

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

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 分钟」并允许一键失败重置(前端已部分具备「无活跃任务则推进失败」逻辑)。
相关推荐
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(83):Inside Out——用可演化 PersonaTree 构建 Agent 的核心长期记忆
论文阅读·人工智能·学习·开源·github
运行时异常3 小时前
【WMS 仓储系统集成 AI Agent 实战】第 8 讲(终篇):生产部署与并发安全——Semaphore 放进 Flux.defer 的坑,压测抓了一晚上
人工智能·安全
杨杨杨大侠3 小时前
Jev、Kev、Laya:决策模型怎么选,什么时候需要微调?
人工智能·python·agent
代码方舟3 小时前
零信任架构实战:基于天远人企关联构建自动化供应链金融网关
运维·人工智能·架构·自动化
虹科网络安全3 小时前
“顶会”看安全(十八):超越越狱:揭示由能力边界模糊引发的 LLM 应用安全风险
人工智能·安全
Maiko Star4 小时前
* LangChain 提示词模板详解:ChatPromptTemplate 的使用与高级特性
java·人工智能·langchain
鬓戈4 小时前
Rust 语言与 AI 应用生态调研及学习路径
人工智能·学习·rust
天远API4 小时前
零信任架构实战:基于天远人企关联构建自动化图谱网关
网络·人工智能·架构·自动化
爱吃提升4 小时前
文生视频模型发展趋势(2026)
人工智能·音视频
AIGS0014 小时前
什么是本体语义平台?和知识图谱、传统数据中台的区别在哪
人工智能·知识图谱·数据中台·ai问数·本体语义平台·智能数据中台