xhs_factory/ui/app.py
zhoujie b5deafa2cc feat(config): 更新模型配置与LLM提示词指南
- 将默认LLM模型从gemini-2.0-flash升级为gemini-3-flash-preview
- 将博主人设从"性感福利主播"更改为"二次元coser"
- 优化LLM生成SD提示词的指南,新增中国审美人物描述规则
- 为各SD模型添加颜值核心词、示范prompt和禁止使用的关键词
- 新增三维人物描述法(眼睛/肤色/气质)和专属光线词指导

📦 build(openspec): 归档旧规范并创建新规范

- 将improve-maintainability规范归档至2026-02-25目录
- 新增2026-02-26-improve-ui-layout规范,包含UI布局优化设计
- 新增2026-02-26-optimize-image-generation规范,包含图片生成优化设计
- 在根目录openspec/specs下新增图片质量、后处理、中国审美和LLM提示词规范

♻️ refactor(sd_service): 优化SD模型配置和图片后处理

- 为各SD模型添加中国审美特征词和欧美面孔排除词
- 新增高画质预设档,SDXL模型启用Hires Fix参数
- 将后处理拆分为beauty_enhance和anti_detect_postprocess两个独立函数
- 新增美化增强功能,支持通过enhance_level参数控制强度

♻️ refactor(services): 更新内容生成服务以支持美化增强

- 在generate_images函数中新增enhance_level参数
- 将美化强度参数传递至SDService.txt2img调用

♻️ refactor(ui): 优化UI布局和添加美化强度控件

- 注入自定义CSS主题层,优化字体、按钮和卡片样式
- 将全局设置迁移至独立的"⚙️ 配置"Tab,优化Tab顺序
- 在内容创作Tab的高级设置中添加美化强度滑块控件
- 优化自动运营Tab布局,改为2列卡片网格展示
2026-02-26 22:58:05 +08:00

