- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手) - 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度 - 登录页重构,整合微信登录与 OMS JWT 认证 - 订单、渠道、用户中心等页面与 API 增强 Co-authored-by: Cursor <cursoragent@cursor.com>
525 lines
19 KiB
Python
525 lines
19 KiB
Python
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Any
|
||
|
||
# 爆款公式四要素(对齐超级内容引擎:情绪30% + 实用30% + 视觉20% + 社交20%)
|
||
FORMULA_FACTORS: list[tuple[str, str, str, str]] = [
|
||
("emotion", "情绪共鸣", "让读者产生情感连接", "例:「说实话,我之前也是一团糟…」"),
|
||
("utility", "实用价值", "可收藏、可照做的具体方法", "例:3步清单 + 收藏起来慢慢实践"),
|
||
("visual", "视觉冲击", "标题与封面第一眼抓停", "例:大标题封面 + 步骤卡片配图"),
|
||
("social", "社交货币", "值得转发、讨论、晒圈", "例:转发给朋友也有用 / 表达我的观点"),
|
||
]
|
||
|
||
# 标题技法(多选搭配)
|
||
TITLE_TYPES: list[tuple[str, str, str, str]] = [
|
||
("number", "数字型", "量化结果,降低阅读门槛", "5个让效率翻倍的神器推荐!"),
|
||
("question", "疑问型", "激发好奇,促点击", "为什么你的笔记总是乱?"),
|
||
("compare", "对比型", "制造反差与选择", "别再用 Word 记笔记了"),
|
||
("story", "故事型", "人物弧线,增强信任", "我从一团糟到每天节省2小时"),
|
||
]
|
||
|
||
# 正文四维 — 选项与超级内容引擎 ContentDomain / ContentType / Emotion / SocialCurrency 对齐
|
||
BODY_FIELDS: list[tuple[str, str, list[str], dict[str, str]]] = [
|
||
(
|
||
"domain",
|
||
"内容领域",
|
||
[
|
||
"效率工具",
|
||
"职场发展",
|
||
"情感共鸣",
|
||
"美妆护肤",
|
||
"生活方式",
|
||
"理财副业",
|
||
"学习技能",
|
||
"美食探店",
|
||
"旅行攻略",
|
||
"家居装修",
|
||
"自定义",
|
||
],
|
||
{
|
||
"效率工具": "关键词:笔记工具、时间管理 → PRODUCTIVITY",
|
||
"职场发展": "关键词:升职、面试、人际关系 → CAREER",
|
||
"情感共鸣": "关键词:情绪、关系、治愈 → EMOTIONAL",
|
||
"美妆护肤": "关键词:护肤、妆容、好物 → BEAUTY",
|
||
"理财副业": "关键词:副业、搞钱、理财 → SIDE_HUSTLE",
|
||
"学习技能": "关键词:学习方法、考证 → LEARNING",
|
||
},
|
||
),
|
||
(
|
||
"content_type",
|
||
"内容类型",
|
||
[
|
||
"干货教程",
|
||
"经验分享",
|
||
"资源推荐",
|
||
"避坑指南",
|
||
"热点追蹭",
|
||
"槽点讨论",
|
||
"种草推荐",
|
||
"测评体验",
|
||
"问答互动",
|
||
"自定义",
|
||
],
|
||
{
|
||
"干货教程": "新号建信任首选 · TUTORIAL",
|
||
"经验分享": "职场/成长类常用 · EXPERIENCE",
|
||
"资源推荐": "工具清单型 · RESOURCE",
|
||
"避坑指南": "存在感确认 + 观点表达 · PITFALL",
|
||
"热点追蹭": "成长阶段拉曝光 · TREND",
|
||
"槽点讨论": "激活评论 · DISCUSSION",
|
||
"种草推荐": "成熟阶段变现 · RECOMMEND",
|
||
},
|
||
),
|
||
(
|
||
"emotion",
|
||
"情绪触发",
|
||
[
|
||
"焦虑共鸣",
|
||
"生活场景",
|
||
"转变故事",
|
||
"理想生活",
|
||
"吐槽宣泄",
|
||
"错失恐惧",
|
||
"梦想向往",
|
||
"自定义",
|
||
],
|
||
{
|
||
"焦虑共鸣": "「你也这样吗?」效率/职场爆款组合",
|
||
"生活场景": "「这也太真实了」情感共鸣首选",
|
||
"转变故事": "「从XX到XX」配合故事型标题",
|
||
"理想生活": "「这就是我想要的」职场/生活方式",
|
||
"吐槽宣泄": "职场 PUA、不合理现象",
|
||
},
|
||
),
|
||
(
|
||
"social",
|
||
"社交货币",
|
||
[
|
||
"价值传递",
|
||
"身份标签",
|
||
"观点表达",
|
||
"知识炫耀",
|
||
"情感支持",
|
||
"娱乐价值",
|
||
"自定义",
|
||
],
|
||
{
|
||
"价值传递": "收藏起来慢慢看 · 转给朋友有用",
|
||
"身份标签": "「我是这样的人」职场/生活方式",
|
||
"观点表达": "转发表达我的态度",
|
||
"情感支持": "共鸣内容,点赞支持",
|
||
},
|
||
),
|
||
]
|
||
|
||
# 图片呈现
|
||
IMAGE_MODES: list[tuple[str, str, str, str]] = [
|
||
("cover_impact", "强对比封面", "大标题 + 高对比色块", "封面 emoji + 15字内大标题"),
|
||
("step_cards", "步骤卡片", "Markdown 渲染多图卡片", "cover.png + card_1/2/3…"),
|
||
("before_after", "前后对比", "效果对比拼图", "使用前 vs 使用后"),
|
||
("info_chart", "信息图表", "结构图 / 数据清单", "清单体 + 图标排版"),
|
||
("lifestyle", "氛围实拍", "场景感生活图", "探店 / 家居 / Vlog 截图"),
|
||
]
|
||
|
||
# 视频呈现
|
||
VIDEO_MODES: list[tuple[str, str, str, str]] = [
|
||
("talking_head", "口播人设", "真人出镜讲解", "开头3秒钩子 + 人设口头禅"),
|
||
("screen_record", "屏幕录制", "实操演示录屏", "笔记工具 / AI 工具实操"),
|
||
("vlog_clip", "实拍 Vlog", "生活场景切片", "探店 / 日常 vlog"),
|
||
("slideshow", "图文轮播", "卡片配乐滑动", "渲染图转视频发布"),
|
||
("mixed_cut", "混剪卡点", "快剪 + BGM", "热点 / 挑战类"),
|
||
]
|
||
|
||
# 官方示例模板(来自超级内容引擎爆款组合表 + 运营阶段建议)
|
||
PRESET_TEMPLATES: list[tuple[str, str, dict[str, Any]]] = [
|
||
(
|
||
"efficiency_tutorial",
|
||
"效率工具 · 干货教程(新号信任)",
|
||
{
|
||
"formula": ["emotion", "utility", "visual", "social"],
|
||
"title_types": ["number", "question"],
|
||
"body": {
|
||
"domain": "效率工具",
|
||
"content_type": "干货教程",
|
||
"emotion": "焦虑共鸣",
|
||
"social": "价值传递",
|
||
},
|
||
"image_modes": ["cover_impact", "step_cards"],
|
||
"video_modes": [],
|
||
"notes": "话题示例:笔记工具提升效率。钩子:说实话,我之前笔记也是一团糟… CTA:收藏起来,慢慢实践",
|
||
},
|
||
),
|
||
(
|
||
"career_experience",
|
||
"职场发展 · 经验分享",
|
||
{
|
||
"formula": ["emotion", "utility", "social"],
|
||
"title_types": ["story", "compare"],
|
||
"body": {
|
||
"domain": "职场发展",
|
||
"content_type": "经验分享",
|
||
"emotion": "理想生活",
|
||
"social": "身份标签",
|
||
},
|
||
"image_modes": ["cover_impact", "lifestyle"],
|
||
"video_modes": [],
|
||
"notes": "话题示例:职场人际关系。角度:从打工人到… 社交:证明我是上进的人",
|
||
},
|
||
),
|
||
(
|
||
"emotional_relatable",
|
||
"情感共鸣 · 真实场景",
|
||
{
|
||
"formula": ["emotion", "social"],
|
||
"title_types": ["story", "question"],
|
||
"body": {
|
||
"domain": "情感共鸣",
|
||
"content_type": "经验分享",
|
||
"emotion": "生活场景",
|
||
"social": "情感支持",
|
||
},
|
||
"image_modes": ["lifestyle"],
|
||
"video_modes": ["vlog_clip"],
|
||
"notes": "让读者觉得「这也太真实了」。结尾互动:你也有过吗?",
|
||
},
|
||
),
|
||
(
|
||
"pitfall_opinion",
|
||
"避坑指南 · 观点表达",
|
||
{
|
||
"formula": ["emotion", "utility", "social"],
|
||
"title_types": ["compare", "number"],
|
||
"body": {
|
||
"domain": "职场发展",
|
||
"content_type": "避坑指南",
|
||
"emotion": "吐槽宣泄",
|
||
"social": "观点表达",
|
||
},
|
||
"image_modes": ["before_after", "info_chart"],
|
||
"video_modes": [],
|
||
"notes": "话题示例:避坑指南。情绪:存在感确认。适合引发评论讨论",
|
||
},
|
||
),
|
||
(
|
||
"trend_discussion",
|
||
"热点追蹭 · 槽点讨论(成长曝光)",
|
||
{
|
||
"formula": ["emotion", "visual", "social"],
|
||
"title_types": ["question", "compare"],
|
||
"body": {
|
||
"domain": "生活方式",
|
||
"content_type": "热点追蹭",
|
||
"emotion": "吐槽宣泄",
|
||
"social": "观点表达",
|
||
},
|
||
"image_modes": ["cover_impact"],
|
||
"video_modes": ["mixed_cut"],
|
||
"notes": "成长阶段:TREND + DISCUSSION 拉曝光。注意热点时效",
|
||
},
|
||
),
|
||
(
|
||
"recommend_review",
|
||
"种草测评 · 成熟变现",
|
||
{
|
||
"formula": ["utility", "visual", "social"],
|
||
"title_types": ["number", "story"],
|
||
"body": {
|
||
"domain": "美妆护肤",
|
||
"content_type": "种草推荐",
|
||
"emotion": "转变故事",
|
||
"social": "知识炫耀",
|
||
},
|
||
"image_modes": ["before_after", "lifestyle"],
|
||
"video_modes": ["talking_head", "screen_record"],
|
||
"notes": "成熟阶段:RECOMMEND / REVIEW。真实测评 + 优缺点",
|
||
},
|
||
),
|
||
]
|
||
|
||
|
||
@dataclass
|
||
class SkillRecipe:
|
||
"""可自主搭配的爆款技能配方。"""
|
||
|
||
formula: list[str] = field(default_factory=list)
|
||
title_types: list[str] = field(default_factory=list)
|
||
body: dict[str, str] = field(default_factory=dict)
|
||
image_modes: list[str] = field(default_factory=list)
|
||
video_modes: list[str] = field(default_factory=list)
|
||
notes: str = ""
|
||
|
||
@classmethod
|
||
def empty(cls) -> SkillRecipe:
|
||
return cls(
|
||
formula=["emotion", "utility", "visual", "social"],
|
||
title_types=[],
|
||
body={},
|
||
image_modes=[],
|
||
video_modes=[],
|
||
)
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"formula": list(self.formula),
|
||
"title_types": list(self.title_types),
|
||
"body": dict(self.body),
|
||
"image_modes": list(self.image_modes),
|
||
"video_modes": list(self.video_modes),
|
||
"notes": self.notes,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, raw: dict[str, Any] | None) -> SkillRecipe:
|
||
if not raw:
|
||
return cls.empty()
|
||
return cls(
|
||
formula=[str(x) for x in raw.get("formula") or []],
|
||
title_types=[str(x) for x in raw.get("title_types") or []],
|
||
body={str(k): str(v) for k, v in (raw.get("body") or {}).items()},
|
||
image_modes=[str(x) for x in raw.get("image_modes") or []],
|
||
video_modes=[str(x) for x in raw.get("video_modes") or []],
|
||
notes=str(raw.get("notes") or ""),
|
||
)
|
||
|
||
|
||
def _label_map(items: list[tuple]) -> dict[str, str]:
|
||
return {row[0]: row[1] for row in items}
|
||
|
||
|
||
def _example_map(items: list[tuple]) -> dict[str, str]:
|
||
out: dict[str, str] = {}
|
||
for row in items:
|
||
if len(row) >= 4:
|
||
out[row[0]] = row[3]
|
||
return out
|
||
|
||
|
||
def body_field_hints() -> dict[str, dict[str, str]]:
|
||
return {key: hints for key, _label, _opts, hints in BODY_FIELDS}
|
||
|
||
|
||
def get_body_hint(field_key: str, value: str) -> str:
|
||
hints = body_field_hints().get(field_key, {})
|
||
return hints.get(value, "")
|
||
|
||
|
||
def preset_recipe(preset_id: str) -> SkillRecipe | None:
|
||
for pid, _name, raw in PRESET_TEMPLATES:
|
||
if pid == preset_id:
|
||
return SkillRecipe.from_dict(raw)
|
||
return None
|
||
|
||
|
||
def list_preset_templates() -> list[tuple[str, str]]:
|
||
return [(pid, name) for pid, name, _ in PRESET_TEMPLATES]
|
||
|
||
|
||
def compose_title_example(recipe: SkillRecipe) -> str:
|
||
"""根据已选标题类型合成示例标题。"""
|
||
examples = _example_map(TITLE_TYPES)
|
||
parts = [examples[k] for k in recipe.title_types if k in examples]
|
||
if not parts:
|
||
return ""
|
||
if len(parts) == 1:
|
||
return parts[0]
|
||
return f"{parts[0]} / {parts[1]}" if len(parts) > 1 else parts[0]
|
||
|
||
|
||
def compose_hook_example(recipe: SkillRecipe) -> str:
|
||
"""根据正文情绪合成钩子示例。"""
|
||
emotion = recipe.body.get("emotion", "")
|
||
hooks = {
|
||
"焦虑共鸣": "说实话,我之前也是一团糟…",
|
||
"生活场景": "昨天发生一件事,太真实了…",
|
||
"转变故事": "半年前我还…现在终于…",
|
||
"理想生活": "这就是我想要的效率状态",
|
||
"吐槽宣泄": "真的忍不住了,必须说说…",
|
||
"错失恐惧": "别人都在用,就你还不知道?",
|
||
}
|
||
if emotion in hooks:
|
||
return hooks[emotion]
|
||
domain = recipe.body.get("domain", "")
|
||
if domain == "效率工具":
|
||
return "从0到1的笔记工具路径,今天不说虚的"
|
||
return ""
|
||
|
||
|
||
def compose_cta_example(recipe: SkillRecipe) -> str:
|
||
social = recipe.body.get("social", "")
|
||
ctas = {
|
||
"价值传递": "收藏起来,慢慢实践",
|
||
"身份标签": "认同的姐妹/兄弟点个赞",
|
||
"观点表达": "你觉得呢?评论区聊聊",
|
||
"情感支持": "有过同感的抱抱",
|
||
"知识炫耀": "转发给需要的朋友",
|
||
}
|
||
return ctas.get(social, "点赞收藏,下次用得上")
|
||
|
||
|
||
def compose_workflow(recipe: SkillRecipe) -> str:
|
||
"""将配方合成为卡片/预览用的流程文案。"""
|
||
lines: list[str] = []
|
||
|
||
f_labels = _label_map(FORMULA_FACTORS)
|
||
if recipe.formula:
|
||
parts = [f_labels.get(k, k) for k in recipe.formula]
|
||
lines.append("【爆款公式】" + " × ".join(parts))
|
||
|
||
t_labels = _label_map(TITLE_TYPES)
|
||
if recipe.title_types:
|
||
lines.append("【标题】" + " + ".join(t_labels.get(k, k) for k in recipe.title_types))
|
||
title_ex = compose_title_example(recipe)
|
||
if title_ex:
|
||
lines.append(f" 示例:{title_ex}")
|
||
|
||
if recipe.body:
|
||
field_labels = {row[0]: row[1] for row in BODY_FIELDS}
|
||
body_parts = []
|
||
for key, label, _opts, _hints in BODY_FIELDS:
|
||
val = recipe.body.get(key, "").strip()
|
||
if val:
|
||
body_parts.append(f"{field_labels.get(key, key)}:{val}")
|
||
if body_parts:
|
||
lines.append("【正文】" + " | ".join(body_parts))
|
||
hook = compose_hook_example(recipe)
|
||
cta = compose_cta_example(recipe)
|
||
if hook:
|
||
lines.append(f" 钩子:{hook}")
|
||
if cta:
|
||
lines.append(f" CTA:{cta}")
|
||
|
||
i_labels = _label_map(IMAGE_MODES)
|
||
if recipe.image_modes:
|
||
lines.append("【图片】" + " + ".join(i_labels.get(k, k) for k in recipe.image_modes))
|
||
ex = _example_map(IMAGE_MODES)
|
||
img_ex = " · ".join(ex[k] for k in recipe.image_modes[:2] if k in ex)
|
||
if img_ex:
|
||
lines.append(f" 示例:{img_ex}")
|
||
|
||
v_labels = _label_map(VIDEO_MODES)
|
||
if recipe.video_modes:
|
||
lines.append("【视频】" + " + ".join(v_labels.get(k, k) for k in recipe.video_modes))
|
||
|
||
if recipe.notes.strip():
|
||
lines.append("【备注】" + recipe.notes.strip())
|
||
|
||
return "\n".join(lines) if lines else "(请搭配各维度选项)"
|
||
|
||
|
||
def compose_card_preview_text(recipe: SkillRecipe) -> str:
|
||
"""卡片正文:多行搭配预览,在固定高度内尽可能展示更多信息。"""
|
||
f_labels = _label_map(FORMULA_FACTORS)
|
||
t_labels = _label_map(TITLE_TYPES)
|
||
i_labels = _label_map(IMAGE_MODES)
|
||
v_labels = _label_map(VIDEO_MODES)
|
||
|
||
lines: list[str] = []
|
||
if recipe.formula:
|
||
lines.append("公式 " + "×".join(f_labels.get(k, k) for k in recipe.formula))
|
||
if recipe.title_types:
|
||
title_part = "+".join(t_labels.get(k, k) for k in recipe.title_types)
|
||
title_ex = compose_title_example(recipe)
|
||
if title_ex:
|
||
title_part += f" →「{title_ex}」"
|
||
lines.append("标题 " + title_part)
|
||
body_bits = [
|
||
recipe.body.get(k, "").strip()
|
||
for k in ("domain", "content_type", "emotion", "social")
|
||
if recipe.body.get(k, "").strip()
|
||
]
|
||
if body_bits:
|
||
lines.append("正文 " + " · ".join(body_bits))
|
||
hook = compose_hook_example(recipe)
|
||
if hook:
|
||
lines.append("钩子 " + hook)
|
||
if recipe.image_modes:
|
||
lines.append("图片 " + "+".join(i_labels.get(k, k) for k in recipe.image_modes))
|
||
if recipe.video_modes:
|
||
lines.append("视频 " + "+".join(v_labels.get(k, k) for k in recipe.video_modes))
|
||
cta = compose_cta_example(recipe)
|
||
if cta:
|
||
lines.append("CTA " + cta)
|
||
|
||
if not lines:
|
||
return "(请编辑搭配各维度)"
|
||
return "\n".join(lines)
|
||
|
||
|
||
def compose_card_preview_line(recipe: SkillRecipe) -> str:
|
||
"""兼容:单行摘要。"""
|
||
return compose_card_preview_text(recipe).replace("\n", " · ")
|
||
|
||
|
||
def recipe_tag_groups(recipe: SkillRecipe) -> tuple[list[str], list[str]]:
|
||
"""底部标签:标题方向 | 正文(领域/类型/情绪/社交)。"""
|
||
t_labels = _label_map(TITLE_TYPES)
|
||
title_tags = [t_labels.get(k, k) for k in recipe.title_types if k in t_labels]
|
||
body_tags: list[str] = []
|
||
for key in ("domain", "content_type", "emotion", "social"):
|
||
val = recipe.body.get(key, "").strip()
|
||
if val:
|
||
body_tags.append(val)
|
||
return title_tags, body_tags
|
||
|
||
|
||
def compose_card_summary(recipe: SkillRecipe) -> str:
|
||
"""卡片上显示的精简摘要(2~3 行)。"""
|
||
lines: list[str] = []
|
||
f_labels = _label_map(FORMULA_FACTORS)
|
||
if recipe.formula:
|
||
lines.append(" × ".join(f_labels.get(k, k) for k in recipe.formula[:4]))
|
||
domain = recipe.body.get("domain", "")
|
||
ctype = recipe.body.get("content_type", "")
|
||
if domain or ctype:
|
||
lines.append(f"{domain} · {ctype}".strip(" · "))
|
||
title_ex = compose_title_example(recipe)
|
||
if title_ex:
|
||
lines.append(f"「{title_ex[:28]}{'…' if len(title_ex) > 28 else ''}」")
|
||
return "\n".join(lines) if lines else "点击编辑搭配技能"
|
||
|
||
|
||
def compose_prompt_brief(recipe: SkillRecipe) -> str:
|
||
"""生成给 AI / 超级内容引擎使用的创作指令。"""
|
||
wf = compose_workflow(recipe)
|
||
topic_hint = recipe.body.get("domain", "你的主题")
|
||
parts = [
|
||
"请按以下爆款技能配方创作笔记内容(参考超级内容引擎策略):",
|
||
"",
|
||
wf,
|
||
"",
|
||
"【创作要求】",
|
||
"- 标题与正文风格一致,口语化、有温度",
|
||
"- 正文 200~800 字,段落清晰,适量 emoji",
|
||
"- 结尾 5~10 个 SEO 标签",
|
||
f"- 话题方向:{topic_hint}",
|
||
]
|
||
hook = compose_hook_example(recipe)
|
||
cta = compose_cta_example(recipe)
|
||
if hook:
|
||
parts.append(f"- 开头钩子参考:{hook}")
|
||
if cta:
|
||
parts.append(f"- 行动召唤:{cta}")
|
||
if recipe.image_modes:
|
||
parts.append("- 配图:封面抓眼球 + 正文分步卡片(可 Markdown 渲染)")
|
||
parts.append("")
|
||
parts.append("输出:标题 + 正文 + 标签建议")
|
||
return "\n".join(parts)
|
||
|
||
|
||
def suggest_skill_title(recipe: SkillRecipe) -> str:
|
||
parts: list[str] = []
|
||
t_labels = _label_map(TITLE_TYPES)
|
||
if recipe.title_types:
|
||
parts.append("/".join(t_labels.get(k, k) for k in recipe.title_types[:2]))
|
||
domain = recipe.body.get("domain", "").strip()
|
||
if domain and domain != "自定义":
|
||
parts.append(domain)
|
||
if recipe.image_modes and not recipe.video_modes:
|
||
parts.append("图文")
|
||
elif recipe.video_modes:
|
||
parts.append("视频")
|
||
if not parts:
|
||
return "我的爆款技能"
|
||
return " · ".join(parts[:3])
|