zhoujie d88b4e9a3b ♻️ refactor(config): 实现配置安全存储与原子写
- 新增 `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/` 目录
2026-02-24 21:53:36 +08:00

5.2 KiB
Raw Blame History

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 Serviceconfig.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 内完成。


决策 3JSON 原子写使用 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 重构时不会遗失。


决策 6UI 拆分采用渐进式迁移,不一次性重写

ui/ 目录为目标,先提取最大的 Tab内容创作 Tabui/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

  1. 无需数据迁移config.json 的旧明文 Key 在首次调用 get_secure() 时自动读取并写入 keyring同时将 config.json 中对应字段替换为 "[keyring]" 占位符,单次完成。
  2. 依赖安装pip install keyring 并更新 requirements.txt
  3. 无回滚风险:各项改动均向后兼容,若 keyring 不可用则自动回退明文模式。

Open Questions

  • 是否需要在 UI 上增加「导出加密备份配置」功能,方便用户迁移设备?(本次范围外,记录待后续评估)
  • ui/ 拆分后是否需要引入 pytest-gradio 进行 UI 层单元测试?(本次不实施)