♻️ 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:
2026-02-24 21:53:36 +08:00
parent 4cde2f7c67
commit d88b4e9a3b
23 changed files with 836 additions and 190 deletions
+1
View File
@@ -0,0 +1 @@
# UI 模块包
+172
View File
@@ -0,0 +1,172 @@
"""
内容创作 Tab UI 模块
包含 Tab 1「✨ 内容创作」的所有 Gradio 组件定义和事件绑定
"""
import gradio as gr
def build_tab(
config: dict,
styles: list,
sd_preset_names: list,
default_negative: str,
# 共享的 Gradio 组件(由 main.py 创建并传入)
llm_model,
sd_model,
sd_url,
persona,
status_bar,
face_swap_toggle,
face_image_preview,
mcp_url,
# 业务处理函数(由 main.py 传入,避免循环导入)
fn_gen_copy,
fn_gen_img,
fn_export,
fn_publish,
fn_get_sd_preset,
fn_cfg_set,
fn_cfg_update,
):
"""
构建「✨ 内容创作」Tab,注册所有事件绑定。
Parameters
----------
config : 当前配置字典(用于读取初始值)
styles : 文案风格列表
sd_preset_names : SD 生成模式名称列表
default_negative: 默认反向提示词
llm_model / sd_model / ... : 由全局设置栏创建的共享组件引用
fn_* : 业务逻辑回调函数
Returns
-------
None —— 所有事件绑定均在此函数内完成,无需外部再次绑定。
"""
with gr.Tab("✨ 内容创作"):
# 本 Tab 私有的图片状态(不被其他 Tab 使用)
state_images = gr.State([])
with gr.Row():
# ---- 左栏:输入 ----
with gr.Column(scale=1):
gr.Markdown("### 💡 构思")
topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭")
style = gr.Dropdown(
styles,
label="风格", value="好物种草",
)
btn_gen_copy = gr.Button("✨ 第一步:生成文案", variant="primary")
gr.Markdown("---")
gr.Markdown("### 🎨 绘图参数")
quality_mode = gr.Radio(
sd_preset_names,
label="生成模式",
value=config.get("quality_mode", "标准 (约1分钟)"),
info="快速≈30s 标准≈1min 精细≈2-3min (SDXL)",
)
with gr.Accordion("高级设置 (覆盖预设)", open=False):
neg_prompt = gr.Textbox(
label="反向提示词",
value=config.get("sd_negative_prompt", default_negative),
lines=2,
)
steps = gr.Slider(8, 50, value=config.get("sd_steps", 20), step=1, label="步数")
cfg_scale = gr.Slider(1, 15, value=config.get("sd_cfg_scale", 5.5), step=0.5, label="CFG Scale")
btn_gen_img = gr.Button("🎨 第二步:生成图片", variant="primary")
# ---- 中栏:文案编辑 ----
with gr.Column(scale=1):
gr.Markdown("### 📝 文案编辑")
res_title = gr.Textbox(
label="标题",
interactive=True,
info="小红书限制 ≤20 字,超出将无法发布",
)
res_content = gr.TextArea(
label="正文 (可手动修改)", lines=12, interactive=True,
)
res_prompt = gr.TextArea(
label="绘图提示词", lines=3, interactive=True,
)
res_tags = gr.Textbox(
label="话题标签 (逗号分隔)", interactive=True,
placeholder="穿搭, 春季, 好物种草",
)
# ---- 右栏:预览 & 发布 ----
with gr.Column(scale=1):
gr.Markdown("### 🖼️ 视觉预览")
gallery = gr.Gallery(label="AI 生成图片", columns=2, height=300)
local_images = gr.File(
label="📁 上传本地图片(可混排)",
file_count="multiple",
file_types=["image"],
)
gr.Markdown("### 🚀 发布")
schedule_time = gr.Textbox(
label="定时发布 (可选, ISO8601格式)",
placeholder="如 2026-02-08T18:00:00+08:00,留空=立即发布",
)
with gr.Row():
btn_export = gr.Button("📂 导出本地", variant="secondary")
btn_publish = gr.Button("🚀 发布到小红书", variant="primary")
publish_msg = gr.Markdown("")
# ---- 事件绑定 ----
btn_gen_copy.click(
fn=fn_gen_copy,
inputs=[llm_model, topic, style, sd_model, persona],
outputs=[res_title, res_content, res_prompt, res_tags, status_bar],
)
def save_quality_mode(mode):
fn_cfg_set("quality_mode", mode)
p = fn_get_sd_preset(mode)
return p["steps"], p["cfg_scale"]
quality_mode.change(
fn=save_quality_mode,
inputs=[quality_mode],
outputs=[steps, cfg_scale],
)
steps.change(fn=lambda s: fn_cfg_set("sd_steps", s), inputs=[steps], outputs=[])
cfg_scale.change(fn=lambda c: fn_cfg_set("sd_cfg_scale", c), inputs=[cfg_scale], outputs=[])
neg_prompt.change(fn=lambda n: fn_cfg_set("sd_negative_prompt", n), inputs=[neg_prompt], outputs=[])
btn_gen_img.click(
fn=fn_gen_img,
inputs=[sd_url, res_prompt, neg_prompt, sd_model, steps, cfg_scale,
face_swap_toggle, face_image_preview, quality_mode, persona],
outputs=[gallery, state_images, status_bar],
)
btn_export.click(
fn=fn_export,
inputs=[res_title, res_content, state_images],
outputs=[publish_msg],
)
btn_publish.click(
fn=fn_publish,
inputs=[res_title, res_content, res_tags, state_images,
local_images, mcp_url, schedule_time],
outputs=[publish_msg],
)
# 返回可能被其他 Tab 引用的组件
return {
"res_title": res_title,
"res_content": res_content,
"res_prompt": res_prompt,
"res_tags": res_tags,
"quality_mode": quality_mode,
"steps": steps,
"cfg_scale": cfg_scale,
"neg_prompt": neg_prompt,
}