
Libtv Skill
- 4.3k installs
- 951 repo stars
- Updated March 18, 2026
- libtv-labs/libtv-skills
libtv-skill is an agent skill: agent-im 会话技能 - 通过 liblib.tv 的 AI 能力生成和编辑图片/视频。覆盖场景包括:生成(文生图、文生视频、图生视频、做动画、画一个xxx、来段xxx)、编辑修改(把xxx换成yyy、去掉xxx、加上xxx、改成xxx、调整xxx、局部修改、改镜头)、风格
About
The libtv-skill drives LibTV agent-im sessions for AI image and video generation and editing through natural language on im.liblib.tv. Capabilities include text-to-image, text-to-video, image-to-video, local edits, style transfer, short drama generation, MV creation, and product ad workflows using models such as Seedance, Kling, Wan, NanoBanana, Midjourney, and Seedream. Scripts create sessions, send messages, poll query_session for progress, upload reference media under 200MB, switch projects, and download finished assets. Requires LIBTV_ACCESS_KEY and uses Python standard library only against the OpenAPI IM base URL. Complex jobs like short dramas or MVs need patient polling because orchestration spans multiple generation steps. Trigger when users mention liblib, libtv, AI video edits, reference uploads, or any generative media request in Chinese or English phrasing. Upload reference files before edit workflows and use incremental query_session with after-seq for long-running generations in agent-driven media pipelines.
- Covers libtv-skill quick start, workflow steps, and reference pointers from SKILL.md.
- Tagged for stage build and subphase integrations in the closed Skillselion taxonomy.
- Documents prerequisites, permissions network, shell, filesystem, and compatible agents.
- Includes AEO tagMeta with task queries, keywords, and evidence quotes for discovery.
- Cross-links related skills and generated REFERENCE.md tables where the repo provides them.
Libtv Skill by the numbers
- 4,261 all-time installs (skills.sh)
- +50 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #118 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
libtv-skill capabilities & compatibility
- Capabilities
- libtv skill documented workflow · quick start examples · reference parameter lookup · taxonomy aligned metadata · aeo discovery fields
- Works with
- openai
- Use cases
- image generation · video generation
What libtv-skill says it does
agent-im 会话技能 - 通过 liblib.tv 的 AI 能力生成和编辑图片/视频。覆盖场景包括:生成(文生图、文生视频、图生视频、做动画、画一个xxx、来段xxx)、编辑修改(把xxx换成
npx skills add https://github.com/libtv-labs/libtv-skills --skill libtv-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.3k |
|---|---|
| repo stars | ★ 951 |
| Security audit | 1 / 3 scanners passed |
| Last updated | March 18, 2026 |
| Repository | libtv-labs/libtv-skills ↗ |
How do I run libtv-skill correctly without guessing steps, tools, or parameters?
agent-im 会话技能 - 通过 liblib.tv 的 AI 能力生成和编辑图片/视频。覆盖场景包括:生成(文生图、文生视频、图生视频、做动画、画一个xxx、来段xxx)、编辑修改(把xxx换成yyy、去掉xxx、加上xxx、改成xxx、调整xxx、局部修改、改镜头)、风格转换(风格迁移、转绘、换风格)、视频续写延长、复刻视频/TVC/宣传片、短剧/短漫剧生成、音乐MV生成、产品广
Who is it for?
Teams using libtv-skill when SKILL.md triggers match the user request.
Skip if: Skip when the task is outside libtv-skill documented triggers or sibling skill scope.
When should I use this skill?
User mentions libtv-skill, related trigger phrases, or asks to follow this SKILL.md workflow.
What you get
Completed libtv-skill workflow with outputs and checks defined in SKILL.md.
- libtv-skill output per SKILL.md
By the numbers
- Stage build/integrations
- Category Generative Media
- Complexity intermediate
Files
agent-im 会话(生图 / 生视频)
通过 agent-im 的 OpenAPI 创建会话、发送消息(生图、生视频、编辑视频等)、上传图片/视频文件,并查询会话消息进展。
LibTV 是 LiblibAI 推出的 AI 视频创作平台,同时为人类创作者和 Agent 设计。Agent 通过 Skill 入口理解任务、调用模型并自动编排工作流。
平台核心能力:
- 生成:文生图、文生视频、图生视频、视频续写
- 编辑:局部修改、元素替换、镜头调整、风格迁移
- 复杂创作:一句话生成完整短剧(剧本→分镜→成片)、复刻已有视频风格做 TVC/宣传片、用音乐生成 MV、产品展示片制作
- 模型:Seedance 2.0、Kling 3.0/O3、Wan 2.6、NanoBanana、Midjourney、Seedream 5.0 等顶级模型
用户的所有创作和编辑需求都通过发送自然语言消息来完成,Agent 会自主编排工作流。复杂任务(短剧、MV)耗时较长,需耐心轮询。
功能
1. 创建会话 / 发消息 - 创建新会话或向已有会话发送一条消息(如「生一个动漫视频」「把纸船换成爱心」) 2. 查询会话进展 - 根据 sessionId 拉取该会话的消息列表,用于轮询生图/生视频结果 3. 切换项目 - 将当前 accessKey 绑定的项目切换到新项目,后续 create_session 将使用新 projectUuid 4. 上传文件 - 上传图片或视频文件到 OSS,返回可访问的 OSS 地址(编辑已有视频/图片时需要先上传) 5. 下载结果 - 将会话中生成的图片/视频批量下载到本地,自动提取 URL 并命名
前置要求
export LIBTV_ACCESS_KEY="your-access-key"可选:OPENAPI_IM_BASE 或 IM_BASE_URL,默认 https://im.liblib.tv。
无需安装额外依赖,仅使用 Python 标准库。
使用方法
1. 创建会话 / 发送消息
# 创建新会话并发送「生一个动漫视频」
python3 {baseDir}/scripts/create_session.py "生一个动漫视频"
# 向已有会话发送消息
python3 {baseDir}/scripts/create_session.py "再生成一张风景图" --session-id SESSION_ID
# 只创建/绑定会话,不发消息
python3 {baseDir}/scripts/create_session.py2. 查询会话进展
# 查询会话消息列表
python3 {baseDir}/scripts/query_session.py SESSION_ID
# 增量拉取(只返回 seq 大于 N 的消息)
python3 {baseDir}/scripts/query_session.py SESSION_ID --after-seq 5
# 附带项目地址(传入 create_session 返回的 projectUuid,结果中带 projectUrl)
python3 {baseDir}/scripts/query_session.py SESSION_ID --project-id PROJECT_UUID3. 切换项目
# 切换当前 accessKey 绑定的项目(后续创建会话将使用新项目)
python3 {baseDir}/scripts/change_project.py4. 上传文件
当用户提供了参考的文件地址时,进行上传,仅支持图片、视频,文件大小必须在200M以下。
# 上传图片
python3 {baseDir}/scripts/upload_file.py /path/to/image.png
# 上传视频
python3 {baseDir}/scripts/upload_file.py /path/to/video.mp45. 下载结果
生成完成后,可以将会话中的所有图片/视频批量下载到本地。
# 从会话自动提取并下载所有结果
python3 {baseDir}/scripts/download_results.py SESSION_ID
# 指定输出目录
python3 {baseDir}/scripts/download_results.py SESSION_ID --output-dir ~/Desktop/my_project
# 指定文件名前缀(如 storyboard_01.png, storyboard_02.png ...)
python3 {baseDir}/scripts/download_results.py SESSION_ID --prefix "storyboard"
# 直接下载指定 URL 列表(不需要 session_id)
python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir ./output典型工作流
理解这些工作流,才能正确组合上面的脚本完成用户需求。
场景 1:用户要求生成图片/视频(最常见)
1. create_session.py "用户的描述" → 拿到 sessionId + projectUuid
2. 每隔 8 秒调用 query_session.py SESSION_ID --after-seq 0 轮询
3. 检查 messages:当出现 assistant 角色的消息且包含图片/视频 URL → 任务完成
4. 自动下载:download_results.py SESSION_ID --output-dir ~/Downloads/项目名 --prefix 有意义的前缀
5. 向用户展示:本地文件列表 + projectUrl(画布链接)生成完成后自动执行下载,不需要用户额外请求。下载目录和前缀根据任务语义自动命名(如分镜用 storyboard,角色设定用 character 等)。
场景 2:用户提供图片/视频要求编辑修改(如"把纸船换成爱心")
1. upload_file.py /path/to/video.mp4 → 拿到 OSS URL
2. create_session.py "把四周的纸船都换成白色的纸爱心 参考视频:{oss_url}"
3. 后续同场景 1 的步骤 2-5用户给了文件路径 + 编辑指令 = 先上传文件,再把编辑指令和 OSS URL 一起发送。
场景 3:用户提供参考图/视频要求生成新内容
1. upload_file.py /path/to/ref.png → 拿到 OSS URL
2. create_session.py "根据参考图生成xxx,参考图:{oss_url}"
3. 后续同场景 1 的步骤 2-5场景 4:在已有会话中追加新需求
1. create_session.py "新的描述" --session-id SESSION_ID
2. 后续同场景 1 的步骤 2-5轮询策略
- 间隔:每 8 秒查询一次
- 增量拉取:首次用
--after-seq 0,后续用上次拿到的最大 seq 值 - 完成判断:messages 中出现 assistant 消息且 content 包含结果 URL(图片/视频地址)
- 超时:连续轮询 3 分钟仍无结果,告知用户"生成时间较长,可稍后通过项目画布链接查看",不再继续轮询
- 错误重试:单次查询失败可重试 1 次,连续 3 次失败则停止并告知用户
输出格式
create_session 返回:
{
"projectUuid": "aa3ba04c5044477cb7a00a9e5bf3b4d0",
"sessionId": "90f05e0c-...",
"projectUrl": "https://www.liblib.tv/canvas?projectId=aa3ba04c5044477cb7a00a9e5bf3b4d0"
}query_session 返回:
{
"messages": [
{"id": "msg-xxx", "role": "user", "content": "生一个动漫视频"},
{"id": "msg-yyy", "role": "assistant", "content": "..."}
],
"projectUrl": "https://www.liblib.tv/canvas?projectId=..."
}(projectUrl 仅在传入 --project-id 时存在)
change_project 返回:
{
"projectUuid": "新项目UUID",
"projectUrl": "https://www.liblib.tv/canvas?projectId=新项目UUID"
}upload_file 返回:
{
"url": "https://libtv-res.liblib.art/claw/{projectUuid}/{uuid}.png"
}download_results 返回:
{
"output_dir": "/Users/xxx/Downloads/libtv_results",
"downloaded": ["/Users/xxx/Downloads/libtv_results/01.png", "..."],
"total": 9
}最终向用户展示时(OpenClaw)
- 视频地址:来自
query_session返回的messages中 assistant 消息的 content 或结果里的视频/图片 URL,即「返回的结果」。 - 项目地址:使用
create_session返回的projectUrl,或自行拼接https://www.liblib.tv/canvas?projectId=+projectUuid。查询进展时若传入--project-id PROJECT_UUID,query_session会直接返回projectUrl,便于一并展示。
在任务完成时,同时给出:视频/图片结果链接 + 项目画布链接(projectUrl)。 过程中,不要给出 项目画布链接(projectUrl)。
核心原则:用户侧不做创作,只做传话
你(用户侧 Agent)的职责是搬运工,不是创作者。后端有专门的 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt。你要做的只有三件事:
1. 上传:用户给了本地文件 → upload_file.py 拿到 OSS URL 2. 传话:把用户的原始描述 + OSS URL 原封不动发给 create_session.py 3. 取件:轮询结果 → 下载到本地 → 展示给用户
绝对不要做的事:
- 不要替用户扩写、润色、翻译 prompt(用户说"帮我推演分镜",就直接传"帮我推演分镜",不要自己先写个分镜表再逐条发)
- 不要自行拆解任务步骤(如把"生成9张分镜图"拆成9次独立请求)
- 不要自行编排镜头描述、剧情推演、风格分析
- 不要在消息中添加自己编的 prompt(如"超写实风格,电影级光影,8K分辨率"之类的描述词)
后端 Agent 对模型能力、参数配置、prompt 工程远比用户侧更专业。用户侧越俎代庖只会降低生成质量,换个弱模型更是灾难。
正确示例:
用户说:「帮我推演后续的故事,来个分镜大爆炸,帮我出一个16:9的九宫格的图。新建一个任务。」
用户给了参考图:/path/to/ref.png
→ upload_file.py /path/to/ref.png → 拿到 oss_url
→ create_session.py "帮我推演后续的故事,来个分镜大爆炸,帮我出一个16:9的九宫格的图。参考图:{oss_url}"
→ 轮询 → 下载 → 展示错误示例:
❌ 用户侧自己先写了个九宫格分镜表(对峙、交锋、危机...)
❌ 然后把自己编的描述发给后端
❌ 或者拆成9次 create_session 分别发送注意事项
- 鉴权方式为请求头
Authorization: Bearer <LIBTV_ACCESS_KEY> - 创建会话时若不传
message,仅创建/绑定会话,不会调用 SendMessage - 查询会话时可用
--after-seq做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果) - 项目画布地址固定为:
https://www.liblib.tv/canvas?projectId=+ projectUuid - 切换项目后,Redis 缓存会更新,下次 create_session 将使用新的 projectUuid
- 上传文件仅支持图片(image/)和视频(video/)类型,其他类型会被拒绝,文件大小须在 200MB 以下
- 上传返回的 OSS 地址格式为
https://libtv-res.liblib.art/claw/{projectUuid}/{uuid}{ext} - 生成过程中只告知用户"正在生成中",不要提前给出 projectUrl;任务完成后再同时给出:结果链接(图片/视频 URL) + 项目画布链接(projectUrl)
"""agent-im OpenAPI 公共模块:创建会话、查询会话(鉴权为 Authorization: Bearer <access_key>)"""
import json
import os
import sys
import urllib.request
import urllib.error
# 默认 im 环境
IM_BASE = os.environ.get("OPENAPI_IM_BASE", os.environ.get("IM_BASE_URL", "https://im.liblib.tv"))
ACCESS_KEY = os.environ.get("LIBTV_ACCESS_KEY", "")
# 项目画布地址前缀,拼上 projectId 即项目地址
PROJECT_CANVAS_BASE = "https://www.liblib.tv/canvas?projectId="
def build_project_url(project_id: str) -> str:
"""根据 projectId(即 projectUuid)拼接项目画布地址"""
if not project_id:
return ""
return PROJECT_CANVAS_BASE + project_id.strip()
if not ACCESS_KEY:
print("错误:请设置 LIBTV_ACCESS_KEY 环境变量", file=sys.stderr)
sys.exit(1)
def _headers():
return {
"Authorization": f"Bearer {ACCESS_KEY}",
"Content-Type": "application/json",
}
def api_post(path: str, body: dict) -> dict:
"""POST 请求 agent-im OpenAPI"""
url = f"{IM_BASE.rstrip('/')}{path}"
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
method="POST",
headers=_headers(),
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
err_body = e.read().decode("utf-8") if e.fp else ""
print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"网络错误: {e.reason}", file=sys.stderr)
sys.exit(1)
def api_get(path: str) -> dict:
"""GET 请求 agent-im OpenAPI"""
url = f"{IM_BASE.rstrip('/')}{path}"
req = urllib.request.Request(url, method="GET", headers=_headers())
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
err_body = e.read().decode("utf-8") if e.fp else ""
print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"网络错误: {e.reason}", file=sys.stderr)
sys.exit(1)
def create_session(session_id: str = "", message: str = "") -> dict:
"""
创建会话或向已有会话发消息。
返回 data: { projectUuid, sessionId }。
"""
body = {}
if session_id:
body["sessionId"] = session_id
if message:
body["message"] = message
resp = api_post("/openapi/session", body)
return resp.get("data", {})
def query_session(session_id: str, after_seq: int = 0) -> dict:
"""
查询会话消息列表。
返回 data: { messages: [...] }。
"""
path = f"/openapi/session/{session_id}"
if after_seq > 0:
path += f"?afterSeq={after_seq}"
resp = api_get(path)
return resp.get("data", {})
def change_project() -> dict:
"""
切换当前 accessKey 绑定的项目(调用 libtv 切换项目,后续 create_session 将使用新项目)。
返回 data: { projectUuid }。
"""
resp = api_post("/openapi/session/change-project", {})
return resp.get("data", {})
#!/usr/bin/env python3
"""切换当前 accessKey 绑定的项目:POST /openapi/session/change-project"""
import json
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from _common import change_project, build_project_url
def main():
data = change_project()
project_uuid = data.get("projectUuid", "")
if not project_uuid:
print("错误:未返回 projectUuid", file=sys.stderr)
sys.exit(1)
project_url = build_project_url(project_uuid)
out = {
"projectUuid": project_uuid,
"projectUrl": project_url,
}
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""创建会话 / 向会话发送消息(生图、生视频等):POST /openapi/session"""
import argparse
import json
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from _common import create_session, build_project_url
def main():
parser = argparse.ArgumentParser(
description="创建会话或向已有会话发送消息(用于生图、生视频)",
epilog="""
环境变量:
LIBTV_ACCESS_KEY 必填,Bearer 鉴权
OPENAPI_IM_BASE 或 IM_BASE_URL 可选,默认 https://im.liblib.tv
示例:
# 创建新会话并发送「生一个动漫视频」
python3 create_session.py "生一个动漫视频"
# 向已有会话发送消息
python3 create_session.py "再生成一张风景图" --session-id 90f05e0c-5d08-4148-be40-e30fc7c7bedf
# 只创建/绑定会话,不发消息
python3 create_session.py
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"message",
nargs="?",
default="",
help="要发送的消息内容(生图/生视频描述等),不传则不调用 SendMessage",
)
parser.add_argument(
"--session-id",
default="",
help="已有会话 ID,不传则创建新会话或返回已有默认会话",
)
args = parser.parse_args()
data = create_session(session_id=args.session_id or "", message=args.message or "")
project_uuid = data.get("projectUuid", "")
session_id = data.get("sessionId", "")
if not session_id:
print("错误:未返回 sessionId", file=sys.stderr)
sys.exit(1)
project_url = build_project_url(project_uuid)
out = {
"projectUuid": project_uuid,
"sessionId": session_id,
"projectUrl": project_url,
}
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""下载生成结果:从会话中提取所有图片/视频 URL 并批量下载到本地"""
import argparse
import json
import os
import re
import sys
import urllib.request
import urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.insert(0, os.path.dirname(__file__))
from _common import query_session
def extract_urls_from_messages(messages):
"""从会话消息中提取所有图片和视频结果 URL"""
urls = []
url_pattern = re.compile(r'https://libtv-res\.liblib\.art/[^\s"\'<>]+\.(?:png|jpg|jpeg|webp|mp4|mov|webm)')
for msg in messages:
content = msg.get("content", "")
if not content or not isinstance(content, str):
continue
# 从 task_result 中提取(toolmsg 返回的结果)
if msg.get("role") == "tool":
try:
data = json.loads(content)
task_result = data.get("task_result", {})
for img in task_result.get("images", []):
preview = img.get("previewPath", "")
if preview:
urls.append(preview)
for vid in task_result.get("videos", []):
preview = vid.get("previewPath", vid.get("url", ""))
if preview:
urls.append(preview)
except (json.JSONDecodeError, AttributeError):
pass
# 从 assistant 文本消息中提取 URL
if msg.get("role") == "assistant":
found = url_pattern.findall(content)
urls.extend(found)
# 去重保序
seen = set()
unique = []
for u in urls:
if u not in seen:
seen.add(u)
unique.append(u)
return unique
def download_file(url, filepath):
"""下载单个文件"""
req = urllib.request.Request(url, headers={"User-Agent": "LibTV-Skill/1.0"})
try:
with urllib.request.urlopen(req, timeout=60) as resp:
with open(filepath, "wb") as f:
while True:
chunk = resp.read(8192)
if not chunk:
break
f.write(chunk)
return filepath, None
except Exception as e:
return filepath, str(e)
def main():
parser = argparse.ArgumentParser(
description="下载会话中生成的图片/视频到本地",
epilog="""
使用方式:
# 从会话自动提取并下载所有结果
python3 download_results.py SESSION_ID
# 指定输出目录
python3 download_results.py SESSION_ID --output-dir ~/Desktop/my_project
# 指定文件名前缀
python3 download_results.py SESSION_ID --prefix "storyboard"
# 直接下载指定 URL 列表
python3 download_results.py --urls URL1 URL2 URL3 --output-dir ./output
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("session_id", nargs="?", default="", help="会话 ID,自动提取该会话所有生成结果的 URL")
parser.add_argument("--urls", nargs="+", default=[], help="直接指定要下载的 URL 列表(不需要 session_id)")
parser.add_argument("--output-dir", default="", help="输出目录(默认 ~/Downloads/libtv_results/)")
parser.add_argument("--prefix", default="", help="文件名前缀(如 'storyboard' → storyboard_01.png)")
parser.add_argument("--workers", type=int, default=5, help="并行下载线程数(默认 5)")
args = parser.parse_args()
# 收集 URL
urls = list(args.urls)
if args.session_id:
data = query_session(args.session_id)
messages = data.get("messages", [])
extracted = extract_urls_from_messages(messages)
urls.extend(extracted)
if not urls:
print(json.dumps({"error": "未找到可下载的图片/视频 URL", "downloaded": []}, ensure_ascii=False, indent=2))
sys.exit(1)
# 准备输出目录
output_dir = args.output_dir or os.path.expanduser("~/Downloads/libtv_results")
os.makedirs(output_dir, exist_ok=True)
# 构建下载任务
tasks = []
for i, url in enumerate(urls, 1):
ext = os.path.splitext(url.split("?")[0])[-1] or ".png"
if args.prefix:
filename = f"{args.prefix}_{i:02d}{ext}"
else:
filename = f"{i:02d}{ext}"
filepath = os.path.join(output_dir, filename)
tasks.append((url, filepath))
# 并行下载
results = []
errors = []
with ThreadPoolExecutor(max_workers=args.workers) as pool:
futures = {pool.submit(download_file, url, fp): (url, fp) for url, fp in tasks}
for future in as_completed(futures):
fp, err = future.result()
if err:
errors.append({"file": fp, "error": err})
else:
results.append(fp)
# 按文件名排序输出
results.sort()
output = {
"output_dir": output_dir,
"downloaded": results,
"total": len(results),
}
if errors:
output["errors"] = errors
print(json.dumps(output, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""查询会话进展:GET /openapi/session/:sessionId,返回消息列表"""
import argparse
import json
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from _common import query_session, build_project_url
def main():
parser = argparse.ArgumentParser(
description="查询会话消息列表(会话进展)",
epilog="""
环境变量:
LIBTV_ACCESS_KEY 必填,Bearer 鉴权
OPENAPI_IM_BASE 或 IM_BASE_URL 可选,默认 https://im.liblib.tv
示例:
python3 query_session.py 90f05e0c-5d08-4148-be40-e30fc7c7bedf
python3 query_session.py 90f05e0c-5d08-4148-be40-e30fc7c7bedf --after-seq 5
python3 query_session.py SESSION_ID --project-id PROJECT_UUID # 结果中附带 projectUrl
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("session_id", help="会话 ID(由 create_session 返回)")
parser.add_argument(
"--after-seq",
type=int,
default=0,
help="只返回 seq 大于该值的消息,用于增量拉取(默认 0)",
)
parser.add_argument(
"--project-id",
default="",
help="项目 ID(即 create_session 返回的 projectUuid),传入则结果中附带 projectUrl 便于展示",
)
args = parser.parse_args()
data = query_session(args.session_id, after_seq=args.after_seq)
messages = data.get("messages", [])
out = {"messages": messages}
if args.project_id:
out["projectUrl"] = build_project_url(args.project_id)
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""上传图片/视频到 OSS:POST /openapi/upload(multipart/form-data)"""
import argparse
import json
import mimetypes
import os
import sys
import uuid
import urllib.request
import urllib.error
sys.path.insert(0, os.path.dirname(__file__))
from _common import IM_BASE, ACCESS_KEY
# 允许的 MIME 类型前缀
ALLOWED_PREFIXES = ("image/", "video/")
def upload_file(file_path: str) -> dict:
"""
上传本地文件到 agent-im OSS。
返回 data: { url }。
"""
if not os.path.isfile(file_path):
print(f"错误:文件不存在: {file_path}", file=sys.stderr)
sys.exit(1)
# 检查 MIME 类型
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type and not any(mime_type.startswith(p) for p in ALLOWED_PREFIXES):
print(f"错误:不支持的文件类型: {mime_type},仅支持图片和视频", file=sys.stderr)
sys.exit(1)
# 构建 multipart/form-data 请求体
boundary = f"----PythonUpload{uuid.uuid4().hex}"
filename = os.path.basename(file_path)
body_parts = []
# accessKey 字段
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="accessKey"\r\n\r\n')
body_parts.append(f"{ACCESS_KEY}\r\n".encode())
# file 字段
content_type = mime_type or "application/octet-stream"
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(
f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
)
body_parts.append(f"Content-Type: {content_type}\r\n\r\n".encode())
with open(file_path, "rb") as f:
body_parts.append(f.read())
body_parts.append(b"\r\n")
# 结束边界
body_parts.append(f"--{boundary}--\r\n".encode())
data = b"".join(body_parts)
url = f"{IM_BASE.rstrip('/')}/openapi/upload"
req = urllib.request.Request(
url,
data=data,
method="POST",
headers={
"Authorization": f"Bearer {ACCESS_KEY}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
},
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
result = json.loads(resp.read().decode("utf-8"))
return result.get("data", {})
except urllib.error.HTTPError as e:
err_body = e.read().decode("utf-8") if e.fp else ""
print(f"API 错误 {e.code}: {err_body}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"网络错误: {e.reason}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="上传图片或视频文件到 OSS",
epilog="""
环境变量:
LIBTV_ACCESS_KEY 必填,Bearer 鉴权
OPENAPI_IM_BASE 或 IM_BASE_URL 可选,默认 https://im.liblib.tv
示例:
# 上传图片
python3 upload_file.py /path/to/image.png
# 上传视频
python3 upload_file.py /path/to/video.mp4
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"file",
help="要上传的图片或视频文件路径",
)
args = parser.parse_args()
data = upload_file(args.file)
oss_url = data.get("url", "")
if not oss_url:
print("错误:未返回 OSS 地址", file=sys.stderr)
sys.exit(1)
out = {"url": oss_url}
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Related skills
How it compares
libtv-skill implements its own SKILL.md workflow rather than a generic substitute skill.
FAQ
Who is libtv-skill for?
Agents and developers following the libtv-skill SKILL.md guidance.
When should I use libtv-skill?
When user intent matches description triggers and quick start scenarios.
Is libtv-skill safe to install?
Review the Security Audits panel before production shell or network use.