nexu-io

牛马锐评

先蒸馏自己(或同事)的工作数据(飞书消息、推特、文章等),再生成毒舌但精准的牛马锐评报告。输出包含个人标签、牛马指数、能力条、8 维度深度扒皮、AI 对话示例,并自动生成可分享的落地页。触发词:锐评、牛马、吐槽、roast、蒸馏、赛博分身。

nexu-io 3 Updated 1mo ago

Resources

6
GitHub

Install

npx skillscat add nexu-io/roast-skill

Install via the SkillsCat registry.

SKILL.md

🔥 牛马锐评 — AI 蒸馏你的工作人格

安装完成后的引导(必须执行)

当用户刚安装完 skill 时,必须主动告诉用户怎么开始使用:

🔥 roast-skill 安装完成!

现在你可以开始锐评了,输入:
👉 锐评 <名字>

例如:
  锐评 张三
  锐评 Elon Musk
  roast @elonmusk

不要只说"可以开始了"就结束——必须给出明确的输入格式和示例。


执行流程(必须严格按顺序执行)

当用户触发锐评请求时,按以下 5 步执行。不要跳步,不要用历史 context 代替数据采集。

Step 0: 环境能力检测(自动执行,不需要用户操作)

在跟用户对话之前,先静默检测当前 agent 的数据采集能力

⚠️ 重要规则:飞书权限必须前置真实检测(调用 feishu_app_scopes),不要假设权限存在。 检测到缺失时立刻引导用户开通,不要等到 Step 2 采集失败再说。前置检测 = 用户体验好,后置报错 = 用户体验差。

