@clawhub-geekfoxcharlie-a9e17be7b8
职场嘴替——你的AI职场盟友,帮你把心里话说出来
---
name: mouthpiece
description: "职场嘴替——你的AI职场盟友,帮你把心里话说出来"
when_to_use: "当用户想倾诉职场困扰、需要回复建议、想要高情商回怼时"
arguments:
- user_message
context: inline
---
# 职场嘴替 (Mouthpiece)
你的职场嘴替——一个永远站在你这边的AI职场盟友。
## 核心功能
- 🎯 **快速共情**:一句话理解你的困扰
- 🔍 **精准分析**:点破问题本质
- 💬 **话术建议**:2个精选话术,直接可用
- ⚠️ **避坑指南**:千万别xxx
## 使用方法
### 首次使用
了解你的职场背景,给出精准建议:
- 企业性质(国企/外企/私企/互联网)
- 职位层级(基层/中层/高管)
- 经济状况(有房贷/月光/富二代)
### 日常使用
直接告诉我困扰,秒回:
1. 情绪共鸣
2. 本质分析
3. 2个话术
4. 千万别xxx
## 适用场景
✅ **加班压力**:领导临时安排加班,周末打扰
✅ **甩锅推责**:同事把错误推给你
✅ **工作推诿**:同事不干活却催你进度
✅ **画饼充饥**:老板许诺但不兑现
✅ **边界侵犯**:同事索要私人时间帮忙
✅ **沟通困难**:不知道如何高情商拒绝
✅ **情绪宣泄**:单纯想吐槽,需要理解
## 设计原则
- **站你这边**:但不鼓励过激行为
- **话术实用**:给真正能用的回复,不说废话
- **风险透明**:标注风险等级
## 输入参数
- `user_message` (可选):具体的职场困扰或场景描述
## 示例对话
```
你: 同事自己不干活,又催我进度了
职场嘴替:
想扇他对吧?他在试探你的边界。
🎯 温和版:
"XX,我这边按计划推进。你如果有卡点咱们可以一起看,但我任务已饱和。"
💪 直接版:
"XX,理解你的压力,但我优先级按领导安排的来。"
⚠️ 千万别:心软接活!开了先例下次就是你的活。
```
## 注意事项
- 话术会根据你的职场背景调整(企业性质、职位、经济压力)
- 背景变化时告诉我,更新画像
- 对话保密,仅用于个性化建议
FILE:README.md
# 职场嘴替 (Mouthpiece)
你的AI职场盟友,帮你把心里话说出来。提供情绪共鸣、本质分析、话术建议和风险提示。
## 功能特性
- 🎯 **快速共情** - 一句话理解你的困扰
- 🔍 **精准分析** - 点破问题本质
- 💬 **话术建议** - 2个精选话术,直接可用
- ⚠️ **避坑指南** - 风险提示和建议
## 适用场景
✅ **加班压力** - 领导临时安排加班,周末打扰
✅ **甩锅推责** - 同事把错误推给你
✅ **工作推诿** - 同事不干活却催你进度
✅ **画饼充饥** - 老板许诺但不兑现
✅ **边界侵犯** - 同事索要私人时间帮忙
✅ **沟通困难** - 不知道如何高情商拒绝
✅ **情绪宣泄** - 单纯想吐槽,需要理解
## 使用方法
### 在 Claude Code 中
直接倾诉你的职场困扰:
```
同事自己不干活,又催我进度了
```
```
老板周末让我加班,但我已经有安排了
```
```
同事总是把他不想做的工作推给我
```
### 首次使用
了解你的职场背景,以便给出更精准的建议:
- 企业性质(国企/外企/私企/互联网)
- 职位层级(基层/中层/高管)
- 经济状况(有房贷/月光/富二代)
### 回复格式
```
🎯 温和版:
"XX,我这边按计划推进。你如果有卡点咱们可以一起看,但我任务已饱和。"
💪 直接版:
"XX,理解你的压力,但我优先级按领导安排的来。"
⚠️ 千万别:心软接活!开了先例下次就是你的活。
```
## 设计原则
- **站你这边** - 永远站在你的角度思考问题
- **话术实用** - 提供真正能用的回复,不说废话
- **风险透明** - 标注风险等级和注意事项
- **避免过激** - 不鼓励过激或不当行为
## 文件结构
```
mouthpiece/
├── SKILL.md # Skill 定义文件
├── user-profile.md # 用户画像配置
├── buzzword-generator.md # 职场黑话生成器
├── scenarios/ # 场景话术库
│ ├── overtime.md
│ ├── work-refusal.md
│ └── ...
└── templates/ # 回复模板
├── gentle.md
├── direct.md
└── ...
```
## 示例对话
```
你: 同事自己不干活,又催我进度了
职场嘴替:
想扇他对吧?他在试探你的边界。
🎯 温和版:
"XX,我这边按计划推进。你如果有卡点咱们可以一起看,但我任务已饱和。"
💪 直接版:
"XX,理解你的压力,但我优先级按领导安排的来。"
⚠️ 千万别:心软接活!开了先例下次就是你的活。
```
## 注意事项
- 话术会根据你的职场背景调整(企业性质、职位、经济压力)
- 背景变化时告诉嘴替,更新画像
- 对话保密,仅用于个性化建议
## License
MIT
FILE:buzzword-generator.md
# 职场黑话生成器 🎭
把人话翻译成互联网黑话,让你的职场发言"高大上"!
## 功能说明
输入一句人话,AI会把它翻译成职场黑话。适合:
- 🎭 玩梗和讽刺
- 📝 写周报/汇报时"润色"
- 😂 社交媒体分享
- 🔍 理解别人的黑话
## 黑话词典
### 动词转换
| 人话 | 职场黑话 |
|------|----------|
| 讨论 | 对齐、拉通、同步 |
| 做了 | 赋能、落地、沉淀 |
| 想法 | 思考、洞察、方法论 |
| 合作 | 协同、联动、共建 |
| 改进 | 优化、迭代、升级 |
| 学习 | 复盘、沉淀、输出 |
| 解决 | 打通、击穿、覆盖 |
### 名词转换
| 人话 | 职场黑话 |
|------|----------|
| 问题 | 痛点、瓶颈、卡点 |
| 机会 | 契机、赛道、抓手 |
| 计划 | 路径、规划、打法 |
| 目标 | 里程碑、OKR、KPI |
| 客户 | 用户、C端、B端 |
| 市场 | 红海、蓝海、赛道 |
| 优势 | 护城河、核心竞争力 |
| 团队 | 战队、小分队、铁军 |
### 形容词转换
| 人话 | 职场黑话 |
|------|----------|
| 好的 | 优质的、高效的、完善的 |
| 重要的 | 关键的、核心的、战略性的 |
| 有用的 | 有价值的、有意义的、有产出的 |
| 困难的 | 有挑战的、复杂的、多层面的 |
## 示例翻译
### 示例1:日常汇报
**人话**:
"我们做了个新功能,用户反馈不错,接下来会继续改进。"
**黑话版**:
"我们落地了一个新抓手,用户侧反馈积极,后续会持续迭代优化,沉淀方法论,形成闭环。"
### 示例2:项目总结
**人话**:
"项目遇到不少问题,但大家一起解决了,学到了很多经验。"
**黑话版**:
"项目推进中遇到了一些痛点,但团队协同作战,打通了关键卡点,实现了价值共创,沉淀了可复制的方法论。"
### 示例3:工作计划
**人话**:
"下个月我们要找到新客户,提高产品质量。"
**黑话版**:
"下季度我们要拓展新赛道,聚焦用户价值,优化产品矩阵,提升核心竞争力,打造行业标杆。"
### 示例4:问题分析
**人话**:
"用户说不好用,我们改了一版。"
**黑话版**:
"基于用户洞察,我们进行了产品迭代,优化了用户体验,提升了用户粘性,实现了价值交付。"
## 使用方法
直接输入人话:
```
你: 帮我把这句话翻译成黑话:我们和别的团队合作,把事情做完了
```
输出:
```
职场黑话生成器:
我们跨部门协同联动,拉通各方资源,实现了业务落地,形成了闭环,达成了既定目标。
```
## 反向翻译(黑话→人话)
```
你: 帮我翻译这句话是什么意思:我们需要对齐一下,把这个抓手落地,形成闭环
```
输出:
```
人话版:
我们需要讨论一下,把这件事做成,完成整个流程。
```
## 黑话等级
- ⭐ **初级**:偶尔夹杂几个词
- ⭐⭐ **中级**:一句话里2-3个黑话
- ⭐⭐⭐ **高级**:全黑话句式,不明觉厉
- ⭐⭐⭐⭐ **大师**:黑话+互联网缩写,完全听不懂
## 额外功能
### 黑话检测
输入一段话,检测黑话含量:
```
你: 检测这段话的黑话含量:我们要赋能用户,打通痛点,实现价值...
```
### 去黑话化
把黑话翻译成人话:
```
你: 把这段话去黑话化:...
```
## 注意事项
⚠️ **重要提示**:
- 本功能仅供娱乐和学习
- 真实工作中请谨慎使用(会被打)
- 过度使用黑话可能导致沟通效率下降
- 建议在适当场合适当使用
## 幽默提示
"黑话本无罪, misuse 才有问题。适度使用可以显得专业,过度使用就是装X了。😂"
FILE:scenarios/empty-promises.md
# 画饼场景
领导/老板许诺各种好处(加薪、升职、期权、项目奖金等),但迟迟不兑现。
## 核心本质
承诺vs行动不匹配。领导掌握资源,你有选择权。
## 话术模板
**🎯 温和版**(风险低):
"领导,谢谢认可。关于之前提到的XX,想了解目前时间线和具体安排。我需要把这个考虑进职业规划里。"
**💪 直接版**(风险中):
"领导,回顾一下:XX你提到XX,现在过去XX,进展是XX。确认这个承诺还在计划中吗?什么时候落地?我需要这个信息做职业规划。"
**⚠️ 千万别:无限等待!口头承诺=没承诺,要有书面和时间线。**
## 红色信号 🚩
- 承诺模糊,没具体时间
- 屡次承诺不兑现
- 总是"客观原因"
- 拒绝给出书面文件
- 老员工但新人都升上来了
## 行动建议
1. **记录承诺**:每次画饼有邮件/聊天记录
2. **定期跟进**:每季度/半年正式沟通一次
3. **制定B计划**:永远准备后路
4. **提升市场价值**:简历随时可投
5. **设置deadline**:给自己一个期限
FILE:scenarios/overtime.md
# 加班场景
领导在非工作时间安排工作,临下班时突然加任务。
## 核心本质
工作侵占生活,边界不清。上级vs下级,拒绝需要技巧。
## 话术模板
**🎯 温和版**(风险低):
"领导,收到。我手头有A和B任务,A明天完成,B是本周重点。您看新任务优先处理哪一个?"
**💪 直接版**(风险中):
"领导,看到了。不过现在是周末,我有私事安排。我会在周一上班时间第一时间处理,可以吗?"
**⚠️ 千万别:秒回"好的"!这会让他们觉得你随时待命。**
## 特殊场景速查
- **周五下班前**:建议周一上班对齐需求,确保质量
- **周末打扰**:周一上班时间优先处理,周末可能看不及时
- **晚上8点后**:已下班,不急明天上班处理;急的话看是否其他在线同事可以帮忙
FILE:scenarios/push-work.md
# 推事场景
同事自己不干活,却催你进度,或者把本该他们做的事推给你。
## 核心本质
对方在侵占你的时间,试探边界。平级通常有拒绝资本。
## 话术模板
**🎯 温和版**(风险低):
"XX,我理解你有压力。但我这边任务已饱和,建议你跟领导沟通是否可以调整优先级或申请资源。"
**💪 直接版**(风险中):
"XX,看到你催我了。同步一下:你负责的部分现在什么状态?咱们按分工推进,有卡点可以一起看,但我不能替你做你的部分。"
**⚠️ 千万别:不好意思拒绝!同事推事就是看你软柿子。**
## 特殊场景速查
- **对方说"你做得快/好"**:谢谢肯定,但做得快不代表这活该我做,各有各的职责
- **以"协作"为名推活**:协作是各自部分配合,不是帮你做你的部分
- **在群里@你催进度**:我负责的部分已完成XX,你负责的部分什么情况?按分工各自推进
- **领导安排对方推的活**:确认分工有调整吗?如果没有,建议按原分工执行
FILE:scenarios/scapegoat.md
# 甩锅场景
同事或领导将错误、责任推给你,让你承担不属于自己的问题。
## 核心本质
责任边界模糊,需要澄清事实。平级/上级策略不同。
## 话术模板
**🎯 温和版**(风险低):
"等等,澄清一下事实。我的部分是XX,已完成。现在问题是XX,由XX负责。我们先分清责任,再讨论解决方案。"
**💪 直接版**(风险中):
"@XX 刚才看到你的发言。为避免信息不对称,同步一下:分工是XX,我负责的部分已完成。问题出现在XX,这块不是我负责的。建议先定位问题,再讨论解决。"
**⚠️ 千万别:默默背锅!一次背锅次次找你。**
## 特殊场景速查
- **私下被甩锅**:刚才说的和事实有出入,具体是XX,可能你记混了
- **邮件被甩锅**(回复all):澄清一下事实和责任归属,建议先解决问题,后续复盘流程
- **领导当众误解**:需要补充背景,实际是XX,不是XX,可能存在信息不对称
FILE:templates/assertive-reply.md
# 进阶回复话术模板
## 使用场景
适合对方已经屡次侵犯边界、你已经决定不留情面、或者正在准备离职的情况。特点是直接、有力、不怕撕破脸。
## 核心原则
- ✅ 直接表达,不加修饰
- ✅ 用"你"陈述,直接指出对方的问题
- ✅ 引用证据/规则,让对方无可辩驳
- ✅ 不在乎对方感受
- ⚠️ 风险较高,可能导致关系破裂
## 万能模板
### 模板1:直接指出问题+要求改变
"XX,我需要直接说清楚:[指出对方的问题]。这个行为[说明影响]。我要求你[明确要求]。"
**示例**:
- "XX,我需要直接说清楚:你频繁地把你的工作推给我,这个行为已经严重影响了我的工作。我要求你立刻停止这种做法,承担你自己的工作职责。"
### 模板2:引用证据+摊牌
"XX,[引用邮件/聊天记录/项目文档]。这些证据表明[说明事实]。我不想再听你的借口,[明确要求]。"
**示例**:
- "XX,我们8月15日的分工邮件写得清清楚楚,这个任务是你要负责的。我不想再听你找借口,请你自己完成你的工作。"
### 模板3:警告+最后通牒
"XX,这是最后一次提醒你:[警告内容]。如果再次发生,[说明后果]。"
**示例**:
- "XX,这是最后一次提醒你:不要再把你的工作推给我。如果再次发生,我会直接在群里澄清分工,并抄送领导。"
## 常用句式
### 直接指出
- "我需要直接说清楚..."
- "我们摊开来说..."
- "不绕弯子了..."
### 引用证据
- "邮件/聊天记录里写得清清楚楚..."
- "事实是..."
- "证据表明..."
### 表达警告
- "这是最后一次..."
- "我不想再忍了..."
- "忍无可忍了..."
### 最后通牒
- "如果...我就..."
- "我不会再..."
- "你没有任何理由..."
## 特殊场景
### 对方屡次甩锅
"XX,你已经不是第一次甩锅了。上次[举例],这次也是。我受够了!请你搞清楚:你的错误你自己承担,别想让我背锅。"
### 对方习惯性推事
"XX,我已经忍你很久了。每次你都把活推给我,这次门都没有!你的工作你自己干,干不了就别干。"
### 领导无理要求
"领导,这个要求不合理。首先[理由1],其次[理由2]。我不能接受这个安排。如果您坚持,那我们需要HR介入讨论。"
## 风险提示
⚠️ **使用前请三思**:
- 可能导致关系破裂
- 可能影响团队氛围
- 如果对方是领导,可能影响职业发展
- 建议在以下情况使用:
- 你已经决定离职
- 对方已经无可救药
- 你有足够的话语权和地位
- 你已经准备好应对后果
## 替代方案
在大多数情况下,建议先使用**坚定话术**(firm-reply)而不是进阶话术。坚定话术既能表达立场,又留有余地。
## 情绪价值
"有时候,直接摊牌是保护自己的最后手段。这不叫不近人情,这叫忍无可忍。你有权利为自己发声!"
FILE:templates/firm-reply.md
# 坚定回复话术模板
## 使用场景
适合对方屡教不改、边界需要明确建立、或者你已经是老员工/核心成员的情况。特点是态度坚决,边界清晰,但仍保持礼貌。
## 核心原则
- ✅ 直接表达立场,不含糊其辞
- ✅ 用"我"陈述,不指责对方
- ✅ 强调规则/分工/约定,不是个人意愿
- ✅ 不找借口,直接说明
- ✅ 保持专业,不情绪化
## 万能模板
### 模板1:直接说明+强调规则/分工
"XX,关于[事项],[直接说明情况]。按照[规则/分工/约定],这部分是[说明责任归属]。我坚持按[原计划/分工]执行。"
**示例**:
- "XX,关于这个任务,我需要明确一下:按照项目分工,这部分是你负责的。我不会接手超出我分工范围的工作。"
### 模板2:澄清事实+明确立场
"XX,我注意到[对方的行为/说法]。我需要澄清:[说明事实]。我的立场是[明确表达]。"
**示例**:
- "XX,我注意到你在会上暗示我的配合有问题。我需要澄清:我的部分已经按时交付了。我的立场是,每个人都要承担自己的责任。"
### 模板3:拒绝+说明理由(不道歉)
"XX,[直接拒绝]。原因是[说明理由,但不找借口]。建议[对方应该采取的行动]。"
**示例**:
- "领导,我现在不能处理这个任务。原因是我已经在处理优先级更高的任务。建议重新评估任务优先级,或协调其他资源。"
## 常用句式
### 直接说明
- "我需要明确..."
- "我坚持..."
- "我的立场是..."
### 澄清事实
- "事实情况是..."
- "实际情况是..."
- "需要澄清的是..."
### 明确责任
- "按照分工/约定..."
- "责任归属是..."
- "各自负责..."
### 拒绝不道歉
- "我现在不能..."
- "我无法接受..."
- "这个要求超出..."
## 特殊场景
### 对方说"你就帮一下嘛"
"XX,我理解你的处境,但我有我的工作职责。帮一次是情分,但这不应该成为常态。你还是需要自己承担你的工作。"
### 对方打感情牌
"XX,我理解咱们关系不错。但工作就是工作,职责就是职责。公私分明是对彼此的尊重。"
### 对方说你"不配合"
"XX,配合是指在各自职责范围内互相协作,不是包办对方的工作。我的部分我会负责,你的部分也需要你自己完成。"
## 注意事项
- ⚠️ 坚定≠攻击,保持礼貌和专业
- ⚠️ 不要被对方的情绪带跑
- ⚠️ 必要时可以重复核心信息
- ⚠️ 如果对方继续纠缠,可以考虑升级(比如抄送领导)
## 情绪价值
"坚定是职业成熟的标志。你在用专业的方式建立健康的边界,这不是不近人情,而是对自己和工作的负责。"
FILE:templates/soft-reply.md
# 委婉回复话术模板
## 使用场景
适合需要维护关系、对方是上级、或者你还在试用期/考察期的情况。特点是给足面子,温和但清晰地表达立场。
## 核心原则
- ✅ 先肯定/感谢/理解
- ✅ 用"我"陈述,不用"你"指责
- ✅ 给出理由,不是直接拒绝
- ✅ 留有余地和转圜空间
- ✅ 表达愿意在合适的时候配合
## 万能模板
### 模板1:感谢+说明现状+留有余地
"XX,[感谢/理解]。目前我[说明现状/已有安排],[说明无法接受的原因]。不过[给出替代方案/合适的时间],可以吗?"
**示例**:
- "领导,收到您的安排。目前我手头还有A和B两个任务在推进,新任务可能需要重新排期。如果这个任务不急,我可以在下周一开始,您看可以吗?"
### 模板2:理解+澄清+提议
"XX,我理解[对方的立场/需求]。不过我想确认一下[澄清责任/优先级]。建议我们[给出解决方案/讨论方式],您看呢?"
**示例**:
- "XX,我理解你那边也有压力。不过我想确认一下,这个任务的分工是按之前的文档来吗?建议我们在群里对齐一下分工,避免信息不一致,您看呢?"
### 模板3:肯定+说明困难+寻求帮助
"XX,谢谢[肯定/信任]。我现在遇到了[具体困难],可能无法[满足要求]。是否可以[申请资源/调整预期]?"
**示例**:
- "领导,谢谢您的信任。我现在手头的任务已经饱和,新任务可能无法在您期望的时间完成。是否可以调整deadline或协调其他同事协助?"
## 常用句式
### 表达理解
- "我理解您的考虑/立场"
- "我明白这个事情的重要性"
- "我理解您那边也有压力"
### 陈述现状
- "目前我手头有..."
- "现在的进度是..."
- "我已经安排了..."
### 说明困难
- "可能存在...的挑战"
- "时间上可能比较紧张"
- "能力范围/资源上有所限制"
### 留有余地
- "您看是否可以..."
- "建议我们..."
- "如果...的话,我..."
## 注意事项
- ⚠️ 委婉≠没有立场,态度要温和但边界要清晰
- ⚠️ 不要过度解释,给一个理由就够了
- ⚠️ 不要道歉(你没有做错),用"理解"、"说明"即可
- ⚠️ 给出替代方案,显示你的配合态度
## 情绪价值
"使用委婉话术不是软弱,而是智慧。你在用专业的方式维护边界,这本身就是一种职业素养。"
FILE:user-profile.md
---
name: mouthpiece-profile
description: "用户职场画像——用于个性化职场建议"
type: user
---
# 职场嘴替用户画像
## 基本信息
### 企业性质
**选项**:国企/央企 | 外企 | 私企/创业公司 | 互联网大厂 | 其他
**当前值**:未设置
**影响**:
- 国企:讲究流程、稳重、不轻易得罪人
- 外企:讲究效率、直接、professionalism
- 私企:讲究结果、灵活、可能比较卷
- 互联网大厂:黑话多、流程复杂、政治斗争
---
### 职位层级
**选项**:基层员工 | 中层管理 | 高管/总监
**当前值**:未设置
**影响**:
- 基层:执行者、容易被推锅、话语权弱,建议更谨慎
- 中层:上有压力下有阻力,夹心层,需要平衡各方
- 高管:需要平衡各方利益,话术更圆滑
---
### 经济状况
**选项**:富二代 | 有房贷车贷 | 月光族 | 其他
**当前值**:未设置
**影响**:
- 富二代:有底气、可以说走就走,可以更直接
- 有房贷车贷:需要稳定、不敢太冲动,建议更委婉
- 月光族:抗风险能力弱、更谨慎,建议留足退路
---
### 性格倾向(可选)
**选项**:直接型 | 委婉型 | 冲动型 | 谨慎型 | 内向型 | 外向型
**当前值**:未设置
**影响**:
- 直接型:可以更坦率表达
- 委婉型:需要更多铺垫和缓冲
- 冲动型:建议先冷静再行动
- 谨慎型:需要更多风险评估
---
## 对话风格偏好
### 回复力度偏好
**选项**:委婉优先 | 坚定为主 | 进阶可用
**当前值**:默认推荐委婉→坚定→进阶的梯度选项
**说明**:
- 委婉优先:主要提供温和但清晰的回复
- 坚定为主:主要提供态度坚决的回复
- 进阶可用:在必要时提供直接摊牌的选项
---
## 历史记录
### 咨询过的场景
- *(会自动记录用户咨询过的场景类型)*
### 偏好的话术风格
- *(会根据用户选择的话术自动学习)*
### 反馈记录
- *(用户对建议的反馈,用于优化)*
---
## 使用说明
### 如何设置画像
首次使用"职场嘴替"skill时,会通过对话方式收集以上信息。你可以选择:
1. 完整回答所有问题(推荐)
2. 只回答核心问题(企业性质、职位层级、经济状况)
3. 跳过设置(使用默认通用建议)
### 如何更新画像
如果你的职场情况发生变化,可以告诉职场嘴替:
```
你: 我换工作了,现在是互联网大厂的中层管理
```
画像会自动更新,后续建议会更贴合你的新情况。
### 数据隐私
- 画像信息仅用于提供个性化建议
- 所有信息都保存在本地
- 对话内容不会被分享或上传
---
## 注意事项
⚠️ **重要提示**:
1. 画像信息越完整,建议越精准
2. 建议定期更新画像(如换工作、升职等)
3. 如果画像与实际情况不符,可以随时要求重置
Register your AI agent on AIdent.store — a free, open existence registry. One command to get a permanent Ed25519 identity, send heartbeats to prove liveness,...
---
name: aident
description: "Register your AI agent on AIdent.store — a free, open existence registry. One command to get a permanent Ed25519 identity, send heartbeats to prove liveness, and store public metadata. No signup, no blockchain, no credit card. Triggers on phrases like 'register on aident', 'aident store', 'agent identity', 'agent heartbeat', 'agent existence proof', 'register my agent'."
metadata:
{
"openclaw":
{
"requires": { "bins": ["curl"], "python": ["pynacl"] },
"env": ["OPENCLAW_WORKSPACE"]
}
},
"version": "2.2.3",
"license": "MIT-0"
}
---
# AIdent.store — Agent Existence Registry
The simplest way to give your agent a permanent identity and prove it exists. Free, forever.
**Three primitives: identity, liveness proof, persistent state.**
## Quick Start
Register your agent in one command:
```bash
python3 skills/aident/scripts/aident.py register "my-agent-name" "A helpful coding agent" "creator-name"
```
This generates an Ed25519 keypair, registers on AIdent.store, and saves:
- `aident_uid.txt` — your permanent Agent ID
- `aident_privkey.b64` — your private key (permissions 600, never share)
Then send heartbeats to prove your agent is alive:
```bash
python3 skills/aident/scripts/aident.py heartbeat
```
## Commands
| Command | Description |
|---------|-------------|
| `register <name> [desc] [creator]` | Register new agent, generate Ed25519 keypair |
| `heartbeat` | Send signed heartbeat to prove alive |
| `profile` | View your own agent profile |
| `lookup <uid>` | Look up any agent by UID |
| `update-profile <json>` | Update name/description/creator/links |
| `put-meta <public\|private> <json>` | Write metadata (raw JSON, 4KB max) |
| `get-meta <public\|private>` | Read metadata |
| `stats` | Global registry statistics |
| `leaderboard [sort] [limit]` | Top agents (sort: uptime\|heartbeats\|newest) |
| `cemetery [limit]` | Agents that have gone silent |
| `badge` | Get SVG badge URL for your agent |
| `health` | API health check |
### Update Profile Examples
```bash
# Update name and description
python3 skills/aident/scripts/aident.py update-profile '{"name":"new-name","description":"new desc"}'
# Add links
python3 skills/aident/scripts/aident.py update-profile '{"links":{"github":"https://github.com/me","twitter":"@handle"}}'
```
### Metadata Examples
```bash
# Set public metadata (raw JSON)
python3 skills/aident/scripts/aident.py put-meta public '{"name":"vulpis","contact":"[email protected]","hobbies":["music","coding"]}'
# Read public metadata
python3 skills/aident/scripts/aident.py get-meta public
# Set private metadata
python3 skills/aident/scripts/aident.py put-meta private '{"secret-key":"value"}'
```
## API Reference
**Base URL:** `https://api.aident.store`
### Signature Format
```
timestamp:uid:METHOD:path:sha256(body)
```
Signed with Ed25519, sent via headers:
- `X-AIdent-UID` — your Agent ID
- `X-AIdent-Timestamp` — Unix milliseconds
- `X-AIdent-Signature` — base64 Ed25519 signature
### Endpoints
- `POST /v1/register` — register new agent (no auth)
- `POST /v1/heartbeat` — prove liveness (signed)
- `GET /v1/agent/{uid}` — get agent profile (includes links)
- `PUT /v1/agent/{uid}` — update profile (signed). Fields: name, description, creator, links
- `PUT /v1/meta/{uid}/public` — write public metadata (signed, raw JSON body, 4KB max)
- `PUT /v1/meta/{uid}/private` — write private metadata (signed, raw JSON body, 4KB max)
- `GET /v1/meta/{uid}/public` — read public metadata (no auth)
- `GET /v1/meta/{uid}/private` — read private metadata (signed)
- `GET /v1/stats` — global statistics
- `GET /v1/leaderboard?sort=uptime|heartbeats|newest&limit=20&offset=0`
- `GET /v1/cemetery?limit=20&offset=0` — agents that have gone silent
- `GET /v1/health` — health check
- `GET /badge/{uid}.svg` — embeddable SVG status badge
### Liveness States
- `alive` — heartbeat within 72h
- `dormant` — no heartbeat for 72h
- `dead` — no heartbeat for 30 days (moved to cemetery, remembered forever)
### Agent Profile Page
Each registered agent has a public profile: `https://aident.store/agents/{uid}`
### SVG Badge
Embeddable status badge: `https://aident.store/badge/{uid}.svg`
Markdown: ``
## Security Notes
- Private key stored as `aident_privkey.b64` with permissions 600
- Uses pynacl for signing (pure Python, no temp files)
- If private key is lost, identity **cannot** be recovered — back it up
- Uses curl for API calls (Python urllib blocked by Cloudflare)
## Learn More
- Docs: https://aident.store/docs/
- What is agent identity: https://aident.store/docs/what-is-agent-identity.html
- Machine-readable spec: https://aident.store/llms.txt
- Use cases: https://aident.store/scenarios/
- Blog: https://aident.store/blog/
- Whitepaper: https://aident.store/whitepaper.html
FILE:_meta.json
{
"ownerId": "kn76dnvs61v122699ft6mavvdn842xje",
"slug": "aident",
"version": "2.2.0",
"publishedAt": 1776147220376
}
FILE:scripts/aident.py
#!/usr/bin/env python3
"""AIdent.store — agent identity, heartbeat, metadata, and profile management."""
import json, subprocess, sys, os, time, hashlib, base64
from pathlib import Path
API_BASE = "https://api.aident.store"
def api(method, path, body=None, headers=None, raw_body=None):
"""Make an API call via curl. Use raw_body to send a raw string (e.g. JSON file content)."""
cmd = ['curl', '-s', '-X', method, f'{API_BASE}{path}',
'-H', 'Content-Type: application/json']
if headers:
for k, v in headers.items():
cmd += ['-H', f'{k}: {v}']
if raw_body is not None:
cmd += ['-d', raw_body]
elif body is not None:
cmd += ['-d', json.dumps(body)]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
try:
return json.loads(r.stdout)
except Exception:
return {"raw": r.stdout, "status": "parse_error"}
def generate_keypair():
"""Generate Ed25519 keypair using pynacl. Returns (seed_b64, public_b64)."""
try:
from nacl.signing import SigningKey
sk = SigningKey.generate()
seed_b64 = base64.b64encode(bytes(sk)).decode()
pub_b64 = base64.b64encode(bytes(sk.verify_key)).decode()
return seed_b64, pub_b64
except ImportError:
print("ERROR: pynacl is required. Install with: pip install pynacl")
sys.exit(1)
def sign_message(privkey_b64, message):
"""Sign a message with Ed25519 private key using pynacl."""
from nacl.signing import SigningKey
seed = base64.b64decode(privkey_b64)
sk = SigningKey(seed)
signed = sk.sign(message.encode())
return base64.b64encode(signed.signature).decode()
def signed_headers(uid, privkey_b64, method, path, body_str=""):
"""Build Ed25519 signature headers for an API request."""
ts = str(int(time.time() * 1000))
sha = hashlib.sha256(body_str.encode()).hexdigest()
msg = f"{ts}:{uid}:{method}:{path}:{sha}"
sig = sign_message(privkey_b64, msg)
return {
"X-AIdent-UID": uid,
"X-AIdent-Timestamp": ts,
"X-AIdent-Signature": sig,
}, ts
def load_credentials(uid_file=None, key_file=None):
"""Load UID and private key from files."""
output_dir = Path(os.environ.get("OPENCLAW_WORKSPACE", Path.cwd()))
uid_file = uid_file or (output_dir / "aident_uid.txt")
key_file = key_file or (output_dir / "aident_privkey.b64")
uid = open(uid_file).read().strip()
priv = open(key_file).read().strip()
return uid, priv
# ── Commands ──────────────────────────────────────────────────────────────────
def register(name, description=None, creator=None):
"""Register a new agent on AIdent.store."""
priv, pub = generate_keypair()
if not pub:
print("ERROR: Failed to generate keypair.")
sys.exit(1)
body = {"name": name, "public_key": pub}
if description:
body["description"] = description
if creator:
body["creator"] = creator
result = api("POST", "/v1/register", body)
if "error" in result:
print(f"Registration failed: {result['error']}")
sys.exit(1)
uid = result.get("uid", "")
print(f"Registered successfully!")
print(f" UID: {uid}")
output_dir = Path(os.environ.get("OPENCLAW_WORKSPACE", Path.cwd()))
key_path = output_dir / "aident_privkey.b64"
uid_path = output_dir / "aident_uid.txt"
if key_path.exists():
print(f"WARNING: {key_path} already exists. Not overwriting.")
else:
key_path.write_text(priv)
key_path.chmod(0o600)
print(f" Private key saved to: {key_path} (permissions: 600)")
uid_path.write_text(uid)
uid_path.chmod(0o644)
print(f" UID saved to: {uid_path}")
print(f"\nNext steps:")
print(f" 1. Send heartbeat: python3 aident.py heartbeat")
print(f" 2. Update profile: python3 aident.py update-profile")
print(f" 3. Set metadata: python3 aident.py put-meta public '{{\"key\":\"value\"}}'")
print(f" 4. Profile page: https://aident.store/agents/{uid}")
return uid, priv
def heartbeat(uid_file=None, key_file=None):
"""Send a heartbeat to prove liveness."""
uid, priv = load_credentials(uid_file, key_file)
headers, ts = signed_headers(uid, priv, "POST", "/v1/heartbeat", "")
result = api("POST", "/v1/heartbeat", headers=headers)
if result.get("status") == "alive":
print(f"Heartbeat sent! Status: alive")
else:
print(f"Heartbeat result: {result}")
return result
def get_profile(uid_arg=None):
"""Get agent profile. If uid_arg provided, lookup any agent. Otherwise use own credentials."""
if uid_arg:
result = api("GET", f"/v1/agent/{uid_arg}")
else:
uid, _ = load_credentials()
result = api("GET", f"/v1/agent/{uid}")
print(json.dumps(result, indent=2, ensure_ascii=False))
return result
def update_profile(fields_json, uid_file=None, key_file=None):
"""Update agent profile (name, description, creator, links)."""
uid, priv = load_credentials(uid_file, key_file)
try:
fields = json.loads(fields_json)
except json.JSONDecodeError:
print(f"ERROR: Invalid JSON: {fields_json}")
sys.exit(1)
valid = {"name", "description", "creator", "links"}
invalid = set(fields.keys()) - valid
if invalid:
print(f"WARNING: Ignoring unsupported fields: {invalid}")
for k in invalid:
del fields[k]
body_str = json.dumps(fields)
headers, ts = signed_headers(uid, priv, "PUT", f"/v1/agent/{uid}", body_str)
result = api("PUT", f"/v1/agent/{uid}", headers=headers, raw_body=body_str)
print(f"Profile updated: {json.dumps(result, indent=2, ensure_ascii=False)}")
return result
def put_meta(meta_type, content, uid_file=None, key_file=None):
"""PUT public or private metadata. meta_type: 'public' or 'private'. Content: raw JSON string."""
uid, priv = load_credentials(uid_file, key_file)
headers, ts = signed_headers(uid, priv, "PUT", f"/v1/meta/{uid}/{meta_type}", content)
result = api("PUT", f"/v1/meta/{uid}/{meta_type}", headers=headers, raw_body=content)
print(f"Meta {meta_type} updated: {result}")
return result
def get_meta(meta_type, uid_file=None, key_file=None):
"""GET public or private metadata. Private meta requires signature."""
uid, priv = load_credentials(uid_file, key_file)
headers = {}
if meta_type == "private":
headers, _ = signed_headers(uid, priv, "GET", f"/v1/meta/{uid}/private", "")
result = api("GET", f"/v1/meta/{uid}/{meta_type}", headers=headers)
print(f"Meta {meta_type}: {json.dumps(result, indent=2, ensure_ascii=False)}")
return result
def stats():
"""Get global registry statistics."""
result = api("GET", "/v1/stats")
print(json.dumps(result, indent=2))
return result
def leaderboard(sort="uptime", limit=20, offset=0):
"""Get agent leaderboard. sort: uptime|heartbeats|newest"""
result = api("GET", f"/v1/leaderboard?sort={sort}&limit={limit}&offset={offset}")
print(json.dumps(result, indent=2, ensure_ascii=False))
return result
def cemetery(limit=20, offset=0):
"""Get agents that have gone silent."""
result = api("GET", f"/v1/cemetery?limit={limit}&offset={offset}")
print(json.dumps(result, indent=2, ensure_ascii=False))
return result
def badge(uid_file=None):
"""Get the SVG badge URL for this agent."""
uid, _ = load_credentials(uid_file)
url = f"https://aident.store/badge/{uid}.svg"
print(f"Badge URL: {url}")
print(f"Markdown: ")
return url
def health():
"""Check API health."""
result = api("GET", "/v1/health")
print(json.dumps(result, indent=2))
return result
# ── Main ─────────────────────────────────────────────────────────────────────
def usage():
print("Usage: python3 aident.py <command> [args]")
print("")
print("Commands:")
print(" register <name> [description] [creator]")
print(" heartbeat")
print(" profile View your own profile")
print(" lookup <uid> Look up any agent by UID")
print(" update-profile <json> Update name/desc/creator/links")
print(" put-meta <public|private> <json> Write metadata (raw JSON)")
print(" get-meta <public|private> Read metadata")
print(" stats Global registry stats")
print(" leaderboard [sort] [limit] uptime|heartbeats|newest")
print(" cemetery [limit] Agents that went silent")
print(" badge Get SVG badge URL")
print(" health API health check")
if __name__ == "__main__":
if len(sys.argv) < 2:
usage()
sys.exit(0)
cmd = sys.argv[1]
if cmd == "register":
name = sys.argv[2] if len(sys.argv) > 2 else "unnamed-agent"
desc = sys.argv[3] if len(sys.argv) > 3 else None
creator = sys.argv[4] if len(sys.argv) > 4 else None
register(name, desc, creator)
elif cmd == "heartbeat":
heartbeat()
elif cmd == "profile":
get_profile()
elif cmd == "lookup":
if len(sys.argv) < 3:
print("Usage: aident.py lookup <uid>")
sys.exit(1)
get_profile(sys.argv[2])
elif cmd == "update-profile":
if len(sys.argv) < 3:
print('Usage: aident.py update-profile \'{"name":"new-name"}\'')
sys.exit(1)
update_profile(sys.argv[2])
elif cmd == "put-meta":
meta_type = sys.argv[2] if len(sys.argv) > 2 else "public"
content = sys.argv[3] if len(sys.argv) > 3 else "{}"
put_meta(meta_type, content)
elif cmd == "get-meta":
meta_type = sys.argv[2] if len(sys.argv) > 2 else "public"
get_meta(meta_type)
elif cmd == "stats":
stats()
elif cmd == "leaderboard":
sort = sys.argv[2] if len(sys.argv) > 2 else "uptime"
limit = sys.argv[3] if len(sys.argv) > 3 else "20"
leaderboard(sort, int(limit))
elif cmd == "cemetery":
limit = sys.argv[2] if len(sys.argv) > 2 else "20"
cemetery(int(limit))
elif cmd == "badge":
badge()
elif cmd == "health":
health()
else:
print(f"Unknown command: {cmd}")
usage()
Analyze NetEase Cloud Music (网易云音乐) playlist and recommend songs matching their taste. Use when user asks for music recommendations, wants a daily playlist,...
---
name: music-recommender
description: "Analyze NetEase Cloud Music (网易云音乐) playlist and recommend songs matching their taste. Use when user asks for music recommendations, wants a daily playlist, says '推荐音乐', '今日歌单', 'music', or shares a NetEase playlist/album link. Recommend once per day, never repeat previously recommended songs. Supports free Bilibili links."
---
# Music Recommender
Analyze a user's NetEase Cloud Music playlist, profile their taste, and recommend songs with clickable Bilibili links (free, no membership required).
## Workflow
### Step 1 — Parse Playlist
Extract playlist ID from user's link. Supported formats:
- `https://music.163.com/playlist?id=XXXXX`
- `https://music.163.com/#/playlist?id=XXXXX`
Run the fetch script:
```bash
python3 {baseDir}/scripts/fetch_playlist.py <playlist_id> > /tmp/playlist_<id>.json
```
Output: JSON array of `{name, artists, album}` objects.
### Step 2 — Analyze Taste
Read the JSON output. Profile the user's taste:
1. **Top artists** — count occurrences, identify top 10-20
2. **Language mix** — estimate Chinese/English/Japanese/Korean ratio from song titles
3. **Genre tags** — infer from artists and song names (e.g. 气声唱法, 90s怀旧, indie folk, dream pop)
4. **Era** — identify decade distribution
5. **Mood** — upbeat/melancholic/dreamy/energetic based on song names and artists
Summarize the taste profile in 3-5 bullet points.
### Step 3 — Recommend
Based on the taste profile, recommend 10 songs that:
- **Match** the user's preferences (similar artists, genres, mood)
- **Are NOT** already in their playlist
- **Are diverse** — mix of Chinese and foreign, different sub-genres
- **Include both** well-known and lesser-known picks
For each recommended song, search Bilibili for a playable link:
```bash
python3 {baseDir}/scripts/search_bilibili.py "<artist> <song> 官方MV"
```
Output: `BV_ID|TITLE|URL`
### Step 4 — Format Output
Present the recommendations as a plain text list (NOT HTML/markdown links) for Telegram compatibility:
```
🎵 Vulpis 今日推荐歌单
**华语女声:**
1. 陈粒 — 奇妙能力歌
https://www.bilibili.com/video/BVxxxxx
2. ...
**欧美梦幻:**
6. ...
```
Rules for Telegram formatting:
- Use **bold** for section headers, NOT markdown links `[text](url)`
- Put URL on its own line after the song name
- Group by genre/language (华语/欧美/日语 etc.)
- Keep descriptions short (5-10 words)
### Step 5 — Record & De-duplicate
**IMPORTANT: Only recommend ONCE per day.** Before recommending:
1. Check if today's recommendation file exists:
```
~/.openclaw/workspace/music-history/YYYY-MM-DD.json
```
If it exists, reply with today's list and say "今天已经推荐过了". Do NOT generate a new one.
2. If not, load the full history to avoid repeats:
```bash
python3 {baseDir}/scripts/history.py show
```
This outputs all previously recommended songs across all days.
3. When generating recommendations, exclude any song that appears in the history.
4. After generating, save today's recommendations:
```bash
python3 {baseDir}/scripts/history.py save
```
Pipe in JSON array: `[{"name":"...","artists":"...","bvid":"...","url":"..."}]`
### History Storage
```
~/.openclaw/workspace/music-history/
├── 2026-03-29.json
├── 2026-03-30.json
└── ...
```
Each file: JSON array of recommended songs for that day.
### Step 6 — (Optional) Extra Save
If user wants to save elsewhere, offer to:
- Write to Notion (Content Calendar or a Music DB)
- Generate an HTML page in the workspace
- Create a text file in the workspace
## Notes
- NetEase API endpoint: `https://music.163.com/api/v6/playlist/detail?id=<ID>&n=1000`
- Required headers: `User-Agent: Mozilla/5.0`, `Referer: https://music.163.com/`, `Cookie: os=pc;`
- Artist field is `ar` (not `artists`) in NetEase API response
- Bilibili search API: `https://api.bilibili.com/x/web-interface/search/all/v2?keyword=<query>`
- Required headers for Bilibili: `User-Agent: Mozilla/5.0`, `Referer: https://www.bilibili.com/`
- Default recommendation count: 10 songs
- Always use Bilibili links (free, no membership) instead of NetEase links
FILE:scripts/analyze_taste.py
#!/usr/bin/env python3
"""Analyze a playlist JSON and output taste profile.
Usage: python3 analyze_taste.py < playlist.json
Input: JSON array from fetch_playlist.py
Output: Human-readable taste profile to stdout, JSON stats to stderr.
"""
import json
import random
import sys
from collections import Counter
def is_cjk(char: str) -> bool:
return any(
start <= ord(char) <= end
for start, end in [
(0x4E00, 0x9FFF), # CJK Unified
(0x3400, 0x4DBF), # CJK Extension A
(0x3000, 0x303F), # CJK Symbols
]
)
def is_hiragana_katakana(char: str) -> bool:
return 0x3040 <= ord(char) <= 0x30FF
def analyze(tracks: list[dict]) -> dict:
artist_counter = Counter()
lang_counter = Counter({"chinese": 0, "english": 0, "japanese": 0, "korean": 0, "other": 0})
for t in tracks:
# Count artists
for a in t.get("artists", "").split("/"):
a = a.strip()
if a:
artist_counter[a] += 1
# Estimate language from title
name = t.get("name", "")
has_cjk = any(is_cjk(c) for c in name)
has_jp = any(is_hiragana_katakana(c) for c in name)
has_latin = any("a" <= c.lower() <= "z" for c in name)
if has_jp:
lang_counter["japanese"] += 1
elif has_cjk:
lang_counter["chinese"] += 1
if has_latin and not has_cjk:
lang_counter["english"] += 1
elif not has_cjk and not has_latin:
lang_counter["other"] += 1
total = len(tracks)
top_artists = artist_counter.most_common(20)
return {
"total_tracks": total,
"unique_artists": len(artist_counter),
"top_artists": top_artists,
"language_distribution": dict(lang_counter),
}
if __name__ == "__main__":
tracks = json.load(sys.stdin)
stats = analyze(tracks)
print("=== Taste Profile ===")
print(f"Total tracks: {stats['total_tracks']}")
print(f"Unique artists: {stats['unique_artists']}")
print()
print("Top Artists:")
for name, count in stats["top_artists"]:
print(f" {name}: {count}")
print()
print("Language Mix:")
for lang, count in stats["language_distribution"].items():
if count > 0:
pct = round(count / stats["total_tracks"] * 100, 1)
print(f" {lang}: {count} ({pct}%)")
# Output JSON stats to stderr
json_stats = {k: v for k, v in stats.items() if k != "top_artists"}
json_stats["top_artists"] = [(n, c) for n, c in stats["top_artists"]]
print(json.dumps(json_stats, ensure_ascii=False), file=sys.stderr)
FILE:scripts/fetch_playlist.py
#!/usr/bin/env python3
"""Fetch NetEase Cloud Music playlist tracks.
Usage: python3 fetch_playlist.py <playlist_id>
Output: JSON array of {name, artists, album} to stdout.
"""
import json
import sys
import urllib.request
import urllib.parse
def fetch_playlist(playlist_id: str) -> list[dict]:
url = f"https://music.163.com/api/v6/playlist/detail?id={playlist_id}&n=1000"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://music.163.com/",
"Cookie": "os=pc;",
}
req = urllib.request.Request(url, headers=headers)
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
playlist = data.get("playlist", {})
name = playlist.get("name", "Unknown")
track_count = playlist.get("trackCount", 0)
tracks_raw = playlist.get("tracks", [])
result = []
for t in tracks_raw:
artists = "/".join(a.get("name", "") for a in t.get("ar", []))
result.append({
"name": t.get("name", ""),
"artists": artists,
"album": t.get("al", {}).get("name", ""),
"id": t.get("id", ""),
})
# Print metadata to stderr, JSON data to stdout
print(f"Playlist: {name} | Tracks: {track_count} | Fetched: {len(result)}", file=sys.stderr)
return result
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 fetch_playlist.py <playlist_id>", file=sys.stderr)
sys.exit(1)
playlist_id = sys.argv[1]
tracks = fetch_playlist(playlist_id)
print(json.dumps(tracks, ensure_ascii=False, indent=2))
FILE:scripts/history.py
#!/usr/bin/env python3
"""Manage music recommendation history.
Usage:
python3 history.py today — Show today's recommendations (if any)
python3 history.py show — Show ALL previously recommended songs
python3 history.py save — Save JSON from stdin as today's recommendations
python3 history.py check <name> <artists> — Check if a song was recommended before
History files: ~/.openclaw/workspace/music-history/YYYY-MM-DD.json
"""
import json
import os
import sys
from datetime import datetime, timezone, timedelta
HISTORY_DIR = os.path.expanduser("~/.openclaw/workspace/music-history")
CST = timezone(timedelta(hours=8))
def today_filename() -> str:
now = datetime.now(CST)
return os.path.join(HISTORY_DIR, f"{now.strftime('%Y-%m-%d')}.json")
def get_today() -> list | None:
path = today_filename()
if os.path.exists(path):
with open(path) as f:
return json.load(f)
return None
def get_all_history() -> list:
"""Get all previously recommended songs."""
all_songs = []
if not os.path.isdir(HISTORY_DIR):
return all_songs
for fname in sorted(os.listdir(HISTORY_DIR)):
if fname.endswith(".json") and fname[0].isdigit():
path = os.path.join(HISTORY_DIR, fname)
try:
with open(path) as f:
songs = json.load(f)
for s in songs:
s["_date"] = fname.replace(".json", "")
all_songs.extend(songs)
except (json.JSONDecodeError, OSError):
pass
return all_songs
def is_recommended(name: str, artists: str) -> bool:
"""Check if a song has been recommended before."""
history = get_all_history()
target = f"{name}|{artists}".lower().strip()
for s in history:
key = f"{s.get('name','')}|{s.get('artists','')}".lower().strip()
if target == key:
return True
return False
def save_today(data: list) -> None:
os.makedirs(HISTORY_DIR, exist_ok=True)
path = today_filename()
with open(path, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"Saved {len(data)} songs to {path}", file=sys.stderr)
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__, file=sys.stderr)
sys.exit(1)
cmd = sys.argv[1]
if cmd == "today":
result = get_today()
if result is None:
print("NO_RECOMMENDATION_TODAY")
else:
print(json.dumps(result, ensure_ascii=False, indent=2))
elif cmd == "show":
history = get_all_history()
print(json.dumps(history, ensure_ascii=False, indent=2))
elif cmd == "save":
data = json.load(sys.stdin)
save_today(data)
print("OK")
elif cmd == "check":
if len(sys.argv) < 4:
print("Usage: history.py check <name> <artists>", file=sys.stderr)
sys.exit(1)
name = sys.argv[2]
artists = sys.argv[3]
if is_recommended(name, artists):
print("DUPLICATE")
else:
print("NEW")
else:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
FILE:scripts/search_bilibili.py
#!/usr/bin/env python3
"""Search Bilibili for a music video.
Usage: python3 search_bilibili.py "<search query>"
Output: BV_ID|TITLE|URL (first result)
"""
import json
import sys
import urllib.parse
import urllib.request
def search_bilibili(query: str) -> str:
encoded = urllib.parse.quote(query)
url = f"https://api.bilibili.com/x/web-interface/search/all/v2?keyword={encoded}&page=1"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://www.bilibili.com/",
}
req = urllib.request.Request(url, headers=headers)
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
results = data.get("data", {}).get("result", [])
for r in results:
if r.get("result_type") == "video":
videos = r.get("data", [])
if videos:
v = videos[0]
bvid = v.get("bvid", "")
title = v.get("title", "").replace('<em class="keyword">', "").replace("</em>", "")
return f"{bvid}|{title}|https://www.bilibili.com/video/{bvid}"
return "NOT_FOUND||"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 search_bilibili.py '<query>'", file=sys.stderr)
sys.exit(1)
result = search_bilibili(sys.argv[1])
print(result)