feat(automation): 新增无人值守自动化运营模块

- 新增完整的自动化运营模块,包含一键评论、一键发布和定时调度功能
- 【一键评论】自动搜索高赞笔记,AI分析内容并生成个性化评论进行引流
- 【一键发布】随机选择主题和风格,AI生成文案,SD生成图片并自动发布到小红书
- 【定时调度】支持随机时间间隔的自动评论和发布,模拟真人操作节奏降低风险
- 新增自动化日志系统,实时记录操作状态和结果
- 在UI中新增“自动运营”标签页,提供完整的配置和操作界面

📝 docs(prompts): 优化SD提示词生成模板

- 更新文案生成提示词,适配JuggernautXL模型并优化质量描述
- 新增详细的质量词、光影、风格、构图和细节要求
- 移除括号权重语法,改为英文逗号分隔的描述方式
- 优化反向提示词,针对SDXL模型进行适配和增强

🔧 chore(config): 更新安全令牌配置

- 更新config.json中的xsec_token为新的安全令牌值

️ perf(sd): 优化Stable Diffusion服务参数

- 增加SD服务超时时间至900秒,适应高质量图片生成
- 优化文生图和图生图的默认参数,适配JuggernautXL模型
- 新增采样器和调度器参数配置,提升图片生成质量
- 优化默认反向提示词,针对SDXL模型进行专门优化
This commit is contained in:
zhoujie 2026-02-08 22:23:25 +08:00
parent 88dfc09e2a
commit d27ffe94f4
4 changed files with 471 additions and 17 deletions

View File

@ -21,5 +21,5 @@
"base_url": "https://wolfai.top/v1"
}
],
"xsec_token": "ABS1TagQqhCpZmeNlq0VoCfNEyI6Q83GJzjTGJvzEAq5I="
"xsec_token": "ABdAEbqP9ScgelmyolJxsnpCr_e645SCpnub2dLZJc4Ck="
}

View File

@ -26,7 +26,13 @@ PROMPT_COPYWRITING = """
3. 结尾必须有 5 个以上相关话题标签(#)。
绘图 Prompt
生成对应的 Stable Diffusion 英文提示词强调masterpiece, best quality, 8k, soft lighting, ins style
生成对应的 Stable Diffusion 英文提示词适配 JuggernautXL 模型强调
- 质量词masterpiece, best quality, ultra detailed, 8k uhd, high resolution
- 光影natural lighting, soft shadows, studio lighting, golden hour 根据场景选择
- 风格photorealistic, cinematic, editorial photography, ins style
- 构图dynamic angle, depth of field, bokeh
- 细节detailed skin texture, sharp focus, vivid colors
注意不要使用括号权重语法直接用英文逗号分隔描述
返回 JSON 格式
{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}
@ -105,7 +111,11 @@ PROMPT_COPY_WITH_REFERENCE = """
3. 结尾有 5 个以上话题标签(#)。
绘图 Prompt
生成 Stable Diffusion 英文提示词
生成 Stable Diffusion 英文提示词适配 JuggernautXL 模型
- 必含质量词masterpiece, best quality, ultra detailed, 8k uhd
- 风格photorealistic, cinematic, editorial photography
- 光影和细节natural lighting, sharp focus, vivid colors, detailed skin texture
- 用英文逗号分隔不用括号权重语法
返回 JSON 格式
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}

433
main.py
View File