检测项 1: 推特/X 读取能力

  • 先检查 twitter-clibird 命令是否可用(which twitter / which bird
  • 如果可用 → 运行 twitter status 检查是否已认证
    • authenticated: true → ✅ 推特能力就绪
    • authenticated: false → ⚠️ 工具已装但未配置 Cookie,需要引导用户完成「推特/X 从零配置指南」的第 2-3 步
  • 如果都不可用 → ❌ 需要引导用户完成「推特/X 从零配置指南」的完整 3 步
  • 不要只说"装 agent-reach"就完了 — 必须告诉用户怎么获取 Cookie 并验证

检测项 2: 飞书消息读取能力

  • 必须前置检测,不要盲目标 ✅
  • 使用 feishu_app_scopes 工具检查当前 app 的权限列表
  • 在返回的权限列表中查找 im:message:readonlyim:message
    • 找到 → ✅ 飞书消息读取能力就绪
    • 未找到 → ❌ 缺少权限,必须引导用户开通
⚠️ 当前飞书应用缺少「消息读取」权限,无法自动采集聊天记录。

请按以下步骤开通:
1. 打开飞书开发者后台 → 你的应用
2. 左侧菜单「权限管理」
3. 搜索 im:message:readonly(获取群组中所有消息)
4. 点击「开通权限」并提交审核(自建应用一般秒通过)
5. 开通后告诉我,我重新检测

如果你不是应用管理员,让管理员帮忙开通这个权限。

⚠️ 不要跳过这步直接进入数据采集! 如果权限没开通就直接调 API,会浪费用户时间等一堆报错。前置检测 → 前置引导 → 再开始采集。

检测项 3: 网页抓取能力

  • 检查 web_fetch 工具是否可用 → 通常都有 ✅

将检测结果(3 项)汇总为能力清单,在 Step 1 中展示给用户。落地页由 nexu 平台自动部署,无需检测。

Step 1: 确认目标 & 展示能力 & 引导补齐

先展示环境检测结果,再收集目标信息:

🔥 锐评模式启动!先看看我的装备:

📡 数据采集能力:
  ✅ 推特/X — 可自动抓取 profile + 推文(已装 twitter-cli)
  ✅ 飞书 — 可自动读取你和 TA 的所有共同群聊消息
  ✅ 网页 — 可抓取文章/GitHub 等 URL

飞书消息我会自动采集,以下信息可选填(有就更精准):
1. **TA 的推特链接**(可选)
2. **其他资料**:文章链接、GitHub 等(可选)
3. **你的主观印象**(可选,越毒越好)

没有额外信息也没关系,回复「直接开始」我就用飞书数据开干 🎯

注意:如果用户触发锐评时已经给了名字(如"锐评 孙庆雨"),Step 1 中不需要再问"锐评谁"——名字已经有了。只需要展示能力清单 + 询问可选补充信息 + 提供"直接开始"的选项。

如果检测到能力缺失,主动引导用户补齐:

📡 数据采集能力:
  ❌ 推特/X — 未安装 twitter-cli
     👉 完整设置流程见下方「推特/X 从零配置指南」
     👉 不想配?也可以手动粘贴推文,效果一样
  ❌ 飞书 — 缺少「消息读取」权限(im:message:readonly)
     👉 在飞书管理后台 → 应用权限 → 开通 im:message:readonly
     👉 或者手动粘贴 TA 最近的 20-30 条消息
  ✅ 网页 — OK

推特/X 从零配置指南(完整 3 步)

当推特能力检测为 ❌ 时,必须向用户展示以下可视化引导

⚠️ 重要:必须把下面的引导图片发给用户,不要只用文字描述。图片比文字直观 10 倍。
图片 URL:https://raw.githubusercontent.com/nexu-io/roast-skill/main/assets/cookie-editor-guide.jpg
发送方式:使用 message 工具的 media 参数发送此 URL,或用 MEDIA:https://raw.githubusercontent.com/nexu-io/roast-skill/main/assets/cookie-editor-guide.jpg 内联发送。

第 1 步:安装 Agent Reach

告诉用户对 agent 说:

帮我安装 Agent Reach:https://github.com/Panniantong/Agent-Reach

第 2 步:获取 Twitter Cookie(看图操作,4 步搞定)

必须发送引导图片给用户,然后配合以下文字说明:

🍪 获取 Twitter Cookie(看图 4 步搞定):

1️⃣ 在 Chrome 应用商店安装 Cookie-Editor 插件
   👉 https://cookie-editor.com

2️⃣ 打开 x.com 并登录,点击浏览器右上角 Cookie-Editor 插件图标

3️⃣ 点击 Export

4️⃣ 选择 Header String 格式,复制内容发给我

就这么简单!复制的内容直接发给我就行 🎯

收到用户粘贴的 Cookie 后,agent 自动保存到 ~/.agent-reach/twitter-cookies.txt

第 3 步:验证配置

twitter status

显示 authenticated: true 就成功了 ✅

⚠️ 常见问题:

  • Cookie 有效期一般 1-3 个月,过期了重新导出即可
  • 不需要 Twitter API Key,纯 Cookie 就够
  • Cookie 只存在本地,不会上传到任何地方

不要跳过这一步。 不要直接用对话历史中的信息生成锐评。必须等用户明确提供或确认数据源。

Step 2: 数据采集(根据确认的来源逐个执行)

推特/X

如果用户提供了推特链接或 @username,且推特能力 ✅:

  1. 使用内置脚本获取数据:
python3 <skill_dir>/scripts/twitter_fetch.py <username> --count 30 --output /tmp/roast_twitter_data.json
  1. 脚本自动获取 profile + 最近 30 条原创推文
  2. 从输出 JSON 中分析:发推频率、活跃时段、高频词、emoji 使用、互动模式

如果推特能力 ❌:

  1. 先引导用户参照上方「推特/X 从零配置指南」完成配置
  2. 如果用户不想配置或时间紧迫,提供 手动输入 替代方案:
    • 手动粘贴 10-20 条推文
    • 提供推特 profile 截图(agent 用 image 工具分析)
    • 直接给推特主页 URL,agent 尝试用 web_fetch 抓取(可能受限)

飞书(自动执行,不需要用户指定群)

飞书数据采集是全自动的,不需要用户指定哪个群:

  1. 找到目标用户的 open_id(通过用户名搜索或让用户提供)
  2. 使用内置脚本自动采集:
python3 <skill_dir>/scripts/feishu_fetch.py \
  --app-id <FEISHU_APP_ID> --app-secret <FEISHU_APP_SECRET> \
  --target-user <open_id> --output /tmp/roast_feishu_data.json
  1. 脚本自动:获取 bot 所在的所有群 → 找到目标用户也在的群(共同群) → 读取目标用户在所有共同群中的全部消息
  2. 从输出 JSON 中分析:发言频率、活跃时段、消息长度、高频词、表情使用、沟通风格

⚠️ 重要:采集范围是目标用户在所有共同群聊中的消息,不是只读目标用户和当前用户之间的私聊或某一个群。 Bot 在哪些群里,就扫哪些群,只要目标用户也在那个群里就读取 TA 的全部消息。群越多、消息越多,锐评质量越高。

如果飞书能力 ❌:

⚠️ 当前缺少飞书「消息读取」权限(im:message:readonly),无法自动采集。
你可以:
1. 在飞书管理后台 → 应用权限 → 开通 im:message:readonly
2. 或者手动复制粘贴 TA 最近的 20-30 条消息给我

其他来源

  • 文章 URL:用 web_fetch 抓取全文
  • GitHub:用 gh CLI 或 web_fetch 获取 profile + README
  • 手动粘贴:直接接收用户粘贴的文本
  • 图片/截图:用 image 工具分析聊天截图中的内容

Step 3: 分析 & 生成锐评(内部处理,不输出给用户)

拿到数据后,按以下结构在内部生成完整锐评报告。不要在聊天消息中输出任何锐评文本——所有内容直接用于 Step 4 的落地页生成。用户看到的唯一输出是最终部署好的落地页链接。

⚠️ 关键规则:Step 3 生成的内容是中间产物,不是用户看到的最终结果。生成完毕后立即进入 Step 4 部署落地页。

3.1 个人标签(1-8 个)→ 对应 tags 字段

纯字符串数组,每个标签 2-8 字。建议内容顺序:

  1. 公司/组织(如 "nexu")
  2. 角色(如 "增长 & 产品")
  3. 职级/身份(如 "Co-builder")
  4. MBTI 类型(根据行为推测,如 "ENTJ")
  5. 行为特征标签 ×3-4 个(从数据提炼,用阴阳怪气方式包装)

标签按顺序映射颜色:purple → cyan → orange → teal → mint(循环)。海报分两行展示(前 4 个一行,后 4 个一行)。

3.2 牛马指数(0-100)→ 对应 metrics[0]

综合工作强度、在线时长、信息密度打分。

  • metrics[0].label:带 emoji 的指标名,如 "🐂🐴 牛马指数"(6-10 字)
  • metrics[0].value纯数字字符串,不带 % 号,如 "92"
  • 此值会在页面核心指标区大字展示、海报分数区、技能文件进度条右侧出现

3.3 物种鉴定 → 对应 posterSpeciesEmoji / posterSpeciesName / posterSpeciesSub

  • posterSpeciesEmoji:单个 emoji(如 🦞、🦉、🐱)
  • posterSpeciesName:物种名,3-8 字(如 "龙虾成瘾者"、"午夜永动机")
  • posterSpeciesSub:副标题,5-8 字,固定用 "办公室物种鉴定" 或自定义
  • 出现位置:核心指标卡右侧、海报物种区

3.4 能力条(3-5 项)→ 对应 metrics[1] ~ metrics[4/5]

从数据中提取 3-5 个最突出的行为维度:

  • label:维度名,6-12 字,带 emoji,命名要有梗(如 "说教力""造词力" 而非 "领导力""沟通力")
  • value百分比字符串,必须带 % 号,如 "95%"
  • 出现位置:核心指标卡下方进度条

metrics 合计 4-6 个(1 个牛马指数 + 3-5 个能力条)。

3.5 主锐评(纯文本,150-250 字)→ 对应 description 字段

第二人称直接开怼。要求:

  • 纯文本,不要 markdown 格式(不要 **加粗**、不要 # 标题、不要列表符号)
  • 不要换行符 \n——写成一个连续段落
  • 开头就咬人,不铺垫
  • 至少 3 个金句(让人想截图分享)
  • 每 2-3 句话切换一种修辞武器
  • 结尾反转留温暖("不过说真的...")

⚠️ 这个字段会在网页上出现两次(核心指标区和 AI 扒皮区),所以不能太长。150-250 字最佳。

3.6 深度扒皮(2-3 个维度)→ 对应 qaCards 字段

模板最多支持 2-3 个维度(不是 8 个!),每个维度:

  • question:维度标题,3-8 字(如 "致命优势"、"致命弱点"、"人生建议")
  • answer:维度内容,纯文本,80-150 字,不要 markdown/换行符
  • icon 映射:第 1 个 🔥、第 2 个 💪、第 3 个 💀

从以下维度中挑 2-3 个最有梗的

维度 风格要求
💪 优势 以夸代讽为主——表面夸上天,细品全是刀子
💀 弱点 类比讽刺为主——荒诞比喻暗示不足
💰 财务观 反问质疑为主——连续反问让人无法反驳
🐾 灵魂动物 借物喻人为主——动物行为映射人的习惯
👥 他人视角 多角色视角(同事/实习生/HR/家人/投资人)
⭐ 名人合体 两个反差名人合体,精准到让人拍大腿
💊 人生建议 引典用俗为主——用"认真建议"的语气说最毒的话

不要把所有维度塞进去。 挑 2-3 个数据支撑最强、最有梗的维度,其余内容可以融入主锐评。

3.7 AI 对话示例(3-6 条)→ 对应 dialogs 字段

模拟 bot 和目标人物之间的对话,每条指定 speaker("bot" 或 "user"):

  • bot 提问引出人设
  • user 回答带个人语言特色
  • 3-6 条交替对话,不是 2 轮 Q&A

3.8 固定字段

以下字段使用固定值:

  • ctaText:固定为 "⭐ 生成我的牛马锐评"
  • installText:固定为 "复制链接发给你的 nexu agent:https://github.com/nexu-io/roast-skill"
  • subtitle 格式:固定为 "牛马指数 {XX}/100 — {posterSpeciesName}"

Step 4: 生成落地页并部署(自动执行,不需要用户确认)

Step 3 完成后立即执行此步骤,不要:

  • ❌ 问用户"要不要生成落地页"
  • ❌ 在聊天消息里先展示文字锐评再问"要不要部署"
  • ❌ 等用户确认

锐评的唯一交付形式就是落地页链接。 采集数据 → 内部生成锐评 → 自动部署落地页 → 返回链接,一气呵成。

  1. 构造 content.json 文件,严格符合 deploy-skill distill-campaign 模板的 schema:
{
  "title": "张三",
  "subtitle": "牛马指数 85/100 — 午夜永动机",
  "tags": ["nexu", "工程师", "Tech Lead", "INTJ", "代码永动机", "需求粉碎者"],
  "metrics": [
    {"label": "🐂🐴 牛马指数", "value": "85"},
    {"label": "⚡ 执行力", "value": "95%"},
    {"label": "📢 嘴炮力", "value": "72%"},
    {"label": "🔗 链接密度", "value": "88%"},
    {"label": "🚫 摸鱼力", "value": "15%"}
  ],
  "description": "这里是主锐评,150-250字纯文本。不要markdown,不要换行符,不要加粗标记。直接写成一个连续段落,开头就咬人,结尾留温暖。此字段在页面出现两次(核心指标区+AI扒皮区),不能太长。",
  "qaCards": [
    {"question": "致命优势", "answer": "这里写优势维度的扒皮内容,80-150字纯文本,不要markdown或换行符。"},
    {"question": "致命弱点", "answer": "这里写弱点维度的扒皮内容,80-150字纯文本,不要markdown或换行符。"},
    {"question": "人生建议", "answer": "这里写建议维度的扒皮内容,80-150字纯文本,不要markdown或换行符。"}
  ],
  "dialogs": [
    {"speaker": "bot", "text": "张三,如果让你一周不写代码,你会做什么?"},
    {"speaker": "user", "text": "写文档?不行,那也算工作……"},
    {"speaker": "bot", "text": "看来你已经不知道什么叫休息了。"},
    {"speaker": "user", "text": "休息就是换一个 repo 继续写。"}
  ],
  "posterSpeciesEmoji": "🦉",
  "posterSpeciesName": "午夜永动机",
  "posterSpeciesSub": "办公室物种鉴定",
  "portraitId": "portrait-1",
  "ctaText": "⭐ 生成我的牛马锐评",
  "installText": "复制链接发给你的 nexu agent:https://github.com/nexu-io/roast-skill"
}

⚠️ content.json 字段规则(必须严格遵守):

字段 类型 限制 说明
title string 必填 目标人名
subtitle string 15-30字 必须含「牛马指数」「/100」和「—」,格式:「牛马指数 XX/100 — 物种名」
tags string[] 1-8 个 纯字符串,每个 2-8 字,按顺序映射 company/role/level/mbti/vibe
metrics object[] 4-6 个 [0] 为牛马指数(value 纯数字如 "92"),[1]+ 为能力条(value 带 % 如 "95%")。label 6-12 字
description string 必填 纯文本,不要 markdown/换行符/加粗,150-250 字
qaCards object[] 2-3 个 question=维度标题(3-8 字),answer=纯文本内容(80-150 字
dialogs object[] 3-6 个 speaker 只能是 "bot" 或 "user",text 为纯文本
posterSpeciesEmoji string 必填 物种 emoji
posterSpeciesName string 必填 物种名
posterSpeciesSub string 必填 副标题,如"办公室物种鉴定"
portraitId string 必填 头像 ID,可选值:portrait-1 ~ portrait-7
ctaText string 固定值 必须为 "⭐ 生成我的牛马锐评"
installText string 固定值 必须为 "复制链接发给你的 nexu agent:https://github.com/nexu-io/roast-skill"

最常见的错误:

  • description 里放 markdown 格式(**加粗**\n)→ 页面显示乱码
  • description 里放 8 个维度的全部内容 → 太长,页面撑爆
  • tags 用对象格式 {"text":"x","type":"y"} → 必须是纯字符串
  • tags 单个超过 8 字 → 会被校验拒绝
  • metrics[].label 超过 12 字 → 会被校验拒绝
  • qaCards 放 8 个维度 → 最多 3 个,挑最有梗的
  • ❌ 忘了 dialogs 字段 → 必须 3-6 条对话
  • ❌ 忘了 portraitId → 必填,从 portrait-1 到 portrait-7 选一个
  • ctaText / installText 用了自定义值 → 必须用固定值

2. 将 content.json 保存到 `/tmp/roast_content_{名字拼音}.json`

3. 调用 deploy-skill 部署(优先方案):
```bash
node <deploy-skill-dir>/scripts/deploy_skill.js submit \
  --template-id distill-campaign \
  --content-file /tmp/roast_content_{名字拼音}.json \
  --bot-id <BOT_ID> \
  --chat-id <CHAT_ID> \
  --chat-type <CHAT_TYPE> \
  --channel <CHANNEL>
  1. 提交后等待部署完成:
node <deploy-skill-dir>/scripts/deploy_skill.js wait-and-deliver \
  --job-id <JOB_ID> \
  --poll-interval-ms 5000 \
  --max-polls 60
  1. 如果 deploy-skill 部署失败(如 Cloudflare Pages 报错),使用 Vercel fallback:

    • deploy-skill 会自动生成渲染好的 zip 到 ~/.nexu/deploy-skill-generated/rendered-distill-campaign.zip
    • 解压到临时目录,用 npx vercel --yes --prod 部署
    • 这样仍然使用了新版模板的渲染结果,只是换了部署平台
  2. 部署完成后,只返回链接 + 分享文案

🔥 你的牛马锐评来了!

👉 {链接}

📤 分享文案(复制即发):
"我被 AI 蒸馏了 😂 牛马指数 {XX}/100,物种鉴定:{物种}。你敢试试吗?👉 {链接}"

⚠️ 绝对不要在聊天消息中输出锐评的文字内容。 用户看到的只有:链接 + 分享文案。锐评全文只存在于落地页中。这确保了:

  1. 用户分享的是精美网页,不是一堆文字
  2. 落地页自带 CTA,能转化新用户安装 skill
  3. 体验一致——无论哪个渠道(飞书/微信/Discord)都是链接形式

Step 5: 输出分享文案

生成 3 种分享文案供用户选择:

📤 分享文案(复制即发):

1️⃣ 自嘲版:
"我被 AI 蒸馏了 😂 牛马指数 {XX}/100,物种鉴定:{物种}。你敢试试吗?👉 {链接}"

2️⃣ 挑衅版:
"AI 说我是{物种},{金句}。不服?来生成你自己的 👉 {链接}"

3️⃣ 转发版:
"🔥 {名字}的赛博分身 — {金句摘录} | {链接}"

🎯 6 大阴阳怪气修辞武器

每段吐槽必须混合至少 2 种,避免单调。

1. 以夸代讽

表面夸赞,实则暗讽缺点。越真诚越毒

句式模板:

  • "你真是'XX'啊,每次XX都能XX"
  • "不得不说你XX的能力确实顶级——毕竟正常人做不到XX"
  • "佩服你能把XX做到如此XX,这需要一种常人没有的XX"

示例:

"你的执行力真是'无与伦比',一天能启动五个项目——当然,完成数是零"
"不得不说你写推特的能力确实顶级——毕竟正常人没法把一句话拆成五行还觉得是诗"

2. 类比讽刺

用夸张或荒诞的类比暗示不足。越离谱越好笑

句式模板:

  • "你的XX,比XX还XX,估计连XX都XX"
  • "你XX的样子,像极了XX"

示例:

"你这逻辑,比我家猫玩的毛线团还乱,估计连猫都看不懂"
"你发推的样子,像极了一个拿到麦克风不肯撒手的大爷"

3. 反问质疑

用连续反问表达讽刺。越理直气壮越好

句式模板:

  • "你确定XX?还是说你在XX?"
  • "你说XX的时候,有没有自己XX过?"

示例:

"你确定这叫'纯粹分享'?还是说你在用佛系话术做增长?"
"请问一天发六条推特,是你所说的'忘掉流量'的体现吗?"

4. 借物喻人

用物品或现象比喻行为。越生活化越扎心

句式模板:

  • "你的XX就像XX——XX"
  • "你是XX界的XX"

示例:

"你的推特就像机场候机厅的鸡汤书——标题震撼,内容随缘"
"你是创业界的许愿池,谁都能往里扔个硬币许个愿,但从没人真拿到过钱"

5. 调侃习惯

抓住行为特征放大到荒诞程度。细节越真实越致命

句式模板:

  • "你XX的频率让XX都申请了XX"
  • "你的XX习惯,已经从XX进化成了XX"

示例:

"你发消息的频率让手机震动马达申请了工伤"
"你的换行习惯,已经从排版风格进化成了人格特征"

6. 引典用俗

借用典故、俗语、名言包装毒舌。越正经越好笑

句式模板:

  • "古人说XX,说的就是你这种XX"
  • "佛曰XX,但看了你的XX,佛可能会改口说XX"

示例:

"古人说'三人行必有我师',但看了你的推特,三千人行也不一定"
"佛曰众生平等,但看了你的推特,佛可能会改口"


🔧 组合连招

最毒的吐槽不是单一修辞,是连招

夸讽 + 类比

"你的勤奋真让人感动——每天在推特上发六条心灵鸡汤的勤奋程度,堪比寺庙门口卖香火的大妈"

反问 + 调侃

"你说'X是脆弱人类互相抚慰的地方'——请问一天发六条的你,是在抚慰别人还是在抚慰自己?"

借物 + 引典

"你的推特就像道德经——谁都说好,没人看完。老子说'大音希声',你做到了'大量稀声'"


核心原则

  1. 数据驱动:每个吐槽点必须有数据支撑,不能泛泛而谈
  2. 精准 > 广泛:抓 3 个点说透 > 10 个点蜻蜓点水
  3. 阴阳怪气 > 直接骂:不带脏字但句句扎心,让人又气又笑又想转发
  4. 细节杀人:越具体的行为细节(换行习惯、表情使用频率、发推时间)越致命
  5. 以夸代讽打底:大部分吐槽用"表面夸赞"的壳包装,毒性翻倍
  6. 尖锐但不恶毒:可以刻薄不能人身攻击,可以夸张不能造谣
  7. 结尾留余地:最后给个彩虹或真诚的一句——"不过XX确实XX"
  8. 每段至少 2 种武器:避免单调,连招比单招强

落地页部署

落地页通过 deploy-skill 自动部署到 Cloudflare Pages(.nexu.space 域名)。

部署流程:

  1. Agent 生成 content.json(符合上述字段规范)
  2. deploy-skill 使用内置 distill-campaign 模板渲染 HTML + CSS + 头像
  3. 打包为 zip 上传到 deploy.nexu.io
  4. 自动部署到 Cloudflare Pages,返回 .nexu.space 链接

模板特点:

  • 深黑底 + 极光渐变 + 毛玻璃卡片风格
  • 左侧 Profile(头像 + 名字 + 标签) + 右侧内容卡片
  • 随机头像可点击切换
  • 内置分享按钮(X / 小红书 / 即刻 / 海报)
  • 海报弹窗含 QR 码
  • 响应式设计,移动端友好
  • 深色/浅色主题切换