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"
This commit is contained in:
2026-02-24 22:50:56 +08:00
parent d88b4e9a3b
commit b635108b89
28 changed files with 5076 additions and 4271 deletions
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 开机自启管理函数迁移至独立模块
系统 SHALL 将开机自启相关常量和函数从 `main.py` 提取至 `services/autostart.py`,包括:`_APP_NAME`、`_STARTUP_REG_KEY`、`_get_startup_script_path`、`_get_startup_bat_path`、`_create_startup_scripts`、`is_autostart_enabled`、`enable_autostart`、`disable_autostart`、`toggle_autostart`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.autostart import toggle_autostart, is_autostart_enabled`
- **THEN** 函数可正常调用
#### Scenario: Windows 注册表操作行为不变
- **WHEN** `enable_autostart()` 在 Windows 系统上被调用
- **THEN** SHALL 向注册表 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` 写入启动项,行为与迁移前完全一致
#### Scenario: 非 Windows 平台处理不变
- **WHEN** `enable_autostart()` 在非 Windows 系统上被调用
- **THEN** SHALL 返回与迁移前相同的平台不支持提示信息
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 连接管理函数迁移至独立模块
系统 SHALL 将所有 LLM / SD / MCP 连接管理及认证相关函数从 `main.py` 提取至 `services/connection.py`,包括:`_get_llm_config`、`connect_llm`、`add_llm_provider`、`remove_llm_provider`、`on_provider_selected`、`connect_sd`、`on_sd_model_change`、`check_mcp_status`、`get_login_qrcode`、`logout_xhs`、`_auto_fetch_xsec_token`、`check_login`、`save_my_user_id`、`upload_face_image`、`load_saved_face_image`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.connection import connect_llm, connect_sd` 等导入
- **THEN** 所有函数可正常调用,行为与迁移前完全一致
#### Scenario: 外部依赖通过参数传入
- **WHEN** `services/connection.py` 中的函数需要访问 `cfg`、`llm`(`LLMService`)、`sd`(`SDService`)、`mcp`(`MCPClient`)
- **THEN** 这些依赖 SHALL 通过函数参数接收,`services/connection.py` 模块顶层不创建单例实例
#### Scenario: 无循环导入
- **WHEN** Python 解释器加载 `services/connection.py`
- **THEN** 不产生 `ImportError` 或循环导入错误
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 内容生成函数迁移至独立模块
系统 SHALL 将内容生成、图片生成、发布及导出相关函数从 `main.py` 提取至 `services/content.py`,包括:`generate_copy`、`generate_images`、`one_click_export`、`publish_to_xhs`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.content import generate_copy, generate_images, publish_to_xhs, one_click_export`
- **THEN** 所有函数可正常调用,行为与迁移前完全一致
#### Scenario: 内容生成保留现有验证逻辑
- **WHEN** 调用 `publish_to_xhs` 时标题超过 20 字或图片数量不合法
- **THEN** 函数 SHALL 返回与迁移前相同的错误提示,不改变验证行为
#### Scenario: 临时文件清理逻辑保留
- **WHEN** `publish_to_xhs` 执行完毕(成功或失败)
- **THEN** `finally` 块中的 AI 临时文件清理逻辑 SHALL 正常执行
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 互动自动化函数迁移至独立模块
系统 SHALL 将评论、点赞、收藏、回复等互动自动化函数从 `main.py` 提取至 `services/engagement.py`,包括:`load_note_for_comment`、`ai_generate_comment`、`send_comment`、`fetch_my_notes`、`on_my_note_selected`、`fetch_my_note_comments`、`ai_reply_comment`、`send_reply`、`auto_comment_once`、`_auto_comment_with_log`、`auto_like_once`、`_auto_like_with_log`、`auto_favorite_once`、`_auto_favorite_with_log`、`auto_reply_once`、`_auto_reply_with_log`、`_auto_publish_with_log`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.engagement import auto_comment_once, auto_like_once` 等导入
- **THEN** 所有函数可正常调用
#### Scenario: 日志回调参数化
- **WHEN** `engagement.py` 中的 `_with_log` 函数需要追加日志时
- **THEN** 函数 SHALL 接收 `log_fn` 参数(callable)用于写入日志,不直接依赖外部 `_auto_log` 列表
#### Scenario: 频率限制集成
- **WHEN** `auto_comment_once` 等函数执行前需要检查每日限额和冷却状态
- **THEN** 通过调用 `rate_limiter` 模块中的函数实现,不在 `engagement.py` 内复制限流逻辑
@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: 热点探测函数迁移至独立模块
系统 SHALL 将热点搜索与分析相关函数从 `main.py` 提取至 `services/hotspot.py`,包括:`search_hotspots`、`analyze_and_suggest`、`generate_from_hotspot`、`_set_cache`、`_get_cache`、`_fetch_and_cache`、`_pick_from_cache`、`fetch_proactive_notes`、`on_proactive_note_selected`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.hotspot import search_hotspots, analyze_and_suggest` 等导入
- **THEN** 所有函数可正常调用
#### Scenario: 线程安全缓存随模块迁移
- **WHEN** `_cache_lock`(`threading.RLock`)随函数一起迁移至 `services/hotspot.py`
- **THEN** `_set_cache` / `_get_cache` 的线程安全行为保持不变
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 人设管理函数及常量迁移至独立模块
系统 SHALL 将人设相关的常量和函数从 `main.py` 提取至 `services/persona.py`,包括:`DEFAULT_PERSONAS`、`RANDOM_PERSONA_LABEL`、`PERSONA_POOL_MAP`、`DEFAULT_TOPICS`、`DEFAULT_STYLES`、`DEFAULT_COMMENT_KEYWORDS`、`_match_persona_pools`、`get_persona_topics`、`get_persona_keywords`、`on_persona_changed`、`_resolve_persona`。
#### Scenario: 常量可从模块导入
- **WHEN** `main.py` 执行 `from services.persona import DEFAULT_PERSONAS, PERSONA_POOL_MAP`
- **THEN** 常量值 SHALL 与迁移前完全一致
#### Scenario: 人设解析正确处理随机人设标签
- **WHEN** `_resolve_persona(RANDOM_PERSONA_LABEL)` 被调用
- **THEN** SHALL 返回从人设池中随机选取的人设文本,行为与迁移前一致
#### Scenario: 人设变更回调正常触发
- **WHEN** `on_persona_changed(persona_text)` 被调用
- **THEN** SHALL 返回更新后的话题列表和关键词列表,供 Gradio UI 使用
@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: 用户主页解析函数迁移至独立模块
系统 SHALL 将用户主页数据获取与解析相关函数从 `main.py` 提取至 `services/profile.py`,包括:`_parse_profile_json`、`_parse_count`、`fetch_my_profile`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.profile import fetch_my_profile`
- **THEN** 函数可正常调用,行为与迁移前一致
#### Scenario: 解析容错性保留
- **WHEN** `_parse_count` 接收到格式异常的数值字符串(如 "1.2万"、"--")
- **THEN** SHALL 返回与迁移前相同的浮点数或 0,不抛出异常
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 排期队列操作函数迁移至独立模块
系统 SHALL 将内容排期队列相关函数从 `main.py` 提取至 `services/queue_ops.py`,包括:`generate_to_queue`、`_queue_publish_callback`、`queue_refresh_table`、`queue_refresh_calendar`、`queue_preview_item`、`queue_approve_item`、`queue_reject_item`、`queue_delete_item`、`queue_retry_item`、`queue_publish_now`、`queue_start_processor`、`queue_stop_processor`、`queue_get_status`、`queue_batch_approve`、`queue_generate_and_refresh`。
#### Scenario: 模块导入成功
- **WHEN** `main.py` 执行 `from services.queue_ops import queue_generate_and_refresh, queue_refresh_table` 等导入
- **THEN** 所有函数可正常调用
#### Scenario: publish callback 在 main.py 完成注册
- **WHEN** 应用启动时 `main.py` 调用 `pub_queue.set_publish_callback(_queue_publish_callback)`(`_queue_publish_callback` 已迁移至 `queue_ops.py`)
- **THEN** 队列发布回调 SHALL 正常注册并在队列处理时触发
#### Scenario: 队列操作读写 pub_queue 单例
- **WHEN** `queue_ops.py` 中的函数需要访问 `pub_queue` 或 `queue_publisher`
- **THEN** 这些单例 SHALL 通过函数参数传入,不在 `queue_ops.py` 模块顶层初始化
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 频率控制与每日限额函数迁移至独立模块
系统 SHALL 将频率控制、每日限额及冷却相关的所有状态变量和函数从 `main.py` 提取至 `services/rate_limiter.py`,包括:`_auto_running`、`_op_history`、`_daily_stats`、`DAILY_LIMITS`、`_consecutive_errors`、`_error_cooldown_until`、`_reset_daily_stats_if_needed`、`_check_daily_limit`、`_increment_stat`、`_record_error`、`_clear_error_streak`、`_is_in_cooldown`、`_is_in_operating_hours`、`_get_stats_summary`。
#### Scenario: 模块级状态初始化一次
- **WHEN** Python 首次导入 `services/rate_limiter.py`
- **THEN** `_daily_stats`、`_op_history` 等模块级变量 SHALL 仅初始化一次(Python 模块单例语义)
#### Scenario: 每日限额检查正常工作
- **WHEN** `_check_daily_limit("comment")` 被调用
- **THEN** 返回值 SHALL 与迁移前行为完全一致
#### Scenario: 运营时段限制正常工作
- **WHEN** 当前时间不在 `start_hour` 至 `end_hour` 范围内时调用 `_is_in_operating_hours`
- **THEN** 返回 `False`,阻止自动化操作执行
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 自动调度器函数迁移至独立模块
系统 SHALL 将调度器相关的状态变量和函数从 `main.py` 提取至 `services/scheduler.py`,包括:`_scheduler_next_times`、`_auto_log`(列表)、`_auto_log_append`、`_scheduler_loop`、`start_scheduler`、`stop_scheduler`、`get_auto_log`、`get_scheduler_status`、`_learn_running`、`_learn_scheduler_loop`、`start_learn_scheduler`、`stop_learn_scheduler`。
#### Scenario: 调度器启停正常工作
- **WHEN** `start_scheduler(...)` 被调用并传入合法参数
- **THEN** 调度器线程 SHALL 正常启动,`get_scheduler_status()` 返回运行中状态
#### Scenario: 日志追加线程安全
- **WHEN** 多个自动化任务并发调用 `_auto_log_append(msg)`
- **THEN** 日志条目 SHALL 正确追加,不丢失和乱序
#### Scenario: engagement 通过回调写日志
- **WHEN** `services/engagement.py` 中的函数需要写日志时
- **THEN** SHALL 通过 `log_fn` 参数(由 `scheduler.py` 传入 `_auto_log_append`)写入,不直接导入 `scheduler.py`
@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: 剩余 Gradio Tab 提取为独立 UI 模块
系统 SHALL 将 `main.py` 中除 Tab 1(已完成)之外的 7 个 Gradio Tab 各自提取为 `ui/tab_*.py` 模块文件,每个文件暴露 `build_tab(...)` 函数:
| 模块文件 | Tab 名称 |
|---|---|
| `ui/tab_hotspot.py` | 🔥 热点探测 |
| `ui/tab_engage.py` | 💬 互动运营 |
| `ui/tab_profile.py` | 👤 我的主页 |
| `ui/tab_auto.py` | 🤖 自动运营 |
| `ui/tab_queue.py` | 📅 内容排期 |
| `ui/tab_analytics.py` | 📊 数据分析 |
| `ui/tab_settings.py` | ⚙️ 系统设置 |
#### Scenario: 每个 Tab 模块暴露 build_tab 函数
- **WHEN** `main.py` 执行 `from ui.tab_hotspot import build_tab as build_tab_hotspot`
- **THEN** 调用 `build_tab_hotspot(fn_*, ...)` 后 SHALL 返回包含需跨 Tab 共享组件的 dict
#### Scenario: build_tab 接收回调而非直接导入 services
- **WHEN** `build_tab(...)` 内部需要调用业务函数时
- **THEN** 业务函数 SHALL 通过 `fn_*` 参数传入(与 `tab_create.py` 已有模式一致),不在 `ui/tab_*.py` 内直接 `import services.*`
#### Scenario: 事件绑定在 build_tab 内完成
- **WHEN** `build_tab(...)` 被调用
- **THEN** 本 Tab 所有 Gradio 组件的 `.click()`、`.change()` 等事件绑定 SHALL 在函数内完成,`main.py` 不保留本 Tab 的事件绑定代码
#### Scenario: main.py 成为纯入口层
- **WHEN** 所有 11 个 capability 均完成迁移后
- **THEN** `main.py` 行数 SHALL 不超过 400 行,且不包含任何业务逻辑(仅含导入、单例初始化、UI 组装、`app.launch()`)