@ -10,6 +10,9 @@ import time
import logging
import platform
import subprocess
import threading
import random
from datetime import datetime
from PIL import Image
import matplotlib
import matplotlib.pyplot as plt
@ -881,6 +884,306 @@ def fetch_my_profile(user_id, xsec_token, mcp_url):
return f"{e}", "", None, None, None
# ==================================================
# 自动化运营模块
# ==================================================
# 自动化状态
_auto_running = threading.Event()
_auto_thread: threading.Thread | None = None
_auto_log: list[str] = []
DEFAULT_TOPICS = [
"春季穿搭", "通勤穿搭", "约会穿搭", "显瘦穿搭", "平价好物",
"护肤心得", "妆容教程", "好物分享", "生活好物", "减脂餐分享",
"居家好物", "收纳技巧", "咖啡探店", "书单推荐", "旅行攻略",
]
DEFAULT_STYLES = ["好物种草", "干货教程", "情绪共鸣", "生活Vlog", "测评避雷"]
DEFAULT_COMMENT_KEYWORDS = [
"穿搭", "美食", "护肤", "好物推荐", "旅行", "生活日常", "减脂",
]
def _auto_log_append(msg: str):
"""记录自动化日志"""
ts = datetime.now().strftime("%H:%M:%S")
entry = f"[{ts}] {msg}"
_auto_log.append(entry)
if len(_auto_log) > 500:
_auto_log[:] = _auto_log[-300:]
logger.info("[自动化] %s", msg)
def _auto_comment_with_log(keywords_str, mcp_url, model, persona_text):
"""一键评论 + 同步刷新日志"""
msg = auto_comment_once(keywords_str, mcp_url, model, persona_text)
return msg, get_auto_log()
def auto_comment_once(keywords_str, mcp_url, model, persona_text):
"""一键评论:自动搜索高赞笔记 → AI生成评论 → 发送"""
try:
keywords = [k.strip() for k in keywords_str.split(",") if k.strip()] if keywords_str else DEFAULT_COMMENT_KEYWORDS
keyword = random.choice(keywords)
_auto_log_append(f"🔍 搜索关键词: {keyword}")
client = get_mcp_client(mcp_url)
# 搜索高赞笔记
entries = client.search_feeds_parsed(keyword, sort_by="最多点赞")
if not entries:
_auto_log_append("⚠️ 搜索无结果,尝试推荐列表")
entries = client.list_feeds_parsed()
if not entries:
return "❌ 未找到任何笔记"
# 过滤掉自己的笔记
my_uid = cfg.get("my_user_id", "")
if my_uid:
filtered = [e for e in entries if e.get("user_id") != my_uid]
if filtered:
entries = filtered
# 从前10个中随机选择
target = random.choice(entries[:min(10, len(entries))])
feed_id = target["feed_id"]
xsec_token = target["xsec_token"]
title = target.get("title", "未知")
_auto_log_append(f"🎯 选中: {title[:30]} (@{target.get('author', '未知')})")
if not feed_id or not xsec_token:
return "❌ 笔记缺少必要参数 (feed_id/xsec_token)"
# 模拟浏览延迟
time.sleep(random.uniform(2, 5))
# 加载笔记详情
result = client.get_feed_detail(feed_id, xsec_token, load_all_comments=True)
if "error" in result:
return f"❌ 加载笔记失败: {result['error']}"
full_text = result.get("text", "")
if "评论" in full_text:
parts = full_text.split("评论", 1)
content_part = parts[0].strip()[:600]
comments_part = ("评论" + parts[1])[:800] if len(parts) > 1 else ""
else:
content_part = full_text[:500]
comments_part = ""
# AI 生成评论
api_key, base_url, _ = _get_llm_config()
if not api_key:
return "❌ LLM 未配置,请先在全局设置中配置提供商"
svc = LLMService(api_key, base_url, model)
comment = svc.generate_proactive_comment(
persona_text, title, content_part, comments_part
)
_auto_log_append(f"💬 生成评论: {comment[:60]}...")
# 随机等待后发送
time.sleep(random.uniform(3, 8))
result = client.post_comment(feed_id, xsec_token, comment)
resp_text = result.get("text", "")
_auto_log_append(f"📡 MCP 响应: {resp_text[:200]}")
if "error" in result:
_auto_log_append(f"❌ 评论发送失败: {result['error']}")
return f"❌ 评论发送失败: {result['error']}"
# 检查是否真正成功
if "成功" not in resp_text and "success" not in resp_text.lower() and not resp_text:
_auto_log_append(f"⚠️ 评论可能未成功MCP 原始响应: {result}")
return f"⚠️ 评论状态不确定,请手动检查\nMCP 响应: {resp_text[:300]}\n📝 评论: {comment}"
_auto_log_append(f"✅ 评论已发送到「{title[:20]}")
return f"✅ 已评论「{title[:25]}\n📝 评论: {comment}\n\n💡 小红书可能有内容审核延迟,请稍等 1-2 分钟后查看"
except Exception as e:
_auto_log_append(f"❌ 一键评论异常: {e}")
return f"❌ 评论失败: {e}"
def _auto_publish_with_log(topics_str, mcp_url, sd_url_val, sd_model_name, model):
"""一键发布 + 同步刷新日志"""
msg = auto_publish_once(topics_str, mcp_url, sd_url_val, sd_model_name, model)
return msg, get_auto_log()
def auto_publish_once(topics_str, mcp_url, sd_url_val, sd_model_name, model):
"""一键发布:自动生成文案 → 生成图片 → 发布到小红书"""
try:
topics = [t.strip() for t in topics_str.split(",") if t.strip()] if topics_str else DEFAULT_TOPICS
topic = random.choice(topics)
style = random.choice(DEFAULT_STYLES)
_auto_log_append(f"📝 主题: {topic} | 风格: {style}")
# 生成文案
api_key, base_url, _ = _get_llm_config()
if not api_key:
return "❌ LLM 未配置,请先在全局设置中配置提供商"
svc = LLMService(api_key, base_url, model)
data = svc.generate_copy(topic, style)
title = (data.get("title", "") or "")[:20]
content = data.get("content", "")
sd_prompt = data.get("sd_prompt", "")
tags = data.get("tags", [])
if not title:
return "❌ 文案生成失败:无标题"
_auto_log_append(f"📄 文案: {title}")
# 生成图片
if not sd_url_val or not sd_model_name:
return "❌ SD WebUI 未连接或未选择模型,请先在全局设置中连接"
sd_svc = SDService(sd_url_val)
images = sd_svc.txt2img(prompt=sd_prompt, model=sd_model_name)
if not images:
return "❌ 图片生成失败:没有返回图片"
_auto_log_append(f"🎨 已生成 {len(images)} 张图片")
# 保存图片到临时目录
temp_dir = os.path.join(OUTPUT_DIR, "_temp_publish")
os.makedirs(temp_dir, exist_ok=True)
image_paths = []
ts = int(time.time())
for idx, img in enumerate(images):
if isinstance(img, Image.Image):
path = os.path.abspath(os.path.join(temp_dir, f"auto_{ts}_{idx}.png"))
img.save(path)
image_paths.append(path)
if not image_paths:
return "❌ 图片保存失败"
# 发布到小红书
client = get_mcp_client(mcp_url)
result = client.publish_content(
title=title, content=content, images=image_paths, tags=tags
)
if "error" in result:
_auto_log_append(f"❌ 发布失败: {result['error']}")
return f"❌ 发布失败: {result['error']}"
_auto_log_append(f"🚀 发布成功: {title}")
return f"✅ 发布成功!\n📌 标题: {title}\n{result.get('text', '')}"
except Exception as e:
_auto_log_append(f"❌ 一键发布异常: {e}")
return f"❌ 发布失败: {e}"
def _scheduler_loop(comment_enabled, publish_enabled,
comment_min, comment_max, publish_min, publish_max,
keywords, topics, mcp_url, sd_url_val, sd_model_name,
model, persona_text):
"""后台定时调度循环"""
_auto_log_append("🤖 自动化调度器已启动")
# 首次执行的随机延迟
next_comment = time.time() + random.randint(10, 60)
next_publish = time.time() + random.randint(30, 120)
while _auto_running.is_set():
now = time.time()
# 自动评论
if comment_enabled and now >= next_comment:
try:
_auto_log_append("--- 🔄 执行自动评论 ---")
msg = auto_comment_once(keywords, mcp_url, model, persona_text)
_auto_log_append(msg)
except Exception as e:
_auto_log_append(f"❌ 自动评论异常: {e}")
interval = random.randint(int(comment_min) * 60, int(comment_max) * 60)
next_comment = time.time() + interval
_auto_log_append(f"⏰ 下次评论: {interval // 60} 分钟后")
# 自动发布
if publish_enabled and now >= next_publish:
try:
_auto_log_append("--- 🔄 执行自动发布 ---")
msg = auto_publish_once(topics, mcp_url, sd_url_val, sd_model_name, model)
_auto_log_append(msg)
except Exception as e:
_auto_log_append(f"❌ 自动发布异常: {e}")
interval = random.randint(int(publish_min) * 60, int(publish_max) * 60)
next_publish = time.time() + interval
_auto_log_append(f"⏰ 下次发布: {interval // 60} 分钟后")
# 每5秒检查一次停止信号
for _ in range(5):
if not _auto_running.is_set():
break
time.sleep(1)
_auto_log_append("🛑 自动化调度器已停止")
def start_scheduler(comment_on, publish_on, c_min, c_max, p_min, p_max,
keywords, topics, mcp_url, sd_url_val, sd_model_name,
model, persona_text):
"""启动定时自动化"""
global _auto_thread
if _auto_running.is_set():
return "⚠️ 调度器已在运行中,请先停止"
if not comment_on and not publish_on:
return "❌ 请至少启用一项自动化功能(评论或发布)"
api_key, _, _ = _get_llm_config()
if not api_key:
return "❌ LLM 未配置,请先在全局设置中配置提供商"
_auto_running.set()
_auto_thread = threading.Thread(
target=_scheduler_loop,
args=(comment_on, publish_on,
c_min, c_max, p_min, p_max,
keywords, topics, mcp_url, sd_url_val, sd_model_name,
model, persona_text),
daemon=True,
)
_auto_thread.start()
parts = []
if comment_on:
parts.append(f"评论 (每 {int(c_min)}-{int(c_max)} 分钟)")
if publish_on:
parts.append(f"发布 (每 {int(p_min)}-{int(p_max)} 分钟)")
_auto_log_append(f"调度器已启动: {' + '.join(parts)}")
return f"✅ 自动化已启动 🟢\n任务: {' | '.join(parts)}\n\n💡 点击「刷新日志」查看实时进度"
def stop_scheduler():
"""停止定时自动化"""
if not _auto_running.is_set():
return "⚠️ 调度器未在运行"
_auto_running.clear()
_auto_log_append("⏹️ 收到停止信号,等待当前任务完成...")
return "🛑 调度器停止中...当前任务完成后将完全停止"
def get_auto_log():
"""获取自动化运行日志"""
if not _auto_log:
return "📋 暂无日志\n\n💡 点击「一键评论」「一键发布」或启动定时后日志将在此显示"
return "\n".join(_auto_log[-80:])
def get_scheduler_status():
"""获取调度器运行状态"""
if _auto_running.is_set():
return "🟢 **调度器运行中**"
return "⚪ **调度器未运行**"
# ==================================================
# UI 构建
# ==================================================
@ -1294,6 +1597,101 @@ with gr.Blocks(
label="笔记数据明细",
)
# -------- Tab 6: 自动运营 --------
with gr.Tab("🤖 自动运营"):
gr.Markdown(
"### 🤖 无人值守自动化运营\n"
"> 一键评论引流 + 一键内容发布 + 随机定时全自动\n\n"
"⚠️ **注意**: 请确保已连接 LLM、SD WebUI 和 MCP 服务"
)
with gr.Row():
# 左栏: 一键操作
with gr.Column(scale=1):
gr.Markdown("#### 💬 一键智能评论")
gr.Markdown(
"> 自动搜索高赞笔记 → AI 分析内容 → 生成评论 → 发送\n"
"每次随机选关键词搜索,从结果中随机选笔记"
)
auto_comment_keywords = gr.Textbox(
label="评论关键词池 (逗号分隔)",
value="穿搭, 美食, 护肤, 好物推荐, 旅行, 生活日常",
placeholder="关键词1, 关键词2, ...",
)
btn_auto_comment = gr.Button(
"💬 一键评论 (单次)", variant="primary", size="lg",
)
auto_comment_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("#### 🚀 一键智能发布")
gr.Markdown(
"> 随机选主题+风格 → AI 生成文案 → SD 生成图片 → 自动发布"
)
auto_publish_topics = gr.Textbox(
label="主题池 (逗号分隔)",
value="春季穿搭, 通勤穿搭, 显瘦穿搭, 平价好物, 护肤心得, 好物分享",
placeholder="主题1, 主题2, ...",
)
btn_auto_publish = gr.Button(
"🚀 一键发布 (单次)", variant="primary", size="lg",
)
auto_publish_result = gr.Markdown("")
# 右栏: 定时自动化
with gr.Column(scale=1):
gr.Markdown("#### ⏰ 随机定时自动化")
gr.Markdown(
"> 设置时间间隔后启动,系统将在随机时间自动执行\n"
"> 模拟真人操作节奏,降低被检测风险"
)
sched_status = gr.Markdown("⚪ **调度器未运行**")
with gr.Group():
sched_comment_on = gr.Checkbox(
label="✅ 启用自动评论", value=True,
)
with gr.Row():
sched_c_min = gr.Number(
label="评论最小间隔(分钟)", value=15, minimum=5,
)
sched_c_max = gr.Number(
label="评论最大间隔(分钟)", value=45, minimum=10,
)
with gr.Group():
sched_publish_on = gr.Checkbox(
label="✅ 启用自动发布", value=True,
)
with gr.Row():
sched_p_min = gr.Number(
label="发布最小间隔(分钟)", value=60, minimum=30,
)
sched_p_max = gr.Number(
label="发布最大间隔(分钟)", value=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("---")
gr.Markdown("#### 📋 运行日志")
with gr.Row():
btn_refresh_log = gr.Button("🔄 刷新日志", size="sm")
btn_clear_log = gr.Button("🗑️ 清空日志", size="sm")
auto_log_display = gr.TextArea(
label="自动化运行日志",
value="📋 暂无日志\n\n💡 执行操作后日志将在此显示",
lines=15,
interactive=False,
)
# ==================================================
# 事件绑定
# ==================================================
@ -1482,6 +1880,41 @@ with gr.Blocks(
outputs=[data_status, profile_card, chart_interact, chart_notes, notes_detail],
)
# ---- Tab 6: 自动运营 ----
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_publish.click(
fn=_auto_publish_with_log,
inputs=[auto_publish_topics, mcp_url, sd_url, sd_model, llm_model],
outputs=[auto_publish_result, auto_log_display],
)
btn_start_sched.click(
fn=start_scheduler,
inputs=[sched_comment_on, sched_publish_on,
sched_c_min, sched_c_max, sched_p_min, sched_p_max,
auto_comment_keywords, auto_publish_topics,
mcp_url, sd_url, sd_model, llm_model, persona],
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],
)
# ---- 启动时自动刷新 SD ----
app.load(fn=connect_sd, inputs=[sd_url], outputs=[sd_model, status_bar])

