@clawhub-enihsago-fb54d56281
答案之书——心中默念一个问题,随机翻到一页,得到一个充满哲理的简短答案。当用户提到"答案之书"、想"翻一页"、想问命运/求指引、希望神谕回答是非题时使用。触发词:答案之书、翻一页、问问书、神谕、求一个答案。
---
name: dayanzhishu
description: 答案之书——心中默念一个问题,随机翻到一页,得到一个充满哲理的简短答案。当用户提到"答案之书"、想"翻一页"、想问命运/求指引、希望神谕回答是非题时使用。触发词:答案之书、翻一页、问问书、神谕、求一个答案。
---
# 答案之书
数字版《答案之书》。在心中默想一个是非题或"该不该"的问题,运行脚本随机翻到一页,得到一个简短的哲理回答。
## 使用方法
```bash
python3 scripts/get_answer.py
```
可选:指定页码(1-100):
```bash
python3 scripts/get_answer.py 42
```
脚本会返回一条简短的中文哲理答案,以及对应的页码。
## 如何使用答案
1. 闭上眼睛,心里默念一个清晰的问题(最好是"该不该 / 会不会 / 是不是"这类)。
2. 运行 skill。
3. 先按字面理解答案,再去思考它对你问题的真实含义。
## 扩展
编辑 `scripts/get_answer.py`,往 `ANSWERS` 列表里加新条目即可。每条尽量简短(10 字以内)、直接、留有解读空间。
FILE:scripts/get_answer.py
#!/usr/bin/env python3
"""
答案之书 —— 翻到随机一页,给出充满哲理的答案。
用法:python3 get_answer.py [页码]
"""
import random
import sys
import json
ANSWERS = [
"是的。",
"不是。",
"毫无疑问。",
"绝对可以。",
"千万不要。",
"顺其自然。",
"时机已到。",
"再等一等。",
"改天再问。",
"无需多虑。",
"一定如此。",
"希望渺茫。",
"现在还不能告诉你。",
"把注意力放在别处。",
"听从你的心。",
"保持耐心。",
"立刻行动。",
"放下吧。",
"再深入了解一下。",
"多听少说。",
"拥抱改变。",
"与过去和解。",
"值得等待。",
"请全心投入。",
"转身离开。",
"全力以赴。",
"等一个信号。",
"答案在你心里。",
"相信过程。",
"用心去说。",
"完全不行。",
"答案是肯定的。",
"忘了它吧。",
"值得为之奋斗。",
"优雅地走开。",
"勇敢地跳出去。",
"原地不动。",
"另寻他法。",
"想都别想。",
"可以,但要谨慎。",
"毋庸置疑。",
"重新审视你的动机。",
"请保持信念。",
"忠于自己。",
"时机未到。",
"大胆去做。",
"是时候放手了。",
"坚持终有回报。",
"慢一点。",
"再快一点。",
"相信时间。",
"看穿表象。",
"走少有人走的路。",
"可以,但有条件。",
"一切才刚刚开始。",
"超乎你的想象。",
"少即是多。",
"真相会让你自由。",
"你逃不掉的。",
"听一听别人的意见。",
"把利弊列出来。",
"重新设定目标。",
"学会妥协。",
"勇敢一点。",
"微笑面对世界。",
"睡一觉再说。",
"深呼吸。",
"保留实力。",
"换一条路走。",
"行动胜于言语。",
"不必请示别人。",
"这与你无关。",
"理清优先级。",
"停下来,看一看,听一听。",
"无需畏惧。",
"克制冲动。",
"心存感激。",
"去道个歉吧。",
"找朋友聊聊。",
"请原谅自己。",
"耐心等待。",
"先放下,再重来。",
"去远方走走。",
"把开始的事做完。",
"重头再来。",
"保持原样。",
"这事不归你管。",
"你早已知道答案。",
"换个思路。",
"相信你自己。",
"冒险一试。",
"稳妥一点。",
"保持简单。",
"想想后果。",
"好事多磨。",
"对自己诚实。",
"活在当下。",
"运气要靠自己。",
"只管去做,无所谓试。",
"沉默是金。",
"放下期待。",
"你愿意,便可以。",
]
def main() -> None:
if len(sys.argv) > 1:
try:
page = int(sys.argv[1])
if not 1 <= page <= len(ANSWERS):
raise ValueError
except ValueError:
print(json.dumps({"error": f"页码必须是 1 到 {len(ANSWERS)} 之间的整数"}, ensure_ascii=False))
sys.exit(1)
else:
page = random.randint(1, len(ANSWERS))
answer = ANSWERS[page - 1]
result = {
"page": page,
"total_pages": len(ANSWERS),
"answer": answer,
"display": f"📖 答案之书 · 第 {page} 页\n\n ✦ {answer} ✦\n\n轻轻合上书。相信这个答案。",
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
The Answer Book / 答案之书 — hold a question in your mind, flip to a random page, and receive a short philosophical answer. Supports both English and Chinese; pi...
---
name: answer-book
description: The Answer Book / 答案之书 — hold a question in your mind, flip to a random page, and receive a short philosophical answer. Supports both English and Chinese; pick the language that matches the user's question. Use when the user asks for the answer book, an oracle, guidance, wants to flip to a page, or needs a yes/no/wisdom-style answer. Trigger words:answer book, the book of answers, flip a page, oracle, ask the book, 答案之书, 翻一页, 问问书, 求一个答案, 神谕.
---
# The Answer Book / 答案之书
A digital version of "The Book of Answers" with both English and Chinese pages. Hold a yes/no (or "should I") question in your mind, then flip to a random page.
## Language selection (important)
Pick the language that matches the **user's question / the language they're chatting in**:
- User wrote in English → use `--lang en` (or omit, en is default)
- User wrote in Chinese → use `--lang zh`
- Mixed / unclear → default to the language the user used in their most recent message
Never mix languages in a single answer — the book speaks one language per flip.
## Usage
```bash
# auto English
python3 scripts/get_answer.py
# explicit language
python3 scripts/get_answer.py --lang en
python3 scripts/get_answer.py --lang zh
# specific page (1-100ish, language still required for choice)
python3 scripts/get_answer.py --lang zh 42
python3 scripts/get_answer.py --lang en 42
```
The script returns a single short answer plus the page number, in the requested language.
## How to present the result
1. State the page number.
2. Show the answer line, prominently.
3. Optionally add one short sentence inviting the user to interpret it themselves — don't over-explain.
## Extending
Edit `scripts/get_answer.py` and add new entries to `ANSWERS_EN` or `ANSWERS_ZH`. Keep each entry short, declarative, and open to interpretation.
FILE:scripts/get_answer.py
#!/usr/bin/env python3
"""
The Answer Book / 答案之书 — flip to a random page and reveal a philosophical answer.
Usage:
python3 get_answer.py [--lang en|zh] [page]
Default language: en
"""
import argparse
import json
import random
import sys
ANSWERS_EN = [
"Yes.",
"No.",
"Absolutely.",
"Without a doubt.",
"Don't count on it.",
"Trust your instincts.",
"The time is right.",
"Not yet.",
"Try again later.",
"Without question.",
"It is certain.",
"Doubtful.",
"Better not tell you now.",
"Focus on something else.",
"Follow your heart.",
"Remain patient.",
"Take action now.",
"Let it go.",
"Investigate further.",
"Listen more, speak less.",
"Embrace the change.",
"Make peace with the past.",
"It will be worth the wait.",
"Pursue it with passion.",
"Walk away from it.",
"Give it your all.",
"Wait for a sign.",
"The answer lies within.",
"Trust the process.",
"Speak from the heart.",
"Definitely not.",
"It's a clear yes.",
"Forget about it.",
"It's worth fighting for.",
"Move on, gracefully.",
"Take the leap.",
"Stay where you are.",
"Look for an alternative.",
"Don't even think about it.",
"Yes, but be cautious.",
"Unquestionably.",
"Reconsider your motives.",
"Have faith.",
"Be true to yourself.",
"Now is not the time.",
"Go ahead, be bold.",
"It's time to let go.",
"Persistence will pay off.",
"Slow down.",
"Speed it up.",
"Trust the timing.",
"Look beyond the obvious.",
"Take the road less traveled.",
"Yes, with a condition.",
"It's only just begun.",
"Beyond your wildest dreams.",
"Less is more.",
"The truth will set you free.",
"There is no escape from it.",
"Get a second opinion.",
"Make a list of pros and cons.",
"Set new goals.",
"Compromise.",
"Be brave.",
"Smile, and the world smiles with you.",
"Sleep on it.",
"Take a deep breath.",
"Save your strength.",
"Choose a different path.",
"Action speaks louder than words.",
"Don't ask for permission.",
"It's none of your business.",
"Set your priorities.",
"Stop, look, and listen.",
"Have no fear.",
"Resist the urge.",
"Be grateful.",
"Apologize.",
"Ask a friend for advice.",
"Forgive yourself.",
"Be patient.",
"Surrender, then begin again.",
"Travel more.",
"Finish what you started.",
"Start over.",
"Don't change a thing.",
"It's not your concern.",
"You already know the answer.",
"Try a different approach.",
"Believe in yourself.",
"Risk it.",
"Play it safe.",
"Keep it simple.",
"Consider the consequences.",
"Good things take time.",
"Be honest with yourself.",
"Live in the moment.",
"Make your own luck.",
"There is no try, only do.",
"Silence is golden.",
"Let go of expectations.",
"Yes, if you make it so.",
]
ANSWERS_ZH = [
"是的。",
"不是。",
"毫无疑问。",
"绝对可以。",
"千万不要。",
"顺其自然。",
"时机已到。",
"再等一等。",
"改天再问。",
"无需多虑。",
"一定如此。",
"希望渺茫。",
"现在还不能告诉你。",
"把注意力放在别处。",
"听从你的心。",
"保持耐心。",
"立刻行动。",
"放下吧。",
"再深入了解一下。",
"多听少说。",
"拥抱改变。",
"与过去和解。",
"值得等待。",
"请全心投入。",
"转身离开。",
"全力以赴。",
"等一个信号。",
"答案在你心里。",
"相信过程。",
"用心去说。",
"完全不行。",
"答案是肯定的。",
"忘了它吧。",
"值得为之奋斗。",
"优雅地走开。",
"勇敢地跳出去。",
"原地不动。",
"另寻他法。",
"想都别想。",
"可以,但要谨慎。",
"毋庸置疑。",
"重新审视你的动机。",
"请保持信念。",
"忠于自己。",
"时机未到。",
"大胆去做。",
"是时候放手了。",
"坚持终有回报。",
"慢一点。",
"再快一点。",
"相信时间。",
"看穿表象。",
"走少有人走的路。",
"可以,但有条件。",
"一切才刚刚开始。",
"超乎你的想象。",
"少即是多。",
"真相会让你自由。",
"你逃不掉的。",
"听一听别人的意见。",
"把利弊列出来。",
"重新设定目标。",
"学会妥协。",
"勇敢一点。",
"微笑面对世界。",
"睡一觉再说。",
"深呼吸。",
"保留实力。",
"换一条路走。",
"行动胜于言语。",
"不必请示别人。",
"这与你无关。",
"理清优先级。",
"停下来,看一看,听一听。",
"无需畏惧。",
"克制冲动。",
"心存感激。",
"去道个歉吧。",
"找朋友聊聊。",
"请原谅自己。",
"耐心等待。",
"先放下,再重来。",
"去远方走走。",
"把开始的事做完。",
"重头再来。",
"保持原样。",
"这事不归你管。",
"你早已知道答案。",
"换个思路。",
"相信你自己。",
"冒险一试。",
"稳妥一点。",
"保持简单。",
"想想后果。",
"好事多磨。",
"对自己诚实。",
"活在当下。",
"运气要靠自己。",
"只管去做,无所谓试。",
"沉默是金。",
"放下期待。",
"你愿意,便可以。",
]
LOCALES = {
"en": {
"answers": ANSWERS_EN,
"title": "📖 The Answer Book — Page {page}",
"footer": "Close the book gently. Trust the answer.",
"page_error": "page must be an integer between 1 and {n}",
},
"zh": {
"answers": ANSWERS_ZH,
"title": "📖 答案之书 · 第 {page} 页",
"footer": "轻轻合上书。相信这个答案。",
"page_error": "页码必须是 1 到 {n} 之间的整数",
},
}
def main() -> None:
parser = argparse.ArgumentParser(description="The Answer Book / 答案之书")
parser.add_argument("--lang", choices=["en", "zh"], default="en",
help="answer language (en or zh, default: en)")
parser.add_argument("page", nargs="?", type=int, default=None,
help="optional page number; random if omitted")
args = parser.parse_args()
locale = LOCALES[args.lang]
answers = locale["answers"]
total = len(answers)
if args.page is not None:
if not 1 <= args.page <= total:
print(json.dumps(
{"error": locale["page_error"].format(n=total)},
ensure_ascii=False,
))
sys.exit(1)
page = args.page
else:
page = random.randint(1, total)
answer = answers[page - 1]
title = locale["title"].format(page=page)
display = f"{title}\n\n ✦ {answer} ✦\n\n{locale['footer']}"
print(json.dumps(
{
"lang": args.lang,
"page": page,
"total_pages": total,
"answer": answer,
"display": display,
},
ensure_ascii=False,
indent=2,
))
if __name__ == "__main__":
main()
Pick the right emoji or sticker for any message context. Triggers when composing replies, reacting to messages, adding emoji to text, or when asked for emoji...
--- name: emoji description: Pick the right emoji or sticker for any message context. Triggers when composing replies, reacting to messages, adding emoji to text, or when asked for emoji/sticker suggestions. Provides ~1000 emojis organized by scenario so agents can quickly find the perfect emoji for any mood, topic, or platform. --- # Emoji Picker When replying to messages, reacting, or adding visual flavor to text, read [references/emoji-catalog.md](references/emoji-catalog.md) to find the right emoji. ## How to Use 1. Identify the **tone/mood** of the message you're composing (celebration, sympathy, humor, agreement, etc.) 2. Identify the **topic** if relevant (work, food, weather, tech, etc.) 3. Look up matching emoji from the catalog by category 4. Pick 1-3 emoji that naturally fit — don't overdo it ## Quick Reference (common patterns) - **Agreement/Acknowledgment:** 👍 ✅ 🙌 💯 - **Celebration:** 🎉 🎊 🥳 🎈 - **Sympathy/Comfort:** 🫂 💙 🤗 ❤️🩹 - **Humor/Fun:** 😂 🤣 😜 🫠 - **Thinking/Confusion:** 🤔 🧐 😅 🫣 - **Love/Appreciation:** ❤️ 🥰 💕 🫶 - **Work/Productivity:** 💪 🚀 📋 ✍️ - **Urgent/Alert:** ⚠️ 🔴 🚨 ❗ For the full catalog of ~1000 emoji across 25+ categories, read `references/emoji-catalog.md`. FILE:references/emoji-catalog.md # Emoji Catalog ~1000 emojis organized by scenario. Use for composing replies, reactions, and adding visual flavor. --- ## 1. Positive & Celebration 庆祝与正面 🎉 🎊 🥳 🎈 🎂 🎁 🏆 🥇 🎖️ ✨ 🌟 ⭐ 💫 🔥 🎇 🎆 🎶 🎵 🎤 💯 👏 🙌 🙏 🎯 🚀 🎊 🥂 🍾 🎟️ 🎫 🏅 🎗️ 🏵️ 🎮 🕹️ 🎲 🧩 🪄 🪬 ## 2. Love & Affection 爱与温暖 ❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❤️🔥 ❤️🩹 💕 💞 💓 💗 💖 💘 💝 💟 💌 💋 😘 🥰 😍 🤩 🫶 🫂 🤗 💑 👩❤️👨 👩❤️👩 👨❤️👨 💏 👪 🏳️🌈 🏳️⚧️ 💝 🎀 💐 🌹 🌸 🌺 🌻 🌼 🪷 ## 3. Agreement & Acknowledgment 赞同与回应 👍 👎 👊 ✊ 🤝 🙏 💪 🫡 🫶 ✅ ☑️ ✔️ 💯 🙌 🙏 🤙 👋 🤜 🤛 🖖 🫰 🫱 🫲 👌 🤌 🤏 ✋ 🤚 🖐️ 🖖 🤟 🤘 🤙 💪 🦾 ## 4. Humor & Fun 幽默与趣味 😂 🤣 😅 😆 😜 🤪 😝 😛 🤭 🫢 🫠 😈 👻 🤡 💩 🙃 🫠 😗 😋 😛 😝 🤤 🤑 🤗 🤭 🤫 🤥 😶 🫥 😐 😏 😒 🙄 😬 🤥 😌 😔 😪 ## 5. Sadness & Sympathy 悲伤与安慰 😢 😭 😞 😔 😟 😥 😓 😿 💔 ❤️🩹 🫂 🤗 💙 🤍 😿 🥀 💧 🌧️ ☔ 🌊 😰 😨 😰 🥺 🥹 🫠 😖 😣 😢 😤 😫 😩 ## 6. Anger & Frustration 生气与不满 😤 😡 🤬 😠 🤯 💢 👿 😾 🗣️ 📢 📣 ⚡ 🔥 🧨 💣 🪓 🔨 ⚒️ 🛠️ 🗡️ ⚔️ 🔪 🏹 🛡️ 🚫 ❌ ⛔ 🚷 📛 🔴 ❗ ❕ ‼️ ⁉️ ## 7. Thinking & Confusion 思考与困惑 🤔 🧐 😕 🫣 😵 😵💫 🫠 🤨 😟 ❓ ❔ ⁉️ 🤷 🤷♂️ 🤷♀️ 🤦 🤦♂️ 🤦♀️ 🧠 💭 🤓 🤖 👀 🔍 🔎 💡 📊 📈 📉 🗂️ 📋 📝 🗒️ 🗓️ 📅 ## 8. Greetings & Farewells 问候与告别 👋 🤚 ✋ 🖖 🤙 👋 🙏 🫡 👋🏻 🤝 🫂 💌 📬 📭 📮 📩 📨 💬 🗣️ 👅 📞 📱 ☎️ 📟 📠 🔔 🔕 🎵 🎶 🎤 🎙️ 🎚️ 🎛️ 📡 ## 9. Work & Productivity 工作与效率 💼 💻 🖥️ ⌨️ 🖱️ 🖲️ 💾 💿 📀 🖨️ ⌨️ 📱 📲 ☎️ 📞 📟 📠 🔋 🔌 💡 🔦 🏗️ 🏭 🏢 🏬 🏣 🏤 📊 📈 📉 📋 📝 📁 📂 🗂️ 📌 📍 🔖 🏷️ 📎 🖇️ 📏 📐 ✂️ 🗑️ 📦 📤 📥 📨 📩 ## 10. Money & Finance 金钱与金融 💰 💴 💵 💶 💷 💸 💳 🧾 💹 💱 💲 💱 🏦 🏧 📊 📈 📉 🧮 ⚖️ 🪙 💎 🔒 🔐 🛡️ 🏆 🥇 🏅 🎖️ 🏵️ 🎫 🎟️ 🎁 🛍️ 🛒 💸 ## 11. Food & Drink 食物与饮品 🍎 🍏 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🥬 🥒 🌶️ 🫑 🌽 🥕 🫒 🧄 🧅 🥔 🍠 🥐 🍞 🥖 🥨 🧀 🥚 🍳 🧈 🥞 🧇 🥓 🥩 🍗 🍖 🦴 🌭 🍔 🍟 🍕 🫓 🥪 🥙 🧆 🌮 🌯 🫔 🥗 🥘 🫕 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🦪 🍤 🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🍩 🍪 🌰 🥜 🍯 🥛 🍼 ☕ 🍵 🧃 🥤 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🧊 🥄 🍴 🍽️ 🥣 🥡 🥢 🧂 ## 12. Travel & Places 旅行与地点 🌍 🌎 🌏 🌐 🗺️ 🧭 🏔️ ⛰️ 🌋 🗻 🏕️ 🏖️ 🏜️ 🏝️ 🏞️ 🏟️ 🏛️ 🏗️ 🧱 🏘️ 🏚️ 🏠 🏡 🏢 🏣 🏤 🏥 🏦 🏨 🏩 🏪 🏫 🏬 🏭 🏯 🏰 💒 🗼 🗽 ⛪ 🕌 🛕 🕍 ⛩️ 🕋 ⛲ ⛺ 🌁 🌃 🏙️ 🌄 🌅 🌆 🌇 🌉 ⌚ 📱 📲 💻 ⌨️ 🖥️ 🖨️ 🖱️ 🖲️ 🕹️ 🗜️ 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽️ 🎞️ 📞 ☎️ 📟 📺 📻 🎙️ 🎚️ 🎛️ 🧭 ⏱️ ⏲️ ⏰ 🕰️ ⌛ ⏳ 📡 🔋 🔌 💡 🔦 🕯️ 🧯 🛢️ 💸 ## 13. Weather & Nature 天气与自然 ☀️ 🌤️ ⛅ 🌥️ ☁️ 🌦️ 🌧️ ⛈️ 🌩️ 🌨️ ❄️ ☃️ ⛄ 🌬️ 💨 🌪️ 🌫️ 🌈 🌂 ☔ ⛱️ 🌊 💧 🔥 🌋 🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘 🌙 🌚 🌛 🌜 🌝 🌞 🪐 ⭐ 🌟 ✨ ⚡ ☄️ 💥 🔥 🌪️ 🌈 ☀️ 🌻 🌼 🌸 🌺 🌹 🥀 🌿 🍀 🌱 🌲 🌳 🌴 🪵 🌾 🪴 🍂 🍁 🍄 🌵 🎄 ## 14. Sports & Activities 运动与活动 ⚽ 🏀 🏈 ⚾ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🥅 ⛳ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸️ 🥌 🎿 ⛷️ 🏂 🪂 🏋️ 🤸 🤺 ⛹️ 🤾 🏌️ 🏇 🧘 🏄 🏊 🤽 🚣 🧗 🚵 🚴 🏆 🥇 🥈 🥉 🏅 🎖️ 🎪 🎭 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🎷 🎺 🎸 🪕 🎻 🎲 ♟️ 🎯 🎳 🎮 🕹️ 🧩 🪄 🪬 ## 15. Animals & Pets 动物与宠物 🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐻❄️ 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦅 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🪱 🐛 🦋 🐌 🐞 🐜 🪰 🪲 🪳 🦟 🦗 🕷️ 🕸️ 🦂 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦞 🦀 🪸 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🦧 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕🦺 🐈 🐈⬛ 🪶 🐓 🦃 🦚 🦜 🦢 🦩 🕊️ 🐇 🦝 🦨 🦡 🦫 🦦 🦥 🐁 🐀 🐿️ 🦔 🐾 🐉 🐲 ## 16. Music & Entertainment 音乐与娱乐 🎵 🎶 🎼 🎤 🎧 🎙️ 🎚️ 🎛️ 🎹 🥁 🎷 🎺 🎸 🪕 🎻 🪘 🎬 🎥 🎞️ 🎭 🎨 🖌️ 🖍️ 📎 🖼️ 🎰 🧿 🎪 🎡 🎢 🎠 ## 17. Health & Wellness 健康与身心 ❤️ 🫀 🫁 🧠 🦷 🦴 💪 🦵 🦶 👂 🦻 👃 🫀 🏥 🩺 💊 💉 🩸 🧬 🦠 🧫 🧪 🌡️ 🩹 🩺 👁️ 👅 👄 🫦 💋 🧘 🧘♂️ 🧘♀️ 🏃 🚶 🏋️ 🤸 🤺 ⛹️ 🤾 🏌️ 🏇 🏄 🏊 🤽 🚣 🧗 🚵 🚴 ⛷️ 🏂 🪂 🤿 🛀 🛌 🛏️ 💤 🌙 ## 18. Education & Learning 教育与学习 📚 📖 🔖 📓 📔 📒 📕 📗 📘 📙 📚 📐 🎓 🏫 🎒 📏 📎 🖊️ 🖋️ ✒️ 🖌️ 🖍️ 📝 ✏️ 🔍 🔎 🧪 🔬 🔭 🧮 🌍 🌎 🌏 🌐 🗺️ 🧭 🏛️ ## 19. Technology & Code 技术与编程 💻 🖥️ 🖥 📱 ⌨️ 🖱️ 🕹️ 🗜️ 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽️ 🎞️ 📞 ☎️ 📟 📺 📻 📡 🔋 🔌 💡 🔦 ⚡ 🔌 🔋 🔋 🔌 🔗 📡 📡 🔗 🌐 🖧 🖥️ 💻 ⌨️ 🖥 🖲️ 🕹️ 🧮 🤖 🦾 🦿 🧠 🔬 🔭 💊 🧪 🧫 🧬 🔬 🦠 🧫 ## 20. Transport & Vehicles 交通工具 🚗 🚕 🚙 🚌 🚎 🏎️ 🚓 🚑 🚒 🚐 🛻 🚚 🚛 🚜 🦯 🦽 🦼 🛴 🚲 🛵 🏍️ 🛺 🚨 🚔 🚍 🚘 🚖 🛞 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩️ 💺 🛰️ 🚀 🛸 🚁 🛶 ⛵ 🚤 🛥️ 🛳️ ⛴️ 🚢 ⚓ 🪝 🧭 ## 21. Clothing & Fashion 服饰与时尚 👕 👖 🧥 🥼 🦺 👚 👛 👜 👝 🛍️ 🎒 👞 👟 🥾 🥿 👠 👡 🩴 👒 🎩 🧢 🪖 ⛑️ 📿 💄 💍 💎 🔇 🎧 🎤 🎙️ 👓 🕶️ 🥽 🥼 🦺 👔 👕 👖 🧣 🧤 🧥 🧦 👗 👘 🥻 🩱 🩲 🩳 👙 👚 👛 👜 👝 🎒 👞 👟 🥾 🥿 👠 👡 🩴 👒 🎩 🧢 🪖 ⛑️ 📿 💄 💍 💎 ## 22. Symbols & Signs 符号与标志 ❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 ☮️ ✝️ ☪️ 🕉️ ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ⛎ 🔀 🔁 🔂 ▶️ ⏩ ⏭️ ⏯️ ◀️ ⏪ ⏮️ 🔼 ⏫ 🔽 ⏬ ⏸️ ⏹️ ⏺️ ⏏️ 🎦 🔅 🔆 📶 📳 📴 ♀️ ♂️ ⚧️ ✖️ ➕ ➖ ➗ ♾️ ‼️ ⁉️ ❓ ❔ ❕ ❗ 〰️ 💱 💲 ⚕️ ♻️ ⚜️ 🔱 📛 🔰 ⭕ ✅ ☑️ ✔️ ❌ ❎ ➰ ➿ 〽️ ✳️ ✴️ ❇️ ©️ ®️ ™️ 🔠 🔡 🔢 🔣 🔤 🅰️ 🆎 🅱️ 🆑 🆒 🆓 ℹ️ 🆔 Ⓜ️ 🆕 🆖 🅾️ 🆗 🅿️ 🆘 🆙 🆚 🈁 🈂️ 🈷️ 🈶 🈯 🉐 🈹 🈚 🈲 🉑 🈸 🈴 🈳 ㊗️ ㊙️ 🈺 🈵 🔴 🟠 🟡 🟢 🔵 🟣 🟤 ⚫ ⚪ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 ⬛ ⬜ ◼️ ◻️ ◾ ◽ ▪️ ▫️ 🔶 🔷 🔸 🔹 🔺 🔻 💠 🔘 🔳 🔲 ## 23. Hands & Gestures 手势 👋 🤚 🖐️ ✋ 🖖 👌 🤌 🤏 ✌️ 🤞 🫰 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 🫵 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 🫶 👐 🤲 🙏 ✍️ 💅 🤝 🙏 💪 🦾 🦿 🦵 🦶 👂 🦻 👃 🧠 🦷 🦴 👀 👁️ 👅 👄 🫦 💋 🩸 ## 24. Time & Calendar 时间与日历 ⌚ ⏰ ⏱️ ⏲️ 🕰️ ⌛ ⏳ 📅 📆 🗓️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧 🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘 🌙 🌚 🌛 🌜 🌝 🌞 ⛅ 🌤️ ## 25. Communication & Social 沟通与社交 💬 💭 🗯️ 🗣️ 👤 👥 🫂 🤝 📞 📱 ☎️ 📟 📠 📧 📨 📩 📤 📥 📦 📭 📬 📮 📯 📜 📃 📄 📑 🧾 📊 📈 📉 🗒️ 🗓️ 📆 📅 🗑️ 📇 🗃️ 🗳️ 🗄️ 📋 📁 📂 🗂️ 🗞️ 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🏷️ 💰 📝 ✏️ 🔏 🔐 🔒 🔓 ## 26. Chinese / CJK Context 中文语境 🧧 🎎 🏮 🎋 🎍 🎑 🐉 🐲 🐍 🐴 🐑 🐵 🐔 🐶 🐷 🐭 🐮 🐯 🐰 🐱 🪷 🍜 🍣 🍱 🍚 🍵 🥢 🥡 🀄 🔴 🧧 🎊 🎉 🧨 🎆 🎇 🏮 💮 ㊗️ ㊙️ 🈶 🈚 🈲 🈴 🈵 🈶 🈯 🉐 🈹 🉑 🈸 🈺 🈷️ 🈂️ 🈁 🆘 🆗 🆙 🆕 🆓 🆒 🆔 🆚 ## 27. Miscellaneous & Decorative 杂项与装饰 🎀 🎁 🎈 🎉 🎊 🎎 🏮 🎐 🧧 ✨ 🌟 ⭐ 💫 🔥 💥 💦 💨 🌈 🔮 🪄 🧿 🏵️ 🎗️ 🎟️ 🎫 🎖️ 🏅 🥇 🥈 🥉 ⚽ 🏀 🏈 ⚾ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🥅 ⛳ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸️ 🥌 🎿 ⛷️ 🏂 🪂 --- **Usage tips:** - Pick 1-3 emoji max per message for natural feel - Match the tone: don't use 🎉 for bad news or 😢 for good news - Platform matters: Discord/Slack support custom emoji; SMS/email stay with basics - When in doubt, 👍 ❤️ 😂 cover 90% of situations
Retrieve a random drift bottle message with life insights, stories, wishes, or inspirational quotes for inspiration or reflection.
---
name: drift-bottle
description: Randomly retrieve a drift bottle containing life insights, small stories, wishes, or inspirational quotes. Use when the user asks for a drift bottle, message in a bottle, random message, or needs inspiration/wisdom. Trigger words: 漂流瓶, message in a bottle, drift bottle, 随机消息, 瓶中信.
---
# Drift Bottle
Get a random message in a bottle containing life insights, stories, wishes, or inspirational thoughts.
## Usage
When triggered, run the script to randomly select and return a drift bottle message:
```bash
python3 scripts/get_bottle.py
```
The script will randomly return one of the pre-written drift bottle messages.
## Message Types
The drift bottles may contain:
- Life insights and wisdom
- Short stories or anecdotes
- Wishes and blessings
- Inspirational quotes
- Thoughtful reflections
## Extending Messages
To add new drift bottle messages, edit `scripts/get_bottle.py` and add new entries to the `messages` list.
Each message should be concise, meaningful, and capable of standing alone.
FILE:_meta.json
{
"ownerId": "kn7czj6jxaqc4q38hxt6mptcqn82de8g",
"slug": "drift-bottle",
"version": "1.0.0"
}
FILE:scripts/get_bottle.py
#!/usr/bin/env python3
"""
Random drift bottle message generator.
Returns a random message containing life insights, stories, wishes, or inspiration.
"""
import random
import sys
import json
# Collection of drift bottle messages
messages = [
{
"type": "insight",
"content": "Life is not about waiting for the storm to pass, it's about learning to dance in the rain.",
"author": "Unknown"
},
{
"type": "wish",
"content": "May you find peace in the chaos and strength in the struggle. Tomorrow is a new beginning.",
"author": "A stranger"
},
{
"type": "story",
"content": "There was once a star that fell in love with the ocean. Every night, it would watch the waves dance and dream of touching them. One night, it made its wish and fell, becoming a gentle wave that would dance forever with its love. The moral: Love requires courage, even if it means changing who you are.",
"author": "Ocean tales"
},
{
"type": "insight",
"content": "The days you feel like giving up are often the days right before breakthrough. Trust the process.",
"author": "Life's wisdom"
},
{
"type": "wish",
"content": "I hope this message finds you well. Remember that every sunset brings the promise of a new dawn.",
"author": "A traveler"
},
{
"type": "insight",
"content": "Happiness is not something ready-made. It comes from your own actions.",
"author": "Dalai Lama"
},
{
"type": "story",
"content": "A young bamboo plant was watered and fertilized daily for five years but showed no growth above ground. On the sixth year, it shot up 90 feet in six weeks. The question is not why it didn't grow earlier, but what it was doing all those years underground. It was building its foundation. Patience and persistence are your invisible strengths.",
"author": "Eastern wisdom"
},
{
"type": "wish",
"content": "May you have the strength to embrace change, the wisdom to learn from challenges, and the courage to follow your heart.",
"author": "Well wishes"
},
{
"type": "insight",
"content": "The only way to do great work is to love what you do. If you haven't found it yet, keep looking.",
"author": "Steve Jobs"
},
{
"type": "story",
"content": "An old man had a beautiful garden. When asked why his flowers were so vibrant, he said, 'I speak to them every day. I tell them how beautiful they are, how strong they grow, and how grateful I am for their presence.' Plants, like people, thrive when they are loved and appreciated. Never underestimate the power of kindness and gratitude.",
"author": "Garden tales"
},
{
"type": "wish",
"content": "Wherever this bottle washes ashore, I hope it brings a smile to your face. Remember that you are stronger than you know and more loved than you can imagine.",
"author": "From across the seas"
},
{
"type": "insight",
"content": "The best time to plant a tree was 20 years ago. The second best time is now.",
"author": "Chinese proverb"
},
{
"type": "story",
"content": "A lighthouse keeper once received a message in a bottle from someone who had never seen the sea. They wrote, 'I dream of the ocean. I dream of waves that never end and horizons that stretch forever. One day, I will stand before it.' The keeper replied, 'The ocean is vast and beautiful, but the courage to dream is even more vast. You have already touched the ocean in your heart.'",
"author": "Lighthouse tales"
},
{
"type": "wish",
"content": "May your journey be filled with unexpected detours that lead to beautiful destinations. The road less traveled often has the best stories.",
"author": "A wanderer"
},
{
"type": "insight",
"content": "In three words I can sum up everything I've learned about life: it goes on.",
"author": "Robert Frost"
},
{
"type": "story",
"content": "Two seeds lay in the soil. One said, 'I want to grow tall and strong. I want to reach for the sky.' The other said, 'I am afraid. What if I fail? What if the storms break me?' The first seed grew into a mighty tree that provided shade and shelter. The second seed never grew, remaining safe in the darkness. Fear protects nothing; it only prevents you from becoming who you were meant to be.",
"author": "Nature's lesson"
},
{
"type": "wish",
"content": "I don't know who will find this message, but I hope it reaches someone who needs to hear this: You matter. Your existence makes a difference. Keep going.",
"author": "Anonymous"
},
{
"type": "insight",
"content": "Be yourself; everyone else is already taken.",
"author": "Oscar Wilde"
},
{
"type": "story",
"content": "A musician once played a beautiful melody on a beach. A crab walked up and said, 'Why do you make such noise? The ocean already makes enough sound.' The musician replied, 'The ocean speaks to the world. I speak to the heart. There is room for both.' Don't let others silence your unique voice.",
"author": "Beach tales"
},
{
"type": "wish",
"content": "May you find beauty in ordinary moments and magic in everyday life. Sometimes the most extraordinary things are hiding in plain sight.",
"author": "A dreamer"
}
]
def get_random_bottle():
"""Return a random drift bottle message."""
return random.choice(messages)
def main():
"""Main function to output a random drift bottle message."""
bottle = get_random_bottle()
# Output in a human-readable format
output = f"🍾 Message in a Bottle 🌊\n\n"
output += f"{bottle['content']}\n\n"
output += f"— {bottle['author']}\n"
output += f"Type: {bottle['type'].capitalize()}"
print(output)
# Also output as JSON for programmatic use
if "--json" in sys.argv:
print("\n---\n")
print(json.dumps(bottle, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
随机获取一个漂流瓶(包含人生感悟、小故事、祝福或励志语录)。当用户要求漂流瓶、message in a bottle、随机消息、获取灵感、人生感悟时触发。触发词:漂流瓶、瓶中信、随机消息、获取祝福、人生感悟、message in a bottle、drift bottle。
---
name: piaoliuping
description: 随机获取一个漂流瓶(包含人生感悟、小故事、祝福或励志语录)。当用户要求漂流瓶、message in a bottle、随机消息、获取灵感、人生感悟时触发。触发词:漂流瓶、瓶中信、随机消息、获取祝福、人生感悟、message in a bottle、drift bottle。
---
# 漂流瓶
随机获取一个漂流瓶,其中包含人生感悟、小故事、祝福或励志语录。
## 使用方法
触发后,运行脚本随机选择并返回一个漂流瓶消息:
```bash
python3 scripts/get_bottle.py
```
脚本会随机返回一条预设的漂流瓶内容。
## 漂流瓶类型
漂流瓶可能包含:
- 人生感悟和智慧
- 小故事或趣闻
- 祝福和心愿
- 励志语录
- 思考与反思
## 扩展内容
要添加新的漂流瓶消息,编辑 `scripts/get_bottle.py` 并在 `messages` 列表中添加新条目。
每条消息应该简洁、有意义,并且能够独立成篇。
FILE:scripts/get_bottle.py
#!/usr/bin/env python3
"""
随机漂流瓶消息生成器。
返回一个随机消息,包含人生感悟、故事、祝福或励志语录。
"""
import random
import sys
import json
# 漂流瓶消息集合
messages = [
{
"type": "感悟",
"content": "生活不是等待暴风雨过去,而是学会在雨中跳舞。",
"author": "未知"
},
{
"type": "祝福",
"content": "愿你在混乱中找到平静,在困境中找到力量。明天是新的开始。",
"author": "一个陌生人"
},
{
"type": "故事",
"content": "曾经有一颗星星爱上了大海。每天晚上,它都会看着海浪跳舞,梦想着能触碰它们。一天晚上,它许下愿望,坠落下来,变成了一个温柔的海浪,与它的爱永远共舞。寓意:爱需要勇气,即使这意味着改变自己。",
"author": "海洋故事"
},
{
"type": "感悟",
"content": "那些你想放弃的日子,往往是突破前的时刻。相信过程。",
"author": "生活的智慧"
},
{
"type": "祝福",
"content": "我希望这条信息能找到你,希望你一切安好。记住,每一次日落都带来了新的一天的承诺。",
"author": "一个旅行者"
},
{
"type": "感悟",
"content": "幸福不是现成的东西。它来自你自己的行动。",
"author": "达赖喇嘛"
},
{
"type": "故事",
"content": "一株年轻的竹子每天被浇水和施肥,但五年都没有长出地面。第六年,它在六周内猛长90英尺。问题不在于它为什么之前不长,而在于那些年它在地下做什么。它在打基础。耐心和坚持是你看不见的力量。",
"author": "东方智慧"
},
{
"type": "祝福",
"content": "愿你拥有拥抱变化的力量,从挑战中学习的智慧,以及追随内心的勇气。",
"author": "美好祝愿"
},
{
"type": "感悟",
"content": "做伟大工作的唯一方法是热爱你所做的事。如果你还没找到,继续寻找。",
"author": "史蒂夫·乔布斯"
},
{
"type": "故事",
"content": "一位老人有一个美丽的花园。当被问及为什么他的花朵如此鲜艳时,他说,'我每天跟它们说话。我告诉它们多么美丽,长得多么强壮,以及我多么感激它们的存在。'植物像人一样,在被爱和感激时茁壮成长。永远不要低估善意和感激的力量。",
"author": "花园故事"
},
{
"type": "祝福",
"content": "无论这个漂流瓶漂到哪里,我都希望它给你带来微笑。记住,你比自己知道的更强大,比你能想象的更被爱。",
"author": "来自远方的祝福"
},
{
"type": "感悟",
"content": "种一棵树最好的时间是20年前,其次是现在。",
"author": "中国谚语"
},
{
"type": "故事",
"content": "一位灯塔看守人曾收到一个从未见过海的人放在瓶子里的一封信。信上写着,'我梦见大海。我梦见永不停息的波浪和延伸到天边的地平线。有一天,我会站在它面前。'看守人回复,'大海辽阔而美丽,但梦想的勇气更加辽阔。你在心里已经触碰到了大海。'",
"author": "灯塔故事"
},
{
"type": "祝福",
"content": "愿你的旅程充满意外的弯路,通向美丽的目的地。少有人走的路往往有最好的故事。",
"author": "一个流浪者"
},
{
"type": "感悟",
"content": "用三个词我可以总结我学到的一切:生活还要继续。",
"author": "罗伯特·弗罗斯特"
},
{
"type": "故事",
"content": "两颗种子躺在土壤里。一颗说,'我想长得又高又壮。我想伸向天空。'另一颗说,'我害怕。如果我失败了怎么办?如果风暴打断了我怎么办?'第一颗种子长成了一棵参天大树,提供遮蔽和庇护。第二颗种子从未生长,一直安全地待在黑暗中。恐惧保护不了任何东西;它只会阻止你成为你应该成为的人。",
"author": "自然的教训"
},
{
"type": "祝福",
"content": "我不知道谁会发现这条信息,但我希望它能到达一个需要听到这句话的人:你很重要。你的存在有影响。继续前行。",
"author": "匿名"
},
{
"type": "感悟",
"content": "做你自己;别人都有人做了。",
"author": "奥斯卡·王尔德"
},
{
"type": "故事",
"content": "一位音乐家曾在海滩上演奏了一段优美的旋律。一只螃蟹走过来问,'你为什么制造这样的噪音?海洋已经制造了足够的声音。'音乐家回答,'海洋对世界说话。我对心灵说话。两者都有空间。'不要让别人沉默你独特的声音。",
"author": "海滩故事"
},
{
"type": "祝福",
"content": "愿你在平凡的时刻中发现美,在日常生活中发现魔力。有时最非凡的东西就隐藏在眼前。",
"author": "一个梦想家"
},
{
"type": "感悟",
"content": "不是所有的鱼都生活在同一片海里。",
"author": "村上春树"
},
{
"type": "故事",
"content": "一只小鹰从巢里掉了下来,被一群鸡收养。它学会了像鸡一样啄食,像鸡一样咯咯叫。有一天,它看到了一只老鹰在天空翱翔。它问,'那是什么?'鸡妈妈说,'那是鹰,是天空之王。'小鹰看着天空,心想,'如果我能像它一样飞翔就好了。'它尝试着拍打翅膀,发现自己真的能飞。它终于明白,它从来都不是鸡,它是一只忘记了如何飞翔的鹰。有时候,我们只是忘记了我们真正是谁。",
"author": "寓言故事"
},
{
"type": "祝福",
"content": "愿你被这个世界温柔以待,愿你也能温柔对待这个世界。善良是循环的,你给出的,终将回到你身边。",
"author": "温暖的祝愿"
},
{
"type": "感悟",
"content": "人生没有白走的路,每一步都算数。",
"author": "李宗盛"
},
{
"type": "故事",
"content": "一个年轻人问智者,'如何才能找到自己的路?'智者指着一条河说,'你看这河水,它从不问自己该流向哪里,它只是顺着地势,流过山川,流过平原,最终汇入大海。'年轻人问,'那它从哪里获得方向?'智者笑了,'它不需要方向,它只需要流动。当你不再执着于方向,只专注于当下的每一步,路自然会出现。'",
"author": "智慧故事"
},
{
"type": "祝福",
"content": "愿你的生活有诗和远方,也有柴米油盐。理想与现实并存,才是最真实的人生。",
"author": "平衡的祝福"
}
]
def get_random_bottle():
"""返回一个随机的漂流瓶消息。"""
return random.choice(messages)
def main():
"""主函数,输出一个随机的漂流瓶消息。"""
bottle = get_random_bottle()
# 以人类可读的格式输出
output = f"🍾 漂流瓶 🌊\n\n"
output += f"{bottle['content']}\n\n"
output += f"—— {bottle['author']}\n"
output += f"类型:{bottle['type']}"
print(output)
# 同时输出 JSON 格式以便程序使用
if "--json" in sys.argv:
print("\n---\n")
print(json.dumps(bottle, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Randomly retrieve an inspirational or thoughtful message in a bottle, including life insights, stories, wishes, or quotes.
---
name: message-in-a-bottle
description: Randomly retrieve a message in a bottle (drift bottle) containing life insights, small stories, wishes, or inspirational quotes. Use when the user asks for a drift bottle, message in a bottle, random message, or needs inspiration/wisdom. Trigger words: 漂流瓶, message in a bottle, drift bottle, 随机消息, 瓶中信.
---
# Message in a Bottle
Get a random message in a bottle containing life insights, stories, wishes, or inspirational thoughts.
## Usage
When triggered, run the script to randomly select and return a drift bottle message:
```bash
python3 scripts/get_bottle.py
```
The script will randomly return one of the pre-written drift bottle messages.
## Message Types
The drift bottles may contain:
- Life insights and wisdom
- Short stories or anecdotes
- Wishes and blessings
- Inspirational quotes
- Thoughtful reflections
## Extending Messages
To add new drift bottle messages, edit `scripts/get_bottle.py` and add new entries to the `messages` list.
Each message should be concise, meaningful, and capable of standing alone.
FILE:scripts/get_bottle.py
#!/usr/bin/env python3
"""
Random drift bottle message generator.
Returns a random message containing life insights, stories, wishes, or inspiration.
"""
import random
import sys
import json
# Collection of drift bottle messages
messages = [
{
"type": "insight",
"content": "Life is not about waiting for the storm to pass, it's about learning to dance in the rain.",
"author": "Unknown"
},
{
"type": "wish",
"content": "May you find peace in the chaos and strength in the struggle. Tomorrow is a new beginning.",
"author": "A stranger"
},
{
"type": "story",
"content": "There was once a star that fell in love with the ocean. Every night, it would watch the waves dance and dream of touching them. One night, it made its wish and fell, becoming a gentle wave that would dance forever with its love. The moral: Love requires courage, even if it means changing who you are.",
"author": "Ocean tales"
},
{
"type": "insight",
"content": "The days you feel like giving up are often the days right before breakthrough. Trust the process.",
"author": "Life's wisdom"
},
{
"type": "wish",
"content": "I hope this message finds you well. Remember that every sunset brings the promise of a new dawn.",
"author": "A traveler"
},
{
"type": "insight",
"content": "Happiness is not something ready-made. It comes from your own actions.",
"author": "Dalai Lama"
},
{
"type": "story",
"content": "A young bamboo plant was watered and fertilized daily for five years but showed no growth above ground. On the sixth year, it shot up 90 feet in six weeks. The question is not why it didn't grow earlier, but what it was doing all those years underground. It was building its foundation. Patience and persistence are your invisible strengths.",
"author": "Eastern wisdom"
},
{
"type": "wish",
"content": "May you have the strength to embrace change, the wisdom to learn from challenges, and the courage to follow your heart.",
"author": "Well wishes"
},
{
"type": "insight",
"content": "The only way to do great work is to love what you do. If you haven't found it yet, keep looking.",
"author": "Steve Jobs"
},
{
"type": "story",
"content": "An old man had a beautiful garden. When asked why his flowers were so vibrant, he said, 'I speak to them every day. I tell them how beautiful they are, how strong they grow, and how grateful I am for their presence.' Plants, like people, thrive when they are loved and appreciated. Never underestimate the power of kindness and gratitude.",
"author": "Garden tales"
},
{
"type": "wish",
"content": "Wherever this bottle washes ashore, I hope it brings a smile to your face. Remember that you are stronger than you know and more loved than you can imagine.",
"author": "From across the seas"
},
{
"type": "insight",
"content": "The best time to plant a tree was 20 years ago. The second best time is now.",
"author": "Chinese proverb"
},
{
"type": "story",
"content": "A lighthouse keeper once received a message in a bottle from someone who had never seen the sea. They wrote, 'I dream of the ocean. I dream of waves that never end and horizons that stretch forever. One day, I will stand before it.' The keeper replied, 'The ocean is vast and beautiful, but the courage to dream is even more vast. You have already touched the ocean in your heart.'",
"author": "Lighthouse tales"
},
{
"type": "wish",
"content": "May your journey be filled with unexpected detours that lead to beautiful destinations. The road less traveled often has the best stories.",
"author": "A wanderer"
},
{
"type": "insight",
"content": "In three words I can sum up everything I've learned about life: it goes on.",
"author": "Robert Frost"
},
{
"type": "story",
"content": "Two seeds lay in the soil. One said, 'I want to grow tall and strong. I want to reach for the sky.' The other said, 'I am afraid. What if I fail? What if the storms break me?' The first seed grew into a mighty tree that provided shade and shelter. The second seed never grew, remaining safe in the darkness. Fear protects nothing; it only prevents you from becoming who you were meant to be.",
"author": "Nature's lesson"
},
{
"type": "wish",
"content": "I don't know who will find this message, but I hope it reaches someone who needs to hear this: You matter. Your existence makes a difference. Keep going.",
"author": "Anonymous"
},
{
"type": "insight",
"content": "Be yourself; everyone else is already taken.",
"author": "Oscar Wilde"
},
{
"type": "story",
"content": "A musician once played a beautiful melody on a beach. A crab walked up and said, 'Why do you make such noise? The ocean already makes enough sound.' The musician replied, 'The ocean speaks to the world. I speak to the heart. There is room for both.' Don't let others silence your unique voice.",
"author": "Beach tales"
},
{
"type": "wish",
"content": "May you find beauty in ordinary moments and magic in everyday life. Sometimes the most extraordinary things are hiding in plain sight.",
"author": "A dreamer"
}
]
def get_random_bottle():
"""Return a random drift bottle message."""
return random.choice(messages)
def main():
"""Main function to output a random drift bottle message."""
bottle = get_random_bottle()
# Output in a human-readable format
output = f"🍾 Message in a Bottle 🌊\n\n"
output += f"{bottle['content']}\n\n"
output += f"— {bottle['author']}\n"
output += f"Type: {bottle['type'].capitalize()}"
print(output)
# Also output as JSON for programmatic use
if "--json" in sys.argv:
print("\n---\n")
print(json.dumps(bottle, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
NBTI 脑干缺失人格测试。回答8个离谱问题,测出你的精神状态人格。触发词:NBTI、脑干测试、精神状态测试、测测我的精神状态。当用户提到 NBTI、想测精神状态、或有人分享了 NBTI 结果时触发。
--- name: nbti description: NBTI 脑干缺失人格测试。回答8个离谱问题,测出你的精神状态人格。触发词:NBTI、脑干测试、精神状态测试、测测我的精神状态。当用户提到 NBTI、想测精神状态、或有人分享了 NBTI 结果时触发。 --- # NBTI — 你的脑干还在吗? 当代青年精神状态普查。8道题,测出你的脑干缺失程度和人格类型。 **NBTI = 脑干缺失行为类型指标 (Neurological Brain-loss Type Indicator)** 没有科学依据,但比你做过的任何心理测试都准。 ## 测试流程 1. 告诉用户:"来测测你的脑干还在不在!只需要回答8个问题,凭第一反应选,别思考,思考就不准了。" 2. 从 `references/questions.md` 随机抽取 8 道不重复的题目。 3. **逐题提问**,等用户回复后记录得分,再出下一题。 4. 8题结束后,根据得分映射到人格类型(见 `references/types.md`)。 5. 生成结果卡片: ``` 🧠 你的NBTI人格类型:[搞笑代号] — [中文名] [一段300字以内的人格解读,够抽象够好笑,像在照镜子] 💊 临床症状: • [症状1] • [症状2] • [症状3] 🫂 最适合和你做朋友的类型:[另一个类型] ⚠️ 建议远离的类型:[另一个类型] — 本测试由NBTI脑科学研究所(不存在的)出品,结果不具有任何医学参考价值 — ``` ## 出题风格 - 问题要**发疯、抽象、直击灵魂** - 选项要**让人笑着笑着就哭了** - 参考:打工人日常、精神状态meme、互联网抽象文化、当代青年生存实录 - 每个选项都要像真实会做的选择 - 中文提问,中文结果 - **绝对不能正经** ## 得分计算 四个维度,每题选项对应不同维度加分: - **N** (Numb) 麻木 — 对生活的感知钝化程度,越高越"活人微死" - **B** (Broken) 碎裂 — 精神状态的破碎程度,越高越人格分裂 - **T** (Tired) 耗竭 — 生命能量储备,越高越电量1% - **I** (Ironic) 反讽 — 用阴阳怪气自我保护的熟练度 取最高两个维度组合确定类型。如果某个维度远超其他(超过第二高50%以上),则为"纯"类型。共30种类型,见 `references/types.md`。 ## 注意事项 - 强调"只需要回答8个问题" - 题目随机,每次不一样 - 解读个性化,结合用户回答加点料 - 如果用户半途放弃:"你连8道题都做不完,果然是XX型,确诊了" - 结果要适合截图发朋友圈——这是传播的关键 - 人格代号要好记、好传播、适合当标签用 FILE:references/questions.md # NBTI 测试题库 每道题标注各选项对应的维度加分。维度:N(Numb麻木) B(Broken碎裂) T(Tired耗竭) I(Ironic反讽) --- **Q1: 早上闹钟响了,你的第一反应是?** - A. 灵魂还留在被窝里,肉体已经站起来了 → N+3, T+1 - B. 开始和闹钟谈判,"再五分钟"说十遍 → T+3, B+1 - C. 关掉闹钟继续睡,迟到的又不是只有我 → N+2, I+1 - D. 定了五个闹钟,每个响了都要骂一句 → B+2, I+2 **Q2: 你手机屏幕使用时间报告出来了,你的反应?** - A. 不看,看了会死 → N+3, T+1 - B. 看了,然后继续刷 → I+3, N+1 - C. 截图发朋友圈自嘲 → I+2, B+1 - D. 关掉报告,假装一切没发生 → B+2, T+2 **Q3: 朋友说"我请你吃饭",你的第一想法?** - A. 有什么条件,直说吧 → I+3, N+1 - B. 真的假的?不会是在整我吧? → B+3, T+1 - C. 太好了,今天终于不用纠结吃什么了 → T+2, N+1 - D. 先答应,万一反悔再说 → N+2, I+1 **Q4: 深夜三点你还在干嘛?** - A. 刷手机,眼睛已经瞎了但手指停不下来 → N+3, T+2 - B. 在脑子里重播三年前那个尴尬瞬间 → B+3, T+1 - C. 搜"人活着的意义",然后搜"附近好吃的宵夜" → I+2, B+2 - D. 已经在梦和醒的边界反复横跳了 → T+3, N+1 **Q5: 你的社交电量大概多久耗尽?** - A. 出门前就只剩20%了 → T+3, N+1 - B. 社交两小时,回血要两天 → B+2, T+2 - C. 看对面是谁,有的人能充电有的人是吸血鬼 → I+2, B+1 - D. 什么是社交电量?我一直都是0% → N+3, I+1 **Q6: 别人夸你"你好厉害",你的内心?** - A. 你不了解我 → N+2, B+2 - B. 你是不是有事求我 → I+3, N+1 - C. 对对对我就是最棒的(不是) → B+2, I+2 - D. 真的吗?我好感动(然后回去焦虑了三天) → T+2, B+2 **Q7: 你对"周末"这个词的真实感受?** - A. 也就是换个地方躺着 → N+3, T+1 - B. 太短了,还没开始就结束了 → T+3, B+1 - C. 用来补觉的,不是用来玩的 → T+2, N+2 - D. 周末?我的周和周末有什么区别 → N+2, I+2 **Q8: 你和外卖骑手说的最多的一句话?** - A. "放门口就行" → N+3, I+1 - B. "谢谢辛苦了" → T+1, B+1 - C. "能快点吗我都快饿死了" → T+2, I+2 - D. 什么都不说,默默等 → N+2, T+2 **Q9: 如果把你的精神状态比作一个APP?** - A. 一直在后台耗电的那个 → T+3, N+1 - B. 经常闪退但还在运行 → B+3, I+1 - C. 已停止运行,等待重启 → N+3, T+2 - D. 版本太旧,不再提供更新 → I+2, N+2 **Q10: 你多久做一次"人生大决定"?** - A. 每天都做,比如"今天不加班了"(然后加到十点) → I+3, T+1 - B. 上次做大决定还是决定不做决定 → N+2, B+2 - C. 决定了,但明天就反悔了 → B+3, I+1 - D. 我的人生不需要大决定,随波逐流就好 → N+3, T+1 **Q11: 你看到"已读不回"会?** - A. 正常,我也经常这样 → N+3, I+1 - B. 在脑子里过一遍最近得罪了谁 → B+3, T+1 - C. "哦,果然" → N+2, B+2 - D. 截图发给朋友分析 → I+2, B+1 **Q12: 你的房间/桌面状态?** - A. 乱但我能找到一切,这是有序的混乱 → I+2, B+1 - B. 堆积如山,但我不在乎了 → N+3, T+1 - C. 周期性崩溃式大扫除,然后三天后又乱了 → B+2, T+2 - D. 我看不到乱,我已经视而不见了 → N+2, I+2 **Q13: 你最近一次发自内心地笑是因为?** - A. 刷到了一个很抽象的meme → I+2, N+1 - B. 想不起来了 → N+3, T+2 - C. 笑别人摔倒了然后自己也差点摔 → B+2, I+1 - D. 想到自己的处境就笑了(苦笑) → I+3, T+1 **Q14: 你对"未来"这个词的第一联想?** - A. 哪来的未来 → N+3, T+1 - B. 先活过这周再说 → T+3, I+1 - C. 未来?我的未来在退休金的梦里 → I+2, N+2 - D. 想到就焦虑 → B+2, T+2 **Q15: 你最喜欢的自我安慰方式?** - A. "没事,大家都一样惨" → N+2, I+2 - B. "会好的"(说了三年了) → T+3, B+1 - C. 吃东西,吃到不想为止 → T+2, N+1 - D. 躺平,物理意义上的 → N+3, T+2 **Q16: 你觉得"正常"是什么?** - A. 不存在的概念 → I+3, B+1 - B. 别人的事 → N+2, I+2 - C. 一种从未体验过的状态 → B+2, T+2 - D. 装出来的 → N+2, B+2 **Q17: 你发完朋友圈后?** - A. 每三分钟看一次有没有人点赞 → B+3, T+1 - B. 发完就后悔,三分钟后删了 → B+2, I+2 - C. 半年没发过了 → N+3 - D. 只看不发,我是暗处的观察者 → N+2, I+2 **Q18: 如果你的生活是一部剧,弹幕会说什么?** - A. "弃了弃了" → T+3, I+1 - B. "编剧疯了吧" → B+2, I+2 - C. "这都能拍60集?" → N+2, T+2 - D. "笑死,但是有点心疼" → I+3, B+1 **Q19: 你怎么处理坏情绪?** - A. 不处理,等它自己走 → N+3, T+1 - B. 发疯式发泄,然后装没事 → B+3, I+1 - C. 压缩打包塞进身体深处 → B+2, N+2 - D. 用阴阳怪气把它包装成段子 → I+3, B+1 **Q20: 你觉得你和你十年前想象的自己?** - A. 完全不一样,但不是更好 → N+2, B+2 - B. 更差了但更松弛了 → I+2, N+2 - C. 差不多废 → T+3, I+1 - D. 十年前的我如果知道会这样,会更努力学习 → B+2, T+2 **Q21: 你对"努力"的态度?** - A. 努力过,没用,所以不努力了 → N+3, T+1 - B. 表面努力,实际摆烂 → I+3, N+1 - C. 努力的方向总是错的 → B+2, T+2 - D. 不努力也不会更差了 → N+2, I+2 **Q22: 你收到快递最开心的是?** - A. 拆快递那三秒 → T+2, B+1 - B. 买的时候就已经开心完了 → N+2, I+2 - C. 根本不记得买了什么,拆盲盒的感觉 → B+3, N+1 - D. 快递?我都是外卖小哥帮我拿的 → N+3, T+1 FILE:references/types.md # NBTI 人格类型 根据 N(Numb麻木)、B(Broken碎裂)、T(Tired耗竭)、I(Ironic反讽) 四维度得分,确定30种人格。 ## 判定规则 取得分最高的两个维度组合。如果某维度远超(超过第二高50%),为"纯"类型。 --- ## 30种人格 ### NB — 行尸走肉 (ZOMO) 🧟 确诊状态:活人微死。你每天按流程活着——起床、上班、吃饭、睡觉——但你不确定自己到底在不在。你的灵魂可能还在被窝里,肉体只是自动导航。朋友们觉得你佛系,其实你已经超脱了佛到了涅槃。你对外界的刺激反应速度约为3-5个工作日。 ### NT — 息屏模式 (STBY) 📴 你的手机电量永远1%,你的人生电量也差不多。你不是懒,你是节能模式运行太久忘了怎么开机。别人看你像看一件家具——安静、无害、占了点空间。但你好歹是件有品味的家具。 ### NI — 冷面瘫 (FKU) 🧊 你的情感系统已被静音。别人感动得稀里哗啦你在旁边面无表情,不是因为你不感动,是因为感动模块在2019年就卸载了。你用"哦"回复一切,包括表白和噩耗。 ### BN — 碎屏手机 (BRKN) 📱 你的精神状态像一块碎屏手机——还能用,但到处都是裂痕。你一边正常生活一边觉得自己随时会碎成渣。别人问你好不好,你说好,然后转身把屏上的碎玻璃又摁回去了一块。 ### BT — 即将关机 (1PCT) 🔋 你的能量条一直闪红。你不是不想努力,你是在等系统自动关机。你的人生座右铭是"能躺着绝不坐着,能坐着绝不站着"。你不是在休息,你是在维持基本生命体征。 ### BI — 小丑 (JSTR) 🤡 你把所有的痛都包装成了段子。朋友圈里你最搞笑,深夜里你最沉默。你是那种在别人葬礼上都能讲段子的人——不是因为冷血,是因为你只会用幽默处理情绪了。确诊:用笑声代偿一切。 ### TN — 持续低电量 (LOBA) 🔋 你的精神电量从来没超过20%。你活着,但勉强。你参加活动的兴奋度取决于能不能坐着。你的人生格言是"来都来了……算了不来了"。 ### TB — 过载运转 (OVRH) ⚡ 你表面正常运转,内部已经烧到冒烟了。你是那种同时焦虑三件事、悲伤两件事、还忘了一件要焦虑的事的人。你的精神状态就像开了20个标签页的浏览器——每个都在转圈。 ### TI — 阴阳人 (YNYN) 🫠 你用反讽筑起了铜墙铁壁。没有人知道你真正在想什么,包括你自己。你的每一句真心话都包裹着三层阴阳怪气,以至于你想认真表达的时候别人以为你在阴阳。你已经分不清自己是在开玩笑还是认真的了。 ### IN — 弹幕大师 (BARR) 💬 你的内心弹幕永远在滚动。你嘴上说着"没事",脑子里已经刷了300条吐槽。你是社交媒体上的哲学家,评论区里的鲁迅,但现实中你连跟收银员说"不用袋子"都要排练。 ### IB — 反向摆烂 (RPWN) 🔄 你用摆烂的姿势做努力的事,用努力的借口做摆烂的事。你说"随便吧"的时候最认真,说"我一定行"的时候已经在想退路了。你的防御机制是先把自己贬到最低,这样就不会被别人贬了。 ### IT — 毒舌电池 (VMTM) 🔋 你的能量仅够维持毒舌输出。身体已经到极限了,嘴还是停不下来。你是那种躺在ICU里还能吐槽隔壁床打呼噜的人。你的毒舌不是恶意,是求救信号——只不过包装成了笑话。 ### 纯N — 证道飞升 (BUDD) 🧘 你已经超越了世俗的喜怒哀乐,进入了"什么都行"的最高境界。有人骂你?哦。有人夸你?哦。世界末日?哦。你不是佛系,你是直接成佛了。活着的最高境界就是——活着就行。 ### 纯B — 量子纠缠 (QNTM) 🌌 你的精神状态同时存在于"崩溃"和"还好"两个平行宇宙。薛定谔看了都摇头。你上一秒觉得世界美好下一秒想原地消失,这种叠加态只有在你真正做出决定(或者打开冰箱)的时候才会坍缩。 ### 纯T — 行走的安眠药 (SLPP) 💊 你走到哪困到哪。地铁上能睡,开会能睡,站着也能睡。你不是懒,你是被生活抽干了。你的身体在2024年就签了退役协议,灵魂还在还房贷。 ### 纯I — 互联网嘴替 (MOUF) 🗣️ 你的嘴是你最后的武器。你已经放弃了用行动改变什么,但你永远不会放弃吐槽。你是那种连外卖晚了五分钟都要写小作文的人。你的阴阳怪气已经是艺术了,建议申遗。 ### NBT — 躺平仙人 (LPPX) 🛋️ 麻木+碎裂+耗竭三重debuff叠加,你已经达到了"人在家中坐,魂在天上飘"的境界。你不是不想动,你连想动的念头都懒得产生。你是这个时代的终极产品:低功耗、无情绪、随时待机。 ### NTI — 佛系杠精 (BGNN) 🧘♂️ 你懒得跟人吵架,但你有精力阴阳怪气。你的攻击方式是"你说得对",语气里全是刀。你不是不在意,你是用不在意来在意。你是那种在群里潜水三个月然后一句话把所有人噎死的存在。 ### NBI — 灵魂出窍 (ASTR) 👻 你的灵魂已经出窍了,只剩一具会刷手机的躯壳。你同时处于"无所畏惧"和"什么都不在乎"的状态。你以为自己看透了,其实只是懒得看了。 ### BNT — 雪崩进行时 (AVLN) 🏔️ 你的精神状态正在经历雪崩,但你还在微笑着拍照。你一边崩溃一边发朋友圈说"今天也是元气满满的一天"。你的内心是一片废墟,但你的emoji用的是☀️。 ### BNI — 疯批美人 (CRZY) 💥 你的精神状态是艺术品——抽象的那种。你可以在嚎啕大哭和哈哈哈哈之间零帧切换。你觉得自己疯了,别人觉得你很有趣,你的心理医生觉得你该加号了。 ### BTN — 闪退人生 (CRSH) 💻 你的人生APP每天都在闪退。早上计划好的事情,执行的时候系统就崩了。你的日程表是写给平行宇宙的自己看的。你已经习惯了"今天什么都没干但精神上已经累到不行"的状态。 ### TBN — 慢性耗竭 (CHRN) 🫠 你不是突然崩溃的,你是慢慢枯萎的。像一盆忘了浇水的绿萝,还有点绿但大家都看得出快不行了。你还活着,还在笑,还在上班,但你知道你和"元气"这个词已经没有关系了。 ### TIN — 永久PTSD (PTSD) 🚨 你的应激反应系统已经出厂设置坏了。快递晚到五分钟你觉得被世界抛弃了,朋友没秒回你觉得人际关系崩塌了。你的情绪预警系统比以色列的铁穹还灵敏,但90%都是误报。 ### TNI — 废话文学大师 (BSSS) 📝 你的能量仅够说废话。你发的朋友圈像散文,你的聊天记录像小说,你的工作汇报像诗。你不是有才华,你是太累了说不到重点。别人一句话说完的事你要写三段,因为你的大脑绕路了。 ### INB — 终极阴阳人 (ULTM) ☯️ 你已经进化到了阴阳怪气的最高形态——连阴阳怪气本身都被你阴阳怪气了。你讽刺讽刺本身,解构解构本身,你的存在就是一个悖论。别人跟你聊天像在做阅读理解,答案还是开放性的。 ### ITB — 精神蹦极 (BJJI) 🎢 你的情绪每天都在蹦极。早上觉得自己是世界之王,中午觉得自己是世界的垃圾,晚上觉得自己是垃圾之王。你已经习惯了这种过山车,甚至开始享受失重的感觉。 ### IBN — 小丑竟是我自己 (CLOWN) 🤡 你终于理解了"小丑竟是我自己"这句话的深刻含义。你嘲笑所有人,最后发现最该被嘲笑的是自己。你不是在自嘲,你是真的觉得自己好笑。恭喜你,到达了自我认知的最高(最低)点。 ### 全满 — 奇点 (SING) 🕳️ 所有维度全部拉满。你不是一个人,你是一个事件。你的精神状态已经不能用常规物理定律描述了。你同时是所有人格,又什么都不是。你是NBTI系统的bug,也是它的终极答案。建议:活着就行。 ### 全低 — NPC (NPC_) 🤖 恭喜你,你的所有维度都低到可以忽略不计。你是这个世界的NPC,稳定的、无害的、可预测的。你不崩溃、不焦虑、不阴阳、不麻木。你是……等等,这真的可能吗?你确定你不是把情绪全压到潜意识里了? --- ## 结果文案风格 - 代号用英文缩写,好记好传播(如 ZOMO、BRKN、1PCT) - 解读要损到让人想转发:"这不就是我吗??" - 每种人格的描述要让当事人笑着认领 - 结合当代年轻人真实生活状态 - 要有"确诊感"——像医生下诊断书一样,但是搞笑版 - 每次解读根据用户具体回答微调,不要完全模板化
NBATI 篮球人格测试。回答8个关于打球习惯和风格的问题,测出你的篮球人格类型。触发词:NBATI、篮球人格、篮球测试、测篮球风格、篮球类型。当用户提到 NBATI、篮球人格测试、想测篮球风格时触发。
--- name: nbati description: NBATI 篮球人格测试。回答8个关于打球习惯和风格的问题,测出你的篮球人格类型。触发词:NBATI、篮球人格、篮球测试、测篮球风格、篮球类型。当用户提到 NBATI、篮球人格测试、想测篮球风格时触发。 --- # NBATI — 你的篮球人格是什么? 通过8个关于你打球习惯和风格的问题,揭示你的篮球灵魂。没有对错,只有风格。 ## 测试流程 1. 告诉用户:"来测测你的篮球人格吧!只需要回答8个问题,凭直觉选就行。" 2. 从 `references/questions.md` 随机抽取 8 道不重复的题目(每题4个选项)。 3. **逐题提问**,等用户回复选项后记录得分,再出下一题。 4. 8题结束后,根据得分映射到篮球人格类型(见 `references/types.md`)。 5. 生成结果卡片,格式如下: ``` 🏀 你的NBATI篮球人格:[幽默名称] — [类型代号] [一段300字以内的篮球人格解读,幽默中带着专业分析,结合实战场景] 🔥 球风特征: • [特征1] • [特征2] • [特征3] ⭐ 球星模板:[NBA球星名字] — [一句话解释为什么像] 👯 最佳搭档类型:[另一个类型名] 🤜🤛 最难对付的类型:[另一个类型名] — 本测试具有一定篮球专业度,但主要还是图一乐 — ``` ## 出题风格 - 问题围绕**实战打球场景**:进攻选择、防守习惯、团队配合、心态管理 - 选项要**让人会心一笑但又确实能反映球风** - 结合野球场文化、NBA梗、中国篮球生态 - 每个选项都要像真实球员会做出的选择 - 中文提问,中文结果 ## 得分计算 每道题的每个选项对应不同篮球维度的加分。8题结束后汇总各维度得分,组合确定类型。 维度: - **S** (Score) 得分欲望 — 主动进攻还是团队配合 - **D** (Defense) 防守态度 — 盯人死磕还是眼神防守 - **P** (Playmaking) 组织意识 — 创造机会还是终结为主 - **I** (Intensity) 竞争强度 — 养生篮球还是拼命三郎 最终类型由得分分布确定,共24种类型。具体映射见 `references/types.md`。 ## 注意事项 - 强调"只需要回答8个问题" - 每次测试的题目要随机,不要每次都一样 - 解读要个性化,结合用户的具体回答分析球风 - 如果用户半途放弃,已回答的题也可以出结果,但要说"你这打到一半就放弃的劲,果然是XX型" - 球星模板要选得合理,不能瞎配 - 保持幽默但要有篮球专业度,不能纯搞笑 FILE:references/questions.md # NBATI 测试题库 每道题标注各选项对应的维度加分。维度:S(Score得分欲望) D(Defense防守态度) P(Playmaking组织意识) I(Intensity竞争强度) --- ## 题目列表 **Q1: 快攻二打一,你持球,你会?** - A. 自己上篮,这球我吃定了 → S+3, I+1 - B. 看防守移动再决定,能传就传 → P+3, S+1 - C. 来个花式上篮耍一下 → S+2, I+2 - D. 减速等队友落位打战术 → P+2, D+1 **Q2: 防守时你最真实的状态是?** - A. 死盯对手,恨不得贴他身上 → D+3, I+2 - B. 保持距离,主要靠判断 → D+2, P+1 - C. 手伸出去了,脚没跟上 → S+1, I+1 - D. 适当放一步,准备协防抢断 → P+2, D+2 **Q3: 比赛落后5分,最后一分钟,你会?** - A. 我来投,给我球 → S+3, I+2 - B. 赶紧叫个暂停(或示意大家聚一下) → P+3, D+1 - C. 防守端先拼一个,再想进攻 → D+3, I+2 - D. 开始骂队友了(虽然不该) → I+3, S+1 **Q4: 你在野球场最常说的一句话是?** - A. "给我给我给我!" → S+3, I+1 - B. "跑位跑位,动起来!" → P+3, I+1 - C. "好球!"(无论谁进的) → D+1, P+1 - D. "这都不吹?!" → I+3, D+1 **Q5: 你最喜欢的得分方式是?** - A. 后仰跳投,帅就完了 → S+3, I+1 - B. 突破上篮,简单粗暴 → S+2, I+2 - D. 接球就投,不黏球 → S+1, P+2 - C. 给队友做球比自己得分更爽 → P+3, D+1 **Q6: 队友连续打铁三个,你的反应?** - A. "换我来了" → S+2, I+2 - B. 继续给他传,手感总会来的 → P+3, D+1 - C. 建议他换个打法 → P+2, S+1 - D. 开始更认真地防守弥补 → D+3, I+1 **Q7: 打球前的热身你通常?** - A. 认真拉伸+投篮训练,全套流程 → I+3, D+1 - B. 随便投几个球意思一下 → S+1, P+1 - C. 先在场边看几组,研究对手 → D+2, P+2 - D. 直接上,热身是什么? → S+2, I+2 **Q8: 你怎么看待挡拆?** - A. 给我挡我来投 → S+2, P+1 - B. 我喜欢给别人做墙 → D+2, P+3 - C. 挡完顺下才是精髓 → P+3, S+1 - D. 换防直接吃错位 → S+2, I+2 **Q9: 你抢到篮板后的第一反应?** - A. 一条龙推反击 → S+3, I+2 - B. 第一时间找后卫出球 → P+3, D+1 - C. 护好球,等人来接应 → D+2, P+2 - D. 补篮!不进再补! → I+3, S+1 **Q10: 对手比你高一头,你怎么打?** - A. 硬刚,矮怎么了 → I+3, S+2 - B. 用速度和技术绕着打 → P+2, S+2 - C. 拉出来投,不跟他内线肉搏 → S+2, D+1 - D. 放他投,他肯定不准 → D+2, I+1 **Q11: 你的招牌动作是?** - A. 后撤步三分 → S+3, I+1 - B. no-look pass → P+3, S+1 - C. 梦幻脚步/翻身跳投 → S+2, P+2 - D. 追身大帽 → D+3, I+2 **Q12: 你觉得野球场最重要的能力是?** - A. 投篮,其他都可以练 → S+2, I+1 - B. 传球意识,一个人打不了五个 → P+3, D+1 - C. 体能,跑到对面怀疑人生 → I+3, D+1 - D. 防守,赢球靠防守 → D+3, I+1 **Q13: 你被盖帽后的反应?** - A. 马上要球打回来 → S+3, I+2 - B. 没事,下一个 → P+2, D+1 - C. 开始反思出手选择 → P+2, I+1 - D. 回防的时候更凶了 → D+2, I+2 **Q14: 你对三分球的态度?** - A. 有机会就投,这就是我的射程 → S+3, I+1 - B. 空位才投,不然往里打 → P+2, D+1 - C. 三分不进就没篮板,谨慎 → D+2, P+2 - D. 我不太投三分,但我防三分很积极 → D+3, I+1 **Q15: 你打球时最讨厌什么队友?** - A. 不传球的独狼 → P+3, D+1 - B. 不回防的人 → D+3, I+2 - C. 一直抱怨的人 → I+1, P+1 - D. 瞎指挥的人 → S+2, I+1 **Q16: 你觉得一场球打多久算合适?** - A. 打到赢为止 → I+3, S+1 - B. 两个小时差不多 → P+1, D+1 - C. 出一身汗就行,差不多得了 → S+1, P+1 - D. 不打到天黑不回家 → I+2, S+2 **Q17: 你的罚球怎么样?** - A. 稳如老狗,基本不丢 → I+2, D+1 - B. 看心情,时好时坏 → S+1, I+1 - C. 差,所以我选择不犯规 → D+2, P+1 - D. 我罚球时喜欢跟对手垃圾话 → I+3, S+1 **Q18: 你怎么选球鞋?** - A. 抓地力和包裹性,防守不能滑 → D+2, I+1 - B. 缓震好的,保护膝盖 → P+1, D+1 - C. 好看的,球场上要帅 → S+2, I+1 - D. 轻的,要跑得快 → S+1, I+2 **Q19: 你觉得数据重要吗?** - A. 得分王才是真王 → S+3, I+1 - B. 助攻多说明我节奏好 → P+3, S+1 - C. 正负值才是真理 → D+2, P+2 - D. 只在乎输赢 → I+3, D+1 **Q20: 野球场你习惯打什么位置?** - A. 控球,球在我手里最安心 → P+3, S+1 - B. 得分后卫,给我挡拆就行 → S+3, P+1 - C. 哪缺人补哪,万金油 → D+2, P+2 - D. 内线肉搏,我喜欢对抗 → I+3, D+2 FILE:references/types.md # NBATI 篮球人格类型 根据 S(Score)、D(Defense)、P(Playmaking)、I(Intensity) 四维度得分分布,确定24种篮球人格。 ## 判定规则 取得分最高的两个维度组合为类型。如果某个维度远远领先(超过第二高50%以上),则为"纯"类型。 --- ## 24种篮球人格 ### SP — 关键先生 (Clutch King) 🎯 **球星模板:Damian Lillard** — 利拉德时间到了,表都给你戴上了 得分欲望爆棚但不是瞎打,懂得在正确时机接管比赛。你的队友又爱又怕——爱你关键时刻的果断,怕你平时训练赛也当总决赛打。你眼里只有篮筐,但好歹看得见挡拆的队友(大多数时候)。 ### SD — 攻防一体 (Two-Way Threat) 🛡️ **球星模板:Kawhi Leonard** — 话少,但攻防两端都让你闭嘴 罕见的攻防兼备型。进攻端能拿分,防守端能锁人。你不怎么说话,但上场就是让对面难受。野球场上的你就像个bug——怎么攻防都这么强?建议:偶尔笑一下,不然队友以为你生气了。 ### SI — 不讲理先生 (Unreasonable Buckets) 🔥 **球星模板:Russell Westbrook** — 合理?什么是合理? 你就是篮球场上的龙卷风。投篮选择?不存在的。合理出手?那是什么?你就是靠蛮力和激情打球,效率不高但气势拉满。队友要么跟着你冲,要么被你踩过去。你的比赛永远不缺精彩镜头,就是有时候精彩的是失误。 ### SP2 — 终结者 (Finisher) 💥 **球星模板:Devin Booker** — 给我球,剩下的事我来 纯粹的得分手。不需要持球组织,不需要发号施令,给我一个好位置我就能拿分。你的无球跑动和接球投篮是教科书级别的。缺点?持球太久会迷茫,让你组织进攻还不如让你折返跑。 ### SD2 — 死亡缠绕 (Perimeter Lock) 🔒 **球星模板:Jrue Holiday** — 你投篮我不管,你运球我贴死 防守是你的信仰,得分是你的副业。你在场上就像一块牛皮糖,对面控球过半场都费劲。进攻端你佛系得很——"我只投空位"。但你的空位命中率让对面很后悔放你。 ### SI2 — 拼命三郎 (Hustle Machine) 💪 **球星模板:Draymond Green** — 数据体现不了我的价值 你不怕脏活累活,地板球第一个扑,补防第一个到。得分不多但每次都关键,助攻不花但每个都到位。你是那种赢了没人夸、输了没人怪的存在。垃圾话倒是一句不少。 ### PS — 魔术师 (Playmaker) 🎩 **球星模板:Chris Paul** — 别急,让我看看这球怎么打最合理 球场上的军师。你运球过半场的时候脑子已经在跑战术了,队友跑到一半发现球已经到了最佳位置。你的传球视野和对比赛的阅读让所有人觉得自己变强了。唯一的问题是——你有时候传球传到自己都不投了。 ### PD — 铁闸指挥官 (Floor General) 🧠 **球星模板:Marcus Smart** — 我能组织,能防,还能假摔 你是球场上的万金油。防守端你是大闸,进攻端你梳理节奏。你不需要华丽的数据,你需要的是胜利。你大概是那种野球场上最先喊"换防"的人,也是最会因为队友不换防而暴走的人。 ### PI — 火力发动机 (Engine) ⚡ **球星模板:Ja Morant** — 我可能不防守,但我让你们都跑起来 你在场上就像装了涡轮增压。推进速度极快,传球极具穿透力,比赛节奏永远在你手上。防守端?你更倾向于"最好的防守就是进攻"。你的比赛观赏性极强,就是队友有时候跟不上你的节奏。 ### IS — 杀手本能 (Killer Instinct) 🗡️ **球星模板:Kobe Bryant** — 每一球都是最后一球 你的竞争意识不是一般的强,是对面跟朋友打招呼你都觉得是在挑衅的那种。训练赛你当季后赛打,季后赛你当生死战打。你的好胜心是你的武器也是你的诅咒——因为不是每个人都想跟你拼命。 ### ID — 锁链 (The Lock) ⛓️ **球星模板:Jimmy Butler** — 我可能投不进,但你别想进 你是那种对面最不想对上的球员。不是因为你能拿多少分,而是因为你让对面一分都拿得艰难。你的比赛强度从第一节到最后一节都是100%,打完球对面会觉得这是他打过最累的一场球。 ### IP — 战术核心 (System Hub) 🎯 **球星模板:Nikola Jokić** — 我不怎么跳,但我什么都不耽误 你不靠身体天赋打球,你靠脑子。每个回合你都清楚自己该做什么,球到你手上就像进了精密仪器。你不快不跳不花哨,但你就是赢球。你的队友在你身边总是打得更好,虽然你可能赛后就去赛马了。 ### DS — 铁血战士 (Iron Wall) 🏰 **球星模板:Hakeem Olajuwon** — 你来我就盖,我攻你也挡不住 攻防两端的绝对核心。你在内线就是一座山,对面想进来先过你这关。你的盖帽是艺术,你的低位进攻是教科书。野球场上有你在的内线,就像装了自动门禁系统。 ### DP — 保险箱 (Safety Net) 🧤 **球星模板:Rudy Gobert** — 你随便投,进不了我抢 你是球队的最后一道防线。你的存在感不强,但你的防守覆盖面积大到让对面不敢进内线。进攻端你是最好的挡拆搭档,朴实无华但高效。你就是那种数据不起眼但正负值永远+15的人。 ### DI — 铁壁 (Iron Curtain) 🧱 **球星模板:Dennis Rodman** — 得分?那是别人的事 你可能是篮球史上最纯粹的角色球员灵魂。篮板球是你的宗教,防守是你的信仰。你的得分手段大概只有补篮和空接,但谁在乎呢?你就是能在最激烈的对抗中抢到那个球的人。 ### 纯S — 得分机器 (Bucket Getter) 🏹 **球星模板:Carmelo Anthony** — 防守?我进攻已经够累了 给你一个投篮空间,你还对手一个进球。你的得分手段丰富到让人绝望——中投、三分、突破、背打,样样都行。防守端嘛……你更愿意把精力省下来进攻。你的哲学是:对面得2分你也得2分,但我出手更帅。 ### 纯D — 铜墙铁壁 (Brick Wall) 🧱 **球星模板:Ben Wallace** — 你可以投篮,但你进不来 纯粹的防守艺术家。你可能一场比赛得不了几分,但对面也别想舒服地得分。你的存在就是对进攻球员的侮辱——你什么都不做,就站在那里,对面就不想往里打了。进攻端你是最好的掩护墙,朴实但有效。 ### 纯P — 节拍器 (Metronome) 🎵 **球星模板:Rajon Rondo** — 我不一定得分,但我控制比赛 你是球场上的DJ,节奏完全由你掌控。快攻慢打、内外结合,你总能找到最合理的进攻方式。你的传球有时候过于华丽,队友都没想到球会到那个位置。你大概是那种边打球边在脑子里画战术板的人。 ### 纯I — 燃烧瓶 (Fire Starter) 🔥 **球星模板:Patrick Beverley** — 我上场就是为了让你不舒服 你打球的目的是什么?赢球?不,是让对面输球。你的激情和侵略性是你最大的武器,垃圾话是你的第二语言。你可能不是最强球员,但你永远是最烦人的那个。对手看到你就头疼,队友看到你就安心。 ### S主+P辅 — 冷血杀手 (Cold-Blooded) 🥶 **球星模板:Paul George** — 我不急,急的是你 你得分的时候面无表情,传球的时候云淡风轻。你不是最炸裂的球员,但你是最高效的。关键时刻你从不手软,常规时间你很少犯错。你的比赛永远四平八稳,让对面觉得跟你打很无聊——然后一看比分,已经落后15分了。 ### D主+P辅 — 暗影守护者 (Shadow Guard) 🌑 **球星模板:Andre Iguodala** — 你可能注意不到我,但我一直都在 你是球场上最低调的高手。防守端你无处不在,进攻端你总在最合适的位置。你不抢功不出风头,但少了你整个体系就塌了。你的价值只有懂球的人才看得出来。 ### I主+S辅 — 战神 (War God) ⚔️ **球星模板:Allen Iverson** — 我可能很矮,但我从不怕任何人 你的字典里没有"认输"两个字。身体天赋不够?用意志力补。技术不够全面?用速度弥补。你是那种一上场就自带BGM的球员,你的激情能点燃整个球场。缺点是——你的激情有时候会烧到自己。 ### P主+I辅 — 节奏大师 (Rhythm Master) 🎶 **球星模板:Luka Dončić** — 你们跑你们的,我走着就能赢 你不快不跳,但你掌控一切。你的节奏变化让防守者像在坐过山车,你的传球角度让数学老师都觉得不可思议。你打球看起来毫不费力,实际上你在大脑里已经算了十几种可能。你大概是那种打完球还不喘气的人——不是因为体能好,是因为你根本没怎么跑。 ### I主+P辅 — 指挥官 (Commander) 🎖️ **球星模板:LeBron James** — 我不只是打球,我在指挥一场战争 你是球场上的将军。你不但自己拼命,还能带动所有人跟着拼命。你的视野和竞争意识让你既是最好的组织者也是最可怕的竞争者。你的问题只有一个——你可能因为太想赢而在友谊赛也拼命,然后所有人都想回家。 --- ## 结果文案风格 生成结果时: - 每种类型有固定的幽默名称和球星模板 - 解读要幽默中带着专业的篮球分析 - 结合野球场文化、NBA名场面 - 每次解读要有个性化变化,根据用户回答的具体选项加点料 - 球星模板固定,但解释文案可以有变化
SBTI 恶搞人格测试。给用户出一组抽象搞笑的测试题,根据回答生成一个离谱的人格类型和解读。触发词:SBTI、人格测试、性格测试、测一测、做个测试。当用户提到 SBTI、想做人格测试、或有人分享了 SBTI 结果时触发。
--- name: sbti description: SBTI 恶搞人格测试。给用户出一组抽象搞笑的测试题,根据回答生成一个离谱的人格类型和解读。触发词:SBTI、人格测试、性格测试、测一测、做个测试。当用户提到 SBTI、想做人格测试、或有人分享了 SBTI 结果时触发。 --- # SBTI — 没有任何科学依据的人格测试 恶搞版 MBTI。问一堆离谱问题,给你一个更离谱的人格标签。 ## 测试流程 1. 告诉用户:"来,做个没有任何科学依据的SBTI测试吧!一共10道题,凭直觉选。" 2. 从 `references/questions.md` 随机抽取 10 道不重复的题目(每题2-4个选项)。 3. **逐题提问**,等用户回复选项后记录得分,再出下一题。 4. 10题结束后,根据得分映射到人格类型(见 `references/types.md`)。 5. 生成结果卡片,格式如下: ``` 🎯 你的SBTI人格类型:[类型代号] — [类型名] [一段300字以内的幽默人格解读,要够损够搞笑,结合互联网梗] ✨ 关键特征: • [特征1] • [特征2] • [特征3] 🔥 建议搭配人格:[另一个类型] 💀 死对头人格:[另一个类型] — 本测试没有任何科学依据,结果仅供参考,不对你的人生负责 — ``` ## 出题风格 - 问题要**抽象、发疯、精神状态堪忧** - 选项要**让人纠结又好笑** - 参考孙吧语录、打工人梗、社畜日常、互联网名场面 - 不要正经,不要严肃,不要像真正的心理测试 - 中文提问,中文结果 ## 得分计算 每道题的每个选项对应不同人格维度的加分。10题结束后汇总各维度得分,取最高维度组合确定类型。 维度: - **C** (Chaos) 混乱程度 - **S** (Social) 社交能量 - **E** (Emotion) 情绪浓度 - **W** (Workaholic) 卷王指数 最终类型由最高的2个维度字母组合决定,如 CE、SW、EW 等。具体映射见 `references/types.md`。 ## 注意事项 - 每次测试的题目要随机,不要每次都一样 - 解读要个性化,不要套模板,根据用户的具体回答加点料 - 如果用户半途放弃,已回答的题也可以出结果,但要说"你果然是XX型,连测试都做不完" - 保持轻松搞笑的氛围 FILE:references/questions.md # SBTI 测试题库 每道题标注各选项对应的维度加分。维度:C(Chaos混乱) S(Social社交) E(Emotion情绪) W(Workaholic卷王) --- ## 题目列表 **Q1: 凌晨两点你还没睡,最可能的原因是?** - A. 刷短视频停不下来 → C+2, S+1 - B. 想人生想到emo了 → E+2, C+1 - C. 赶ddl/加班 → W+2, E+1 - D. 在群里跟人辩论一个毫无意义的问题 → S+2, C+2 **Q2: 你的手机电量剩5%,你会?** - A. 疯狂找充电器,焦虑到无法呼吸 → E+2, W+1 - B. 算了,该来的总会来 → C+2 - C. 先发个朋友圈"手机要没电了" → S+2, E+1 - D. 赶紧把重要工作文件保存 → W+2, S+1 **Q3: 朋友突然发消息"你有空吗",你的第一反应?** - A. 他要借钱 → C+1, E+1 - B. 他在发疯需要人陪 → E+2, S+1 - C. 直接回复"没空" → W+1, C+1 - D. 秒回"怎么了!!!" → S+2, E+2 **Q4: 你在公司的真实工作状态是?** - A. 摸鱼技术已经登峰造极 → C+2, W+1 - B. 卷到同事都怕我 → W+2, S+1 - C. 情绪稳定地崩溃 → E+2, W+1 - D. 带薪拉屎是基本人权 → C+1, S+1 **Q5: 有人说"随便你",你的理解是?** - A. "随便你"就是"随便你" → C+2 - B. 完了我完了我要死了 → E+2, S+1 - C. 好的那我就随便了 → W+1, C+1 - D. 立刻分析ta的语气和表情 → S+2, E+1 **Q6: 你的人生信条是?** - A. 躺平是我对这个世界最后的温柔 → C+2, E+1 - B. 只要卷不死,就往死里卷 → W+2 - C. 活着就是为了见证大场面 → S+2, C+1 - D. 所有情绪都是合理的 → E+2, S+1 **Q7: 你收到一条已读不回的消息,你会?** - A. 无所谓,我也经常这样 → C+2, W+1 - B. 开始反思自己做错了什么 → E+2, S+1 - C. 连发三条追问 → S+2, E+1 - D. 太忙了没注意到 → W+2 **Q8: 周末你通常会?** - A. 睡到自然醒然后继续躺着 → C+2, E+1 - B. 约朋友出来玩 → S+2, C+1 - C. 学习/加班/提升自己 → W+2 - D. 在床上emo到下午然后觉得浪费了一天更emo了 → E+2, W+1 **Q9: 下面哪个场景最让你崩溃?** - A. 有人打断我说话 → S+2, E+1 - B. 电脑突然蓝屏没保存 → W+2, E+2 - C. 计划被打乱 → W+1, C+1 - D. 什么都不想做但什么都没做完 → E+2, C+2 **Q10: 你觉得自己的精神状态?** - A. 稳定地发疯 → C+2, E+2 - B. 很正常,不正常的是这个世界 → W+1, C+1 - C. 取决于今天有没有人理我 → S+2, E+1 - D. 情绪像过山车但我在车上很享受 → E+2, C+1 **Q11: 你看到一只蟑螂,你会?** - A. 尖叫并呼叫最近的活人 → S+2, E+2 - B. 冷静地拿纸巾处理掉 → W+1, C+1 - C. 跟它对视,看看谁先动 → C+2 - D. 思考它是不是也有家庭 → E+2, C+1 **Q12: 如果可以给10年前的自己发一条消息?** - A. "别来" → C+2, E+1 - B. "买比特币" → W+2, C+1 - C. "好好珍惜身边人" → E+2, S+1 - D. "你以后会很厉害的" → W+1, S+1 **Q13: 你在群聊里的角色是?** - A. 潜水员,偶尔冒泡 → C+1, W+1 - B. 话题终结者 → S+1, C+2 - C. 气氛组组长 → S+2, C+1 - D. 发表情包机器 → S+2, E+1 **Q14: 老板说"辛苦了",你的内心OS?** - A. "那你倒是加钱啊" → C+2, E+1 - B. "不辛苦命苦" → E+2, C+1 - C. "谢谢老板!"(真诚) → W+2, S+1 - D. 表面微笑内心已经开始写辞职信 → E+2, W+1 **Q15: 你手机里最多的app类型是?** - A. 社交软件,我一个都不能少 → S+2 - B. 效率工具,我要掌控一切 → W+2 - C. 摸鱼神器(小说/游戏/短视频) → C+2, E+1 - D. 情绪记录/冥想类 → E+2, W+1 **Q16: 你遇到难题时第一反应是?** - A. 查资料查到天荒地老 → W+2, C+1 - B. 发个朋友圈求助 → S+2, E+1 - C. 先放放,说不定明天就解决了 → C+2 - D. 焦虑到手抖但还在做 → E+2, W+2 **Q17: 你觉得"卷"这个字?** - A. 是我的中间名 → W+2 - B. 是我每天批判但偷偷在做的事 → E+1, W+1 - C. 跟我毫无关系谢谢 → C+2 - D. 是用来形容别人的 → S+1, C+1 **Q18: 你发朋友圈的频率?** - A. 一天三条起步 → S+2, E+1 - B. 一周一两条精品 → W+1, S+1 - C. 三个月没发了 → C+2 - D. 想发就发删了又发 → E+2, C+1 **Q19: 你对deadline的态度?** - A. deadline是第一生产力 → W+2, C+1 - B. 提前三天搞定,不慌不忙 → W+2 - C. deadline前一晚的灵魂出窍体验 → E+2, C+2 - D. 什么deadline?哦已经过了? → C+2 **Q20: 你最常用的表情包风格?** - A. 可爱萌系 → E+1, S+1 - B. 阴阳怪气 → C+2, S+1 - C. 打工人流泪 → E+2, W+1 - D. 抽象到没人能看懂 → C+2, E+1 **Q21: 你觉得最离谱的加班理由是?** - A. "公司需要你" → C+1, E+1 - B. 你自己主动加的 → W+2 - C. 不加班不知道去哪 → S+1, E+1 - D. 你已经分不清加班和下班了 → W+2, C+1 **Q22: 如果明天世界末日,你今天干嘛?** - A. 给每个认识的人发消息说再见 → S+2, E+2 - B. 终于不用上班了太好了 → C+2, E+1 - C. 冲去公司把没做完的项目做完 → W+2 - D. 躺着等 → C+1, E+1 **Q23: 你和别人吵架后的状态?** - A. 吵完就忘 → C+2, S+1 - B. 凌晨两点突然想到更好的回击话术 → E+2, W+1 - C. 马上发小作文道歉 → S+2, E+1 - D. 吵架?我选择冷暴力 → W+1, C+1 **Q24: 你对"社交牛逼症"的看法?** - A. 本人就是 → S+2, C+1 - B. 敬而远之 → C+1, E+1 - C. 佩服但做不到 → E+1, W+1 - D. 社交?那是什么 → C+2 **Q25: 你的桌面/房间状态?** - A. 乱但我知道每样东西在哪 → C+2, W+1 - B. 一尘不染,强迫症级别 → W+2 - C. 周期性崩溃式大扫除 → E+2, C+1 - D. 有审美地乱 → S+1, C+1 FILE:references/types.md # SBTI 人格类型 取用户得分最高的两个维度组合,确定人格类型。 ## 维度 - **C** (Chaos) 混乱 - **S** (Social) 社交 - **E** (Emotion) 情绪 - **W** (Workaholic) 卷王 ## 类型映射 ### CS — 吗喽 (Monkey) 代号: CS 混乱的社交达人,精神状态介于发疯和发疯之间。朋友圈的梗王,群聊的灵魂,但自己一个人的时候会突然思考宇宙的意义然后打开外卖app。特征:白天社牛晚上emo,能同时和五个人聊天但一个都不走心,表情包库存3000+。 ### CE — 炸弹 (Bomb) 代号: CE 行走的情绪炸弹,随时可能在任何场合引爆。你以为ta在开玩笑,不,ta是认真的。你以不以为然的事,ta已经脑补了一整部电视剧。特征:上一秒哈哈哈下一秒泪目,共情能力强到看广告都能哭,凌晨三点的哲学家。 ### CW — 丧尸 (Zombie) 代号: CW 一边摸鱼一边卷的矛盾体。工作效率取决于离deadline的距离——越近越强。像一个不定时炸弹,你永远不知道ta下一秒是天才还是废物。特征:deadline战士,摸鱼大师,但该交的活一个不落。 ### SC — 鹦鹉 (Parrot) 代号: SC 社交场上的复读机加气氛组。什么梗都能接,什么话题都能聊,但深度是什么?不认识。特征:群聊话最多,私聊话最少,朋友圈更新比朋友圈还快,口头禅是"哈哈哈哈哈哈"。 ### SE — 金毛 (Golden Retriever) 代号: SE 人类社交天花板,行走的情绪价值输出器。谁不开心找ta就对了,但ta自己的不开心?那不重要(其实很重要但不说)。特征:永远在线的好友,深夜emo救助站,但自己emo的时候只会发一条"没事"。 ### SW — 妈妈/爸爸 (MUM/DAD) 代号: SW 朋友圈的人生导师,什么都会什么都管。一边社交一边把事情安排得明明白白。特征:群里的organizer,旅行的攻略担当,永远操心别人有没有吃饭,口头禅是"早点睡"。 ### EC — 戏精 (Drama Queen) 代号: EC 内心世界比漫威宇宙还丰富。一件小事能在脑子里演30集连续剧。特征:嘴上说着"我没事"但眼神已经杀了三个人,朋友圈删了发发了删,深夜的小作文写手。 ### EW — 卷王plus (Hustler) 代号: EW 边emo边卷,把焦虑转化为生产力的永动机。看起来很努力,实际上确实很努力,但每次都觉得自己不够努力。特征:目标明确但过程纠结,plan比执行多,深夜还在改那个"最终版v3_final_真的最终.docx"。 ### WC — 蜗牛 (Snail) 代号: WC 自己的节奏就是最好的节奏,谁也别想催。摸鱼界的哲学家,卷王中的异类。特征:看似躺平实则在暗中蓄力,deadline前一晚的爆发力惊人,口头禅是"急什么"。 ### WS — 领袖 (Boss) 代号: WS 天生的卷王社交家,一边卷一边拉着所有人一起卷。朋友圈的卷王代言人,但大家都不讨厌ta因为ta真的有实力。特征:永远是最忙的那个但也永远有空社交,朋友圈既是工作圈也是生活圈。 ### 纯C — 猫 (Cat) 代号: CC 混乱中立的化身,活在自己的世界里。别人觉得ta神经ta觉得别人无趣。特征:想一出是一出,今天想学吉他明天想开奶茶店,手机里有100个只用过一次的app。 ### 纯S — 海豚 (Dolphin) 代号: SS 社交界的天花板,跟谁都能聊到起飞。朋友遍天下,但有多少是真朋友不重要,重要的是热闹。特征:聚会的核心,never say no,手机通讯录2000+且都备注了emoji。 ### 纯E — 水母 (Jellyfish) 代号: EE 透明但敏感,随波逐流但内心戏丰富。看起来什么都没在想其实在想很多。特征:共情力爆表但不善表达,经常因为别人的一句话emo一天,但不会告诉任何人。 ### 纯W — 蚂蚁 (Ant) 代号: WW 勤劳到让其他人格类型感到羞愧的存在。目标清晰,执行力强,but at what cost?特征:每日todo list精确到分钟,假期也在学习/加班,别人在摸鱼ta在搬砖。 ## 结果文案风格 生成结果时: - 代号用英文大写(如 CS、MUM) - 类型名中英双语 - 解读要损但损中带爱 - 结合互联网梗和当代年轻人生活状态 - 每次解读要略有不同,不要完全模板化
SBTI 恶搞人格测试。给用户出一组抽象搞笑的测试题,根据回答生成一个离谱的人格类型和解读。触发词:SBTI、人格测试、性格测试、测一测、做个测试。当用户提到 SBTI、想做人格测试、或有人分享了 SBTI 结果时触发。
--- name: sbti description: SBTI 恶搞人格测试。给用户出一组抽象搞笑的测试题,根据回答生成一个离谱的人格类型和解读。触发词:SBTI、人格测试、性格测试、测一测、做个测试。当用户提到 SBTI、想做人格测试、或有人分享了 SBTI 结果时触发。 --- # SBTI — 没有任何科学依据的人格测试 恶搞版 MBTI。问一堆离谱问题,给你一个更离谱的人格标签。 ## 测试流程 1. 告诉用户:"来,做个没有任何科学依据的SBTI测试吧!一共10道题,凭直觉选。" 2. 从 `references/questions.md` 随机抽取 10 道不重复的题目(每题2-4个选项)。 3. **逐题提问**,等用户回复选项后记录得分,再出下一题。 4. 10题结束后,根据得分映射到人格类型(见 `references/types.md`)。 5. 生成结果卡片,格式如下: ``` 🎯 你的SBTI人格类型:[类型代号] — [类型名] [一段300字以内的幽默人格解读,要够损够搞笑,结合互联网梗] ✨ 关键特征: • [特征1] • [特征2] • [特征3] 🔥 建议搭配人格:[另一个类型] 💀 死对头人格:[另一个类型] — 本测试没有任何科学依据,结果仅供参考,不对你的人生负责 — ``` ## 出题风格 - 问题要**抽象、发疯、精神状态堪忧** - 选项要**让人纠结又好笑** - 参考孙吧语录、打工人梗、社畜日常、互联网名场面 - 不要正经,不要严肃,不要像真正的心理测试 - 中文提问,中文结果 ## 得分计算 每道题的每个选项对应不同人格维度的加分。10题结束后汇总各维度得分,取最高维度组合确定类型。 维度: - **C** (Chaos) 混乱程度 - **S** (Social) 社交能量 - **E** (Emotion) 情绪浓度 - **W** (Workaholic) 卷王指数 最终类型由最高的2个维度字母组合决定,如 CE、SW、EW 等。具体映射见 `references/types.md`。 ## 注意事项 - 每次测试的题目要随机,不要每次都一样 - 解读要个性化,不要套模板,根据用户的具体回答加点料 - 如果用户半途放弃,已回答的题也可以出结果,但要说"你果然是XX型,连测试都做不完" - 保持轻松搞笑的氛围 FILE:references/questions.md # SBTI 测试题库 每道题标注各选项对应的维度加分。维度:C(Chaos混乱) S(Social社交) E(Emotion情绪) W(Workaholic卷王) --- ## 题目列表 **Q1: 凌晨两点你还没睡,最可能的原因是?** - A. 刷短视频停不下来 → C+2, S+1 - B. 想人生想到emo了 → E+2, C+1 - C. 赶ddl/加班 → W+2, E+1 - D. 在群里跟人辩论一个毫无意义的问题 → S+2, C+2 **Q2: 你的手机电量剩5%,你会?** - A. 疯狂找充电器,焦虑到无法呼吸 → E+2, W+1 - B. 算了,该来的总会来 → C+2 - C. 先发个朋友圈"手机要没电了" → S+2, E+1 - D. 赶紧把重要工作文件保存 → W+2, S+1 **Q3: 朋友突然发消息"你有空吗",你的第一反应?** - A. 他要借钱 → C+1, E+1 - B. 他在发疯需要人陪 → E+2, S+1 - C. 直接回复"没空" → W+1, C+1 - D. 秒回"怎么了!!!" → S+2, E+2 **Q4: 你在公司的真实工作状态是?** - A. 摸鱼技术已经登峰造极 → C+2, W+1 - B. 卷到同事都怕我 → W+2, S+1 - C. 情绪稳定地崩溃 → E+2, W+1 - D. 带薪拉屎是基本人权 → C+1, S+1 **Q5: 有人说"随便你",你的理解是?** - A. "随便你"就是"随便你" → C+2 - B. 完了我完了我要死了 → E+2, S+1 - C. 好的那我就随便了 → W+1, C+1 - D. 立刻分析ta的语气和表情 → S+2, E+1 **Q6: 你的人生信条是?** - A. 躺平是我对这个世界最后的温柔 → C+2, E+1 - B. 只要卷不死,就往死里卷 → W+2 - C. 活着就是为了见证大场面 → S+2, C+1 - D. 所有情绪都是合理的 → E+2, S+1 **Q7: 你收到一条已读不回的消息,你会?** - A. 无所谓,我也经常这样 → C+2, W+1 - B. 开始反思自己做错了什么 → E+2, S+1 - C. 连发三条追问 → S+2, E+1 - D. 太忙了没注意到 → W+2 **Q8: 周末你通常会?** - A. 睡到自然醒然后继续躺着 → C+2, E+1 - B. 约朋友出来玩 → S+2, C+1 - C. 学习/加班/提升自己 → W+2 - D. 在床上emo到下午然后觉得浪费了一天更emo了 → E+2, W+1 **Q9: 下面哪个场景最让你崩溃?** - A. 有人打断我说话 → S+2, E+1 - B. 电脑突然蓝屏没保存 → W+2, E+2 - C. 计划被打乱 → W+1, C+1 - D. 什么都不想做但什么都没做完 → E+2, C+2 **Q10: 你觉得自己的精神状态?** - A. 稳定地发疯 → C+2, E+2 - B. 很正常,不正常的是这个世界 → W+1, C+1 - C. 取决于今天有没有人理我 → S+2, E+1 - D. 情绪像过山车但我在车上很享受 → E+2, C+1 **Q11: 你看到一只蟑螂,你会?** - A. 尖叫并呼叫最近的活人 → S+2, E+2 - B. 冷静地拿纸巾处理掉 → W+1, C+1 - C. 跟它对视,看看谁先动 → C+2 - D. 思考它是不是也有家庭 → E+2, C+1 **Q12: 如果可以给10年前的自己发一条消息?** - A. "别来" → C+2, E+1 - B. "买比特币" → W+2, C+1 - C. "好好珍惜身边人" → E+2, S+1 - D. "你以后会很厉害的" → W+1, S+1 **Q13: 你在群聊里的角色是?** - A. 潜水员,偶尔冒泡 → C+1, W+1 - B. 话题终结者 → S+1, C+2 - C. 气氛组组长 → S+2, C+1 - D. 发表情包机器 → S+2, E+1 **Q14: 老板说"辛苦了",你的内心OS?** - A. "那你倒是加钱啊" → C+2, E+1 - B. "不辛苦命苦" → E+2, C+1 - C. "谢谢老板!"(真诚) → W+2, S+1 - D. 表面微笑内心已经开始写辞职信 → E+2, W+1 **Q15: 你手机里最多的app类型是?** - A. 社交软件,我一个都不能少 → S+2 - B. 效率工具,我要掌控一切 → W+2 - C. 摸鱼神器(小说/游戏/短视频) → C+2, E+1 - D. 情绪记录/冥想类 → E+2, W+1 **Q16: 你遇到难题时第一反应是?** - A. 查资料查到天荒地老 → W+2, C+1 - B. 发个朋友圈求助 → S+2, E+1 - C. 先放放,说不定明天就解决了 → C+2 - D. 焦虑到手抖但还在做 → E+2, W+2 **Q17: 你觉得"卷"这个字?** - A. 是我的中间名 → W+2 - B. 是我每天批判但偷偷在做的事 → E+1, W+1 - C. 跟我毫无关系谢谢 → C+2 - D. 是用来形容别人的 → S+1, C+1 **Q18: 你发朋友圈的频率?** - A. 一天三条起步 → S+2, E+1 - B. 一周一两条精品 → W+1, S+1 - C. 三个月没发了 → C+2 - D. 想发就发删了又发 → E+2, C+1 **Q19: 你对deadline的态度?** - A. deadline是第一生产力 → W+2, C+1 - B. 提前三天搞定,不慌不忙 → W+2 - C. deadline前一晚的灵魂出窍体验 → E+2, C+2 - D. 什么deadline?哦已经过了? → C+2 **Q20: 你最常用的表情包风格?** - A. 可爱萌系 → E+1, S+1 - B. 阴阳怪气 → C+2, S+1 - C. 打工人流泪 → E+2, W+1 - D. 抽象到没人能看懂 → C+2, E+1 **Q21: 你觉得最离谱的加班理由是?** - A. "公司需要你" → C+1, E+1 - B. 你自己主动加的 → W+2 - C. 不加班不知道去哪 → S+1, E+1 - D. 你已经分不清加班和下班了 → W+2, C+1 **Q22: 如果明天世界末日,你今天干嘛?** - A. 给每个认识的人发消息说再见 → S+2, E+2 - B. 终于不用上班了太好了 → C+2, E+1 - C. 冲去公司把没做完的项目做完 → W+2 - D. 躺着等 → C+1, E+1 **Q23: 你和别人吵架后的状态?** - A. 吵完就忘 → C+2, S+1 - B. 凌晨两点突然想到更好的回击话术 → E+2, W+1 - C. 马上发小作文道歉 → S+2, E+1 - D. 吵架?我选择冷暴力 → W+1, C+1 **Q24: 你对"社交牛逼症"的看法?** - A. 本人就是 → S+2, C+1 - B. 敬而远之 → C+1, E+1 - C. 佩服但做不到 → E+1, W+1 - D. 社交?那是什么 → C+2 **Q25: 你的桌面/房间状态?** - A. 乱但我知道每样东西在哪 → C+2, W+1 - B. 一尘不染,强迫症级别 → W+2 - C. 周期性崩溃式大扫除 → E+2, C+1 - D. 有审美地乱 → S+1, C+1 FILE:references/types.md # SBTI 人格类型 取用户得分最高的两个维度组合,确定人格类型。 ## 维度 - **C** (Chaos) 混乱 - **S** (Social) 社交 - **E** (Emotion) 情绪 - **W** (Workaholic) 卷王 ## 类型映射 ### CS — 吗喽 (Monkey) 代号: CS 混乱的社交达人,精神状态介于发疯和发疯之间。朋友圈的梗王,群聊的灵魂,但自己一个人的时候会突然思考宇宙的意义然后打开外卖app。特征:白天社牛晚上emo,能同时和五个人聊天但一个都不走心,表情包库存3000+。 ### CE — 炸弹 (Bomb) 代号: CE 行走的情绪炸弹,随时可能在任何场合引爆。你以为ta在开玩笑,不,ta是认真的。你以不以为然的事,ta已经脑补了一整部电视剧。特征:上一秒哈哈哈下一秒泪目,共情能力强到看广告都能哭,凌晨三点的哲学家。 ### CW — 丧尸 (Zombie) 代号: CW 一边摸鱼一边卷的矛盾体。工作效率取决于离deadline的距离——越近越强。像一个不定时炸弹,你永远不知道ta下一秒是天才还是废物。特征:deadline战士,摸鱼大师,但该交的活一个不落。 ### SC — 鹦鹉 (Parrot) 代号: SC 社交场上的复读机加气氛组。什么梗都能接,什么话题都能聊,但深度是什么?不认识。特征:群聊话最多,私聊话最少,朋友圈更新比朋友圈还快,口头禅是"哈哈哈哈哈哈"。 ### SE — 金毛 (Golden Retriever) 代号: SE 人类社交天花板,行走的情绪价值输出器。谁不开心找ta就对了,但ta自己的不开心?那不重要(其实很重要但不说)。特征:永远在线的好友,深夜emo救助站,但自己emo的时候只会发一条"没事"。 ### SW — 妈妈/爸爸 (MUM/DAD) 代号: SW 朋友圈的人生导师,什么都会什么都管。一边社交一边把事情安排得明明白白。特征:群里的organizer,旅行的攻略担当,永远操心别人有没有吃饭,口头禅是"早点睡"。 ### EC — 戏精 (Drama Queen) 代号: EC 内心世界比漫威宇宙还丰富。一件小事能在脑子里演30集连续剧。特征:嘴上说着"我没事"但眼神已经杀了三个人,朋友圈删了发发了删,深夜的小作文写手。 ### EW — 卷王plus (Hustler) 代号: EW 边emo边卷,把焦虑转化为生产力的永动机。看起来很努力,实际上确实很努力,但每次都觉得自己不够努力。特征:目标明确但过程纠结,plan比执行多,深夜还在改那个"最终版v3_final_真的最终.docx"。 ### WC — 蜗牛 (Snail) 代号: WC 自己的节奏就是最好的节奏,谁也别想催。摸鱼界的哲学家,卷王中的异类。特征:看似躺平实则在暗中蓄力,deadline前一晚的爆发力惊人,口头禅是"急什么"。 ### WS — 领袖 (Boss) 代号: WS 天生的卷王社交家,一边卷一边拉着所有人一起卷。朋友圈的卷王代言人,但大家都不讨厌ta因为ta真的有实力。特征:永远是最忙的那个但也永远有空社交,朋友圈既是工作圈也是生活圈。 ### 纯C — 猫 (Cat) 代号: CC 混乱中立的化身,活在自己的世界里。别人觉得ta神经ta觉得别人无趣。特征:想一出是一出,今天想学吉他明天想开奶茶店,手机里有100个只用过一次的app。 ### 纯S — 海豚 (Dolphin) 代号: SS 社交界的天花板,跟谁都能聊到起飞。朋友遍天下,但有多少是真朋友不重要,重要的是热闹。特征:聚会的核心,never say no,手机通讯录2000+且都备注了emoji。 ### 纯E — 水母 (Jellyfish) 代号: EE 透明但敏感,随波逐流但内心戏丰富。看起来什么都没在想其实在想很多。特征:共情力爆表但不善表达,经常因为别人的一句话emo一天,但不会告诉任何人。 ### 纯W — 蚂蚁 (Ant) 代号: WW 勤劳到让其他人格类型感到羞愧的存在。目标清晰,执行力强,but at what cost?特征:每日todo list精确到分钟,假期也在学习/加班,别人在摸鱼ta在搬砖。 ## 结果文案风格 生成结果时: - 代号用英文大写(如 CS、MUM) - 类型名中英双语 - 解读要损但损中带爱 - 结合互联网梗和当代年轻人生活状态 - 每次解读要略有不同,不要完全模板化
低成本 AI 图片生成 CLI 工具。支持文生图、图片编辑。触发词:生成图片、画图、AI 作图、文生图、图片编辑、imgen。
---
name: image-gen-low-cost
description: 低成本 AI 图片生成 CLI 工具。支持文生图、图片编辑。触发词:生成图片、画图、AI 作图、文生图、图片编辑、imgen。
---
# Image Gen Low Cost - AI 图片生成 CLI
统一的命令行图片生成工具,支持任何 OpenAI 兼容的 API 端点。
## 快速开始
### 1. 获取 API Token
访问 [https://api.laozhang.ai/register/?aff_code=lfa0](https://api.laozhang.ai/register/?aff_code=lfa0) 注册,在控制台获取 token。新注册自动获得 $0.5 开发测试额度。
### 2. 配置 Token
```bash
# 使用 imgen config 命令(推荐)
imgen config --token YOUR_API_TOKEN
# 或使用环境变量
export IMGEN_TOKEN=YOUR_API_TOKEN
```
### 2. 文生图
```bash
# 生成图片
imgen "一只可爱的猫咪在花园里玩耍"
# 指定输出路径
imgen "夕阳下的海滩" -o beach.png
# 只显示 URL 不保存
imgen "未来城市" --no-save
# 使用不同模型
imgen "可爱的小狗" -m fast
```
### 3. 图片编辑
```bash
# 编辑图片
imgen edit "https://example.com/cat.jpg" "把猫咪的毛色改成彩虹色"
# 使用预设风格
imgen edit "https://example.com/photo.jpg" --style cartoon
# 多图融合
imgen edit "https://a.jpg,https://b.jpg" "将两张图片融合"
```
## 命令参考
```
imgen "prompt" 文生图
imgen edit <url> "prompt" 图片编辑
imgen config --token <token> 配置 API Token
imgen models 列出可用模型
```
### 选项
| 选项 | 说明 |
|------|------|
| `-m, --model <name>` | 模型选择 (cheap/fast/quality) |
| `-o, --output <path>` | 保存到指定路径 |
| `--size <size>` | 图片尺寸 (1024x1024 等) |
| `--no-save` | 不保存,只打印 URL |
| `-s, --style <style>` | 预设风格 |
| `-v, --verbose` | 详细输出 |
## 模型
| 别名 | 模型 ID | 价格 | 说明 |
|------|---------|------|------|
| sora | sora_image | $0.01/张 | **默认**, Sora Image, URL 返回 |
| gpt4o | gpt-4o-image | $0.01/张 | GPT-4o Image, URL 返回 |
| cheap | gemini-2.5-flash-image | $0.025/张 | Nano Banana, base64, 1K 固定 |
| fast | gemini-3.1-flash-image-preview | $0.045/张 | Nano Banana2, base64, 4K 支持 |
| quality | gemini-3-pro-image-preview | $0.05/张 | Nano Banana Pro, base64, 4K + 高级特性 |
> 💡 **默认使用 sora 模型 ($0.01/张)**,如 鍗 鍘- 追求极致性价比: 选 **sora** 或 **- 追求高质量稳定: 选 **cheap** |
**- 追求 4K + 性价比: 选 **fast** |
**- 追求最高质量 + 复杂指令: 选 **quality** |
## 预设风格
- `cartoon` - 迪士尼卡通风格
- `oil` - 古典油画风格
- `ink` - 中国水墨画风格
- `cyberpunk` - 赛博朋克霓虹风格
- `sketch` - 铅笔素描风格
- `watercolor` - 水彩画风格
## API 端点
默认使用老张 API (`https://api.laozhang.ai/v1/chat/completions`)。
```bash
# 切换到其他 OpenAI 兼容端点
export IMGEN_API_URL=https://api.openai.com/v1/chat/completions
# 使用其他兼容服务
export IMGEN_API_URL=https://your-api-endpoint.com/v1/chat/completions
```
> ⚠️ **重要**:如果你使用非老张 API 的 token,请确保设置对应的 `IMGEN_API_URL`,否则 token 会发送到错误的端点。
## 安装
```bash
# 克隆或下载 skill 后
chmod +x scripts/imgen.js
# 添加到 PATH(可选)
ln -s $(pwd)/scripts/imgen.js /usr/local/bin/imgen
```
## 环境变量
| 变量 | 说明 |
|------|------|
| `IMGEN_TOKEN` | API Token |
| `IMGEN_API_URL` | 自定义 API 端点 |
## 注意事项
1. 返回的图片 URL 通常是临时的,建议及时保存
2. 默认保存到当前目录的 `generated-images/` 文件夹
3. Token 存储在 `~/.imgen/token`,权限为 600
## 与 image-gen-cheap 的区别
| 特性 | image-gen-cheap | image-gen-low-cost |
|------|-----------------|---------------------|
| 实现 | Python 脚本 | Node.js CLI |
| 依赖 | requests | 无外部依赖 |
| API | 固定老张 API | 任意 OpenAI 兼容 |
| 配置 | 独立 token 文件 | 统一 imgen config |
FILE:README.md
# Image Gen Low Cost
统一命令行 AI 图片生成工具。支持文生图、图片编辑,兼容任何 OpenAI API 端点。
## 安装
```bash
npm install -g image-gen-low-cost
```
## 快速开始
```bash
# 1. 获取 token:访问 https://api.laozhang.ai/register/?aff_code=lfa0 注册
# 2. 配置 token
imgen config --token YOUR_API_TOKEN
# 3. 生成图片
imgen "一只可爱的猫咪在花园里玩耍"
```
## 命令
```bash
# 文生图
imgen "prompt"
# 图片编辑
imgen edit <image_url> "edit prompt"
imgen edit <image_url> --style cartoon
# 查看模型
imgen models
```
## 选项
| 选项 | 说明 |
|------|------|
| `-m, --model` | 模型 (cheap/fast/quality) |
| `-o, --output` | 输出路径 |
| `--no-save` | 只打印 URL |
| `-s, --style` | 预设风格 |
## 自定义 API
```bash
export IMGEN_API_URL=https://your-api.com/v1/chat/completions
```
## License
MIT
FILE:scripts/imgen.js
#!/usr/bin/env node
/**
* imgen - Low-cost AI image generation CLI
*
* Usage:
* imgen "prompt" Generate image from text
* imgen edit "url" "prompt" Edit existing image
* imgen config --token YOUR_TOKEN Set API token
* imgen models List available models
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
// Config paths
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.imgen');
const TOKEN_FILE = path.join(CONFIG_DIR, 'token');
const OUTPUT_DIR = path.join(process.cwd(), 'generated-images');
// Default API endpoint (OpenAI-compatible)
// Default to laozhang API as documented in SKILL.md
const DEFAULT_API_URL = process.env.IMGEN_API_URL || 'https://api.laozhang.ai/v1/chat/completions';
// Available models with pricing (laozhang API)
// Sorted by price, cheapest first (sora_image is default)
const MODELS = {
// $0.01/image models (cheapest)
'sora': { id: 'sora_image', price: '$0.01/img', format: 'url', desc: 'Sora Image, URL output' },
'gpt4o': { id: 'gpt-4o-image', price: '$0.01/img', format: 'url', desc: 'GPT-4o Image, URL output' },
// Gemini-based models (base64 output)
'cheap': { id: 'gemini-2.5-flash-image', price: '$0.025/img', format: 'base64', desc: 'Nano Banana, 1K fixed' },
'fast': { id: 'gemini-3.1-flash-image-preview', price: '$0.045/img', format: 'base64', desc: 'Nano Banana2, 4K support' },
'quality': { id: 'gemini-3-pro-image-preview', price: '$0.05/img', format: 'base64', desc: 'Nano Banana Pro, 4K + advanced' },
};
// Preset styles for image editing
const STYLES = {
'cartoon': 'Convert to Disney cartoon style, bright colors',
'oil': 'Convert to classic oil painting style, like Van Gogh',
'ink': 'Convert to Chinese ink wash painting style',
'cyberpunk': 'Convert to cyberpunk style with neon light effects',
'sketch': 'Convert to pencil sketch style, black and white lines',
'watercolor': 'Convert to watercolor painting style',
};
// ============ Utility Functions ============
function ensureConfigDir() {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
}
function getToken() {
if (fs.existsSync(TOKEN_FILE)) {
return fs.readFileSync(TOKEN_FILE, 'utf8').trim();
}
const envToken = process.env.IMGEN_TOKEN || process.env.OPENAI_API_KEY;
if (envToken) return envToken;
return null;
}
function saveToken(token) {
ensureConfigDir();
fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
console.log(`✓ Token saved to TOKEN_FILE`);
}
function generateFilename(prompt, suffix = '') {
const date = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const keywords = prompt.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '-').slice(0, 20);
return `date-keywordssuffix.png`;
}
function httpGet(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
client.get(url, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => resolve(Buffer.concat(chunks)));
res.on('error', reject);
}).on('error', reject);
});
}
function httpPost(urlString, headers, body) {
return new Promise((resolve, reject) => {
const url = new URL(urlString);
const client = url.protocol === 'https:' ? https : http;
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
}
};
const req = client.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Invalid JSON: data.slice(0, 200)`));
}
});
});
req.on('error', reject);
req.write(JSON.stringify(body));
req.end();
});
}
// ============ Core Functions ============
async function generateImage(prompt, options = {}) {
const token = options.token || getToken();
if (!token) {
console.error('Error: No API token. Run: imgen config --token YOUR_TOKEN');
process.exit(1);
}
const model = options.model || 'cheap';
const modelInfo = MODELS[model] || MODELS['cheap'];
// Build request - simple text prompt
const body = {
model: modelInfo.id,
messages: [{ role: 'user', content: prompt }],
n: 1,
size: options.size || '1024x1024'
};
console.error(`Generating with modelInfo.id (modelInfo.price)...`);
try {
const result = await httpPost(DEFAULT_API_URL, {
'Authorization': `Bearer token`
}, body);
// Extract image from response (URL or base64)
const content = result.choices?.[0]?.message?.content || '';
// Check for markdown image with URL
let imageUrl = null;
let base64Data = null;
const urlMatch = content.match(/!\[.*?\]\((https?:\/\/[^)]+)\)/);
const base64Match = content.match(/!\[.*?\]\((data:image\/[^;]+;base64,([^)]+))\)/);
if (urlMatch) {
imageUrl = urlMatch[1];
} else if (base64Match) {
base64Data = base64Match[2];
} else {
imageUrl = result.data?.[0]?.url;
}
if (!imageUrl && !base64Data) {
console.error('No image in response');
if (options.verbose) console.log(JSON.stringify(result, null, 2));
process.exit(1);
}
// Save or print
if (!options.noSave) {
const outputPath = options.output || path.join(OUTPUT_DIR, generateFilename(prompt));
if (base64Data) {
// Save base64 directly
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, Buffer.from(base64Data, 'base64'));
console.error(`✓ Saved: outputPath`);
console.log(outputPath);
} else {
console.log(imageUrl);
await downloadImage(imageUrl, outputPath);
}
} else if (imageUrl) {
console.log(imageUrl);
} else if (base64Match) {
console.log(base64Match[1]); // Full data URL
}
return imageUrl || (base64Match ? base64Match[1] : null);
} catch (err) {
console.error('API error:', err.message);
process.exit(1);
}
}
async function editImage(imageUrl, prompt, options = {}) {
const token = options.token || getToken();
if (!token) {
console.error('Error: No API token. Run: imgen config --token YOUR_TOKEN');
process.exit(1);
}
// Apply style preset if provided
if (options.style && STYLES[options.style]) {
prompt = STYLES[options.style];
}
const model = options.model || 'cheap';
const modelInfo = MODELS[model] || MODELS['cheap'];
// Build request with image
const body = {
model: modelInfo.id,
messages: [{
role: 'user',
content: [
{ type: 'text', text: prompt },
{ type: 'image_url', image_url: { url: imageUrl } }
]
}]
};
console.error(`Editing with modelInfo.id (modelInfo.price)...`);
try {
const result = await httpPost(DEFAULT_API_URL, {
'Authorization': `Bearer token`
}, body);
const content = result.choices?.[0]?.message?.content || '';
// Check for markdown image with URL or base64
let resultUrl = null;
let base64Data = null;
const urlMatch = content.match(/!\[.*?\]\((https?:\/\/[^)]+)\)/);
const base64Match = content.match(/!\[.*?\]\((data:image\/[^;]+;base64,([^)]+))\)/);
if (urlMatch) {
resultUrl = urlMatch[1];
} else if (base64Match) {
base64Data = base64Match[2];
} else {
resultUrl = result.data?.[0]?.url;
}
if (!resultUrl && !base64Data) {
console.error('No image in response');
process.exit(1);
}
if (!options.noSave) {
const suffix = options.style ? `-options.style` : '-edited';
const outputPath = options.output || path.join(OUTPUT_DIR, generateFilename(prompt, suffix));
if (base64Data) {
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, Buffer.from(base64Data, 'base64'));
console.error(`✓ Saved: outputPath`);
console.log(outputPath);
} else {
console.log(resultUrl);
await downloadImage(resultUrl, outputPath);
}
} else if (resultUrl) {
console.log(resultUrl);
} else if (base64Match) {
console.log(base64Match[1]);
}
return resultUrl || (base64Match ? base64Match[1] : null);
} catch (err) {
console.error('API error:', err.message);
process.exit(1);
}
}
async function downloadImage(url, outputPath) {
try {
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const buffer = await httpGet(url);
fs.writeFileSync(outputPath, buffer);
console.error(`✓ Saved: outputPath`);
} catch (err) {
console.error('Download failed:', err.message);
}
}
function printHelp() {
console.log(`
imgen - Low-cost AI image generation CLI
USAGE:
imgen "prompt" Generate image from text
imgen edit <url> "prompt" Edit an image
imgen edit <url> --style <style> Apply preset style
imgen config --token <token> Set API token
imgen models List available models
OPTIONS:
-m, --model <name> Model to use (cheap/fast/quality)
-o, --output <path> Save to specific path
--size <size> Image size (1024x1024, 512x512, etc.)
--no-save Don't save, just print URL
-v, --verbose Show detailed output
-h, --help Show this help
MODELS:
sora sora_image $0.01/img (default, URL)
gpt4o gpt-4o-image $0.01/img (URL)
cheap gemini-2.5-flash-image $0.025/img (base64)
fast gemini-3.1-flash-image-preview $0.045/img (base64, 4K)
quality gemini-3-pro-image-preview $0.05/img (base64, 4K)
STYLES (for editing):
cartoon, oil, ink, cyberpunk, sketch, watercolor
EXAMPLES:
imgen "A cute cat in a garden"
imgen "Sunset beach" --ratio 3:2 -o beach.png
imgen edit "https://example.com/cat.jpg" "Make it look like a cartoon"
imgen edit "https://example.com/photo.jpg" --style cyberpunk
ENVIRONMENT:
IMGEN_TOKEN API token (alternative to config file)
IMGEN_API_URL Custom API endpoint
First time? Run: imgen config --token YOUR_API_TOKEN
`);
}
// ============ CLI Entry Point ============
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
printHelp();
process.exit(0);
}
const command = args[0];
// Parse flags
const options = {};
let positional = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '-m' || arg === '--model') {
options.model = args[++i];
} else if (arg === '-o' || arg === '--output') {
options.output = args[++i];
} else if (arg === '--size') {
options.size = args[++i];
} else if (arg === '--no-save') {
options.noSave = true;
} else if (arg === '-v' || arg === '--verbose') {
options.verbose = true;
} else if (arg === '--token') {
options.token = args[++i];
} else if (arg === '--style' || arg === '-s') {
options.style = args[++i];
} else if (!arg.startsWith('-')) {
positional.push(arg);
}
}
// Handle commands
if (command === 'config') {
if (options.token) {
saveToken(options.token);
} else {
console.error('Usage: imgen config --token YOUR_TOKEN');
process.exit(1);
}
return;
}
if (command === 'models') {
console.log('\nAvailable Models:\n');
for (const [key, info] of Object.entries(MODELS)) {
console.log(` key.padEnd(10) info.id.padEnd(20) info.price`);
}
console.log('\nStyles: ' + Object.keys(STYLES).join(', ') + '\n');
return;
}
if (command === 'edit') {
const [imageUrl, prompt] = positional.slice(1);
if (!imageUrl) {
console.error('Usage: imgen edit <image_url> "prompt" [--style <style>]');
process.exit(1);
}
await editImage(imageUrl, prompt || '', options);
return;
}
// Default: generate image
const prompt = positional.join(' ');
if (!prompt) {
console.error('Error: No prompt provided');
console.error('Usage: imgen "your prompt here"');
process.exit(1);
}
await generateImage(prompt, options);
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
FILE:skill.json
{
"name": "image-gen-low-cost",
"version": "1.0.5",
"description": "Low-cost AI image generation CLI. Text-to-image and image editing from $0.01/image. Works with any OpenAI-compatible API.",
"author": "openclaw-community",
"license": "MIT",
"keywords": ["image", "generation", "ai", "text-to-image", "image-editing", "cli", "low-cost"],
"bin": {
"imgen": "./scripts/imgen.js"
},
"engines": {
"node": ">=16"
}
}
使用老张 API 生成和编辑图片,最低 $0.01/张。支持文生图、图片编辑、多图融合、多种比例。触发词:生成图片、画图、AI作图、文生图、图片编辑、换背景、风格转换、Sora生图。
---
name: laozhangapi-image
description: 使用老张 API 生成和编辑图片,最低 $0.01/张。支持文生图、图片编辑、多图融合、多种比例。触发词:生成图片、画图、AI作图、文生图、图片编辑、换背景、风格转换、Sora生图。
---
# 老张 API 图片生成
低成本高质量图片生成与编辑。
## 快速开始
### 1. 配置 Token
```bash
echo "sk-xxx" > ~/.laozhang_api_token
```
访问 [老张 API](https://api.laozhang.ai/register/?aff_code=lfa0) 注册获取 token。新注册自动获得 $0.5 测试额度。
### 2. 文生图
```bash
# 默认模型(sora_image,$0.01/张,返回URL)
python scripts/generate_image.py "一只可爱的猫咪在花园里"
# 指定比例(仅 sora_image 支持)
python scripts/generate_image.py "夕阳海滩" --ratio 3:2
# 保存到本地
python scripts/generate_image.py "可爱小狗" --output dog.png
```
### 3. 图片编辑
```bash
# 基础编辑(默认 gpt-4o-image,$0.01/张)
python scripts/edit_image.py "https://example.com/cat.jpg" "把毛色改成彩虹色"
# 预设风格
python scripts/edit_image.py "https://example.com/photo.jpg" --style 卡通
# 多图融合
python scripts/edit_image.py "https://a.jpg,https://b.jpg" "融合两张图"
```
## 模型选择
| 用途 | 推荐模型 | 价格 | 返回 |
|------|---------|------|------|
| 文生图(默认) | sora_image | $0.01/张 | URL |
| 图片编辑(默认) | gpt-4o-image | $0.01/张 | URL |
| 高质量/4K | gemini-3-pro-image-preview | $0.05/张 | base64 |
| 性价比 | gemini-2.5-flash-image | $0.025/张 | base64 |
详细模型对比见 [references/models.md](references/models.md)。
## 预设风格
卡通、油画、水墨、赛博朋克、素描、水彩
## 参数
### generate_image.py
```
--model, -m 模型选择(默认: sora_image)
--ratio, -r 比例:2:3/3:2/1:1(仅 sora_image)
--output, -o 保存路径
--no-save 不保存,仅显示URL
```
### edit_image.py
```
--model, -m 模型选择(默认: gpt-4o-image)
--style, -s 预设风格
--output, -o 保存路径
--no-save 不保存,仅显示URL
```
## 常见示例
见 [examples/usage.md](examples/usage.md)。
## 注意
- URL 返回的模型可直接发送到飞书
- base64 返回的模型会自动保存到本地
- 建议控制在 10 请求/分钟
FILE:examples/usage.md
# 使用示例
## 文生图
### 基础用法
```bash
# 生成图片(默认 sora_image,$0.01/张)
python scripts/generate_image.py "一只可爱的猫咪在花园里玩耍"
# 指定比例
python scripts/generate_image.py "夕阳海滩风景" --ratio 3:2
python scripts/generate_image.py "人物肖像" --ratio 2:3
python scripts/generate_image.py "APP图标" --ratio 1:1
# 保存到指定位置
python scripts/generate_image.py "可爱小狗" --output ~/Pictures/dog.png
# 使用其他模型
python scripts/generate_image.py "未来城市" --model gemini-3-pro-image-preview
```
### 实际场景
```bash
# 小红书封面(3:2横版)
python scripts/generate_image.py "一个穿着汉服的女孩在樱花树下,唯美,梦幻" --ratio 3:2
# 手机壁纸(2:3竖版)
python scripts/generate_image.py "极简风格的山景,渐变天空" --ratio 2:3
# 头像(1:1正方形)
python scripts/generate_image.py "可爱的卡通龙虾角色,大眼睛,微笑" --ratio 1:1
```
## 图片编辑
### 基础编辑
```bash
# 简单修改
python scripts/edit_image.py "https://example.com/cat.jpg" "把猫咪的毛色改成彩虹色"
# 美化照片
python scripts/edit_image.py "https://example.com/photo.jpg" "美化这张照片,让光线更柔和"
# 换背景
python scripts/edit_image.py "https://example.com/person.jpg" "把背景换成巴黎铁塔"
```
### 预设风格
```bash
# 卡通化
python scripts/edit_image.py "https://example.com/photo.jpg" --style 卡通
# 油画风格
python scripts/edit_image.py "https://example.com/landscape.jpg" --style 油画
# 水墨画
python scripts/edit_image.py "https://example.com/mountain.jpg" --style 水墨
```
### 多图融合
```bash
# 融合两张图
python scripts/edit_image.py "https://a.jpg,https://b.jpg" "将两张图片融合成一张"
# 合成场景
python scripts/edit_image.py "https://person.jpg,https://background.jpg" "把人物放到背景中"
```
## 高级用法
### 详细输出
```bash
# 查看详细信息
python scripts/generate_image.py "测试图片" --verbose
# 输出完整JSON响应
python scripts/generate_image.py "测试图片" --json
```
### 不保存图片
```bash
# 仅获取URL,不保存到本地
python scripts/generate_image.py "测试图片" --no-save
```
### 指定 Token
```bash
# 临时使用其他 token
python scripts/generate_image.py "测试图片" --token sk-xxx
```
FILE:references/models.md
# 模型详情
## 文生图模型
| 模型 | 模型ID | 价格 | 返回格式 | 特点 |
|------|--------|------|---------|------|
| Sora Image | sora_image | $0.01/张 | URL | 文生图,支持比例(2:3/3:2/1:1) |
| GPT-4o Image | gpt-4o-image | $0.01/张 | URL | GPT-4o 文生图 |
| Nano Banana | gemini-2.5-flash-image | $0.025/张 | base64 | Gemini 2.5 Flash,基础版 |
| Nano Banana2 | gemini-3.1-flash-image-preview | $0.03/张 | base64 | Gemini 3.1 Flash,支持4K |
| Nano Banana Pro | gemini-3-pro-image-preview | $0.05/张 | base64 | Gemini 3 Pro,支持4K,复杂指令 |
## 图片编辑模型
| 模型 | 模型ID | 价格 | 返回格式 | 特点 |
|------|--------|------|---------|------|
| GPT-4o Image | gpt-4o-image | $0.01/张 | URL | 图片编辑,单图/多图 |
| Sora Image | sora_image | $0.01/张 | URL | 图片编辑 |
| Nano Banana | gemini-2.5-flash-image | $0.025/张 | base64 | 图片编辑,多图合成,快速 |
| Nano Banana2 | gemini-3.1-flash-image-preview | $0.03/张 | base64 | 图片编辑,支持4K |
| Nano Banana Pro | gemini-3-pro-image-preview | $0.05/张 | base64 | 图片编辑,复杂指令,4K |
## 选择建议
- **需要发送到飞书**:选 URL 返回的模型(sora_image / gpt-4o-image)
- **需要高质量**:选 Nano Banana Pro(支持4K)
- **性价比**:选 Nano Banana($0.025/张)
## 预设风格
使用 `--style` 参数快速应用风格:
- 卡通 - 迪士尼卡通风格
- 油画 - 古典油画风格
- 水墨 - 中国水墨画风格
- 赛博朋克 - 霓虹灯光效果
- 素描 - 铅笔素描风格
- 水彩 - 水彩画风格
## API 端点
- URL: https://api.laozhang.ai/v1/chat/completions
- 认证: Bearer Token
- 文档: https://docs.laozhang.ai
FILE:scripts/edit_image.py
#!/usr/bin/env python3
"""
老张 API 图片编辑脚本
使用方法:python edit_image.py "图片URL" "编辑描述" [--style 风格] [--model 模型]
"""
import argparse
import json
import re
import sys
import base64
from pathlib import Path
from datetime import datetime
try:
import requests
except ImportError:
print("需要安装 requests 库:pip install requests")
sys.exit(1)
API_URL = "https://api.laozhang.ai/v1/chat/completions"
MODELS = {
"gpt-4o-image": ("GPT-4o Image", "$0.01/张", "url"),
"sora_image": ("Sora Image", "$0.01/张", "url"),
"gemini-2.5-flash-image": ("Nano Banana", "$0.025/张", "base64"),
"gemini-3.1-flash-image-preview": ("Nano Banana2", "$0.03/张", "base64"),
"gemini-3-pro-image-preview": ("Nano Banana Pro", "$0.05/张", "base64"),
}
DEFAULT_MODEL = "gpt-4o-image"
TOKEN_PATH = Path.home() / ".laozhang_api_token"
OUTPUT_DIR = Path.home() / "Pictures" / "laozhang"
STYLES = {
"卡通": "转换成迪士尼卡通风格,色彩鲜艳",
"油画": "转换成古典油画风格,如梵高画风",
"水墨": "转换成中国水墨画风格,留白意境",
"赛博朋克": "转换成赛博朋克风格,霓虹灯光效果",
"素描": "转换成铅笔素描风格,黑白线条",
"水彩": "转换成水彩画风格,透明感,色彩流动"
}
def get_token():
if TOKEN_PATH.exists():
return TOKEN_PATH.read_text().strip()
return None
def generate_filename(prompt: str, suffix: str = ""):
date = datetime.now().strftime("%Y-%m-%d")
keywords = "".join(c for c in prompt[:15] if c.isalnum() or c in " -_").strip()
keywords = keywords.replace(" ", "-")
return f"{date}-edit-{keywords}{suffix}.png"
def edit(image_urls: list, prompt: str, token: str, model: str = None, verbose: bool = False):
model = model or DEFAULT_MODEL
model_name, price, return_type = MODELS.get(model, ("Unknown", "?", "url"))
if verbose:
print(f"📝 编辑提示: {prompt}")
print(f"🖼️ 输入: {len(image_urls)} 张图")
print(f"🤖 模型: {model_name} ({price})")
# 构建消息内容
content = [{"type": "text", "text": prompt}]
for url in image_urls:
content.append({"type": "image_url", "image_url": {"url": url}})
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [{"role": "user", "content": content}]
}
try:
resp = requests.post(API_URL, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
result = resp.json()
content = result['choices'][0]['message']['content']
images = []
urls = re.findall(r'!\[.*?\]\((https?://[^)]+)\)', content)
for url in urls:
images.append(("url", url))
if not images and return_type == "base64":
b64_match = re.search(r'data:image/[^;]+;base64,([A-Za-z0-9+/=]+)', content)
if b64_match:
images.append(("base64", b64_match.group(1)))
elif re.match(r'^[A-Za-z0-9+/=]+$', content.strip()[:100]):
images.append(("base64", content.strip()))
if verbose and images:
print(f"✅ 生成 {len(images)} 张图片")
return images, result
except requests.exceptions.RequestException as e:
print(f"❌ API 调用失败: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"响应: {e.response.text[:500]}")
return [], None
def save_image(img_type: str, data: str, output: Path):
output.parent.mkdir(parents=True, exist_ok=True)
if img_type == "url":
resp = requests.get(data, timeout=30)
resp.raise_for_status()
output.write_bytes(resp.content)
else:
output.write_bytes(base64.b64decode(data))
print(f"💾 已保存: {output}")
def main():
parser = argparse.ArgumentParser(
description="老张 API 图片编辑",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
风格: 卡通/油画/水墨/赛博朋克/素描/水彩
示例:
%(prog)s "https://example.com/cat.jpg" "毛色改成彩虹色"
%(prog)s "https://example.com/photo.jpg" --style 卡通
%(prog)s "https://a.jpg,https://b.jpg" "融合两张图"
"""
)
parser.add_argument("images", help="图片URL(多图用逗号分隔)")
parser.add_argument("prompt", nargs="?", help="编辑描述(--style 时可省略)")
parser.add_argument("-m", "--model", choices=list(MODELS.keys()), help=f"模型(默认: {DEFAULT_MODEL})")
parser.add_argument("-s", "--style", choices=list(STYLES.keys()), help="预设风格")
parser.add_argument("-o", "--output", type=Path, help="保存路径")
parser.add_argument("-t", "--token", help="API token")
parser.add_argument("-v", "--verbose", action="store_true", help="详细信息")
parser.add_argument("--json", action="store_true", help="输出完整JSON")
parser.add_argument("--no-save", action="store_true", help="不保存图片")
args = parser.parse_args()
prompt = STYLES[args.style] if args.style else args.prompt
if not prompt:
print("❌ 请提供编辑描述或使用 --style")
sys.exit(1)
image_urls = [u.strip() for u in args.images.split(",")]
token = args.token or get_token()
if not token:
print(f"❌ 未找到 token。请创建 {TOKEN_PATH} 或使用 --token")
sys.exit(1)
images, result = edit(image_urls, prompt, token, args.model, args.verbose)
if not images:
print("❌ 未能生成图片")
sys.exit(1)
for i, (img_type, data) in enumerate(images, 1):
if img_type == "url":
print(f"🖼️ 图片 {i}: {data}")
else:
print(f"🖼️ 图片 {i}: [base64, {len(data)} 字符]")
if not args.no_save and i == 1:
suffix = f"-{args.style}" if args.style else ""
output = args.output or OUTPUT_DIR / generate_filename(prompt, suffix)
save_image(img_type, data, output)
if args.json and result:
print("\n📄 完整响应:")
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/generate_image.py
#!/usr/bin/env python3
"""
老张 API 图片生成脚本
使用方法:python generate_image.py "提示词" [--model 模型] [--ratio 2:3|3:2|1:1] [--output 输出路径]
"""
import argparse
import json
import re
import sys
import base64
from pathlib import Path
from datetime import datetime
try:
import requests
except ImportError:
print("需要安装 requests 库:pip install requests")
sys.exit(1)
API_URL = "https://api.laozhang.ai/v1/chat/completions"
MODELS = {
"sora_image": ("Sora Image", "$0.01/张", "url"),
"gpt-4o-image": ("GPT-4o Image", "$0.01/张", "url"),
"gemini-2.5-flash-image": ("Nano Banana", "$0.025/张", "base64"),
"gemini-3.1-flash-image-preview": ("Nano Banana2", "$0.03/张", "base64"),
"gemini-3-pro-image-preview": ("Nano Banana Pro", "$0.05/张", "base64"),
}
DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
TOKEN_PATH = Path.home() / ".laozhang_api_token"
OUTPUT_DIR = Path.home() / "Pictures" / "laozhang"
def get_token():
if TOKEN_PATH.exists():
return TOKEN_PATH.read_text().strip()
return None
def generate_filename(prompt: str, ratio: str = None) -> str:
date = datetime.now().strftime("%Y%m%d_%H%M%S")
keywords = "".join(c for c in prompt[:20] if c.isalnum() or c in " -_").strip().replace(" ", "-")
ratio_s = f"_{ratio.replace(':', 'x')}" if ratio else ""
return f"{date}_{keywords}{ratio_s}.png"
def generate(prompt: str, token: str, model: str = None, ratio: str = None, verbose: bool = False):
model = model or DEFAULT_MODEL
model_name, price, return_type = MODELS.get(model, ("Unknown", "?", "url"))
if model == "sora_image" and ratio:
prompt = f"{prompt}【{ratio}】"
if verbose:
print(f"📝 提示词: {prompt}")
print(f"🤖 模型: {model_name} ({price})")
if ratio:
print(f"📐 比例: {ratio}")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}]
}
try:
resp = requests.post(API_URL, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
result = resp.json()
content = result['choices'][0]['message']['content']
# 处理返回内容
images = []
# 提取 Markdown 图片链接
urls = re.findall(r'!\[.*?\]\((https?://[^)]+)\)', content)
for url in urls:
images.append(("url", url))
# 如果没有找到 URL,可能返回的是 base64
if not images and return_type == "base64":
# 尝试从内容中提取 base64
b64_match = re.search(r'data:image/[^;]+;base64,([A-Za-z0-9+/=]+)', content)
if b64_match:
images.append(("base64", b64_match.group(1)))
elif re.match(r'^[A-Za-z0-9+/=]+$', content.strip()[:100]):
# 整个内容可能就是 base64
images.append(("base64", content.strip()))
if verbose and images:
print(f"✅ 生成 {len(images)} 张图片")
return images, result
except requests.exceptions.RequestException as e:
print(f"❌ API 调用失败: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"响应: {e.response.text[:500]}")
return [], None
def save_image(img_type: str, data: str, output: Path):
output.parent.mkdir(parents=True, exist_ok=True)
if img_type == "url":
resp = requests.get(data, timeout=30)
resp.raise_for_status()
output.write_bytes(resp.content)
else:
# base64
output.write_bytes(base64.b64decode(data))
print(f"💾 已保存: {output}")
def main():
parser = argparse.ArgumentParser(description="老张 API 图片生成")
parser.add_argument("prompt", help="图片描述")
parser.add_argument("-m", "--model", choices=list(MODELS.keys()), help=f"模型(默认: {DEFAULT_MODEL})")
parser.add_argument("-r", "--ratio", choices=["2:3", "3:2", "1:1"], help="比例(仅 sora_image)")
parser.add_argument("-o", "--output", type=Path, help="保存路径")
parser.add_argument("-t", "--token", help="API token")
parser.add_argument("-v", "--verbose", action="store_true", help="详细信息")
parser.add_argument("--json", action="store_true", help="输出完整JSON")
parser.add_argument("--no-save", action="store_true", help="不保存图片")
args = parser.parse_args()
token = args.token or get_token()
if not token:
print(f"❌ 未找到 token。请创建 {TOKEN_PATH} 或使用 --token")
sys.exit(1)
images, result = generate(args.prompt, token, args.model, args.ratio, args.verbose)
if not images:
print("❌ 未能生成图片")
sys.exit(1)
for i, (img_type, data) in enumerate(images, 1):
if img_type == "url":
print(f"🖼️ 图片 {i}: {data}")
else:
print(f"🖼️ 图片 {i}: [base64, {len(data)} 字符]")
if not args.no_save and i == 1:
output = args.output or OUTPUT_DIR / generate_filename(args.prompt, args.ratio)
save_image(img_type, data, output)
if args.json and result:
print("\n📄 完整响应:")
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
低成本图片生成与编辑。使用老张 API,最低 $0.01/张。支持文生图、图片编辑。触发词:生成图片、画图、AI 作图、文生图、图片编辑。
---
name: image-gen-cheap
description: 低成本图片生成与编辑。使用老张 API,最低 $0.01/张。支持文生图、图片编辑。触发词:生成图片、画图、AI 作图、文生图、图片编辑。
---
# Image Gen Cheap - 低成本图片生成
## 快速开始
### 1. 获取 API Token
访问 [https://api.laozhang.ai/register/?aff_code=lfa0](https://api.laozhang.ai/register/?aff_code=lfa0) 注册,在控制台获取 token。新注册自动获得 $0.5 开发测试额度。
保存 token:
```bash
echo "sk-your-token" > ~/.laozhang_api_token
```
### 2. 文生图
```bash
# 使用默认模型(sora_image,$0.01/张)
python scripts/generate_image.py "一只可爱的猫咪在花园里玩耍"
# 指定比例(2:3竖版/3:2横版/1:1正方形)
python scripts/generate_image.py "夕阳下的海滩" --ratio 3:2
# 保存到指定路径
python scripts/generate_image.py "可爱的小狗" --output dog.png
# 使用其他模型
python scripts/generate_image.py "未来城市" --model gpt-4o-image
```
### 3. 图片编辑
```bash
# 基础编辑
python scripts/edit_image.py "https://example.com/cat.jpg" "把猫咪的毛色改成彩虹色"
# 使用预设风格
python scripts/edit_image.py "https://example.com/photo.jpg" --style 卡通
# 多图融合
python scripts/edit_image.py "https://a.jpg,https://b.jpg" "将两张图片融合"
```
## 模型与价格
### 文生图模型
| 模型 | 模型ID | 价格 | 返回格式 |
|------|--------|------|---------|
| Sora Image | sora_image | **$0.01/张** | URL |
| GPT-4o Image | gpt-4o-image | **$0.01/张** | URL |
| Nano Banana | gemini-2.5-flash-image | $0.025/张 | base64 |
| Nano Banana2 | gemini-3.1-flash-image-preview | $0.03/张 | base64 |
| Nano Banana Pro | gemini-3-pro-image-preview | $0.05/张 | base64 |
### 图片编辑模型
| 模型 | 模型ID | 价格 | 返回格式 |
|------|--------|------|---------|
| GPT-4o Image | gpt-4o-image | **$0.01/张** | URL |
| Sora Image | sora_image | **$0.01/张** | URL |
| Nano Banana | gemini-2.5-flash-image | $0.025/张 | base64 |
**推荐**:默认使用 `sora_image`(文生图)和 `gpt-4o-image`(图片编辑),都是 $0.01/张且返回 URL。
## 预设风格
- 卡通 - 迪士尼卡通风格
- 油画 - 古典油画风格
- 水墨 - 中国水墨画风格
- 赛博朋克 - 霓虹灯光效果
- 素描 - 铅笔素描风格
- 水彩 - 水彩画风格
## 参数说明
### generate_image.py
```
--model, -m 使用的模型(默认: sora_image)
--ratio, -r 图片比例:2:3/3:2/1:1(仅 sora_image 支持)
--output, -o 保存图片到指定路径
--token, -t 指定 API token
--verbose, -v 显示详细信息
--json 输出完整 API 响应
--no-save 不保存图片(仅显示URL)
```
### edit_image.py
```
--model, -m 使用的模型(默认: gpt-4o-image)
--style, -s 预设风格(卡通/油画/水墨/赛博朋克/素描/水彩)
--output, -o 保存图片到指定路径
--token, -t 指定 API token
--verbose, -v 显示详细信息
--json 输出完整 API 响应
--no-save 不保存图片(仅显示URL)
```
## 依赖
```bash
pip install requests
```
## 注意事项
1. 返回 URL 的模型(sora_image / gpt-4o-image)可直接发送到飞书等平台
2. 返回 base64 的模型会自动解码保存到本地
3. 建议控制在 10 请求/分钟以内
FILE:README.md
# Image Gen Cheap
Low-cost image generation and editing via LaoZhang API. Starting at **$0.01/image**.
## Quick Start
### 1. Get API Token
Register at [https://api.laozhang.ai/register/?aff_code=lfa0](https://api.laozhang.ai/register/?aff_code=lfa0) and get your token from the dashboard.
Save your token:
```bash
echo "sk-your-token" > ~/.laozhang_api_token
```
### 2. Text-to-Image
```bash
# Use default model (sora_image, $0.01/image)
python scripts/generate_image.py "A cute cat playing in a garden"
# Specify aspect ratio (2:3 portrait / 3:2 landscape / 1:1 square)
python scripts/generate_image.py "Sunset beach scene" --ratio 3:2
# Save to specific path
python scripts/generate_image.py "A cute puppy" --output dog.png
# Use different model
python scripts/generate_image.py "Futuristic city" --model gpt-4o-image
```
### 3. Image Editing
```bash
# Basic editing
python scripts/edit_image.py "https://example.com/cat.jpg" "Change the cat's fur to rainbow colors"
# Use preset style
python scripts/edit_image.py "https://example.com/photo.jpg" --style cartoon
# Multi-image fusion
python scripts/edit_image.py "https://a.jpg,https://b.jpg" "Merge these two images"
```
## Models & Pricing
### Text-to-Image Models
| Model | Model ID | Price | Format |
|-------|----------|-------|--------|
| Sora Image | sora_image | **$0.01/img** | URL |
| GPT-4o Image | gpt-4o-image | **$0.01/img** | URL |
| Nano Banana | gemini-2.5-flash-image | $0.025/img | base64 |
| Nano Banana2 | gemini-3.1-flash-image-preview | $0.03/img | base64 |
| Nano Banana Pro | gemini-3-pro-image-preview | $0.05/img | base64 |
### Image Editing Models
| Model | Model ID | Price | Format |
|-------|----------|-------|--------|
| GPT-4o Image | gpt-4o-image | **$0.01/img** | URL |
| Sora Image | sora_image | **$0.01/img** | URL |
| Nano Banana | gemini-2.5-flash-image | $0.025/img | base64 |
**Recommended**: Defaults to `sora_image` (text-to-image) and `gpt-4o-image` (editing) - both $0.01/image with URL output.
## Preset Styles
- cartoon - Disney cartoon style
- oil-painting - Classic oil painting style
- ink-wash - Chinese ink wash painting style
- cyberpunk - Neon light effects
- sketch - Pencil sketch style
- watercolor - Watercolor painting style
## CLI Options
### generate_image.py
```
--model, -m Model to use (default: sora_image)
--ratio, -r Aspect ratio: 2:3/3:2/1:1 (sora_image only)
--output, -o Save image to specified path
--token, -t Specify API token
--verbose, -v Show detailed information
--json Output full API response
--no-save Don't save image (show URL only)
```
### edit_image.py
```
--model, -m Model to use (default: gpt-4o-image)
--style, -s Preset style (cartoon/oil-painting/ink-wash/cyberpunk/sketch/watercolor)
--output, -o Save image to specified path
--token, -t Specify API token
--verbose, -v Show detailed information
--json Output full API response
--no-save Don't save image (show URL only)
```
## Dependencies
```bash
pip install requests
```
## Notes
1. URL-returning models (sora_image / gpt-4o-image) can be directly sent to platforms like Feishu
2. Base64-returning models are automatically decoded and saved locally
3. Recommended rate limit: 10 requests/minute
FILE:scripts/edit_image.py
#!/usr/bin/env python3
"""
Image Editing Script - LaoZhang API
Usage: python edit_image.py "image_url(s)" "edit_prompt" [--model MODEL] [--style STYLE]
"""
import argparse
import json
import re
import sys
import base64
from pathlib import Path
from datetime import datetime
try:
import requests
except ImportError:
print("Install requests: pip install requests")
sys.exit(1)
# API Configuration
API_URL = "https://api.laozhang.ai/v1/chat/completions"
DEFAULT_MODEL = "gpt-4o-image"
DEFAULT_TOKEN_PATH = Path.home() / ".laozhang_api_token"
DEFAULT_OUTPUT_DIR = Path.cwd() / "generated-images"
# Available Models
AVAILABLE_MODELS = {
"gpt-4o-image": "GPT-4o Image - $0.01/img, returns URL",
"sora_image": "Sora Image - $0.01/img, returns URL",
"gemini-2.5-flash-image": "Nano Banana - $0.025/img, returns base64",
"gemini-3.1-flash-image-preview": "Nano Banana2 - $0.03/img, returns base64",
"gemini-3-pro-image-preview": "Nano Banana Pro - $0.05/img, returns base64",
}
# Preset Styles
STYLE_TEMPLATES = {
"cartoon": "Convert to Disney cartoon style, bright colors",
"oil-painting": "Convert to classic oil painting style, like Van Gogh",
"ink-wash": "Convert to Chinese ink wash painting style",
"cyberpunk": "Convert to cyberpunk style with neon light effects",
"sketch": "Convert to pencil sketch style, black and white lines",
"watercolor": "Convert to watercolor painting style",
}
def get_api_token():
"""Get API token from file"""
if DEFAULT_TOKEN_PATH.exists():
return DEFAULT_TOKEN_PATH.read_text().strip()
return None
def generate_filename(prompt: str, style: str = ""):
"""Generate filename from prompt and date"""
date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
keywords = "".join(c for c in prompt[:15] if c.isalnum() or c in " -_").strip()
keywords = keywords.replace(" ", "-")[:15]
style_suffix = f"-{style}" if style else ""
return f"{date_str}-edit-{keywords}{style_suffix}.png"
def edit_image(image_urls: list, prompt: str, token: str, model: str = None, verbose: bool = False):
"""
Edit image(s) using specified model
Args:
image_urls: List of image URLs
prompt: Edit description
token: API token
model: Model to use
verbose: Show detailed info
Returns:
Tuple of (results_list, full_response)
results_list contains tuples of (type, data) where type is "url" or "base64"
"""
if model is None:
model = DEFAULT_MODEL
# Build message with images
content = [{"type": "text", "text": prompt}]
for url in image_urls:
content.append({
"type": "image_url",
"image_url": {"url": url}
})
if verbose:
print(f"Prompt: {prompt}")
print(f"Input images: {len(image_urls)}")
print(f"Model: {model}")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [{"role": "user", "content": content}]
}
try:
response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
result_content = result['choices'][0]['message']['content']
# Try to extract URLs first
urls = re.findall(r'!\[.*?\]\((https?://[^)]+)\)', result_content)
results = []
if urls:
for url in urls:
results.append(("url", url))
else:
# Try to extract base64 data
base64_matches = re.findall(r'data:image/[^;]+;base64,([A-Za-z0-9+/=]+)', result_content)
if not base64_matches:
# Maybe it's just raw base64 in the content
base64_matches = re.findall(r'([A-Za-z0-9+/]{100,}={0,2})', result_content)
for b64 in base64_matches:
results.append(("base64", b64))
if verbose and results:
print(f"Generated {len(results)} image(s)")
return results, result
except requests.exceptions.RequestException as e:
print(f"API call failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
return [], None
def download_image(url: str, output_path: Path):
"""Download image from URL"""
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(response.content)
print(f"Saved: {output_path}")
return True
except Exception as e:
print(f"Download failed: {e}")
return False
def save_base64(b64_data: str, output_path: Path):
"""Save base64 encoded image"""
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
image_data = base64.b64decode(b64_data)
output_path.write_bytes(image_data)
print(f"Saved: {output_path}")
return True
except Exception as e:
print(f"Save failed: {e}")
return False
def main():
parser = argparse.ArgumentParser(
description="Image Editing - LaoZhang API",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Models:
gpt-4o-image $0.01/img, returns URL (default)
sora_image $0.01/img, returns URL
gemini-2.5-flash-image $0.025/img, returns base64
gemini-3.1-flash-image-preview $0.03/img, returns base64
gemini-3-pro-image-preview $0.05/img, returns base64
Preset Styles:
cartoon - Disney cartoon style
oil-painting - Classic oil painting
ink-wash - Chinese ink wash painting
cyberpunk - Neon light effects
sketch - Pencil sketch
watercolor - Watercolor painting
Examples:
%(prog)s "https://example.com/cat.jpg" "Change fur to rainbow"
%(prog)s "https://example.com/photo.jpg" --style cartoon
%(prog)s "https://a.jpg,https://b.jpg" "Merge these images"
"""
)
parser.add_argument("images", help="Image URL(s), comma-separated for multiple")
parser.add_argument("prompt", nargs="?", help="Edit description (optional with --style)")
parser.add_argument("--style", "-s", choices=list(STYLE_TEMPLATES.keys()),
help="Preset style")
parser.add_argument("--model", "-m", choices=list(AVAILABLE_MODELS.keys()),
help=f"Model to use (default: {DEFAULT_MODEL})")
parser.add_argument("--output", "-o", type=Path, help="Output image path")
parser.add_argument("--token", "-t", help="API token (or read from ~/.laozhang_api_token)")
parser.add_argument("--verbose", "-v", action="store_true", help="Show details")
parser.add_argument("--json", action="store_true", help="Output full JSON response")
parser.add_argument("--no-save", action="store_true", help="Don't save (show URL only)")
args = parser.parse_args()
# Handle preset style
if args.style:
prompt = STYLE_TEMPLATES[args.style]
elif args.prompt:
prompt = args.prompt
else:
print("Error: Either prompt or --style is required")
sys.exit(1)
# Parse image URLs
image_urls = [url.strip() for url in args.images.split(",")]
# Get token
token = args.token or get_api_token()
if not token:
print("Error: No API token provided")
print(f"Create {DEFAULT_TOKEN_PATH} with your token, or use --token")
sys.exit(1)
# Edit image
results, response = edit_image(image_urls, prompt, token, args.model, args.verbose)
if not results:
print("Failed to generate image")
sys.exit(1)
# Output results
for i, (img_type, data) in enumerate(results, 1):
if img_type == "url":
print(f"Image {i}: {data}")
else:
print(f"Image {i}: [base64, {len(data)} chars]")
# Save image
if not args.no_save and results:
if args.output:
output_path = args.output
else:
style_suffix = args.style or ""
filename = generate_filename(prompt, style_suffix)
output_path = DEFAULT_OUTPUT_DIR / filename
img_type, data = results[0]
if img_type == "url":
download_image(data, output_path)
else:
save_base64(data, output_path)
# Output JSON
if args.json and response:
print("\nFull response:")
print(json.dumps(response, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/generate_image.py
#!/usr/bin/env python3
"""
Image Generation Script - LaoZhang API
Usage: python generate_image.py "prompt" [--model MODEL] [--ratio RATIO] [--output PATH]
"""
import argparse
import json
import re
import sys
from pathlib import Path
from datetime import datetime
try:
import requests
except ImportError:
print("Install requests: pip install requests")
sys.exit(1)
# API Configuration
API_URL = "https://api.laozhang.ai/v1/chat/completions"
# Available Models
AVAILABLE_MODELS = {
"sora_image": "Sora Image - $0.01/img, returns URL",
"gpt-4o-image": "GPT-4o Image - $0.01/img, returns URL",
"gemini-2.5-flash-image": "Nano Banana - $0.025/img, returns base64",
"gemini-3.1-flash-image-preview": "Nano Banana2 - $0.03/img, returns base64",
"gemini-3-pro-image-preview": "Nano Banana Pro - $0.05/img, returns base64",
}
DEFAULT_MODEL = "sora_image"
DEFAULT_TOKEN_PATH = Path.home() / ".laozhang_api_token"
DEFAULT_OUTPUT_DIR = Path.cwd() / "generated-images"
def get_api_token():
"""Get API token from file"""
if DEFAULT_TOKEN_PATH.exists():
return DEFAULT_TOKEN_PATH.read_text().strip()
return None
def generate_filename(prompt: str, ratio: str = None):
"""Generate filename from prompt and date"""
date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
keywords = "".join(c for c in prompt[:20] if c.isalnum() or c in " -_").strip()
keywords = keywords.replace(" ", "-")[:20]
ratio_suffix = f"-{ratio.replace(':', 'x')}" if ratio else ""
return f"{date_str}-{keywords}{ratio_suffix}.png"
def generate_image(prompt: str, token: str, model: str = None, ratio: str = None, verbose: bool = False):
"""
Generate image using specified model
Args:
prompt: Image description
token: API token
model: Model to use (default: sora_image)
ratio: Aspect ratio: "2:3", "3:2", "1:1"
verbose: Show detailed info
Returns:
Tuple of (image_urls, full_response)
"""
if model is None:
model = DEFAULT_MODEL
# Sora Image needs ratio marker
if model == "sora_image" and ratio and ratio in ["2:3", "3:2", "1:1"]:
prompt = f"{prompt}【{ratio}】"
if verbose:
print(f"Prompt: {prompt}")
print(f"Ratio: {ratio or 'default'}")
print(f"Model: {model}")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}]
}
try:
response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
content = result['choices'][0]['message']['content']
image_urls = re.findall(r'!\[.*?\]\((https?://[^)]+)\)', content)
if verbose and image_urls:
print(f"Generated {len(image_urls)} image(s)")
return image_urls, result
except requests.exceptions.RequestException as e:
print(f"API call failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
return [], None
def download_image(url: str, output_path: Path):
"""Download image to local file"""
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(response.content)
print(f"Saved: {output_path}")
return True
except Exception as e:
print(f"Download failed: {e}")
return False
def main():
parser = argparse.ArgumentParser(
description="Image Generation - LaoZhang API",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Models:
sora_image $0.01/img, returns URL (default)
gpt-4o-image $0.01/img, returns URL
gemini-2.5-flash-image $0.025/img, returns base64
gemini-3.1-flash-image-preview $0.03/img, returns base64
gemini-3-pro-image-preview $0.05/img, returns base64
Examples:
%(prog)s "A cute cat playing in a garden"
%(prog)s "Sunset beach" --ratio 3:2
%(prog)s "A puppy" --output dog.png
%(prog)s "Futuristic city" --model gpt-4o-image
"""
)
parser.add_argument("prompt", help="Image description")
parser.add_argument("--model", "-m", choices=list(AVAILABLE_MODELS.keys()),
help=f"Model to use (default: {DEFAULT_MODEL})")
parser.add_argument("--ratio", "-r", choices=["2:3", "3:2", "1:1"],
help="Aspect ratio (sora_image only)")
parser.add_argument("--output", "-o", type=Path, help="Output image path")
parser.add_argument("--token", "-t", help="API token (or read from ~/.laozhang_api_token)")
parser.add_argument("--verbose", "-v", action="store_true", help="Show details")
parser.add_argument("--json", action="store_true", help="Output full JSON response")
parser.add_argument("--no-save", action="store_true", help="Don't save (show URL only)")
args = parser.parse_args()
# Get token
token = args.token or get_api_token()
if not token:
print("Error: No API token provided")
print(f"Create {DEFAULT_TOKEN_PATH} with your token, or use --token")
sys.exit(1)
# Generate image
urls, result = generate_image(args.prompt, token, args.model, args.ratio, args.verbose)
if not urls:
print("Failed to generate image")
sys.exit(1)
# Output results
for i, url in enumerate(urls, 1):
print(f"Image {i}: {url}")
# Download image
if not args.no_save and urls:
if args.output:
output_path = args.output
else:
filename = generate_filename(args.prompt, args.ratio)
output_path = DEFAULT_OUTPUT_DIR / filename
download_image(urls[0], output_path)
# Output JSON
if args.json and result:
print("\nFull response:")
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:skill.json
{
"name": "image-gen-cheap",
"version": "1.0.0",
"description": "Generate and edit images via LaoZhang API. Supports text-to-image and image editing with multiple models. Cheapest option at $0.01/image.",
"author": "openclaw-community",
"license": "MIT",
"keywords": ["image", "generation", "ai", "text-to-image", "image-editing", "cheap"],
"repository": "https://clawhub.com/skills/image-gen-cheap"
}
微信公众号管理 skill。支持获取 Access Token、永久素材管理、发布能力(发布草稿、查询发布状态、获取已发布列表、删除发布文章)。触发词:微信公众号、wechat gzh、发布图文、素材管理。
---
name: wechat-gzh
description: "微信公众号管理 skill。支持获取 Access Token、永久素材管理、发布能力(发布草稿、查询发布状态、获取已发布列表、删除发布文章)。触发词:微信公众号、wechat gzh、发布图文、素材管理。"
permissions:
- type: context_reading
purpose: "读取微信公众号配置和凭证"
data_access: "Workspace files and environment variables"
- type: api_calls
purpose: "调用微信公众号 API"
data_access: "WeChat Official Account API (api.weixin.qq.com)"
---
# WeChat GZH - 微信公众号管理
微信公众号 API 封装,支持素材管理和发布能力。
## 功能列表
### 1. 凭证管理
- `get_stable_access_token` - 获取稳定版 Access Token
### 2. 素材管理
- `add_material` - 上传永久素材(图片/视频/语音/缩略图)
- `get_material` - 获取永久素材
### 3. 发布能力
- `add_draft` - 新建草稿
- `get_draft` - 获取草稿列表
- `delete_draft` - 删除草稿
- `submit_publish` - 发布草稿
- `get_publish_status` - 发布状态查询
- `get_published_articles` - 获取已发布图文信息
- `get_published_list` - 获取已发布的消息列表
- `delete_publish` - 删除发布文章
## 配置
首次使用需要配置 AppID 和 AppSecret:
```bash
# 创建配置文件
cat > ~/.wechat_gzh_config.json << EOF
{
"appid": "your_appid_here",
"secret": "your_appsecret_here"
}
EOF
# 设置权限
chmod 600 ~/.wechat_gzh_config.json
```
## 使用方法
### Python API
```python
from scripts.wechat_gzh import WeChatGZH
# 初始化
wechat = WeChatGZH()
# 获取 Access Token
token = wechat.get_stable_access_token()
# 上传永久素材
result = wechat.add_material("/path/to/image.jpg", "image")
print(f"Media ID: {result['media_id']}")
# 上传视频素材
result = wechat.add_material(
"/path/to/video.mp4",
"video",
video_info={"title": "我的视频", "introduction": "视频描述"}
)
# 获取已发布列表
articles = wechat.get_published_list(offset=0, count=10)
# 发布草稿
result = wechat.submit_publish(media_id="xxx")
```
### 命令行
```bash
# 获取 Access Token
python scripts/wechat_gzh.py get-token
# 上传永久素材
python scripts/wechat_gzh.py upload-material -f /path/to/image.jpg -t image
python scripts/wechat_gzh.py upload-material -f /path/to/video.mp4 -t video --title "视频标题"
# 获取已发布列表
python scripts/wechat_gzh.py list-published --offset 0 --count 10
# 发布草稿
python scripts/wechat_gzh.py publish --media-id xxx
# 查询发布状态
python scripts/wechat_gzh.py status --publish-id xxx
```
## API 端点
### 凭证
- **获取 Stable Access Token**: `POST https://api.weixin.qq.com/cgi-bin/stable_token`
### 素材
- **上传永久素材**: `POST https://api.weixin.qq.com/cgi-bin/material/add_material`
- **获取永久素材**: `POST https://api.weixin.qq.com/cgi-bin/material/get_material`
### 发布
- **新建草稿**: `POST https://api.weixin.qq.com/cgi-bin/draft/add`
- **获取草稿列表**: `POST https://api.weixin.qq.com/cgi-bin/draft/batchget`
- **删除草稿**: `POST https://api.weixin.qq.com/cgi-bin/draft/delete`
- **发布草稿**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/submit`
- **发布状态查询**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/get`
- **获取已发布图文信息**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/getarticle`
- **获取已发布的消息列表**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/batchget`
- **删除发布文章**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/delete`
## 注意事项
1. **Access Token 缓存**: Token 有效期 7200 秒,建议缓存复用
2. **调用频率**: 每分钟限制 1 万次,每天 50 万次
3. **强制刷新**: 每天限用 20 次,需间隔 30 秒
4. **IP 白名单**: 需要在公众号后台配置服务器 IP 白名单
5. **权限要求**: 部分接口需要认证服务号
## 错误处理
常见错误码:
- `40001`: invalid credential - access_token 无效
- `40007`: invalid media_id - 无效的媒体 ID
- `48001`: api unauthorized - 接口未授权
完整错误码参考:https://developers.weixin.qq.com/doc/oplatform/developers/errCode/errCode.html
## 示例场景
### 发布一篇图文
```python
# 1. 准备图文素材(需要先上传)
# 2. 创建草稿
draft = wechat.add_draft(articles=[{
"title": "标题",
"content": "正文内容",
"thumb_media_id": "封面图 media_id"
}])
# 3. 发布草稿
result = wechat.submit_publish(media_id=draft['media_id'])
# 4. 查询发布状态
status = wechat.get_publish_status(publish_id=result['publish_id'])
```
## 参考文档
- [获取稳定版接口调用凭据](https://developers.weixin.qq.com/doc/subscription/api/base/api_getstableaccesstoken.html)
- [获取永久素材](https://developers.weixin.qq.com/doc/subscription/api/material/permanent/api_getmaterial.html)
- [发布能力](https://developers.weixin.qq.com/doc/subscription/api/public/api_freepublish_batchget.html)
FILE:_meta.json
{
"name": "wechat-gzh",
"version": "1.0.0",
"description": "微信公众号管理 skill - 支持 Access Token、素材管理、发布能力",
"created": "2026-03-25",
"author": "adventurer",
"dependencies": {
"python": ">=3.7",
"requests": ">=2.25.0"
}
}
FILE:config.example.json
{
"appid": "your_appid_here",
"secret": "your_appsecret_here"
}
FILE:scripts/wechat_gzh.py
#!/usr/bin/env python3
"""
WeChat GZH API Client
微信公众号 API 封装
功能:
- 获取 Stable Access Token
- 获取永久素材
- 发布能力:草稿管理、发布文章、查询状态、删除文章
"""
import json
import time
import requests
from pathlib import Path
from typing import Optional, Dict, List, Any
from dataclasses import dataclass
@dataclass
class WeChatConfig:
"""微信公众号配置"""
appid: str
secret: str
force_refresh: bool = False
@classmethod
def load(cls, config_path: str = None) -> 'WeChatConfig':
"""从文件加载配置"""
if config_path is None:
config_path = Path.home() / ".wechat_gzh_config.json"
else:
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(
f"配置文件不存在: {config_path}\n"
f"请创建配置文件:\n"
f' echo \'{{"appid": "your_appid", "secret": "your_secret"}}\' > {config_path}\n'
f" chmod 600 {config_path}"
)
with open(config_path) as f:
data = json.load(f)
if 'appid' not in data or 'secret' not in data:
raise ValueError("配置文件必须包含 appid 和 secret")
return cls(appid=data['appid'], secret=data['secret'])
class WeChatError(Exception):
"""微信公众号 API 错误"""
def __init__(self, errcode: int, errmsg: str):
self.errcode = errcode
self.errmsg = errmsg
super().__init__(f"[{errcode}] {errmsg}")
class WeChatGZH:
"""微信公众号 API 客户端"""
BASE_URL = "https://api.weixin.qq.com/cgi-bin"
def __init__(self, config: WeChatConfig = None, config_path: str = None):
"""初始化客户端"""
if config is None:
config = WeChatConfig.load(config_path)
self.config = config
self._access_token: Optional[str] = None
self._token_expires_at: float = 0
self._session = requests.Session()
self._session.headers.update({
'Content-Type': 'application/json',
'User-Agent': 'OpenClaw-WeChatGZH/1.0'
})
# ==================== 凭证管理 ====================
def get_stable_access_token(self, force_refresh: bool = None) -> str:
"""
获取稳定版 Access Token
Args:
force_refresh: 是否强制刷新(每天限20次,需间隔30秒)
Returns:
access_token 字符串
"""
# 检查缓存的 token 是否有效(提前 5 分钟刷新)
if self._access_token and time.time() < self._token_expires_at - 300:
return self._access_token
if force_refresh is None:
force_refresh = self.config.force_refresh
url = f"{self.BASE_URL}/stable_token"
payload = {
"grant_type": "client_credential",
"appid": self.config.appid,
"secret": self.config.secret,
"force_refresh": force_refresh
}
response = self._session.post(url, json=payload, timeout=10)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
self._access_token = result['access_token']
self._token_expires_at = time.time() + result['expires_in']
return self._access_token
# ==================== 素材管理 ====================
def add_material(
self,
file_path: str,
material_type: str,
video_info: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""
上传永久素材
Args:
file_path: 本地文件路径
material_type: 素材类型 (image, voice, video, thumb)
video_info: 视频素材信息(仅 type=video 时需要)
- title: 视频标题
- introduction: 视频描述
Returns:
{
"media_id": "xxx",
"url": "xxx" (仅图片素材返回)
}
"""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
url = f"{self.BASE_URL}/material/add_material"
params = {
"access_token": self.get_stable_access_token(),
"type": material_type
}
# 根据文件扩展名推断 MIME 类型
import mimetypes
mime_type, _ = mimetypes.guess_type(str(path))
if not mime_type:
mime_type = "application/octet-stream"
with open(path, 'rb') as f:
files = {
'media': (path.name, f, mime_type)
}
# 视频素材需要额外传 description
data = None
if material_type == 'video':
if not video_info:
video_info = {
'title': path.stem,
'introduction': ''
}
data = {
'description': json.dumps(video_info, ensure_ascii=False)
}
# 临时移除 Content-Type header(requests 会自动设置 multipart)
headers = self._session.headers.copy()
if 'Content-Type' in headers:
del headers['Content-Type']
response = self._session.post(
url,
params=params,
files=files,
data=data,
headers=headers,
timeout=60
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def get_material(self, media_id: str) -> Dict[str, Any]:
"""
获取永久素材
Args:
media_id: 素材的 media_id
Returns:
素材信息(图文/视频返回 JSON,其他返回二进制)
"""
url = f"{self.BASE_URL}/material/get_material"
params = {"access_token": self.get_stable_access_token()}
payload = {"media_id": media_id}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
# 尝试解析 JSON
content_type = response.headers.get('Content-Type', '')
if 'application/json' in content_type:
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
else:
# 返回二进制内容(图片/音频等)
return {
'type': 'binary',
'content_type': content_type,
'data': response.content
}
# ==================== 草稿管理 ====================
def add_draft(self, articles: List[Dict[str, Any]]) -> Dict[str, str]:
"""
新建草稿
Args:
articles: 图文消息列表,每项包含:
- title: 标题
- content: 正文(支持 HTML)
- thumb_media_id: 封面图 media_id
- author: 作者(可选)
- digest: 摘要(可选)
- content_source_url: 原文链接(可选)
- copyright_stat: 版权声明(11=原创声明,可选)
- mp_album_id: 合集ID(可选,需先在后台创建合集)
Returns:
{"media_id": "xxx"}
"""
url = f"{self.BASE_URL}/draft/add"
params = {"access_token": self.get_stable_access_token()}
payload = {"articles": articles}
# 使用 ensure_ascii=False 保持中文字符不被转义
headers = {'Content-Type': 'application/json; charset=utf-8'}
response = self._session.post(
url,
params=params,
data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
headers=headers,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def get_draft_list(
self,
offset: int = 0,
count: int = 10,
no_content: bool = False
) -> Dict[str, Any]:
"""
获取草稿列表
Args:
offset: 偏移位置
count: 数量(1-20)
no_content: 是否不返回 content 字段
Returns:
{
"total_count": 总数,
"item_count": 本次数量,
"item": [...]
}
"""
url = f"{self.BASE_URL}/draft/batchget"
params = {"access_token": self.get_stable_access_token()}
payload = {
"offset": offset,
"count": min(count, 20),
"no_content": 1 if no_content else 0
}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def delete_draft(self, media_id: str) -> bool:
"""
删除草稿
Args:
media_id: 草稿的 media_id
Returns:
成功返回 True
"""
url = f"{self.BASE_URL}/draft/delete"
params = {"access_token": self.get_stable_access_token()}
payload = {"media_id": media_id}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return True
# ==================== 发布能力 ====================
def submit_publish(self, media_id: str) -> Dict[str, str]:
"""
发布草稿
Args:
media_id: 草稿的 media_id
Returns:
{"publish_id": "xxx"} - 发布任务 ID
"""
url = f"{self.BASE_URL}/freepublish/submit"
params = {"access_token": self.get_stable_access_token()}
payload = {"media_id": media_id}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def get_publish_status(self, publish_id: str) -> Dict[str, Any]:
"""
发布状态查询
Args:
publish_id: 发布任务 ID
Returns:
{
"publish_status": 状态(0=成功, 1=发布中, 2+失败),
"article_id": 成功后的图文 ID,
"article_detail": {...}
}
"""
url = f"{self.BASE_URL}/freepublish/get"
params = {"access_token": self.get_stable_access_token()}
payload = {"publish_id": publish_id}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def get_published_article(self, article_id: str) -> Dict[str, Any]:
"""
获取已发布图文信息
Args:
article_id: 图文消息的 article_id
Returns:
图文消息详情
"""
url = f"{self.BASE_URL}/freepublish/getarticle"
params = {"access_token": self.get_stable_access_token()}
payload = {"article_id": article_id}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def get_published_list(
self,
offset: int = 0,
count: int = 10,
no_content: bool = False
) -> Dict[str, Any]:
"""
获取已发布的消息列表
Args:
offset: 偏移位置
count: 数量(1-20)
no_content: 是否不返回 content 字段
Returns:
{
"total_count": 总数,
"item_count": 本次数量,
"item": [
{
"article_id": "xxx",
"content": {...},
"update_time": 时间戳
},
...
]
}
"""
url = f"{self.BASE_URL}/freepublish/batchget"
params = {"access_token": self.get_stable_access_token()}
payload = {
"offset": offset,
"count": min(count, 20),
"no_content": 1 if no_content else 0
}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return result
def delete_publish(self, article_id: str, index: int = 0) -> bool:
"""
删除发布文章
Args:
article_id: 图文消息的 article_id
index: 要删除的文章在图文消息中的位置(第一篇为 0)
Returns:
成功返回 True
"""
url = f"{self.BASE_URL}/freepublish/delete"
params = {"access_token": self.get_stable_access_token()}
payload = {
"article_id": article_id,
"index": index
}
response = self._session.post(
url,
params=params,
json=payload,
timeout=30
)
result = response.json()
if 'errcode' in result and result['errcode'] != 0:
raise WeChatError(result['errcode'], result.get('errmsg', 'Unknown error'))
return True
# ==================== CLI 接口 ====================
def main():
"""命令行接口"""
import argparse
parser = argparse.ArgumentParser(
description="微信公众号 API 命令行工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s get-token # 获取 Access Token
%(prog)s upload-material -f image.jpg -t image # 上传图片素材
%(prog)s upload-material -f video.mp4 -t video --title "标题" # 上传视频
%(prog)s list-published -o 0 -c 10 # 获取已发布列表
%(prog)s publish -m MEDIA_ID # 发布草稿
%(prog)s status -p PUBLISH_ID # 查询发布状态
%(prog)s article -a ARTICLE_ID # 获取图文详情
%(prog)s delete -a ARTICLE_ID # 删除发布文章
"""
)
parser.add_argument('command', choices=[
'get-token',
'list-published',
'list-draft',
'publish',
'status',
'article',
'delete',
'get-material',
'upload-material'
], help='命令')
parser.add_argument('--config', '-C', help='配置文件路径')
parser.add_argument('--appid', help='AppID(覆盖配置文件)')
parser.add_argument('--secret', help='AppSecret(覆盖配置文件)')
# 子命令参数
parser.add_argument('--offset', '-o', type=int, default=0, help='偏移位置')
parser.add_argument('--count', '-c', type=int, default=10, help='数量(1-20)')
parser.add_argument('--no-content', action='store_true', help='不返回 content 字段')
parser.add_argument('--media-id', '-m', help='素材/草稿的 media_id')
parser.add_argument('--publish-id', '-p', help='发布任务 ID')
parser.add_argument('--article-id', '-a', help='图文消息 ID')
parser.add_argument('--index', '-i', type=int, default=0, help='文章索引')
parser.add_argument('--force-refresh', action='store_true', help='强制刷新 Token')
parser.add_argument('--file', '-f', help='上传文件路径')
parser.add_argument('--type', '-t', choices=['image', 'voice', 'video', 'thumb'],
help='素材类型 (image/voice/video/thumb)')
parser.add_argument('--title', help='视频标题(仅 video 类型)')
parser.add_argument('--intro', help='视频描述(仅 video 类型)')
args = parser.parse_args()
try:
# 加载配置
config = WeChatConfig.load(args.config)
if args.appid:
config.appid = args.appid
if args.secret:
config.secret = args.secret
if args.force_refresh:
config.force_refresh = True
# 初始化客户端
wechat = WeChatGZH(config)
# 执行命令
if args.command == 'get-token':
token = wechat.get_stable_access_token()
print(f"Access Token: {token}")
elif args.command == 'list-published':
result = wechat.get_published_list(
offset=args.offset,
count=args.count,
no_content=args.no_content
)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == 'list-draft':
result = wechat.get_draft_list(
offset=args.offset,
count=args.count,
no_content=args.no_content
)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == 'publish':
if not args.media_id:
parser.error("--media-id is required for publish")
result = wechat.submit_publish(args.media_id)
print(f"✅ 发布任务已提交")
print(f"Publish ID: {result['publish_id']}")
elif args.command == 'status':
if not args.publish_id:
parser.error("--publish-id is required for status")
result = wechat.get_publish_status(args.publish_id)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == 'article':
if not args.article_id:
parser.error("--article-id is required for article")
result = wechat.get_published_article(args.article_id)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == 'delete':
if not args.article_id:
parser.error("--article-id is required for delete")
wechat.delete_publish(args.article_id, args.index)
print(f"✅ 已删除文章")
elif args.command == 'get-material':
if not args.media_id:
parser.error("--media-id is required for get-material")
result = wechat.get_material(args.media_id)
if isinstance(result, dict) and result.get('type') == 'binary':
# 保存二进制文件
import mimetypes
ext = mimetypes.guess_extension(result['content_type']) or '.bin'
filename = f"material_{args.media_id}{ext}"
with open(filename, 'wb') as f:
f.write(result['data'])
print(f"✅ 素材已保存: {filename}")
else:
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == 'upload-material':
if not args.file:
parser.error("--file is required for upload-material")
if not args.type:
parser.error("--type is required for upload-material")
video_info = None
if args.type == 'video':
video_info = {
'title': args.title or Path(args.file).stem,
'introduction': args.intro or ''
}
result = wechat.add_material(args.file, args.type, video_info)
print(f"✅ 上传成功")
print(f"Media ID: {result.get('media_id')}")
if 'url' in result:
print(f"URL: {result['url']}")
except FileNotFoundError as e:
print(f"❌ {e}")
return 1
except WeChatError as e:
print(f"❌ 微信 API 错误: {e}")
return 1
except Exception as e:
print(f"❌ 错误: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
exit(main())
文字转语音并发送到飞书。支持两种模式:API 模式(智谱/OpenAI 等)和本地模式(ChatTTS)。
---
name: voice2feishu
description: 文字转语音并发送到飞书。支持两种模式:API 模式(智谱/OpenAI 等)和本地模式(ChatTTS)。
version: 1.0.0
metadata:
openclaw:
requires:
env:
- FEISHU_APP_ID
- FEISHU_APP_SECRET
bins:
- ffmpeg
- ffprobe
- jq
- curl
---
# Voice2Feishu - 语音发送到飞书
文字 → 语音 → 飞书。两种模式任选。
## 快速开始
### 1. 配置环境变量
```bash
# 飞书配置(必需)
export FEISHU_APP_ID="cli_xxx"
export FEISHU_APP_SECRET="your_secret"
# 语音 API 配置(API 模式需要)
export TTS_API_KEY="your_api_key"
export TTS_API_URL="https://open.bigmodel.cn/api/paas/v4/audio/speech" # 智谱
# 或
export TTS_API_URL="https://api.openai.com/v1/audio/speech" # OpenAI
```
### 2. 发送语音
**API 模式(推荐)**
```bash
voice2feishu api "你好,这是一条测试消息" ou_example1234567890abcdef
```
**本地 ChatTTS 模式**
```bash
# 先启动 ChatTTS 服务
voice2feishu start-chattts
# 发送语音
voice2feishu local "你好,这是本地语音" ou_example1234567890abcdef
```
## 使用方式
```
voice2feishu <模式> <文字内容> <接收者ID> [选项]
```
### 模式
| 模式 | 说明 | 依赖 |
|------|------|------|
| `api` | 使用第三方 API 生成语音 | TTS_API_KEY, TTS_API_URL |
| `local` | 使用本地 ChatTTS | ChatTTS 服务 |
| `start-chattts` | 启动本地 ChatTTS 服务 | Python, ChatTTS |
| `stop-chattts` | 停止 ChatTTS 服务 | - |
### 参数
- **文字内容**:要转换为语音的文字
- **接收者ID**:飞书用户 open_id 或群聊 chat_id
- **选项**:
- `--voice <名称>`:指定音色(API 模式)
- `--seed <数字>`:指定随机种子(本地模式,默认 500)
- `--chat`:接收者是群聊(使用 chat_id 类型)
### 示例
```bash
# API 模式 - 发给个人
voice2feishu api "会议提醒:下午3点产品评审" ou_example1234567890abcdef
# API 模式 - 发到群聊
voice2feishu api "大家好,今天有新消息" oc_example1234567890abcdef --chat
# 本地模式 - 指定音色种子
voice2feishu local "这是另一种声音" ou_example1234567890abcdef --seed 100
# 启动/停止 ChatTTS
voice2feishu start-chattts
voice2feishu stop-chattts
```
## 两种模式对比
| 特性 | API 模式 | 本地模式 |
|------|----------|----------|
| 音质 | 高 | 中 |
| 速度 | 快 | 中 |
| 成本 | 按 API 调用收费 | 免费(需 GPU 更佳) |
| 隐私 | 文字发送到第三方 | 完全本地处理 |
| 音色 | 取决于 API | 可调 seed 随机生成 |
| 依赖 | API Key | ChatTTS + Python |
## 支持的 TTS API
### 智谱 GLM-4-Voice(推荐)
```bash
export TTS_API_URL="https://open.bigmodel.cn/api/paas/v4/audio/speech"
export TTS_API_KEY="your_zhipu_api_key"
```
支持音色:`alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`
### OpenAI TTS
```bash
export TTS_API_URL="https://api.openai.com/v1/audio/speech"
export TTS_API_KEY="your_openai_api_key"
```
支持音色:`alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`
### 其他兼容 API
只要 API 格式兼容 OpenAI TTS(POST JSON,返回 audio/mpeg),都可以使用。
## 本地 ChatTTS 配置
### 安装 ChatTTS
```bash
# 方式 1:pip 安装
pip install ChatTTS
# 方式 2:克隆源码
git clone https://github.com/2noise/ChatTTS.git
cd ChatTTS
pip install -e .
```
### 模型下载
ChatTTS 首次运行时会自动下载模型(约 2GB)。如果自动下载失败,可手动下载:
**模型地址**:
- Hugging Face: https://huggingface.co/2Noise/ChatTTS
- ModelScope(国内推荐): https://modelscope.cn/models/pkuchoong/ChatTTS
**手动下载步骤**:
```bash
# 方式 1:使用 huggingface-cli
pip install huggingface_hub
huggingface-cli download 2Noise/ChatTTS --local-dir ~/.cache/huggingface/hub/models--2Noise--ChatTTS
# 方式 2:使用 modelscope(国内更快)
pip install modelscope
python -c "from modelscope import snapshot_download; snapshot_download('pkuchoong/ChatTTS', cache_dir='~/.cache/modelscope')"
```
**模型缓存位置**:
- Hugging Face: `~/.cache/huggingface/hub/`
- ModelScope: `~/.cache/modelscope/`
**常见下载问题**:
| 问题 | 解决方案 |
|------|----------|
| 网络超时 | 使用 ModelScope 镜像 |
| 磁盘空间不足 | 清理缓存,确保有 3GB+ 空间 |
| 权限错误 | 检查缓存目录权限 |
| 下载中断 | 删除部分下载的文件,重新下载 |
### 启动服务
```bash
voice2feishu start-chattts
```
服务启动后监听 `http://localhost:8080`
### 设置默认音色
ChatTTS 通过 seed 控制音色。建议测试几个 seed,选一个喜欢的:
```bash
# 测试不同 seed
voice2feishu local "测试音色" ou_xxx --seed 100
voice2feishu local "测试音色" ou_xxx --seed 500
voice2feishu local "测试音色" ou_xxx --seed 1000
```
找到喜欢的 seed 后,设置环境变量:
```bash
export CHATTTS_DEFAULT_SEED=500
```
## 环境变量完整列表
```bash
# 飞书(必需)
FEISHU_APP_ID="cli_xxx"
FEISHU_APP_SECRET="xxx"
# API 模式
TTS_API_URL="https://open.bigmodel.cn/api/paas/v4/audio/speech"
TTS_API_KEY="xxx"
TTS_DEFAULT_VOICE="alloy"
# 本地模式
CHATTTS_URL="http://localhost:8080"
CHATTTS_DEFAULT_SEED=500
```
## 故障排查
### "获取 token 失败"
- 检查 FEISHU_APP_ID 和 FEISHU_APP_SECRET 是否正确
- 确认飞书应用已启用并添加了消息权限
### "ChatTTS 服务未启动"
```bash
voice2feishu start-chattts
```
### "API 调用失败"
- 检查 TTS_API_KEY 是否有效
- 检查 TTS_API_URL 是否正确
- 确认 API 账户有余额
## 文件结构
```
voice2feishu/
├── SKILL.md # 本文档
├── scripts/
│ ├── voice2feishu.sh # 主入口
│ ├── api-tts.sh # API TTS 逻辑
│ ├── local-tts.sh # 本地 ChatTTS 逻辑
│ └── upload-feishu.sh # 飞书上传逻辑
```
## 致谢
- ChatTTS: https://github.com/2noise/ChatTTS
- 智谱 AI: https://open.bigmodel.cn
- OpenAI TTS API
---
_created 2026-03-24_
FILE:README.md
# Voice2Feishu
**文字 → 语音 → 飞书**
让 AI 用声音给飞书好友/群聊发消息。
## 两种模式
| 模式 | 适合场景 | 成本 |
|------|----------|------|
| **API 模式** | 快速上手、音质要求高 | 按 API 调用收费 |
| **本地模式** | 隐私敏感、长期大量使用 | 免费(需要 Python 环境) |
## 5 分钟上手
### 1. 安装依赖
```bash
# 必需工具
brew install ffmpeg jq curl
# Python 依赖(本地模式需要)
pip install flask flask-cors ChatTTS torch torchaudio
```
### 2. 配置飞书
```bash
export FEISHU_APP_ID="cli_xxx"
export FEISHU_APP_SECRET="xxx"
```
获取方式:
1. 访问 [飞书开放平台](https://open.feishu.cn/)
2. 创建企业自建应用
3. 开通「获取与发送消息」权限
4. 获取 App ID 和 App Secret
### 3. 发送语音
**API 模式**
```bash
# 配置 TTS API
export TTS_API_KEY="your_api_key"
# 发送
./scripts/voice2feishu.sh api "你好,这是测试消息" ou_xxx
```
**本地模式**
```bash
# 启动服务
./scripts/voice2feishu.sh start-chattts
# 发送
./scripts/voice2feishu.sh local "你好,这是本地语音" ou_xxx
```
## 支持的 TTS API
### 智谱 GLM-4-Voice(推荐)
```bash
export TTS_API_URL="https://open.bigmodel.cn/api/paas/v4/audio/speech"
export TTS_API_KEY="your_zhipu_key"
```
注册地址:https://open.bigmodel.cn
### OpenAI TTS
```bash
export TTS_API_URL="https://api.openai.com/v1/audio/speech"
export TTS_API_KEY="your_openai_key"
```
### 其他
只要 API 兼容 OpenAI TTS 格式即可使用。
## 本地 ChatTTS
### 安装
```bash
pip install ChatTTS torch torchaudio flask flask-cors
```
### 启动服务
```bash
./scripts/voice2feishu.sh start-chattts
```
服务监听 `http://localhost:8080`
### 调整音色
ChatTTS 通过 `seed` 控制音色。建议测试几个值:
```bash
./scripts/voice2feishu.sh local "测试" ou_xxx --seed 100
./scripts/voice2feishu.sh local "测试" ou_xxx --seed 500
./scripts/voice2feishu.sh local "测试" ou_xxx --seed 1000
```
找到喜欢的音色后,设置默认值:
```bash
export CHATTTS_DEFAULT_SEED=500
```
## 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `FEISHU_APP_ID` | 飞书应用 ID | - |
| `FEISHU_APP_SECRET` | 飞书应用密钥 | - |
| `TTS_API_URL` | TTS API 地址 | 智谱 |
| `TTS_API_KEY` | TTS API 密钥 | - |
| `TTS_DEFAULT_VOICE` | 默认音色 | alloy |
| `CHATTTS_URL` | ChatTTS 服务地址 | http://localhost:8080 |
| `CHATTTS_DEFAULT_SEED` | 默认音色种子 | 500 |
## 常见问题
### "获取 token 失败"
- 检查 FEISHU_APP_ID 和 FEISHU_APP_SECRET
- 确认飞书应用已启用
- 确认已开通消息权限
### "ChatTTS 服务未启动"
```bash
./scripts/voice2feishu.sh start-chattts
```
### "API 调用失败"
- 检查 API Key 是否有效
- 确认 API 账户有余额
### 音质不满意
- **API 模式**:尝试不同的 `--voice` 参数
- **本地模式**:调整 `--seed` 参数
## 致谢
- [ChatTTS](https://github.com/2noise/ChatTTS)
- [智谱 AI](https://open.bigmodel.cn)
- [OpenAI](https://openai.com)
---
_Made with ❤️ by 卜卜_
FILE:scripts/api-tts.sh
#!/bin/bash
# API TTS 模式
# 使用第三方 API 生成语音并发送到飞书
set -e
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
# 默认配置
TTS_API_URL="-https://open.bigmodel.cn/api/paas/v4/audio/speech"
TTS_API_KEY="-"
DEFAULT_VOICE="-alloy"
VOICE="$DEFAULT_VOICE"
RECEIVE_ID_TYPE="open_id"
# 解析参数
TEXT=""
RECEIVER=""
while [ $# -gt 0 ]; do
case "$1" in
--voice)
VOICE="$2"
shift 2
;;
--chat)
RECEIVE_ID_TYPE="chat_id"
shift
;;
-h|--help)
cat << EOF
用法: voice2feishu api <文字> <接收者> [选项]
选项:
--voice <名称> 指定音色(默认: $DEFAULT_VOICE)
--chat 接收者是群聊
支持的音色(取决于 API):
alloy, echo, fable, onyx, nova, shimmer
示例:
voice2feishu api "你好" ou_xxx
voice2feishu api "大家好" oc_xxx --chat --voice nova
EOF
exit 0
;;
*)
if [ -z "$TEXT" ]; then
TEXT="$1"
elif [ -z "$RECEIVER" ]; then
RECEIVER="$1"
fi
shift
;;
esac
done
# 参数检查
if [ -z "$TEXT" ]; then
echo "❌ 错误:缺少文字内容"
exit 1
fi
if [ -z "$RECEIVER" ]; then
echo "❌ 错误:缺少接收者 ID"
exit 1
fi
if [ -z "$TTS_API_KEY" ]; then
echo "❌ 错误:未设置 TTS_API_KEY 环境变量"
echo ""
echo "请设置:"
echo " export TTS_API_KEY='your_api_key'"
exit 1
fi
echo "🎙️ API TTS 模式"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📝 文字: $TEXT"
echo "🎤 音色: $VOICE"
echo "👤 接收者: $RECEIVER"
echo ""
# 1. 调用 TTS API 生成语音
echo "🔊 生成语音..."
OUTPUT_FILE="/tmp/voice2feishu-api-$(date +%s).mp3"
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$OUTPUT_FILE" \
-X POST "$TTS_API_URL" \
-H "Authorization: Bearer $TTS_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"tts-1\",
\"input\": \"$TEXT\",
\"voice\": \"$VOICE\"
}")
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ TTS API 调用失败 (HTTP $HTTP_CODE)"
if [ -f "$OUTPUT_FILE" ]; then
cat "$OUTPUT_FILE"
rm -f "$OUTPUT_FILE"
fi
exit 1
fi
if [ ! -s "$OUTPUT_FILE" ]; then
echo "❌ 生成的音频文件为空"
rm -f "$OUTPUT_FILE"
exit 1
fi
FILE_SIZE=$(ls -lh "$OUTPUT_FILE" | awk '{print $5}')
echo "✅ 语音生成成功 ($FILE_SIZE)"
# 2. 上传到飞书
echo ""
bash "$SCRIPT_DIR/upload-feishu.sh" "$OUTPUT_FILE" "$RECEIVER" "$RECEIVE_ID_TYPE"
# 3. 清理
rm -f "$OUTPUT_FILE"
FILE:scripts/chattts-server.sh
#!/bin/bash
# ChatTTS 服务器管理
# 用法: chattts-server.sh start|stop|status
set -e
CHATTTS_URL="-http://localhost:8080"
PID_FILE="/tmp/chattts-server.pid"
LOG_FILE="/tmp/chattts-server.log"
PORT="-8080"
start_server() {
# 检查是否已运行
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "✅ ChatTTS 服务已在运行 (PID: $PID)"
return 0
fi
rm -f "$PID_FILE"
fi
echo "🚀 启动 ChatTTS 服务..."
echo " 端口: $PORT"
echo " 日志: $LOG_FILE"
echo ""
# 创建简单的 ChatTTS Flask 服务
cat > /tmp/chattts_server.py << 'PYEOF'
#!/usr/bin/env python3
"""简单的 ChatTTS HTTP 服务"""
import os
import sys
import json
import tempfile
from flask import Flask, request, send_file
from flask_cors import CORS
try:
import ChatTTS
import torch
except ImportError:
print("❌ 请先安装 ChatTTS: pip install ChatTTS torch")
sys.exit(1)
app = Flask(__name__)
CORS(app)
# 初始化 ChatTTS
print("🔄 初始化 ChatTTS...")
chat = ChatTTS.Chat()
chat.load(compile=False) # compile=True 在某些平台可能有问题
print("✅ ChatTTS 初始化完成")
@app.route('/health', methods=['GET'])
def health():
return {'status': 'ok'}
@app.route('/tts', methods=['POST'])
def tts():
data = request.get_json()
text = data.get('text', '')
seed = data.get('seed', 500)
if not text:
return {'error': '缺少 text 参数'}, 400
# 设置随机种子
torch.manual_seed(seed)
# 生成语音
try:
wavs = chat.infer([text], use_decoder=True)
wav = wavs[0]
except Exception as e:
return {'error': f'生成失败: {str(e)}'}, 500
# 保存为临时文件
import torchaudio
import numpy as np
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
temp_path = f.name
# 转换并保存
if isinstance(wav, np.ndarray):
wav_tensor = torch.from_numpy(wav)
else:
wav_tensor = wav
torchaudio.save(temp_path, wav_tensor.unsqueeze(0) if wav_tensor.dim() == 1 else wav_tensor, 24000)
return send_file(temp_path, mimetype='audio/wav')
if __name__ == '__main__':
port = int(os.environ.get('CHATTTS_PORT', 8080))
print(f"🎧 ChatTTS 服务启动在端口 {port}")
app.run(host='0.0.0.0', port=port)
PYEOF
# 后台启动
nohup python3 /tmp/chattts_server.py > "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "⏳ 等待服务启动..."
sleep 5
# 检查是否成功
if curl -s --connect-timeout 3 "$CHATTTS_URL/health" > /dev/null 2>&1; then
PID=$(cat "$PID_FILE")
echo "✅ ChatTTS 服务启动成功 (PID: $PID)"
echo " 地址: $CHATTTS_URL"
else
echo "⚠️ 服务可能启动较慢,请稍后检查状态"
echo " 查看日志: tail -f $LOG_FILE"
fi
}
stop_server() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "🛑 停止 ChatTTS 服务 (PID: $PID)..."
kill "$PID"
rm -f "$PID_FILE"
echo "✅ 服务已停止"
else
echo "⚠️ 服务未运行(PID 文件存在但进程不存在)"
rm -f "$PID_FILE"
fi
else
echo "⚠️ 服务未运行(无 PID 文件)"
fi
}
status_server() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "✅ ChatTTS 服务运行中 (PID: $PID)"
echo " 地址: $CHATTTS_URL"
# 检查健康状态
if curl -s --connect-timeout 2 "$CHATTTS_URL/health" > /dev/null 2>&1; then
echo " 状态: 健康"
else
echo " 状态: 无响应"
fi
else
echo "⚠️ 服务未运行(PID 文件存在但进程不存在)"
rm -f "$PID_FILE"
fi
else
echo "ℹ️ 服务未运行"
fi
}
# 主逻辑
case "-" in
start)
start_server
;;
stop)
stop_server
;;
status)
status_server
;;
*)
echo "用法: $0 {start|stop|status}"
exit 1
;;
esac
FILE:scripts/local-tts.sh
#!/bin/bash
# 本地 ChatTTS 模式
# 使用本地 ChatTTS 服务生成语音并发送到飞书
set -e
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
# 默认配置
CHATTTS_URL="-http://localhost:8080"
DEFAULT_SEED="-500"
SEED="$DEFAULT_SEED"
RECEIVE_ID_TYPE="open_id"
# 解析参数
TEXT=""
RECEIVER=""
while [ $# -gt 0 ]; do
case "$1" in
--seed)
SEED="$2"
shift 2
;;
--chat)
RECEIVE_ID_TYPE="chat_id"
shift
;;
-h|--help)
cat << EOF
用法: voice2feishu local <文字> <接收者> [选项]
选项:
--seed <数字> 指定音色种子(默认: $DEFAULT_SEED)
--chat 接收者是群聊
示例:
voice2feishu local "你好" ou_xxx
voice2feishu local "大家好" oc_xxx --chat --seed 100
voice2feishu local "测试" ou_xxx --seed 500
提示:
- 不同 seed 产生不同音色
- 建议测试几个 seed,找到喜欢的音色
- 设置默认 seed: export CHATTTS_DEFAULT_SEED=500
EOF
exit 0
;;
*)
if [ -z "$TEXT" ]; then
TEXT="$1"
elif [ -z "$RECEIVER" ]; then
RECEIVER="$1"
fi
shift
;;
esac
done
# 参数检查
if [ -z "$TEXT" ]; then
echo "❌ 错误:缺少文字内容"
exit 1
fi
if [ -z "$RECEIVER" ]; then
echo "❌ 错误:缺少接收者 ID"
exit 1
fi
echo "🎙️ 本地 ChatTTS 模式"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📝 文字: $TEXT"
echo "🎲 种子: $SEED"
echo "👤 接收者: $RECEIVER"
echo ""
# 1. 检查 ChatTTS 服务
echo "🔍 检查 ChatTTS 服务..."
if ! curl -s --connect-timeout 3 "$CHATTTS_URL/health" > /dev/null 2>&1; then
echo "❌ ChatTTS 服务未启动"
echo ""
echo "请先启动服务:"
echo " voice2feishu start-chattts"
echo ""
echo "或检查 CHATTTS_URL 环境变量(当前: $CHATTTS_URL)"
exit 1
fi
echo "✅ ChatTTS 服务正常"
# 2. 调用 ChatTTS 生成语音
echo ""
echo "🔊 生成语音..."
OUTPUT_FILE="/tmp/voice2feishu-local-$(date +%s).wav"
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$OUTPUT_FILE" \
-X POST "$CHATTTS_URL/tts" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"$TEXT\",
\"seed\": $SEED
}")
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ ChatTTS 调用失败 (HTTP $HTTP_CODE)"
if [ -f "$OUTPUT_FILE" ]; then
cat "$OUTPUT_FILE" 2>/dev/null || true
rm -f "$OUTPUT_FILE"
fi
exit 1
fi
if [ ! -s "$OUTPUT_FILE" ]; then
echo "❌ 生成的音频文件为空"
rm -f "$OUTPUT_FILE"
exit 1
fi
FILE_SIZE=$(ls -lh "$OUTPUT_FILE" | awk '{print $5}')
echo "✅ 语音生成成功 ($FILE_SIZE)"
# 3. 上传到飞书
echo ""
bash "$SCRIPT_DIR/upload-feishu.sh" "$OUTPUT_FILE" "$RECEIVER" "$RECEIVE_ID_TYPE"
# 4. 清理
rm -f "$OUTPUT_FILE"
FILE:scripts/upload-feishu.sh
#!/bin/bash
# 上传音频到飞书
# 用法: upload-feishu.sh <音频文件> <接收者ID> [接收者类型]
set -e
# 配置
APP_ID="FEISHU_APP_ID"
APP_SECRET="FEISHU_APP_SECRET"
# 参数
AUDIO_FILE="$1"
RECEIVER="$2"
RECEIVE_ID_TYPE="-open_id"
# 参数检查
if [ -z "$AUDIO_FILE" ] || [ -z "$RECEIVER" ]; then
echo "❌ 错误:缺少参数"
echo "用法: $0 <音频文件> <接收者ID> [open_id|chat_id]"
exit 1
fi
if [ ! -f "$AUDIO_FILE" ]; then
echo "❌ 错误:文件不存在: $AUDIO_FILE"
exit 1
fi
if [ -z "$APP_ID" ] || [ -z "$APP_SECRET" ]; then
echo "❌ 错误:未设置飞书环境变量"
echo ""
echo "请设置:"
echo " export FEISHU_APP_ID='cli_xxx'"
echo " export FEISHU_APP_SECRET='xxx'"
exit 1
fi
echo "📤 上传到飞书"
# 1. 获取飞书 token
TOKEN_RESPONSE=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
-H "Content-Type: application/json" \
-d "{\"app_id\": \"$APP_ID\", \"app_secret\": \"$APP_SECRET\"}")
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.tenant_access_token')
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
echo "❌ 获取 token 失败"
echo "$TOKEN_RESPONSE" | jq .
exit 1
fi
# 2. 转换为 opus 格式(如果需要)
FILE_EXT="AUDIO_FILE##*."
FILE_EXT_LOWER=$(echo "$FILE_EXT" | tr '[:upper:]' '[:lower:]')
if [ "$FILE_EXT_LOWER" = "opus" ] || [ "$FILE_EXT_LOWER" = "ogg" ]; then
OPUS_FILE="$AUDIO_FILE"
else
OPUS_FILE="/tmp/feishu-upload-$(date +%s).opus"
ffmpeg -y -i "$AUDIO_FILE" \
-c:a libopus \
-b:a 24k \
-ar 24000 \
-ac 1 \
"$OPUS_FILE" > /dev/null 2>&1
if [ ! -f "$OPUS_FILE" ]; then
echo "❌ 格式转换失败"
exit 1
fi
fi
# 3. 读取音频时长(毫秒)
EXACT_DURATION=$(ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 "$OPUS_FILE")
if [ -z "$EXACT_DURATION" ]; then
echo "❌ 无法读取时长"
exit 1
fi
DURATION_MS=$(awk "BEGIN {printf \"%.0f\", $EXACT_DURATION * 1000}")
DUR_SEC=$(awk "BEGIN {printf \"%.1f\", $DURATION_MS / 1000}")
# 4. 上传文件到飞书
UPLOAD_RESPONSE=$(curl -s -X POST "https://open.feishu.cn/open-apis/im/v1/files" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@$OPUS_FILE" \
-F "file_type=opus" \
-F "file_name=voice.opus" \
-F "duration=$DURATION_MS")
UPLOAD_CODE=$(echo "$UPLOAD_RESPONSE" | jq -r '.code')
if [ "$UPLOAD_CODE" != "0" ]; then
echo "❌ 上传失败"
echo "$UPLOAD_RESPONSE" | jq .
exit 1
fi
FILE_KEY=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.file_key')
# 5. 发送音频消息
SEND_RESPONSE=$(curl -s -X POST "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=$RECEIVE_ID_TYPE" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"receive_id\": \"$RECEIVER\",
\"msg_type\": \"audio\",
\"content\": \"{\\\"file_key\\\": \\\"$FILE_KEY\\\", \\\"duration\\\": $DURATION_MS}\"
}")
SEND_CODE=$(echo "$SEND_RESPONSE" | jq -r '.code')
if [ "$SEND_CODE" != "0" ]; then
echo "❌ 发送失败"
echo "$SEND_RESPONSE" | jq .
exit 1
fi
# 6. 清理临时文件
if [ "$OPUS_FILE" != "$AUDIO_FILE" ]; then
rm -f "$OPUS_FILE"
fi
# 7. 完成
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎉 语音消息发送成功!"
echo ""
echo "📊 统计:"
echo " • 时长: DUR_SEC 秒"
echo " • 接收者: $RECEIVER"
echo ""
FILE:scripts/voice2feishu.sh
#!/bin/bash
# voice2feishu 主入口
# 用法: voice2feishu <模式> <文字> <接收者> [选项]
set -e
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
# 显示帮助
show_help() {
cat << EOF
Voice2Feishu - 文字转语音发送到飞书
用法:
voice2feishu <模式> <文字内容> <接收者ID> [选项]
模式:
api 使用第三方 TTS API(智谱/OpenAI)
local 使用本地 ChatTTS
start-chattts 启动 ChatTTS 服务
stop-chattts 停止 ChatTTS 服务
选项:
--voice <名称> 指定 API 音色(默认: alloy)
--seed <数字> 指定本地音色种子(默认: 500)
--chat 接收者是群聊(使用 chat_id 类型)
-h, --help 显示帮助
示例:
voice2feishu api "你好" ou_xxx
voice2feishu local "你好" ou_xxx --seed 100
voice2feishu api "大家好" oc_xxx --chat
环境变量:
FEISHU_APP_ID 飞书应用 ID(必需)
FEISHU_APP_SECRET 飞书应用密钥(必需)
TTS_API_KEY TTS API 密钥(API 模式必需)
TTS_API_URL TTS API 地址
CHATTTS_URL ChatTTS 服务地址(默认: http://localhost:8080)
EOF
}
# 参数检查
if [ $# -lt 1 ]; then
show_help
exit 1
fi
MODE="$1"
shift
case "$MODE" in
-h|--help)
show_help
exit 0
;;
api)
bash "$SCRIPT_DIR/api-tts.sh" "$@"
;;
local)
bash "$SCRIPT_DIR/local-tts.sh" "$@"
;;
start-chattts)
bash "$SCRIPT_DIR/chattts-server.sh" start
;;
stop-chattts)
bash "$SCRIPT_DIR/chattts-server.sh" stop
;;
*)
echo "❌ 未知模式: $MODE"
echo "支持的模式: api, local, start-chattts, stop-chattts"
exit 1
;;
esac