zhoujie b635108b89 refactor: split monolithic main.py into services/ + ui/ modules (improve-maintainability)
- main.py: 4360 → 146 lines (96.6% reduction), entry layer only
- services/: rate_limiter, autostart, persona, connection, profile,
  hotspot, content, engagement, scheduler, queue_ops (10 business modules)
- ui/app.py: all Gradio UI code extracted into build_app(cfg, analytics)
- Fix: with gr.Blocks() indented inside build_app function
- Fix: cfg.all property (not get_all method)
- Fix: STATUS_LABELS, get_persona_keywords, fetch_proactive_notes imports
- Fix: queue_ops module-level set_publish_callback moved into configure()
- Fix: pub_queue.format_*() wrapped as queue_format_table/calendar helpers
- All 14 files syntax-verified, build_app() runtime-verified
- 58/58 tasks complete"
2026-02-24 22:50:56 +08:00

6.0 KiB
Raw Blame History

Context

main.py 是整个项目的单一入口,目前 4359 行,包含 10+ 个业务域的业务逻辑、全局状态、UI 组件和事件绑定。上一轮重构已将 Tab 1内容创作提取为 ui/tab_create.py,建立了 Tab 模块化的模式。本次设计延续该模式,将业务逻辑提取为 services/ 目录,并完成剩余 Tab 的 UI 拆分。

已有外部服务层:config_manager.pyllm_service.pysd_service.pymcp_client.pyanalytics_service.pypublish_queue.py
main.py 中剩余的函数是对这些服务的编排层,应该独立成模块而非继续膨胀在入口文件里。

约束:

  • Python 3.10+(当前环境)
  • 不引入新的外部依赖
  • 重构不改变任何业务行为

Goals / Non-Goals

Goals:

  • main.py 瘦身至 ~300 行纯入口层(导入 + UI 组装 + app.launch()
  • 建立清晰的分层:services/(业务编排)→ ui/Gradio 组件)→ main.py(入口)
  • 消除模块间隐式依赖,所有依赖通过函数参数显式传递
  • 保持所有函数签名、Gradio 回调绑定不变

Non-Goals:

  • 不重构或重写任何现有业务逻辑(本次是纯搬迁)
  • 不改变 config_manager.pyllm_service.py 等已有服务层
  • 不引入类/面向对象重构(保持现有函数式风格)
  • 不添加单元测试(独立变更)

Decisions

D1分层架构 —— services/ 不依赖 ui/ui/ 不依赖 services/

main.py  (入口层:组装 + 启动)
├── services/  (业务编排层:纯 Python无 Gradio)
│   ├── connection.py
│   ├── content.py
│   ├── hotspot.py
│   ├── engagement.py
│   ├── rate_limiter.py
│   ├── profile.py
│   ├── persona.py
│   ├── scheduler.py
│   ├── queue_ops.py
│   └── autostart.py
├── ui/  (UI 层Gradio 组件 + 事件绑定)
│   ├── tab_create.py  ← 已存在
│   ├── tab_hotspot.py
│   ├── tab_engage.py
│   ├── tab_profile.py
│   ├── tab_auto.py
│   ├── tab_queue.py
│   ├── tab_analytics.py
│   └── tab_settings.py

为何不让 ui/ 依赖 services/ 每个 build_tab() 接收回调函数作为参数(已有 tab_create.py 的模式),而非直接 import service。这样 UI 层完全解耦,可独立测试或替换。

D2共享单例通过 main.py 初始化,作为参数传入

cfgmcpanalyticspub_queuequeue_publisher 仍在 main.py 顶层初始化。Service 函数需要它们时通过函数参数接收,不在 service 模块顶层 import。

为何不在各 service 模块初始化单例: 防止循环依赖、防止多次初始化、保持测试时可替换。

D3有状态模块使用模块级变量不封装成类

rate_limiterschedulerengagement 内部有 threading.Event_daily_stats 等状态。保持现有模块级变量风格(不改成类),仅将变量和函数整体搬迁到对应模块。

为何不改成类: 本次目标是结构拆分,不是重构设计模式,避免引入额外变更风险。

D4迁移策略 —— 先提取后删除,不做重定向

每个域的迁移步骤:

  1. 在 service 模块中写入函数(复制粘贴 + 调整 import
  2. main.py 中删除对应函数,改为 from services.xxx import ...
  3. 运行 ast.parse() 验证语法
  4. 运行应用验证启动不报错

为何不做 main.py 中的临时 re-export 简单场景直接删+导入更清晰,且 Gradio 回调绑定在 main.py 中通过变量名引用,只需保证同名变量在作用域内即可。

D5UI Tab 模块统一使用 build_tab(fn_*, ...) 签名

复用 tab_create.py 已建立的模式:

  • 每个 build_tab() 接收所需的回调函数和共享 Gradio 组件作为参数
  • 函数内部创建本 Tab 的所有 Gradio 组件及事件绑定
  • 返回 dict,包含需要被其他 Tab 或 app.load() 引用的组件

D6services/ui/ 均需 __init__.py

使用空文件标记为 Python 包,与 ui/__init__.py 已有做法一致。

Risks / Trade-offs

  • [风险] 大量 import 调整可能遗漏 → 每个模块完成后执行 ast.parse() + 应用启动验证,逐域推进
  • [风险] 全局状态的隐式共享 → 调度器、限流器的模块级变量在模块首次 import 时初始化Python 模块单例语义保证只初始化一次,行为与当前一致
  • [权衡] build_tab() 参数列表长 → 与现有 tab_create.py 的做法一致,接受这种显式依赖的冗长性,可在后续变更中引入 dataclass 参数包

Migration Plan

按以下顺序逐域提取,每步验证后再继续:

  1. services/rate_limiter.py —— 无外部依赖,最安全的起点
  2. services/autostart.py —— 独立,平台相关逻辑隔离
  3. services/persona.py —— 仅依赖 cfg
  4. services/connection.py —— 依赖 cfgllm_servicesd_servicemcp_client
  5. services/profile.py —— 依赖 mcp_client
  6. services/hotspot.py —— 依赖 llm_servicemcp_client
  7. services/content.py —— 依赖多个服务,最复杂
  8. services/engagement.py —— 依赖 rate_limitermcp_client
  9. services/scheduler.py —— 依赖 engagementcontent
  10. services/queue_ops.py —— 依赖 contentpub_queue
  11. ui/tab_hotspot.py ~ ui/tab_settings.py —— 7 个 Tab UI 拆分

回滚策略: 所有修改通过 git 追踪;每个 service 提取为一个独立 commit任意步骤可 git revert

Open Questions

  • _auto_log 列表(被 engagementscheduler 共同写入)归属哪个模块?
    → 暂定置于 services/scheduler.pyengagement 接收 log_fn 回调参数
  • queue_publisher 的 callback 注册(set_publish_callback)在哪里调用?
    → 保留在 main.py 初始化段callback 函数迁移到 services/queue_ops.py