♻️ 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/` 目录
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: JSON 文件写入使用原子操作
|
||||
`ConfigManager.save()`、`AnalyticsService._save_analytics()` 和 `AnalyticsService._save_weights()` SHALL 使用「写临时文件 → `os.replace()` 原子重命名」的方式持久化数据。临时文件 SHALL 创建于与目标文件相同的目录(同卷),以确保 `os.replace()` 的原子性。
|
||||
|
||||
#### Scenario: 写入过程中进程中断不产生损坏文件
|
||||
- **WHEN** JSON 写入过程中进程被强制终止
|
||||
- **THEN** 目标文件保持写入前的完整状态,不出现空文件或半写入的 JSON
|
||||
|
||||
#### Scenario: 正常写入成功替换目标文件
|
||||
- **WHEN** `ConfigManager.save()` 被调用且数据合法
|
||||
- **THEN** 目标 `config.json` 被更新为最新内容,写入前存在的临时文件已被清理
|
||||
|
||||
#### Scenario: 临时文件与目标文件在同一目录
|
||||
- **WHEN** 调用任意原子写函数
|
||||
- **THEN** 临时文件的父目录与目标文件的父目录相同(通过 `tempfile.mkstemp(dir=<target_dir>)` 实现)
|
||||
@@ -0,0 +1,30 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 发布前校验标题、正文和图片
|
||||
`publish_to_xhs()` 函数 SHALL 在调用 MCP 发布接口前执行以下校验,任何校验失败 SHALL 立即返回包含明确说明的错误消息字符串,不发起网络请求:
|
||||
|
||||
| 字段 | 规则 |
|
||||
|------|------|
|
||||
| 标题 | 非空,长度 ≤ 20 个字符(中英文均按 1 字符计) |
|
||||
| 图片数量 | 至少 1 张,至多 18 张 |
|
||||
| 图片文件 | 每个路径对应的文件在磁盘上真实存在 |
|
||||
|
||||
#### Scenario: 标题超长时返回明确错误
|
||||
- **WHEN** `publish_to_xhs()` 被调用且标题字符数超过 20
|
||||
- **THEN** 返回包含「标题超长」提示及当前字符数的错误字符串,不调用 MCP 接口
|
||||
|
||||
#### Scenario: 无图片时返回明确错误
|
||||
- **WHEN** `publish_to_xhs()` 被调用且最终收集到的图片路径列表为空
|
||||
- **THEN** 返回「至少需要 1 张图片」的错误字符串
|
||||
|
||||
#### Scenario: 图片数量超限时返回明确错误
|
||||
- **WHEN** 最终图片路径列表超过 18 张
|
||||
- **THEN** 返回包含当前图片数和限制数的错误字符串,不发起发布请求
|
||||
|
||||
#### Scenario: 图片文件不存在时返回明确错误
|
||||
- **WHEN** 图片路径列表中有路径对应的文件不存在于磁盘
|
||||
- **THEN** 返回包含该文件路径的「文件不存在」错误字符串
|
||||
|
||||
#### Scenario: 校验通过后正常发布
|
||||
- **WHEN** 所有字段均通过校验
|
||||
- **THEN** 正常调用 MCP 接口发布,行为与改造前一致
|
||||
@@ -0,0 +1,20 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 敏感字段通过系统 keyring 或环境变量存储
|
||||
ConfigManager SHALL 提供 `get_secure(key: str) -> str` 和 `set_secure(key: str, value: str)` 接口,用于读写需要保护的配置项(API Key 等)。读取优先级:环境变量 `AUTOBOT_<KEY>` > 系统 keyring > `config.json` 明文(兼容旧版,读取后自动迁移)。`config.json` 中已迁移的字段值替换为占位符字符串 `"[keyring]"`。
|
||||
|
||||
#### Scenario: 首次读取明文 API Key 时自动迁移
|
||||
- **WHEN** 调用 `get_secure("api_key")` 且 `config.json` 中该字段为普通字符串(非占位符)
|
||||
- **THEN** 系统将该值写入 keyring,将 `config.json` 中该字段更新为 `"[keyring]"`,并返回原始值
|
||||
|
||||
#### Scenario: keyring 不可用时降级为明文
|
||||
- **WHEN** 系统 keyring 后端不可用(抛出 `NoKeyringError`)且无对应环境变量
|
||||
- **THEN** `get_secure()` 直接读取 `config.json` 中的明文值,并打印 WARNING 日志,不抛出异常
|
||||
|
||||
#### Scenario: 环境变量优先于 keyring
|
||||
- **WHEN** 环境变量 `AUTOBOT_API_KEY` 已设置且 keyring 中也有相同 key 的值
|
||||
- **THEN** `get_secure("api_key")` 返回环境变量的值
|
||||
|
||||
#### Scenario: 通过 UI 设置新的 API Key
|
||||
- **WHEN** 用户在 Gradio UI 中输入新的 API Key 并保存
|
||||
- **THEN** 调用 `set_secure("api_key", value)` 将值存入 keyring(或在降级模式下写入 `config.json`),UI 不显示原始值
|
||||
@@ -0,0 +1,23 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 发布完成后清理本次生成的临时图片文件
|
||||
`publish_to_xhs()` 函数 SHALL 在发布流程(无论成功或失败)结束后,删除本次调用写入 `_temp_publish/` 目录的 AI 生成临时图片文件。删除失败 SHALL 仅记录 WARNING 日志,不影响返回结果。
|
||||
|
||||
#### Scenario: 发布成功后临时文件被清理
|
||||
- **WHEN** `publish_to_xhs()` 发布成功并返回成功消息
|
||||
- **THEN** 本次写入的所有 `ai_N.jpg` 临时文件已从磁盘删除
|
||||
|
||||
#### Scenario: 发布失败后临时文件同样被清理
|
||||
- **WHEN** `publish_to_xhs()` 因网络错误等原因抛出异常或返回失败消息
|
||||
- **THEN** 本次写入的所有 `ai_N.jpg` 临时文件已从磁盘删除
|
||||
|
||||
#### Scenario: 清理失败不阻断主流程
|
||||
- **WHEN** 临时文件删除时抛出 `OSError`(如文件已被其他进程占用)
|
||||
- **THEN** 系统记录 WARNING 日志并继续,`publish_to_xhs()` 的返回值不受影响
|
||||
|
||||
### Requirement: 不清理其他会话的临时文件
|
||||
发布清理逻辑 SHALL 只删除本次调用写入的文件(通过追踪写入路径列表),不执行 `_temp_publish/` 目录的全量清空。
|
||||
|
||||
#### Scenario: 并发发布场景下不误删其他文件
|
||||
- **WHEN** 两次发布准备流程同时写入 `_temp_publish/` 目录
|
||||
- **THEN** 每次清理只删除自己写入的文件,不影响另一次的文件
|
||||
@@ -0,0 +1,19 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 笔记列表缓存读写受互斥锁保护
|
||||
模块级全局变量 `_cached_proactive_entries` 和 `_cached_my_note_entries` 的所有读写操作 SHALL 在 `threading.RLock` 的保护下执行,以防止 Gradio 回调并发调用时产生数据竞态。
|
||||
|
||||
#### Scenario: 并发刷新时缓存更新不出现竞态
|
||||
- **WHEN** 两个 Gradio 回调线程同时调用 `_fetch_and_cache()`
|
||||
- **THEN** 最终缓存状态为其中一次完整写入的结果,不出现部分更新或列表长度异常
|
||||
|
||||
#### Scenario: 读取缓存时不被并发写入中断
|
||||
- **WHEN** `_pick_from_cache()` 正在迭代缓存列表时,另一线程触发缓存更新
|
||||
- **THEN** 迭代过程不抛出 `RuntimeError: list changed size during iteration`
|
||||
|
||||
### Requirement: 缓存操作封装为受保护的工具函数
|
||||
模块 SHALL 提供 `_set_cache(name, entries)` 和 `_get_cache(name)` 两个内部函数,统一管理缓存读写,不在业务函数中直接赋值全局列表。
|
||||
|
||||
#### Scenario: 缓存写入通过统一接口
|
||||
- **WHEN** 任意函数需要更新笔记缓存
|
||||
- **THEN** 必须调用 `_set_cache(name, entries)` 而非直接赋值 `_cached_*` 变量
|
||||
@@ -0,0 +1,19 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 内容创作 Tab 的 UI 代码迁移至独立模块
|
||||
`ui/tab_create.py` SHALL 包含原 `main.py` 中「内容创作 Tab」的全部 Gradio 组件定义和事件绑定,并导出 `build_tab() -> None` 函数,该函数接受一个 `gr.Blocks` 上下文,在其中构建 Tab 内容。`main.py` SHALL 通过 `from ui.tab_create import build_tab` 调用该函数,不在主文件中保留重复的组件代码。
|
||||
|
||||
#### Scenario: main.py 正常启动并显示内容创作 Tab
|
||||
- **WHEN** 运行 `python main.py` 启动 Gradio 应用
|
||||
- **THEN** 内容创作 Tab 正常显示,所有组件与迁移前功能一致
|
||||
|
||||
#### Scenario: tab_create 模块可独立导入
|
||||
- **WHEN** 在 Python 中执行 `from ui.tab_create import build_tab`
|
||||
- **THEN** 不抛出任何导入错误,`build_tab` 为可调用对象
|
||||
|
||||
### Requirement: ui/ 目录结构规范
|
||||
`ui/` 目录 SHALL 包含 `__init__.py`,每个 Tab 模块文件命名约定为 `tab_<name>.py`,不在 Tab 模块中直接调用全局服务初始化代码(如 `ConfigManager()`、`LLMService()` 等单例初始化应由 `main.py` 完成并通过参数或模块级引用传入)。
|
||||
|
||||
#### Scenario: 新增 Tab 模块的标准结构
|
||||
- **WHEN** 开发者创建新的 `ui/tab_*.py` 文件
|
||||
- **THEN** 该文件导出 `build_tab(...)` 函数,且顶层不包含副作用代码(不在 import 时触发服务连接)
|
||||
Reference in New Issue
Block a user