xhs_factory/ui/tab_create.py
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

173 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
内容创作 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,
}