- 新增 `get_secure()` 和 `set_secure()` 方法,优先从环境变量或系统 keyring 读取敏感配置,`config.json` 中仅存储占位符 - 将 `save()` 方法改为使用临时文件 + `os.replace()` 的原子写入,防止进程中断导致配置文件损坏 - 在 `add_llm_provider()` 和 `get_active_llm()` 中集成安全配置读写,自动迁移旧版明文 API Key ♻️ refactor(analytics): 实现分析数据原子写 - 将 `_save_analytics()` 和 `_save_weights()` 方法改为使用临时文件 + `os.replace()` 的原子写入 - 确保在写入过程中进程被终止时,原始数据文件保持完整 ♻️ refactor(main): 增强发布功能健壮性与代码模块化 - 在 `publish_to_xhs()` 中增加发布前输入校验【标题长度、图片数量、文件存在性】并在 `finally` 块中自动清理本次生成的临时图片文件 - 为全局笔记列表缓存 `_cached_proactive_entries` 和 `_cached_my_note_entries` 引入 `threading.RLock` 保护,新增 `_set_cache()` 和 `_get_cache()` 线程安全操作函数 - 将「内容创作」Tab 的 UI 构建代码拆分至 `ui/tab_create.py` 模块,主文件通过 `build_tab()` 函数调用并组装 - 将 Gradio 应用的 CSS 和主题配置提取为模块级变量,提升可维护性 📦 build(deps): 新增 keyring 依赖 - 在 `requirements.txt` 中添加 `keyring>=24.0.0` 以支持系统凭证管理 📝 docs(openspec): 新增生产就绪审计文档 - 在 `openspec/changes/archive/2026-02-24-production-readiness-audit/` 下新增设计文档、提案、任务清单及各功能规格说明 - 将核心功能规格同步至 `openspec/specs/` 目录
5.2 KiB
Context
当前项目是一个单机运行的小红书 AI 爆文自动化工具,使用 Gradio 作为 UI 框架,SQLite 为发布队列持久化,JSON 文件存储配置与分析数据。随着功能迭代至 V2.0,主文件 main.py 已超过 4400 行,并积累了多处生产风险:API Key 以明文写入 config.json、全局列表变量被多个回调线程无锁读写、_temp_publish/ 目录的临时图片在发布后未清理、JSON 文件采用直接覆盖写入(电量耗尽或进程中断时会写出空文件)、发布前无标题/正文/图片数量的校验。
Goals / Non-Goals
Goals:
- 消除 6 项已知的生产风险,使项目能安全、稳定地长期自动运营
- 保持所有 Gradio 回调函数签名不变(不破坏现有 UI 绑定)
- 每项改动独立可部署,不相互耦合
- 为后续功能拓展提供更清晰的代码结构基础
Non-Goals:
- 重写为 Web 服务或引入数据库 ORM
- 改变现有 UI 交互逻辑或视觉设计
- 实现性能优化或多账号支持
- 修改 MCP / SD / LLM 服务接口
Decisions
决策 1:敏感配置存储方案选 keyring + 环境变量覆盖,放弃纯加密文件
选项 A(选定): 使用 keyring 库将 API Key 存入系统凭证管理器(Windows Credential Manager / macOS Keychain / Linux Secret Service),config.json 中仅保留占位符 "[keyring]";同时支持 AUTOBOT_API_KEY_<NAME> 环境变量在无 GUI 场景(Docker / CI)下覆盖。
选项 B(放弃): 自行用 Fernet 加密后写入 config.json。缺点:密钥仍需本地存储,安全提升有限,且增加密钥管理复杂度。
选项 C(放弃): 要求用户全部改用环境变量。对当前 Gradio UI 用户体验极差,用户每次重启需重新设置。
结论:keyring 方案对 Windows 单机用户最为透明,且 Docker 场景通过环境变量无缝降级。新增 ConfigManager.get_secure(key) / set_secure(key, value) 接口,内部优先读取环境变量,其次读 keyring,最后回退旧版明文(自动迁移一次)。
决策 2:全局缓存改用模块级 threading.RLock 保护,不引入新类
选项 A(选定): 在 main.py 模块顶层声明一个 _cache_lock = threading.RLock(),所有读写 _cached_proactive_entries / _cached_my_note_entries 的函数用 with _cache_lock: 包裹。
选项 B(放弃): 封装为 CacheManager 类。当前代码耦合 Gradio UI 较深,引入类会导致较大重构,收益不成比例。
结论:最小侵入方案,改动 5 处函数约 20 行,可在一个 PR 内完成。
决策 3:JSON 原子写使用 tempfile + os.replace
Python 标准库 tempfile.NamedTemporaryFile + os.replace(同目录)在 POSIX 和 Windows 均为原子操作(Windows Vista+ 支持)。无需引入新依赖。
适用范围:ConfigManager.save()、AnalyticsService._save_analytics()、AnalyticsService._save_weights()。
决策 4:临时文件清理在发布回调内同步执行
在 publish_to_xhs() 函数的 try/finally 块中清理 _temp_publish/ 下以本次调用 ai_N.jpg 命名的文件。不使用全目录清空,以免并发发布时误删其他会话文件(Gradio 多用户虽少见,但更安全)。逐文件删除,删除失败仅打印 warning,不阻断流程。
决策 5:发布校验集中在 publish_to_xhs() 一处,不分散到 UI
校验逻辑写在业务函数中,而非 Gradio 的 gr.Textbox 校验器,保证逻辑可测试,且 UI 重构时不会遗失。
决策 6:UI 拆分采用渐进式迁移,不一次性重写
以 ui/ 目录为目标,先提取最大的 Tab(内容创作 Tab)为 ui/tab_create.py,返回 (components, callbacks) 元组,main.py 调用并注册。其余 Tab 在后续迭代中逐步迁移。本次变更只迁移 ui/tab_create.py 作为示范,不强制完成全部拆分。
Risks / Trade-offs
| 风险 | 缓解措施 |
|---|---|
keyring 在部分 Linux headless 环境无后端可用 |
检测到 keyring.errors.NoKeyringError 时降级为明文(打印警告),行为与改造前一致 |
os.replace 在跨卷(不同磁盘分区)时失败 |
使用 tempfile.mkstemp(dir=same_dir) 确保临时文件与目标同卷 |
并发发布时 ai_N.jpg 命名冲突 |
当前 Gradio 为单进程单用户;若未来支持多用户,改用 UUID 命名 |
| UI 拆分期间双重维护 | 每次只迁移一个 Tab,迁移完成前旧代码仍有效 |
Migration Plan
- 无需数据迁移:
config.json的旧明文 Key 在首次调用get_secure()时自动读取并写入 keyring,同时将config.json中对应字段替换为"[keyring]"占位符,单次完成。 - 依赖安装:
pip install keyring并更新requirements.txt。 - 无回滚风险:各项改动均向后兼容,若 keyring 不可用则自动回退明文模式。
Open Questions
- 是否需要在 UI 上增加「导出加密备份配置」功能,方便用户迁移设备?(本次范围外,记录待后续评估)
ui/拆分后是否需要引入 pytest-gradio 进行 UI 层单元测试?(本次不实施)