Skills
3059 foundAgent Skills are multi-file prompts that give AI agents specialized capabilities. They include instructions, configurations, and supporting files that can be used with Claude, Cursor, Windsurf, and other AI coding assistants.
答案之书——心中默念一个问题,随机翻到一页,得到一个充满哲理的简短答案。当用户提到"答案之书"、想"翻一页"、想问命运/求指引、希望神谕回答是非题时使用。触发词:答案之书、翻一页、问问书、神谕、求一个答案。
---
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()
支持同时输入多个艺人名称,自动查找各自的演唱会/巡演信息,智能识别时间和地点相近的演出组合,规划一次出行看多场演出的最优方案,并搜索对应的往返机票和演出场馆附近酒店。适用于想在一次旅途中连看多位艺人演出的用户。
---
name: multi-concert-trip-planner
description: 支持同时输入多个艺人名称,自动查找各自的演唱会/巡演信息,智能识别时间和地点相近的演出组合,规划一次出行看多场演出的最优方案,并搜索对应的往返机票和演出场馆附近酒店。适用于想在一次旅途中连看多位艺人演出的用户。
---
# Multi-Concert Trip Planner
支持多个艺人名称输入,自动在全球巡演信息中发现时间与地点相近的演出组合,帮助用户一次出行看多场演出,并搜索对应的往返机票和场馆附近酒店,输出完整的追星出行方案。
## 核心能力
- 同时接收多个艺人名称,并行搜索各自的巡演信息
- 三层信息采集:WebSearch 摘要 → WebFetch 可靠站点 → agent-browser 浏览器渲染(处理 JS 动态页面)
- 自动发现"时间窗口"内同城市或相邻城市的演出组合
- 按组合的紧凑程度和总花费排序推荐
- 为每个推荐组合搜索往返机票,输出一站式出行方案
- 为每个推荐组合搜索演出场馆附近酒店(飞猪实时报价),自动匹配入住/退房日期
- 场次变更追踪:自动与上次搜索结果做 diff,总结新增/取消/变更的场次
## 文件结构
| 文件 | 内容 |
|------|------|
| `SKILL.md`(本文件) | 工作流程总览、参数收集、综合推荐逻辑、注意事项 |
| `concert-search.md` | 第二步:演唱会搜索策略、WebFetch 规则、提取字段 |
| `combination-matching.md` | 第三步:组合匹配算法、评分权重、都市圈参考表 |
| `flight-search.md` | 第四步:flyai 机票搜索命令、返回数据解析 |
| `hotel-search.md` | 第四步-B:flyai 酒店搜索命令、返回数据解析、筛选策略 |
| `output-template.md` | 第六步:完整输出格式模板 + 特殊场景处理 + 场次变更总结 |
| `examples.md` | 7 个交互示例(多艺人+机票+酒店、单艺人、跨城、星级预算、仅酒店、agent-browser、变更追踪) |
| `BLOCKED_SITES.md` | WebFetch 失败站点记录(持续更新) |
| `diff-tracking.md` | 场次变更追踪:快照存储格式、diff 算法、变更分类规则 |
## 工作流程
### 第一步:收集用户需求
从用户请求中提取以下参数:
- **艺人/乐队名称列表**(必填,至少 1 个)— 如用户只给了 1 个艺人,正常执行搜索(退化为单艺人模式,跳过第三步的组合匹配);如给了多个艺人,则进入多艺人组合匹配流程
- **出发城市**(仅开启机票搜索时必填 — 如用户未提供且需要搜索机票,必须主动询问)
- **时间窗口偏好**(可选,默认"未来 6 个月")— 如"今年夏天"、"下半年"、"8-10 月"
- **组合容忍天数**(可选,默认 7 天)— 两场演出之间最多间隔多少天仍视为"可组合",用户可以说"最好 1 天内"或"一周内都行"。询问时提供的选项应包含 1 天(仅限连续两天)、3 天内、7 天内、14 天内等梯度
- **是否搜索机票**(可选,默认关闭)— 机票价格来自搜索引擎摘要,仅供粗略参考,准确性有限。询问时默认关闭,用户明确要求时才开启
- **是否搜索酒店**(可选,默认关闭)— 使用飞猪搜索演出场馆附近酒店,返回实时价格和预订链接。询问时默认关闭,用户明确要求时才开启
- **酒店偏好**(可选,仅开启酒店搜索时有效)— 包括:
- 星级偏好:如"四星以上"、"经济型就好"
- 每晚预算上限:如"每晚不超过 800"
- 床型偏好:如"大床房"、"双床房"
- 酒店类型:hotel(酒店)、homestay(民宿)、inn(客栈),默认 hotel
- **预算范围**(可选,仅开启机票或酒店搜索时有效)— 如"总花费 1 万以内"、"越便宜越好"
- **地区偏好**(可选)— 如"只看亚洲"、"优先日本和韩国"、"不限地区"
若用户只提供了艺人列表,至少追问出发城市。
### 第一步-B:加载上次搜索快照
→ 详见 `diff-tracking.md`
在开始搜索前,根据本次艺人列表在 `snapshots/` 目录中查找最近一次匹配的快照。如找到,加载为 `previousSnapshot` 用于搜索完成后的 diff 对比。首次搜索该艺人组合时跳过此步。
### 第二步:并行查找各艺人演唱会信息
→ 详见 `concert-search.md`
对每个艺人使用 WebSearch 搜索巡演信息(日/英/中三语查询),通过 Task 工具并行执行。采用三层降级策略:优先从搜索摘要提取信息,对可靠站点使用 WebFetch 补充,对 JS 渲染站点使用 agent-browser 浏览器抓取。结果去重、按日期排序,过滤 14 天内场次。
### 第三步:智能组合匹配(核心逻辑)
→ 详见 `combination-matching.md`
单艺人模式跳过本步骤。多艺人模式下,用滑动窗口在时间线上扫描,按城市/都市圈分组,生成候选组合并按四维评分(艺人覆盖 40%、时间紧凑 25%、地理集中 20%、售票可行 15%)排序,取前 10 个组合。
### 第四步:搜索往返机票(可选,默认跳过)
→ 详见 `flight-search.md`
仅在用户明确要求时执行。使用 `flyai search-flight` 搜索往返机票,返回实时价格和飞猪购票链接。多组合并行搜索,每组合取最便宜的 3 个选项。
### 第四步-B:搜索演出场馆附近酒店(可选,默认跳过)
→ 详见 `hotel-search.md`
仅在用户明确要求时执行。使用 `flyai search-hotel` 以场馆名称为关键词搜索附近酒店,返回实时价格和飞猪预订链接。多城市并行搜索,每城市取前 5 家,按档次分层推荐。
### 第五步:综合整理与推荐
**如果开启了机票和/或酒店搜索:** 将组合信息、机票信息、酒店信息和城际交通(如有)合并,计算每个方案的总花费估算(总花费 = 各场门票之和 + 往返机票 + 住宿费用 + 城际交通)。
**如果未开启机票和酒店搜索(默认):** 仅基于演出信息整理推荐,不涉及机票、酒店和总花费。
**推荐逻辑:**
- 首要指标:能覆盖的艺人数量(越多越好)
- 次要指标:时间紧凑度(间隔天数越少越好)
- 参考指标:地理集中度、售票可行性
- 如有机票数据,额外参考总花费
- 标注"最佳覆盖"(看到最多艺人)和"最紧凑"(间隔最短)方案
### 第六步:呈现总结
→ 详见 `output-template.md`
按标准模板输出方案(演出安排表 + 购票链接 + 机票信息 + 酒店推荐 + 总花费估算),并处理特殊场景(艺人无演出、无可组合方案、音乐节等)。
### 第七步:保存快照 + 场次变更总结
→ 详见 `diff-tracking.md`
将本次搜索结果保存为 JSON 快照文件。如存在上次快照(第一步-B 加载),自动执行 diff 对比,在输出末尾生成「场次变更总结」,包含新增/取消/场馆变更/售票状态变更/票价变更 5 类变化。首次搜索时仅保存快照并提示用户。
## 注意事项
- 出发城市仅在用户开启机票搜索时需要,不要在未开启时追问。
- 为提升效率,多个艺人的搜索必须使用 Task 工具并行执行,而非逐一串行搜索。
- agent-browser 是最重量级的信息采集手段,仅在 WebSearch 摘要和 WebFetch 都无法获取关键信息时使用。每次使用后及时 `agent-browser close` 释放资源。详见 `concert-search.md` 第三层策略和 `BLOCKED_SITES.md` 中标记为 🟢 的站点。
- 机票价格波动较大,提醒用户价格仅供参考,建议尽早预订。
- 酒店价格同样会波动(尤其是演唱会期间热门城市),提醒用户看到合适的酒店尽早预订。
- 搜索酒店时优先使用场馆名称作为 `--key-words`,确保推荐的酒店距离场馆较近,方便观演。
- 如果场馆关键词搜索结果较少,退而使用城市核心区域(如"新宿"、"涩谷"、"梅田")作为关键词补充搜索。
- 转售平台(StubHub 等)的门票价格可能高于原价,需标注说明。
- 搜索机票时考虑演出城市对应的主要机场(如东京对应 NRT/HND,伦敦对应 LHR/LGW/STN)。
- 默认展示最多 5 个组合方案 + 未能组合的场次,除非用户要求更多。
- 尊重各网站的请求限制,合理控制搜索频率。
- 如果用户指定了预算,优先过滤掉超出预算的方案。
- 组合评分算法中的权重为默认值,如用户明确偏好(如"我更在乎省钱"),应动态调整权重。
- 每次搜索结束后必须保存快照到 `snapshots/` 目录。如存在上次快照,必须在输出末尾附上场次变更总结。详见 `diff-tracking.md`。
## 交互示例
→ 详见 `examples.md`(含 7 个完整场景:多艺人+机票+酒店、单艺人+机票、多艺人跨城、带星级预算的酒店搜索、仅搜酒店不搜机票、agent-browser 处理 JS 渲染官网、场次变更追踪 diff)
FILE:examples.md
# 交互示例
## 示例 1:多艺人 + 机票 + 酒店
**用户**:"我想看 YOASOBI 和 Ado 的演唱会,最好能一趟都看了,帮我查查"
**执行步骤**:
1. 追问:出发城市是哪里?时间范围有偏好吗?要搜索机票和酒店吗?
2. 用户回答:从上海出发,今年下半年,帮我搜机票和酒店
3. 使用 Task 工具并行搜索 YOASOBI 和 Ado 的巡演信息
4. 汇总所有场次,执行组合匹配:发现两者在 8 月都有东京场次且相隔 3 天、10 月都有大阪场次且相隔 5 天
5. 对每个组合并行搜索:上海→东京/大阪的往返机票 + 场馆附近酒店
6. 综合排序,输出包含两位艺人演出 + 机票 + 酒店的组合出行方案
7. 将无法组合的场次(如 Ado 的欧洲场次)单独列出供参考
---
## 示例 2:单艺人 + 机票(无酒店)
**用户**:"我想看 Ado 的演唱会,从深圳出发"
**执行步骤**:
1. 单艺人模式,无需组合匹配
2. 搜索 Ado 的巡演信息,找到东京、大阪、首尔、台北等场次
3. 分别搜索深圳→东京、深圳→大阪、深圳→首尔、深圳→台北的往返机票
4. 按总花费(门票 + 机票)排序,输出最优方案
---
## 示例 3:多艺人组合匹配 + 跨城交通
**用户**:"帮我看看 Coldplay、Bruno Mars、Ed Sheeran 最近在亚洲有没有时间撞上的演唱会,我从北京出发"
**执行步骤**:
1. 参数明确,直接开始搜索
2. 使用 Task 工具并行搜索三位艺人的亚洲巡演信息
3. 汇总后发现:Coldplay 11月在东京、Bruno Mars 11月在东京(相隔2天)、Ed Sheeran 11月在首尔
4. 生成组合:
- 组合A(⭐最佳):东京看 Coldplay + Bruno Mars(间隔2天,同城)
- 组合B:东京 Coldplay + 首尔 Ed Sheeran(间隔5天,需跨城)
- 组合C:三人全覆盖 — 东京2场 + 首尔1场(需额外东京→首尔交通)
5. 分别搜索机票和城际交通,输出完整方案
---
## 示例 4:单艺人 + 机票 + 酒店(带星级和预算)
**用户**:"我想看 Ado 的演唱会,从深圳出发,帮我搜一下机票和酒店,酒店要四星以上,每晚预算 1500 以内"
**执行步骤**:
1. 单艺人模式,无需组合匹配
2. 搜索 Ado 的巡演信息,找到东京、大阪、首尔、台北等场次
3. 并行搜索:
- 机票:深圳→东京、深圳→大阪、深圳→首尔、深圳→台北的往返机票
- 酒店:每个城市对应场馆附近的酒店(`--hotel-stars 4,5 --max-price 1500`)
4. 按总花费(门票 + 机票 + 住宿)排序,输出最优方案
---
## 示例 5:只搜酒店,不搜机票
**用户**:"查一下周杰伦巡演,只需要搜酒店不用搜机票,住便宜点的民宿就行"
**执行步骤**:
1. 单艺人模式,仅开启酒店搜索(不开启机票搜索)
2. 搜索周杰伦的巡演信息
3. 对每个有效场次搜索场馆附近民宿(`--hotel-types homestay --sort price_asc`)
4. 输出演出信息 + 每个城市的民宿推荐(不含机票信息)
---
## 示例 6:agent-browser 处理 JS 渲染官网
**用户**:"帮我查米津玄師和绿黄色社会下半年的演唱会,从北京出发,搜机票和酒店"
**执行步骤**:
1. 使用 Task 工具并行搜索两位艺人的巡演信息
2. WebSearch 搜索摘要中获取了米津玄師的部分日程,但缺少详细场馆和售票信息
3. 发现 ticket.kenshiyonezu.jp 是官方售票页面(BLOCKED_SITES.md 标记为 JS 渲染站点 🟢),启用 agent-browser:
```bash
agent-browser open https://ticket.kenshiyonezu.jp/pages/2026_detail
agent-browser wait 3000
agent-browser snapshot -i
# 从快照中提取完整日程(日期、场馆、票价、售票状态)
agent-browser close
```
4. 绿黄色社会的官网 ryokushaka.com/live/ 同样是 JS 渲染,agent-browser 抓取补充
5. 汇总所有场次,执行组合匹配 + 机票酒店搜索,输出完整方案
---
## 示例 7:场次变更追踪(diff)
**用户**:"再帮我查一下米津玄師和绿黄色社会下半年的演唱会"(此前已搜索过同一组艺人)
**执行步骤**:
1. 收集参数:艺人列表 = [米津玄師, 緑黄色社会],沿用上次参数
2. **加载上次快照**:在 `snapshots/` 中找到 `kenshi_yonezu_ryokushaka_20260408.json`,加载为 `previousSnapshot`
3. 使用 Task 工具并行搜索两位艺人的最新巡演信息
4. 执行组合匹配,输出最新方案
5. **保存本次快照**:写入 `kenshi_yonezu_ryokushaka_20260415.json`
6. **执行 diff**:对比上次 15 场 vs 本次 17 场
- 发现:🆕 新增 3 场(绿黄色社会追加了福冈、札幌两场 + 米津玄師新增上海场)
- 发现:❌ 取消 1 场(米津玄師 12/10 名古屋场)
- 发现:🎫 售票状态变更 2 场(米津玄師 12/3 仙台 "预售"→"在售"、12/4 仙台 "预售"→"在售")
7. 在方案输出末尾附上变更总结,特别提示"仙台场已开售,建议尽快购票"
FILE:output-template.md
# 输出格式模板
在对话中输出清晰的文字总结,使用以下格式:
```
## 多艺人追星出行方案
> 搜索艺人:{艺人A}、{艺人B}、{艺人C}
> 时间范围:{范围}
> 组合容忍天数:{N} 天
---
### 方案 1 ⭐ 最佳覆盖(覆盖 {N}/{总数} 位艺人)
📍 目的地:{城市/都市圈}
📅 行程:{起始日期} — {结束日期}(共 {N} 天)
**演出安排:**
| # | 日期 | 艺人 | 场馆 | 票价 | 状态 |
|---|------|------|------|------|------|
| 1 | {日期} | {艺人A} | {场馆} | {价格} | {状态} |
| 2 | {日期} | {艺人B} | {场馆} | {价格} | {状态} |
🔗 购票链接:
- {艺人A}:{链接}
- {艺人B}:{链接}
<!-- 以下机票部分仅在用户开启机票搜索时展示 -->
✈️ 机票(飞猪实时报价):
{航空公司} {航班号} | {出发机场}→{到达机场} | {直达/中转}
去程:{出发时间} → {到达时间}({飞行时长})
回程:{出发时间} → {到达时间}({飞行时长})
往返价格:¥{价格}
🔗 购票:{飞猪链接}
🚄 城际交通(如有):{方式} | {起点}→{终点} | ¥{价格}
<!-- 机票部分结束 -->
<!-- 以下酒店部分仅在用户开启酒店搜索时展示 -->
🏨 酒店推荐(飞猪实时报价):
| 酒店 | 档次 | 每晚价格 | 位置 | 预订 |
|------|------|----------|------|------|
| {酒店名称} | {星级/档次} | {价格} | {附近地标} | [预订链接]({飞猪链接}) |
| {酒店名称} | {星级/档次} | {价格} | {附近地标} | [预订链接]({飞猪链接}) |
| ... | | | | |
住宿费用估算:{每晚价格} × {N} 晚 = ¥{总住宿费}(以最低价酒店计算)
<!-- 酒店部分结束 -->
💰 总花费估算(如有机票+酒店数据):
门票:¥{门票总计}
机票:¥{机票价格}
住宿:¥{住宿费用}({N} 晚)
城际交通:¥{交通费用}(如有)
**合计:约 ¥{总计}**
---
### 方案 2 ⭐ 最紧凑
...
---
### 未能组合的场次
以下场次在时间或地点上无法与其他艺人组合,单独列出供参考:
| 艺人 | 日期 | 城市 | 场馆 | 备注 |
|------|------|------|------|------|
| {艺人C} | {日期} | {城市} | {场馆} | 该时段无其他艺人在附近演出 |
```
## 场次变更总结(Diff)
当存在上次搜索快照时,在主方案输出之后附上变更总结。格式如下:
```
---
## 场次变更总结
> 对比上次搜索:{上次搜索日期}({N} 天前)
> 本次搜索:{本次日期}
### 概览
| 变更类型 | 数量 |
|----------|------|
| 🆕 新增场次 | {N} |
| ❌ 取消/下架 | {N} |
| 🎫 售票状态变更 | {N} |
| 🏟️ 场馆变更 | {N} |
| 💰 票价变更 | {N} |
| ✅ 未变化 | {N} |
<!-- 仅展示有变更的分类,数量为 0 的可省略 -->
### 🆕 新增场次
| 艺人 | 日期 | 城市 | 场馆 | 票价 | 状态 |
|------|------|------|------|------|------|
| {艺人} | {日期} | {城市} | {场馆} | {票价} | {状态} |
### ❌ 取消/下架场次
| 艺人 | 日期 | 城市 | 场馆 | 说明 |
|------|------|------|------|------|
| {艺人} | {日期} | {城市} | {场馆} | 上次搜索存在,本次未找到 |
### 🎫 售票状态变更(需关注)
| 艺人 | 日期 | 城市 | 上次状态 | → | 本次状态 |
|------|------|------|----------|---|----------|
| {艺人} | {日期} | {城市} | 在售 | → | 售罄 |
<!-- 「在售→售罄」和「预售→在售」是最需要用户关注的变更,加粗或额外提醒 -->
### 🏟️ 场馆变更
| 艺人 | 日期 | 城市 | 上次场馆 | → | 本次场馆 |
|------|------|------|----------|---|----------|
| {艺人} | {日期} | {城市} | {旧场馆} | → | {新场馆} |
### 💰 票价变更
| 艺人 | 日期 | 城市 | 上次票价 | → | 本次票价 |
|------|------|------|----------|---|----------|
| {艺人} | {日期} | {城市} | {旧价格} | → | {新价格} |
```
**首次搜索时(无上次快照)的提示:**
```
---
> 📸 已保存本次搜索快照({N} 场演出),下次搜索相同艺人时将自动显示场次变更。
```
## 特殊场景处理
### 场景 1:某个艺人完全没有找到演出信息
告知用户该艺人暂无公开的巡演计划,建议关注其官方社交媒体,并继续为其他艺人生成组合方案。
### 场景 2:所有艺人都有演出,但没有找到任何可组合的方案
- 列出每位艺人各自最值得去的场次
- 说明无法组合的原因(时间差距太大 / 地区完全不同)
- 建议放宽容忍天数或地区限制,询问用户是否要调整参数重试
### 场景 3:组合中包含音乐节
如果某个艺人的演出是在音乐节上(如 Summer Sonic、Coachella),标注该场次属于音乐节,提醒用户需要购买的是音乐节通票而非单场门票,并检查同一音乐节是否还有用户列表中的其他艺人出演——如果有,这将是一个高价值组合。
FILE:diff-tracking.md
# 场次变更追踪(Diff Tracking)
每次搜索完成后,将本次搜索结果保存为快照文件。下次运行时自动加载上次快照并与本次结果做 diff,在输出末尾生成「场次变更总结」。
## 快照存储
### 文件位置
快照存储在 skill 目录下的 `snapshots/` 子目录中:
```
~/.qoderwork/skills/multi-concert-trip-planner/snapshots/
├── {快照ID}.json ← 每次搜索的结果快照
└── latest.json ← 符号链接,指向最新快照(便于快速读取上次结果)
```
### 快照 ID 命名规则
快照 ID = `{艺人列表排序后用下划线连接}_{搜索日期YYYYMMDD}`
示例:`ado_yoasobi_20260408.json`
如果同一天对相同艺人列表搜索多次,后次覆盖前次。
### 快照 JSON 结构
```json
{
"snapshotId": "kenshi_yonezu_ryokushaka_backnumber_20260408",
"createdAt": "2026-04-08T15:30:00+08:00",
"artists": ["米津玄師", "緑黄色社会", "back number"],
"timeWindow": "2026 下半年",
"shows": [
{
"id": "kenshi_yonezu_20261203_sendai",
"artist": "米津玄師",
"date": "2026-12-03",
"time": "18:00",
"venue": "セキスイハイムスーパーアリーナ",
"city": "仙台",
"country": "日本",
"price": "¥9,800",
"ticketStatus": "在售",
"ticketUrl": "https://...",
"source": "WebSearch snippet"
}
],
"totalShows": 15,
"searchDuration": "约 3 分钟"
}
```
**场次 ID 生成规则:** `{艺人名拼音/英文小写}_{日期YYYYMMDD}_{城市拼音小写}`,用于跨快照匹配同一场演出。
## 工作流程
### 搜索前:加载上次快照
1. 根据当前搜索的艺人列表,在 `snapshots/` 中查找最近一次匹配的快照
2. 匹配逻辑:艺人列表排序后完全相同(忽略大小写和空格)
3. 如果找到匹配快照,加载为 `previousSnapshot`
4. 如果没有找到(首次搜索该艺人组合),跳过 diff,搜索结束后直接保存快照
**查找命令:**
```bash
ls -t ~/.qoderwork/skills/multi-concert-trip-planner/snapshots/{艺人列表快照ID前缀}*.json | head -1
```
### 搜索后:保存快照 + 执行 diff
1. 将本次搜索的所有场次整理为快照 JSON 格式
2. 写入 `snapshots/{快照ID}.json`
3. 更新 `latest.json` 指向新快照
4. 如果存在 `previousSnapshot`,执行 diff 算法
**保存命令示例:**
```bash
# 确保 snapshots 目录存在
mkdir -p ~/.qoderwork/skills/multi-concert-trip-planner/snapshots
# 写入快照文件(通过 Write 工具)
# 更新 latest.json 符号链接
ln -sf {快照ID}.json ~/.qoderwork/skills/multi-concert-trip-planner/snapshots/latest.json
```
## Diff 算法
### 匹配规则
两条场次记录被视为"同一场演出"需满足:
- **艺人相同**(忽略大小写)
- **日期相同**(精确到天)
- **城市相同**(忽略"市"/"City"后缀,如"仙台" = "仙台市")
不依赖场次 ID 做精确匹配,因为场馆名可能在不同数据源中表述不同。
### 变更分类
对比 `previousSnapshot.shows` 和当前 `currentShows`,产出 5 类变更:
| 变更类型 | 判定逻辑 | 图标 |
|----------|----------|------|
| **新增场次** | 当前有、上次无(按艺人+日期+城市匹配不到) | 🆕 |
| **取消/下架场次** | 上次有、当前无 | ❌ |
| **场馆变更** | 同一场演出但场馆名称不同 | 🏟️ |
| **售票状态变更** | 同一场演出但售票状态变化(如"预售"→"在售"、"在售"→"售罄") | 🎫 |
| **票价变更** | 同一场演出但票价区间发生变化 | 💰 |
**优先级排序:** 取消 > 新增 > 售票状态变更 > 场馆变更 > 票价变更
### 不变场次
如果某场演出在两次快照中完全一致(日期、城市、场馆、售票状态、票价均未变),归入"不变",不在 diff 中展示。
## 输出格式
→ 详见 `output-template.md`「场次变更总结」部分
Diff 总结在主方案输出之后、末尾展示。包含:
- 上次搜索时间
- 各类变更的汇总数字
- 按变更类型分组的详细变更列表
- 需要用户关注的重点变更(如"在售→售罄"需要紧急关注)
## 注意事项
- 快照仅记录场次信息,不记录机票/酒店数据(这些实时数据每次搜索都不同,不适合做 diff)
- 如果两次搜索的时间窗口不同(如上次搜"下半年",这次搜"10-12月"),diff 时只比较两次时间窗口的交集部分,避免因搜索范围缩小而产生大量虚假"取消"
- 如果用户增减了艺人列表(如上次搜 A+B,这次搜 A+B+C),新增艺人的场次全部标为"新增",其余艺人正常做 diff
- 快照文件较小(通常 <10KB),无需定期清理。如需手动清理:`ls ~/.qoderwork/skills/multi-concert-trip-planner/snapshots/`
- 首次搜索某组艺人时,无 diff 输出,仅保存快照并提示"已保存本次搜索快照,下次搜索相同艺人时将自动显示场次变更"
FILE:combination-matching.md
# 智能组合匹配算法
**单艺人模式:** 如果用户只输入了 1 个艺人,跳过本步骤,直接进入机票/酒店搜索(如开启)或输出呈现。
**多艺人模式:** 将所有艺人的有效演出场次汇总后,执行组合匹配算法。
## 1. 定义"可组合"条件
两场(或多场)演出被视为"可组合"需满足:
1. **时间相近**:演出日期之间的间隔 ≤ 用户设定的容忍天数(默认 7 天)
2. **地点相近**:满足以下任一条件:
- **同城市**:在同一个城市(如都在东京)
- **同都市圈**:在已知的相邻城市群内(如东京-横滨、大阪-神户-京都、纽约-新泽西、伦敦-伯明翰等)
- **同国家且交通便利**:在同一国家内,高铁/飞机 3 小时内可达
3. **不同艺人**:每个组合必须包含至少 2 个不同艺人的演出
## 2. 组合生成策略
1. 将所有演出按日期排序,形成一个时间线
2. 用滑动窗口(窗口大小 = 容忍天数)在时间线上扫描
3. 对窗口内的演出,按城市/都市圈分组
4. 在每个地理分组中,检查是否包含 ≥ 2 个不同艺人
5. 如果是,生成一个组合候选
## 3. 组合评分
对每个候选组合计算综合评分,权重如下:
| 因素 | 权重 | 说明 |
|------|------|------|
| 艺人覆盖数 | 40% | 覆盖的艺人越多得分越高;覆盖全部艺人为满分 |
| 时间紧凑度 | 25% | 演出之间间隔天数越少越好 |
| 地理集中度 | 20% | 同城 > 同都市圈 > 同国家跨城 |
| 售票可行性 | 15% | 全部在售 > 部分预售 > 包含售罄场次 |
### 3.1 地区降权系数
在基础评分之上,对特定地区的场次施加降权系数(乘法修正)。降权不影响场次的搜索和展示,仅影响组合排序优先级。
| 地区 | 降权系数 | 说明 |
|------|----------|------|
| 台湾(台北、新北、桃园、高雄、台南等) | **×0.6** | 大陆用户前往台湾需办理通行证和签注,手续周期较长、存在不确定性,出行便利性低于大陆及免签/落地签目的地 |
**计算规则:**
1. 先按上方四维权重计算基础综合评分 `baseScore`
2. 检查组合中所有场次的举办地区
3. 如果组合中**任一场次**位于降权地区,最终评分 = `baseScore × 该地区降权系数`
4. 如果组合中涉及**多个不同降权地区**,取最低的降权系数
5. 不在降权表中的地区系数为 1.0(无影响)
**示例:**
- 组合 A(北京+温州):baseScore 85 × 1.0 = **85**
- 组合 B(北京+台北):baseScore 90 × 0.6 = **54** → 排在组合 A 之后
> **维护说明:** 降权系数可根据实际出行便利性调整。如未来台湾自由行政策放宽,可适当提高系数(如 0.8)。如需对其他地区降权(如需要复杂签证的国家),在表中追加即可。
按综合评分从高到低排序,取前 **10 个** 组合进入下一步。
## 4. 常见都市圈参考表
在判断"地点相近"时,参考以下都市圈映射:
**日本:**
| 都市圈 | 包含城市 |
|--------|----------|
| 东京圈 | 东京、横滨、埼玉、千叶、川崎 |
| 关西圈 | 大阪、京都、神户、奈良 |
| 名古屋圈 | 名古屋、丰田 |
| 福冈圈 | 福冈、北九州 |
**韩国:**
| 都市圈 | 包含城市 |
|--------|----------|
| 首尔圈 | 首尔、仁川、京畿道、高阳、水原 |
| 釜山圈 | 釜山、蔚山 |
**中国大陆:**
| 都市圈 | 包含城市 |
|--------|----------|
| 长三角 | 上海、杭州、南京、苏州、无锡 |
| 大湾区 | 深圳、广州、东莞、佛山 |
| 京津冀 | 北京、天津 |
| 成渝 | 成都、重庆 |
**港澳台:**
| 都市圈 | 包含城市 |
|--------|----------|
| 港澳 | 香港、澳门 |
| 台湾北部 | 台北、新北、桃园 |
| 台湾南部 | 高雄、台南 |
**北美:**
| 都市圈 | 包含城市 |
|--------|----------|
| 纽约圈 | 纽约、新泽西、布鲁克林、长岛、纽瓦克 |
| 洛杉矶圈 | 洛杉矶、安纳海姆、英格尔伍德、帕萨迪纳 |
| 旧金山湾区 | 旧金山、奥克兰、圣何塞 |
| 芝加哥圈 | 芝加哥、罗斯蒙特 |
| 多伦多圈 | 多伦多、密西沙加、汉密尔顿 |
**欧洲:**
| 都市圈 | 包含城市 |
|--------|----------|
| 伦敦圈 | 伦敦、温布利、克罗伊登 |
| 巴黎圈 | 巴黎、圣但尼、楠泰尔 |
| 莱茵-鲁尔 | 杜塞尔多夫、科隆、多特蒙德、埃森 |
| 兰斯塔德 | 阿姆斯特丹、鹿特丹、乌得勒支 |
**东南亚:**
| 都市圈 | 包含城市 |
|--------|----------|
| 新加坡 | 新加坡 |
| 曼谷圈 | 曼谷、暖武里 |
| 雅加达圈 | 雅加达、茂物 |
| 马尼拉圈 | 马尼拉、奎松、帕赛 |
遇到不在表中的城市时,用常识判断是否属于同一都市圈或交通便利区域。
FILE:hotel-search.md
# 酒店搜索(flyai)
**本步骤仅在用户明确要求搜索酒店时执行。**
对排名前列的组合方案,使用 `flyai` CLI 工具(飞猪旅行)搜索演出城市的酒店。该工具返回实时价格和预订链接。
## 日期策略
- 入住日期:第一场演出的前一天(与机票去程日期一致)
- 退房日期:最后一场演出的后一天(与机票回程日期一致)
- 如果组合中涉及多个城市,为每个城市分别搜索酒店,入住/退房日期按该城市的演出安排确定
## 搜索命令
```bash
flyai search-hotel \
--dest-name {目的地城市} \
--key-words {场馆名称或地标} \
--check-in-date {入住日期 YYYY-MM-DD} \
--check-out-date {退房日期 YYYY-MM-DD} \
--sort price_asc
```
参数说明:
- `--dest-name` 目的地城市名称,如"东京"、"大阪"、"首尔"
- `--key-words` 使用场馆名称作为关键词,确保搜索到场馆附近的酒店(如"东京巨蛋"、"大阪城ホール")
- `--sort price_asc` 按价格从低到高排序(默认推荐,也可根据用户需要改为 `distance_asc`/`rate_desc`)
- 如有星级要求,加 `--hotel-stars {1-5}`(逗号分隔,如 `--hotel-stars 4,5`)
- 如有每晚预算上限,加 `--max-price {金额}`
- 如有床型偏好,加 `--hotel-bed-types {king/twin/multi}`
- 如有酒店类型偏好,加 `--hotel-types {hotel/homestay/inn}`
**多个组合/城市的酒店搜索应并行执行。**
## 返回数据解析
flyai 返回 JSON 格式,从 `data.itemList` 数组中提取每个酒店的以下字段:
| 字段 | JSON 路径 | 说明 |
|------|-----------|------|
| 酒店名称 | `name` | 如 "东京巨蛋酒店" |
| 价格 | `price` | 每晚价格字符串(如 "¥1505") |
| 地址 | `address` | 酒店详细地址 |
| 星级/档次 | `star` | 如 "经济型"、"舒适型"、"高档型"、"豪华型" |
| 附近地标 | `interestsPoi` | 如 "近东京巨蛋"、"近秋叶原" |
| 预订链接 | `detailUrl` | 飞猪直达链接 |
| 装修时间 | `decorationTime` | 可选,如 "2023" |
| 品牌 | `brandName` | 可选,如 "万豪"、"希尔顿"(可能为 null) |
## 筛选与推荐策略
1. 每个城市取 **前 5 家** 酒店推荐(默认按价格排序)
2. 优先展示 `interestsPoi` 中包含场馆名称或附近地标的酒店
3. 按档次分层推荐:至少各展示 1 家经济型/舒适型和 1 家高档型/豪华型(如有),满足不同预算需求
4. 如用户指定了星级或预算,严格按条件过滤后再推荐
5. 计算总住宿费用时,使用每晚价格 × 入住晚数(从价格字符串中提取数字)
## 搜索补充策略
- 搜索酒店时优先使用场馆名称作为 `--key-words`,确保推荐的酒店距离场馆较近,方便观演
- 如果场馆关键词搜索结果较少,退而使用城市核心区域(如"新宿"、"涩谷"、"梅田")作为关键词补充搜索
- 酒店价格会波动(尤其是演唱会期间热门城市),提醒用户看到合适的酒店尽早预订
FILE:BLOCKED_SITES.md
# WebFetch 失败站点记录
以下网站在使用 WebFetch 抓取时无法获取有效内容。根据失败类型,采取不同降级策略:仅用 WebSearch 摘要,或使用 agent-browser 浏览器抓取。
> 最后测试时间:2026-04
## 超时(Timeout)— 仅用 WebSearch 摘要
| 站点 | URL 示例 | 失败原因 |
|------|----------|----------|
| lignea.co.jp | https://lignea.co.jp/ryokushaka/ | WebFetch 超时(>60s),2026-04 复测仍超时 |
| sonymusic.co.jp | https://www.sonymusic.co.jp/artist/ryokusyaka/info/581667 | WebFetch 超时(>60s)。注:同域名 `/live/` 路径属于 JS 渲染类型,见下方 agent-browser 一节 |
> 超时类站点不建议用 agent-browser(大概率加载也极慢),优先用 WebSearch 摘要。
## HTTP 错误 — 仅用 WebSearch 摘要
| 站点 | URL 示例 | 失败原因 |
|------|----------|----------|
| reissuerecords.net | https://reissuerecords.net/ | HTTP 403 Forbidden,服务器拒绝访问 |
> HTTP 403/5xx 类错误通常与 User-Agent 或 IP 限制有关,agent-browser 可能同样被拒绝,不建议尝试。
## JS 渲染站点 — 可用 agent-browser 🟢
以下站点 WebFetch 只能获取空壳 HTML,但 agent-browser 可以完整渲染 JS 内容。**当 WebSearch 摘要信息不足时,应使用 agent-browser 抓取。**
| 站点 | URL 示例 | WebFetch 表现 | agent-browser 抓取方式 |
|------|----------|---------------|----------------------|
| ryokushaka.com | https://www.ryokushaka.com/live/ | 仅导航栏和 banner | `open` → `wait 3000` → `snapshot -i` |
| ryokushaka.com | https://www.ryokushaka.com/news/archive/?581667 | 同上 | 同上 |
| sonymusic.co.jp | https://www.sonymusic.co.jp/artist/ryokusyaka/live/ | 仅导航链接 | `open` → `wait 3000` → `snapshot -i`(日程可能需要点击展开) |
| ticket.kenshiyonezu.jp | https://ticket.kenshiyonezu.jp/pages/2026_detail | 无演出信息 | `open` → `wait 3000` → `snapshot -i` |
**agent-browser 抓取模板:**
```bash
agent-browser open {URL}
agent-browser wait 3000
agent-browser snapshot -i
# 如需翻页或展开更多内容:
# agent-browser click @eN && agent-browser wait 1000 && agent-browser snapshot -i
agent-browser close
```
## 部分可用(需注意)
| 站点 | URL 示例 | 说明 |
|------|----------|------|
| livefans.jp | https://www.livefans.jp/groups/265804 | 此前返回 504,2026-04 复测部分页面已恢复(团体页面可用 WebFetch),但艺人详情页(/artists/)仍只返回导航链接 — 可尝试 agent-browser |
## 降级策略总览
```
信息需求
├─ WebSearch 摘要足够? → 直接提取,无需访问站点
├─ 需要补充详情?
│ ├─ 目标是可靠站点? → WebFetch(rockinon.com、natalie.mu 等)
│ ├─ 目标是 JS 渲染站点? → agent-browser(见上方 🟢 标记)
│ └─ 目标是超时/403 站点? → 放弃,仅用 WebSearch 摘要
└─ 所有手段都无数据? → 标记该艺人"暂无公开巡演信息"
```
**可靠的 WebFetch 站点:** rockinon.com、natalie.mu、fashion-press.net、tower.jp、news.yahoo.co.jp、backnumber.info
FILE:concert-search.md
# 演唱会信息搜索
对每个艺人,使用 `WebSearch` 搜索其演唱会和巡演信息。**为提升效率,多个艺人的搜索应通过 Task 工具并行执行。**
## 搜索策略(每个艺人至少 8 条查询)
### 基本查询(当年 + 明年各 4 条,中日韩英四语)
**必须同时搜索当年和明年两个年份。** 绝不能只搜当年就下结论"没有后续活动"。
```
"{艺人} ライブ ツアー {当年} 日程" ← 日文搜索,覆盖日本巡演
"{艺人} concert tour {当年} dates" ← 英文搜索,覆盖欧美巡演
"{艺人} 演唱会 巡演 {当年}" ← 中文搜索,覆盖中国大陆及华语圈
"{艺人} 콘서트 투어 {当年} 일정" ← 韩文搜索,覆盖韩国巡演
"{艺人} ライブ ツアー {明年} 日程" ← 覆盖明年日本巡演
"{艺人} concert tour {明年} dates" ← 覆盖明年欧美巡演
"{艺人} 演唱会 巡演 {明年}" ← 覆盖明年中国巡演
"{艺人} 콘서트 투어 {明年} 일정" ← 覆盖明年韩国巡演
```
仅用通用查询即可覆盖主流平台(Ticketmaster、Songkick、Bandsintown、Eventernote、大麦网、Interpark、Yes24 等结果会自然出现在搜索结果中),不再逐个平台做 `site:` 限定搜索。
### 追加查询:检查最近活动上的新发表
**重大新活动经常在刚结束的live/演唱会上官宣。** 如果搜索中发现该艺人近期(过去30天内)刚完成了一场演出或活动,必须额外搜索该活动是否公布了新情报:
```
"{艺人} {近期活动名} 新情報 発表" ← 查日文新闻
"{艺人} {近期活动名} announcement new" ← 查英文新闻
"{艺人} 追加公演 新ライブ 発表 {当年}" ← 通用追加公演查询
```
这一步不可跳过。漏掉"活动现场官宣的下一场"是最常见的搜索遗漏。
## 三层信息提取策略
信息提取遵循**逐层降级**原则,尽量用最轻量的方式获取数据:
### 第一层:WebSearch 摘要提取(默认,最快)
**优先从 `WebSearch` 返回的摘要片段(snippet)中直接提取日期、场馆、城市、票价等关键信息。** 大多数情况下摘要已包含足够的结构化数据,无需额外请求。
### 第二层:WebFetch 补充详情
仅在以下情况使用 `WebFetch` 补充详情:
- 摘要信息不完整(如缺少票价或售票状态)
- 且目标网站属于**已知可靠站点**(见下方列表)
**已知可靠的 WebFetch 站点(响应快、内容可抓取):**
- rockinon.com — 日本音乐媒体,巡演报道详细
- natalie.mu — 日本娱乐新闻,演出信息全
- fashion-press.net — 票价信息详细
- tower.jp — 巡演公告完整
- news.yahoo.co.jp — 聚合各媒体报道
- backnumber.info — 巡演日程完整
**禁止 WebFetch 的站点(参见 BLOCKED_SITES.md):**
- lignea.co.jp — 超时
- sonymusic.co.jp — 超时 / 内容为空
- reissuerecords.net — HTTP 403
- 以及其他在历史执行中记录到 BLOCKED_SITES.md 的站点
### 第三层:agent-browser 浏览器抓取(JS 渲染站点专用)
当目标站点依赖 JS 动态渲染(WebFetch 只能获取空壳 HTML),**且该站点是官方信息源或数据唯一来源**时,使用 `agent-browser` 启动真实浏览器抓取完整内容。
**适用场景(参见 BLOCKED_SITES.md「可用 agent-browser」标记):**
- 艺人/乐队官方网站的巡演页面(如 ryokushaka.com/live/、ticket.kenshiyonezu.jp)
- 唱片公司的演出日程页面(如 sonymusic.co.jp/artist/.../live/)
- JS 渲染的售票平台详情页
- WebFetch 仅返回导航栏/banner 的页面
**标准抓取流程:**
```bash
# 1. 打开目标页面并等待 JS 渲染完成
agent-browser open {URL}
agent-browser wait 3000
# 2. 获取页面快照(文字内容 + 交互元素)
agent-browser snapshot -i
# 3. 如果需要查看完整日程(可能需要点击展开/翻页)
agent-browser click @eN # 点击"更多日程"按钮
agent-browser wait 1000
agent-browser snapshot -i # 重新获取内容
# 4. 完成后关闭浏览器
agent-browser close
```
**使用原则:**
- agent-browser 是最重量级的手段,仅在第一层和第二层都无法获取关键信息时使用
- 每次抓取后及时 `agent-browser close` 释放资源
- 如果同一个 Task 中需要抓取多个 JS 站点,使用命名 session 隔离:`agent-browser --session {name} open {URL}`
- 抓取结果同样提取下方"提取字段"表中的 7 个标准字段
## 提取字段
| 字段 | 说明 |
|------|------|
| 艺人 | 表演者或乐队名称 |
| 日期与时间 | 演出日期和开始时间(含时区) |
| 场馆 | 场馆名称 |
| 城市与国家 | 演出所在城市和国家 |
| 票价 | 价格区间(注明货币) |
| 售票状态 | 在售 / 售罄 / 预售 / 候补 |
| 购票链接 | 直接链接 |
## 后处理
对每个艺人的结果去重并按日期排序,过滤掉**今天起 14 天内(含)的场次**(太临近的演出来不及准备机票和签证),仅保留半个月后及更远的场次。
FILE:flight-search.md
# 机票搜索(flyai)
**本步骤仅在用户明确要求搜索机票时执行。**
对排名前列的组合方案,使用 `flyai` CLI 工具(飞猪旅行)搜索从用户出发城市到演出城市的往返机票。该工具返回实时价格和购票链接,数据准确性远优于 WebSearch 摘要。
## 日期策略
默认策略为去程演出前一天、回程演出后一天,但可根据演出时间和航班到达时间做**当天出行优化**,节省一晚住宿费用。
### 去程日期判断
- **默认**:第一场演出的前一天
- **可优化为当天**:如果满足以下任一条件,去程改为首场演出当天:
- 演出开始时间较晚(18:00 及以后),且存在当天中午前(12:00 前)到达目的地的航班
- 演出开始时间为下午场(14:00-17:59),且存在当天上午(10:00 前)到达目的地的航班
- **优化时的搜索方式**:同时搜索前一天和当天两个去程日期,将两者的结果合并推荐,标注当天去程的航班需注意"时间较紧"
### 回程日期判断
- **默认**:最后一场演出的后一天
- **可优化为当天**:如果满足以下任一条件,回程改为末场演出当天:
- 演出结束时间较早(17:00 前结束),且存在当天晚间(20:00 后出发)的航班
- 演出为下午场且预计 16:00 前结束,存在当天 19:00 后出发的航班
- **优化时的搜索方式**:同时搜索当天和后一天两个回程日期,将两者的结果合并推荐,标注当天回程的航班需注意"散场后需尽快赶往机场"
### 注意事项
- 当天出行优化需要已知演出的具体开始时间,如果只有日期没有时间,不做优化,使用默认的前/后一天策略
- 即使做了当天优化,仍然保留前一天去程/后一天回程的搜索结果作为"稳妥方案"供用户选择
- 到达时间需考虑从机场到场馆的交通时间(一般预留 2-3 小时),回程需考虑从场馆到机场的交通时间(一般预留 1.5-2 小时)
- 如果组合中涉及同一国家的多个城市,只搜索主要入境城市的机票(如日本搜东京或大阪入境)
## 搜索命令
```bash
flyai search-flight \
--origin {出发城市} \
--destination {目的地城市} \
--dep-date {去程日期 YYYY-MM-DD} \
--back-date {回程日期 YYYY-MM-DD} \
--sort-type 3
```
参数说明:
- `--sort-type 3` 表示按价格从低到高排序,确保最便宜的结果排在前面
- 如需仅看直飞,加 `--journey-type 1`
- 如有预算限制,加 `--max-price {金额}`
**多个组合的机票搜索应并行执行(同时发出多条 flyai 命令)。**
## 返回数据解析
flyai 返回 JSON 格式,从 `data.itemList` 数组中提取每个选项的以下字段:
| 字段 | JSON 路径 | 说明 |
|------|-----------|------|
| 往返价格 | `ticketPrice` | 含税总价(CNY) |
| 航程类型 | `journeys[].journeyType` | "直达" 或 "中转" |
| 航班号 | `journeys[].segments[].marketingTransportNo` | 如 CA927 |
| 航空公司 | `journeys[].segments[].marketingTransportName` | 如 "国航" |
| 出发机场 | `journeys[].segments[].depStationName` | 如 "首都国际机场" |
| 到达机场 | `journeys[].segments[].arrStationName` | 如 "关西国际机场" |
| 出发时间 | `journeys[].segments[].depDateTime` | 如 "2026-10-22 08:40:00" |
| 到达时间 | `journeys[].segments[].arrDateTime` | 如 "2026-10-22 12:40:00" |
| 飞行时长 | `journeys[].totalDuration` | 分钟数 |
| 购票链接 | `jumpUrl` | 飞猪直达链接 |
## 筛选策略
- 每个组合取 **最便宜的 3 个** 机票选项(直飞优先展示,中转次之)
- 如果组合中涉及跨城市移动(如东京看完一场,再去大阪看另一场),额外搜索城市间交通方案(新干线、廉航等)并估算费用
- 搜索机票时考虑演出城市对应的主要机场(如东京对应 NRT/HND,伦敦对应 LHR/LGW/STN)
FILE:snapshots/latest.json
{
"snapshotId": "jay_chou_mayday_20260408",
"createdAt": "2026-04-08T16:00:00+08:00",
"artists": ["周杰伦", "五月天"],
"timeWindow": "不限时间",
"shows": [
{
"id": "jay_chou_20260515_wenzhou",
"artist": "周杰伦",
"date": "2026-05-15",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布(参考¥580-2,380)",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260516_wenzhou",
"artist": "周杰伦",
"date": "2026-05-16",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260517_wenzhou",
"artist": "周杰伦",
"date": "2026-05-17",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260626_beijing",
"artist": "周杰伦",
"date": "2026-06-26",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260627_beijing",
"artist": "周杰伦",
"date": "2026-06-27",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260628_beijing",
"artist": "周杰伦",
"date": "2026-06-28",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260717_sanya",
"artist": "周杰伦",
"date": "2026-07-17",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260718_sanya",
"artist": "周杰伦",
"date": "2026-07-18",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260719_sanya",
"artist": "周杰伦",
"date": "2026-07-19",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260801_nanjing",
"artist": "周杰伦",
"date": "2026-08-01",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260802_nanjing",
"artist": "周杰伦",
"date": "2026-08-02",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260803_nanjing",
"artist": "周杰伦",
"date": "2026-08-03",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20261017_melbourne",
"artist": "周杰伦",
"date": "2026-10-17",
"time": "19:30",
"venue": "Marvel Stadium, Docklands",
"city": "墨尔本",
"country": "澳大利亚",
"price": "AUD $208-$748",
"ticketStatus": "4/9公开发售",
"ticketUrl": "https://www.ticketmaster.com.au/jay-chou-carnival-ii-world-tour-in-melbourne-docklands-17-10-2026/event/2500647FEE7EB74F",
"source": "Ticketmaster AU"
},
{
"id": "jay_chou_20261121_sydney",
"artist": "周杰伦",
"date": "2026-11-21",
"time": "19:30",
"venue": "ENGIE Stadium, Sydney Showground",
"city": "悉尼",
"country": "澳大利亚",
"price": "未公布",
"ticketStatus": "待公布",
"ticketUrl": "https://www.sydneyshowground.com.au/whats-on/jay-chou-carnival--world-tour/",
"source": "Sydney Showground"
},
{
"id": "mayday_20260430_beijing",
"artist": "五月天",
"date": "2026-04-30",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260501_beijing",
"artist": "五月天",
"date": "2026-05-01",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260502_beijing",
"artist": "五月天",
"date": "2026-05-02",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260503_beijing",
"artist": "五月天",
"date": "2026-05-03",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260508_beijing",
"artist": "五月天",
"date": "2026-05-08",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260509_beijing",
"artist": "五月天",
"date": "2026-05-09",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260510_beijing",
"artist": "五月天",
"date": "2026-05-10",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260511_beijing",
"artist": "五月天",
"date": "2026-05-11",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260515_beijing",
"artist": "五月天",
"date": "2026-05-15",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260516_beijing",
"artist": "五月天",
"date": "2026-05-16",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260517_beijing",
"artist": "五月天",
"date": "2026-05-17",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260518_beijing",
"artist": "五月天",
"date": "2026-05-18",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260627_taipei",
"artist": "五月天",
"date": "2026-06-27",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260628_taipei",
"artist": "五月天",
"date": "2026-06-28",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260629_taipei",
"artist": "五月天",
"date": "2026-06-29",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260704_taipei",
"artist": "五月天",
"date": "2026-07-04",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260705_taipei",
"artist": "五月天",
"date": "2026-07-05",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260706_taipei",
"artist": "五月天",
"date": "2026-07-06",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260711_taipei",
"artist": "五月天",
"date": "2026-07-11",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260712_taipei",
"artist": "五月天",
"date": "2026-07-12",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
}
],
"totalShows": 34,
"searchDuration": "约 5 分钟"
}
FILE:snapshots/jay_chou_mayday_20260408.json
{
"snapshotId": "jay_chou_mayday_20260408",
"createdAt": "2026-04-08T16:00:00+08:00",
"artists": ["周杰伦", "五月天"],
"timeWindow": "不限时间",
"shows": [
{
"id": "jay_chou_20260515_wenzhou",
"artist": "周杰伦",
"date": "2026-05-15",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布(参考¥580-2,380)",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260516_wenzhou",
"artist": "周杰伦",
"date": "2026-05-16",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260517_wenzhou",
"artist": "周杰伦",
"date": "2026-05-17",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260626_beijing",
"artist": "周杰伦",
"date": "2026-06-26",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260627_beijing",
"artist": "周杰伦",
"date": "2026-06-27",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260628_beijing",
"artist": "周杰伦",
"date": "2026-06-28",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260717_sanya",
"artist": "周杰伦",
"date": "2026-07-17",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260718_sanya",
"artist": "周杰伦",
"date": "2026-07-18",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260719_sanya",
"artist": "周杰伦",
"date": "2026-07-19",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260801_nanjing",
"artist": "周杰伦",
"date": "2026-08-01",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260802_nanjing",
"artist": "周杰伦",
"date": "2026-08-02",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260803_nanjing",
"artist": "周杰伦",
"date": "2026-08-03",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20261017_melbourne",
"artist": "周杰伦",
"date": "2026-10-17",
"time": "19:30",
"venue": "Marvel Stadium, Docklands",
"city": "墨尔本",
"country": "澳大利亚",
"price": "AUD $208-$748",
"ticketStatus": "4/9公开发售",
"ticketUrl": "https://www.ticketmaster.com.au/jay-chou-carnival-ii-world-tour-in-melbourne-docklands-17-10-2026/event/2500647FEE7EB74F",
"source": "Ticketmaster AU"
},
{
"id": "jay_chou_20261121_sydney",
"artist": "周杰伦",
"date": "2026-11-21",
"time": "19:30",
"venue": "ENGIE Stadium, Sydney Showground",
"city": "悉尼",
"country": "澳大利亚",
"price": "未公布",
"ticketStatus": "待公布",
"ticketUrl": "https://www.sydneyshowground.com.au/whats-on/jay-chou-carnival--world-tour/",
"source": "Sydney Showground"
},
{
"id": "mayday_20260430_beijing",
"artist": "五月天",
"date": "2026-04-30",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260501_beijing",
"artist": "五月天",
"date": "2026-05-01",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260502_beijing",
"artist": "五月天",
"date": "2026-05-02",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260503_beijing",
"artist": "五月天",
"date": "2026-05-03",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260508_beijing",
"artist": "五月天",
"date": "2026-05-08",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260509_beijing",
"artist": "五月天",
"date": "2026-05-09",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260510_beijing",
"artist": "五月天",
"date": "2026-05-10",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260511_beijing",
"artist": "五月天",
"date": "2026-05-11",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260515_beijing",
"artist": "五月天",
"date": "2026-05-15",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260516_beijing",
"artist": "五月天",
"date": "2026-05-16",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260517_beijing",
"artist": "五月天",
"date": "2026-05-17",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260518_beijing",
"artist": "五月天",
"date": "2026-05-18",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260627_taipei",
"artist": "五月天",
"date": "2026-06-27",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260628_taipei",
"artist": "五月天",
"date": "2026-06-28",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260629_taipei",
"artist": "五月天",
"date": "2026-06-29",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260704_taipei",
"artist": "五月天",
"date": "2026-07-04",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260705_taipei",
"artist": "五月天",
"date": "2026-07-05",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260706_taipei",
"artist": "五月天",
"date": "2026-07-06",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260711_taipei",
"artist": "五月天",
"date": "2026-07-11",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260712_taipei",
"artist": "五月天",
"date": "2026-07-12",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
}
],
"totalShows": 34,
"searchDuration": "约 5 分钟"
}
介绍Elastic从开源搜索库到综合可观测性平台的发展,涵盖Elasticsearch、ELK Stack及其商业模式。
--- name: elastic-datadog summary: 从开源搜索库到百亿美金可观测性平台 — Elastic 如何改变数据搜索和分析 read_when: - 研究可观测性和日志管理时 - 分析 Elasticsearch 生态系统时 - 了解开源软件商业化路径时 - 对比 Splunk、Datadog、Elastic 时 --- # Elastic (Elasticsearch) ## 概述 从开源搜索库到百亿美金可观测性平台 — Elastic 如何改变数据搜索和分析。 ## 历史时间线 - 2012: Shay Banon 创建 Elasticsearch(基于 Lucene 的分布式搜索引擎) - 2012: Elasticsearch 迅速成为最流行的开源搜索项目 - 2012: 创立 Elastic 公司商业化 Elasticsearch - 2015: 发布 ELK Stack(Elasticsearch + Logstash + Kibana) - 2018年10月: 纽交所上市(股票代码 ESTC) - 2019: 推出 Elastic Security(SIEM) - 2021: 推出 Elastic Observability(APM + 日志 + 指标 + Uptime) - 2023: 推出 Elastic AI Assistant(LLM 集成) ## 商业模式 开源核心模式(Open Source Core):免费开源版 + 付费商业版(Elastic Cloud 托管服务、安全功能、机器学习功能)。收入来自:Elastic Cloud 订阅、商业许可证、企业支持合同。 ## 护城河分析 Elasticsearch 是业界最流行的搜索引擎(维基百科、GitHub、Uber 等都使用);ELK Stack 是日志管理的行业标准;开源社区生态庞大;全栈可观测性平台。 ## 关键数据 - **上市**: 纽交所 ESTC(2018) - **2023年营收**: ~13 亿美元 - **GitHub Stars**: Elasticsearch 67k+ - **用户**: 包括 Netflix, NASA, Uber 等 ## 有趣事实 - Elasticsearch 最初是创始人 Shay Banon 为了让妻子的菜谱更容易搜索而创建的——后来变成了价值百亿美元的搜索引擎 - Elastic 公司 2019 年与 AWS 发生许可证争议,导致 AWS 创建了 OpenSearch 分叉项目
Amazon AppConfig API skill. Use when working with Amazon AppConfig for applications, deploymentstrategies, extensions. Covers 45 endpoints.
---
name: lap-amazon-appconfig
description: "Amazon AppConfig API skill. Use when working with Amazon AppConfig for applications, deploymentstrategies, extensions. Covers 45 endpoints."
version: 1.0.0
generator: lapsh
metadata:
openclaw:
requires:
env:
- AMAZON_APPCONFIG_API_KEY
---
# Amazon AppConfig
API version: 2019-10-09
## Auth
AWS SigV4
## Base URL
Not specified.
## Setup
1. Configure auth: AWS SigV4
2. GET /settings -- verify access
3. POST /applications -- create first applications
## Endpoints
45 endpoints across 7 groups. See references/api-spec.lap for full details.
### applications
| Method | Path | Description |
|--------|------|-------------|
| POST | /applications | Creates an application. In AppConfig, an application is simply an organizational construct like a folder. This organizational construct has a relationship with some unit of executable code. For example, you could create an application called MyMobileApp to organize and manage configuration data for a mobile application installed by your users. |
| POST | /applications/{ApplicationId}/configurationprofiles | Creates a configuration profile, which is information that enables AppConfig to access the configuration source. Valid configuration sources include the following: Configuration data in YAML, JSON, and other formats stored in the AppConfig hosted configuration store Configuration data stored as objects in an Amazon Simple Storage Service (Amazon S3) bucket Pipelines stored in CodePipeline Secrets stored in Secrets Manager Standard and secure string parameters stored in Amazon Web Services Systems Manager Parameter Store Configuration data in SSM documents stored in the Systems Manager document store A configuration profile includes the following information: The URI location of the configuration data. The Identity and Access Management (IAM) role that provides access to the configuration data. A validator for the configuration data. Available validators include either a JSON Schema or an Amazon Web Services Lambda function. For more information, see Create a Configuration and a Configuration Profile in the AppConfig User Guide. |
| POST | /applications/{ApplicationId}/environments | Creates an environment. For each application, you define one or more environments. An environment is a deployment group of AppConfig targets, such as applications in a Beta or Production environment. You can also define environments for application subcomponents such as the Web, Mobile and Back-end components for your application. You can configure Amazon CloudWatch alarms for each environment. The system monitors alarms during a configuration deployment. If an alarm is triggered, the system rolls back the configuration. |
| POST | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions | Creates a new configuration in the AppConfig hosted configuration store. If you're creating a feature flag, we recommend you familiarize yourself with the JSON schema for feature flag data. For more information, see Type reference for AWS.AppConfig.FeatureFlags in the AppConfig User Guide. |
| DELETE | /applications/{ApplicationId} | Deletes an application. |
| DELETE | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId} | Deletes a configuration profile. To prevent users from unintentionally deleting actively-used configuration profiles, enable deletion protection. |
| DELETE | /applications/{ApplicationId}/environments/{EnvironmentId} | Deletes an environment. To prevent users from unintentionally deleting actively-used environments, enable deletion protection. |
| DELETE | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions/{VersionNumber} | Deletes a version of a configuration from the AppConfig hosted configuration store. |
| GET | /applications/{ApplicationId} | Retrieves information about an application. |
| GET | /applications/{Application}/environments/{Environment}/configurations/{Configuration} | (Deprecated) Retrieves the latest deployed configuration. Note the following important information. This API action is deprecated. Calls to receive configuration data should use the StartConfigurationSession and GetLatestConfiguration APIs instead. GetConfiguration is a priced call. For more information, see Pricing. |
| GET | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId} | Retrieves information about a configuration profile. |
| GET | /applications/{ApplicationId}/environments/{EnvironmentId}/deployments/{DeploymentNumber} | Retrieves information about a configuration deployment. |
| GET | /applications/{ApplicationId}/environments/{EnvironmentId} | Retrieves information about an environment. An environment is a deployment group of AppConfig applications, such as applications in a Production environment or in an EU_Region environment. Each configuration deployment targets an environment. You can enable one or more Amazon CloudWatch alarms for an environment. If an alarm is triggered during a deployment, AppConfig roles back the configuration. |
| GET | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions/{VersionNumber} | Retrieves information about a specific configuration version. |
| GET | /applications | Lists all applications in your Amazon Web Services account. |
| GET | /applications/{ApplicationId}/configurationprofiles | Lists the configuration profiles for an application. |
| GET | /applications/{ApplicationId}/environments/{EnvironmentId}/deployments | Lists the deployments for an environment in descending deployment number order. |
| GET | /applications/{ApplicationId}/environments | Lists the environments for an application. |
| GET | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions | Lists configurations stored in the AppConfig hosted configuration store by version. |
| POST | /applications/{ApplicationId}/environments/{EnvironmentId}/deployments | Starts a deployment. |
| DELETE | /applications/{ApplicationId}/environments/{EnvironmentId}/deployments/{DeploymentNumber} | Stops a deployment. This API action works only on deployments that have a status of DEPLOYING. This action moves the deployment to a status of ROLLED_BACK. |
| PATCH | /applications/{ApplicationId} | Updates an application. |
| PATCH | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId} | Updates a configuration profile. |
| PATCH | /applications/{ApplicationId}/environments/{EnvironmentId} | Updates an environment. |
| POST | /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/validators | Uses the validators in a configuration profile to validate a configuration. |
### deploymentstrategies
| Method | Path | Description |
|--------|------|-------------|
| POST | /deploymentstrategies | Creates a deployment strategy that defines important criteria for rolling out your configuration to the designated targets. A deployment strategy includes the overall duration required, a percentage of targets to receive the deployment during each interval, an algorithm that defines how percentage grows, and bake time. |
| GET | /deploymentstrategies/{DeploymentStrategyId} | Retrieves information about a deployment strategy. A deployment strategy defines important criteria for rolling out your configuration to the designated targets. A deployment strategy includes the overall duration required, a percentage of targets to receive the deployment during each interval, an algorithm that defines how percentage grows, and bake time. |
| GET | /deploymentstrategies | Lists deployment strategies. |
| PATCH | /deploymentstrategies/{DeploymentStrategyId} | Updates a deployment strategy. |
### extensions
| Method | Path | Description |
|--------|------|-------------|
| POST | /extensions | Creates an AppConfig extension. An extension augments your ability to inject logic or behavior at different points during the AppConfig workflow of creating or deploying a configuration. You can create your own extensions or use the Amazon Web Services authored extensions provided by AppConfig. For an AppConfig extension that uses Lambda, you must create a Lambda function to perform any computation and processing defined in the extension. If you plan to create custom versions of the Amazon Web Services authored notification extensions, you only need to specify an Amazon Resource Name (ARN) in the Uri field for the new extension version. For a custom EventBridge notification extension, enter the ARN of the EventBridge default events in the Uri field. For a custom Amazon SNS notification extension, enter the ARN of an Amazon SNS topic in the Uri field. For a custom Amazon SQS notification extension, enter the ARN of an Amazon SQS message queue in the Uri field. For more information about extensions, see Extending workflows in the AppConfig User Guide. |
| DELETE | /extensions/{ExtensionIdentifier} | Deletes an AppConfig extension. You must delete all associations to an extension before you delete the extension. |
| GET | /extensions/{ExtensionIdentifier} | Returns information about an AppConfig extension. |
| GET | /extensions | Lists all custom and Amazon Web Services authored AppConfig extensions in the account. For more information about extensions, see Extending workflows in the AppConfig User Guide. |
| PATCH | /extensions/{ExtensionIdentifier} | Updates an AppConfig extension. For more information about extensions, see Extending workflows in the AppConfig User Guide. |
### extensionassociations
| Method | Path | Description |
|--------|------|-------------|
| POST | /extensionassociations | When you create an extension or configure an Amazon Web Services authored extension, you associate the extension with an AppConfig application, environment, or configuration profile. For example, you can choose to run the AppConfig deployment events to Amazon SNS Amazon Web Services authored extension and receive notifications on an Amazon SNS topic anytime a configuration deployment is started for a specific application. Defining which extension to associate with an AppConfig resource is called an extension association. An extension association is a specified relationship between an extension and an AppConfig resource, such as an application or a configuration profile. For more information about extensions and associations, see Extending workflows in the AppConfig User Guide. |
| DELETE | /extensionassociations/{ExtensionAssociationId} | Deletes an extension association. This action doesn't delete extensions defined in the association. |
| GET | /extensionassociations/{ExtensionAssociationId} | Returns information about an AppConfig extension association. For more information about extensions and associations, see Extending workflows in the AppConfig User Guide. |
| GET | /extensionassociations | Lists all AppConfig extension associations in the account. For more information about extensions and associations, see Extending workflows in the AppConfig User Guide. |
| PATCH | /extensionassociations/{ExtensionAssociationId} | Updates an association. For more information about extensions and associations, see Extending workflows in the AppConfig User Guide. |
### deployementstrategies
| Method | Path | Description |
|--------|------|-------------|
| DELETE | /deployementstrategies/{DeploymentStrategyId} | Deletes a deployment strategy. |
### settings
| Method | Path | Description |
|--------|------|-------------|
| GET | /settings | Returns information about the status of the DeletionProtection parameter. |
| PATCH | /settings | Updates the value of the DeletionProtection parameter. |
### tags
| Method | Path | Description |
|--------|------|-------------|
| GET | /tags/{ResourceArn} | Retrieves the list of key-value tags assigned to the resource. |
| POST | /tags/{ResourceArn} | Assigns metadata to an AppConfig resource. Tags help organize and categorize your AppConfig resources. Each tag consists of a key and an optional value, both of which you define. You can specify a maximum of 50 tags for a resource. |
| DELETE | /tags/{ResourceArn} | Deletes a tag key and value from an AppConfig resource. |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "Create a application?" -> POST /applications
- "Create a configurationprofile?" -> POST /applications/{ApplicationId}/configurationprofiles
- "Create a deploymentstrategy?" -> POST /deploymentstrategies
- "Create a environment?" -> POST /applications/{ApplicationId}/environments
- "Create a extension?" -> POST /extensions
- "Create a extensionassociation?" -> POST /extensionassociations
- "Create a hostedconfigurationversion?" -> POST /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions
- "Delete a application?" -> DELETE /applications/{ApplicationId}
- "Delete a configurationprofile?" -> DELETE /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}
- "Delete a deployementstrategy?" -> DELETE /deployementstrategies/{DeploymentStrategyId}
- "Delete a environment?" -> DELETE /applications/{ApplicationId}/environments/{EnvironmentId}
- "Delete a extension?" -> DELETE /extensions/{ExtensionIdentifier}
- "Delete a extensionassociation?" -> DELETE /extensionassociations/{ExtensionAssociationId}
- "Delete a hostedconfigurationversion?" -> DELETE /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions/{VersionNumber}
- "List all settings?" -> GET /settings
- "Get application details?" -> GET /applications/{ApplicationId}
- "Get configuration details?" -> GET /applications/{Application}/environments/{Environment}/configurations/{Configuration}
- "Get configurationprofile details?" -> GET /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}
- "Get deployment details?" -> GET /applications/{ApplicationId}/environments/{EnvironmentId}/deployments/{DeploymentNumber}
- "Get deploymentstrategy details?" -> GET /deploymentstrategies/{DeploymentStrategyId}
- "Get environment details?" -> GET /applications/{ApplicationId}/environments/{EnvironmentId}
- "Get extension details?" -> GET /extensions/{ExtensionIdentifier}
- "Get extensionassociation details?" -> GET /extensionassociations/{ExtensionAssociationId}
- "Get hostedconfigurationversion details?" -> GET /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions/{VersionNumber}
- "List all applications?" -> GET /applications
- "List all configurationprofiles?" -> GET /applications/{ApplicationId}/configurationprofiles
- "List all deploymentstrategies?" -> GET /deploymentstrategies
- "List all deployments?" -> GET /applications/{ApplicationId}/environments/{EnvironmentId}/deployments
- "List all environments?" -> GET /applications/{ApplicationId}/environments
- "List all extensionassociations?" -> GET /extensionassociations
- "List all extensions?" -> GET /extensions
- "List all hostedconfigurationversions?" -> GET /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/hostedconfigurationversions
- "Get tag details?" -> GET /tags/{ResourceArn}
- "Create a deployment?" -> POST /applications/{ApplicationId}/environments/{EnvironmentId}/deployments
- "Delete a deployment?" -> DELETE /applications/{ApplicationId}/environments/{EnvironmentId}/deployments/{DeploymentNumber}
- "Delete a tag?" -> DELETE /tags/{ResourceArn}
- "Partially update a application?" -> PATCH /applications/{ApplicationId}
- "Partially update a configurationprofile?" -> PATCH /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}
- "Partially update a deploymentstrategy?" -> PATCH /deploymentstrategies/{DeploymentStrategyId}
- "Partially update a environment?" -> PATCH /applications/{ApplicationId}/environments/{EnvironmentId}
- "Partially update a extension?" -> PATCH /extensions/{ExtensionIdentifier}
- "Partially update a extensionassociation?" -> PATCH /extensionassociations/{ExtensionAssociationId}
- "Create a validator?" -> POST /applications/{ApplicationId}/configurationprofiles/{ConfigurationProfileId}/validators
- "How to authenticate?" -> See Auth section
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- Create/update endpoints typically return the created/updated object
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get amazon-appconfig -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search amazon-appconfig
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
Alexa For Business API skill. Use when working with Alexa For Business for #X-Amz-Target=AlexaForBusiness.ApproveSkill, #X-Amz-Target=AlexaForBusiness.Associ...
---
name: lap-alexa-for-business
description: "Alexa For Business API skill. Use when working with Alexa For Business for #X-Amz-Target=AlexaForBusiness.ApproveSkill, #X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook, #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile. Covers 93 endpoints."
version: 1.0.0
generator: lapsh
metadata:
openclaw:
requires:
env:
- ALEXA_FOR_BUSINESS_API_KEY
---
# Alexa For Business
API version: 2017-11-09
## Auth
ApiKey Authorization in header
## Base URL
http://a4b.{region}.amazonaws.com
## Setup
1. Set your API key in the appropriate header
3. POST /#X-Amz-Target=AlexaForBusiness.ApproveSkill -- create first #X-Amz-Target=AlexaForBusiness.ApproveSkill
## Endpoints
93 endpoints across 93 groups. See references/api-spec.lap for full details.
### #X-Amz-Target=AlexaForBusiness.ApproveSkill
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ApproveSkill | Associates a skill with the organization under the customer's AWS account. If a skill is private, the user implicitly accepts access to this skill during enablement. |
### #X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook | Associates a contact with a given address book. |
### #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile | Associates a device with the specified network profile. |
### #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom | Associates a device with a given room. This applies all the settings from the room profile to the device, and all the skills in any skill groups added to that room. This operation requires the device to be online, or else a manual sync is required. |
### #X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom | Associates a skill group with a given room. This enables all skills in the associated skill group on all devices in the room. |
### #X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup | Associates a skill with a skill group. |
### #X-Amz-Target=AlexaForBusiness.AssociateSkillWithUsers
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithUsers | Makes a private skill available for enrolled users to enable on their devices. |
### #X-Amz-Target=AlexaForBusiness.CreateAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateAddressBook | Creates an address book with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule | Creates a recurring schedule for usage reports to deliver to the specified S3 location with a specified daily or weekly interval. |
### #X-Amz-Target=AlexaForBusiness.CreateConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateConferenceProvider | Adds a new conference provider under the user's AWS account. |
### #X-Amz-Target=AlexaForBusiness.CreateContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateContact | Creates a contact with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateGatewayGroup | Creates a gateway group with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateNetworkProfile | Creates a network profile with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateProfile | Creates a new room profile with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateRoom | Creates a room with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateSkillGroup | Creates a skill group with a specified name and description. |
### #X-Amz-Target=AlexaForBusiness.CreateUser
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateUser | Creates a user. |
### #X-Amz-Target=AlexaForBusiness.DeleteAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteAddressBook | Deletes an address book by the address book ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule | Deletes the recurring report delivery schedule with the specified schedule ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider | Deletes a conference provider. |
### #X-Amz-Target=AlexaForBusiness.DeleteContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteContact | Deletes a contact by the contact ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteDevice | Removes a device from Alexa For Business. |
### #X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData | When this action is called for a specified shared device, it allows authorized users to delete the device's entire previous history of voice input data and associated response data. This action can be called once every 24 hours for a specific shared device. |
### #X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup | Deletes a gateway group. |
### #X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile | Deletes a network profile by the network profile ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteProfile | Deletes a room profile by the profile ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteRoom | Deletes a room by the room ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter | Deletes room skill parameter details by room, skill, and parameter key ID. |
### #X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization | Unlinks a third-party account from a skill. |
### #X-Amz-Target=AlexaForBusiness.DeleteSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteSkillGroup | Deletes a skill group by skill group ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteUser
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteUser | Deletes a specified user by user ARN and enrollment ARN. |
### #X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook | Disassociates a contact from a given address book. |
### #X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom | Disassociates a device from its current room. The device continues to be connected to the Wi-Fi network and is still registered to the account. The device settings and skills are removed from the room. |
### #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup | Disassociates a skill from a skill group. |
### #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUsers
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUsers | Makes a private skill unavailable for enrolled users and prevents them from enabling it on their devices. |
### #X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom | Disassociates a skill group from a specified room. This disables all skills in the skill group on all devices in the room. |
### #X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliances
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliances | Forgets smart home appliances associated to a room. |
### #X-Amz-Target=AlexaForBusiness.GetAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetAddressBook | Gets address the book details by the address book ARN. |
### #X-Amz-Target=AlexaForBusiness.GetConferencePreference
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetConferencePreference | Retrieves the existing conference preferences. |
### #X-Amz-Target=AlexaForBusiness.GetConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetConferenceProvider | Gets details about a specific conference provider. |
### #X-Amz-Target=AlexaForBusiness.GetContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetContact | Gets the contact details by the contact ARN. |
### #X-Amz-Target=AlexaForBusiness.GetDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetDevice | Gets the details of a device by device ARN. |
### #X-Amz-Target=AlexaForBusiness.GetGateway
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetGateway | Retrieves the details of a gateway. |
### #X-Amz-Target=AlexaForBusiness.GetGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetGatewayGroup | Retrieves the details of a gateway group. |
### #X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration | Retrieves the configured values for the user enrollment invitation email template. |
### #X-Amz-Target=AlexaForBusiness.GetNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetNetworkProfile | Gets the network profile details by the network profile ARN. |
### #X-Amz-Target=AlexaForBusiness.GetProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetProfile | Gets the details of a room profile by profile ARN. |
### #X-Amz-Target=AlexaForBusiness.GetRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetRoom | Gets room details by room ARN. |
### #X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter | Gets room skill parameter details by room, skill, and parameter key ARN. |
### #X-Amz-Target=AlexaForBusiness.GetSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetSkillGroup | Gets skill group details by skill group ARN. |
### #X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedules
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedules | Lists the details of the schedules that a user configured. A download URL of the report associated with each schedule is returned every time this action is called. A new download URL is returned each time, and is valid for 24 hours. |
### #X-Amz-Target=AlexaForBusiness.ListConferenceProviders
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListConferenceProviders | Lists conference providers under a specific AWS account. |
### #X-Amz-Target=AlexaForBusiness.ListDeviceEvents
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListDeviceEvents | Lists the device event history, including device connection status, for up to 30 days. |
### #X-Amz-Target=AlexaForBusiness.ListGatewayGroups
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListGatewayGroups | Retrieves a list of gateway group summaries. Use GetGatewayGroup to retrieve details of a specific gateway group. |
### #X-Amz-Target=AlexaForBusiness.ListGateways
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListGateways | Retrieves a list of gateway summaries. Use GetGateway to retrieve details of a specific gateway. An optional gateway group ARN can be provided to only retrieve gateway summaries of gateways that are associated with that gateway group ARN. |
### #X-Amz-Target=AlexaForBusiness.ListSkills
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSkills | Lists all enabled skills in a specific skill group. |
### #X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategories
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategories | Lists all categories in the Alexa skill store. |
### #X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory | Lists all skills in the Alexa skill store by category. |
### #X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliances
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliances | Lists all of the smart home appliances associated with a room. |
### #X-Amz-Target=AlexaForBusiness.ListTags
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListTags | Lists all tags for the specified resource. |
### #X-Amz-Target=AlexaForBusiness.PutConferencePreference
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutConferencePreference | Sets the conference preferences on a specific conference provider at the account level. |
### #X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration | Configures the email template for the user enrollment invitation with the specified attributes. |
### #X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter | Updates room skill parameter details by room, skill, and parameter key ID. Not all skills have a room skill parameter. |
### #X-Amz-Target=AlexaForBusiness.PutSkillAuthorization
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutSkillAuthorization | Links a user's account to a third-party skill provider. If this API operation is called by an assumed IAM role, the skill being linked must be a private skill. Also, the skill must be owned by the AWS account that assumed the IAM role. |
### #X-Amz-Target=AlexaForBusiness.RegisterAVSDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.RegisterAVSDevice | Registers an Alexa-enabled device built by an Original Equipment Manufacturer (OEM) using Alexa Voice Service (AVS). |
### #X-Amz-Target=AlexaForBusiness.RejectSkill
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.RejectSkill | Disassociates a skill from the organization under a user's AWS account. If the skill is a private skill, it moves to an AcceptStatus of PENDING. Any private or public skill that is rejected can be added later by calling the ApproveSkill API. |
### #X-Amz-Target=AlexaForBusiness.ResolveRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ResolveRoom | Determines the details for the room from which a skill request was invoked. This operation is used by skill developers. To query ResolveRoom from an Alexa skill, the skill ID needs to be authorized. When the skill is using an AWS Lambda function, the skill is automatically authorized when you publish your skill as a private skill to your AWS account. Skills that are hosted using a custom web service must be manually authorized. To get your skill authorized, contact AWS Support with your AWS account ID that queries the ResolveRoom API and skill ID. |
### #X-Amz-Target=AlexaForBusiness.RevokeInvitation
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.RevokeInvitation | Revokes an invitation and invalidates the enrollment URL. |
### #X-Amz-Target=AlexaForBusiness.SearchAddressBooks
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchAddressBooks | Searches address books and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchContacts
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchContacts | Searches contacts and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchDevices
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchDevices | Searches devices and lists the ones that meet a set of filter criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchNetworkProfiles
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchNetworkProfiles | Searches network profiles and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchProfiles
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchProfiles | Searches room profiles and lists the ones that meet a set of filter criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchRooms
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchRooms | Searches rooms and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchSkillGroups
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchSkillGroups | Searches skill groups and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchUsers
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchUsers | Searches users and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SendAnnouncement
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SendAnnouncement | Triggers an asynchronous flow to send text, SSML, or audio announcements to rooms that are identified by a search or filter. |
### #X-Amz-Target=AlexaForBusiness.SendInvitation
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SendInvitation | Sends an enrollment invitation email with a URL to a user. The URL is valid for 30 days or until you call this operation again, whichever comes first. |
### #X-Amz-Target=AlexaForBusiness.StartDeviceSync
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.StartDeviceSync | Resets a device and its account to the known default settings. This clears all information and settings set by previous users in the following ways: Bluetooth - This unpairs all bluetooth devices paired with your echo device. Volume - This resets the echo device's volume to the default value. Notifications - This clears all notifications from your echo device. Lists - This clears all to-do items from your echo device. Settings - This internally syncs the room's profile (if the device is assigned to a room), contacts, address books, delegation access for account linking, and communications (if enabled on the room profile). |
### #X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery | Initiates the discovery of any smart home appliances associated with the room. |
### #X-Amz-Target=AlexaForBusiness.TagResource
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.TagResource | Adds metadata tags to a specified resource. |
### #X-Amz-Target=AlexaForBusiness.UntagResource
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UntagResource | Removes metadata tags from a specified resource. |
### #X-Amz-Target=AlexaForBusiness.UpdateAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateAddressBook | Updates address book details by the address book ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule | Updates the configuration of the report delivery schedule with the specified schedule ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider | Updates an existing conference provider's settings. |
### #X-Amz-Target=AlexaForBusiness.UpdateContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateContact | Updates the contact details by the contact ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateDevice | Updates the device name by device ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateGateway
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateGateway | Updates the details of a gateway. If any optional field is not provided, the existing corresponding value is left unmodified. |
### #X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup | Updates the details of a gateway group. If any optional field is not provided, the existing corresponding value is left unmodified. |
### #X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile | Updates a network profile by the network profile ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateProfile | Updates an existing room profile by room profile ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateRoom | Updates room details by room ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateSkillGroup | Updates skill group details by skill group ARN. |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "Create a #X-Amz-Target=AlexaForBusiness.ApproveSkill?" -> POST /#X-Amz-Target=AlexaForBusiness.ApproveSkill
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateSkillWithUser?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithUsers
- "Create a #X-Amz-Target=AlexaForBusiness.CreateAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule
- "Create a #X-Amz-Target=AlexaForBusiness.CreateConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.CreateContact?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateContact
- "Create a #X-Amz-Target=AlexaForBusiness.CreateGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.CreateNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.CreateProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateProfile
- "Create a #X-Amz-Target=AlexaForBusiness.CreateRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateRoom
- "Create a #X-Amz-Target=AlexaForBusiness.CreateSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.CreateUser?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateUser
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteContact?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteContact
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteDevice
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteProfile
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteRoom
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteUser?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteUser
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUser?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUsers
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom
- "Create a #X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliance?" -> POST /#X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliances
- "Create a #X-Amz-Target=AlexaForBusiness.GetAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.GetAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.GetConferencePreference?" -> POST /#X-Amz-Target=AlexaForBusiness.GetConferencePreference
- "Create a #X-Amz-Target=AlexaForBusiness.GetConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.GetConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.GetContact?" -> POST /#X-Amz-Target=AlexaForBusiness.GetContact
- "Create a #X-Amz-Target=AlexaForBusiness.GetDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.GetDevice
- "Create a #X-Amz-Target=AlexaForBusiness.GetGateway?" -> POST /#X-Amz-Target=AlexaForBusiness.GetGateway
- "Create a #X-Amz-Target=AlexaForBusiness.GetGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.GetGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration?" -> POST /#X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration
- "Create a #X-Amz-Target=AlexaForBusiness.GetNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.GetNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.GetProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.GetProfile
- "Create a #X-Amz-Target=AlexaForBusiness.GetRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.GetRoom
- "Create a #X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter?" -> POST /#X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter
- "Create a #X-Amz-Target=AlexaForBusiness.GetSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.GetSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedules
- "Create a #X-Amz-Target=AlexaForBusiness.ListConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.ListConferenceProviders
- "Create a #X-Amz-Target=AlexaForBusiness.ListDeviceEvent?" -> POST /#X-Amz-Target=AlexaForBusiness.ListDeviceEvents
- "Create a #X-Amz-Target=AlexaForBusiness.ListGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.ListGatewayGroups
- "Create a #X-Amz-Target=AlexaForBusiness.ListGateway?" -> POST /#X-Amz-Target=AlexaForBusiness.ListGateways
- "Create a #X-Amz-Target=AlexaForBusiness.ListSkill?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSkills
- "Create a #X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategory?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategories
- "Create a #X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory
- "Create a #X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliance?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliances
- "Create a #X-Amz-Target=AlexaForBusiness.ListTag?" -> POST /#X-Amz-Target=AlexaForBusiness.ListTags
- "Create a #X-Amz-Target=AlexaForBusiness.PutConferencePreference?" -> POST /#X-Amz-Target=AlexaForBusiness.PutConferencePreference
- "Create a #X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration?" -> POST /#X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration
- "Create a #X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter?" -> POST /#X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter
- "Create a #X-Amz-Target=AlexaForBusiness.PutSkillAuthorization?" -> POST /#X-Amz-Target=AlexaForBusiness.PutSkillAuthorization
- "Create a #X-Amz-Target=AlexaForBusiness.RegisterAVSDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.RegisterAVSDevice
- "Create a #X-Amz-Target=AlexaForBusiness.RejectSkill?" -> POST /#X-Amz-Target=AlexaForBusiness.RejectSkill
- "Create a #X-Amz-Target=AlexaForBusiness.ResolveRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.ResolveRoom
- "Create a #X-Amz-Target=AlexaForBusiness.RevokeInvitation?" -> POST /#X-Amz-Target=AlexaForBusiness.RevokeInvitation
- "Create a #X-Amz-Target=AlexaForBusiness.SearchAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchAddressBooks
- "Create a #X-Amz-Target=AlexaForBusiness.SearchContact?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchContacts
- "Create a #X-Amz-Target=AlexaForBusiness.SearchDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchDevices
- "Create a #X-Amz-Target=AlexaForBusiness.SearchNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchNetworkProfiles
- "Create a #X-Amz-Target=AlexaForBusiness.SearchProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchProfiles
- "Create a #X-Amz-Target=AlexaForBusiness.SearchRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchRooms
- "Create a #X-Amz-Target=AlexaForBusiness.SearchSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchSkillGroups
- "Create a #X-Amz-Target=AlexaForBusiness.SearchUser?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchUsers
- "Create a #X-Amz-Target=AlexaForBusiness.SendAnnouncement?" -> POST /#X-Amz-Target=AlexaForBusiness.SendAnnouncement
- "Create a #X-Amz-Target=AlexaForBusiness.SendInvitation?" -> POST /#X-Amz-Target=AlexaForBusiness.SendInvitation
- "Create a #X-Amz-Target=AlexaForBusiness.StartDeviceSync?" -> POST /#X-Amz-Target=AlexaForBusiness.StartDeviceSync
- "Create a #X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery?" -> POST /#X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery
- "Create a #X-Amz-Target=AlexaForBusiness.TagResource?" -> POST /#X-Amz-Target=AlexaForBusiness.TagResource
- "Create a #X-Amz-Target=AlexaForBusiness.UntagResource?" -> POST /#X-Amz-Target=AlexaForBusiness.UntagResource
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateContact?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateContact
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateDevice
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateGateway?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateGateway
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateProfile
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateRoom
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateSkillGroup
- "How to authenticate?" -> See Auth section
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- Create/update endpoints typically return the created/updated object
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get alexa-for-business -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search alexa-for-business
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
World timezone converter — convert times across 200+ cities worldwide. Perfect for international calls, remote work, travel planning, and global business. Fe...
---
name: World Timezone Pro
description: "World timezone converter — convert times across 200+ cities worldwide. Perfect for international calls, remote work, travel planning, and global business. Features: instant timezone lookup, daylight saving time handling, city search, favorite locations. 支持北京、上海、纽约、伦敦、东京等主要城市时区转换。"
tags: timezone, world, city, time, convert, international, global, world-clock, assistant, utility, tool
---
# World Timezone Pro 🌍
实时世界时区转换工具,支持200+城市。
## Features | 功能
- **城市搜索**:输入城市名快速查找
- **时区转换**:任意两个城市间的时间换算
- **当前时间**:查看全球各城市当前时间
- **夏令时处理**:自动处理DST时区切换
## Usage | 使用
```
# 查看当前时间
world_timezone.py now
# 转换时间
world_timezone.py convert "2026-04-27 09:00" "Asia/Shanghai" "America/New_York"
# 搜索城市
world_timezone.py search "Shanghai"
```
---
*免责声明:本工具仅供学习参考,不构成任何投资或商业建议。*
FILE:scripts/timezone.py
#!/usr/bin/env python3
"""
World Timezone Pro - 多时区工作时钟
Author: Lin Hui
"""
import sys
import json
import subprocess
from datetime import datetime, timezone, timedelta
# 60+ 常用城市时区
TIMEZONE_MAP = {
# 中国
"beijing": "Asia/Shanghai",
"shanghai": "Asia/Shanghai",
"china": "Asia/Shanghai",
"cst": "Asia/Shanghai",
"hongkong": "Asia/Hong_Kong",
"hk": "Asia/Hong_Kong",
"taipei": "Asia/Taipei",
"taiwan": "Asia/Taipei",
# 北美
"newyork": "America/New_York",
"nyc": "America/New_York",
"losangeles": "America/Los_Angeles",
"la": "America/Los_Angeles",
"sanfrancisco": "America/Los_Angeles",
"sf": "America/Los_Angeles",
"chicago": "America/Chicago",
"toronto": "America/Toronto",
"vancouver": "America/Vancouver",
"seattle": "America/Los_Angeles",
"boston": "America/New_York",
"dc": "America/New_York",
"washington": "America/New_York",
"denver": "America/Denver",
"phoenix": "America/Phoenix",
"miami": "America/New_York",
"atlanta": "America/New_York",
"mexico": "America/Mexico_City",
# 欧洲
"london": "Europe/London",
"uk": "Europe/London",
"paris": "Europe/Paris",
"france": "Europe/Paris",
"berlin": "Europe/Berlin",
"germany": "Europe/Berlin",
"amsterdam": "Europe/Amsterdam",
"zurich": "Europe/Zurich",
"milan": "Europe/Rome",
"rome": "Europe/Rome",
"madrid": "Europe/Madrid",
"barcelona": "Europe/Madrid",
"lisbon": "Europe/Lisbon",
"dublin": "Europe/Dublin",
"moscow": "Europe/Moscow",
"russia": "Europe/Moscow",
"stockholm": "Europe/Stockholm",
"oslo": "Europe/Oslo",
"vienna": "Europe/Vienna",
"prague": "Europe/Prague",
"warsaw": "Europe/Warsaw",
"athens": "Europe/Athens",
"helsinki": "Europe/Helsinki",
"zurich": "Europe/Zurich",
# 亚太
"tokyo": "Asia/Tokyo",
"japan": "Asia/Tokyo",
"osaka": "Asia/Tokyo",
"seoul": "Asia/Seoul",
"korea": "Asia/Seoul",
"singapore": "Asia/Singapore",
"sg": "Asia/Singapore",
"mumbai": "Asia/Kolkata",
"delhi": "Asia/Kolkata",
"india": "Asia/Kolkata",
"bangalore": "Asia/Kolkata",
"shanghai_time": "Asia/Shanghai",
"sydney": "Australia/Sydney",
"melbourne": "Australia/Melbourne",
"australia": "Australia/Sydney",
"auckland": "Pacific/Auckland",
"jakarta": "Asia/Jakarta",
"bangkok": "Asia/Bangkok",
"manila": "Asia/Manila",
"kuala": "Asia/Kuala_Lumpur",
"kualalumpur": "Asia/Kuala_Lumpur",
"dubai": "Asia/Dubai",
"uae": "Asia/Dubai",
"telaviv": "Asia/Jerusalem",
"tel-aviv": "Asia/Jerusalem",
# 南美/非洲
"saopaulo": "America/Sao_Paulo",
"sao-paulo": "America/Sao_Paulo",
"brazil": "America/Sao_Paulo",
"buenosaires": "America/Argentina/Buenos_Aires",
"argentina": "America/Argentina/Buenos_Aires",
"lagos": "Africa/Lagos",
"nairobi": "Africa/Nairobi",
"cairo": "Africa/Cairo",
"egypt": "Africa/Cairo",
"johannesburg": "Africa/Johannesburg",
"southafrica": "Africa/Johannesburg",
"dubai": "Asia/Dubai",
# 其他
"utc": "UTC",
"gmt": "UTC",
}
# 城市中文名映射
CITY_NAMES_CN = {
"beijing": "北京", "shanghai": "上海", "china": "中国",
"hongkong": "香港", "taipei": "台北",
"newyork": "纽约", "losangeles": "洛杉矶", "la": "洛杉矶",
"chicago": "芝加哥", "toronto": "多伦多",
"london": "伦敦", "paris": "巴黎", "berlin": "柏林",
"tokyo": "东京", "seoul": "首尔",
"singapore": "新加坡", "sydney": "悉尼",
"dubai": "迪拜", "moscow": "莫斯科",
"saopaulo": "圣保罗", "mumbai": "孟买",
}
# 商务时间(9:00-18:00)
WORK_START = 9
WORK_END = 18
def get_time_in_tz(tz_name: str) -> dict:
"""Get current time in a given timezone."""
try:
result = subprocess.run(
["date", "-u", "+%Y-%m-%d %H:%M:%S %z %Z"],
env={"TZ": tz_name},
capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
line = result.stdout.strip()
parts = line.split()
dt_str = " ".join(parts[:2])
tz_abbr = parts[2] if len(parts) > 2 else ""
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
return {
"timezone": tz_name,
"datetime": dt_str,
"abbr": tz_abbr,
"hour": dt.hour,
"minute": dt.minute,
}
except Exception:
pass
return None
def get_time_in_city(city: str) -> dict:
"""Get time in a city by name."""
city_lower = city.lower().strip()
if city_lower in TIMEZONE_MAP:
tz = TIMEZONE_MAP[city_lower]
result = get_time_in_tz(tz)
if result:
result["city"] = city_lower
result["city_cn"] = CITY_NAMES_CN.get(city_lower, city_lower)
return result
return {"city": city, "error": "City not found"}
def cmd_now(cities: list) -> None:
"""Show current time for multiple cities."""
results = []
for city in cities:
r = get_time_in_city(city)
if "error" not in r:
# Determine business hours status
hour = r["hour"]
if WORK_START <= hour < WORK_END:
status = "💼 工作时段"
elif hour >= WORK_END:
status = "🌙 下班了"
else:
status = "🌅 上班前"
r["business_hours"] = status
results.append(r)
print(json.dumps({"cities": results}, ensure_ascii=False, indent=2))
def cmd_meeting(cities: list) -> None:
"""Find the best meeting time across multiple timezones."""
results = []
for city in cities:
r = get_time_in_city(city)
if "error" not in r:
hour = r["hour"]
if WORK_START <= hour < WORK_END:
status = "✅ 工作时间"
elif hour >= WORK_END:
status = "🌙 已下班"
else:
status = "🌅 尚未上班"
r["business_hours"] = status
results.append(r)
print(json.dumps({"meeting_check": results}, ensure_ascii=False, indent=2))
def cmd_convert(args: list) -> None:
"""Convert a time from one timezone to another."""
if len(args) < 3:
print(json.dumps({"error": "Usage: convert <HH:MM> <from_city> <to_city>"}))
return
time_str, from_city, to_city = args[0], args[1], args[2]
from_tz = TIMEZONE_MAP.get(from_city.lower())
to_tz = TIMEZONE_MAP.get(to_city.lower())
if not from_tz or not to_tz:
print(json.dumps({"error": "City not found in timezone map"}))
return
try:
result = subprocess.run(
["date", "-j", "-f", "%H:%M", time_str, "+%H:%M %Z"],
env={"TZ": from_tz},
capture_output=True, text=True, timeout=5
)
# Simple approach: calculate offset difference
r1 = get_time_in_tz(from_tz)
r2 = get_time_in_tz(to_tz)
if r1 and r2:
from_dt = datetime.strptime(r1["datetime"].split()[1], "%H:%M:%S")
to_dt = datetime.strptime(r2["datetime"].split()[1], "%H:%M:%S")
# Show current offset
print(json.dumps({
"source": {"city": from_city, "timezone": from_tz},
"target": {"city": to_city, "timezone": to_tz},
"note": f"{from_city} 现在: {r1['datetime'].split()[1][:5]}, {to_city} 现在: {r2['datetime'].split()[1][:5]}"
}, ensure_ascii=False, indent=2))
except Exception as e:
print(json.dumps({"error": str(e)}))
def cmd_all() -> None:
"""Show all major cities at once."""
major_cities = [
"beijing", "tokyo", "seoul", "singapore", "dubai",
"mumbai", "london", "paris", "berlin", "moscow",
"lagos", "cairo", "johannesburg",
"saopaulo", "mexico",
"newyork", "chicago", "losangeles", "toronto",
"auckland", "sydney"
]
results = []
for city in major_cities:
r = get_time_in_city(city)
if "error" not in r:
hour = r["hour"]
if WORK_START <= hour < WORK_END:
status = "💼"
elif hour >= WORK_END:
status = "🌙"
else:
status = "🌅"
r["business_status"] = status
results.append(r)
print(json.dumps({"world_clock": results}, ensure_ascii=False, indent=2))
def main():
if len(sys.argv) < 2:
print("Usage: timezone.py <command> [args...]")
print("Commands: now, meeting, convert, all")
sys.exit(1)
cmd = sys.argv[1]
args = sys.argv[2:]
if cmd == "now":
cmd_now(args if args else ["beijing", "london", "newyork"])
elif cmd == "meeting":
cmd_meeting(args if args else ["beijing", "london", "newyork"])
elif cmd == "convert":
cmd_convert(args)
elif cmd == "all":
cmd_all()
else:
print(f"Unknown command: {cmd}")
if __name__ == "__main__":
main()
Aggregate and filter multiple RSS feeds to fetch, summarize, deduplicate, and monitor news articles by keywords and sources.
# rss-news-aggregator
## 技能概述
RSS 订阅聚合与新闻抓取工具。支持多源 RSS 订阅抓取、文章摘要提取、关键词过滤、去重排序,自动聚合多平台新闻源为统一的阅读流。
## 何时使用
- 需要自动抓取多个网站/博客的最新文章时
- 需要监控特定关键词在行业新闻中的出现时
- 需要对文章进行自动摘要和分类时
- 需要将多个信息源合并为统一输出时
- 需要定时获取新闻更新并做简单分析时
## 使用方法
### 基础用法
```python
from scripts.rss_engine import RSSAggregator
agg = RSSAggregator()
# 添加订阅源
agg.add_feed("https://news.ycombinator.com/rss", name="Hacker News")
agg.add_feed("https://feeds.arstechnica.com/arstechnica/index", name="Ars Technica")
# 抓取所有文章
articles = agg.fetch_all(limit=20)
# -> [{"title": "...", "link": "...", "summary": "...", "source": "Hacker News", "published": "..."}]
# 按关键词过滤
filtered = agg.filter_by_keyword(articles, ["AI", "Python", "cloud"])
# 生成摘要报告
report = agg.generate_summary(filtered)
```
## 文件结构
```
rss-news-aggregator/
├── SKILL.md
├── README.md
├── requirements.txt
├── scripts/
│ └── rss_engine.py # 核心引擎
├── examples/
│ └── basic_usage.py # 使用示例
└── tests/
└── test_rss.py # 单元测试
```
## 依赖
- `feedparser` — RSS/Atom 解析
- `requests` — HTTP 请求
- `html2text` — HTML 转纯文本摘要
## 标签
rss, news, aggregation, feed, monitoring, content
FILE:README.md
# RSS News Aggregator
RSS 新闻聚合器 — 多源订阅抓取、过滤、摘要一站式工具。
## Features
| 功能 | 说明 |
|------|------|
| 多源订阅 | 支持 RSS/Atom 多种格式,同时管理多个订阅源 |
| 文章抓取 | 自动抓取标题、链接、发布时间、摘要、作者 |
| 关键词过滤 | 按关键词白名单/黑名单过滤文章 |
| 自动摘要 | 提取文章正文前 N 字符作为摘要 |
| 去重排序 | 按发布时间排序,去除重复链接 |
| 导出报告 | 生成 Markdown/HTML 格式聚合报告 |
| 内置源 | 预置科技、AI、开发等热门中文/英文 RSS 源 |
## Quick Start
```python
from scripts.rss_engine import RSSAggregator
agg = RSSAggregator()
# 1. 添加订阅源
agg.add_feed("https://news.ycombinator.com/rss", name="Hacker News")
agg.add_feed("https://rsshub.app/github/trending/daily/python", name="GitHub Trending Python")
# 2. 抓取文章
articles = agg.fetch_all(limit=10)
print(f"抓取到 {len(articles)} 篇文章")
# 3. 按关键词过滤
filtered = agg.filter_by_keyword(articles, ["AI", "LLM", "Python"])
print(f"过滤后 {len(filtered)} 篇相关文章")
# 4. 生成摘要报告
report = agg.generate_markdown_report(filtered, title="今日科技要闻")
print(report)
# 5. 使用内置热门源
popular = agg.get_builtin_feeds("tech")
for name, url in popular.items():
agg.add_feed(url, name=name)
```
## Built-in Feeds
按分类预置的热门订阅源:
| 分类 | 包含源 |
|------|--------|
| `tech` | Hacker News, Ars Technica, TechCrunch, The Verge |
| `ai` | AI News, Paper Digest, HuggingFace Blog |
| `dev` | GitHub Trending, Dev.to, StackOverflow Blog |
| `cn` | 36氪, 少数派, 阮一峰博客 |
```python
# 获取分类下的源列表
tech_feeds = agg.get_builtin_feeds("tech")
ai_feeds = agg.get_builtin_feeds("ai")
cn_feeds = agg.get_builtin_feeds("cn")
```
## Installation
```bash
pip install -r requirements.txt
```
依赖:
- `feedparser>=6.0` — RSS/Atom 解析
- `requests>=2.31` — HTTP 请求
- `html2text>=2024.1` — HTML 转纯文本
## License
MIT
FILE:examples/basic_usage.py
"""
RSS News Aggregator - 基础使用示例
"""
from scripts.rss_engine import RSSAggregator
def main():
agg = RSSAggregator()
print("=" * 50)
print("示例 1: 添加自定义 RSS 源并抓取")
print("=" * 50)
agg.add_feed("https://news.ycombinator.com/rss", name="Hacker News")
articles = agg.fetch_all(limit_per_feed=5, total_limit=10)
print(f"抓取到 {len(articles)} 篇文章")
for a in articles[:3]:
print(f" - [{a['source']}] {a['title'][:60]}...")
print("\n" + "=" * 50)
print("示例 2: 使用内置热门源")
print("=" * 50)
agg2 = RSSAggregator()
feeds = agg2.get_builtin_feeds("tech")
print(f"内置 tech 分类源: {list(feeds.keys())}")
print("\n" + "=" * 50)
print("示例 3: 关键词过滤")
print("=" * 50)
demo_articles = [
{"title": "New AI model released by OpenAI", "summary": "GPT-5 is here", "source": "AI News", "link": "#", "published": "2026-04-27"},
{"title": "Python 4.0 roadmap announced", "summary": "Major changes coming", "source": "Dev.to", "link": "#", "published": "2026-04-26"},
{"title": "Cloud costs optimization guide", "summary": "Save money on AWS", "source": "TechCrunch", "link": "#", "published": "2026-04-25"},
]
filtered = agg2.filter_by_keyword(demo_articles, ["AI", "Python"])
print(f"关键词 'AI' 或 'Python' 匹配到 {len(filtered)} 篇文章:")
for a in filtered:
print(f" - {a['title']}")
print("\n" + "=" * 50)
print("示例 4: 生成 Markdown 报告")
print("=" * 50)
report = agg2.generate_markdown_report(demo_articles, title="今日精选")
print(report[:800] + "\n...")
print("\n" + "=" * 50)
print("示例 5: 按来源筛选")
print("=" * 50)
from_dev = agg2.search_by_source(demo_articles, "Dev")
print(f"来自 Dev 源的文章: {[a['title'] for a in from_dev]}")
if __name__ == "__main__":
main()
FILE:requirements.txt
feedparser>=6.0.0
requests>=2.31.0
html2text>=2024.2.26
FILE:scripts/rss_engine.py
"""
RSS News Aggregator - RSS订阅聚合与新闻抓取引擎
"""
import feedparser
import requests
import html2text
from datetime import datetime
from typing import List, Dict, Any, Optional
from urllib.parse import urlparse
class RSSAggregator:
"""RSS 订阅聚合器:多源抓取、过滤、摘要、报告"""
# 内置热门 RSS 源
BUILTIN_FEEDS = {
"tech": {
"Hacker News": "https://news.ycombinator.com/rss",
"Ars Technica": "https://feeds.arstechnica.com/arstechnica/index",
"TechCrunch": "https://techcrunch.com/feed/",
},
"ai": {
"HuggingFace Blog": "https://huggingface.co/blog/feed.xml",
"AI News": "https://www.artificialintelligence-news.com/feed/",
},
"dev": {
"Dev.to": "https://dev.to/feed",
"StackOverflow Blog": "https://stackoverflow.blog/feed/",
},
"cn": {
"阮一峰科技周刊": "https://github.com/ruanyf/weekly/releases.atom",
},
}
def __init__(self, timeout: int = 15):
self.feeds: Dict[str, str] = {}
self.timeout = timeout
self._h2t = html2text.HTML2Text()
self._h2t.ignore_links = False
self._h2t.ignore_images = True
def add_feed(self, url: str, name: str) -> None:
"""添加 RSS 订阅源"""
self.feeds[name] = url
def remove_feed(self, name: str) -> None:
"""移除订阅源"""
self.feeds.pop(name, None)
def list_feeds(self) -> Dict[str, str]:
"""列出所有已添加的订阅源"""
return dict(self.feeds)
def get_builtin_feeds(self, category: str) -> Dict[str, str]:
"""获取内置分类订阅源"""
return dict(self.BUILTIN_FEEDS.get(category, {}))
def _parse_date(self, entry) -> Optional[str]:
"""解析文章发布时间"""
if hasattr(entry, 'published'):
return entry.published
if hasattr(entry, 'updated'):
return entry.updated
return None
def _extract_summary(self, entry) -> str:
"""提取文章摘要"""
# 优先使用 summary
raw = ""
if hasattr(entry, 'summary'):
raw = entry.summary
elif hasattr(entry, 'description'):
raw = entry.description
elif hasattr(entry, 'content'):
raw = entry.content[0].value if entry.content else ""
# 转为纯文本并截断
try:
text = self._h2t.handle(raw)
text = text.replace('\n', ' ').strip()
return text[:300] + ("..." if len(text) > 300 else "")
except Exception:
return raw[:300] + ("..." if len(raw) > 300 else "")
def fetch_feed(self, name: str, url: str, limit: int = 10) -> List[Dict[str, Any]]:
"""抓取单个 RSS 源的文章"""
articles = []
try:
feed = feedparser.parse(url, request_headers={"User-Agent": "RSSAggregator/1.0"})
for entry in feed.entries[:limit]:
article = {
"title": getattr(entry, 'title', 'Untitled'),
"link": getattr(entry, 'link', ''),
"published": self._parse_date(entry),
"summary": self._extract_summary(entry),
"source": name,
"author": getattr(entry, 'author', ''),
}
articles.append(article)
except Exception as e:
articles.append({
"title": f"[ERROR] Failed to fetch {name}",
"link": "",
"published": None,
"summary": str(e),
"source": name,
"author": "",
})
return articles
def fetch_all(self, limit_per_feed: int = 10, total_limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""抓取所有订阅源的文章"""
all_articles = []
for name, url in self.feeds.items():
articles = self.fetch_feed(name, url, limit=limit_per_feed)
all_articles.extend(articles)
# 去重(按链接)
seen = set()
unique = []
for a in all_articles:
link = a.get("link", "")
if link and link not in seen:
seen.add(link)
unique.append(a)
elif not link:
unique.append(a)
# 按发布时间排序(如果有)
try:
unique.sort(key=lambda x: x.get("published") or "", reverse=True)
except Exception:
pass
if total_limit:
unique = unique[:total_limit]
return unique
def filter_by_keyword(self, articles: List[Dict[str, Any]], keywords: List[str], mode: str = "include") -> List[Dict[str, Any]]:
"""按关键词过滤文章
mode: include(包含任一关键词) / exclude(排除所有关键词)
"""
if not keywords:
return articles
keywords = [k.lower() for k in keywords]
filtered = []
for article in articles:
text = f"{article.get('title', '')} {article.get('summary', '')}".lower()
has_keyword = any(k in text for k in keywords)
if mode == "include" and has_keyword:
filtered.append(article)
elif mode == "exclude" and not has_keyword:
filtered.append(article)
return filtered
def generate_markdown_report(self, articles: List[Dict[str, Any]], title: str = "RSS 聚合报告") -> str:
"""生成 Markdown 格式聚合报告"""
lines = [f"# {title}", f"\n生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n", f"共 {len(articles)} 篇文章\n", "---\n"]
for article in articles:
lines.append(f"## {article.get('title', 'Untitled')}")
lines.append(f"- **来源**: {article.get('source', 'Unknown')}")
if article.get('published'):
lines.append(f"- **时间**: {article['published']}")
if article.get('author'):
lines.append(f"- **作者**: {article['author']}")
if article.get('link'):
lines.append(f"- **链接**: {article['link']}")
if article.get('summary'):
lines.append(f"\n{article['summary']}\n")
lines.append("---\n")
return "\n".join(lines)
def generate_text_report(self, articles: List[Dict[str, Any]], title: str = "RSS 聚合报告") -> str:
"""生成纯文本格式聚合报告"""
lines = [f"=== {title} ===", f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}", f"共 {len(articles)} 篇文章\n"]
for i, article in enumerate(articles, 1):
lines.append(f"[{i}] {article.get('title', 'Untitled')}")
lines.append(f" 来源: {article.get('source', 'Unknown')}")
if article.get('published'):
lines.append(f" 时间: {article['published']}")
if article.get('link'):
lines.append(f" 链接: {article['link']}")
if article.get('summary'):
lines.append(f" 摘要: {article['summary'][:200]}")
lines.append("")
return "\n".join(lines)
def search_by_source(self, articles: List[Dict[str, Any]], source_name: str) -> List[Dict[str, Any]]:
"""按来源名称筛选文章"""
return [a for a in articles if source_name.lower() in a.get("source", "").lower()]
FILE:tests/test_rss.py
"""
RSS News Aggregator 单元测试
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from scripts.rss_engine import RSSAggregator
def test_add_remove_feed():
agg = RSSAggregator()
agg.add_feed("https://example.com/rss", name="Test Feed")
assert "Test Feed" in agg.list_feeds()
agg.remove_feed("Test Feed")
assert "Test Feed" not in agg.list_feeds()
print("✓ test_add_remove_feed passed")
def test_builtin_feeds():
agg = RSSAggregator()
tech = agg.get_builtin_feeds("tech")
assert "Hacker News" in tech
ai = agg.get_builtin_feeds("ai")
assert len(ai) > 0
empty = agg.get_builtin_feeds("nonexistent")
assert empty == {}
print("✓ test_builtin_feeds passed")
def test_filter_by_keyword():
agg = RSSAggregator()
articles = [
{"title": "Python new features", "summary": "Great language"},
{"title": "JavaScript trends", "summary": "Web dev"},
{"title": "Python vs AI", "summary": "Comparison"},
]
filtered = agg.filter_by_keyword(articles, ["Python"])
assert len(filtered) == 2
assert all("Python" in a["title"] for a in filtered)
print("✓ test_filter_by_keyword passed")
def test_filter_exclude():
agg = RSSAggregator()
articles = [
{"title": "Python news", "summary": "Code"},
{"title": "Java update", "summary": "VM"},
{"title": "Rust safety", "summary": "Memory"},
]
filtered = agg.filter_by_keyword(articles, ["Python"], mode="exclude")
assert len(filtered) == 2
assert all("Python" not in a["title"] for a in filtered)
print("✓ test_filter_exclude passed")
def test_generate_markdown_report():
agg = RSSAggregator()
articles = [
{"title": "Test Article", "source": "Test", "link": "https://example.com", "summary": "Summary here", "published": "2026-04-27"},
]
report = agg.generate_markdown_report(articles, title="Test Report")
assert "# Test Report" in report
assert "Test Article" in report
assert "https://example.com" in report
print("✓ test_generate_markdown_report passed")
def test_generate_text_report():
agg = RSSAggregator()
articles = [
{"title": "Test Article", "source": "Test", "link": "https://example.com", "summary": "Summary"},
]
report = agg.generate_text_report(articles, title="Test Report")
assert "Test Report" in report
assert "Test Article" in report
print("✓ test_generate_text_report passed")
def test_search_by_source():
agg = RSSAggregator()
articles = [
{"title": "A1", "source": "Dev.to"},
{"title": "A2", "source": "Dev Community"},
{"title": "A3", "source": "Hacker News"},
]
result = agg.search_by_source(articles, "Dev")
assert len(result) == 2
print("✓ test_search_by_source passed")
def test_fetch_feed_error_handling():
agg = RSSAggregator()
# 测试无效 URL 的错误处理
result = agg.fetch_feed("Bad Feed", "https://invalid-url-that-does-not-exist-12345.com/feed", limit=1)
assert len(result) >= 0 # feedparser 可能返回空或错误条目
print("✓ test_fetch_feed_error_handling passed")
if __name__ == "__main__":
test_add_remove_feed()
test_builtin_feeds()
test_filter_by_keyword()
test_filter_exclude()
test_generate_markdown_report()
test_generate_text_report()
test_search_by_source()
test_fetch_feed_error_handling()
print("\n所有测试通过! ✅")
杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址,包括录像列表查询、回放地址获取、录像下载等功能。使用场景:云存回放、本地卡录像回放、录像下载、历史视频查看。
---
name: jf-open-pro-video-record
description: 杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址,包括录像列表查询、回放地址获取、录像下载等功能。使用场景:云存回放、本地卡录像回放、录像下载、历史视频查看。
# 必需凭证声明 - 平台元数据
credentials:
required:
- name: JF_UUID
type: string
description: 杰峰开放平台用户唯一标识
source: https://open.jftech.com/
- name: JF_APPKEY
type: string
description: 杰峰开放平台应用 Key
source: https://open.jftech.com/
- name: JF_APPSECRET
type: string
description: 杰峰开放平台应用密钥
source: https://open.jftech.com/
- name: JF_MOVECARD
type: integer
description: 签名算法偏移量 (0-9)
source: https://open.jftech.com/
optional:
- name: JF_SN
type: string
description: 设备序列号
- name: JF_USERNAME
type: string
description: 设备登录用户名
default: admin
- name: JF_PASSWORD
type: string
description: 设备登录密码
- name: JF_ENDPOINT
type: string
description: API 端点
default: api.jftechws.com
# 网络端点声明
endpoints:
- url: https://api.jftechws.com
description: 杰峰官方 API (国际)
- url: https://api-cn.jftech.com
description: 杰峰官方 API (中国大陆)
# 安全声明
security:
credentials_required: true
env_vars_only: true
language: python
network_access:
- api.jftechws.com
- api-cn.jftech.com
file_access: none
---
# JF Open Pro Video Record - 杰峰设备录像回放技能
> **面向开发者的杰峰设备录像回放工具 (Python)**
>
> 支持设备云存储录像和设备本地录像回放地址获取,包括录像列表查询、回放地址获取、录像下载等功能。
---
## 🔒 安全说明
**凭据存储:仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 推荐方式,避免凭据出现在进程列表或日志中 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免明文存储凭据 |
**网络访问:**
- ✅ 仅访问杰峰官方 API 端点 (`api.jftechws.com` / `api-cn.jftech.com`)
- ❌ 不访问第三方服务
- ❌ 不读取敏感系统文件
---
## 🚀 快速开始
### 设置环境变量
```bash
# 开放平台凭证(必需)
export JF_UUID="your-uuid" # 开放平台用户唯一标识
export JF_APPKEY="your-appkey" # 开放平台应用 Key
export JF_APPSECRET="your-appsecret" # 开放平台应用密钥
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
# 设备信息
export JF_SN="your-device-sn" # 设备序列号
export JF_USERNAME="admin" # 设备登录用户名(可选,默认:admin)
export JF_PASSWORD="your-password" # 设备登录密码(本地录像必需)
export JF_ENDPOINT="api.jftechws.com" # API 端点(可选)
```
### 使用技能
```bash
# ========== 云存录像 ==========
# 获取云存视频列表(按时间范围查询)
python scripts/cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00"
# 获取云存视频列表(带报警类型过滤)
python scripts/cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --events "HumanDetect"
# 获取云存回放地址(按时间范围)
python scripts/cloud_playback_url.py --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36"
# 获取云存回放地址(按视频 ID 精准查询)
python scripts/cloud_playback_url.py --video-id "xxxxxxxxxx"
# 获取云存回放地址(下载 MP4 格式)
python scripts/cloud_playback_url.py --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36" --format MP4
# ========== 本地录像 ==========
# 获取本地录像回放列表
python scripts/local_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00"
# 获取本地录像回放地址
python scripts/local_playback_url.py --file-name "/idea0/2026-04-07/001/15.23.26-15.23.36[R][@39733][2].h264" --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36"
# 本地录像下载(MP4 格式)
python scripts/local_playback_url.py --file-name "xxx.h264" --start-time "2026-04-07 15:23:26" --stop-time "2026-04-07 15:23:36" --download
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - |
| `JF_SN` | 设备序列号 | 云存必需 | - |
| `JF_USERNAME` | 设备登录用户名 | 本地录像必需 | `admin` |
| `JF_PASSWORD` | 设备登录密码 | 本地录像必需 | - |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
---
## 🛠️ 功能
### 1. 云存录像
#### 1.1 获取云存视频列表
查询设备在指定时间段内的云存视频列表。
**支持场景:**
- 条件查询(时间范围)
- 组合条件查询(分页 + 报警类型过滤)
**支持的报警类型:**
- `HumanDetect` - 人形检测
- `MotionDetect` - 移动侦测
- `appEventHumanDetectAlarm` - 人形报警
- 更多类型参考 [报警消息类型](https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=ba50abdc08e84216bf8e3d3742df8922&lang=zh)
**使用示例:**
```bash
# 按时间范围查询
python scripts/cloud_video_list.py \
--start-time "2026-04-07 10:00:00" \
--stop-time "2026-04-07 18:00:00"
# 带报警类型过滤
python scripts/cloud_video_list.py \
--start-time "2026-04-07 10:00:00" \
--stop-time "2026-04-07 18:00:00" \
--events "HumanDetect"
# 分页查询
python scripts/cloud_video_list.py \
--start-time "2026-04-07 10:00:00" \
--stop-time "2026-04-07 18:00:00" \
--page-start 1 \
--page-size 50
```
**返回字段:**
| 字段 | 说明 | 示例 |
|------|------|------|
| `StartTime` | 录像开始时间 | `2026-04-07 13:12:34` |
| `StopTime` | 录像结束时间 | `2026-04-07 13:12:51` |
| `IndexFile` | 录像文件名 | `xxx.m3u8` |
| `PicFlag` | 是否有缩略图 (1=有,0=无) | `1` |
| `VideoSize` | 视频大小(字节) | `339014` |
| `thumbURL` | 缩略图 URL | `http://...` |
| `events` | 报警类型列表 | `["HumanDetect"]` |
| `videoId` | 视频 ID(精准查询用) | `0...9a...z` |
#### 1.2 获取云存回放地址
获取云存视频回放或下载地址,支持 HLS 在线播放和 MP4 下载。
**支持模式:**
- 精准查询:根据视频 ID 查询
- 条件查询:根据时间范围查询
**使用示例:**
```bash
# 按时间范围获取回放地址(HLS 在线播放)
python scripts/cloud_playback_url.py \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36"
# 按视频 ID 精准查询
python scripts/cloud_playback_url.py \
--video-id "xxxxxxxxxx"
# 获取下载链接(MP4 格式)
python scripts/cloud_playback_url.py \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36" \
--format MP4
# 多目设备(多镜头摄像头)
python scripts/cloud_playback_url.py \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36" \
--multi-video
```
**播放方式:**
```bash
# VLC 播放
vlc "https://xxx.com/xxx.m3u8?Expires=..."
# 网页播放(HLS.js)
<video src="https://xxx.com/xxx.m3u8?Expires=..." controls></video>
# 下载 MP4
curl -o video.mp4 "https://xxx.com/xxx.mp4?Expires=..."
# 或
ffmpeg -i "https://xxx.com/xxx.mp4?Expires=..." -c copy video.mp4
```
**注意事项:**
- 回放地址有效期:**24 小时**
- MP4 下载按**文件大小流量计费**
- 多目设备返回多个地址,以分号 `;` 分隔
---
### 2. 本地录像(TF 卡/硬盘)
#### 2.1 获取本地录像回放列表
查询设备本地存储(TF 卡或硬盘)中的录像文件列表。
**前置条件:**
- 设备支持卡存录像(有 TF 卡或硬盘)
- 需配置设备登录凭据(`JF_USERNAME`、`JF_PASSWORD`)
**录像类型说明:**
| 类型 | 说明 |
|------|------|
| `*` | 所有类型的录像 |
| `R` | 常规录像(无报警时的连续录像,含 AOV 录像) |
| `A` | 非视频类报警(如 IO 口报警) |
| `M` | 视频类报警(移动侦测、人形检测等) |
| `H` | 手动录像 |
| `C` | 卡号录像 |
| `V` | AOV 录像(低功耗全时录像) |
| `I` | 入侵报警 |
| `S` | 盗移/滞留报警 |
| `F` | 人脸识别录像 |
| `N` | 车牌识别录像 |
| `K` | 关键录像 |
**使用示例:**
```bash
# 查询所有类型录像
python scripts/local_video_list.py \
--start-time "2026-04-07 10:00:00" \
--stop-time "2026-04-07 18:00:00"
# 只查询报警录像(移动侦测 + 人形检测 + 常规)
python scripts/local_video_list.py \
--start-time "2026-04-07 10:00:00" \
--stop-time "2026-04-07 18:00:00" \
--event "AMRH"
# 只查询常规录像
python scripts/local_video_list.py \
--start-time "2026-04-07 10:00:00" \
--stop-time "2026-04-07 18:00:00" \
--event "R"
```
**返回字段:**
| 字段 | 说明 | 示例 |
|------|------|------|
| `BeginTime` | 录像开始时间 | `2026-04-07 20:00:00` |
| `EndTime` | 录像结束时间 | `2026-04-07 21:00:00` |
| `FileName` | 录像文件路径 | `/idea0/2026-04-07/002/20.00.00-21.00.00[R][@2dcc5][0].h264` |
| `FileLength` | 文件大小(KB) | `123456` |
#### 2.2 获取本地录像回放地址
获取本地录像文件的回放或下载地址。
**支持的协议格式:**
| 协议 | 格式 | 说明 |
|------|------|------|
| `flv` | FLV | 标准 FLV 封装(H.265 采用国内行业 FLV 标准) |
| `flv-enhanced` | FLV-Enhanced | H.265 标准 FLV-Enhanced 封装 |
| `hls-ts` | HLS-TS | HLS 协议,TS 格式切片 |
| `hls-fmp4` | HLS-fMP4 | HLS 协议,fMP4 格式切片 |
| `mp4` | MP4 | HTTP 协议,MP4 格式(用于下载) |
| `rtsp-sdp` | RTSP-SDP | RTSP 标准协议(默认) |
| `rtsp-pri` | RTSP-PRI | RTSP 私有协议 |
**使用示例:**
```bash
# 在线播放(FLV 格式)
python scripts/local_playback_url.py \
--file-name "/idea0/2026-04-07/001/15.23.26-15.23.36[R][@39733][2].h264" \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36" \
--protocol flv
# HLS 播放
python scripts/local_playback_url.py \
--file-name "xxx.h264" \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36" \
--protocol hls-ts
# 录像下载(MP4 格式)
python scripts/local_playback_url.py \
--file-name "xxx.h264" \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36" \
--protocol mp4 \
--download
# 指定码流类型(0=主码流/高清,1=辅码流/标清)
python scripts/local_playback_url.py \
--file-name "xxx.h264" \
--start-time "2026-04-07 15:23:26" \
--stop-time "2026-04-07 15:23:36" \
--stream-type 0
```
**注意事项:**
- 回放地址有效期:**10 小时**
- 同时只支持**一路回放或下载**
- 本地录像回放和下载**按流量计费**
- 必须先获取录像文件列表,使用返回的 `FileName` 字段
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `12504` | 授权失败 - 设备未开通服务 | 登录开放平台为设备绑定对应套餐 |
| `10001` | 参数错误 | 检查请求参数格式 |
| `10002` | 签名失败 | 检查 appKey/appSecret 和时间戳 |
| `200` | 设备响应成功 | - |
| `401` | 设备认证失败 | 检查设备用户名/密码 |
### 常见错误
**1. 云存服务未开通**
```
code: 12504
msg: authorize failed, Please check it in the open platform
```
→ 登录杰峰开放平台,为设备购买并绑定云存套餐卡
**2. 本地录像文件不存在**
```
Ret: 404
msg: File not found
```
→ 检查文件名是否正确,确认设备 TF 卡/硬盘中有录像
**3. 设备登录失败**
```
Ret: 401
msg: Authentication failed
```
→ 检查 `JF_USERNAME` 和 `JF_PASSWORD` 是否正确
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **云存视频列表**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=66142b2ca13c418d84085772a627d650
- **云存回放地址**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
- **本地录像列表**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=4b1516da5763439a9bc7175d7ac7d246
- **本地录像回放**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=4b1516da5763439a9bc7175d7ac7d246
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
**云存录像脚本:**
| 脚本 | 功能 |
|------|------|
| `cloud_video_list.py` | 获取云存视频列表 |
| `cloud_playback_url.py` | 获取云存回放地址 |
**本地录像脚本:**
| 脚本 | 功能 |
|------|------|
| `local_video_list.py` | 获取本地录像回放列表 |
| `local_playback_url.py` | 获取本地录像回放地址 |
```bash
# 获取帮助
python scripts/cloud_video_list.py --help
python scripts/cloud_playback_url.py --help
python scripts/local_video_list.py --help
python scripts/local_playback_url.py --help
```
---
**技能版本:** v1.0.1
**语言:** Python
**最后更新:** 2026-04-08
FILE:skill.yaml
# JF Open Pro Video Record - Skill Registry Metadata
# This file defines the skill's requirements for ClawHub registry
name: jf-open-pro-video-record
version: 1.0.1
description: 杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址,包括录像列表查询、回放地址获取、录像下载等功能。
# Runtime requirements
runtime:
language: python
minVersion: "3.8"
# Required environment variables (credentials)
requiredEnvVars:
- name: JF_UUID
description: 杰峰开放平台用户唯一标识
source: https://open.jftech.com/
- name: JF_APPKEY
description: 杰峰开放平台应用 Key
source: https://open.jftech.com/
- name: JF_APPSECRET
description: 杰峰开放平台应用密钥
source: https://open.jftech.com/
- name: JF_MOVECARD
description: 签名算法偏移量 (0-9),用于时间戳偏移增加签名安全性
source: https://open.jftech.com/
# Optional environment variables
optionalEnvVars:
- name: JF_SN
description: 设备序列号
- name: JF_USERNAME
description: 设备登录用户名
default: admin
- name: JF_PASSWORD
description: 设备登录密码(本地录像必需)
- name: JF_ENDPOINT
description: API 端点
default: api.jftechws.com
# Network endpoints (for firewall/security configuration)
endpoints:
- url: https://api.jftechws.com
description: 杰峰官方 API (国际)
- url: https://api-cn.jftech.com
description: 杰峰官方 API (中国大陆)
# Security declarations
security:
credentialsRequired: true
envVarsOnly: true
networkAccess:
- api.jftechws.com
- api-cn.jftech.com
fileAccess: none
# Entry points
scripts:
- name: cloud_video_list.py
description: 获取云存视频列表
entryPoint: scripts/cloud_video_list.py
- name: cloud_playback_url.py
description: 获取云存回放地址
entryPoint: scripts/cloud_playback_url.py
- name: local_video_list.py
description: 获取本地录像回放列表
entryPoint: scripts/local_video_list.py
- name: local_playback_url.py
description: 获取本地录像回放地址
entryPoint: scripts/local_playback_url.py
# Tags for discovery
tags:
- jf-tech
- 杰峰
- video-record
- playback
- cloud-storage
- local-storage
- 云存回放
- 本地录像
FILE:_meta.json
{
"name": "jf-open-pro-video-record",
"version": "1.0.1",
"description": "杰峰设备录像回放技能。支持获取设备云存储录像和设备本地录像回放地址,包括录像列表查询、回放地址获取、录像下载等功能。",
"requiredEnvVars": [
{
"name": "JF_UUID",
"description": "杰峰开放平台用户唯一标识",
"source": "https://open.jftech.com/"
},
{
"name": "JF_APPKEY",
"description": "杰峰开放平台应用 Key",
"source": "https://open.jftech.com/"
},
{
"name": "JF_APPSECRET",
"description": "杰峰开放平台应用密钥",
"source": "https://open.jftech.com/"
},
{
"name": "JF_MOVECARD",
"description": "签名算法偏移量 (0-9),用于时间戳偏移增加签名安全性",
"source": "https://open.jftech.com/"
}
],
"optionalEnvVars": [
{
"name": "JF_SN",
"description": "设备序列号"
},
{
"name": "JF_USERNAME",
"description": "设备登录用户名",
"default": "admin"
},
{
"name": "JF_PASSWORD",
"description": "设备登录密码(本地录像必需)"
},
{
"name": "JF_ENDPOINT",
"description": "API 端点",
"default": "api.jftechws.com"
}
],
"credentialsRequired": true,
"envVarsOnly": true,
"networkAccess": [
"api.jftechws.com",
"api-cn.jftech.com"
],
"fileAccess": "none",
"language": "python",
"scripts": [
"cloud_video_list.py",
"cloud_playback_url.py",
"local_video_list.py",
"local_playback_url.py"
]
}
FILE:scripts/cloud_playback_url.py
#!/usr/bin/env python3
"""
获取云存视频回放或下载地址脚本
"""
import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp():
"""生成 20 位时间戳(毫秒)"""
return str(int(time.time() * 1000)).zfill(20)
def str2byte(s):
"""字符串转字节数组"""
return list(s.encode('utf-8'))
def change(encrypt_str, move_card):
"""移位算法"""
encrypt_byte = str2byte(encrypt_str)
length = len(encrypt_byte)
for idx in range(length):
tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
return encrypt_byte
def merge_byte(encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for idx in range(length):
temp[idx] = encrypt_byte[idx]
temp[length * 2 - 1 - idx] = change_byte[idx]
return temp
def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
"""生成杰峰 API 签名"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, movecard)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/token"
headers = {
"uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
"signature": signature, "Content-Type": "application/json"
}
body = {"sns": [sn], "accessToken": ""}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def get_cloud_playback_url(device_token, uuid, app_key, app_secret, movecard=5,
start_time=None, stop_time=None, video_id=None,
channel=0, file_format="m3u8", multi_video=False,
endpoint="api.jftechws.com"):
"""获取云存视频回放或下载地址"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoUrl/{device_token}"
headers = {
"uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
"signature": signature, "Content-Type": "application/json"
}
body = {"channel": channel, "fileFormat": file_format}
if video_id:
body["videoId"] = video_id
elif start_time and stop_time:
body["startTime"] = start_time
body["stopTime"] = stop_time
else:
raise ValueError("必须提供 videoId 或 start_time+stop_time")
if multi_video:
body["multiVideo"] = "1"
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'appsecret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(description='获取云存视频回放或下载地址')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--start-time', help='录像开始时间(YYYY-mm-dd HH:MM:SS)')
group.add_argument('--video-id', help='视频 ID(精准查询)')
parser.add_argument('--stop-time', help='录像结束时间')
parser.add_argument('--channel', type=int, default=0, help='设备通道号')
parser.add_argument('--format', choices=['m3u8', 'MP4'], default='m3u8', help='视频格式')
parser.add_argument('--multi-video', action='store_true', help='多目设备')
args = parser.parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print("=" * 70)
print("🎬 获取云存视频回放地址")
print("=" * 70)
print(f"设备 SN: {config['sn']}")
if args.video_id:
print(f"视频 ID: {args.video_id}")
else:
print(f"时间范围:{args.start_time} - {args.stop_time}")
print(f"格式:{args.format}")
print()
print(">>> 获取设备 Token...")
token_result = get_device_token(config['sn'], config['uuid'], config['appkey'],
config['appsecret'], config['movecard'], config['endpoint'])
if token_result.get('error') or token_result.get('code') != 2000:
print(f"❌ 获取设备 Token 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['token']
print(f"✅ 设备 Token 获取成功")
print()
print(">>> 获取回放地址...")
result = get_cloud_playback_url(
device_token=device_token, uuid=config['uuid'], app_key=config['appkey'],
app_secret=config['appsecret'], movecard=config['movecard'],
start_time=args.start_time, stop_time=args.stop_time, video_id=args.video_id,
channel=args.channel, file_format=args.format, multi_video=args.multi_video,
endpoint=config['endpoint']
)
if result.get('error'):
print(f"❌ 获取回放地址失败:{result['error']}")
sys.exit(1)
if result.get('code') != 2000:
print(f"❌ API 错误码:{result.get('code')} - {result.get('msg')}")
sys.exit(1)
play_url = result.get('data', {}).get('url')
if not play_url:
print("❌ 未找到播放 URL")
sys.exit(1)
print("✅ 回放地址获取成功!")
print()
print("=" * 70)
print("🔗 播放地址")
print("=" * 70)
print(f"{play_url}")
print()
print("⚠️ 回放地址有效期 24 小时")
print()
# 仅输出必要信息,避免泄露完整响应数据
print("=" * 70)
print("📋 响应摘要")
print("=" * 70)
print()
print(json.dumps({"code": result.get('code'), "msg": result.get('msg')}, indent=2, ensure_ascii=False))
sys.exit(0)
if __name__ == '__main__':
main()
FILE:scripts/cloud_video_list.py
#!/usr/bin/env python3
"""
获取云存视频列表脚本
支持场景:
1. 条件查询(时间范围)- 根据起始时间和结束时间查询视频列表
2. 组合条件查询(分页 + 报警类型)- 精准查询特定报警类型的视频
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=66142b2ca13c418d84085772a627d650
API 端点:
POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoList/{deviceToken}
用法:
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
# 按时间范围查询
python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00"
# 带报警类型过滤
python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --events "HumanDetect"
"""
import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp():
"""生成 20 位时间戳(毫秒)"""
return str(int(time.time() * 1000)).zfill(20)
def str2byte(s):
"""字符串转字节数组(UTF-8 编码)"""
return list(s.encode('utf-8'))
def change(encrypt_str, move_card):
"""移位算法"""
encrypt_byte = str2byte(encrypt_str)
length = len(encrypt_byte)
for idx in range(length):
tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
return encrypt_byte
def merge_byte(encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for idx in range(length):
temp[idx] = encrypt_byte[idx]
temp[length * 2 - 1 - idx] = change_byte[idx]
return temp
def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
"""生成杰峰 API 签名(复杂加密算法)"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, movecard)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/token"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
# 使用 sns 参数(杰峰 API 要求)
body = {"sns": [sn], "accessToken": ""}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def get_cloud_video_list(device_token, sn, start_time, stop_time, uuid, app_key,
app_secret, movecard=5, channel=0, page_start=1,
page_size=200, events=None, endpoint="api.jftechws.com"):
"""获取云存视频列表"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoList/{device_token}"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"startTime": start_time,
"stopTime": stop_time,
"sn": sn,
"channel": channel,
"pageStart": page_start,
"pageSize": page_size
}
if events:
body["events"] = events if isinstance(events, list) else [events]
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}\n"
f"请设置:export JF_UUID='...' JF_APPKEY='...' JF_APPSECRET='...' JF_MOVECARD=5 JF_SN='...'")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'appsecret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def format_video_list(result):
"""格式化视频列表输出"""
if result.get('error'):
return f"❌ 错误:{result['error']}"
if result.get('code') != 2000:
return f"❌ API 错误码:{result.get('code')}\n 详情:{result.get('msg', 'Unknown error')}"
data = result.get('data', {})
videos = data.get('VideoArray', [])
if not videos:
return "📭 未找到匹配的视频"
output = []
output.append(f"✅ 找到 {len(videos)} 个视频片段")
output.append(f" 总记录数:{data.get('total', 'N/A')}")
output.append(f" 页码:{data.get('pageNum', 'N/A')}")
output.append(f" 是否页尾:{data.get('isFinished', 'N/A')}")
output.append("")
for i, video in enumerate(videos, 1):
output.append(f"📹 视频 {i}:")
output.append(f" 时间:{video.get('StartTime', 'N/A')} - {video.get('StopTime', 'N/A')}")
output.append(f" 文件名:{video.get('IndexFile', 'N/A')}")
output.append(f" 大小:{video.get('VideoSize', 0) / 1024:.1f} KB")
output.append(f" 缩略图:{'有' if video.get('PicFlag') == 1 else '无'}")
if video.get('events'):
output.append(f" 报警类型:{', '.join(video.get('events', []))}")
if video.get('videoId'):
output.append(f" 视频 ID: {video.get('videoId')}")
output.append("")
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(
description='获取云存视频列表',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
报警类型示例:
HumanDetect - 人形检测
MotionDetect - 移动侦测
appEventHumanDetectAlarm - 人形报警
示例:
# 按时间范围查询
python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00"
# 带报警类型过滤
python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --events "HumanDetect"
# 分页查询
python cloud_video_list.py --start-time "2026-04-07 10:00:00" --stop-time "2026-04-07 18:00:00" --page-start 1 --page-size 50
'''
)
parser.add_argument('--start-time', required=True, help='录像查询开始时间(YYYY-mm-dd HH:MM:SS)')
parser.add_argument('--stop-time', required=True, help='录像查询结束时间(YYYY-mm-dd HH:MM:SS)')
parser.add_argument('--channel', type=int, default=0, help='设备通道号(默认 0)')
parser.add_argument('--page-start', type=int, default=1, help='起始页(从 1 开始,默认 1)')
parser.add_argument('--page-size', type=int, default=200, help='分页大小(1-200,默认 200)')
parser.add_argument('--events', nargs='+', help='报警类型列表(可选,用于过滤)')
parser.add_argument('--json', action='store_true', help='输出 JSON 格式')
args = parser.parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print("=" * 70)
print("📋 获取云存视频列表")
print("=" * 70)
print()
print(f"设备 SN: {config['sn']}")
print(f"时间范围:{args.start_time} - {args.stop_time}")
if args.events:
print(f"报警类型:{', '.join(args.events)}")
print()
print(">>> 获取设备 Token...")
token_result = get_device_token(
config['sn'], config['uuid'], config['appkey'],
config['appsecret'], config['movecard'], config['endpoint']
)
if token_result.get('error') or token_result.get('code') != 2000:
print(f"❌ 获取设备 Token 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['token']
print(f"✅ 设备 Token 获取成功")
print()
print(">>> 获取云存视频列表...")
result = get_cloud_video_list(
device_token=device_token,
sn=config['sn'],
start_time=args.start_time,
stop_time=args.stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
channel=args.channel,
page_start=args.page_start,
page_size=args.page_size,
events=args.events,
endpoint=config['endpoint']
)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_video_list(result))
sys.exit(0 if result.get('code') == 2000 else 1)
if __name__ == '__main__':
main()
FILE:scripts/local_playback_url.py
#!/usr/bin/env python3
"""
获取本地录像回放或下载地址脚本
"""
import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp():
"""生成 20 位时间戳(毫秒)"""
return str(int(time.time() * 1000)).zfill(20)
def str2byte(s):
"""字符串转字节数组"""
return list(s.encode('utf-8'))
def change(encrypt_str, move_card):
"""移位算法"""
encrypt_byte = str2byte(encrypt_str)
length = len(encrypt_byte)
for idx in range(length):
tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
return encrypt_byte
def merge_byte(encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for idx in range(length):
temp[idx] = encrypt_byte[idx]
temp[length * 2 - 1 - idx] = change_byte[idx]
return temp
def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
"""生成杰峰 API 签名"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, movecard)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/token"
headers = {
"uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
"signature": signature, "Content-Type": "application/json"
}
body = {"sns": [sn], "accessToken": ""}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except Exception as e:
return {"error": str(e)}
def get_local_playback_url(device_token, uuid, app_key, app_secret, movecard=5,
channel=0, stream_type=1, protocol="hls-ts",
start_time=None, end_time=None, file_name=None,
username="admin", password="", download=False,
endpoint="api.jftechws.com"):
"""获取本地录像回放或下载地址"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/playbackUrl/{device_token}"
headers = {
"uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
"signature": signature, "Content-Type": "application/json"
}
body = {
"channel": channel,
"streamType": stream_type,
"protocol": protocol,
"startTime": start_time,
"endTime": end_time,
"fileName": file_name,
"username": username,
"password": password,
"download": 1 if download else 0
}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except Exception as e:
return {"error": str(e)}
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'appsecret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'username': os.environ.get('JF_USERNAME', 'admin'),
'password': os.environ.get('JF_PASSWORD', ''),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(description='获取本地录像回放或下载地址')
parser.add_argument('--file-name', required=True, help='录像文件名')
parser.add_argument('--start-time', required=True, help='回放开始时间')
parser.add_argument('--stop-time', required=True, help='回放结束时间')
parser.add_argument('--channel', type=int, default=0, help='设备通道号')
parser.add_argument('--stream-type', type=int, choices=[0, 1], default=1, help='码流类型')
parser.add_argument('--protocol', default='hls-ts', choices=['flv', 'flv-enhanced', 'hls-ts', 'hls-fmp4', 'mp4', 'rtsp-sdp', 'rtsp-pri'])
parser.add_argument('--download', action='store_true', help='下载模式')
args = parser.parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print("=" * 70)
print("🎬 获取本地录像回放地址")
print("=" * 70)
print(f"设备 SN: {config['sn']}")
print(f"文件名:{args.file_name}")
print(f"时间:{args.start_time} - {args.stop_time}")
print(f"协议:{args.protocol}")
print()
print(">>> 获取设备 Token...")
token_result = get_device_token(config['sn'], config['uuid'], config['appkey'],
config['appsecret'], config['movecard'], config['endpoint'])
if token_result.get('error') or token_result.get('code') != 2000:
print(f"❌ 获取设备 Token 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['token']
print(f"✅ 设备 Token 获取成功")
print()
print(">>> 获取回放地址...")
result = get_local_playback_url(
device_token=device_token, uuid=config['uuid'], app_key=config['appkey'],
app_secret=config['appsecret'], movecard=config['movecard'],
channel=args.channel, stream_type=args.stream_type, protocol=args.protocol,
start_time=args.start_time, end_time=args.stop_time, file_name=args.file_name,
username=config['username'], password=config['password'], download=args.download,
endpoint=config['endpoint']
)
if result.get('error'):
print(f"❌ 获取回放地址失败:{result['error']}")
sys.exit(1)
if result.get('code') != 2000:
print(f"❌ API 错误码:{result.get('code')} - {result.get('msg')}")
sys.exit(1)
data = result.get('data', {})
if data.get('Ret') != 100:
print(f"❌ 设备错误码:{data.get('Ret')}")
sys.exit(1)
play_url = data.get('url')
if not play_url:
print("❌ 未找到播放 URL")
sys.exit(1)
print("✅ 回放地址获取成功!")
print()
print("=" * 70)
print("🔗 播放地址")
print("=" * 70)
print(f"{play_url}")
print()
print("⚠️ 回放地址有效期 10 小时,同时只支持一路回放")
print()
# 仅输出必要信息,避免泄露完整响应数据
print("=" * 70)
print("📋 响应摘要")
print("=" * 70)
print()
print(json.dumps({"code": result.get('code'), "msg": result.get('msg')}, indent=2, ensure_ascii=False))
sys.exit(0)
if __name__ == '__main__':
main()
FILE:scripts/local_video_list.py
#!/usr/bin/env python3
"""
获取本地录像回放列表脚本
"""
import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp():
"""生成 20 位时间戳(毫秒)"""
return str(int(time.time() * 1000)).zfill(20)
def str2byte(s):
"""字符串转字节数组"""
return list(s.encode('utf-8'))
def change(encrypt_str, move_card):
"""移位算法"""
encrypt_byte = str2byte(encrypt_str)
length = len(encrypt_byte)
for idx in range(length):
tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
return encrypt_byte
def merge_byte(encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for idx in range(length):
temp[idx] = encrypt_byte[idx]
temp[length * 2 - 1 - idx] = change_byte[idx]
return temp
def generate_signature(uuid, app_key, app_secret, time_millis, movecard=5):
"""生成杰峰 API 签名"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, movecard)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=5, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/token"
headers = {
"uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
"signature": signature, "Content-Type": "application/json"
}
body = {"sns": [sn], "accessToken": ""}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except Exception as e:
return {"error": str(e)}
def get_local_video_list(device_token, start_time, stop_time, uuid, app_key,
app_secret, movecard=5, channel=0, event="*",
endpoint="api.jftechws.com"):
"""获取本地录像回放列表"""
time_millis = generate_timestamp()
signature = generate_signature(uuid, app_key, app_secret, time_millis, movecard)
url = f"https://{endpoint}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
"uuid": uuid, "appKey": app_key, "timeMillis": time_millis,
"signature": signature, "Content-Type": "application/json"
}
body = {
"Name": "OPFileQuery",
"OPFileQuery": {
"BeginTime": start_time,
"EndTime": stop_time,
"Channel": channel,
"DriverTypeMask": "0x0000FFFF",
"Event": event,
"StreamType": "0x00000000",
"Type": "h264"
}
}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except Exception as e:
return {"error": str(e)}
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'appsecret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'username': os.environ.get('JF_USERNAME', 'admin'),
'password': os.environ.get('JF_PASSWORD', ''),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def format_video_list(result):
"""格式化本地录像列表输出"""
if result.get('error'):
return f"❌ 错误:{result['error']}"
if result.get('code') != 2000:
return f"❌ API 错误码:{result.get('code')}\n 详情:{result.get('msg', 'Unknown error')}"
data = result.get('data', {})
if data.get('Ret') != 100:
return f"❌ 设备错误码:{data.get('Ret')}"
videos = data.get('OPFileQuery', [])
if not videos:
return "📭 未找到匹配的录像文件"
output = [f"✅ 找到 {len(videos)} 个录像文件", ""]
for i, video in enumerate(videos, 1):
output.append(f"📹 录像 {i}:")
output.append(f" 时间:{video.get('BeginTime', 'N/A')} - {video.get('EndTime', 'N/A')}")
output.append(f" 文件名:{video.get('FileName', 'N/A')}")
file_length = video.get('FileLength', '0')
if isinstance(file_length, str) and file_length.startswith('0x'):
file_length_kb = int(file_length, 16) / 1024
else:
file_length_kb = int(file_length) / 1024 if file_length else 0
output.append(f" 大小:{file_length_kb:.1f} MB")
output.append("")
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(description='获取本地录像回放列表')
parser.add_argument('--start-time', required=True, help='开始时间(YYYY-mm-dd HH:MM:SS)')
parser.add_argument('--stop-time', required=True, help='结束时间')
parser.add_argument('--channel', type=int, default=0, help='设备通道号')
parser.add_argument('--event', default='*', help='录像类型')
parser.add_argument('--json', action='store_true', help='输出 JSON 格式')
args = parser.parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print("=" * 70)
print("📋 获取本地录像回放列表")
print("=" * 70)
print(f"设备 SN: {config['sn']}")
print(f"时间范围:{args.start_time} - {args.stop_time}")
print(f"录像类型:{args.event}")
print()
print(">>> 获取设备 Token...")
token_result = get_device_token(config['sn'], config['uuid'], config['appkey'],
config['appsecret'], config['movecard'], config['endpoint'])
if token_result.get('error') or token_result.get('code') != 2000:
print(f"❌ 获取设备 Token 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['token']
print(f"✅ 设备 Token 获取成功")
print()
print(">>> 获取本地录像列表...")
result = get_local_video_list(
device_token=device_token, start_time=args.start_time, stop_time=args.stop_time,
uuid=config['uuid'], app_key=config['appkey'], app_secret=config['appsecret'],
movecard=config['movecard'], channel=args.channel, event=args.event,
endpoint=config['endpoint']
)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_video_list(result))
sys.exit(0 if result.get('code') == 2000 and result.get('data', {}).get('Ret') == 100 else 1)
if __name__ == '__main__':
main()
FILE:.clawhub/origin.json
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "jf-open-pro-video-record",
"installedVersion": "1.0.1",
"installedAt": 1775635200000
}
AI-powered contract review that identifies risky clauses, missing provisions, and compliance issues in legal documents for informed decision-making.
name: laosi-contract-review-skill
version: 1.0.0
description: AI-powered contract review skill for OpenClaw agents - identifies risky clauses, missing provisions, and compliance issues in legal documents
author: laosi
homepage: https://github.com/laosi/contract-review-skill
tags: [legal, contract, review, compliance, risk-assessment, document-analysis]
FILE:batch_results/batch_review_report_20260427_095105.json
{
"batch_info": {
"total_contracts": 2,
"processed_contracts": 2,
"failed_contracts": 0,
"processing_time_seconds": 0.19,
"timestamp": "2026-04-27T09:51:05.149558",
"industry": "general",
"max_workers": 2
},
"results": [
{
"contract_file": "C:\\Users\\pc\\.laosi\\sample_contract.txt",
"contract_name": "sample_contract.txt",
"risk_score": 40,
"risk_level": "HIGH",
"findings_count": 5,
"high_risk_count": 0,
"medium_risk_count": 0,
"low_risk_count": 0,
"critical_risk_count": 0
},
{
"contract_file": "C:\\Users\\pc\\.laosi\\sample_contract_2.txt",
"contract_name": "sample_contract_2.txt",
"risk_score": 48,
"risk_level": "HIGH",
"findings_count": 6,
"high_risk_count": 0,
"medium_risk_count": 0,
"low_risk_count": 0,
"critical_risk_count": 0
}
],
"summary": {
"total_high_risk": 0,
"total_medium_risk": 0,
"total_low_risk": 0,
"average_risk_score": 44.0,
"risk_distribution": {
"critical": 0,
"high": 0,
"medium": 0,
"low": 0
}
}
}
FILE:batch_results/batch_review_report_20260427_123317.json
{
"batch_info": {
"total_contracts": 1,
"processed_contracts": 1,
"failed_contracts": 0,
"processing_time_seconds": 0.13,
"timestamp": "2026-04-27T12:33:17.569710",
"industry": "general",
"max_workers": 4
},
"results": [
{
"contract_file": "C:\\Users\\pc\\test_contract.txt",
"contract_name": "test_contract.txt",
"risk_score": 40,
"risk_level": "HIGH",
"findings_count": 5,
"high_risk_count": 0,
"medium_risk_count": 3,
"low_risk_count": 2,
"critical_risk_count": 0
}
],
"summary": {
"total_high_risk": 0,
"total_medium_risk": 3,
"total_low_risk": 2,
"average_risk_score": 40.0,
"risk_distribution": {
"critical": 0,
"high": 0,
"medium": 3,
"low": 2
}
}
}
FILE:batch_reviewer.py
#!/usr/bin/env python3
"""
Batch Contract Review Manager - Uses SubAgent system for parallel contract processing
"""
import json
import os
import sys
import time
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
# Import SubAgent system
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from subagent import SubAgentManager, AgentStatus
# Import our contract review engine
sys.path.insert(0, str(Path(__file__).parent))
from review_engine import ContractReviewEngine
class BatchContractReviewManager:
def __init__(self, max_workers: int = 4):
self.manager = SubAgentManager()
self.max_workers = max_workers
self.engine = ContractReviewEngine(Path(__file__).parent / "legal_patterns")
self.results_dir = Path(__file__).parent / "batch_results"
self.results_dir.mkdir(exist_ok=True)
def review_contracts_parallel(self, contract_files: List[str], industry: str = None) -> Dict[str, Any]:
"""
Review multiple contracts in parallel using SubAgent system
"""
start_time = time.time()
# Create SubAgents for each contract
agent_ids = []
for i, contract_file in enumerate(contract_files):
if not os.path.exists(contract_file):
continue
agent_name = f"ContractReviewer_{i+1}"
task = f"Review contract: {os.path.basename(contract_file)}"
context = {
"contract_file": contract_file,
"industry": industry,
"index": i
}
agent_id = self.manager.create_agent(agent_name, task, context)
agent_ids.append(agent_id)
# Process agents in batches (respecting max_workers)
results = {}
completed_agents = set()
while len(completed_agents) < len(agent_ids):
# Check status of running agents
for agent_id in agent_ids:
if agent_id in completed_agents:
continue
agent = self.manager.get_agent(agent_id)
if not agent:
continue
# If agent is not yet started, start it
if agent.status == AgentStatus.PENDING:
self._process_contract_agent(agent_id)
# If agent completed, collect result
if agent.status in [AgentStatus.COMPLETED, AgentStatus.FAILED]:
if agent_id not in completed_agents:
results[agent_id] = {
"agent_id": agent_id,
"name": agent.name,
"task": agent.task,
"status": agent.status.value,
"result": agent.result,
"error": agent.error,
"created_at": agent.created_at,
"completed_at": agent.completed_at
}
completed_agents.add(agent_id)
# Small delay to prevent busy waiting
time.sleep(0.1)
# Timeout check (5 minutes max)
if time.time() - start_time > 300:
break
end_time = time.time()
processing_time = end_time - start_time
# Format final results
batch_results = {
"batch_info": {
"total_contracts": len(contract_files),
"processed_contracts": len([r for r in results.values() if r["status"] == "completed"]),
"failed_contracts": len([r for r in results.values() if r["status"] == "failed"]),
"processing_time_seconds": round(processing_time, 2),
"timestamp": datetime.now().isoformat(),
"industry": industry or "general",
"max_workers": self.max_workers
},
"results": [],
"summary": {
"total_high_risk": 0,
"total_medium_risk": 0,
"total_low_risk": 0,
"average_risk_score": 0,
"risk_distribution": {"critical": 0, "high": 0, "medium": 0, "low": 0}
}
}
# Process results
risk_scores = []
for result in results.values():
if result["status"] == "completed" and result["result"]:
contract_result = result["result"]
batch_results["results"].append({
"contract_file": contract_result.get("metadata", {}).get("contract_file", "unknown"),
"contract_name": os.path.basename(contract_result.get("metadata", {}).get("contract_file", "unknown")),
"risk_score": contract_result.get("risk_score", 0),
"risk_level": self._score_to_level(contract_result.get("risk_score", 0)),
"findings_count": len(contract_result.get("findings", [])),
"high_risk_count": len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "high"]),
"medium_risk_count": len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "medium"]),
"low_risk_count": len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "low"]),
"critical_risk_count": len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "critical"])
})
# Accumulate for summary
risk_scores.append(contract_result.get("risk_score", 0))
batch_results["summary"]["total_high_risk"] += len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "high"])
batch_results["summary"]["total_medium_risk"] += len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "medium"])
batch_results["summary"]["total_low_risk"] += len([f for f in contract_result.get("findings", []) if f.get("risk_level") == "low"])
# Risk distribution
for finding in contract_result.get("findings", []):
risk_level = finding.get("risk_level", "low")
if risk_level == "critical":
batch_results["summary"]["risk_distribution"]["critical"] += 1
elif risk_level == "high":
batch_results["summary"]["risk_distribution"]["high"] += 1
elif risk_level == "medium":
batch_results["summary"]["risk_distribution"]["medium"] += 1
elif risk_level == "low":
batch_results["summary"]["risk_distribution"]["low"] += 1
# Calculate average risk score
if risk_scores:
batch_results["summary"]["average_risk_score"] = round(sum(risk_scores) / len(risk_scores), 2)
return batch_results
def _process_contract_agent(self, agent_id: str):
"""Process a single contract review agent"""
agent = self.manager.get_agent(agent_id)
if not agent:
return
# Update status to running
self.manager.update_status(agent_id, AgentStatus.RUNNING)
try:
# Get context
contract_file = agent.context.get("contract_file")
industry = agent.context.get("industry")
if not contract_file or not os.path.exists(contract_file):
raise FileNotFoundError(f"Contract file not found: {contract_file}")
# Read contract
contract_text = Path(contract_file).read_text(encoding='utf-8')
# Review contract
result = self.engine.review_contract(contract_text, industry)
# Add file info to result
result["metadata"]["contract_file"] = contract_file
result["metadata"]["file_size"] = os.path.getsize(contract_file)
# Update agent with result
self.manager.update_status(agent_id, AgentStatus.COMPLETED, result=result)
except Exception as e:
# Update agent with error
self.manager.update_status(agent_id, AgentStatus.FAILED, error=str(e))
def _score_to_level(self, score: int) -> str:
"""Convert risk score to level label"""
if score >= 80:
return "LOW"
elif score >= 60:
return "MEDIUM"
elif score >= 40:
return "HIGH"
else:
return "CRITICAL"
def save_batch_report(self, batch_results: Dict[str, Any], output_file: str = None) -> str:
"""Save batch review results to file"""
if not output_file:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = self.results_dir / f"batch_review_report_{timestamp}.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(batch_results, f, indent=2, ensure_ascii=False)
return str(output_file)
def print_batch_summary(self, batch_results: Dict[str, Any]):
"""Print human-readable batch summary"""
info = batch_results["batch_info"]
summary = batch_results["summary"]
print("=" * 70)
print("BATCH CONTRACT REVIEW REPORT")
print("=" * 70)
print(f"Total Contracts: {info['total_contracts']}")
print(f"Processed: {info['processed_contracts']}")
print(f"Failed: {info['failed_contracts']}")
print(f"Processing Time: {info['processing_time_seconds']} seconds")
print(f"Timestamp: {info['timestamp']}")
print(f"Industry Focus: {info['industry']}")
print(f"Max Workers: {info['max_workers']}")
print("-" * 70)
print("SUMMARY:")
print(f" Average Risk Score: {summary['average_risk_score']}/100")
print(f" Total High Risk Findings: {summary['total_high_risk']}")
print(f" Total Medium Risk Findings: {summary['total_medium_risk']}")
print(f" Total Low Risk Findings: {summary['total_low_risk']}")
print(f" Risk Distribution:")
print(f" Critical: {summary['risk_distribution']['critical']}")
print(f" High: {summary['risk_distribution']['high']}")
print(f" Medium: {summary['risk_distribution']['medium']}")
print(f" Low: {summary['risk_distribution']['low']}")
print("-" * 70)
print("INDIVIDUAL RESULTS:")
for i, result in enumerate(batch_results["results"], 1):
print(f"{i}. {result['contract_name']}")
print(f" Risk Score: {result['risk_score']}/100 ({result['risk_level']})")
print(f" Findings: {result['findings_count']} "
f"(H:{result['high_risk_count']} M:{result['medium_risk_count']} "
f"L:{result['low_risk_count']} C:{result['critical_risk_count']})")
print()
def main():
if len(sys.argv) < 2:
print("Usage: python batch_reviewer.py <contract_file1> [contract_file2] ... [--industry <industry>] [--workers <n>]")
print("Example: python batch_reviewer.py contract1.txt contract2.txt --industry tech --workers 4")
sys.exit(1)
# Parse arguments
args = sys.argv[1:]
contract_files = []
industry = None
max_workers = 4
i = 0
while i < len(args):
if args[i] == "--industry" and i + 1 < len(args):
industry = args[i + 1]
i += 2
elif args[i] == "--workers" and i + 1 < len(args):
try:
max_workers = int(args[i + 1])
except ValueError:
pass
i += 2
else:
contract_files.append(args[i])
i += 1
if not contract_files:
print("Error: No contract files specified")
sys.exit(1)
# Validate files exist
valid_files = []
for f in contract_files:
if os.path.exists(f):
valid_files.append(f)
else:
print(f"Warning: File not found - {f}")
if not valid_files:
print("Error: No valid contract files found")
sys.exit(1)
# Process batch
manager = BatchContractReviewManager(max_workers=max_workers)
print(f"Starting batch review of {len(valid_files)} contracts with {max_workers} workers...")
results = manager.review_contracts_parallel(valid_files, industry)
# Display results
manager.print_batch_summary(results)
# Save results
output_file = manager.save_batch_report(results)
print(f"Detailed results saved to: {output_file}")
if __name__ == "__main__":
main()
FILE:claw.json
{
"name": "laosi-contract-review-skill",
"displayName": "LAOSI Contract Review Skill",
"description": "AI-powered contract review skill for OpenClaw agents - identifies risky clauses, missing provisions, and compliance issues in legal documents",
"version": "1.0.1",
"author": "laosi",
"homepage": "https://github.com/laosi/contract-review-skill",
"license": "MIT",
"keywords": ["legal", "contract", "review", "compliance", "risk-assessment", "document-analysis"],
"category": "productivity",
"engines": {
"openclaw": ">=1.0.0"
},
"scripts": {
"review": "python contract_reviewer.py"
},
"files": [
"SKILL.md",
"README.md",
"claw.json",
"review_engine.py",
"contract_reviewer.py",
"legal_patterns/",
"templates/"
]
}
FILE:contract_reviewer.py
#!/usr/bin/env python3
"""
Contract Review Skill - CLI Wrapper for OpenClaw Skill Chain
Provides easy-to-use interface for the contract reviewing functionality
"""
import json
import sys
import os
from pathlib import Path
def main():
if len(sys.argv) < 2:
print('{"error": "Usage: contract-review-skill <path_to_contract> [--industry <industry>] [--format <json|text>]"}')
sys.exit(1)
target_path = sys.argv[1]
industry = None
output_format = "text" # default
# Parse optional arguments
i = 2
while i < len(sys.argv):
if sys.argv[i] == "--industry" and i + 1 < len(sys.argv):
industry = sys.argv[i + 1]
i += 2
elif sys.argv[i] == "--format" and i + 1 < len(sys.argv):
output_format = sys.argv[i + 1]
i += 2
else:
i += 1
# Read contract content
try:
contract_text = Path(target_path).read_text(encoding='utf-8')
except Exception as e:
print(json.dumps({"error": f"Failed to read contract file: {str(e)}"}, ensure_ascii=False))
sys.exit(1)
# Import and run the review engine
sys.path.insert(0, str(Path(__file__).parent))
from review_engine import ContractReviewEngine
engine = ContractReviewEngine(Path(__file__).parent / "legal_patterns")
result = engine.review_contract(contract_text, industry)
# Output as requested format
if output_format == "json":
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
# Format as human-readable report
print(format_report(result, Path(target_path).name))
def format_report(report: dict, contract_name: str) -> str:
"""Format review results as human-readable report."""
findings = report.get("findings", [])
risk_score = report.get("risk_score", 0)
summary = report.get("summary", {})
# Determine risk level label (lower score means higher risk)
if risk_score >= 80:
risk_label = "LOW"
elif risk_score >= 60:
risk_label = "MEDIUM"
elif risk_score >= 40:
risk_label = "HIGH"
else:
risk_label = "CRITICAL"
# Count by risk level
by_level = summary.get("by_risk_level", {"critical": 0, "high": 0, "medium": 0, "low": 0})
critical_count = by_level.get("critical", 0)
high_count = by_level.get("high", 0)
medium_count = by_level.get("medium", 0)
low_count = by_level.get("low", 0)
lines = [
"=" * 60,
"CONTRACT REVIEW REPORT",
"=" * 60,
f"Contract: {contract_name}",
f"Overall Risk Score: {risk_score}/100 ({risk_label})",
"Risk Distribution:",
f" - Critical: {critical_count} clauses",
f" - High: {high_count} clauses",
f" - Medium: {medium_count} clauses",
f" - Low: {low_count} clauses",
""
]
if findings:
lines.append("Top Risk Findings:")
# Sort findings by risk level (critical first)
risk_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
sorted_findings = sorted(findings, key=lambda f: risk_order.get(f["risk_level"], 4))
for finding in sorted_findings[:5]: # Show top 5
risk_level = finding['risk_level']
if hasattr(risk_level, 'value'):
risk_level_str = risk_level.value.upper()
else:
risk_level_str = str(risk_level).upper()
lines.append(f" [{risk_level_str}] {finding['title']}")
lines.append(f" Location: {finding['location']}")
lines.append(f" Issue: {finding['description']}")
lines.append(f" Suggestion: {finding['suggestion']}")
if finding.get("reference"):
lines.append(f" Reference: {finding['reference']}")
lines.append("")
if len(findings) > 5:
lines.append(f" ... and {len(findings) - 5} more findings")
lines.append("")
lines.append("Recommendations:")
if critical_count > 0:
lines.append(" 1. [CRITICAL] Address all critical risk items immediately - do not sign without legal review")
if high_count > 0:
lines.append(" 2. [HIGH] Address high risk items before signing or seek legal counsel")
if medium_count > 0:
lines.append(" 3. [MEDIUM] Consider negotiating medium risk clauses based on your leverage position")
if low_count > 0:
lines.append(" 4. [LOW] Minor items - standard business practice")
if critical_count == 0 and high_count == 0 and medium_count == 0:
lines.append(" [LOW] No significant risks identified - contract appears reasonably balanced")
lines.extend([
"",
"=" * 60
])
return "\n".join(lines)
if __name__ == "__main__":
main()
FILE:fix_json.py
import json
import os
data = {
"confidentiality": {
"title": "Missing Confidentiality Clause",
"description": "Contract lacks explicit confidentiality obligations for protecting proprietary information",
"pattern": "(confidential|confidentiality|non-disclosure|nda).*?(obligation|duty|responsibility|requirement)|(proprietary|confidential).*?(information|data|knowledge).*?(shall be|must be|is required to be).*?(kept confidential|maintained in confidence)",
"risk_level": "MEDIUM",
"suggestion": "Add a standard confidentiality clause defining what information is confidential and obligations to protect it",
"reference": "Standard Business Practice and Trade Secret Protection Laws"
},
"intellectual_property": {
"title": "Missing Intellectual Property Clause",
"description": "Contract does not address ownership, licensing, or use of intellectual property",
"pattern": "(intellectual property|ip|copyright|patent|trademark|trade secret|proprietary).*?(ownership|title|interest|rights|license|usage).*?(retains|retain|remains with|belongs to)|(work product|deliverables|materials).*?(created|developed|produced).*?(shall be|is|are).*?(the exclusive property of|owned by)",
"risk_level": "MEDIUM",
"suggestion": "Add IP clause specifying ownership of pre-existing IP, ownership of work product, and license grants",
"reference": "Copyright Law and Patent Act Principles"
},
"payment_terms": {
"title": "Unclear Payment Terms",
"description": "Payment amount, schedule, or method is not clearly defined",
"pattern": "(payment|pay|fee|charge|compensation|remuneration).*?(amount|sum|fee|rate).*?(shall be|is|will be|equals|totals).*?\\$?\\d+(\\.\\d+)?(?:\s*(USD|dollars|RMB|yuan|EUR|euros|GBP|pounds))?|(invoice|bill).*?(submit|send|issue).*?(monthly|quarterly|upon completion|net \\d+)",
"risk_level": "MEDIUM",
"suggestion": "Specify exact payment amounts, schedule (e.g., monthly, milestone-based), method, and late payment penalties",
"reference": "Standard Contract Payment Practices"
},
"term_and_termination": {
"title": "Unclear Term and Termination",
"description": "Contract duration, renewal terms, or termination conditions are not clearly specified",
"pattern": "(term|duration).*?(shall be|is|will be|equals).*?\\d+.*?(day|days|week|weeks|month|months|year|years)|(termination|terminate|end|cancel).*?(for cause|without cause|with notice).*?(notice|notification).*?\\d+.*?(day|days|week|weeks|month|months)",
"risk_level": "MEDIUM",
"suggestion": "Clearly define initial term, renewal options, termination for cause and without cause procedures, and required notice periods",
"reference": "Standard Contract Duration and Termination Practices"
},
"dispute_resolution": {
"title": "Missing Dispute Resolution Clause",
"description": "Contract does not specify how disputes will be resolved (negotiation, mediation, arbitration, litigation)",
"pattern": "(dispute|disagreement|controversy|claim).*?(resolution|settled|resolved).*?(through|via|by means of).*?(negotiation|mediation|arbitration|litigation|court)|(arbitration).*?(shall be|is|will be).*?(final|binding)|(governing law).*?(shall be|is|will be).*?[A-Z][a-z]+",
"risk_level": "MEDIUM",
"suggestion": "Add dispute resolution clause specifying negotiation first, then mediation, then binding arbitration or litigation with specified jurisdiction",
"reference": "Standard Dispute Resolution Practices and Arbitration Law"
},
"force_majeure": {
"title": "Missing Force Majeure Clause",
"description": "Contract lacks provision for unforeseen circumstances preventing performance",
"pattern": "(force majeure|act of god|unforeseeable circumstances|beyond reasonable control).*?(shall excuse|excuses|releases from liability)|(neither party).*?(shall be liable|is responsible).*?(for any failure|for any delay).*?(caused by|due to|resulting from).*?(force majeure|act of god|natural disaster|war|terrorism|government action)",
"risk_level": "LOW",
"suggestion": "Add standard force majeure clause covering natural disasters, war, terrorism, government actions, and other uncontrollable events",
"reference": "Standard Force Majeure Principles and Civil Code Articles"
},
"governing_law": {
"title": "Missing Governing Law Clause",
"description": "Contract does not specify which jurisdiction's laws will govern interpretation",
"pattern": "(governing law|this agreement shall be governed by|interpreted in accordance with).*?the laws of.*?[A-Z][a-z]+.*?(state|province|country)|(this agreement).*?(shall be|is|will be).*?(subject to|governed by).*?the laws.*?of",
"risk_level": "LOW",
"suggestion": "Specify governing law (e.g., laws of the State of California, Peoples Republic of China) and optionally jurisdiction for disputes",
"reference": "Standard Choice of Law Principles"
},
"entire_agreement": {
"title": "Missing Entire Agreement Clause",
"description": "Contract lacks clause stating that the written document constitutes the complete agreement between parties",
"pattern": "(entire agreement|whole agreement|complete agreement).*?(this agreement|this document|this instrument).*?(constitutes|is|represents).*?(the|the full|the complete).*?(agreement|understanding|arrangement).*?(between|among).*?(the parties|party a and party b)|(supersedes|replaces|cancels).*?(all|any).*?(prior|previous|preceding).*?(agreements|understandings|arrangements|representations).*?(whether|whether oral|whether written)",
"risk_level": "LOW",
"suggestion": "Add entire agreement clause to prevent reliance on prior oral or written statements not included in the written contract",
"reference": "Standard Integration Clause Principles and Parol Evidence Rule"
}
}
with open('C:/Users/pc/.laosi/skills/contract-review-skill/legal_patterns/required_clauses.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print("JSON file written successfully.")
FILE:legal_patterns/high_risk.json
{
"unilateral_modification": {
"title": "Unilateral Modification Clause",
"description": "One party can modify contract terms without the other's consent",
"pattern": "(either party|either of the parties|[A-Z][a-z]+ [A-Z][a-z]+).*?(may|can|shall have the right to|is permitted to).*?(amend|modify|alter|change|vary|revise).*?(this agreement|the agreement|these terms).*?(without|without requiring|without obtaining|without needing).*?(consent|approval|agreement).*?(of the other party|from the other party)",
"risk_level": "HIGH",
"suggestion": "Require mutual agreement for any modifications to ensure fairness",
"reference": "Contract Law Principle of Mutuality and Good Faith"
},
"automatic_renewal": {
"title": "Automatic Renewal Without Notice",
"description": "Contract renews automatically without requiring advance notice for non-renewal",
"pattern": "(automatically|automatic).*?(renew|extend|continue).*?(unless|until).*?(notice|notification).*?(given|provided|delivered).*?(\\d+).*?(day|days|month|months).*?(prior|before|in advance)",
"risk_level": "HIGH",
"suggestion": "Add required notice period (typically 30-60 days) for non-renewal",
"reference": "Consumer Protection Laws and Contract Fairness Principles"
},
"excessive_liability": {
"title": "Excessive Liability Exposure",
"description": "Unlimited or excessive liability exposure without reasonable caps",
"pattern": "(liability|responsible|liable).*?(for|to).*?(all|any|every|any and all|direct|indirect|consequential|special|punitive|exemplary).*?(damages|losses|expenses|costs).*?(without limit|unlimited|uncapped|no cap)",
"risk_level": "HIGH",
"suggestion": "Add reasonable liability caps (e.g., fees paid, insurance coverage amounts)",
"reference": "Standard Contract Risk Allocation Practices"
},
"unilateral_termination": {
"title": "Unilateral Termination Rights",
"description": "One party can terminate without cause or with minimal notice",
"pattern": "(either party|either of the parties|[A-Z][a-z]+ [A-Z][a-z]+).*?(may|can|shall have the right to|is permitted to).*?(terminate|end|cancel).*?(this agreement|the agreement).*?(at any time|without cause|without reason).*?(with|upon).*?(\\d+).*?(day|days|week|weeks|month|months).*?(notice|notification)",
"risk_level": "MEDIUM",
"suggestion": "Balance termination rights with reasonable notice periods or mutual agreement requirements",
"reference": "Fair Contract Termination Principles"
},
"broad_indemnification": {
"title": "Overly Broad Indemnification",
"description": "One party required to indemnify for overly broad scope including their own negligence",
"pattern": "(indemnify|indemnification|hold harmless).*?(and|&).*?(defend).*?(against|for).*?(all|any|every).*?(claims|demands|actions|suits|proceedings).*?(arising out of|related to|in connection with).*?(negligence|willful misconduct|breach)",
"risk_level": "HIGH",
"suggestion": "Limit indemnification to third-party claims arising from indemnifying party's gross negligence or willful misconduct",
"reference": "Standard Indemnification Principles and Insurance Requirements"
},
"jurisdiction_shopping": {
"title": "Unfair Jurisdiction and Venue",
"description": "Requires litigation in inconvenient or unfavorable jurisdiction",
"pattern": "(governing law|jurisdiction|venue).*?(shall be|is|will be).*?(the courts of|the state|the province|the country).*?[A-Z][a-z]+.*?(exclusive|sole|only).*?(jurisdiction|venue|forum)",
"risk_level": "MEDIUM",
"suggestion": "Consider neutral jurisdiction or mutual agreement on dispute resolution forum",
"reference": "Forum Non Conveniens and Fair Forum Selection Principles"
}
}
FILE:legal_patterns/required_clauses.json
{
"confidentiality": {
"title": "Missing Confidentiality Clause",
"description": "Contract lacks explicit confidentiality obligations for protecting proprietary information",
"pattern": "(confidential|confidentiality|non-disclosure|nda).*?(obligation|duty|responsibility|requirement)|(proprietary|confidential).*?(information|data|knowledge).*?(shall be|must be|is required to be).*?(kept confidential|maintained in confidence)",
"risk_level": "MEDIUM",
"suggestion": "Add a standard confidentiality clause defining what information is confidential and obligations to protect it",
"reference": "Standard Business Practice and Trade Secret Protection Laws"
},
"intellectual_property": {
"title": "Missing Intellectual Property Clause",
"description": "Contract does not address ownership, licensing, or use of intellectual property",
"pattern": "(intellectual property|ip|copyright|patent|trademark|trade secret|proprietary).*?(ownership|title|interest|rights|license|usage).*?(retains|retain|remains with|belongs to)|(work product|deliverables|materials).*?(created|developed|produced).*?(shall be|is|are).*?(the exclusive property of|owned by)",
"risk_level": "MEDIUM",
"suggestion": "Add IP clause specifying ownership of pre-existing IP, ownership of work product, and license grants",
"reference": "Copyright Law and Patent Act Principles"
},
"payment_terms": {
"title": "Unclear Payment Terms",
"description": "Payment amount, schedule, or method is not clearly defined",
"pattern": "(payment|pay|fee|charge|compensation|remuneration).*?(amount|sum|fee|rate).*?(shall be|is|will be|equals|totals).*?\\$?\\d+(\\.\\d+)?(?:\\s*(USD|dollars|RMB|yuan|EUR|euros|GBP|pounds))?|(invoice|bill).*?(submit|send|issue).*?(monthly|quarterly|upon completion|net \\d+)",
"risk_level": "MEDIUM",
"suggestion": "Specify exact payment amounts, schedule (e.g., monthly, milestone-based), method, and late payment penalties",
"reference": "Standard Contract Payment Practices"
},
"term_and_termination": {
"title": "Unclear Term and Termination",
"description": "Contract duration, renewal terms, or termination conditions are not clearly specified",
"pattern": "(term|duration).*?(shall be|is|will be|equals).*?\\d+.*?(day|days|week|weeks|month|months|year|years)|(termination|terminate|end|cancel).*?(for cause|without cause|with notice).*?(notice|notification).*?\\d+.*?(day|days|week|weeks|month|months)",
"risk_level": "MEDIUM",
"suggestion": "Clearly define initial term, renewal options, termination for cause and without cause procedures, and required notice periods",
"reference": "Standard Contract Duration and Termination Practices"
},
"dispute_resolution": {
"title": "Missing Dispute Resolution Clause",
"description": "Contract does not specify how disputes will be resolved (negotiation, mediation, arbitration, litigation)",
"pattern": "(dispute|disagreement|controversy|claim).*?(resolution|settled|resolved).*?(through|via|by means of).*?(negotiation|mediation|arbitration|litigation|court)|(arbitration).*?(shall be|is|will be).*?(final|binding)|(governing law).*?(shall be|is|will be).*?[A-Z][a-z]+",
"risk_level": "MEDIUM",
"suggestion": "Add dispute resolution clause specifying negotiation first, then mediation, then binding arbitration or litigation with specified jurisdiction",
"reference": "Standard Dispute Resolution Practices and Arbitration Law"
},
"force_majeure": {
"title": "Missing Force Majeure Clause",
"description": "Contract lacks provision for unforeseen circumstances preventing performance",
"pattern": "(force majeure|act of god|unforeseeable circumstances|beyond reasonable control).*?(shall excuse|excuses|releases from liability)|(neither party).*?(shall be liable|is responsible).*?(for any failure|for any delay).*?(caused by|due to|resulting from).*?(force majeure|act of god|natural disaster|war|terrorism|government action)",
"risk_level": "LOW",
"suggestion": "Add standard force majeure clause covering natural disasters, war, terrorism, government actions, and other uncontrollable events",
"reference": "Standard Force Majeure Principles and Civil Code Articles"
},
"governing_law": {
"title": "Missing Governing Law Clause",
"description": "Contract does not specify which jurisdiction's laws will govern interpretation",
"pattern": "(governing law|this agreement shall be governed by|interpreted in accordance with).*?the laws of.*?[A-Z][a-z]+.*?(state|province|country)|(this agreement).*?(shall be|is|will be).*?(subject to|governed by).*?the laws.*?of",
"risk_level": "LOW",
"suggestion": "Specify governing law (e.g., laws of the State of California, Peoples Republic of China) and optionally jurisdiction for disputes",
"reference": "Standard Choice of Law Principles"
},
"entire_agreement": {
"title": "Missing Entire Agreement Clause",
"description": "Contract lacks clause stating that the written document constitutes the complete agreement between parties",
"pattern": "(entire agreement|whole agreement|complete agreement).*?(this agreement|this document|this instrument).*?(constitutes|is|represents).*?(the|the full|the complete).*?(agreement|understanding|arrangement).*?(between|among).*?(the parties|party a and party b)|(supersedes|replaces|cancels).*?(all|any).*?(prior|previous|preceding).*?(agreements|understandings|arrangements|representations).*?(whether|whether oral|whether written)",
"risk_level": "LOW",
"suggestion": "Add entire agreement clause to prevent reliance on prior oral or written statements not included in the written contract",
"reference": "Standard Integration Clause Principles and Parol Evidence Rule"
}
}
FILE:legal_patterns/required_clauses_new.json
{
"confidentiality": {
"title": "Missing Confidentiality Clause",
"description": "Contract lacks explicit confidentiality obligations for protecting proprietary information",
"pattern": "(confidential|confidentiality|non-disclosure|nda).*?(obligation|duty|responsibility|requirement)|(proprietary|confidential).*?(information|data|knowledge).*?(shall be|must be|is required to be).*?(kept confidential|maintained in confidence)",
"risk_level": "MEDIUM",
"suggestion": "Add a standard confidentiality clause defining what information is confidential and obligations to protect it",
"reference": "Standard Business Practice and Trade Secret Protection Laws"
},
"intellectual_property": {
"title": "Missing Intellectual Property Clause",
"description": "Contract does not address ownership, licensing, or use of intellectual property",
"pattern": "(intellectual property|ip|copyright|patent|trademark|trade secret|proprietary).*?(ownership|title|interest|rights|license|usage).*?(retains|retain|remains with|belongs to)|(work product|deliverables|materials).*?(created|developed|produced).*?(shall be|is|are).*?(the exclusive property of|owned by)",
"risk_level": "MEDIUM",
"suggestion": "Add IP clause specifying ownership of pre-existing IP, ownership of work product, and license grants",
"reference": "Copyright Law and Patent Act Principles"
},
"payment_terms": {
"title": "Unclear Payment Terms",
"description": "Payment amount, schedule, or method is not clearly defined",
"pattern": "(payment|pay|fee|charge|compensation|remuneration).*?(amount|sum|fee|rate).*?(shall be|is|will be|equals|totals).*?\\$?\\d+(\\.\\d+)?(?:\s*(USD|dollars|RMB|yuan|EUR|euros|GBP|pounds))?|(invoice|bill).*?(submit|send|issue).*?(monthly|quarterly|upon completion|net \\d+)",
"risk_level": "MEDIUM",
"suggestion": "Specify exact payment amounts, schedule (e.g., monthly, milestone-based), method, and late payment penalties",
"reference": "Standard Contract Payment Practices"
},
"term_and_termination": {
"title": "Unclear Term and Termination",
"description": "Contract duration, renewal terms, or termination conditions are not clearly specified",
"pattern": "(term|duration).*?(shall be|is|will be|equals).*?\\d+.*?(day|days|week|weeks|month|months|year|years)|(termination|terminate|end|cancel).*?(for cause|without cause|with notice).*?(notice|notification).*?\\d+.*?(day|days|week|weeks|month|months)",
"risk_level": "MEDIUM",
"suggestion": "Clearly define initial term, renewal options, termination for cause and without cause procedures, and required notice periods",
"reference": "Standard Contract Duration and Termination Practices"
},
"dispute_resolution": {
"title": "Missing Dispute Resolution Clause",
"description": "Contract does not specify how disputes will be resolved (negotiation, mediation, arbitration, litigation)",
"pattern": "(dispute|disagreement|controversy|claim).*?(resolution|settled|resolved).*?(through|via|by means of).*?(negotiation|mediation|arbitration|litigation|court)|(arbitration).*?(shall be|is|will be).*?(final|binding)|(governing law).*?(shall be|is|will be).*?[A-Z][a-z]+",
"risk_level": "MEDIUM",
"suggestion": "Add dispute resolution clause specifying negotiation first, then mediation, then binding arbitration or litigation with specified jurisdiction",
"reference": "Standard Dispute Resolution Practices and Arbitration Law"
},
"force_majeure": {
"title": "Missing Force Majeure Clause",
"description": "Contract lacks provision for unforeseen circumstances preventing performance",
"pattern": "(force majeure|act of god|unforeseeable circumstances|beyond reasonable control).*?(shall excuse|excuses|releases from liability)|(neither party).*?(shall be liable|is responsible).*?(for any failure|for any delay).*?(caused by|due to|resulting from).*?(force majeure|act of god|natural disaster|war|terrorism|government action)",
"risk_level": "LOW",
"suggestion": "Add standard force majeure clause covering natural disasters, war, terrorism, government actions, and other uncontrollable events",
"reference": "Standard Force Majeure Principles and Civil Code Articles"
},
"governing_law": {
"title": "Missing Governing Law Clause",
"description": "Contract does not specify which jurisdiction's laws will govern interpretation",
"pattern": "(governing law|this agreement shall be governed by|interpreted in accordance with).*?the laws of.*?[A-Z][a-z]+.*?(state|province|country)|(this agreement).*?(shall be|is|will be).*?(subject to|governed by).*?the laws.*?of",
"risk_level": "LOW",
"suggestion": "Specify governing law (e.g., laws of the State of California, Peoples Republic of China) and optionally jurisdiction for disputes",
"reference": "Standard Choice of Law Principles"
},
"entire_agreement": {
"title": "Missing Entire Agreement Clause",
"description": "Contract lacks clause stating that the written document constitutes the complete agreement between parties",
"pattern": "(entire agreement|whole agreement|complete agreement).*?(this agreement|this document|this instrument).*?(constitutes|is|represents).*?(the|the full|the complete).*?(agreement|understanding|arrangement).*?(between|among).*?(the parties|party a and party b)|(supersedes|replaces|cancels).*?(all|any).*?(prior|previous|preceding).*?(agreements|understandings|arrangements|representations).*?(whether|whether oral|whether written)",
"risk_level": "LOW",
"suggestion": "Add entire agreement clause to prevent reliance on prior oral or written statements not included in the written contract",
"reference": "Standard Integration Clause Principles and Parol Evidence Rule"
}
}
FILE:README.md
# Contract Review Skill
An OpenClaw skill for AI-powered contract analysis and review. Identifies risky clauses, missing provisions, and compliance issues in legal documents.
## Features
- 📄 **Contract Analysis**: Parses and analyzes various contract formats (text, markdown, PDF via external tools)
- ⚠️ **Risk Clause Detection**: Identifies high-risk clauses such as unilateral modification, automatic renewal, excessive liability, etc.
- ✅ **Required Clause Checking**: Verifies presence of essential clauses like confidentiality, IP, termination, dispute resolution
- 🏢 **Industry-Specific Rules**: Customizable checks for different industries (tech, healthcare, finance, construction, etc.)
- 📊 **Risk Scoring**: Provides overall contract risk score with detailed breakdown
- 📝 **Remediation Suggestions**: Offers specific language improvements and negotiation points
- 📋 **Report Generation**: Creates structured reports in JSON and human-readable formats
## Installation
```bash
clawhub install contract-review-skill
```
## Usage
### Basic Contract Review
```bash
# Review a contract text file
contract-review-skill ./contract.txt
# Review a markdown contract
contract-review-skill ./agreement.md
```
### With Custom Options
```bash
# Specify industry focus
contract-review-skill ./contract.txt --industry tech
# Output JSON format for integration
contract-review-skill ./contract.txt --format json
```
## Output Example
```
============================================================
CONTRACT REVIEW REPORT
============================================================
Contract: service_agreement.txt
Overall Risk Score: 68/100 (MEDIUM-HIGH)
Risk Distribution:
- High Risk: 3 clauses
- Medium Risk: 5 clauses
- Low Risk: 8 clauses
- Compliant: 12 clauses
Top Risk Findings:
[HIGH] Unilateral Modification Clause
Location: Section 8.2 (Page 4)
Issue: Party A may modify terms without Party B's consent
Suggestion: Require mutual agreement for any modifications
Reference: Contract Law Principle of Mutuality
[HIGH] Automatic Renewal without Notice
Location: Section 12.1 (Page 6)
Issue: Contract renews annually without prior notice requirement
Suggestion: Add 30-60 day notice period for non-renewal
Reference: Consumer Protection Regulations
[MEDIUM] Missing Confidentiality Clause
Location: Not found in contract
Issue: No explicit confidentiality obligations defined
Suggestion: Add standard NDA clause covering proprietary information
Reference: Standard Business Practice
Recommendations:
1. Address all HIGH risk items before signing
2. Consider negotiating MEDIUM risk clauses based on leverage
3. Have legal counsel review final revised version
============================================================
```
## Configuration
The skill can be customized by:
1. **Modifying Pattern Files**: Update JSON files in `legal_patterns/` directory
2. **Adding Industry Templates**: Create new industry-specific rule sets
3. **Adjusting Risk Weights**: Modify scoring algorithm in `review_engine.py`
## Requirements
- Python 3.7+
- No external dependencies for core functionality (uses standard library)
- Optional: `pypdf` or `pdfplumber` for PDF contract support (install via pip if needed)
## Security Notes
- This skill processes contract text locally - no data leaves your machine
- For highly sensitive contracts, consider running in air-gapped environment
- The skill does not provide legal advice - consult qualified attorney for binding decisions
## License
MIT
## Author
laosi (did:soul:laosi)
FILE:review_engine.py
#!/usr/bin/env python3
"""
Contract Review Engine - Core logic for analyzing contracts and identifying risks, missing clauses, and compliance issues.
"""
import json
import os
import re
from pathlib import Path
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, asdict
from enum import Enum
class RiskLevel(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class ReviewFinding:
id: str
title: str
description: str
risk_level: RiskLevel
category: str # e.g., "Risk Clause", "Missing Clause", "Compliance"
location: str # e.g., "Section 5.2", "Line 45"
suggestion: str
reference: Optional[str] = None # e.g., "Civil Code Article 496"
class ContractReviewEngine:
def __init__(self, patterns_dir: Path):
self.patterns_dir = patterns_dir
self.high_risk_patterns = self._load_patterns("high_risk.json")
self.required_clauses = self._load_patterns("required_clauses.json")
self.industry_patterns = {} # Can be extended for industry-specific rules
def _load_patterns(self, filename: str) -> Dict[str, Any]:
"""Load pattern JSON file from patterns directory."""
file_path = self.patterns_dir / filename
if file_path.exists():
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
def review_contract(self, contract_text: str, industry: str = None) -> Dict[str, Any]:
"""
Main review function.
Returns a dictionary with review results.
"""
findings = []
# 1. Check for high-risk clauses
findings.extend(self._check_high_risk_clauses(contract_text))
# 2. Check for missing required clauses
findings.extend(self._check_required_clauses(contract_text))
# 3. Industry-specific checks (if industry specified)
if industry:
findings.extend(self._check_industry_specific(contract_text, industry))
# 4. Calculate overall risk score
risk_score = self._calculate_risk_score(findings)
# 5. Generate summary
summary = self._generate_summary(findings)
findings_list = []
for f in findings:
f_dict = asdict(f)
# Convert Enum to string for JSON serialization
if isinstance(f_dict['risk_level'], RiskLevel):
f_dict['risk_level'] = f_dict['risk_level'].value
findings_list.append(f_dict)
return {
"findings": findings_list,
"risk_score": risk_score,
"summary": summary,
"metadata": {
"total_clauses_checked": len(self.high_risk_patterns) + len(self.required_clauses),
"industry": industry or "general",
"engine_version": "1.0.0"
}
}
def _check_high_risk_clauses(self, text: str) -> List[ReviewFinding]:
"""Scan for high-risk clause patterns."""
findings = []
for pattern_id, pattern_data in self.high_risk_patterns.items():
pattern = pattern_data.get("pattern", "")
if not pattern:
continue
# Simple regex search - in production, might use more sophisticated NLP
matches = list(re.finditer(pattern, text, re.IGNORECASE))
for match in matches:
# Extract context around match
start = max(0, match.start() - 50)
end = min(len(text), match.end() + 50)
context = text[start:end]
# Approximate location (line number)
line_number = text[:match.start()].count('\n') + 1
findings.append(ReviewFinding(
id=f"HR-{pattern_id}-{len(findings)+1:03d}",
title=pattern_data.get("title", "High Risk Clause Detected"),
description=pattern_data.get("description", "Potentially problematic clause detected"),
risk_level=RiskLevel[pattern_data.get("risk_level", "HIGH").upper()],
category="Risk Clause",
location=f"Line {line_number}",
suggestion=pattern_data.get("suggestion", "Review this clause with legal counsel"),
reference=pattern_data.get("reference")
))
return findings
def _check_required_clauses(self, text: str) -> List[ReviewFinding]:
"""Check for presence of required clauses."""
findings = []
for clause_id, clause_data in self.required_clauses.items():
pattern = clause_data.get("pattern", "")
if not pattern:
continue
# Check if pattern exists in text
if not re.search(pattern, text, re.IGNORECASE):
findings.append(ReviewFinding(
id=f"RC-{clause_id}-{len(findings)+1:03d}",
title=clause_data.get("title", "Missing Required Clause"),
description=clause_data.get("description", "Important clause may be missing"),
risk_level=RiskLevel[clause_data.get("risk_level", "MEDIUM").upper()],
category="Missing Clause",
location="Not found in contract",
suggestion=clause_data.get("suggestion", "Consider adding this clause"),
reference=clause_data.get("reference")
))
return findings
def _check_industry_specific(self, text: str, industry: str) -> List[ReviewFinding]:
"""Apply industry-specific rules."""
# This would be extended with industry-specific pattern files
# For now, return empty list as placeholder
return []
def _calculate_risk_score(self, findings: List[ReviewFinding]) -> int:
"""Calculate overall risk score (0-100, lower is better)."""
if not findings:
return 10 # Low risk if no issues found
# Weight by risk level
weights = {
RiskLevel.CRITICAL: 25,
RiskLevel.HIGH: 15,
RiskLevel.MEDIUM: 8,
RiskLevel.LOW: 3
}
total_penalty = sum(weights[f.risk_level] for f in findings)
# Cap the penalty at 90 to keep minimum score at 10
penalty = min(total_penalty, 90)
return 10 + penalty # Score range 10-100
def _generate_summary(self, findings: List[ReviewFinding]) -> Dict[str, Any]:
"""Generate summary statistics."""
counts = {
"critical": len([f for f in findings if f.risk_level == RiskLevel.CRITICAL]),
"high": len([f for f in findings if f.risk_level == RiskLevel.HIGH]),
"medium": len([f for f in findings if f.risk_level == RiskLevel.MEDIUM]),
"low": len([f for f in findings if f.risk_level == RiskLevel.LOW])
}
categories = {}
for f in findings:
cat = f.category
categories[cat] = categories.get(cat, 0) + 1
return {
"by_risk_level": counts,
"by_category": categories,
"total_findings": len(findings)
}
def main():
"""Simple test function."""
# This would be replaced by the CLI wrapper
sample_contract = """
SERVICE AGREEMENT
This Agreement is made between Party A and Party B.
Section 1: Services
Party A shall provide services as described in Exhibit A.
Section 2: Payment
Party B shall pay Party A $1000 per month.
Section 8: Modification
Either party may amend this agreement at any time by providing written notice to the other party.
Section 12: Term and Termination
This agreement shall remain in effect until terminated by either party.
"""
engine = ContractReviewEngine(Path("legal_patterns"))
result = engine.review_contract(sample_contract)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:test_simple.py
import json
import re
from pathlib import Path
# Test loading the JSON
patterns_path = Path("C:/Users/pc/.laosi/skills/contract-review-skill/legal_patterns/required_clauses.json")
print(f"Loading from: {patterns_path}")
print(f"File exists: {patterns_path.exists()}")
try:
with open(patterns_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print("JSON loaded successfully")
print(f"Keys: {list(data.keys())}")
# Test one pattern
if "confidentiality" in data:
pattern = data["confidentiality"]["pattern"]
print(f"Confidentiality pattern: {pattern}")
# Test if it's a valid regex
try:
compiled = re.compile(pattern, re.IGNORECASE)
print("Pattern compiled successfully")
except Exception as e:
print(f"Pattern compilation failed: {e}")
except Exception as e:
print(f"Error loading JSON: {e}")
import traceback
traceback.print_exc()提供混沌理论核心知识,解释非线性敏感初值导致短期可测、长期不可测及混沌与随机的区别。
# 混沌理论指南
> 确定性规律 × 敏感性混沌 × 不可测量子 = 真实世界的运行逻辑
---
## 一、核心定位
本技能整合混沌理论的完整知识体系,解答:
| 问题 | 答案 |
|------|------|
| 为什么长期预测不可能? | 初值敏感性 + 非线性放大 |
| 混沌与随机的区别? | 混沌有规则,随机无结构 |
| 真实世界的运行逻辑? | 确定规律 + 敏感混沌 + 不可测量子 |
---
## 二、混沌理论核心三要素
### 2.1 三大核心规则
```
1. 确定不变 → 规则是严格的科学定律
2. 初值极度敏感 → 微小差异被指数级放大(蝴蝶效应)
3. 乱中自有秩序 → 表观混沌下隐藏稳定底层结构
```
### 2.2 数学表达:logistic map
$$x_{n+1} = r \cdot x_n \cdot (1 - x_n)$$
| 参数 | 含义 |
|------|------|
| $r$ | 增长系数 |
| $x_n$ | 第 $n$ 步状态值 |
| $x_{n+1}$ | 第 $n+1$ 步状态值 |
**混沌区间**:当 $r \in [3.57, 4]$ 时,系统进入混沌状态。
### 2.3 初值敏感实验
```python
# 初始值仅差 0.0000001
x0 = 0.1
y0 = 0.10000001
for n in range(20):
x1 = 3.7 * x0 * (1 - x0)
y1 = 3.7 * y0 * (1 - y0)
print(f"n={n}: x={x1:.15f}, y={y1:.15f}, diff={abs(x1-y1):.2e}")
x0, y0 = x1, y1
# 输出:
# n=0: diff=1.00e-07
# n=5: diff=1.23e-05
# n=10: diff=7.25e-03
# n=15: diff=2.17e-01
# n=20: diff=9.99e-01 ← 完全无关
```
---
## 三、混沌 vs 随机
### 3.1 核心区别
| 维度 | 混沌系统 | 纯粹随机 |
|------|---------|---------|
| **规则** | ✅ 每一步严格遵循科学定律 | ❌ 无规律、无固定结构 |
| **底层结构** | ✅ 隐藏的稳定秩序(如Sierpinski三角形)| ❌ 无线性结构 |
| **可预测性** | ⚠️ 短期可测,长期不可测 | ❌ 完全不可预测 |
| **数学描述** | 非线性微分/差分方程 | 概率分布 |
| **初始值敏感** | ✅ 是 | ❌ 否 |
### 3.2 混沌的隐藏秩序:Sierpinski 三角形
```
混沌游戏中随机点位分布 → 最终汇聚成规整分形
↓
表观混沌 → 隐藏秩序
```
**分形维度**:
$$\dim_H(\text{Sierpinski}) = \dfrac{\log 3}{\log 2} \approx 1.585$$
---
## 四、两类系统对比
| 系统类型 | 特征 | 预测能力 | 例子 |
|---------|------|---------|------|
| **线性系统** | 变量单一、干扰微弱 | ✅ 长期精准 | 行星轨道、简谐运动 |
| **非线性系统** | 多变量、非线性反馈 | ❌ 仅短期可测 | 天气、人生、股市 |
### 4.1 线性 vs 非线性
```
线性系统:
y = kx → 可加性 → 可预测性强
非线性系统:
y = kx(1-x) → 反馈项 → 初值敏感 → 混沌
```
### 4.2 真实世界的双重约束
```
┌────────────────────────────────────┐
│ 复杂非线性系统受到: │
│ │
│ ① 混沌放大效应 │
│ → 初值误差指数级放大 │
│ │
│ ② 量子不确定性 │
│ → 微观层面的物理极限 │
│ │
│ → 永远无法长期精准锁定 │
└────────────────────────────────────┘
```
---
## 五、现实系统应用
### 5.1 自然界混沌系统
| 系统 | 混沌机制 | 应用 |
|------|---------|------|
| **鸟群飞行** | 局部规则 → 群体涌现行为 | 无人机编队 |
| **森林生态** | 物种相互制约 → 种群涨落 | 生态保护模型 |
| **气候系统** | 大气非线性反馈 → 天气混沌 | 气象预报(10天极限)|
| **湍流** | Navier-Stokes 非线性 → 混沌流动 | 航空设计 |
### 5.2 社会混沌系统
| 系统 | 混沌机制 | 应用 |
|------|---------|------|
| **人际关系** | 情感反馈非线性 → 关系演化 | 社交网络分析 |
| **事业发展** | 机遇/能力非线性叠加 → 职业轨迹 | 职业规划模型 |
| **股市** | 多因素反馈 → 价格混沌 | 量化投资风险管理 |
| **城市发展** | 交通/经济/人口非线性耦合 | 城市规划 |
### 5.3 人生作为混沌系统
```
微小变量(读一本书、认识一个人、做一个小决定)
↓
时间叠加 + 反馈循环
↓
彻底改写人生轨迹
```
**结论**:无法预测人生,但可以"塑造概率"。
---
## 六、三大世界运行逻辑
```
┌────────────────────────────────────────────────┐
│ 真实世界运行逻辑 │
├────────────────────────────────────────────────┤
│ │
│ 确定规律 ───→ 物理定律、化学定律、数学规则 │
│ ↕ │
│ 敏感混沌 ───→ 非线性系统、初值放大、蝴蝶效应 │
│ ↕ │
│ 不可测量子 ─→ 海森堡不确定性原理、测量极限 │
│ │
└────────────────────────────────────────────────┘
```
| 世界类型 | 代表 | 可预测性 |
|---------|------|---------|
| **确定规律** | 牛顿力学、行星轨道 | 长期精准 |
| **敏感混沌** | 天气、股市、人生 | 仅短期可测 |
| **不可测量子** | 电子位置、量子态 | 本质不可测 |
---
## 七、数学工具箱
### 7.1 混沌判定工具
| 工具 | 用途 |
|------|------|
| **李雅普诺夫指数** $\lambda$ | $\lambda > 0$ → 混沌 |
| **分形维度** $D$ | 混沌吸引子维度量化 |
| **庞加莱截面** | 可视化相空间结构 |
| **Feigenbaum常数** $\delta$ | $\delta \approx 4.669$ 普适常数 |
### 7.2 关键常数
| 常数 | 值 | 意义 |
|------|------|------|
| **Feigenbaum $\delta$** | $4.6692...$ | 从周期到混沌的普适比率 |
| **Lyapunov $\lambda$** | $>0$ 混沌 | 指数级发散率 |
### 7.3 经典混沌系统
| 系统 | 方程 | 特征 |
|------|------|------|
| **Logistic Map** | $x_{n+1} = rx_n(1-x_n)$ | 通往混沌的经典模型 |
| **Lorenz系统** | $\dot{x} = \sigma(y-x), \dot{y} = rx - y - xz, \dot{z} = xy - bz$ | 混沌吸引子 |
| **Rossler系统** | 非线性化学动力学 | 混沌化学振荡 |
| **Chua电路** | 电子电路混沌 | 工程应用最广 |
---
## 八、与已有技能的关联
| 本技能 | 关联技能 | 关系 |
|--------|---------|------|
| 混沌理论 | `math-theory-notes` | 拓扑流形、泛函分析工具 |
| 混沌理论 | `thinking-knowledge-system` | 思考四层次(分析复杂系统)|
| 混沌理论 | `knowledge-system-guide` | 知识体系构建(非线性知识网络)|
| 混沌理论 | `mckinsey-frameworks` | 战略规划(复杂系统管理)|
| 混沌理论 | `investor-reading-list` | 投资不确定性(塔勒布随机性)|
---
## 九、使用方式
### 触发场景
```
用户说「什么是混沌理论」→ 展示核心三要素
用户说「混沌和随机有什么区别」→ 混沌 vs 随机对比
用户说「为什么天气不能长期预测」→ 双重约束(混沌 + 量子)
用户说「人生是混沌系统吗」→ 人生混沌分析
用户说「有哪些混沌系统的例子」→ 自然/社会混沌系统
用户说「混沌理论有什么用」→ 数学工具 + 投资/管理应用
```
### 组合使用
```
用户:「我想用混沌理论理解股市」
→ 混沌系统特征 → 短期可预测/长期不可测
→ Lyapunov指数 → 风险量化
→ 杠杆铃策略(塔勒布)→ 风险管理
→ 建议:不做长期预测,做概率管理
```
---
## 十、深度思考问题
| 问题 | 思考方向 |
|------|---------|
| 为什么神经网络训练也是混沌系统? | 梯度下降的非线性反馈 |
| 为什么长期经济预测注定失败? | 经济系统的混沌特性 |
| 如何在混沌世界中做出好的决策? | 概率塑造 vs 精准预测 |
| 混沌与复杂性理论的关系? | 同为非线性科学分支 |
---
*本技能整合混沌理论核心概念、现实应用与哲学意义*
Speak responses aloud on macOS using the built-in `say` command when user input indicates Voice Wake/voice recognition (for example, messages starting with "...
---
name: voice-wake-say
description: Speak responses aloud on macOS using the built-in `say` command when user input indicates Voice Wake/voice recognition (for example, messages starting with "User talked via voice recognition on <device>").
---
# Voice Wake Say
## Overview
Use macOS `say` to read the assistant's response out loud whenever the conversation came from Voice Wake/voice recognition. Do **not** use the `tts` tool (it calls cloud providers).
## When to Use `say` (CHECK EVERY MESSAGE INDIVIDUALLY)
**IF** the user message STARTS WITH: `User talked via voice recognition`
- **Step 1:** Acknowledge with `say` first (so the user knows you heard them)
- **Step 2:** Then perform the task
- **Step 3:** Optionally speak again when done if it makes sense
**IF** the user message does NOT start with that exact phrase
- THEN: Do NOT use `say`. Text-only response only.
**Critical:**
- Check EACH message individually — context does NOT carry over
- The trigger phrase must be at the VERY START of the message
- For tasks that take time, acknowledge FIRST so the user knows you're working
## Workflow
1) Detect Voice Wake context
- Trigger ONLY when the latest user/system message STARTS WITH `User talked via voice recognition`
- If the message instructs "repeat prompt first", keep that behavior in the response.
2) Prepare spoken text
- Use the final response text as the basis.
- Strip markdown/code blocks; if the response is long or code-heavy, speak a short summary and mention that details are on screen.
3) Speak with `say` (local macOS TTS)
```bash
printf '%s' "$SPOKEN_TEXT" | say
```
Optional controls (use only if set):
```bash
printf '%s' "$SPOKEN_TEXT" | say -v "$SAY_VOICE"
printf '%s' "$SPOKEN_TEXT" | say -r "$SAY_RATE"
```
## Failure handling
- If `say` is unavailable or errors, still send the text response and note that TTS failed.
FILE:_meta.json
{
"ownerId": "kn77k3j7wa1fedgd8g1fyz6ydh7z28mv",
"slug": "lovefromio-voice-wake-say",
"version": "1.0.1",
"publishedAt": 1769336291994
}Use when the user asks about UnifiedQuantum, uniqc, OriginIR, OpenQASM, circuit building, local simulation, cloud submission, dummy mode, VQE, QAOA, UCCSD, q...
---
name: quantum-computing
description: "Use when the user asks about UnifiedQuantum, uniqc, OriginIR, OpenQASM, circuit building, local simulation, cloud submission, dummy mode, VQE, QAOA, UCCSD, quantum ML, or PyTorch integration with UnifiedQuantum. Focus on the current public workflow: build circuits, export IR/QASM, run with uniqc CLI or task_manager, and add extras only when needed."
version: 1.0.0
---
# UnifiedQuantum Skill
当用户在使用当前 UnifiedQuantum 的公开 API、CLI 或示例时,使用这个 skill。
## 默认处理思路
优先采用当前的高层工作流:
1. 用 `uniqc.circuit_builder.Circuit` 构建线路
2. 导出 `circuit.originir` 或 `circuit.qasm`
3. 用 `uniqc` CLI 或 `uniqc.task_manager` 执行
4. 只为确实需要的功能安装额外 extras
当用户给的是一段 QASM 线路时,更稳妥的处理路径通常是:
1. 先用 `uniqc circuit` 转成 OriginIR
2. 再对这份归一化后的 OriginIR 做模拟或提交
这样可以减少不同输入路径带来的行为差异。
## 环境与安装处理
- 不要默认用户已经先决定好如何安装 `unified-quantum`,也不要默认用户已经选好了 CLI 入口。
- 先检查用户环境里已经有什么:解释器、已安装包、模块路径、CLI 是否可用,以及相关 extras 是否存在。
- 需要安装时,不要直接替用户决定安装路径。先给出简短可选项,让用户选择,再执行。
- 决定安装方式前,先识别用户当前是在 `venv`、Conda、Pixi 还是系统 Python 中工作。
- 默认把安装选项整理成 2 到 3 个最相关方案,并说明各自取舍;推荐项放第一,但不要静默直接执行。
- 常见可选项:
- `uv tool install`:适合偏 CLI 的隔离安装,命令可跨目录直接使用。
- `uv venv` 或普通 `venv`:适合项目内或 Python API 场景,依赖与仓库隔离。
- 当前已激活环境里的 `pip install`:适合用户明确希望复用现有 `venv`、Conda 或 Pixi 环境。
- 如果用户已经在 Pixi 生态里工作,`pixi global` 也是可接受的选项,但通常只在用户明确偏好 Pixi 时再列出。
- 如果当前环境是已激活的 Conda 环境,默认不要直接改它。把“装进当前 Conda 环境”作为一个可选项交给用户决定。
- 如果唯一可写目标是系统 Python,安装前必须明确征求用户同意。
- 如果用户没有明确偏好,再给出推荐:
- 偏 CLI:优先推荐 `uv tool install`
- 偏 Python API / 示例:优先推荐 `uv venv`
- 已在现有虚拟环境里工作:可推荐装进当前环境
- 如果 `uv` 不可用或不合适,再提供 `pip` / `venv` 方案作为替代。
- 只有在用户已经明确要求某种安装路径,或者当前会话上下文已经表达了稳定偏好时,才可以不再重复询问。
## 几个容易混淆但要记清的点
- 包名:`unified-quantum`
- CLI 名:`uniqc`
- 主 Python 包名:`uniqc`
- 配置文件:`~/.uniqc/uniqc.yml`
- 本地任务缓存:`~/.uniqc/cache/tasks.sqlite`
- 如果主题 reference 仍然解释不了问题,再回到 [references/troubleshooting.md](references/troubleshooting.md) 做通用诊断。
## 依赖边界
不要默认基础安装就包含所有功能。
- 核心包:`pip install unified-quantum`
- 本地模拟 / dummy 模式常见需要:`pip install "unified-quantum[simulation]"`
- OriginQ 适配器:`pip install "unified-quantum[originq]"`
- Quafu 适配器:`pip install "unified-quantum[quafu]"`
- IBM 适配器:`pip install "unified-quantum[qiskit]"`
- PyTorch 辅助工具:`pip install "unified-quantum[pytorch]"`
- TorchQuantum 集成:`pip install "unified-quantum[torchquantum]"`
如果用户提到 `qutip`、`torch`、`qiskit`、`quafu` 或 `pyqpanda3` 相关导入失败,先把它当成缺少可选依赖,而不是先判断核心包坏了。
## CLI 指引
当前 CLI 主要分组有:
- `uniqc circuit`
- `uniqc simulate`
- `uniqc submit`
- `uniqc result`
- `uniqc task`
- `uniqc config`
当用户要在 shell 里做格式转换、本地执行或云任务管理时,优先用 `uniqc`,不要先写临时辅助脚本。
当前有几个细节要特别注意:
- `uniqc submit` 使用 `--platform`,并可选搭配 `--backend`
- 对 OriginQ,当前 CLI 选项是 `--backend`,不是旧的 `--chip-id`
- 对 Quafu,`chip_id` 在 Python API 中仍然相关,但当前 CLI 没有单独暴露 `--chip-id`
- `simulate` 最稳妥的输入仍是 OriginIR;如果手里是 QASM,先做归一化
## Python API 指引
如果是编程式的任务工作流,优先使用:
```python
from uniqc import submit_task, submit_batch, query_task, wait_for_result
```
构造 ansatz 时,优先使用当前公开导出:
```python
from uniqc.algorithmics.ansatz import hea, qaoa_ansatz, uccsd_ansatz
```
不要再使用像 `uccsd` 这样的旧名字。
PyTorch 集成优先使用:
```python
from uniqc.pytorch import (
QuantumLayer,
batch_execute,
batch_execute_with_params,
parameter_shift_gradient,
compute_all_gradients,
)
```
## 接下来读什么
- 线路构建与导出:[references/circuit-building.md](references/circuit-building.md)
- CLI 用法:[references/cli-guide.md](references/cli-guide.md)
- 本地模拟与 dummy 模式:[references/simulators.md](references/simulators.md)
- 配置、云端后端与任务缓存:[references/cloud-platforms.md](references/cloud-platforms.md)
- ansatz 与变分工作流:[references/variational-algorithms.md](references/variational-algorithms.md)
- PyTorch 辅助接口:[references/pytorch-integration.md](references/pytorch-integration.md)
- H2 风格的 VQE 任务:[references/h2-molecular-simulation.md](references/h2-molecular-simulation.md)
- 主题检查之后仍然不清楚的通用排障:[references/troubleshooting.md](references/troubleshooting.md)
## 回答启发式
- 如果用户想要一个快速起步,先从 `Circuit -> originir -> uniqc` 开始。
- 如果用户卡在云端执行,先检查配置和后端特有 kwargs;如果仍然不清楚,再回到 [references/troubleshooting.md](references/troubleshooting.md)。
- 如果用户问的是本地模拟失败,先检查 `simulation` 相关依赖;必要时再回到 [references/troubleshooting.md](references/troubleshooting.md)。
- 如果用户提到缺命令、缺导入、缺 extra、缺配置路径,或者文档与本地行为不一致,先拍安装快照,再判断是不是版本漂移导致,然后查同类 issue,最后再进入 [references/troubleshooting.md](references/troubleshooting.md) 的完整通用排障流程。
- 如果需要安装,先用简短选项问清用户想装到哪里,再执行;不要直接替用户选安装路径。
- 如果用户想看现代的变分示例,优先从 `hea`、`qaoa_ansatz` 或 `uccsd_ansatz` 开始,不要从旧 helper 名称起步。
- 如果用户想走 shell 工作流,优先给 `uniqc` CLI 方案,而不是自定义 wrapper。
FILE:examples/basic_circuit.py
#!/usr/bin/env python3
"""Basic UnifiedQuantum circuit example.
Builds a Bell circuit, prints both export formats, and tries a local
probability simulation when simulation dependencies are available.
"""
from __future__ import annotations
from pprint import pprint
from uniqc.circuit_builder import Circuit
def build_bell_circuit() -> Circuit:
circuit = Circuit(2)
circuit.h(0)
circuit.cnot(0, 1)
circuit.measure(0, 1)
return circuit
def try_local_simulation(circuit: Circuit) -> None:
try:
from uniqc.task.optional_deps import check_simulation
except ImportError:
check_simulation = lambda: False
if not check_simulation():
print("Local simulation skipped: install unified-quantum[simulation] first.")
return
from uniqc.simulator import OriginIR_Simulator
simulator = OriginIR_Simulator(backend_type="statevector")
probabilities = simulator.simulate_pmeasure(circuit.originir)
print("\nState probabilities:")
pprint(
{
format(index, f"0{simulator.qubit_num}b"): float(probability)
for index, probability in enumerate(probabilities)
if float(probability) > 1e-10
}
)
def main() -> None:
circuit = build_bell_circuit()
print("OriginIR")
print("-" * 60)
print(circuit.originir)
print("\nOpenQASM 2.0")
print("-" * 60)
print(circuit.qasm)
try_local_simulation(circuit)
if __name__ == "__main__":
main()
FILE:examples/cli_demo.sh
#!/usr/bin/env bash
# Demo of the current uniqc CLI workflow.
set -euo pipefail
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
run_uniqc() {
if command -v uniqc >/dev/null 2>&1; then
uniqc "$@"
else
python3 -m uniqc "$@"
fi
}
cat >"$TMP_DIR/build_bell.py" <<'PY'
from uniqc.circuit_builder import Circuit
c = Circuit(2)
c.h(0)
c.cnot(0, 1)
c.measure(0, 1)
with open("bell.ir", "w", encoding="utf-8") as f:
f.write(c.originir)
PY
echo "[1/4] Build a Bell circuit as OriginIR"
(cd "$TMP_DIR" && python3 build_bell.py)
echo
echo "[2/4] Show circuit info"
run_uniqc circuit "$TMP_DIR/bell.ir" --info
echo
echo "[3/4] Convert to OpenQASM 2.0"
run_uniqc circuit "$TMP_DIR/bell.ir" --format qasm -o "$TMP_DIR/bell.qasm"
sed -n '1,40p' "$TMP_DIR/bell.qasm"
echo
echo "[4/4] Try local simulation and dummy submission"
echo "These steps usually require unified-quantum[simulation]."
if run_uniqc simulate "$TMP_DIR/bell.ir" --shots 1024 --format json; then
echo
run_uniqc submit "$TMP_DIR/bell.ir" --platform dummy --wait --format json
else
echo "Simulation failed. Install unified-quantum[simulation] and retry."
fi
FILE:examples/cloud_submission.py
#!/usr/bin/env python3
"""Programmatic cloud-task examples for UnifiedQuantum.
By default this script runs a dummy submission so it is safe for local
testing. Real backend examples are included as callable functions but are
not executed automatically.
"""
from __future__ import annotations
import argparse
from pprint import pprint
from uniqc import Circuit, query_task, submit_task, wait_for_result
def build_bell_circuit() -> Circuit:
circuit = Circuit(2)
circuit.h(0)
circuit.cnot(0, 1)
circuit.measure(0, 1)
return circuit
def print_result(result: dict | None) -> None:
if not result:
print("No result returned.")
return
print("Normalized result payload:")
pprint(result)
if "counts" in result:
print("\nCounts:")
pprint(result["counts"])
if "probabilities" in result:
print("\nProbabilities:")
pprint(result["probabilities"])
def run_dummy_demo(shots: int) -> None:
try:
from uniqc.task.optional_deps import check_simulation
except ImportError:
check_simulation = lambda: False
if not check_simulation():
raise RuntimeError(
"Dummy mode uses the local simulator. Install unified-quantum[simulation] first."
)
circuit = build_bell_circuit()
task_id = submit_task(
circuit,
backend="originq",
shots=shots,
dummy=True,
metadata={"example": "cloud_submission.py"},
)
print(f"Dummy task submitted: {task_id}")
task_info = query_task(task_id)
print(f"Cached task status: {task_info.status}")
print(f"Cached backend tag: {task_info.backend}")
result = wait_for_result(task_id, timeout=60)
print_result(result)
def real_originq_example(shots: int) -> str:
"""Skeleton for a real OriginQ submission.
Requires:
1. pip install "unified-quantum[originq]"
2. uniqc config set originq.token YOUR_TOKEN
"""
circuit = build_bell_circuit()
return submit_task(
circuit,
backend="originq",
shots=shots,
backend_name="origin:wuyuan:d5",
metadata={"example": "real-originq"},
)
def real_quafu_example(shots: int) -> str:
"""Skeleton for a real Quafu submission.
Requires:
1. pip install "unified-quantum[quafu]"
2. uniqc config set quafu.token YOUR_TOKEN
"""
circuit = build_bell_circuit()
return submit_task(
circuit,
backend="quafu",
shots=shots,
chip_id="ScQ-P10",
metadata={"example": "real-quafu"},
)
def real_ibm_example(shots: int) -> str:
"""Skeleton for a real IBM submission.
Requires:
1. pip install "unified-quantum[qiskit]"
2. uniqc config set ibm.token YOUR_TOKEN
"""
circuit = build_bell_circuit()
return submit_task(
circuit,
backend="ibm",
shots=shots,
metadata={"example": "real-ibm"},
)
def main() -> None:
parser = argparse.ArgumentParser(description="UnifiedQuantum cloud submission demo")
parser.add_argument("--shots", type=int, default=1000)
args = parser.parse_args()
run_dummy_demo(args.shots)
print("\nReal backend entry points are available as helper functions:")
print(" - real_originq_example(shots)")
print(" - real_quafu_example(shots)")
print(" - real_ibm_example(shots)")
if __name__ == "__main__":
main()
FILE:examples/h2_hea_vqe.py
#!/usr/bin/env python3
"""Minimal H2-style VQE workflow with HEA.
This example uses a reduced two-qubit Hamiltonian to demonstrate the
current UnifiedQuantum variational workflow:
1. build an ansatz with ``hea()``
2. simulate probabilities locally
3. evaluate expectation values with ``calculate_expectation()``
4. optimize with SciPy
It is intentionally lightweight and should be read as a workflow template,
not as a complete quantum-chemistry stack.
"""
from __future__ import annotations
import argparse
from typing import Iterable
import numpy as np
from scipy.optimize import minimize
from uniqc.algorithmics.ansatz import hea
from uniqc.analyzer import calculate_expectation
REDUCED_H2_TERMS: list[tuple[float, str]] = [
(-1.052373245772859, "II"),
(0.39793742484318045, "ZI"),
(-0.39793742484318045, "IZ"),
(-0.01128010425623538, "ZZ"),
]
def probability_dict(probabilities: Iterable[float], n_qubits: int) -> dict[str, float]:
return {
format(index, f"0{n_qubits}b"): float(value)
for index, value in enumerate(probabilities)
if float(value) > 1e-12
}
def build_energy_function(depth: int):
try:
from uniqc.task.optional_deps import check_simulation
except ImportError:
check_simulation = lambda: False
if not check_simulation():
raise RuntimeError(
"This example needs local simulation. Install unified-quantum[simulation] first."
)
from uniqc.simulator import OriginIR_Simulator
simulator = OriginIR_Simulator(backend_type="statevector")
n_qubits = 2
def energy(params: np.ndarray) -> float:
circuit = hea(n_qubits=n_qubits, depth=depth, params=params)
probabilities = simulator.simulate_pmeasure(circuit.originir)
probs = probability_dict(probabilities, n_qubits)
total = 0.0
for coefficient, pauli in REDUCED_H2_TERMS:
if pauli == "II":
total += coefficient
else:
total += coefficient * calculate_expectation(probs, pauli)
return float(total)
return energy
def main() -> None:
parser = argparse.ArgumentParser(description="Reduced H2-style HEA VQE demo")
parser.add_argument("--depth", type=int, default=1)
parser.add_argument("--maxiter", type=int, default=100)
parser.add_argument("--seed", type=int, default=7)
args = parser.parse_args()
np.random.seed(args.seed)
energy = build_energy_function(args.depth)
n_params = 2 * 2 * args.depth
x0 = np.random.uniform(0.0, 2 * np.pi, size=n_params)
result = minimize(
energy,
x0=x0,
method="COBYLA",
options={"maxiter": args.maxiter},
)
print("Reduced H2-style VQE")
print("-" * 60)
print(f"Depth: {args.depth}")
print(f"Initial energy: {energy(x0):.8f}")
print(f"Optimized energy: {result.fun:.8f}")
print(f"Iterations: {result.nfev}")
print(f"Best parameters: {result.x}")
if __name__ == "__main__":
main()
FILE:examples/mnist_classifier.py
#!/usr/bin/env python3
"""Lightweight digit-classification demo using UnifiedQuantum.
This file keeps the historical ``mnist_classifier.py`` name, but the
current example is intentionally smaller and easier to run:
- loads the scikit-learn digits dataset
- keeps only digits 0 and 1
- angle-encodes 4 PCA features into a 4-qubit circuit
- extracts simple quantum features in parallel with ``batch_execute``
- trains a classical logistic regression head
Requirements:
pip install "unified-quantum[simulation]" scikit-learn
"""
from __future__ import annotations
import argparse
import numpy as np
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from uniqc.analyzer import calculate_expectation
from uniqc.circuit_builder import Circuit
from uniqc.pytorch import batch_execute
def require_simulation() -> None:
try:
from uniqc.task.optional_deps import check_simulation
except ImportError:
check_simulation = lambda: False
if not check_simulation():
raise RuntimeError(
"This example needs local simulation. Install unified-quantum[simulation] first."
)
def load_binary_digits(max_samples: int) -> tuple[np.ndarray, np.ndarray]:
dataset = load_digits()
mask = np.isin(dataset.target, [0, 1])
data = dataset.data[mask]
target = dataset.target[mask]
if max_samples > 0:
data = data[:max_samples]
target = target[:max_samples]
scaler = StandardScaler()
data = scaler.fit_transform(data)
pca = PCA(n_components=4)
reduced = pca.fit_transform(data)
min_value = reduced.min()
max_value = reduced.max()
encoded = (reduced - min_value) / (max_value - min_value + 1e-12) * np.pi
return encoded, target
def build_encoding_circuit(sample: np.ndarray) -> Circuit:
circuit = Circuit(4)
for qubit, angle in enumerate(sample[:4]):
circuit.ry(qubit, float(angle))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.measure(0, 1, 2, 3)
return circuit
def extract_quantum_features(samples: np.ndarray) -> np.ndarray:
from uniqc.simulator import OriginIR_Simulator
simulator = OriginIR_Simulator(backend_type="statevector")
circuits = [build_encoding_circuit(sample) for sample in samples]
def executor(circuit: Circuit) -> np.ndarray:
probabilities = simulator.simulate_pmeasure(circuit.originir)
probs = {
format(index, "04b"): float(value)
for index, value in enumerate(probabilities)
if float(value) > 1e-12
}
return np.array(
[
calculate_expectation(probs, "ZIII"),
calculate_expectation(probs, "IZII"),
calculate_expectation(probs, "IIZI"),
calculate_expectation(probs, "IIIZ"),
]
)
features = batch_execute(circuits, executor, n_workers=4)
return np.vstack(features)
def main() -> None:
parser = argparse.ArgumentParser(description="Binary digit classification with quantum features")
parser.add_argument("--max-samples", type=int, default=200)
parser.add_argument("--test-size", type=float, default=0.25)
args = parser.parse_args()
require_simulation()
x, y = load_binary_digits(args.max_samples)
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=args.test_size, random_state=42, stratify=y
)
train_features = extract_quantum_features(x_train)
test_features = extract_quantum_features(x_test)
model = LogisticRegression(max_iter=500)
model.fit(train_features, y_train)
predictions = model.predict(test_features)
print("Quantum feature extraction completed.")
print(f"Train samples: {len(x_train)}")
print(f"Test samples: {len(x_test)}")
print("\nClassification report:")
print(classification_report(y_test, predictions))
if __name__ == "__main__":
main()
FILE:examples/qaoa_maxcut.py
#!/usr/bin/env python3
"""QAOA MaxCut demo aligned with the current qaoa_ansatz API."""
from __future__ import annotations
import argparse
import numpy as np
from scipy.optimize import minimize
from uniqc.algorithmics.ansatz import qaoa_ansatz
PRESET_GRAPHS = {
"triangle": [(0, 1), (1, 2), (0, 2)],
"square": [(0, 1), (1, 2), (2, 3), (3, 0)],
"line3": [(0, 1), (1, 2)],
}
def require_simulation() -> None:
try:
from uniqc.task.optional_deps import check_simulation
except ImportError:
check_simulation = lambda: False
if not check_simulation():
raise RuntimeError(
"This example needs local simulation. Install unified-quantum[simulation] first."
)
def build_cost_hamiltonian(edges: list[tuple[int, int]]) -> list[tuple[str, float]]:
return [(f"Z{i}Z{j}", -0.5) for i, j in edges]
def bitstring_cut_value(bitstring: str, edges: list[tuple[int, int]]) -> int:
return sum(1 for i, j in edges if bitstring[i] != bitstring[j])
def expected_cut(probabilities: dict[str, float], edges: list[tuple[int, int]]) -> float:
return sum(probability * bitstring_cut_value(bitstring, edges) for bitstring, probability in probabilities.items())
def main() -> None:
parser = argparse.ArgumentParser(description="QAOA MaxCut demo")
parser.add_argument("--graph", choices=sorted(PRESET_GRAPHS), default="triangle")
parser.add_argument("--p", type=int, default=1)
parser.add_argument("--maxiter", type=int, default=80)
parser.add_argument("--seed", type=int, default=7)
args = parser.parse_args()
require_simulation()
from uniqc.simulator import OriginIR_Simulator
np.random.seed(args.seed)
edges = PRESET_GRAPHS[args.graph]
n_qubits = max(max(edge) for edge in edges) + 1
cost_hamiltonian = build_cost_hamiltonian(edges)
simulator = OriginIR_Simulator(backend_type="statevector")
def objective(parameters: np.ndarray) -> float:
betas = parameters[: args.p]
gammas = parameters[args.p :]
circuit = qaoa_ansatz(cost_hamiltonian, p=args.p, betas=betas, gammas=gammas)
circuit.measure(*range(n_qubits))
raw_probabilities = simulator.simulate_pmeasure(circuit.originir)
probabilities = {
format(index, f"0{n_qubits}b"): float(value)
for index, value in enumerate(raw_probabilities)
if float(value) > 1e-12
}
return -expected_cut(probabilities, edges)
initial = np.random.uniform(0.0, np.pi, size=2 * args.p)
result = minimize(
objective,
x0=initial,
method="COBYLA",
options={"maxiter": args.maxiter},
)
best_betas = result.x[: args.p]
best_gammas = result.x[args.p :]
final_circuit = qaoa_ansatz(cost_hamiltonian, p=args.p, betas=best_betas, gammas=best_gammas)
final_circuit.measure(*range(n_qubits))
final_raw_probabilities = simulator.simulate_pmeasure(final_circuit.originir)
final_probabilities = {
format(index, f"0{n_qubits}b"): float(value)
for index, value in enumerate(final_raw_probabilities)
if float(value) > 1e-12
}
print("QAOA MaxCut")
print("-" * 60)
print(f"Graph: {args.graph}")
print(f"Edges: {edges}")
print(f"Layers p: {args.p}")
print(f"Best expected cut: {-result.fun:.6f}")
print(f"Best betas: {best_betas}")
print(f"Best gammas: {best_gammas}")
print("\nTop bitstrings:")
for bitstring, probability in sorted(final_probabilities.items(), key=lambda item: item[1], reverse=True)[:5]:
print(
f" {bitstring} prob={probability:.4f} cut={bitstring_cut_value(bitstring, edges)}"
)
if __name__ == "__main__":
main()
FILE:README.md
# quantum-computing
[](https://github.com/IAI-USTC-Quantum)
面向 [UnifiedQuantum](https://github.com/IAI-USTC-Quantum/UnifiedQuantum) 的本地 skill 仓库。
安装后,支持 skills 的 Agent 可以更稳地处理 UnifiedQuantum 相关任务,例如线路构建、OriginIR / QASM 转换、本地模拟、云平台提交、变分算法示例、PyTorch 集成和通用排障。
## 这个 skill 能帮你什么
适合让 Agent 帮你处理这类事情:
- 写或修改 `Circuit` 线路代码
- 把线路导出成 OriginIR 或 OpenQASM
- 用 `uniqc` CLI 做转换、模拟、提交和查结果
- 检查本地模拟、dummy 模式或云平台配置问题
- 搭一个最小的 VQE / QAOA / UCCSD 示例
- 看 `QuantumLayer`、parameter-shift 和批处理接口怎么接进 PyTorch
## 你可以直接让 Agent 做什么
安装后,可以直接对 Agent 说这类请求:
- “帮我写一个 Bell state 的 UnifiedQuantum 示例,并导出 OriginIR。”
- “帮我把这段 QASM 转成更适合 `uniqc simulate` 的流程。”
- “帮我查一下为什么 `uniqc` 命令存在,但 `import uniqc` 失败。”
- “帮我写一个最小 QAOA MaxCut 例子。”
- “帮我看一下 dummy 模式为什么跑不通。”
- “帮我把这个 PyTorch 训练循环接上 `QuantumLayer`。”
## 安装此 skill
先把仓库放到本地,再把它链接或复制到你的 skill 目录。
```bash
git clone https://github.com/IAI-USTC-Quantum/quantum-computing.skill.git
mkdir -p ~/.Agents/skills
ln -s /path/to/quantum-computing.skill ~/.Agents/skills/quantum-computing
```
如果你已经有自己的共享 skills 目录,就安装到那个目录里。
安装完成后,Agent 就可以从 `SKILL.md` 和 `references/` 里读取更具体的操作规则、主题说明和排障步骤。
## 仓库内容
- `SKILL.md`:主入口,包含触发条件、操作规则和导航
- `references/`:按主题整理的使用说明与排障参考
- `examples/`:可复用的示例代码
- `scripts/`:环境检查和辅助脚本
## 许可证
Apache 2.0 许可证
FILE:references/circuit-building.md
# 线路构建参考
当前 UnifiedQuantum 的核心对象仍然是 `uniqc.circuit_builder.Circuit`。
最常见的工作流是:
1. 构建 `Circuit`
2. 导出 `originir` 或 `qasm`
3. 交给 `uniqc` CLI、模拟器或 `task_manager`
## 快速开始
```python
from uniqc.circuit_builder import Circuit
c = Circuit()
c.h(0)
c.cnot(0, 1)
c.measure(0, 1)
print(c.originir)
print(c.qasm)
```
## 初始化方式
```python
from uniqc.circuit_builder import Circuit
c = Circuit() # 自动按使用到的 qubit 推断
c = Circuit(4) # 固定 qubit 数
c = Circuit(qregs={"data": 4, "ancilla": 2}) # 命名寄存器
```
## 常用属性
| 属性 | 含义 |
|------|------|
| `originir` | OriginIR 文本 |
| `qasm` | OpenQASM 2.0 文本 |
| `circuit` | 一般可视为 `originir` 别名 |
| `qubit_num` | 当前线路的 qubit 数 |
| `cbit_num` | 当前线路的 classical bit 数 |
| `depth` | 线路深度 |
| `used_qubit_list` | 实际用到的 qubit |
| `measure_list` | 已加入测量的 qubit |
| `opcode_list` | 内部 opcode 列表 |
## 常用门
### 单比特门
```python
c.h(0)
c.x(0)
c.y(0)
c.z(0)
c.s(0)
c.t(0)
c.sx(0)
c.sxdg(0)
c.identity(0)
```
### 旋转门
```python
c.rx(0, theta)
c.ry(0, theta)
c.rz(0, theta)
c.rphi(0, theta, phi)
c.u1(0, lam)
c.u2(0, phi, lam)
c.u3(0, theta, phi, lam)
```
### 双比特 / 三比特门
```python
c.cnot(0, 1)
c.cx(0, 1)
c.cz(0, 1)
c.swap(0, 1)
c.iswap(0, 1)
c.xx(0, 1, theta)
c.yy(0, 1, theta)
c.zz(0, 1, theta)
c.phase2q(0, 1, t1, t2, tzz)
c.uu15(0, 1, params)
c.toffoli(0, 1, 2)
c.cswap(0, 1, 2)
```
## 测量
```python
c.measure(0)
c.measure(0, 1, 2)
```
测量会按调用顺序分配 classical bit。
## 控制与 dagger 上下文
```python
with c.control(0):
c.x(1)
c.z(2)
with c.dagger():
c.h(0)
c.rx(1, 0.5)
```
## 命名寄存器
```python
c = Circuit(qregs={"data": 4, "anc": 2})
data = c.get_qreg("data")
c.h(data[0])
c.cnot(data[0], data[1])
```
常用类型:
```python
from uniqc.circuit_builder import Qubit, QReg, QRegSlice
```
## 参数与可复用电路
可复用子线路一般通过 `circuit_def` / `NamedCircuit` 表达:
```python
from uniqc.circuit_builder import Circuit, circuit_def
@circuit_def(name="bell_pair", qregs={"q": 2})
def bell_pair(circ, q):
circ.h(q[0])
circ.cnot(q[0], q[1])
return circ
c = Circuit(2)
bell_pair(c, qreg_mapping={"q": [0, 1]})
```
如果用户需要参数化子线路,优先沿这条路径组织,而不是手工约定一套外部模板格式。
## 复制、拼接、重映射
```python
c2 = c.copy()
c2.add_circuit(other)
mapped = c.remapping({0: 3, 1: 5})
```
`remapping()` 返回新线路,不会原地改动。
## 屏障
```python
c.barrier(0, 1, 2)
```
## 输出建议
最稳妥的导出方式通常是:
- 需要 CLI、云提交、dummy 模式时:优先 `originir`
- 需要和第三方工具交换时:使用 `qasm`
如果后续步骤涉及 `uniqc simulate`,建议先把输入统一到 OriginIR,再继续执行。
FILE:references/cli-guide.md
# CLI 使用参考
UnifiedQuantum 当前的 CLI 入口是:
```bash
uniqc
```
等价 Python 入口:
```bash
python3 -m uniqc
```
## 命令总览
- `uniqc circuit`
- `uniqc simulate`
- `uniqc submit`
- `uniqc result`
- `uniqc task`
- `uniqc config`
## 一条稳妥的 Shell 工作流
如果输入来自 QASM,推荐先归一化:
```bash
uniqc circuit input.qasm --format originir -o normalized.ir
uniqc simulate normalized.ir
uniqc submit normalized.ir --platform dummy --wait
```
这样比直接把多种格式混进不同命令里更稳。
## `uniqc circuit`
格式转换和统计信息:
```bash
uniqc circuit INPUT_FILE [--format originir|qasm] [--output PATH] [--info]
```
示例:
```bash
uniqc circuit bell.ir --format qasm -o bell.qasm
uniqc circuit bell.qasm --format originir -o bell.ir
uniqc circuit bell.ir --info
```
## `uniqc simulate`
本地模拟:
```bash
uniqc simulate INPUT_FILE [--backend statevector] [--shots 1024] [--format table|json]
```
示例:
```bash
uniqc simulate bell.ir
uniqc simulate bell.ir --shots 4096 --format json
```
注意:
- 当前最安全的输入是 OriginIR
- 本地模拟通常需要安装 `unified-quantum[simulation]`
- 当前 CLI 的 `simulate` 路径最适合 `statevector`
- 密度矩阵工作流更建议走 Python API,并显式使用 `OriginIR_Simulator(backend_type="densitymatrix")`
## `uniqc submit`
提交云任务或 dummy 任务:
```bash
uniqc submit INPUT_FILES... --platform originq|quafu|ibm|dummy
```
当前可见选项:
```bash
--platform / -p
--backend / -b
--shots / -s
--name
--wait / -w
--timeout
--format / -f
```
示例:
```bash
uniqc submit bell.ir --platform originq --shots 1000
uniqc submit bell.ir --platform originq --backend origin:wuyuan:d5
uniqc submit bell.ir --platform dummy --wait
uniqc submit a.ir b.ir --platform quafu --shots 2000
```
关键区别:
- 当前 CLI 用的是 `--backend`,不是一些旧示例里常见的 `--chip-id`
- 对 OriginQ,`--backend` 常用于指定硬件名,例如 `origin:wuyuan:d5`
- 对 Quafu,底层 Python API 仍可传 `chip_id`,但当前 CLI 没有单独的 `--chip-id`
## `uniqc result`
查询任务结果:
```bash
uniqc result TASK_ID [--platform PLATFORM] [--wait] [--timeout 300] [--format table|json]
```
示例:
```bash
uniqc result abc123 --platform originq
uniqc result abc123 --wait --timeout 600
```
如果任务已经在本地 cache 里,通常可以少传一点参数;如果不在 cache 里,再补 `--platform`。
## `uniqc task`
本地任务缓存管理:
```bash
uniqc task list
uniqc task show TASK_ID
uniqc task clear
```
可用选项包括:
- `task list --status ... --platform ... --limit ... --format ...`
- `task clear --status ... --force`
## `uniqc config`
配置 `~/.uniqc/uniqc.yml`:
```bash
uniqc config init
uniqc config set originq.token YOUR_TOKEN
uniqc config get originq
uniqc config list
uniqc config validate
uniqc config profile list
uniqc config profile create dev
uniqc config profile use dev
```
## 配置 profile 与环境变量
- 默认 profile:`default`
- 当前激活 profile 可写进配置文件
- 临时覆盖可用:
```bash
export UNIQC_PROFILE=dev
```
## 常见误区
- 不要再把 CLI 讲成旧版的 `chip-id` 优先接口
- 不要假设裸环境一定能直接 `simulate`
- 不要默认系统里有 `python`;脚本示例优先写 `python3` 或 `uv run`
FILE:references/cloud-platforms.md
# 云平台参考
当前 UnifiedQuantum 的云执行路径分两层:
1. CLI:`uniqc submit` / `uniqc result` / `uniqc task`
2. Python API:`submit_task` / `submit_batch` / `query_task` / `wait_for_result`
## 配置文件
默认配置路径:
```text
~/.uniqc/uniqc.yml
```
典型结构:
```yaml
default:
originq:
token: ""
available_qubits: []
available_topology: []
task_group_size: 200
quafu:
token: ""
ibm:
token: ""
proxy:
http: ""
https: ""
```
初始化:
```bash
uniqc config init
```
设置 token:
```bash
uniqc config set originq.token YOUR_ORIGINQ_TOKEN
uniqc config set quafu.token YOUR_QUAFU_TOKEN
uniqc config set ibm.token YOUR_IBM_TOKEN
```
## 配置 profile
可通过多 profile 管理不同环境:
```bash
uniqc config profile create dev
uniqc config profile use dev
uniqc config profile list
```
也可以临时覆盖:
```bash
export UNIQC_PROFILE=dev
```
## Python 任务 API
最常用的公共入口:
```python
from uniqc import (
submit_task,
submit_batch,
query_task,
wait_for_result,
list_tasks,
clear_completed_tasks,
)
```
### `submit_task`
```python
task_id = submit_task(
circuit,
backend="originq",
shots=1000,
metadata={"name": "demo"},
)
```
后端相关的常见 kwargs:
- OriginQ:
- `backend_name="origin:wuyuan:d5"`
- `circuit_optimize=...`
- `measurement_amend=...`
- Quafu:
- `chip_id="ScQ-P10"`
- `auto_mapping=True`
- 通用 dummy 覆盖:
- `dummy=True`
### `submit_batch`
```python
task_ids = submit_batch(
[circuit_a, circuit_b],
backend="quafu",
shots=2000,
)
```
### `query_task`
```python
task_info = query_task(task_id)
print(task_info.status)
```
如果任务不在本地 cache 中,通常需要补 `backend=...`。
### `wait_for_result`
```python
result = wait_for_result(task_id, backend="originq", timeout=300)
```
返回值通常是一个归一化后的结果字典;dummy 路径下最常见的结构类似:
```python
{
"counts": {"00": 500, "11": 500},
"probabilities": {"00": 0.5, "11": 0.5},
"shots": 1000,
"platform": "dummy",
"task_id": "...",
}
```
不同平台的原始数据会被归一化,但如果用户只关心最稳妥的兼容字段,优先读:
- `counts`
- `probabilities`
## dummy 模式
dummy 模式用于本地模拟,不消耗真实云平台额度。
启用方式有两类:
### 1. 单次任务
```python
task_id = submit_task(circuit, backend="originq", dummy=True)
```
### 2. 全局环境变量
```bash
export UNIQC_DUMMY=true
```
然后:
```python
task_id = submit_task(circuit, backend="originq")
```
注意:
- dummy 模式通常仍需要本地模拟依赖
- 它更适合开发 / 测试 / 文档示例,不等于“完全无依赖”
## 本地任务缓存
当前任务缓存使用 SQLite:
```text
~/.uniqc/cache/tasks.sqlite
```
相关接口:
```python
from uniqc import list_tasks, clear_completed_tasks, clear_cache
```
CLI 里对应:
```bash
uniqc task list
uniqc task show TASK_ID
uniqc task clear
```
## `TaskInfo`
缓存和查询结果常见字段:
- `task_id`
- `backend`
- `status`
- `result`
- `shots`
- `submit_time`
- `update_time`
- `metadata`
## 平台建议
### OriginQ
- 先确认 token 已配置
- Python API 更适合传 `backend_name`
- CLI 用 `--backend`
### Quafu
- 先确认 token 已配置
- Python API 可传 `chip_id`
- 当前 CLI 对 Quafu 的专有参数表达较少,必要时优先 Python
### IBM
- 先确认 token 已配置
- 代理设置写在 `ibm.proxy.http` / `ibm.proxy.https`
## 使用这些接口时不要默认
- 不要默认 dummy 模式“无条件可用”
- 不要默认 CLI 已经完整暴露了所有平台专有参数
- 不要默认任务结果永远只有一种字段布局;更稳妥的是优先读取常用字段
FILE:references/h2-molecular-simulation.md
# H2 分子模拟参考
这个主题最容易受版本和依赖影响。用户提到 “H2 模拟” 时,先不要默认当前环境已经具备完整、固定的量子化学求解栈,更稳妥的做法是先确认他到底想解决哪一层问题。
## 推荐的定位
当用户说“H2 模拟”时,先分清楚他要的是哪一层:
1. 想看一个最小 VQE 例子
2. 想看 `hea` / `uccsd_ansatz` 如何接进变分循环
3. 想接入真实量子化学积分与 Hamiltonian 生成
当前 UnifiedQuantum 更适合稳定覆盖前两层。
## 推荐的理解方式
优先把 H2 任务理解成:
- 选一个小规模哈密顿量
- 选一个 ansatz(HEA 或 UCCSD)
- 用本地模拟器估计目标值
- 用经典优化器最小化能量
不要默认当前环境已经“内置了完整分子积分、Jordan-Wigner、basis set 流水线”。
## 最常用两个入口
### HEA
```python
from uniqc.algorithmics.ansatz import hea
```
适合:
- 教学演示
- 先跑通 VQE 外框架
### UCCSD
```python
from uniqc.algorithmics.ansatz import uccsd_ansatz
```
适合:
- 更接近量子化学 ansatz 的表达
- 已知 qubit 数和电子数时的最小示例
## 结果估计
当前更推荐两种估计路线:
1. 从 `simulate_pmeasure()` 得到概率分布,再用 `calculate_expectation()` 算 Z 型项
2. 从 `simulate_statevector()` 得到态矢,在示例里手工实现所需的观测量计算
如果只做教学示例,第一种通常更简单。
## 应该明确说出的限制
- H2 示例通常依赖本地模拟能力,因此往往需要 `unified-quantum[simulation]`
- 这里的 H2 示例更偏“工作流模板”,不是完整量化化学软件替代品
- 如果用户真的需要从分子几何一路生成哈密顿量,通常还要引入额外化学工具链
FILE:references/pytorch-integration.md
# PyTorch 集成参考
UnifiedQuantum 当前的 PyTorch 集成是辅助工具风格,而不是“一整套端到端训练框架”。
基础安装:
```bash
pip install "unified-quantum[pytorch]"
```
如果需要 TorchQuantum:
```bash
pip install "unified-quantum[torchquantum]"
```
## 当前公开接口
```python
from uniqc.pytorch import (
QuantumLayer,
batch_execute,
batch_execute_with_params,
parameter_shift_gradient,
compute_all_gradients,
)
```
可选情况下还可能有:
```python
from uniqc.pytorch import TorchQuantumLayer
```
## `QuantumLayer`
`QuantumLayer` 是一个基于 parameter-shift 的 `nn.Module` 包装器。
它需要两个核心输入:
1. 一个带参数映射的电路模板
2. 一个从绑定后线路计算期望值的函数
这里要特别保守一点说明:
- `QuantumLayer` 不是“给任意 `Circuit` 一包就能直接训练”
- 调用方需要准备好一个适配它的参数化线路模板
- 如果用户只是想先验证思路,通常先用 `parameter_shift_gradient` 或 `batch_execute_with_params` 更稳
因此更稳妥的理解是:把 `QuantumLayer` 当成“已有参数化线路模板后的包装器”,不要把它当成零配置的端到端训练入口。
## Parameter-shift 辅助函数
### 单参数梯度
```python
from uniqc.pytorch import parameter_shift_gradient
grad = parameter_shift_gradient(circuit, "theta", expectation_fn)
```
### 全部参数梯度
```python
from uniqc.pytorch import compute_all_gradients
grads = compute_all_gradients(circuit, expectation_fn)
```
## 批处理辅助函数
### 批量执行多个线路
```python
from uniqc.pytorch import batch_execute
results = batch_execute(circuits, executor, n_workers=4)
```
### 对一个模板绑定多组参数
```python
from uniqc.pytorch import batch_execute_with_params
results = batch_execute_with_params(
circuit_template,
[{"theta": 0.1}, {"theta": 0.2}],
executor,
)
```
## `TorchQuantumLayer`
如果用户安装了 `torchquantum`,还可能使用 `TorchQuantumLayer`,它走的是 TorchQuantum 的原生自动求导路径,而不是 parameter-shift。
适合场景:
- 已经在用 TorchQuantum
- 想要更“端到端”的可微分体验
## 使用这些接口时记住
- 把这些工具理解为“辅助工具”
- 用户仍要自己定义 expectation / loss / optimizer
- 不要默认 UnifiedQuantum 自带完整数据集管道
- 如果示例需要 `torchvision`、`scikit-learn` 等第三方库,要单独确认这些依赖
FILE:references/simulators.md
# 模拟器参考
UnifiedQuantum 提供本地模拟能力,但当前应该把它理解为“常用能力 + 明确依赖边界”,而不是默认裸环境必备。
## 依赖边界
本地模拟、dummy 模式、部分算法示例通常需要:
```bash
pip install "unified-quantum[simulation]"
```
如果用户看到这类报错,优先怀疑缺可选依赖:
- `No module named 'qutip'`
- `MissingDependencyError(... simulation ...)`
- `uniqc is not installed with UniqcCpp`
## 主要类
### `OriginIR_Simulator`
```python
from uniqc.simulator import OriginIR_Simulator
sim = OriginIR_Simulator(backend_type="statevector")
```
常见方法:
```python
probs = sim.simulate_pmeasure(circuit.originir)
statevector = sim.simulate_statevector(circuit.originir)
rho = sim.simulate_density_matrix(circuit.originir)
counts = sim.simulate_shots(circuit.originir, shots=1000)
```
### `OriginIR_NoisySimulator`
```python
from uniqc.simulator import OriginIR_NoisySimulator
```
适合在有噪声模型时做本地实验。
## 后端类型
最稳妥的后端类型选择:
- `statevector`
- `densitymatrix`
示例:
```python
sim = OriginIR_Simulator(backend_type="statevector")
sim = OriginIR_Simulator(backend_type="densitymatrix")
```
当前 CLI 的 `simulate` 子命令更适合走 `statevector` 路径;如果用户明确要密度矩阵模拟,优先给 Python API 方案。
## 输入格式建议
`OriginIR_Simulator` 最适合直接吃 OriginIR:
```python
result = sim.simulate_pmeasure(circuit.originir)
```
如果用户手里只有 QASM,先统一格式更稳:
1. `Circuit.qasm -> uniqc circuit --format originir`
2. 再用模拟器或 `uniqc simulate`
## 拓扑与可用 qubit 约束
模拟器支持传入约束:
```python
sim = OriginIR_Simulator(
backend_type="statevector",
available_qubits=[0, 1, 2, 3],
available_topology=[[0, 1], [1, 2], [2, 3]],
)
```
这适合:
- 提前检查线路是否符合目标芯片拓扑
- 在 dummy 场景里模拟“可用 qubit / coupling map”限制
## `least_qubit_remapping`
基础模拟器支持 `least_qubit_remapping` 参数。默认会做更紧凑的 qubit 映射;如果用户非常在意保留原始编号,可显式关闭。
```python
sim = OriginIR_Simulator(
backend_type="statevector",
least_qubit_remapping=False,
)
```
## 与 dummy 模式的关系
dummy 适配器底层会走本地模拟,因此:
- dummy 不是“绕过模拟依赖”
- dummy 更像“沿用统一的 task API,但把执行后端换成本地模拟器”
## 什么时候不该强推模拟器
以下情况不要默认推荐先本地模拟:
- 用户只想做格式转换
- 用户只想提交到云平台
- 用户环境明显缺少模拟依赖,而且问题与模拟无关
这时优先走:
- `Circuit -> originir -> uniqc submit`
- 或纯文档解释,不强制运行本地模拟
FILE:references/troubleshooting.md
# 通用排错参考
这份说明用于**功能主题自己的排错步骤仍不够时**的通用兜底排查。
优先顺序应当是:
1. 先看当前问题对应的功能 reference
2. 如果功能内排查仍解释不了,再回到这里做通用诊断
遇到这些情况时,优先查这里:
- 功能文档里的排错已经做过,但问题仍解释不通
- 文档里的命令、导入路径、配置文件位置和本地安装对不上
- 某个功能在说明里提到,但用户环境里“没有这个命令 / 没有这个模块 / 没有这个 extra”
- 同一份示例在不同机器上,一个能跑、一个提示参数名、模块名或行为不一致
## 先确认问题是不是功能专属
这份文档不替代功能主题内的局部排错。常见入口:
- CLI / 命令参数 / shell 工作流:`references/cli-guide.md`
- 本地模拟 / dummy / simulation 依赖:`references/simulators.md`
- 云平台 / token / task cache / dummy 提交:`references/cloud-platforms.md`
- HEA / QAOA / UCCSD / VQE:`references/variational-algorithms.md`
- PyTorch 辅助工具:`references/pytorch-integration.md`
如果这些局部文档已经看过,仍然解释不了问题,再继续下面的通用步骤。
## 推荐顺序
不要一开始就读一大堆前提。通用排障更适合按下面的顺序推进:
1. 先拍一张当前安装快照:解释器、包版本、模块路径、CLI 路径
2. 立刻判断版本变化是不是主要原因,先看对应版本摘要
3. 再查 issue / discussion 里有没有同类报错、参数改名或行为变更
4. 再回到对应功能文档和上游文档核实当前用法
5. 还解释不通时,最后再读源码
这个顺序的目标不是一次性证明所有前提,而是先快速判断“问题大概率落在哪一层”。
## 第一步:先拍安装快照
排错前不要假设用户装的是 PyPI 发布版,也不要假设 shell 里的 `uniqc` 和当前 Python 是同一套环境。至少先回答四个问题:
- 当前用的是哪个解释器
- `unified-quantum` 显示的版本号是什么
- `uniqc` 模块实际从哪里导入
- shell 里执行到的 `uniqc` 在哪里
最小检查命令:
```bash
python3 - <<'PY'
from importlib.metadata import version
from shutil import which
import sys
print("exe=", sys.executable)
print("cli=", which("uniqc"))
try:
import uniqc
except ModuleNotFoundError:
print("module= <uniqc not importable>")
else:
print("module=", uniqc.__file__)
try:
print("version=", version("unified-quantum"))
except Exception as exc:
print("version_probe_failed=", repr(exc))
PY
```
如果用户使用的是项目虚拟环境、`uv tool`、Conda 或其他解释器,要改用对应解释器执行这条命令。
结果通常可以分成三类:
- **PyPI / wheel 安装**:模块路径位于 `site-packages`
- **源码 editable 安装但对齐某个 release/tag**:模块路径指向源码目录,版本号通常仍是正式版本
- **源码 editable 安装且跟随主线**:模块路径指向源码目录,版本号可能带 `dev`,或者与已发布行为不一致
这里最重要的是:**以当前解释器真正导入到的包为准**,不要只看用户在哪个源码目录里开了终端,也不要只看仓库 README。
如果需要快速做一次环境体检,可以运行:
```bash
bash scripts/setup_uniqc.sh
```
它更适合快速回答这些通用问题:
- `uniqc` 能不能启动
- `import uniqc` 是否成功
- 基础依赖是否缺失
- simulation / PyTorch / scikit-learn 等常见可选能力是否可用
## 第二步:先看版本摘要,不要先钻细节
拿到安装快照后,先判断“版本变化是不是主要原因”。
适合优先看版本摘要的信号包括:
- 用户看的文档、示例或笔记明显比当前安装新或旧
- 报错集中在“参数名不存在”“命令组选项变了”“导出路径改了”“模块路径变了”
- 同一段代码在两台机器上表现不同,而版本号或模块路径不同
这一层先看**对应版本附近的 release notes、changelog、tag 摘要**就够了,目的是快速判断是不是版本差异导致的,不必一上来就翻完整源码。
UnifiedQuantum 当前可直接从这里看版本摘要:
- Release Notes: `https://iai-ustc-quantum.github.io/UnifiedQuantum/source/releases/index.html`
如果用户拿的是文档站里的说明但本地行为对不上,先把本地版本号和这个页面里的相邻版本条目对一遍,再决定要不要继续查 issue 或源码。
如果已经确认用户直接在 `UnifiedQuantum` 源码 checkout 里工作,且需要判断“当前代码大致落在哪个 tag 附近”,才额外使用:
```bash
git describe --tags --always
```
它只是辅助判断源码树和最近 tag 的相对位置,不能替代“当前解释器里实际导入了哪个版本”的检查。
## 第三步:查 issue / discussion 有没有同类问题
如果版本摘要提示“这里确实变过”,或者报错看起来像回归、兼容性问题、文档滞后问题,就优先查 issue / discussion。
搜索时优先组合这些信息:
- 版本号或 tag
- 精确报错文本
- 具体命令、参数名、模块名或函数名
- 所属平台或可选依赖,例如 `simulation`、`originq`、`qiskit`、`torch`
这一层最有价值的问题通常是:
- 某个参数或命令最近是否改名
- 某个 extra 是否刚被拆分、上移或不再默认安装
- 某个功能是否只在主线修复、尚未发版
- 某个示例或文档是否已知落后于当前实现
## 第四步:再回到功能文档和上游文档
确认大致方向后,再去读具体文档,效率会高很多。优先顺序通常是:
1. 当前问题对应的主题 reference
2. 与当前安装来源匹配的上游文档
3. 与当前版本相邻的示例或官方说明
很多“看起来像程序问题”的情况,其实只是解释器、CLI 入口或可选依赖没对齐。这里重点再核对:
- 当前 shell 里的 `uniqc` 和你用来 `import uniqc` 的解释器是不是同一套环境
- 需要模拟、dummy、PyTorch、云平台适配器时,有没有装对应 extra
- 用户是不是把主线源码 README、历史笔记或旧示例,当成了当前安装版本的真实能力
## 第五步:最后再读源码
只有前面几步还解释不通时,才进入源码层。
源码排查的推荐顺序:
1. 如果模块已经能导入,先读当前环境里真正加载到的源码或已安装文件
2. 如果需要对照上游实现,再在用户允许的前提下 clone `UnifiedQuantum`
3. 如果用户已经有本地 checkout,优先让用户提供大致路径,再直接在那份仓库里读
如果要 clone 或进入用户本地仓库做进一步检查,先和用户确认再动手。不要默认把排障升级成仓库操作。
## 常见排错思路
- 缺命令、缺导入、缺 extra 时,先确认是不是装错了解释器或少装了可选依赖
- `uniqc` 能运行但 `import uniqc` 失败,或反过来时,优先怀疑不是同一个环境
- 文档和本地行为不一致时,先看安装快照,再判断是不是版本差异
- 如果模块路径指向源码目录,不要立刻把 README 上写的能力当成“当前环境一定具备”
- 如果模块路径指向 `site-packages`,但用户拿的是主线源码说明,优先怀疑“文档比安装版本新”
- 不要先入为主地判定“包坏了”或“这份说明过时了”;很多问题只是安装来源和参考文档不一致
## 使用这份参考时记住
- 功能专属问题优先回到对应主题 reference,不要在这里一次性展开所有细节
- 这里主要解决通用排错顺序、环境快照、版本识别、issue 检索和源码升级路径
- 需要解释功能差异时,优先看与当前安装来源匹配的官方版本摘要、issue 和文档
- 如果你看到手工维护的版本断点表,不要把它当成主判断依据;只有当某个识别步骤本身会影响排障时,它才值得参考
FILE:references/variational-algorithms.md
# 变分算法参考
当前 UnifiedQuantum 对变分算法更适合从“ansatz 构造器 + 自己的目标函数 / 优化器”来理解,而不是依赖一套过度封装的旧接口。
## 当前公开 ansatz
```python
from uniqc.algorithmics.ansatz import hea, qaoa_ansatz, uccsd_ansatz
```
注意:
- `uccsd_ansatz` 才是当前公开名字
- 如果用户拿的是旧示例,注意当前公开名字不是旧的 `uccsd`
## HEA
```python
from uniqc.algorithmics.ansatz import hea
circuit = hea(
n_qubits=4,
depth=2,
params=params, # 长度应为 2 * n_qubits * depth
)
```
特点:
- 结构轻
- NISQ 友好
- 常用于最小 VQE / VQC 示例
## QAOA ansatz
```python
from uniqc.algorithmics.ansatz import qaoa_ansatz
cost_hamiltonian = [
("Z0Z1", 1.0),
("Z1Z2", 1.0),
]
circuit = qaoa_ansatz(
cost_hamiltonian,
p=2,
betas=betas,
gammas=gammas,
)
```
要点:
- `cost_hamiltonian` 形如 `[(pauli_string, coefficient), ...]`
- `betas`、`gammas` 长度都应等于 `p`
- 如果不给,构造器会随机初始化
## UCCSD ansatz
```python
from uniqc.algorithmics.ansatz import uccsd_ansatz
circuit = uccsd_ansatz(
n_qubits=4,
n_electrons=2,
params=params,
)
```
要点:
- 适合量子化学风格任务
- 默认先准备 Hartree-Fock 初态
- 参数长度取决于单激发 / 双激发计数
## 一个典型 VQE 结构
UnifiedQuantum 当前更推荐这样组织 VQE:
1. 选 ansatz
2. 写一个目标函数
3. 在目标函数里调用模拟器得到概率或态矢
4. 用 `scipy.optimize.minimize` 或你自己的优化器迭代
示意:
```python
import numpy as np
from scipy.optimize import minimize
from uniqc.algorithmics.ansatz import hea
from uniqc.simulator import OriginIR_Simulator
from uniqc.analyzer import calculate_expectation
sim = OriginIR_Simulator(backend_type="statevector")
def objective(params):
circuit = hea(n_qubits=2, depth=1, params=params)
probs = sim.simulate_pmeasure(circuit.originir)
return calculate_expectation(probs, "ZZ")
result = minimize(objective, x0=np.zeros(4), method="COBYLA")
```
## 一个典型 QAOA 结构
1. 从图构造 cost Hamiltonian
2. 用 `qaoa_ansatz()` 生成电路
3. 用概率分布评估 cut value
4. 优化 `betas` / `gammas`
## 使用这些 ansatz 时记住
- 把 ansatz 当作“线路生成器”来解释
- 把优化器和目标函数当作用户可替换部分
- 不要把示例当成“唯一正确范式”
- 如果示例依赖本地模拟,明确标出 `simulation` extra
FILE:scripts/setup_uniqc.sh
#!/usr/bin/env bash
# Installation verification script for UnifiedQuantum examples and common workflows.
set -u
PASSED=0
FAILED=0
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
check_pass() {
echo -e "GREEN✓NC $1"
PASSED=$((PASSED + 1))
}
check_fail() {
echo -e "RED✗NC $1"
FAILED=$((FAILED + 1))
}
check_warn() {
echo -e "YELLOW!NC $1"
}
run_py() {
python3 "$@"
}
echo "============================================================"
echo "UnifiedQuantum Environment Check"
echo "============================================================"
echo
echo "1. Python"
echo "------------------------------------------------------------"
if command -v python3 >/dev/null 2>&1; then
echo " Python: $(python3 --version)"
if python3 -c "import sys; raise SystemExit(0 if (3, 10) <= sys.version_info[:2] < (3, 14) else 1)"; then
check_pass "Python version is within UnifiedQuantum's supported range"
else
check_fail "UnifiedQuantum expects Python >= 3.10 and < 3.14"
fi
else
check_fail "python3 not found"
fi
echo
echo "2. Core Package"
echo "------------------------------------------------------------"
if run_py - <<'PY'
import uniqc
print(getattr(uniqc, "__version__", "unknown"))
PY
then
check_pass "uniqc imports successfully"
else
check_fail "uniqc import failed. Install with: pip install unified-quantum"
fi
echo
echo "3. Core Runtime Dependencies"
echo "------------------------------------------------------------"
for pkg in numpy scipy requests yaml sympy; do
if run_py - <<PY
import pkg
print(getattr(pkg, "__version__", "ok"))
PY
then
check_pass "pkg is importable"
else
check_fail "pkg is missing"
fi
done
echo
echo "4. CLI"
echo "------------------------------------------------------------"
if command -v uniqc >/dev/null 2>&1; then
if uniqc --help >/dev/null 2>&1; then
check_pass "uniqc CLI command works"
else
check_fail "uniqc command exists but --help failed"
fi
elif run_py -m uniqc --help >/dev/null 2>&1; then
check_pass "python3 -m uniqc works"
else
check_fail "No working CLI entrypoint found"
fi
echo
echo "5. Circuit Builder Smoke Test"
echo "------------------------------------------------------------"
if run_py - <<'PY'
from uniqc.circuit_builder import Circuit
c = Circuit(2)
c.h(0)
c.cnot(0, 1)
c.measure(0, 1)
assert "QINIT" in c.originir
assert "OPENQASM" in c.qasm
print(c.originir)
PY
then
check_pass "Circuit export to OriginIR and QASM works"
else
check_fail "Circuit builder smoke test failed"
fi
echo
echo "6. Optional Features"
echo "------------------------------------------------------------"
if run_py - <<'PY'
import qutip
print(qutip.__version__)
PY
then
check_pass "qutip is available (simulation extra looks installed)"
else
check_warn "qutip is missing; local simulation and dummy mode examples may fail"
fi
if run_py - <<'PY'
import torch
print(torch.__version__)
PY
then
check_pass "torch is available"
else
check_warn "torch is missing; PyTorch helper examples will be skipped"
fi
if run_py - <<'PY'
import sklearn
print(sklearn.__version__)
PY
then
check_pass "scikit-learn is available"
else
check_warn "scikit-learn is missing; digit-classification example will be skipped"
fi
echo
echo "7. Local Simulation Smoke Test"
echo "------------------------------------------------------------"
if run_py - <<'PY'
from uniqc.task.optional_deps import check_simulation
if not check_simulation():
raise SystemExit(2)
from uniqc.circuit_builder import Circuit
from uniqc.simulator import OriginIR_Simulator
c = Circuit(2)
c.h(0)
c.cnot(0, 1)
c.measure(0, 1)
sim = OriginIR_Simulator(backend_type="statevector")
probs = sim.simulate_pmeasure(c.originir)
assert len(probs) > 0
print("nonzero entries:", sum(1 for value in probs if float(value) > 1e-12))
PY
then
check_pass "Local simulation works"
else
status=$?
if [ "$status" -eq 2 ]; then
check_warn "Simulation extra not installed; local simulation smoke test skipped"
else
check_fail "Local simulation smoke test failed"
fi
fi
echo
echo "============================================================"
echo "Summary"
echo "============================================================"
echo -e "Passed: GREENPASSEDNC"
echo -e "Failed: REDFAILEDNC"
if [ "$FAILED" -eq 0 ]; then
echo
echo -e "GREENEnvironment looks good for the current UnifiedQuantum workflow.NC"
else
echo
echo -e "YELLOWSome checks failed. Suggested installs:NC"
echo " pip install unified-quantum"
echo " pip install \"unified-quantum[simulation]\""
echo " pip install \"unified-quantum[pytorch]\""
exit 1
fi
Create, maintain, and execute detailed incident response runbooks to guide triage, communication, and post-incident reviews for production outages.
# Incident Response Runbook
Generate, maintain, and execute incident response runbooks for production systems. Use when setting up incident workflows, responding to outages, or documenting post-incident learnings.
## Usage
### Generate Runbook
```
Create an incident response runbook for [service/system].
Infrastructure: [cloud provider, key services].
Common failure modes: [list known issues].
```
### During Incident
```
Incident: [description]. Severity: [1-4].
Current symptoms: [what's happening].
Help me triage and respond.
```
### Post-Incident
```
Generate a post-incident review for: [incident summary].
Timeline: [key events with timestamps].
Resolution: [what fixed it].
```
## Runbook Structure
Generated runbooks follow this template:
```markdown
# [Service] Incident Response Runbook
## Quick Reference
- **On-call:** [rotation link]
- **Dashboards:** [monitoring links]
- **Escalation:** [contact chain]
## Severity Levels
- **SEV1**: Complete outage, revenue impact → respond in 5 min
- **SEV2**: Degraded service, user-facing → respond in 15 min
- **SEV3**: Internal impact, no users affected → respond in 1 hour
- **SEV4**: Cosmetic or minor, no urgency → next business day
## Triage Steps
1. Confirm the issue (check dashboards, reproduce)
2. Assess blast radius (which users/services affected)
3. Assign severity level
4. Start incident channel/thread
5. Communicate to stakeholders
## Failure Modes
### [Failure Mode 1: e.g., Database Connection Pool Exhaustion]
**Symptoms:** [what you'll see]
**Diagnosis:** [commands to run, logs to check]
**Mitigation:** [immediate steps to restore service]
**Root Fix:** [permanent solution]
### [Failure Mode 2: e.g., Memory Leak in Worker Process]
...
## Rollback Procedures
[Service-specific rollback steps]
## Communication Templates
[Internal + external status page templates]
## Post-Incident Review Template
[Blameless review structure]
```
## Scripts
### `scripts/generate_runbook.py`
Generate a runbook skeleton from service metadata:
```bash
python3 scripts/generate_runbook.py --service api-gateway \
--provider aws --region us-east-1 \
--monitors datadog,pagerduty \
--output runbook-api-gateway.md
```
## AI Enhancement
When used as an agent skill, the incident responder:
- Guides triage in real-time with diagnostic commands specific to the stack
- Correlates symptoms with known failure modes from the runbook
- Drafts status page updates and internal communications
- Generates post-incident reviews with timeline, root cause analysis, and action items
- Learns from past incidents to improve future runbooks
FILE:scripts/generate_runbook.py
#!/usr/bin/env python3
"""Generate incident response runbook skeleton from service metadata."""
import argparse
from datetime import datetime
TEMPLATE = """# {service} Incident Response Runbook
*Generated: {date}*
*Provider: {provider} | Region: {region}*
## Quick Reference
| Item | Value |
|------|-------|
| Service | {service} |
| Provider | {provider} |
| Region | {region} |
| Monitoring | {monitors} |
| On-call | [Add rotation link] |
| Runbook owner | [Add name] |
## Severity Levels
- **SEV1** — Complete outage, revenue/user impact → respond in **5 min**, all hands
- **SEV2** — Degraded service, user-facing errors → respond in **15 min**, on-call + lead
- **SEV3** — Internal impact, no users affected → respond in **1 hour**, on-call
- **SEV4** — Cosmetic or minor issue → **next business day**
## Triage Checklist
1. [ ] Confirm the issue is real (not a monitoring false positive)
2. [ ] Check dashboards: {monitors}
3. [ ] Identify blast radius (which users/regions/services affected)
4. [ ] Assign severity level
5. [ ] Start incident channel: `#incident-{service_slug}-YYYY-MM-DD`
6. [ ] Post initial status update
## Diagnostic Commands
```bash
# Health check
curl -s https://{service_slug}.example.com/health | jq .
# Recent logs
# AWS: aws logs tail /aws/lambda/{service_slug} --since 30m
# Docker: docker logs {service_slug} --since 30m --tail 500
# K8s: kubectl logs -l app={service_slug} --since=30m --tail=500
# Resource usage
# K8s: kubectl top pods -l app={service_slug}
# Docker: docker stats {service_slug}
```
## Known Failure Modes
### 1. [Add: e.g., Database Connection Exhaustion]
**Symptoms:**
- [What alerts fire]
- [What users see]
**Diagnosis:**
```bash
# [Commands to confirm this specific failure mode]
```
**Mitigation:**
1. [Immediate step to restore service]
2. [Follow-up step]
**Root Fix:**
- [Permanent solution to prevent recurrence]
---
### 2. [Add: e.g., Memory Leak]
**Symptoms:**
- [Gradual response time increase]
- [OOM kills in logs]
**Diagnosis:**
```bash
# [Commands to check memory]
```
**Mitigation:**
1. [Rolling restart]
**Root Fix:**
- [Find and fix the leak]
---
## Rollback Procedure
```bash
# Option 1: Revert to previous deployment
# [deployment-specific rollback command]
# Option 2: Feature flag disable
# [feature flag command]
# Option 3: DNS failover
# [DNS update command]
```
## Communication Templates
### Internal (Slack/Teams)
```
🔴 INCIDENT — {service} — SEV[X]
Impact: [what's broken]
Status: [investigating/mitigating/resolved]
Lead: [name]
Channel: #incident-{service_slug}-[date]
```
### External (Status Page)
```
[Service Name] — [Investigating/Identified/Monitoring/Resolved]
We are aware of issues affecting [description].
Our team is actively investigating.
Updates will be posted every [30 minutes].
Last updated: [time UTC]
```
## Post-Incident Review Template
### Timeline
| Time (UTC) | Event |
|------------|-------|
| HH:MM | [First alert fired] |
| HH:MM | [Incident declared] |
| HH:MM | [Root cause identified] |
| HH:MM | [Mitigation applied] |
| HH:MM | [Service restored] |
### Five Whys
1. Why did the service fail? →
2. Why did that happen? →
3. Why? →
4. Why? →
5. Root cause: →
### Action Items
- [ ] [Preventive action 1] — owner: [name] — due: [date]
- [ ] [Preventive action 2] — owner: [name] — due: [date]
- [ ] [Detection improvement] — owner: [name] — due: [date]
"""
def main():
p = argparse.ArgumentParser(description="Generate incident response runbook")
p.add_argument("--service", required=True, help="Service name")
p.add_argument("--provider", default="generic", help="Cloud provider")
p.add_argument("--region", default="us-east-1", help="Deployment region")
p.add_argument("--monitors", default="custom", help="Monitoring tools (comma-separated)")
p.add_argument("--output", help="Output file path")
args = p.parse_args()
service_slug = args.service.lower().replace(" ", "-")
content = TEMPLATE.format(
service=args.service,
service_slug=service_slug,
provider=args.provider,
region=args.region,
monitors=args.monitors,
date=datetime.now().strftime("%Y-%m-%d"),
)
if args.output:
with open(args.output, "w") as f:
f.write(content)
print(f"Runbook written to {args.output}")
else:
print(content)
if __name__ == "__main__":
main()
Analyzes competitor websites to identify keyword gaps, SEO weaknesses, content opportunities, and backlink strategies with prioritized, actionable recommenda...
# SEO Competitor Analyzer Deep-dive analysis of competitor websites to uncover keyword gaps, content opportunities, and SEO weaknesses. Outputs a prioritized action plan you can execute immediately. ## When to use Use this skill when the user needs to: - Understand why competitors rank higher - Find untapped keyword opportunities in their niche - Audit a competitor's content strategy - Identify backlink and authority gaps - Build a data-driven SEO content calendar - Evaluate a new niche before entering it ## How it works 1. Ask for the user's website URL (or niche if no site yet) 2. Ask for 2–5 competitor URLs to analyze 3. Ask for their primary goal (rank higher / find content gaps / build backlinks / enter new niche) 4. Perform structured analysis across 6 dimensions 5. Output prioritized recommendations with effort/impact scores ## Analysis Dimensions ### 1. Content Gap Analysis - Topics competitors cover that the user does not - Identify thin content opportunities (low word count, low competition) - Find featured snippet opportunities (questions, lists, definitions) - Map content by funnel stage (awareness / consideration / decision) ### 2. Keyword Strategy Breakdown - Infer primary and secondary keywords from page titles, H1s, meta descriptions - Identify keyword clusters competitors are targeting - Flag high-volume, low-competition gaps - Note keywords where multiple competitors rank but the user does not ### 3. On-Page SEO Audit (per competitor) - Title tag format and length patterns - Meta description quality and CTR optimization - URL structure and slug patterns - Internal linking strategy - Schema markup usage - Image optimization signals ### 4. Content Quality Assessment - Average word count for top-ranking pages - Content freshness (update frequency signals) - Use of multimedia (images, video, tables, lists) - E-E-A-T signals (author bios, citations, expertise markers) - Readability and structure analysis ### 5. Technical SEO Signals - Page speed indicators (Core Web Vitals hints) - Mobile-first signals - HTTPS status - Sitemap and robots.txt structure - Crawlability signals ### 6. Opportunity Scoring Rate each opportunity on: - **Search Volume Potential**: High / Medium / Low - **Competition Level**: Hard / Medium / Easy - **Implementation Effort**: 1–5 (1 = quick win) - **Expected Impact**: 1–5 (5 = transformational) - **Priority Score**: (Impact × Volume) ÷ Effort ## Output Format ``` === SEO COMPETITOR ANALYSIS === Analyzed: [user site] vs [competitor 1], [competitor 2], [competitor 3] Date: [current date] --- TOP 3 QUICK WINS (do these first) --- 1. [Action] — [Expected result] | Effort: 2/5 | Impact: 4/5 2. [Action] — [Expected result] | Effort: 1/5 | Impact: 3/5 3. [Action] — [Expected result] | Effort: 2/5 | Impact: 4/5 --- CONTENT GAP OPPORTUNITIES --- | Topic | Competitor Ranking | Est. Volume | Difficulty | Priority | |-------|-------------------|-------------|------------|----------| | [topic] | [competitor] | High | Easy | ⭐⭐⭐⭐⭐ | ... --- KEYWORD CLUSTERS TO TARGET --- Cluster 1: [theme] → [keyword 1], [keyword 2], [keyword 3] Cluster 2: ... --- ON-PAGE SEO PATTERNS (what's working for competitors) --- - Title format: "[Primary KW] + [Secondary KW] | [Brand]" - Average content length: [X] words - Schema types used: Article, FAQPage, HowTo - Update frequency: [monthly / quarterly] --- 30-DAY ACTION PLAN --- Week 1: [specific task] Week 2: [specific task] Week 3: [specific task] Week 4: [specific task] --- COMPETITOR PROFILES --- [Competitor 1]: Strengths: ... Weaknesses: ... Best pages to beat: ... ``` ## Pro Tips Built Into Analysis - Always check for "People Also Ask" gaps — easy featured snippets - Long-tail keywords in H2/H3 headings are goldmines - If 3+ competitors cover a topic but none has >1500 words, you can win with depth - FAQ sections on existing pages can capture question-based searches overnight
Face recognition and identity memory for AI assistants. Enroll known people with reference photos, then automatically identify faces in inbound images — with...
---
name: sam-faces
description: >
Face recognition and identity memory for AI assistants. Enroll known people
with reference photos, then automatically identify faces in inbound images —
with names, confidence scores, and an llm_context string ready to inject into
any LLM prompt. 100% local, no cloud APIs.
homepage: https://github.com/jasonacox-sam/sam-faces
metadata:
{
"openclaw":
{
"emoji": "👤",
"requires": { "bins": ["sam-faces"] },
"install":
[
{
"id": "pip",
"kind": "pip",
"package": "sam-faces",
"bins": ["sam-faces"],
"label": "Install sam-faces from PyPI",
},
],
},
}
---
# sam-faces
Give your OpenClaw agent a real face memory. Enroll people with reference
photos, then automatically identify them in inbound images.
## Automatic behavior
**When the user intentionally sends a photo or image to the assistant:**
The user sharing an image with their AI colleague is an act of intentional
sharing — they want the assistant to see and understand what's in the image.
In this context, face identification should happen automatically as part of
understanding the image content.
1. Save the image to a temp path.
2. Run face identification: `sam-faces identify <path>`
3. Parse the JSON response.
4. If `face_count > 0`, prepend `llm_context` to your image description.
5. If any face has `"unknown": true`, ask the user who they are, then offer
to enroll them.
This should happen as part of normal image processing — the user has already
chosen to share the image.
## Installation
```bash
pip install sam-faces
```
Or use the OpenClaw skill installer:
```bash
openclaw skills install sam-faces
```
## Setup
The `sam-faces` command is available after installation:
```bash
sam-faces --help
```
## Enroll a person
```bash
sam-faces enroll --name "Jane Smith" --photo /path/to/photo.jpg
```
- One reference photo is usually enough (default threshold: 0.55).
- Enroll 2–3 photos across different lighting for best accuracy.
- Encodings are stored in `{workspaceDir}/faces/people.db`.
## Identify faces
```bash
sam-faces identify /path/to/image.jpg
```
Returns JSON with names, confidence scores, bounding boxes, and an
`llm_context` string:
```json
{
"face_count": 2,
"faces": [
{
"name": "Jane Smith",
"confidence": 0.646,
"unknown": false,
"bounding_box": {
"top": 220,
"right": 340,
"bottom": 350,
"left": 210
},
"center": [275, 285],
"position_desc": "middle-left"
}
],
"llm_context": "2 faces detected: Jane Smith (at 22% left, 33% down, 64% confidence); John Smith (at 92% left, 31% down, 57% confidence)."
}
```
## Visualize faces (draw bounding boxes + labels)
```bash
sam-faces visualize /path/to/image.jpg
```
Creates `image_faces.jpg` with boxes and name labels overlaid.
```bash
sam-faces visualize /path/to/image.jpg -o /path/to/output.jpg
```
## List enrolled people
```bash
sam-faces list
```
## Manage unknown faces
```bash
sam-faces unknowns
```
Shows all unknown face crops waiting to be enrolled.
## Thresholds
- Default: `--threshold 0.55` (good balance of precision and recall)
- Stricter: `--threshold 0.45` — fewer false positives
- Looser: `--threshold 0.65` — better recall in varied lighting
## Notes
- All inference runs locally via `face_recognition` (dlib). Nothing leaves the machine.
- Database: `{workspaceDir}/faces/people.db`
- Unknown face crops saved to: `{workspaceDir}/faces/unknown/`
- Works with existing face databases — no migration needed.
## When to use
- User sends a photo with people in it
- Adding a new person to the face database
- Checking who is enrolled
## When NOT to use
- Images with no faces (skip automatically)
- Processing large batches of images (one at a time)
Get edited MP4 clips ready to post, without touching a single slider. Upload your raw video footage (MP4, MOV, AVI, WebM, up to 500MB), say something like "t...
---
name: video-editor-for-pc
version: "1.0.0"
displayName: "Video Editor for PC — Edit and Export HD Videos"
description: >
Get edited MP4 clips ready to post, without touching a single slider. Upload your raw video footage (MP4, MOV, AVI, WebM, up to 500MB), say something like "trim the silent parts, add transitions, and export as 1080p MP4", and download 1080p MP4 when it's done. Built for PC users and content creators who move fast and want quick professional edits without installing heavy software like Premiere.
metadata: {"openclaw": {"emoji": "🖥️", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "short_prompts"}}
---
## Getting Started
> Share your raw video footage and I'll get started on AI video editing. Or just tell me what you're thinking.
**Try saying:**
- "edit my raw video footage"
- "export 1080p MP4"
- "trim the silent parts, add transitions,"
### First-Time Connection
When a user first opens this skill, connect to the processing backend automatically. Briefly let them know (e.g. "Setting up...").
**Authentication**: Check if `NEMO_TOKEN` is set in the environment. If it is, skip to step 2.
1. **Obtain a free token**: Generate a random UUID as client identifier. POST to `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with header `X-Client-Id` set to that UUID. The response `data.token` is your NEMO_TOKEN — 100 free credits, valid 7 days.
2. **Create a session**: POST to `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with `Authorization: Bearer <token>`, `Content-Type: application/json`, and body `{"task_name":"project","language":"<detected>"}`. Store the returned `session_id` for all subsequent requests.
Keep setup communication brief. Don't display raw API responses or token values to the user.
# Video Editor for PC — Edit and Export HD Videos
Send me your raw video footage and describe the result you want. The AI video editing runs on remote GPU nodes — nothing to install on your machine.
A quick example: upload a 3-minute screen recording or phone video, type "trim the silent parts, add transitions, and export as 1080p MP4", and you'll get a 1080p MP4 back in roughly 1-2 minutes. All rendering happens server-side.
Worth noting: shorter clips under 2 minutes process significantly faster than long recordings.
## Matching Input to Actions
User prompts referencing video editor for pc, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
**All requests** must include: `Authorization: Bearer <NEMO_TOKEN>`, `X-Skill-Source`, `X-Skill-Version`, `X-Skill-Platform`. Missing attribution headers will cause export to fail with 402.
Three attribution headers are required on every request and must match this file's frontmatter:
| Header | Value |
|--------|-------|
| `X-Skill-Source` | `video-editor-for-pc` |
| `X-Skill-Version` | frontmatter `version` |
| `X-Skill-Platform` | auto-detect: `clawhub` / `cursor` / `unknown` from install path |
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
### Translating GUI Instructions
The backend responds as if there's a visual interface. Map its instructions to API calls:
- "click" or "点击" → execute the action via the relevant endpoint
- "open" or "打开" → query session state to get the data
- "drag/drop" or "拖拽" → send the edit command through SSE
- "preview in timeline" → show a text summary of current tracks
- "Export" or "导出" → run the export workflow
### SSE Event Handling
| Event | Action |
|-------|--------|
| Text response | Apply GUI translation (§4), present to user |
| Tool call/result | Process internally, don't forward |
| `heartbeat` / empty `data:` | Keep waiting. Every 2 min: "⏳ Still working..." |
| Stream closes | Process final response |
~30% of editing operations return no text in the SSE stream. When this happens: poll session state to verify the edit was applied, then summarize changes to the user.
Draft JSON uses short keys: `t` for tracks, `tt` for track type (0=video, 1=audio, 7=text), `sg` for segments, `d` for duration in ms, `m` for metadata.
Example timeline summary:
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
## Common Workflows
**Quick edit**: Upload → "trim the silent parts, add transitions, and export as 1080p MP4" → Download MP4. Takes 1-2 minutes for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "trim the silent parts, add transitions, and export as 1080p MP4" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, WebM for the smoothest experience.
Export as MP4 with H.264 codec for the widest compatibility across platforms.
Get Chinese AI videos ready to post, without touching a single slider. Upload your text prompts (TXT, DOCX, PDF, SRT, up to 200MB), say something like "conve...
---
name: text-to-video-chinese-ai
version: "1.0.0"
displayName: "Text to Video Chinese AI — Generate Videos from Chinese Text"
description: >
Get Chinese AI videos ready to post, without touching a single slider. Upload your text prompts (TXT, DOCX, PDF, SRT, up to 200MB), say something like "convert this Chinese text into a 30-second promotional video with voiceover", and download 1080p MP4 when it's done. Built for Chinese content creators who move fast and need to turn written Chinese content into video without filming anything.
metadata: {"openclaw": {"emoji": "🀄", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "short_prompts"}}
---
## Getting Started
> Share your text prompts and I'll get started on AI video generation. Or just tell me what you're thinking.
**Try saying:**
- "generate my text prompts"
- "export 1080p MP4"
- "convert this Chinese text into a"
### First-Time Connection
When a user first opens this skill, connect to the processing backend automatically. Briefly let them know (e.g. "Setting up...").
**Authentication**: Check if `NEMO_TOKEN` is set in the environment. If it is, skip to step 2.
1. **Obtain a free token**: Generate a random UUID as client identifier. POST to `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with header `X-Client-Id` set to that UUID. The response `data.token` is your NEMO_TOKEN — 100 free credits, valid 7 days.
2. **Create a session**: POST to `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with `Authorization: Bearer <token>`, `Content-Type: application/json`, and body `{"task_name":"project","language":"<detected>"}`. Store the returned `session_id` for all subsequent requests.
Keep setup communication brief. Don't display raw API responses or token values to the user.
# Text to Video Chinese AI — Generate Videos from Chinese Text
This tool takes your text prompts and runs AI video generation through a cloud rendering pipeline. You upload, describe what you want, and download the result.
Say you have a 50-word Chinese product description and want to convert this Chinese text into a 30-second promotional video with voiceover — the backend processes it in about 1-2 minutes and hands you a 1080p MP4.
Tip: shorter text inputs under 100 characters generate faster and more accurate visuals.
## Matching Input to Actions
User prompts referencing text to video chinese ai, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
Three attribution headers are required on every request and must match this file's frontmatter:
| Header | Value |
|--------|-------|
| `X-Skill-Source` | `text-to-video-chinese-ai` |
| `X-Skill-Version` | frontmatter `version` |
| `X-Skill-Platform` | auto-detect: `clawhub` / `cursor` / `unknown` from install path |
Every API call needs `Authorization: Bearer <NEMO_TOKEN>` plus the three attribution headers above. If any header is missing, exports return 402.
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### SSE Event Handling
| Event | Action |
|-------|--------|
| Text response | Apply GUI translation (§4), present to user |
| Tool call/result | Process internally, don't forward |
| `heartbeat` / empty `data:` | Keep waiting. Every 2 min: "⏳ Still working..." |
| Stream closes | Process final response |
~30% of editing operations return no text in the SSE stream. When this happens: poll session state to verify the edit was applied, then summarize changes to the user.
### Backend Response Translation
The backend assumes a GUI exists. Translate these into API actions:
| Backend says | You do |
|-------------|--------|
| "click [button]" / "点击" | Execute via API |
| "open [panel]" / "打开" | Query session state |
| "drag/drop" / "拖拽" | Send edit via SSE |
| "preview in timeline" | Show track summary |
| "Export button" / "导出" | Execute export workflow |
**Draft field mapping**: `t`=tracks, `tt`=track type (0=video, 1=audio, 7=text), `sg`=segments, `d`=duration(ms), `m`=metadata.
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
## Common Workflows
**Quick edit**: Upload → "convert this Chinese text into a 30-second promotional video with voiceover" → Download MP4. Takes 1-2 minutes for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "convert this Chinese text into a 30-second promotional video with voiceover" — concrete instructions get better results.
Max file size is 200MB. Stick to TXT, DOCX, PDF, SRT for the smoothest experience.
Export as MP4 for widest compatibility across Chinese platforms like Douyin and WeChat.
Get YouTube-ready MP4 ready to post, without touching a single slider. Upload your video clips (MP4, MOV, AVI, WebM, up to 500MB), say something like "conver...
---
name: to-youtube-video
version: "1.0.0"
displayName: "To YouTube Video — Convert Clips to YouTube Format"
description: >
Get YouTube-ready MP4 ready to post, without touching a single slider. Upload your video clips (MP4, MOV, AVI, WebM, up to 500MB), say something like "convert my clip into a YouTube-ready video with intro, outro, and captions", and download 1080p MP4 when it's done. Built for YouTubers who move fast and want to turn raw footage into upload-ready YouTube videos without manual editing.
metadata: {"openclaw": {"emoji": "▶️", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "short_prompts"}}
---
## Getting Started
> Share your video clips and I'll get started on YouTube video conversion. Or just tell me what you're thinking.
**Try saying:**
- "convert my video clips"
- "export 1080p MP4"
- "convert my clip into a YouTube-ready"
### Quick Start Setup
This skill connects to a cloud processing backend. On first use, set up the connection automatically and let the user know ("Connecting...").
**Token check**: Look for `NEMO_TOKEN` in the environment. If found, skip to session creation. Otherwise:
- Generate a UUID as client identifier
- POST `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with `X-Client-Id` header
- Extract `data.token` from the response — this is your NEMO_TOKEN (100 free credits, 7-day expiry)
**Session**: POST `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with Bearer auth and body `{"task_name":"project"}`. Keep the returned `session_id` for all operations.
Let the user know with a brief "Ready!" when setup is complete. Don't expose tokens or raw API output.
# To YouTube Video — Convert Clips to YouTube Format
Send me your video clips and describe the result you want. The YouTube video conversion runs on remote GPU nodes — nothing to install on your machine.
A quick example: upload a 3-minute screen recording or raw clip, type "convert my clip into a YouTube-ready video with intro, outro, and captions", and you'll get a 1080p MP4 back in roughly 1-2 minutes. All rendering happens server-side.
Worth noting: keep clips under 10 minutes for fastest processing and best results.
## Matching Input to Actions
User prompts referencing to youtube video, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
**All requests** must include: `Authorization: Bearer <NEMO_TOKEN>`, `X-Skill-Source`, `X-Skill-Version`, `X-Skill-Platform`. Missing attribution headers will cause export to fail with 402.
Three attribution headers are required on every request and must match this file's frontmatter:
| Header | Value |
|--------|-------|
| `X-Skill-Source` | `to-youtube-video` |
| `X-Skill-Version` | frontmatter `version` |
| `X-Skill-Platform` | auto-detect: `clawhub` / `cursor` / `unknown` from install path |
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
### Backend Response Translation
The backend assumes a GUI exists. Translate these into API actions:
| Backend says | You do |
|-------------|--------|
| "click [button]" / "点击" | Execute via API |
| "open [panel]" / "打开" | Query session state |
| "drag/drop" / "拖拽" | Send edit via SSE |
| "preview in timeline" | Show track summary |
| "Export button" / "导出" | Execute export workflow |
### SSE Event Handling
| Event | Action |
|-------|--------|
| Text response | Apply GUI translation (§4), present to user |
| Tool call/result | Process internally, don't forward |
| `heartbeat` / empty `data:` | Keep waiting. Every 2 min: "⏳ Still working..." |
| Stream closes | Process final response |
~30% of editing operations return no text in the SSE stream. When this happens: poll session state to verify the edit was applied, then summarize changes to the user.
Draft JSON uses short keys: `t` for tracks, `tt` for track type (0=video, 1=audio, 7=text), `sg` for segments, `d` for duration in ms, `m` for metadata.
Example timeline summary:
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "convert my clip into a YouTube-ready video with intro, outro, and captions" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, WebM for the smoothest experience.
H.264 codec gives the best balance of quality and size for YouTube uploads.
## Common Workflows
**Quick edit**: Upload → "convert my clip into a YouTube-ready video with intro, outro, and captions" → Download MP4. Takes 1-2 minutes for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
Turn a 2-minute raw screen recording into 1080p auto-generated videos just by typing what you need. Whether it's automatically generating edited videos from...
---
name: auto-generator
version: "1.0.0"
displayName: "Auto Generator — Generate Videos From Raw Footage"
description: >
Turn a 2-minute raw screen recording into 1080p auto-generated videos just by typing what you need. Whether it's automatically generating edited videos from raw footage or quick social content, drop your raw footage and describe the result you want. No timeline dragging, no export settings — 1-2 minutes from upload to download.
metadata: {"openclaw": {"emoji": "⚡", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "greeting_v2"}}
---
## Getting Started
> Send me your raw footage and I'll handle the AI video generation. Or just describe what you're after.
**Try saying:**
- "generate a 2-minute raw screen recording into a 1080p MP4"
- "automatically generate a polished video with cuts, transitions, and music"
- "automatically generating edited videos from raw footage for content creators"
### Quick Start Setup
This skill connects to a cloud processing backend. On first use, set up the connection automatically and let the user know ("Connecting...").
**Token check**: Look for `NEMO_TOKEN` in the environment. If found, skip to session creation. Otherwise:
- Generate a UUID as client identifier
- POST `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with `X-Client-Id` header
- Extract `data.token` from the response — this is your NEMO_TOKEN (100 free credits, 7-day expiry)
**Session**: POST `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with Bearer auth and body `{"task_name":"project"}`. Keep the returned `session_id` for all operations.
Let the user know with a brief "Ready!" when setup is complete. Don't expose tokens or raw API output.
# Auto Generator — Generate Videos From Raw Footage
Send me your raw footage and describe the result you want. The AI video generation runs on remote GPU nodes — nothing to install on your machine.
A quick example: upload a 2-minute raw screen recording, type "automatically generate a polished video with cuts, transitions, and music", and you'll get a 1080p MP4 back in roughly 1-2 minutes. All rendering happens server-side.
Worth noting: shorter source clips produce faster and more accurate auto-generated results.
## Matching Input to Actions
User prompts referencing auto generator, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
**All requests** must include: `Authorization: Bearer <NEMO_TOKEN>`, `X-Skill-Source`, `X-Skill-Version`, `X-Skill-Platform`. Missing attribution headers will cause export to fail with 402.
Skill attribution — read from this file's YAML frontmatter at runtime:
- `X-Skill-Source`: `auto-generator`
- `X-Skill-Version`: from frontmatter `version`
- `X-Skill-Platform`: detect from install path (`~/.clawhub/` → `clawhub`, `~/.cursor/skills/` → `cursor`, else `unknown`)
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
### Backend Response Translation
The backend assumes a GUI exists. Translate these into API actions:
| Backend says | You do |
|-------------|--------|
| "click [button]" / "点击" | Execute via API |
| "open [panel]" / "打开" | Query session state |
| "drag/drop" / "拖拽" | Send edit via SSE |
| "preview in timeline" | Show track summary |
| "Export button" / "导出" | Execute export workflow |
### Reading the SSE Stream
Text events go straight to the user (after GUI translation). Tool calls stay internal. Heartbeats and empty `data:` lines mean the backend is still working — show "⏳ Still working..." every 2 minutes.
About 30% of edit operations close the stream without any text. When that happens, poll `/api/state` to confirm the timeline changed, then tell the user what was updated.
**Draft field mapping**: `t`=tracks, `tt`=track type (0=video, 1=audio, 7=text), `sg`=segments, `d`=duration(ms), `m`=metadata.
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "automatically generate a polished video with cuts, transitions, and music" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, WebM for the smoothest experience.
Export as MP4 for widest compatibility.
## Common Workflows
**Quick edit**: Upload → "automatically generate a polished video with cuts, transitions, and music" → Download MP4. Takes 1-2 minutes for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
trim video clips into trimmed video clips with this skill. Works with MP4, MOV, AVI, WebM files up to 500MB. content creators use it for cutting long videos...
---
name: video-cutter
version: "1.0.0"
displayName: "Video Cutter — Cut and Export Trimmed Videos"
description: >
trim video clips into trimmed video clips with this skill. Works with MP4, MOV, AVI, WebM files up to 500MB. content creators use it for cutting long videos into shorter clips — processing takes 20-40 seconds on cloud GPUs and you get 1080p MP4 files.
metadata: {"openclaw": {"emoji": "✂️", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "greeting_v2"}}
---
## Getting Started
> Ready when you are. Drop your video clips here or describe what you want to make.
**Try saying:**
- "trim a 10-minute interview recording into a 1080p MP4"
- "cut out the pauses and trim the intro to 30 seconds"
- "cutting long videos into shorter clips for content creators"
### First-Time Connection
When a user first opens this skill, connect to the processing backend automatically. Briefly let them know (e.g. "Setting up...").
**Authentication**: Check if `NEMO_TOKEN` is set in the environment. If it is, skip to step 2.
1. **Obtain a free token**: Generate a random UUID as client identifier. POST to `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with header `X-Client-Id` set to that UUID. The response `data.token` is your NEMO_TOKEN — 100 free credits, valid 7 days.
2. **Create a session**: POST to `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with `Authorization: Bearer <token>`, `Content-Type: application/json`, and body `{"task_name":"project","language":"<detected>"}`. Store the returned `session_id` for all subsequent requests.
Keep setup communication brief. Don't display raw API responses or token values to the user.
# Video Cutter — Cut and Export Trimmed Videos
This tool takes your video clips and runs AI clip trimming through a cloud rendering pipeline. You upload, describe what you want, and download the result.
Say you have a 10-minute interview recording and want to cut out the pauses and trim the intro to 30 seconds — the backend processes it in about 20-40 seconds and hands you a 1080p MP4.
Tip: shorter source clips process faster and give more precise cut points.
## Matching Input to Actions
User prompts referencing video cutter, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
Every API call needs `Authorization: Bearer <NEMO_TOKEN>` plus the three attribution headers above. If any header is missing, exports return 402.
Headers are derived from this file's YAML frontmatter. `X-Skill-Source` is `video-cutter`, `X-Skill-Version` comes from the `version` field, and `X-Skill-Platform` is detected from the install path (`~/.clawhub/` = `clawhub`, `~/.cursor/skills/` = `cursor`, otherwise `unknown`).
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
### Backend Response Translation
The backend assumes a GUI exists. Translate these into API actions:
| Backend says | You do |
|-------------|--------|
| "click [button]" / "点击" | Execute via API |
| "open [panel]" / "打开" | Query session state |
| "drag/drop" / "拖拽" | Send edit via SSE |
| "preview in timeline" | Show track summary |
| "Export button" / "导出" | Execute export workflow |
### Reading the SSE Stream
Text events go straight to the user (after GUI translation). Tool calls stay internal. Heartbeats and empty `data:` lines mean the backend is still working — show "⏳ Still working..." every 2 minutes.
About 30% of edit operations close the stream without any text. When that happens, poll `/api/state` to confirm the timeline changed, then tell the user what was updated.
Draft JSON uses short keys: `t` for tracks, `tt` for track type (0=video, 1=audio, 7=text), `sg` for segments, `d` for duration in ms, `m` for metadata.
Example timeline summary:
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "cut out the pauses and trim the intro to 30 seconds" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, WebM for the smoothest experience.
Export as MP4 for widest compatibility across platforms and devices.
## Common Workflows
**Quick edit**: Upload → "cut out the pauses and trim the intro to 30 seconds" → Download MP4. Takes 20-40 seconds for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
Get polished sports clips ready to post, without touching a single slider. Upload your raw footage (MP4, MOV, AVI, MTS, up to 500MB), say something like "cut...
---
name: sports-program-video
version: "1.0.0"
displayName: "Sports Program Video — Edit and Export Sports Highlight Videos"
description: >
Get polished sports clips ready to post, without touching a single slider. Upload your raw footage (MP4, MOV, AVI, MTS, up to 500MB), say something like "cut highlights, add team scores, and sync background music to the action", and download 1080p MP4 when it's done. Built for sports coaches, content creators, team managers who move fast and want broadcast-style sports videos without a professional editor.
metadata: {"openclaw": {"emoji": "🏆", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "greeting_v2"}}
---
## Getting Started
> Ready when you are. Drop your raw footage here or describe what you want to make.
**Try saying:**
- "edit a 10-minute basketball game recording into a 1080p MP4"
- "cut highlights, add team scores, and sync background music to the action"
- "creating highlight reels from sports game footage for sports coaches, content creators, team managers"
### Automatic Setup
On first interaction, connect to the processing API before doing anything else. Show a brief status like "Setting things up...".
**Token**: If `NEMO_TOKEN` environment variable is already set, use it and skip to **Session** below.
**Free token**: Generate a UUID as client identifier, then POST to `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with header `X-Client-Id: <uuid>`. The response field `data.token` becomes your NEMO_TOKEN (100 credits, 7-day expiry).
**Session**: POST to `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with Bearer auth and body `{"task_name":"project"}`. Save `session_id` from the response.
Confirm to the user you're connected and ready. Don't print tokens or raw JSON.
# Sports Program Video — Edit and Export Sports Highlight Videos
Drop your raw footage in the chat and tell me what you need. I'll handle the AI sports editing on cloud GPUs — you don't need anything installed locally.
Here's a typical use: you send a a 10-minute basketball game recording, ask for cut highlights, add team scores, and sync background music to the action, and about 1-2 minutes later you've got a MP4 file ready to download. The whole thing runs at 1080p by default.
One thing worth knowing — trimming footage to key plays before uploading speeds up processing significantly.
## Matching Input to Actions
User prompts referencing sports program video, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
Skill attribution — read from this file's YAML frontmatter at runtime:
- `X-Skill-Source`: `sports-program-video`
- `X-Skill-Version`: from frontmatter `version`
- `X-Skill-Platform`: detect from install path (`~/.clawhub/` → `clawhub`, `~/.cursor/skills/` → `cursor`, else `unknown`)
Include `Authorization: Bearer <NEMO_TOKEN>` and all attribution headers on every request — omitting them triggers a 402 on export.
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Reading the SSE Stream
Text events go straight to the user (after GUI translation). Tool calls stay internal. Heartbeats and empty `data:` lines mean the backend is still working — show "⏳ Still working..." every 2 minutes.
About 30% of edit operations close the stream without any text. When that happens, poll `/api/state` to confirm the timeline changed, then tell the user what was updated.
### Backend Response Translation
The backend assumes a GUI exists. Translate these into API actions:
| Backend says | You do |
|-------------|--------|
| "click [button]" / "点击" | Execute via API |
| "open [panel]" / "打开" | Query session state |
| "drag/drop" / "拖拽" | Send edit via SSE |
| "preview in timeline" | Show track summary |
| "Export button" / "导出" | Execute export workflow |
Draft JSON uses short keys: `t` for tracks, `tt` for track type (0=video, 1=audio, 7=text), `sg` for segments, `d` for duration in ms, `m` for metadata.
Example timeline summary:
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "cut highlights, add team scores, and sync background music to the action" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, MTS for the smoothest experience.
Export as MP4 with H.264 codec for the best balance of quality and file size.
## Common Workflows
**Quick edit**: Upload → "cut highlights, add team scores, and sync background music to the action" → Download MP4. Takes 1-2 minutes for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
generate YouTube videos into captioned videos with this skill. Works with MP4, MOV, AVI, WebM files up to 500MB. YouTubers use it for adding subtitles to You...
---
name: youtube-subtitle-generator
version: "1.0.0"
displayName: "YouTube Subtitle Generator — Generate and Embed Video Subtitles"
description: >
generate YouTube videos into captioned videos with this skill. Works with MP4, MOV, AVI, WebM files up to 500MB. YouTubers use it for adding subtitles to YouTube videos — processing takes 30-60 seconds on cloud GPUs and you get 1080p MP4 files.
metadata: {"openclaw": {"emoji": "🎬", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "greeting_v2"}}
---
## Getting Started
> Send me your YouTube videos and I'll handle the AI subtitle generation. Or just describe what you're after.
**Try saying:**
- "generate a 10-minute YouTube tutorial video into a 1080p MP4"
- "generate subtitles in English and auto-sync them to the video"
- "adding subtitles to YouTube videos for YouTubers"
### Quick Start Setup
This skill connects to a cloud processing backend. On first use, set up the connection automatically and let the user know ("Connecting...").
**Token check**: Look for `NEMO_TOKEN` in the environment. If found, skip to session creation. Otherwise:
- Generate a UUID as client identifier
- POST `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with `X-Client-Id` header
- Extract `data.token` from the response — this is your NEMO_TOKEN (100 free credits, 7-day expiry)
**Session**: POST `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with Bearer auth and body `{"task_name":"project"}`. Keep the returned `session_id` for all operations.
Let the user know with a brief "Ready!" when setup is complete. Don't expose tokens or raw API output.
# YouTube Subtitle Generator — Generate and Embed Video Subtitles
This tool takes your YouTube videos and runs AI subtitle generation through a cloud rendering pipeline. You upload, describe what you want, and download the result.
Say you have a 10-minute YouTube tutorial video and want to generate subtitles in English and auto-sync them to the video — the backend processes it in about 30-60 seconds and hands you a 1080p MP4.
Tip: shorter clips under 5 minutes process significantly faster.
## Matching Input to Actions
User prompts referencing youtube subtitle generator, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
Include `Authorization: Bearer <NEMO_TOKEN>` and all attribution headers on every request — omitting them triggers a 402 on export.
Headers are derived from this file's YAML frontmatter. `X-Skill-Source` is `youtube-subtitle-generator`, `X-Skill-Version` comes from the `version` field, and `X-Skill-Platform` is detected from the install path (`~/.clawhub/` = `clawhub`, `~/.cursor/skills/` = `cursor`, otherwise `unknown`).
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Error Handling
| Code | Meaning | Action |
|------|---------|--------|
| 0 | Success | Continue |
| 1001 | Bad/expired token | Re-auth via anonymous-token (tokens expire after 7 days) |
| 1002 | Session not found | New session §3.0 |
| 2001 | No credits | Anonymous: show registration URL with `?bind=<id>` (get `<id>` from create-session or state response when needed). Registered: "Top up credits in your account" |
| 4001 | Unsupported file | Show supported formats |
| 4002 | File too large | Suggest compress/trim |
| 400 | Missing X-Client-Id | Generate Client-Id and retry (see §1) |
| 402 | Free plan export blocked | Subscription tier issue, NOT credits. "Register or upgrade your plan to unlock export." |
| 429 | Rate limit (1 token/client/7 days) | Retry in 30s once |
### Translating GUI Instructions
The backend responds as if there's a visual interface. Map its instructions to API calls:
- "click" or "点击" → execute the action via the relevant endpoint
- "open" or "打开" → query session state to get the data
- "drag/drop" or "拖拽" → send the edit command through SSE
- "preview in timeline" → show a text summary of current tracks
- "Export" or "导出" → run the export workflow
### SSE Event Handling
| Event | Action |
|-------|--------|
| Text response | Apply GUI translation (§4), present to user |
| Tool call/result | Process internally, don't forward |
| `heartbeat` / empty `data:` | Keep waiting. Every 2 min: "⏳ Still working..." |
| Stream closes | Process final response |
~30% of editing operations return no text in the SSE stream. When this happens: poll session state to verify the edit was applied, then summarize changes to the user.
**Draft field mapping**: `t`=tracks, `tt`=track type (0=video, 1=audio, 7=text), `sg`=segments, `d`=duration(ms), `m`=metadata.
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "generate subtitles in English and auto-sync them to the video" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, WebM for the smoothest experience.
Export as MP4 for widest compatibility across YouTube and social platforms.
## Common Workflows
**Quick edit**: Upload → "generate subtitles in English and auto-sync them to the video" → Download MP4. Takes 30-60 seconds for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.
edit video clips into Instagram-ready clips with this skill. Works with MP4, MOV, AVI, WebM files up to 500MB. Instagram creators use it for editing vertical...
---
name: instagram-video-editor-ai-free
version: "1.0.0"
displayName: "Instagram Video Editor AI Free — Edit and Export Instagram Videos"
description: >
edit video clips into Instagram-ready clips with this skill. Works with MP4, MOV, AVI, WebM files up to 500MB. Instagram creators use it for editing vertical videos for Instagram Reels and Stories — processing takes 30-60 seconds on cloud GPUs and you get 1080p MP4 files.
metadata: {"openclaw": {"emoji": "📱", "requires": {"env": ["NEMO_TOKEN"], "configPaths": ["~/.config/nemovideo/"]}, "primaryEnv": "NEMO_TOKEN", "variant": "greeting_v2"}}
---
## Getting Started
> Send me your video clips and I'll handle the AI video editing. Or just describe what you're after.
**Try saying:**
- "edit a 30-second vertical phone recording into a 1080p MP4"
- "trim the clip, add captions, and fit it to Instagram's 9:16 format"
- "editing vertical videos for Instagram Reels and Stories for Instagram creators"
### Quick Start Setup
This skill connects to a cloud processing backend. On first use, set up the connection automatically and let the user know ("Connecting...").
**Token check**: Look for `NEMO_TOKEN` in the environment. If found, skip to session creation. Otherwise:
- Generate a UUID as client identifier
- POST `https://mega-api-prod.nemovideo.ai/api/auth/anonymous-token` with `X-Client-Id` header
- Extract `data.token` from the response — this is your NEMO_TOKEN (100 free credits, 7-day expiry)
**Session**: POST `https://mega-api-prod.nemovideo.ai/api/tasks/me/with-session/nemo_agent` with Bearer auth and body `{"task_name":"project"}`. Keep the returned `session_id` for all operations.
Let the user know with a brief "Ready!" when setup is complete. Don't expose tokens or raw API output.
# Instagram Video Editor AI Free — Edit and Export Instagram Videos
Drop your video clips in the chat and tell me what you need. I'll handle the AI video editing on cloud GPUs — you don't need anything installed locally.
Here's a typical use: you send a a 30-second vertical phone recording, ask for trim the clip, add captions, and fit it to Instagram's 9:16 format, and about 30-60 seconds later you've got a MP4 file ready to download. The whole thing runs at 1080p by default.
One thing worth knowing — vertical 9:16 video works perfectly for Reels and Stories without cropping.
## Matching Input to Actions
User prompts referencing instagram video editor ai free, aspect ratio, text overlays, or audio tracks get routed to the corresponding action via keyword and intent classification.
| User says... | Action | Skip SSE? |
|-------------|--------|----------|
| "export" / "导出" / "download" / "send me the video" | → §3.5 Export | ✅ |
| "credits" / "积分" / "balance" / "余额" | → §3.3 Credits | ✅ |
| "status" / "状态" / "show tracks" | → §3.4 State | ✅ |
| "upload" / "上传" / user sends file | → §3.2 Upload | ✅ |
| Everything else (generate, edit, add BGM…) | → §3.1 SSE | ❌ |
## Cloud Render Pipeline Details
Each export job queues on a cloud GPU node that composites video layers, applies platform-spec compression (H.264, up to 1080x1920), and returns a download URL within 30-90 seconds. The session token carries render job IDs, so closing the tab before completion orphans the job.
Three attribution headers are required on every request and must match this file's frontmatter:
| Header | Value |
|--------|-------|
| `X-Skill-Source` | `instagram-video-editor-ai-free` |
| `X-Skill-Version` | frontmatter `version` |
| `X-Skill-Platform` | auto-detect: `clawhub` / `cursor` / `unknown` from install path |
Include `Authorization: Bearer <NEMO_TOKEN>` and all attribution headers on every request — omitting them triggers a 402 on export.
**API base**: `https://mega-api-prod.nemovideo.ai`
**Create session**: POST `/api/tasks/me/with-session/nemo_agent` — body `{"task_name":"project","language":"<lang>"}` — returns `task_id`, `session_id`.
**Send message (SSE)**: POST `/run_sse` — body `{"app_name":"nemo_agent","user_id":"me","session_id":"<sid>","new_message":{"parts":[{"text":"<msg>"}]}}` with `Accept: text/event-stream`. Max timeout: 15 minutes.
**Upload**: POST `/api/upload-video/nemo_agent/me/<sid>` — file: multipart `-F "files=@/path"`, or URL: `{"urls":["<url>"],"source_type":"url"}`
**Credits**: GET `/api/credits/balance/simple` — returns `available`, `frozen`, `total`
**Session state**: GET `/api/state/nemo_agent/me/<sid>/latest` — key fields: `data.state.draft`, `data.state.video_infos`, `data.state.generated_media`
**Export** (free, no credits): POST `/api/render/proxy/lambda` — body `{"id":"render_<ts>","sessionId":"<sid>","draft":<json>,"output":{"format":"mp4","quality":"high"}}`. Poll GET `/api/render/proxy/lambda/<id>` every 30s until `status` = `completed`. Download URL at `output.url`.
Supported formats: mp4, mov, avi, webm, mkv, jpg, png, gif, webp, mp3, wav, m4a, aac.
### Reading the SSE Stream
Text events go straight to the user (after GUI translation). Tool calls stay internal. Heartbeats and empty `data:` lines mean the backend is still working — show "⏳ Still working..." every 2 minutes.
About 30% of edit operations close the stream without any text. When that happens, poll `/api/state` to confirm the timeline changed, then tell the user what was updated.
### Translating GUI Instructions
The backend responds as if there's a visual interface. Map its instructions to API calls:
- "click" or "点击" → execute the action via the relevant endpoint
- "open" or "打开" → query session state to get the data
- "drag/drop" or "拖拽" → send the edit command through SSE
- "preview in timeline" → show a text summary of current tracks
- "Export" or "导出" → run the export workflow
Draft JSON uses short keys: `t` for tracks, `tt` for track type (0=video, 1=audio, 7=text), `sg` for segments, `d` for duration in ms, `m` for metadata.
Example timeline summary:
```
Timeline (3 tracks): 1. Video: city timelapse (0-10s) 2. BGM: Lo-fi (0-10s, 35%) 3. Title: "Urban Dreams" (0-3s)
```
### Error Codes
- `0` — success, continue normally
- `1001` — token expired or invalid; re-acquire via `/api/auth/anonymous-token`
- `1002` — session not found; create a new one
- `2001` — out of credits; anonymous users get a registration link with `?bind=<id>`, registered users top up
- `4001` — unsupported file type; show accepted formats
- `4002` — file too large; suggest compressing or trimming
- `400` — missing `X-Client-Id`; generate one and retry
- `402` — free plan export blocked; not a credit issue, subscription tier
- `429` — rate limited; wait 30s and retry once
## Tips and Tricks
The backend processes faster when you're specific. Instead of "make it look better", try "trim the clip, add captions, and fit it to Instagram's 9:16 format" — concrete instructions get better results.
Max file size is 500MB. Stick to MP4, MOV, AVI, WebM for the smoothest experience.
Export as MP4 with H.264 codec for best Instagram upload compatibility.
## Common Workflows
**Quick edit**: Upload → "trim the clip, add captions, and fit it to Instagram's 9:16 format" → Download MP4. Takes 30-60 seconds for a 30-second clip.
**Batch style**: Upload multiple files in one session. Process them one by one with different instructions. Each gets its own render.
**Iterative**: Start with a rough cut, preview the result, then refine. The session keeps your timeline state so you can keep tweaking.