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" "base_url": "https://wolfai.top/v1"
} }
], ],
"xsec_token": "ABS1TagQqhCpZmeNlq0VoCfNEyI6Q83GJzjTGJvzEAq5I=" "xsec_token": "ABdAEbqP9ScgelmyolJxsnpCr_e645SCpnub2dLZJc4Ck="
} }

View File

@ -26,7 +26,13 @@ PROMPT_COPYWRITING = """
3. 结尾必须有 5 个以上相关话题标签(#)。 3. 结尾必须有 5 个以上相关话题标签(#)。
绘图 Prompt 绘图 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 格式 返回 JSON 格式
{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]} {"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}
@ -105,7 +111,11 @@ PROMPT_COPY_WITH_REFERENCE = """
3. 结尾有 5 个以上话题标签(#)。 3. 结尾有 5 个以上话题标签(#)。
绘图 Prompt 绘图 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 格式 返回 JSON 格式
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}} {{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}

433
main.py
View File

@ -10,6 +10,9 @@ import time
import logging import logging
import platform import platform
import subprocess import subprocess
import threading
import random
from datetime import datetime
from PIL import Image from PIL import Image
import matplotlib import matplotlib
import matplotlib.pyplot as plt 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 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 构建 # UI 构建
# ================================================== # ==================================================
@ -1294,6 +1597,101 @@ with gr.Blocks(
label="笔记数据明细", 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], 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 ---- # ---- 启动时自动刷新 SD ----
app.load(fn=connect_sd, inputs=[sd_url], outputs=[sd_model, status_bar]) 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__) logger = logging.getLogger(__name__)
SD_TIMEOUT = 180 # 图片生成可能需要较长时间 SD_TIMEOUT = 900 # 图片生成可能需要较长时间
# 默认反向提示词 # 默认反向提示词(针对 JuggernautXL / SDXL 优化)
DEFAULT_NEGATIVE = ( DEFAULT_NEGATIVE = (
"nsfw, lowres, bad anatomy, bad hands, text, error, missing fingers, " "nsfw, nudity, lowres, bad anatomy, bad hands, text, error, missing fingers, "
"extra digit, fewer digits, cropped, worst quality, low quality, " "extra digit, fewer digits, cropped, worst quality, low quality, normal quality, "
"normal quality, jpeg artifacts, signature, watermark, blurry" "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, prompt: str,
negative_prompt: str = DEFAULT_NEGATIVE, negative_prompt: str = DEFAULT_NEGATIVE,
model: str = None, model: str = None,
steps: int = 25, steps: int = 30,
cfg_scale: float = 7.0, cfg_scale: float = 5.0,
width: int = 768, width: int = 832,
height: int = 1024, height: int = 1216,
batch_size: int = 2, batch_size: int = 2,
seed: int = -1, seed: int = -1,
sampler_name: str = "DPM++ 2M",
scheduler: str = "Karras",
) -> list[Image.Image]: ) -> list[Image.Image]:
"""文生图""" """文生图(参数针对 JuggernautXL 优化)"""
if model: if model:
self.switch_model(model) self.switch_model(model)
@ -81,6 +86,8 @@ class SDService:
"height": height, "height": height,
"batch_size": batch_size, "batch_size": batch_size,
"seed": seed, "seed": seed,
"sampler_name": sampler_name,
"scheduler": scheduler,
} }
resp = requests.post( resp = requests.post(
@ -101,11 +108,13 @@ class SDService:
init_image: Image.Image, init_image: Image.Image,
prompt: str, prompt: str,
negative_prompt: str = DEFAULT_NEGATIVE, negative_prompt: str = DEFAULT_NEGATIVE,
denoising_strength: float = 0.6, denoising_strength: float = 0.5,
steps: int = 25, steps: int = 30,
cfg_scale: float = 7.0, cfg_scale: float = 5.0,
sampler_name: str = "DPM++ 2M",
scheduler: str = "Karras",
) -> list[Image.Image]: ) -> list[Image.Image]:
"""图生图(参考图修改)""" """图生图(参数针对 JuggernautXL 优化"""
# 将 PIL Image 转为 base64 # 将 PIL Image 转为 base64
buf = io.BytesIO() buf = io.BytesIO()
init_image.save(buf, format="PNG") init_image.save(buf, format="PNG")
@ -120,6 +129,8 @@ class SDService:
"cfg_scale": cfg_scale, "cfg_scale": cfg_scale,
"width": init_image.width, "width": init_image.width,
"height": init_image.height, "height": init_image.height,
"sampler_name": sampler_name,
"scheduler": scheduler,
} }
resp = requests.post( resp = requests.post(