View File

@ -10,13 +10,16 @@ from PIL import Image
logger = logging.getLogger(__name__)
SD_TIMEOUT = 180 # 图片生成可能需要较长时间
SD_TIMEOUT = 900 # 图片生成可能需要较长时间
# 默认反向提示词
# 默认反向提示词(针对 JuggernautXL / SDXL 优化)
DEFAULT_NEGATIVE = (
"nsfw, lowres, bad anatomy, bad hands, text, error, missing fingers, "
"extra digit, fewer digits, cropped, worst quality, low quality, "
"normal quality, jpeg artifacts, signature, watermark, blurry"
"nsfw, nudity, lowres, bad anatomy, bad hands, text, error, missing fingers, "
"extra digit, fewer digits, cropped, worst quality, low quality, normal quality, "
"jpeg artifacts, signature, watermark, blurry, deformed, mutated, disfigured, "
"ugly, duplicate, morbid, mutilated, poorly drawn face, poorly drawn hands, "
"extra limbs, fused fingers, too many fingers, long neck, username, "
"out of frame, distorted, oversaturated, underexposed, overexposed"
)
@ -61,14 +64,16 @@ class SDService:
prompt: str,
negative_prompt: str = DEFAULT_NEGATIVE,
model: str = None,
steps: int = 25,
cfg_scale: float = 7.0,
width: int = 768,
height: int = 1024,
steps: int = 30,
cfg_scale: float = 5.0,
width: int = 832,
height: int = 1216,
batch_size: int = 2,
seed: int = -1,
sampler_name: str = "DPM++ 2M",
scheduler: str = "Karras",
) -> list[Image.Image]:
"""文生图"""
"""文生图(参数针对 JuggernautXL 优化)"""
if model:
self.switch_model(model)
@ -81,6 +86,8 @@ class SDService:
"height": height,
"batch_size": batch_size,
"seed": seed,
"sampler_name": sampler_name,
"scheduler": scheduler,
}
resp = requests.post(
@ -101,11 +108,13 @@ class SDService:
init_image: Image.Image,
prompt: str,
negative_prompt: str = DEFAULT_NEGATIVE,
denoising_strength: float = 0.6,
steps: int = 25,
cfg_scale: float = 7.0,
denoising_strength: float = 0.5,
steps: int = 30,
cfg_scale: float = 5.0,
sampler_name: str = "DPM++ 2M",
scheduler: str = "Karras",
) -> list[Image.Image]:
"""图生图(参考图修改)"""
"""图生图(参数针对 JuggernautXL 优化"""
# 将 PIL Image 转为 base64
buf = io.BytesIO()
init_image.save(buf, format="PNG")
@ -120,6 +129,8 @@ class SDService:
"cfg_scale": cfg_scale,
"width": init_image.width,
"height": init_image.height,
"sampler_name": sampler_name,
"scheduler": scheduler,
}
resp = requests.post(