1412 lines
68 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
ui/app.py
Gradio 应用界面构建函数 — 包含全部 Tab UI 组件和事件绑定
"""
import os
import platform
import logging
import gradio as gr
from config_manager import ConfigManager
from sd_service import SDService, DEFAULT_NEGATIVE, FACE_IMAGE_PATH, SD_PRESET_NAMES, get_sd_preset, SD_MODEL_PROFILES
from analytics_service import AnalyticsService
from ui.tab_create import build_tab
from services.connection import (
connect_llm, add_llm_provider, remove_llm_provider, on_provider_selected,
connect_sd, on_sd_model_change, check_mcp_status,
get_login_qrcode, logout_xhs, check_login,
save_my_user_id, upload_face_image, load_saved_face_image,
)
from services.persona import (
DEFAULT_PERSONAS, RANDOM_PERSONA_LABEL, DEFAULT_TOPICS, DEFAULT_STYLES,
get_persona_topics, get_persona_keywords, on_persona_changed,
)
from services.hotspot import (
search_hotspots, analyze_and_suggest, generate_from_hotspot,
fetch_proactive_notes, on_proactive_note_selected,
)
from services.engagement import (
load_note_for_comment, ai_generate_comment, send_comment,
fetch_my_notes, on_my_note_selected, fetch_my_note_comments,
ai_reply_comment, send_reply,
)
from services.profile import fetch_my_profile
from services.scheduler import (
_auto_log, get_auto_log, get_scheduler_status,
start_scheduler, stop_scheduler,
analytics_collect_data, analytics_calculate_weights,
analytics_llm_deep_analysis, analytics_get_report, analytics_get_weighted_topics,
_auto_comment_with_log, _auto_like_with_log, _auto_favorite_with_log,
_auto_publish_with_log, _auto_reply_with_log,
start_learn_scheduler, stop_learn_scheduler,
_get_stats_summary,
)
from services.queue_ops import (
generate_to_queue, queue_refresh_table, queue_refresh_calendar,
queue_preview_item, queue_approve_item, queue_reject_item,
queue_delete_item, queue_retry_item, queue_publish_now,
queue_start_processor, queue_stop_processor, queue_get_status,
queue_batch_approve, queue_generate_and_refresh,
queue_format_table, queue_format_calendar,
)
from services.autostart import is_autostart_enabled, toggle_autostart
from services.content import generate_copy, generate_images, one_click_export, publish_to_xhs
from publish_queue import STATUS_LABELS
logger = logging.getLogger("autobot")
_GRADIO_CSS = """
/* ── Autobot 主题层 ── */
body, .gradio-container {
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif !important;
}
/* 按钮圆角统一 ≥8px */
.gr-button, button.lg, button.sm, button.md {
border-radius: 8px !important;
}
/* gr.Group 卡片轻阴影 */
.gr-group {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.07) !important;
border-radius: 10px !important;
}
/* 标题层级优化 */
.gradio-container h3 { font-size: 1.05rem; font-weight: 600; }
.gradio-container h4 { font-size: 0.95rem; font-weight: 600; }
/* Tab 导航条优化 */
.tab-nav {
border-bottom: 2px solid #f0f0f0;
gap: 2px;
}
.tab-nav button {
border-radius: 6px 6px 0 0 !important;
font-weight: 500;
padding: 6px 14px;
}
"""
def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
"""构建并返回 Gradio 应用(包含所有 Tab 和事件绑定)"""
config = cfg.all
with gr.Blocks(
title="小红书 AI 爆文工坊 V2.0",
css=_GRADIO_CSS,
) as app:
gr.Markdown(
"# 🍒 小红书 AI 爆文生产工坊 V2.0\n"
"> 灵感 → 文案 → 绘图 → 发布 → 运营,一站式全闭环"
)
# 全局状态
state_search_result = gr.State("")
# ============ Tab 页面 ============
# ⚙️ 配置 Tab 声明在最前以确保共享组件变量先于 build_tab() 定义
# selected=1 令「✨ 内容创作」为默认激活 Tab
with gr.Tabs(selected=1):
# -------- Tab 0: ⚙️ 配置 --------
with gr.Tab("⚙️ 配置"):
gr.Markdown("#### 🤖 LLM 提供商 (支持所有 OpenAI 兼容接口)")
with gr.Row():
llm_provider = gr.Dropdown(
label="选择 LLM 提供商",
choices=cfg.get_llm_provider_names(),
value=cfg.get("active_llm", ""),
interactive=True, scale=2,
)
btn_connect_llm = gr.Button("🔗 连接 LLM", variant="primary", size="sm", scale=1)
with gr.Row():
llm_model = gr.Dropdown(
label="LLM 模型", value=config["model"],
allow_custom_value=True, interactive=True, scale=2,
)
llm_provider_info = gr.Markdown(
value="*选择提供商后显示详情*",
)
with gr.Accordion(" 添加 / 管理 LLM 提供商", open=False):
with gr.Row():
new_provider_name = gr.Textbox(
label="名称", placeholder="如: DeepSeek / GPT-4o / 通义千问",
scale=1,
)
new_provider_key = gr.Textbox(
label="API Key", type="password", scale=2,
)
new_provider_url = gr.Textbox(
label="Base URL", placeholder="https://api.openai.com/v1",
value="https://api.openai.com/v1", scale=2,
)
with gr.Row():
btn_add_provider = gr.Button("✅ 添加提供商", variant="primary", size="sm")
btn_del_provider = gr.Button("🗑️ 删除当前提供商", variant="stop", size="sm")
provider_mgmt_status = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 🔗 服务连接")
with gr.Row():
mcp_url = gr.Textbox(
label="MCP Server URL", value=config["mcp_url"], scale=2,
)
sd_url = gr.Textbox(
label="SD WebUI URL", value=config["sd_url"], scale=2,
)
with gr.Row():
persona = gr.Dropdown(
label="博主人设(评论/回复/自动运营通用)",
choices=[RANDOM_PERSONA_LABEL] + DEFAULT_PERSONAS,
value=config.get("persona", RANDOM_PERSONA_LABEL),
allow_custom_value=True,
interactive=True,
scale=5,
)
with gr.Row():
btn_connect_sd = gr.Button("🎨 连接 SD", variant="primary", size="sm")
btn_check_mcp = gr.Button("📡 检查 MCP", size="sm")
with gr.Row():
sd_model = gr.Dropdown(
label="SD 模型", allow_custom_value=True,
interactive=True, scale=2,
)
sd_model_info = gr.Markdown("选择模型后显示适配信息", elem_id="sd_model_info")
status_bar = gr.Markdown("🔄 等待连接...")
gr.Markdown("---")
gr.Markdown("#### 🎭 AI 换脸 (ReActor)")
gr.Markdown(
"> 上传你的头像,生成含人物的图片时自动替换为你的脸\n"
"> 需要 SD WebUI 已安装 [ReActor](https://github.com/Gourieff/sd-webui-reactor) 扩展"
)
with gr.Row():
face_image_input = gr.Image(
label="上传头像 (正面清晰照片效果最佳)",
type="pil",
height=180,
scale=1,
)
face_image_preview = gr.Image(
label="当前头像",
type="pil",
height=180,
interactive=False,
value=SDService.load_face_image(),
scale=1,
)
with gr.Row():
btn_save_face = gr.Button("💾 保存头像", variant="primary", size="sm")
face_swap_toggle = gr.Checkbox(
label="🎭 生成图片时启用 AI 换脸",
value=os.path.isfile(FACE_IMAGE_PATH),
interactive=True,
)
face_status = gr.Markdown(
"✅ 头像已就绪" if os.path.isfile(FACE_IMAGE_PATH) else " 尚未设置头像"
)
gr.Markdown("---")
gr.Markdown("#### 🖥️ 系统设置")
with gr.Row():
autostart_toggle = gr.Checkbox(
label="🚀 Windows 开机自启(静默后台运行)",
value=is_autostart_enabled(),
interactive=(platform.system() == "Windows"),
)
autostart_status = gr.Markdown(
value="✅ 已启用" if is_autostart_enabled() else "⚪ 未启用",
)
# -------- Tab 1: ✨ 内容创作(默认激活)--------
_tab1 = build_tab(
config=config,
styles=DEFAULT_STYLES,
sd_preset_names=SD_PRESET_NAMES,
default_negative=DEFAULT_NEGATIVE,
llm_model=llm_model,
sd_model=sd_model,
sd_url=sd_url,
persona=persona,
status_bar=status_bar,
face_swap_toggle=face_swap_toggle,
face_image_preview=face_image_preview,
mcp_url=mcp_url,
fn_gen_copy=generate_copy,
fn_gen_img=generate_images,
fn_export=one_click_export,
fn_publish=publish_to_xhs,
fn_get_sd_preset=get_sd_preset,
fn_cfg_set=cfg.set,
fn_cfg_update=cfg.update,
)
res_title = _tab1["res_title"]
res_content = _tab1["res_content"]
res_prompt = _tab1["res_prompt"]
res_tags = _tab1["res_tags"]
quality_mode = _tab1["quality_mode"]
steps = _tab1["steps"]
cfg_scale = _tab1["cfg_scale"]
neg_prompt = _tab1["neg_prompt"]
# -------- Tab 2: 📅 内容排期 --------
with gr.Tab("📅 内容排期"):
gr.Markdown(
"### 📅 内容排期日历 + 发布队列\n"
"> 批量生成内容 → 预览审核 → 排期定时 → 自动发布,内容创作全流程管控\n\n"
"**工作流**: 生成内容 → 📝草稿 → ✅审核通过 → 🕐排期/立即发布 → 🚀自动发布"
)
with gr.Row():
# ===== 左栏: 生成 & 队列控制 =====
with gr.Column(scale=1):
gr.Markdown("#### 🔧 批量生成到队列")
queue_gen_topics = gr.Textbox(
label="主题池 (逗号分隔,随人设自动切换)",
value=", ".join(get_persona_topics(config.get("persona", ""))),
placeholder="会从池中随机选取,切换人设自动更新",
)
with gr.Row():
queue_gen_count = gr.Number(
label="生成数量", value=config.get("queue_gen_count", 3), minimum=1, maximum=10,
)
queue_gen_schedule = gr.Textbox(
label="排期时间 (可选)",
placeholder="如 2026-02-10 18:00:00留空=仅草稿",
)
btn_queue_generate = gr.Button(
"📝 批量生成 → 加入队列", variant="primary", size="lg",
)
queue_gen_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### ⚙️ 队列处理器")
queue_processor_status = gr.Markdown(
value=queue_get_status(),
)
with gr.Row():
btn_queue_start = gr.Button(
"▶️ 启动队列处理", variant="primary",
)
btn_queue_stop = gr.Button(
"⏹️ 停止队列处理", variant="stop",
)
queue_processor_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 🔍 操作单个队列项")
queue_item_id = gr.Textbox(
label="队列项 ID", placeholder="输入 # 号,如 1",
)
with gr.Row():
btn_queue_preview = gr.Button("👁️ 预览", size="sm")
btn_queue_approve = gr.Button("✅ 通过", size="sm", variant="primary")
btn_queue_reject = gr.Button("🚫 拒绝", size="sm", variant="stop")
with gr.Row():
btn_queue_publish_now = gr.Button("🚀 立即发布", size="sm", variant="primary")
btn_queue_retry = gr.Button("🔄 重试", size="sm")
btn_queue_delete = gr.Button("🗑️ 删除", size="sm", variant="stop")
queue_schedule_time = gr.Textbox(
label="排期时间 (审核通过时可指定)",
placeholder="如 2026-02-10 20:00:00留空=立即待发布",
)
btn_queue_batch_approve = gr.Button(
"✅ 批量通过所有草稿", variant="secondary",
)
queue_op_result = gr.Markdown("")
# ===== 右栏: 队列列表 & 日历 =====
with gr.Column(scale=2):
gr.Markdown("#### 📋 发布队列")
with gr.Row():
queue_filter = gr.Dropdown(
label="状态筛选",
choices=["全部"] + list(STATUS_LABELS.values()),
value="全部",
)
btn_queue_refresh = gr.Button("🔄 刷新", size="sm")
queue_table = gr.Markdown(
value=queue_format_table(),
label="队列列表",
)
gr.Markdown("---")
gr.Markdown("#### 📅 排期日历")
queue_calendar = gr.Markdown(
value=queue_format_calendar(),
label="日历视图",
)
gr.Markdown("---")
gr.Markdown("#### 👁️ 内容预览")
queue_preview_display = gr.Markdown(
value="*选择队列项 ID 后点击预览*",
)
# -------- Tab 3: 🔥 热点探测 --------
with gr.Tab("🔥 热点探测"):
gr.Markdown("### 搜索热门内容 → AI 分析趋势 → 一键借鉴创作")
with gr.Row():
with gr.Column(scale=1):
hot_keyword = gr.Textbox(
label="搜索关键词", placeholder="如:春季穿搭",
)
hot_sort = gr.Dropdown(
["综合", "最新", "最多点赞", "最多评论", "最多收藏"],
label="排序", value="综合",
)
btn_search = gr.Button("🔍 搜索", variant="primary")
search_status = gr.Markdown("")
with gr.Column(scale=2):
search_output = gr.TextArea(
label="搜索结果", lines=12, interactive=False,
)
with gr.Row():
btn_analyze = gr.Button("🧠 AI 分析热点趋势", variant="primary")
analysis_status = gr.Markdown("")
analysis_output = gr.Markdown(label="分析报告")
topic_from_hot = gr.Textbox(
label="选择/输入创作选题", placeholder="基于分析选一个方向",
)
with gr.Row():
hot_style = gr.Dropdown(
["好物种草", "干货教程", "情绪共鸣", "生活Vlog", "测评避雷"],
label="风格", value="好物种草",
)
btn_gen_from_hot = gr.Button("✨ 基于热点生成文案", variant="primary")
with gr.Row():
hot_title = gr.Textbox(label="生成的标题", interactive=True)
hot_content = gr.TextArea(label="生成的正文", lines=8, interactive=True)
with gr.Row():
hot_prompt = gr.TextArea(label="绘图提示词", lines=3, interactive=True)
hot_tags = gr.Textbox(label="标签", interactive=True)
hot_gen_status = gr.Markdown("")
btn_sync_to_create = gr.Button(
"📋 同步到「内容创作」Tab → 绘图 & 发布",
variant="primary",
)
# -------- Tab 3: 评论管家 --------
with gr.Tab("💬 评论管家"):
gr.Markdown("### 智能评论管理:主动评论引流 & 自动回复粉丝")
with gr.Tabs():
# ======== 子 Tab A: 主动评论他人 ========
with gr.Tab("✍️ 主动评论引流"):
gr.Markdown(
"> **流程**:搜索/浏览笔记 → 选择目标 → 加载内容 → "
"AI 分析笔记+已有评论自动生成高质量评论 → 一键发送"
)
# 笔记选择器
with gr.Row():
pro_keyword = gr.Textbox(
label="🔍 搜索关键词 (留空则获取推荐)",
placeholder="穿搭、美食、旅行…",
)
btn_pro_fetch = gr.Button("🔍 获取笔记", variant="primary")
with gr.Row():
pro_selector = gr.Dropdown(
label="📋 选择目标笔记",
choices=[], interactive=True,
)
pro_fetch_status = gr.Markdown("")
# 隐藏字段
with gr.Row():
pro_feed_id = gr.Textbox(label="笔记 ID", interactive=False)
pro_xsec_token = gr.Textbox(label="xsec_token", interactive=False)
pro_title = gr.Textbox(label="标题", interactive=False)
# 加载内容 & AI 分析
btn_pro_load = gr.Button("📖 加载笔记内容", variant="secondary")
pro_load_status = gr.Markdown("")
with gr.Row():
with gr.Column(scale=1):
pro_content = gr.TextArea(
label="📄 笔记正文摘要", lines=8, interactive=False,
)
with gr.Column(scale=1):
pro_comments = gr.TextArea(
label="💬 已有评论", lines=8, interactive=False,
)
# 隐藏: 完整文本
pro_full_text = gr.Textbox(visible=False)
gr.Markdown("---")
with gr.Row():
with gr.Column(scale=1):
btn_pro_ai = gr.Button(
"🤖 AI 智能生成评论", variant="primary", size="lg",
)
pro_ai_status = gr.Markdown("")
with gr.Column(scale=2):
pro_comment_text = gr.TextArea(
label="✏️ 评论内容 (可手动修改)", lines=3,
interactive=True,
placeholder="点击左侧按钮自动生成,也可手动编写",
)
with gr.Row():
btn_pro_send = gr.Button("📩 发送评论", variant="primary")
pro_send_status = gr.Markdown("")
# ======== 子 Tab B: 回复我的评论 ========
with gr.Tab("💌 回复粉丝评论"):
gr.Markdown(
"> **流程**:选择我的笔记 → 加载评论 → "
"粘贴要回复的评论 → AI 生成回复 → 一键发送"
)
# 笔记选择器 (自动用已保存的 userId 获取)
with gr.Row():
btn_my_fetch = gr.Button("🔍 获取我的笔记", variant="primary")
with gr.Row():
my_selector = gr.Dropdown(
label="📋 选择我的笔记",
choices=[], interactive=True,
)
my_fetch_status = gr.Markdown("")
with gr.Row():
my_feed_id = gr.Textbox(label="笔记 ID", interactive=False)
my_xsec_token = gr.Textbox(label="xsec_token", interactive=False)
my_title = gr.Textbox(label="笔记标题", interactive=False)
btn_my_load_comments = gr.Button("📥 加载评论", variant="primary")
my_comment_status = gr.Markdown("")
my_comments_display = gr.TextArea(
label="📋 粉丝评论列表", lines=12, interactive=False,
)
gr.Markdown("---")
gr.Markdown("#### 📝 回复评论")
with gr.Row():
with gr.Column(scale=1):
my_target_comment = gr.TextArea(
label="要回复的评论内容", lines=3,
placeholder="从上方评论列表中复制粘贴要回复的评论",
)
btn_my_ai_reply = gr.Button(
"🤖 AI 生成回复", variant="secondary",
)
my_reply_gen_status = gr.Markdown("")
with gr.Column(scale=1):
my_reply_content = gr.TextArea(
label="回复内容 (可修改)", lines=3,
interactive=True,
)
btn_my_send_reply = gr.Button(
"📩 发送回复", variant="primary",
)
my_reply_status = gr.Markdown("")
# -------- Tab 5: 📊 数据看板 --------
with gr.Tab("📊 数据看板"):
gr.Markdown(
"### 我的账号数据看板\n"
"> 用户 ID 和 xsec_token 从「账号登录」自动获取,直接点击加载即可"
)
with gr.Row():
with gr.Column(scale=1):
data_user_id = gr.Textbox(
label="我的用户 ID (自动填充)",
value=config.get("my_user_id", ""),
interactive=True,
)
data_xsec_token = gr.Textbox(
label="xsec_token (自动填充)",
value=config.get("xsec_token", ""),
interactive=True,
)
btn_refresh_token = gr.Button(
"🔄 刷新 Token", variant="secondary",
)
btn_load_my_data = gr.Button(
"📊 加载我的数据", variant="primary", size="lg",
)
data_status = gr.Markdown("")
with gr.Column(scale=2):
profile_card = gr.Markdown(
value="*等待加载...*",
label="账号概览",
)
gr.Markdown("---")
gr.Markdown("### 📈 数据可视化")
with gr.Row():
with gr.Column(scale=1):
chart_interact = gr.Plot(label="📊 核心指标")
with gr.Column(scale=2):
chart_notes = gr.Plot(label="❤ 笔记点赞排行")
gr.Markdown("---")
notes_detail = gr.Markdown(
value="*加载数据后显示笔记明细表格*",
label="笔记数据明细",
)
# -------- Tab 6: 智能学习 --------
with gr.Tab("🧠 智能学习"):
gr.Markdown(
"### 🧠 智能内容学习引擎\n"
"> 自动分析已发布笔记的表现,学习哪些内容受欢迎,用权重指导未来创作\n\n"
"**工作流程**: 采集数据 → 计算权重 → AI 深度分析 → 自动优化创作\n\n"
"💡 启用后,自动发布将优先生成高权重主题的内容"
)
with gr.Row():
# 左栏: 数据采集 & 权重计算
with gr.Column(scale=1):
gr.Markdown("#### 📊 数据采集")
learn_user_id = gr.Textbox(
label="用户 ID", value=config.get("my_user_id", ""),
interactive=True,
)
learn_xsec_token = gr.Textbox(
label="xsec_token", value=config.get("xsec_token", ""),
interactive=True,
)
btn_learn_collect = gr.Button(
"📊 采集笔记数据", variant="primary", size="lg",
)
learn_collect_status = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### ⚖️ 权重计算")
btn_learn_calc = gr.Button(
"⚖️ 计算内容权重", variant="primary", size="lg",
)
learn_calc_status = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 🤖 AI 深度分析")
gr.Markdown("> 用 LLM 分析笔记数据,找出内容规律,生成策略建议")
btn_learn_ai = gr.Button(
"🧠 AI 深度分析", variant="primary", size="lg",
)
gr.Markdown("---")
gr.Markdown("#### ⏰ 定时自动学习")
gr.Markdown("> 每隔 N 小时自动采集数据 + 计算权重 + AI 分析")
learn_interval = gr.Number(
label="学习间隔 (小时)", value=config.get("learn_interval", 6), minimum=1, maximum=48,
)
with gr.Row():
btn_learn_start = gr.Button(
"▶ 启动定时学习", variant="primary", size="sm",
)
btn_learn_stop = gr.Button(
"⏹ 停止", variant="stop", size="sm",
)
learn_sched_status = gr.Markdown("⚪ 定时学习未启动")
gr.Markdown("---")
gr.Markdown("#### 🎯 加权主题预览")
gr.Markdown("> 当前权重最高的主题 (自动发布会优先选择)")
btn_show_topics = gr.Button("🔄 刷新加权主题", size="sm")
learn_weighted_topics = gr.Textbox(
label="加权主题池 (权重从高到低)",
value=analytics.get_weighted_topics_display() or "暂无权重数据",
interactive=False,
lines=2,
)
learn_use_weights = gr.Checkbox(
label="🧠 自动发布时使用智能权重 (推荐)",
value=cfg.get("use_smart_weights", True),
interactive=True,
)
# 右栏: 分析报告
with gr.Column(scale=2):
gr.Markdown("#### 📋 智能学习报告")
learn_report = gr.Markdown(
value=analytics.generate_report(),
label="分析报告",
)
gr.Markdown("---")
learn_ai_report = gr.Markdown(
value="*点击「AI 深度分析」生成*",
label="AI 深度分析报告",
)
# -------- Tab 7: 自动运营 --------
with gr.Tab("🤖 自动运营"):
gr.Markdown(
"### 🤖 无人值守自动化运营\n"
"> 一键评论引流 + 一键点赞 + 一键收藏 + 一键回复 + 一键发布 + 随机定时全自动\n\n"
"⚠️ **注意**: 请确保已连接 LLM、SD WebUI 和 MCP 服务"
)
persona_pool_hint = gr.Markdown(
value=f"🎭 当前人设池: **{config.get('persona', '随机')[:20]}** → 关键词/主题池已匹配",
)
with gr.Row():
# 左栏: 一键操作
with gr.Column(scale=1):
gr.Markdown("#### 💬 一键智能评论")
gr.Markdown(
"> 自动搜索高赞笔记 → AI 分析内容 → 生成评论 → 发送\n"
"每次随机选关键词搜索,从结果中随机选笔记"
)
auto_comment_keywords = gr.Textbox(
label="评论关键词池 (逗号分隔,随人设自动切换)",
value=", ".join(get_persona_keywords(config.get("persona", ""))),
placeholder="关键词1, 关键词2, ... (切换人设自动更新)",
)
btn_auto_comment = gr.Button(
"💬 一键评论 (单次)", variant="primary", size="lg",
)
auto_comment_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 👍 一键自动点赞")
gr.Markdown(
"> 搜索笔记 → 随机选择多篇 → 依次点赞\n"
"提升账号活跃度,无需 LLM"
)
auto_like_count = gr.Number(
label="单次点赞数量", value=config.get("auto_like_count", 5), minimum=1, maximum=20,
)
btn_auto_like = gr.Button(
"👍 一键点赞 (单次)", variant="primary", size="lg",
)
auto_like_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### ⭐ 一键自动收藏")
gr.Markdown(
"> 搜索笔记 → 随机选择多篇 → 依次收藏\n"
"提升账号活跃度,与点赞互补"
)
auto_fav_count = gr.Number(
label="单次收藏数量", value=config.get("auto_fav_count", 3), minimum=1, maximum=15,
)
btn_auto_favorite = gr.Button(
"⭐ 一键收藏 (单次)", variant="primary", size="lg",
)
auto_favorite_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 💌 一键自动回复")
gr.Markdown(
"> 扫描我的所有笔记 → 找到粉丝评论 → AI 生成回复 → 逐条发送\n"
"自动跳过自己的评论,模拟真人间隔回复"
)
auto_reply_max = gr.Number(
label="单次最多回复条数", value=config.get("auto_reply_max", 5), minimum=1, maximum=20,
)
btn_auto_reply = gr.Button(
"💌 一键回复 (单次)", variant="primary", size="lg",
)
auto_reply_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 🚀 一键智能发布")
gr.Markdown(
"> 随机选主题+风格 → AI 生成文案 → SD 生成图片 → 自动发布"
)
auto_publish_topics = gr.Textbox(
label="主题池 (逗号分隔,随人设自动切换)",
value=", ".join(get_persona_topics(config.get("persona", ""))),
placeholder="主题会从池中随机选取,切换人设自动更新",
)
btn_auto_publish = gr.Button(
"🚀 一键发布 (单次)", variant="primary", size="lg",
)
auto_publish_result = gr.Markdown("")
# 右栏: 定时自动化 (2列卡片网格)
with gr.Column(scale=2):
gr.Markdown("#### ⏰ 随机定时自动化")
gr.Markdown(
"> 设置时间间隔后启动,系统将在随机时间自动执行 · 模拟真人操作节奏,降低被检测风险"
)
sched_status = gr.Markdown("⚪ **调度器未运行**")
# ── 第1行: 时段 + 评论 ──
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("##### ⏰ 运营时段")
with gr.Row():
sched_start_hour = gr.Number(
label="开始(整点)", value=config.get("sched_start_hour", 8), minimum=0, maximum=23,
)
sched_end_hour = gr.Number(
label="结束(整点)", value=config.get("sched_end_hour", 23), minimum=1, maximum=24,
)
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("##### 💬 自动评论")
sched_comment_on = gr.Checkbox(
label="启用", value=config.get("sched_comment_on", True),
)
with gr.Row():
sched_c_min = gr.Number(
label="最小间隔(分钟)", value=config.get("sched_c_min", 15), minimum=5,
)
sched_c_max = gr.Number(
label="最大间隔(分钟)", value=config.get("sched_c_max", 45), minimum=10,
)
# ── 第2行: 点赞 + 收藏 ──
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("##### 👍 自动点赞")
sched_like_on = gr.Checkbox(
label="启用", value=config.get("sched_like_on", True),
)
with gr.Row():
sched_l_min = gr.Number(
label="最小间隔(分钟)", value=config.get("sched_l_min", 10), minimum=3,
)
sched_l_max = gr.Number(
label="最大间隔(分钟)", value=config.get("sched_l_max", 30), minimum=5,
)
sched_like_count = gr.Number(
label="每轮点赞数", value=config.get("sched_like_count", 5), minimum=1, maximum=15,
)
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("##### ⭐ 自动收藏")
sched_fav_on = gr.Checkbox(
label="启用", value=config.get("sched_fav_on", True),
)
with gr.Row():
sched_fav_min = gr.Number(
label="最小间隔(分钟)", value=config.get("sched_fav_min", 12), minimum=3,
)
sched_fav_max = gr.Number(
label="最大间隔(分钟)", value=config.get("sched_fav_max", 35), minimum=5,
)
sched_fav_count = gr.Number(
label="每轮收藏数", value=config.get("sched_fav_count", 3), minimum=1, maximum=10,
)
# ── 第3行: 回复 + 发布 ──
with gr.Row():
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("##### 💌 自动回复")
sched_reply_on = gr.Checkbox(
label="启用", value=config.get("sched_reply_on", True),
)
with gr.Row():
sched_r_min = gr.Number(
label="最小间隔(分钟)", value=config.get("sched_r_min", 20), minimum=5,
)
sched_r_max = gr.Number(
label="最大间隔(分钟)", value=config.get("sched_r_max", 60), minimum=10,
)
sched_reply_max = gr.Number(
label="每轮最多回复", value=config.get("sched_reply_max", 3), minimum=1, maximum=10,
)
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("##### 🚀 自动发布")
sched_publish_on = gr.Checkbox(
label="启用", value=config.get("sched_publish_on", True),
)
with gr.Row():
sched_p_min = gr.Number(
label="最小间隔(分钟)", value=config.get("sched_p_min", 60), minimum=30,
)
sched_p_max = gr.Number(
label="最大间隔(分钟)", value=config.get("sched_p_max", 180), minimum=60,
)
with gr.Row():
btn_start_sched = gr.Button(
"▶️ 启动定时", variant="primary", size="lg",
)
btn_stop_sched = gr.Button(
"⏹️ 停止定时", variant="stop", size="lg",
)
sched_result = gr.Markdown("")
gr.Markdown("---")
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("#### 📋 运行日志")
with gr.Row():
btn_refresh_log = gr.Button("🔄 刷新日志", size="sm")
btn_clear_log = gr.Button("🗑️ 清空日志", size="sm", variant="stop")
btn_refresh_stats = gr.Button("📊 刷新统计", size="sm")
auto_log_display = gr.TextArea(
label="自动化运行日志",
value="📋 暂无日志\n\n💡 执行操作后日志将在此显示",
lines=15,
interactive=False,
)
with gr.Column(scale=1):
gr.Markdown("#### 📊 今日运营统计")
auto_stats_display = gr.Markdown(
value=_get_stats_summary(),
)
# -------- Tab 8: 🔐 账号登录 --------
with gr.Tab("🔐 账号登录"):
gr.Markdown(
"### 小红书账号登录\n"
"> 扫码登录后自动获取 xsec_token配合用户 ID 即可使用所有功能"
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown(
"**操作步骤:**\n"
"1. 确保 MCP 服务已启动\n"
"2. 点击「获取登录二维码」→ 用小红书 App 扫码\n"
"3. 点击「检查登录状态」→ 自动获取并保存 xsec_token\n"
"4. 首次使用请填写你的用户 ID 并点击保存\n\n"
"⚠️ 登录后不要在其他网页端登录同一账号,否则会被踢出"
)
btn_get_qrcode = gr.Button(
"📱 获取登录二维码", variant="primary", size="lg",
)
btn_check_login = gr.Button(
"🔍 检查登录状态 (自动获取 Token)",
variant="secondary", size="lg",
)
btn_logout = gr.Button(
"🚪 退出登录 (重新扫码)",
variant="stop", size="lg",
)
login_status = gr.Markdown("🔄 等待操作...")
gr.Markdown("---")
gr.Markdown(
"#### 📌 我的账号信息\n"
"> **注意**: 小红书号 ≠ 用户 ID\n"
"> - **小红书号 (redId)**: 如 `18688457507`,是你在 App 个人页看到的\n"
"> - **用户 ID (userId)**: 如 `5a695db6e8ac2b72e8af2a53`24位十六进制字符串\n\n"
"💡 **如何获取 userId?**\n"
"1. 用浏览器打开你的小红书主页\n"
"2. 网址格式为: `xiaohongshu.com/user/profile/xxxxxxxx`\n"
"3. `profile/` 后面的就是你的 userId"
)
login_user_id = gr.Textbox(
label="我的用户 ID (24位 userId, 非小红书号)",
value=config.get("my_user_id", ""),
placeholder="如: 5a695db6e8ac2b72e8af2a53",
)
login_xsec_token = gr.Textbox(
label="xsec_token (登录后自动获取)",
value=config.get("xsec_token", ""),
interactive=False,
)
btn_save_uid = gr.Button(
"💾 保存用户 ID", variant="secondary",
)
save_uid_status = gr.Markdown("")
with gr.Column(scale=1):
qr_image = gr.Image(
label="扫码登录", height=350, width=350,
)
# ==================================================
# 事件绑定
# ==================================================
# ---- 全局设置: LLM 提供商管理 ----
btn_connect_llm.click(
fn=connect_llm, inputs=[llm_provider],
outputs=[llm_model, status_bar],
)
llm_provider.change(
fn=on_provider_selected,
inputs=[llm_provider],
outputs=[llm_provider_info],
)
btn_add_provider.click(
fn=add_llm_provider,
inputs=[new_provider_name, new_provider_key, new_provider_url],
outputs=[llm_provider, provider_mgmt_status],
)
btn_del_provider.click(
fn=remove_llm_provider,
inputs=[llm_provider],
outputs=[llm_provider, provider_mgmt_status],
)
btn_connect_sd.click(
fn=connect_sd, inputs=[sd_url],
outputs=[sd_model, status_bar, sd_model_info],
)
sd_model.change(
fn=on_sd_model_change, inputs=[sd_model],
outputs=[sd_model_info],
)
btn_check_mcp.click(
fn=check_mcp_status, inputs=[mcp_url],
outputs=[status_bar],
)
# ---- 头像/换脸管理 ----
btn_save_face.click(
fn=upload_face_image,
inputs=[face_image_input],
outputs=[face_image_preview, face_status],
)
# ---- Tab 2: 热点探测 ----
btn_search.click(
fn=search_hotspots,
inputs=[hot_keyword, hot_sort, mcp_url],
outputs=[search_status, search_output],
)
# 搜索结果同步到 state
search_output.change(
fn=lambda x: x, inputs=[search_output], outputs=[state_search_result],
)
btn_analyze.click(
fn=analyze_and_suggest,
inputs=[llm_model, hot_keyword, search_output],
outputs=[analysis_status, analysis_output, topic_from_hot],
)
btn_gen_from_hot.click(
fn=generate_from_hotspot,
inputs=[llm_model, topic_from_hot, hot_style, search_output, sd_model, persona],
outputs=[hot_title, hot_content, hot_prompt, hot_tags, hot_gen_status],
)
# 同步热点文案到内容创作 Tab
btn_sync_to_create.click(
fn=lambda t, c, p, tg: (t, c, p, tg, "✅ 已同步到「内容创作」,可切换 Tab 继续绘图和发布"),
inputs=[hot_title, hot_content, hot_prompt, hot_tags],
outputs=[res_title, res_content, res_prompt, res_tags, status_bar],
)
# ---- Tab 3: 评论管家 ----
# == 子 Tab A: 主动评论引流 ==
btn_pro_fetch.click(
fn=fetch_proactive_notes,
inputs=[pro_keyword, mcp_url],
outputs=[pro_selector, pro_fetch_status],
)
pro_selector.change(
fn=on_proactive_note_selected,
inputs=[pro_selector],
outputs=[pro_feed_id, pro_xsec_token, pro_title],
)
btn_pro_load.click(
fn=load_note_for_comment,
inputs=[pro_feed_id, pro_xsec_token, mcp_url],
outputs=[pro_load_status, pro_content, pro_comments, pro_full_text],
)
btn_pro_ai.click(
fn=ai_generate_comment,
inputs=[llm_model, persona,
pro_title, pro_content, pro_comments],
outputs=[pro_comment_text, pro_ai_status],
)
btn_pro_send.click(
fn=send_comment,
inputs=[pro_feed_id, pro_xsec_token, pro_comment_text, mcp_url],
outputs=[pro_send_status],
)
# == 子 Tab B: 回复粉丝评论 ==
btn_my_fetch.click(
fn=fetch_my_notes,
inputs=[mcp_url],
outputs=[my_selector, my_fetch_status],
)
my_selector.change(
fn=on_my_note_selected,
inputs=[my_selector],
outputs=[my_feed_id, my_xsec_token, my_title],
)
btn_my_load_comments.click(
fn=fetch_my_note_comments,
inputs=[my_feed_id, my_xsec_token, mcp_url],
outputs=[my_comment_status, my_comments_display],
)
btn_my_ai_reply.click(
fn=ai_reply_comment,
inputs=[llm_model, persona,
my_title, my_target_comment],
outputs=[my_reply_content, my_reply_gen_status],
)
btn_my_send_reply.click(
fn=send_reply,
inputs=[my_feed_id, my_xsec_token, my_reply_content, mcp_url],
outputs=[my_reply_status],
)
# ---- Tab 4: 账号登录 ----
btn_get_qrcode.click(
fn=get_login_qrcode,
inputs=[mcp_url],
outputs=[qr_image, login_status],
)
btn_check_login.click(
fn=check_login,
inputs=[mcp_url],
outputs=[login_status, login_user_id, login_xsec_token],
)
btn_logout.click(
fn=logout_xhs,
inputs=[mcp_url],
outputs=[login_status],
)
btn_save_uid.click(
fn=save_my_user_id,
inputs=[login_user_id],
outputs=[save_uid_status],
)
# ---- Tab 5: 数据看板 ----
def refresh_xsec_token(mcp_url):
token = _auto_fetch_xsec_token(mcp_url)
if token:
cfg.set("xsec_token", token)
return gr.update(value=token), "✅ Token 已刷新"
return gr.update(value=cfg.get("xsec_token", "")), "❌ 刷新失败,请确认已登录"
btn_refresh_token.click(
fn=refresh_xsec_token,
inputs=[mcp_url],
outputs=[data_xsec_token, data_status],
)
btn_load_my_data.click(
fn=fetch_my_profile,
inputs=[data_user_id, data_xsec_token, mcp_url],
outputs=[data_status, profile_card, chart_interact, chart_notes, notes_detail],
)
# ---- Tab 6: 智能学习 ----
btn_learn_collect.click(
fn=analytics_collect_data,
inputs=[mcp_url, learn_user_id, learn_xsec_token],
outputs=[learn_collect_status],
)
btn_learn_calc.click(
fn=analytics_calculate_weights,
inputs=[],
outputs=[learn_calc_status, learn_report],
)
btn_learn_ai.click(
fn=analytics_llm_deep_analysis,
inputs=[llm_model],
outputs=[learn_ai_report],
)
btn_learn_start.click(
fn=start_learn_scheduler,
inputs=[mcp_url, learn_user_id, learn_xsec_token, llm_model, learn_interval],
outputs=[learn_sched_status],
)
btn_learn_stop.click(
fn=stop_learn_scheduler,
inputs=[],
outputs=[learn_sched_status],
)
btn_show_topics.click(
fn=analytics_get_weighted_topics,
inputs=[],
outputs=[learn_weighted_topics],
)
learn_use_weights.change(
fn=lambda v: cfg.set("use_smart_weights", v) or ("✅ 智能权重已启用" if v else "⚪ 智能权重已关闭"),
inputs=[learn_use_weights],
outputs=[learn_sched_status],
)
# ---- Tab 7: 自动运营 ----
# 人设切换 → 联动更新评论关键词池、主题池和队列主题池(同时保存到配置)
persona.change(
fn=on_persona_changed,
inputs=[persona],
outputs=[auto_comment_keywords, auto_publish_topics, persona_pool_hint, queue_gen_topics],
)
btn_auto_comment.click(
fn=_auto_comment_with_log,
inputs=[auto_comment_keywords, mcp_url, llm_model, persona],
outputs=[auto_comment_result, auto_log_display],
)
btn_auto_like.click(
fn=_auto_like_with_log,
inputs=[auto_comment_keywords, auto_like_count, mcp_url],
outputs=[auto_like_result, auto_log_display],
)
btn_auto_favorite.click(
fn=_auto_favorite_with_log,
inputs=[auto_comment_keywords, auto_fav_count, mcp_url],
outputs=[auto_favorite_result, auto_log_display],
)
btn_auto_reply.click(
fn=_auto_reply_with_log,
inputs=[auto_reply_max, mcp_url, llm_model, persona],
outputs=[auto_reply_result, auto_log_display],
)
btn_auto_publish.click(
fn=_auto_publish_with_log,
inputs=[auto_publish_topics, mcp_url, sd_url, sd_model, llm_model, persona, quality_mode, face_swap_toggle],
outputs=[auto_publish_result, auto_log_display],
)
btn_start_sched.click(
fn=start_scheduler,
inputs=[sched_comment_on, sched_publish_on, sched_reply_on, sched_like_on,
sched_fav_on,
sched_c_min, sched_c_max, sched_p_min, sched_p_max,
sched_r_min, sched_r_max, sched_reply_max,
sched_l_min, sched_l_max, sched_like_count,
sched_fav_min, sched_fav_max, sched_fav_count,
sched_start_hour, sched_end_hour,
auto_comment_keywords, auto_publish_topics,
mcp_url, sd_url, sd_model, llm_model, persona,
quality_mode, face_swap_toggle],
outputs=[sched_result],
)
btn_stop_sched.click(
fn=stop_scheduler,
inputs=[],
outputs=[sched_result],
)
btn_refresh_log.click(
fn=lambda: (get_auto_log(), get_scheduler_status()),
inputs=[],
outputs=[auto_log_display, sched_status],
)
btn_clear_log.click(
fn=lambda: (_auto_log.clear() or "📋 日志已清空"),
inputs=[],
outputs=[auto_log_display],
)
btn_refresh_stats.click(
fn=lambda: (get_scheduler_status(), _get_stats_summary()),
inputs=[],
outputs=[sched_status, auto_stats_display],
)
# ---- 全局设置参数自动保存 ----
# persona的保存已整合到on_persona_changed函数中
mcp_url.change(fn=lambda v: cfg.set("mcp_url", v), inputs=[mcp_url], outputs=[])
sd_url.change(fn=lambda v: cfg.set("sd_url", v), inputs=[sd_url], outputs=[])
llm_model.change(fn=lambda v: cfg.set("model", v), inputs=[llm_model], outputs=[])
# ---- 自动运营参数自动保存 ----
sched_comment_on.change(fn=lambda v: cfg.set("sched_comment_on", v), inputs=[sched_comment_on], outputs=[])
sched_like_on.change(fn=lambda v: cfg.set("sched_like_on", v), inputs=[sched_like_on], outputs=[])
sched_fav_on.change(fn=lambda v: cfg.set("sched_fav_on", v), inputs=[sched_fav_on], outputs=[])
sched_reply_on.change(fn=lambda v: cfg.set("sched_reply_on", v), inputs=[sched_reply_on], outputs=[])
sched_publish_on.change(fn=lambda v: cfg.set("sched_publish_on", v), inputs=[sched_publish_on], outputs=[])
sched_c_min.change(fn=lambda v: cfg.set("sched_c_min", v), inputs=[sched_c_min], outputs=[])
sched_c_max.change(fn=lambda v: cfg.set("sched_c_max", v), inputs=[sched_c_max], outputs=[])
sched_l_min.change(fn=lambda v: cfg.set("sched_l_min", v), inputs=[sched_l_min], outputs=[])
sched_l_max.change(fn=lambda v: cfg.set("sched_l_max", v), inputs=[sched_l_max], outputs=[])
sched_like_count.change(fn=lambda v: cfg.set("sched_like_count", v), inputs=[sched_like_count], outputs=[])
sched_fav_min.change(fn=lambda v: cfg.set("sched_fav_min", v), inputs=[sched_fav_min], outputs=[])
sched_fav_max.change(fn=lambda v: cfg.set("sched_fav_max", v), inputs=[sched_fav_max], outputs=[])
sched_fav_count.change(fn=lambda v: cfg.set("sched_fav_count", v), inputs=[sched_fav_count], outputs=[])
sched_r_min.change(fn=lambda v: cfg.set("sched_r_min", v), inputs=[sched_r_min], outputs=[])
sched_r_max.change(fn=lambda v: cfg.set("sched_r_max", v), inputs=[sched_r_max], outputs=[])
sched_reply_max.change(fn=lambda v: cfg.set("sched_reply_max", v), inputs=[sched_reply_max], outputs=[])
sched_p_min.change(fn=lambda v: cfg.set("sched_p_min", v), inputs=[sched_p_min], outputs=[])
sched_p_max.change(fn=lambda v: cfg.set("sched_p_max", v), inputs=[sched_p_max], outputs=[])
sched_start_hour.change(fn=lambda v: cfg.set("sched_start_hour", v), inputs=[sched_start_hour], outputs=[])
sched_end_hour.change(fn=lambda v: cfg.set("sched_end_hour", v), inputs=[sched_end_hour], outputs=[])
auto_like_count.change(fn=lambda v: cfg.set("auto_like_count", v), inputs=[auto_like_count], outputs=[])
auto_fav_count.change(fn=lambda v: cfg.set("auto_fav_count", v), inputs=[auto_fav_count], outputs=[])
auto_reply_max.change(fn=lambda v: cfg.set("auto_reply_max", v), inputs=[auto_reply_max], outputs=[])
# ---- 智能学习参数自动保存 ----
learn_interval.change(fn=lambda v: cfg.set("learn_interval", v), inputs=[learn_interval], outputs=[])
# ---- 内容排期参数自动保存 ----
queue_gen_count.change(fn=lambda v: cfg.set("queue_gen_count", v), inputs=[queue_gen_count], outputs=[])
# ---- 开机自启 ----
autostart_toggle.change(
fn=toggle_autostart,
inputs=[autostart_toggle],
outputs=[autostart_status],
)
# ---- Tab 8: 内容排期 ----
# 队列主题池的更新已整合到 Tab 7 的 persona.change() 事件中
# 批量生成到队列
btn_queue_generate.click(
fn=queue_generate_and_refresh,
inputs=[queue_gen_topics, sd_url, sd_model, llm_model,
persona, quality_mode, face_swap_toggle,
queue_gen_count, queue_gen_schedule],
outputs=[queue_gen_result, queue_table, queue_calendar, queue_processor_status],
)
# 刷新队列
btn_queue_refresh.click(
fn=lambda sf: (queue_refresh_table(sf), queue_refresh_calendar(), queue_get_status()),
inputs=[queue_filter],
outputs=[queue_table, queue_calendar, queue_processor_status],
)
queue_filter.change(
fn=lambda sf: queue_refresh_table(sf),
inputs=[queue_filter],
outputs=[queue_table],
)
# 单项操作
btn_queue_preview.click(
fn=queue_preview_item,
inputs=[queue_item_id],
outputs=[queue_preview_display],
)
btn_queue_approve.click(
fn=lambda iid, st: (queue_approve_item(iid, st), queue_format_table(), queue_format_calendar()),
inputs=[queue_item_id, queue_schedule_time],
outputs=[queue_op_result, queue_table, queue_calendar],
)
btn_queue_reject.click(
fn=lambda iid: (queue_reject_item(iid), queue_format_table()),
inputs=[queue_item_id],
outputs=[queue_op_result, queue_table],
)
btn_queue_delete.click(
fn=lambda iid: (queue_delete_item(iid), queue_format_table(), queue_format_calendar()),
inputs=[queue_item_id],
outputs=[queue_op_result, queue_table, queue_calendar],
)
btn_queue_retry.click(
fn=lambda iid: (queue_retry_item(iid), queue_format_table()),
inputs=[queue_item_id],
outputs=[queue_op_result, queue_table],
)
btn_queue_publish_now.click(
fn=lambda iid: (queue_publish_now(iid), queue_format_table(), queue_format_calendar(), queue_get_status()),
inputs=[queue_item_id],
outputs=[queue_op_result, queue_table, queue_calendar, queue_processor_status],
)
btn_queue_batch_approve.click(
fn=lambda sf: (queue_batch_approve(sf), queue_format_table(), queue_format_calendar()),
inputs=[queue_filter],
outputs=[queue_op_result, queue_table, queue_calendar],
)
# 队列处理器
btn_queue_start.click(
fn=lambda: (queue_start_processor(), queue_get_status()),
inputs=[],
outputs=[queue_processor_result, queue_processor_status],
)
btn_queue_stop.click(
fn=lambda: (queue_stop_processor(), queue_get_status()),
inputs=[],
outputs=[queue_processor_result, queue_processor_status],
)
# ---- 启动时自动加载全局设置 ----
def load_global_settings():
"""页面加载时恢复全局设置"""
config = cfg.all
providers = cfg.get_llm_provider_names()
active_llm = cfg.get("active_llm", "")
persona_val = config.get("persona", RANDOM_PERSONA_LABEL)
# 初始化LLM提供商信息显示
provider_info = on_provider_selected(active_llm) if active_llm else "*选择提供商后显示详情*"
# 获取人设对应的关键词和主题池
keywords = get_persona_keywords(persona_val)
topics = get_persona_topics(persona_val)
keywords_str = ", ".join(keywords)
topics_str = ", ".join(topics)
# 尝试连接 SD 并获取模型列表
sd_models = []
sd_status = "🔄 等待连接..."
sd_model_val = None
try:
svc = SDService(config["sd_url"])
ok, msg = svc.check_connection()
if ok:
sd_models = svc.get_models()
sd_model_val = sd_models[0] if sd_models else None
sd_status = f"{msg}"
except Exception:
pass
# 图片生成参数
quality_mode_val = config.get("quality_mode", "标准 (约1分钟)")
steps_val = config.get("sd_steps", 20)
cfg_scale_val = config.get("sd_cfg_scale", 5.5)
neg_prompt_val = config.get("sd_negative_prompt", DEFAULT_NEGATIVE)
# 自动运营参数
return (
gr.update(choices=providers, value=active_llm), # llm_provider
provider_info, # llm_provider_info
config["mcp_url"], # mcp_url
config["sd_url"], # sd_url
persona_val, # persona
gr.update(choices=sd_models, value=sd_model_val), # sd_model
sd_status, # status_bar
keywords_str, # auto_comment_keywords
topics_str, # auto_publish_topics
topics_str, # queue_gen_topics
quality_mode_val, # quality_mode
steps_val, # steps
cfg_scale_val, # cfg_scale
neg_prompt_val, # neg_prompt
config.get("auto_like_count", 5), # auto_like_count
config.get("auto_fav_count", 3), # auto_fav_count
config.get("auto_reply_max", 5), # auto_reply_max
config.get("sched_comment_on", True), # sched_comment_on
config.get("sched_like_on", True), # sched_like_on
config.get("sched_fav_on", True), # sched_fav_on
config.get("sched_reply_on", True), # sched_reply_on
config.get("sched_publish_on", True), # sched_publish_on
config.get("sched_c_min", 15), # sched_c_min
config.get("sched_c_max", 45), # sched_c_max
config.get("sched_l_min", 10), # sched_l_min
config.get("sched_l_max", 30), # sched_l_max
config.get("sched_like_count", 5), # sched_like_count
config.get("sched_fav_min", 12), # sched_fav_min
config.get("sched_fav_max", 35), # sched_fav_max
config.get("sched_fav_count", 3), # sched_fav_count
config.get("sched_r_min", 20), # sched_r_min
config.get("sched_r_max", 60), # sched_r_max
config.get("sched_reply_max", 3), # sched_reply_max
config.get("sched_p_min", 60), # sched_p_min
config.get("sched_p_max", 180), # sched_p_max
config.get("sched_start_hour", 8), # sched_start_hour
config.get("sched_end_hour", 23), # sched_end_hour
config.get("learn_interval", 6), # learn_interval
config.get("queue_gen_count", 3), # queue_gen_count
config.get("use_smart_weights", True), # learn_use_weights
)
app.load(
fn=load_global_settings,
inputs=[],
outputs=[
llm_provider, llm_provider_info, mcp_url, sd_url, persona, sd_model, status_bar,
auto_comment_keywords, auto_publish_topics, queue_gen_topics,
quality_mode, steps, cfg_scale, neg_prompt,
auto_like_count, auto_fav_count, auto_reply_max,
sched_comment_on, sched_like_on, sched_fav_on, sched_reply_on, sched_publish_on,
sched_c_min, sched_c_max, sched_l_min, sched_l_max, sched_like_count,
sched_fav_min, sched_fav_max, sched_fav_count,
sched_r_min, sched_r_max, sched_reply_max,
sched_p_min, sched_p_max, sched_start_hour, sched_end_hour,
learn_interval, queue_gen_count, learn_use_weights,
],
)
# ==================================================
return app