- 新增智能选题引擎 `TopicEngine`,整合热点数据与历史权重,提供多维度评分和创作角度建议 - 新增内容模板系统 `ContentTemplate`,支持从 JSON 文件加载模板并应用于文案生成 - 新增批量创作功能 `batch_generate_copy`,支持串行生成多篇文案并自动入草稿队列 - 升级文案质量流水线:实现 Prompt 分层架构(基础层 + 风格层 + 人设层)、LLM 自检与改写机制、深度去 AI 化后处理 - 优化图文协同:新增封面图策略选择、SD prompt 与文案语义联动、图文匹配度评估 - 集成数据闭环:在文案生成中自动注入 `AnalyticsService` 权重数据,实现发布 → 数据回收 → 优化创作的完整循环 - 更新 UI 组件:新增选题推荐展示区、批量创作折叠面板、封面图策略选择器和图文匹配度评分展示 ♻️ refactor(llm): 重构 Prompt 架构并增强去 AI 化处理 - 将 `PROMPT_COPYWRITING` 拆分为分层架构(基础层 + 风格层 + 人设层),提高维护性和灵活性 - 增强 `_humanize_content` 方法:新增语气词注入、标点不规范化、段落节奏打散和 emoji 密度控制 - 新增 `_self_check` 和 `_self_check_rewrite` 方法,实现文案 AI 痕迹自检与自动改写 - 新增 `evaluate_image_text_match` 方法,支持文案与 SD prompt 的语义匹配度评估(可选,失败不阻塞) - 新增封面图策略配置 `COVER_STRATEGIES` 和情感基调映射 `EMOTION_SD_MAP` 📝 docs(openspec): 归档内容创作优化提案和详细规格 - 新增 `openspec/changes/archive/2026-02-28-optimize-content-creation/` 目录,包含设计文档、提案、规格说明和任务清单 - 新增 `openspec/specs/` 下的批量创作、文案质量流水线、图文协同、服务内容和智能选题引擎规格文档 - 更新 `openspec/specs/services-content/spec.md`,反映新增的批量创作和智能选题入口函数 🔧 chore(config): 更新服务配置和 UI 集成 - 在 `services/content.py` 中集成权重数据自动注入逻辑,实现数据驱动创作 - 在 `ui/app.py` 中新增选题推荐、批量生成和图文匹配度评估的回调函数 - 在 `ui/tab_create.py` 中新增智能选题推荐区、批量创作面板和图文匹配度评估组件 - 修复 `services/sd_service.py` 中的头像文件路径问题,确保目录存在
1450 lines
69 KiB
Python
1450 lines
69 KiB
Python
"""
|
||
ui/app.py
|
||
Gradio 应用界面构建函数 — 包含全部 Tab UI 组件和事件绑定
|
||
"""
|
||
import os
|
||
import platform
|
||
import logging
|
||
|
||
import gradio as gr
|
||
|
||
from services.config_manager import ConfigManager
|
||
from services.sd_service import SDService, DEFAULT_NEGATIVE, FACE_IMAGE_PATH, SD_PRESET_NAMES, get_sd_preset, SD_MODEL_PROFILES
|
||
from services.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, batch_generate_copy
|
||
from services.publish_queue import PublishQueue, STATUS_LABELS
|
||
from services.topic_engine import TopicEngine
|
||
|
||
logger = logging.getLogger("autobot")
|
||
|
||
|
||
# ========== 新增回调: 选题推荐 / 批量创作 / 图文匹配 ==========
|
||
|
||
def _fn_topic_recommend(model_name):
|
||
"""获取智能选题推荐列表"""
|
||
analytics = AnalyticsService()
|
||
engine = TopicEngine(analytics)
|
||
return engine.recommend_topics(count=5)
|
||
|
||
|
||
def _fn_batch_generate(model_name, topics, style, sd_model_name, persona_text, template_name):
|
||
"""批量生成文案并入草稿队列"""
|
||
pq = PublishQueue()
|
||
return batch_generate_copy(
|
||
model=model_name,
|
||
topics=topics,
|
||
style=style,
|
||
sd_model_name=sd_model_name,
|
||
persona_text=persona_text,
|
||
template_name=template_name,
|
||
publish_queue=pq,
|
||
)
|
||
|
||
|
||
def _fn_evaluate_match(model_name, content, sd_prompt):
|
||
"""评估图文匹配度"""
|
||
from services.llm_service import LLMService
|
||
from services.connection import _get_llm_config
|
||
api_key, base_url, _ = _get_llm_config()
|
||
if not api_key:
|
||
return {"match_score": -1, "suggestions": [], "skipped": True}
|
||
svc = LLMService(api_key, base_url, model_name)
|
||
return svc.evaluate_image_text_match(content, sd_prompt)
|
||
|
||
_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,
|
||
fn_batch_generate=_fn_batch_generate,
|
||
fn_topic_recommend=_fn_topic_recommend,
|
||
fn_evaluate_match=_fn_evaluate_match,
|
||
)
|
||
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
|