## 1. 依赖与环境准备

- [x] 1.1 在 `requirements.txt` 中添加 `keyring>=24.0.0`
- [x] 1.2 运行 `pip install keyring` 并验证在当前系统（Windows）可正常使用 `keyring.get_password` / `keyring.set_password`

## 2. 安全配置（secure-config）

- [x] 2.1 在 `config_manager.py` 中新增 `get_secure(key: str) -> str` 方法：优先读取环境变量 `AUTOBOT_<KEY.upper()>`，其次读取系统 keyring，最后回退 `config.json` 明文（自动迁移一次），捕获 `keyring.errors.NoKeyringError` 并降级
- [x] 2.2 在 `config_manager.py` 中新增 `set_secure(key: str, value: str)` 方法：写入系统 keyring（降级模式下写入 `config.json`），并将 `config.json` 对应字段更新为占位符 `"[keyring]"`
- [x] 2.3 将 `main.py` 中 LLM 提供商的 `api_key` 读写全部替换为 `cfg.get_secure()` / `cfg.set_secure()`
- [ ] 2.4 手动测试：重启应用后 `config.json` 中 `api_key` 已变为 `"[keyring]"`，LLM 连接功能正常

## 3. JSON 原子写（atomic-persistence）

- [x] 3.1 在 `config_manager.py` 的 `save()` 方法中将直接 `open(CONFIG_FILE, "w")` 改为 `tempfile.mkstemp(dir=<same_dir>)` + 写入 + `os.replace()` 原子重命名
- [x] 3.2 在 `analytics_service.py` 的 `_save_analytics()` 方法中同样改为原子写
- [x] 3.3 在 `analytics_service.py` 的 `_save_weights()` 方法中同样改为原子写
- [ ] 3.4 测试：在写入过程中（`time.sleep` 模拟）验证目标文件仍完整，临时文件被清理

## 4. 线程安全缓存（thread-safe-cache）

- [x] 4.1 在 `main.py` 顶部声明 `_cache_lock = threading.RLock()`
- [x] 4.2 新增内部函数 `_set_cache(name: str, entries: list)` 和 `_get_cache(name: str) -> list`，内部使用 `with _cache_lock:` 保护
- [x] 4.3 将 `_fetch_and_cache()` 中对 `_cached_proactive_entries` / `_cached_my_note_entries` 的直接赋值改为调用 `_set_cache()`
- [x] 4.4 将 `_pick_from_cache()` 中读取缓存改为调用 `_get_cache()`（在锁内完成列表快照拷贝）
- [x] 4.5 将 `fetch_my_notes()` 中对 `_cached_my_note_entries` 的直接赋值改为调用 `_set_cache()`

## 5. 发布前输入校验（publish-input-validation）

- [x] 5.1 在 `publish_to_xhs()` 函数内、MCP 调用前添加标题长度校验（`len(title) > 20` 返回错误）
- [x] 5.2 添加图片数量下限校验（`len(image_paths) == 0` 返回「至少需要 1 张图片」）
- [x] 5.3 添加图片数量上限校验（`len(image_paths) > 18` 返回含实际数量的错误消息）
- [x] 5.4 添加图片文件存在性校验（遍历 `image_paths`，发现不存在的文件时返回含路径的错误）
- [x] 5.5 在 Gradio UI 的发布按钮标题输入框旁添加字符计数提示（`gr.Textbox` 的 `info` 参数）

## 6. 临时文件生命周期（temp-file-lifecycle）

- [x] 6.1 在 `publish_to_xhs()` 中记录本次写入的 AI 临时图片路径到局部变量 `ai_temp_files = []`
- [x] 6.2 在函数末尾添加 `finally:` 块，遍历 `ai_temp_files` 逐一调用 `os.remove()`，捕获 `OSError` 仅记录 `logger.warning`
- [ ] 6.3 验证：发布成功后 `_temp_publish/` 目录中的 `ai_*.jpg` 文件已被删除；发布失败后同样被清理

## 7. UI 模块拆分（ui-module-split）

- [x] 7.1 创建 `ui/` 目录，添加 `ui/__init__.py`（空文件）
- [x] 7.2 创建 `ui/tab_create.py`，将 `main.py` 中「内容创作 Tab」的所有 Gradio 组件定义和 `.click()` / `.change()` 事件绑定代码迁移至该文件，导出 `build_tab(cfg, mcp_url_box, ...)` 函数
- [x] 7.3 在 `main.py` 中用 `from ui.tab_create import build_tab` + 调用替换原有内容创作 Tab 代码
- [ ] 7.4 启动应用，验证内容创作 Tab 功能与迁移前完全一致（文案生成、图片生成、发布按钮均正常）

## 8. 集成验证

- [ ] 8.1 启动应用，依次测试：LLM 连接 → 文案生成 → 图片生成 → 发布（包含校验不通过和校验通过两个场景）
- [ ] 8.2 检查 `config.json` 中无明文 API Key
- [ ] 8.3 检查 `_temp_publish/` 目录在发布后为空（或只含本次以外的文件）
- [ ] 8.4 检查 `autobot.log` 中无 ERROR 级别日志

