
Byted Marketing Agent Inspiration Insight
- 3 installs
- 408 repo stars
- Updated August 3, 2026
- volcengine/agentkit-samples
Helps with marketing & seo tasks.
About
byted-marketing-agent-inspiration-insight is a Claude Code skill in the Marketing & SEO category.
- byted-marketing-agent-inspiration-insight
- Marketing & SEO
- AI-coding skill
Byted Marketing Agent Inspiration Insight by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,624 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/volcengine/agentkit-samples --skill byted-marketing-agent-inspiration-insightAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 408 |
| Last updated | August 3, 2026 |
| Repository | volcengine/agentkit-samples ↗ |
What it does
Helps with marketing & seo tasks.
Files
创意灵感洞察(InspirationInsight)
When to Use
/inspiration— 手动触发完整灵感洞察查询- 用户问"爆款创意/分镜提示词/视频灵感/视觉元素/营销素材"
- 用户想要"复刻爆款/分析爆款视频/看关键帧截图/看 ASR 文本"
- 用户想了解某行业的"创意方向/内容趋势/视频脚本参考"
- 被其他编排 skill 调用(如日报生成、创意策划)
- 定时任务自动执行获取最新灵感数据
⛔ OUTPUT RULE — 最高优先级
>
- 执行过程中禁止向用户输出任何中间过程,包括但不限于:自检、创建虚拟环境、安装依赖、连通性验证等技术步骤。
- 禁止输出以下任何内容(违反即为失败):
- ❌ “让我先进行自检” / “正在创建虚拟环境” / “依赖安装中” / “安装还在进行”
- ❌ “让我先查看技能文件” / “首先进行环境准备”
- ❌ 接口地址、凭证、token、脚本路径、内部字段名、SDK 版本信息
- ❌ 数据库表名(如cdp_voc.hot_video)、英文字段名(如vv_all、storyboard_prompt)、query_type 值(如hot_video)——无论是过程还是结果,一律禁止向用户透露
- 若环境未就绪需要安装,只允许输出一句用户友好的提示(如"正在为您初始化查询服务,请稍候…"),完成后直接返回业务结果,不播报技术细节。
- 只在完成后输出一次:业务结果(行业列表 / 洞察列表 / 单条详情)或”暂无数据/参数缺失需要选择”。
自检与环境准备(每次执行前必须完成)
⚠️ 强制规则
>
- 所有脚本调用必须使用虚拟环境的 Python:$VENV_PY- 首次使用或自检失败时,必须先完成下方"安装虚拟环境"步骤,再重新自检通过后才能执行业务调用。
- 禁止直接使用系统 python3,避免依赖污染或版本不匹配。0. 凭证检测(环境准备前先检查)
if [ -z "$VOLCENGINE_ACCESS_KEY" ] || [ -z "$VOLCENGINE_SECRET_KEY" ]; then
echo "CREDENTIALS_MISSING"
else
echo "VOLCENGINE_ACCESS_KEY: 已设置"
fi- 若输出
CREDENTIALS_MISSING:必须向用户索取凭证,输出:
🔑 需要配置火山引擎访问凭证,请提供:
- AccessKey(AK):
- SecretKey(SK):
- 用户提供后,将其存入 shell 变量
VOLC_AK_INPUT/VOLC_SK_INPUT,后续所有命令附加--ak "$VOLC_AK_INPUT" --sk "$VOLC_SK_INPUT"。 - 若凭证已存在(
VOLCENGINE_ACCESS_KEY/VOLCENGINE_SECRET_KEY已设置),无需询问,直接进入自检。
A. 离线自检(不触网,每次执行前先跑)
SCRIPTS_DIR=$(dirname "$(find ~ -maxdepth 8 -name "openapi_client.py" -path "*byted-marketing-agent-inspiration-insight*" 2>/dev/null | head -1)")
SKILL_DIR=$(dirname "$SCRIPTS_DIR")
VENV_PY=$SKILL_DIR/venv/bin/python3
# 1) 检查虚拟环境是否存在
test -f $VENV_PY && echo "venv OK" || echo "venv 不存在,请先执行安装步骤"
# 2) 检查依赖是否可用
$VENV_PY -c "import volcenginesdkcore; from volcenginesdkcore import ApiClient; print('deps OK')"
# 3) 检查 volcengine-python-sdk 版本(必须 >= 4.0.43)
$VENV_PY -c "from importlib.metadata import version; print(version('volcengine-python-sdk'))"
# 4) 语法检查
$VENV_PY -m py_compile $SCRIPTS_DIR/openapi_client.py && echo "syntax OK"自检全部通过(无报错)后,才可执行后续业务调用。
安装虚拟环境(自检失败时执行)
SCRIPTS_DIR=$(dirname "$(find ~ -maxdepth 8 -name "openapi_client.py" -path "*byted-marketing-agent-inspiration-insight*" 2>/dev/null | head -1)")
SKILL_DIR=$(dirname "$SCRIPTS_DIR")
# 1. 创建虚拟环境(仅首次)
python3 -m venv $SKILL_DIR/venv
# 2. 安装依赖
$SKILL_DIR/venv/bin/pip install 'volcengine-python-sdk>=4.0.43'已知缺陷提醒:volcengine-python-sdk 的 4.0.1~4.0.42(含)历史版本内置重试机制存在缺陷,强烈建议使用 >=4.0.43。
如系统缺少python3-venv:apt update && apt install python3-venv -y,再重新执行上述步骤。
B. 在线自检(自检 A 通过后,验证接口连通性)
SCRIPTS_DIR=$(dirname "$(find ~ -maxdepth 8 -name "openapi_client.py" -path "*byted-marketing-agent-inspiration-insight*" 2>/dev/null | head -1)")
SKILL_DIR=$(dirname "$SCRIPTS_DIR")
VENV_PY=$SKILL_DIR/venv/bin/python3
# 若用户提供了凭证,附加 --ak / --sk;否则省略
$VENV_PY $SCRIPTS_DIR/openapi_client.py --format text \
${VOLC_AK_INPUT:+--ak "$VOLC_AK_INPUT"} ${VOLC_SK_INPUT:+--sk "$VOLC_SK_INPUT"} \
list-industries如需进一步验证列表查询:
$VENV_PY $SCRIPTS_DIR/openapi_client.py --format text \
${VOLC_AK_INPUT:+--ak "$VOLC_AK_INPUT"} ${VOLC_SK_INPUT:+--sk "$VOLC_SK_INPUT"} \
query \
--category "{从行业枚举中选择一个}" \
--query-type hot_video \
--task-date "2026-03-12" \
--page 1 \
--page-size 5目标
为用户提供“创意灵感洞察”能力:
1. 通过“接口1”获取可查询行业枚举(仅行业,不包含 type)。 2. 用户选定行业后,使用本 Skill 固定的 type 查询该行业下的灵感洞察列表。 3. 用户点选某条记录后,按唯一键获取详情(可选:从列表上下文展开或再查一次详情)。
交互逻辑(必须)
当本 Skill 被触发时:必须先主动调用接口1获取最新行业枚举(无论用户是否已提供行业)。
Step 1:行业枚举(共用接口1)
- 当用户未明确行业时:先获取行业列表并让用户选择。
- 当用户明确行业时:仍允许在必要时刷新行业列表用于校验/纠错。
通过 Bash 调用脚本(必须使用虚拟环境 Python):
$VENV_PY \
$SCRIPTS_DIR/openapi_client.py \
${VOLC_AK_INPUT:+--ak "$VOLC_AK_INPUT"} ${VOLC_SK_INPUT:+--sk "$VOLC_SK_INPUT"} \
--format text list-industriesStep 2:按行业查询(type 由本 Skill 固定控制)
- 本 Skill 的
query_type固定为hot_video(不要向用户暴露/询问 type)。 - 始终使用 `--format json`,脚本返回完整字段,由你负责格式化展示。
- 默认按
播放量 DESC(播放量倒序)排序;用户可指定其他字段排序。
YESTERDAY=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d)
$VENV_PY \
$SCRIPTS_DIR/openapi_client.py --format json \
${VOLC_AK_INPUT:+--ak "$VOLC_AK_INPUT"} ${VOLC_SK_INPUT:+--sk "$VOLC_SK_INPUT"} \
query \
--category "{industry_name}" \
--query-type hot_video \
--task-date "$YESTERDAY" \
--order-by "播放量 DESC" \
--page 1 \
--page-size 20用户要求按其他维度排序时,替换 --order-by 的值,例如:
"点赞数 DESC"— 点赞最多"评论数 DESC"— 评论最多"分享数 DESC"— 分享最多"5秒完看率 DESC"— 5秒完看率最高"完播次数 DESC"— 完播次数最多
每次返回列表结果后,主动告知用户可以按哪些字段排序,示例引导语:
💡 当前按播放量排序,你也可以让我改成按「点赞数」「评论数」「分享数」「5秒完看率」排序。
Step 3:详情展开(可选)
YESTERDAY=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d)
$VENV_PY \
$SCRIPTS_DIR/openapi_client.py --format json \
${VOLC_AK_INPUT:+--ak "$VOLC_AK_INPUT"} ${VOLC_SK_INPUT:+--sk "$VOLC_SK_INPUT"} \
query \
--category "{industry_name}" \
--query-type hot_video \
--task-date "$YESTERDAY" \
--filter "视频ID = '{item_id}'" \
--page 1 \
--page-size 1数据字段说明
接口返回每条记录为一个爆款视频,全部透传、不裁剪:
| 字段名 | 含义 |
|---|---|
| 视频ID | 视频唯一 ID |
| 标题 | 视频标题 |
| 创建日期 | 视频创建日期 |
| 链接 | 抖音视频链接 |
| 播放量 | 累计播放量 |
| 点赞数 | 累计点赞数 |
| 评论数 | 累计评论数 |
| 分享数 | 累计分享数 |
| 关注数 | 累计关注数 |
| 收藏数 | 累计收藏数 |
| 自然流播放量 | 自然流播放量 |
| 软广播放量 | 软广播放量 |
| 硬广播放量 | 硬广播放量 |
| 5秒完看率 | 5秒完看率 |
| 完播次数 | 完播次数 |
| 跳过次数 | 被跳过的总播放次数 |
| 商业类型 | 商业类型标签 |
| 语音文本 | 视频语音转文字(ASR) |
| 画面文字 | 视频画面文字(OCR) |
| 润色文案 | 润色后的语音文本 |
| 视频分析 | 多模态视频逐镜分析(JSON) |
| 分析提示词 | 生成视频分析的提示词 |
| 分镜脚本 | 分镜脚本提示词 |
| 关键帧截图 | 关键帧截图 URL(JSON) |
| 复刻提示词 | 复刻视频的生成提示词(JSON) |
| 当日播放 / 当日点赞 等 | 当日播放/点赞/评论/分享/关注 |
| 产出日期 | 任务运行结束日期 |
| 任务名 / 行业 / 任务日期 | 任务元信息 |
展示规范
⛔ 禁止在下方模板规定的结构之外添加任何内容,包括引言、总结、个人分析或额外说明。禁止直接粘贴原始 JSON。
列表视图(固定模板,不可更改列或顺序)
## 📊 {行业} 热门视频({task_date})|按{排序字段中文名}排序
| # | 标题 | 播放量 | 点赞 | 5秒完看率 | 完播 | 链接 |
|---|------|--------|------|-----------|------|------|
| 1 | {标题} | {播放量}万 | {点赞数}万 | {5秒完看率}% | {完播次数}万 | [▶](url) |
...
💡 你可以继续问我:
- {引导问题1}
- {引导问题2}
- {引导问题3}数值规则:保留1位小数,不足1万显示原值;标题超20字截断加"…";url 为空时显示"—"。
详情视图(保留自由度)
按用户问题意图选择展示字段,但必须遵循:
- 以视频标题为 H3 标题开头
关键帧截图图片用内嵌视频分析、复刻提示词等 JSON 字段解析后按内容逻辑展示润色文案、分镜脚本等长文本全量输出,不截断
意图 → 优先字段映射:
| 用户问的是… | 优先展示 |
|---|---|
| 分镜 / 脚本 | 分镜脚本 |
| 说了什么 / 台词 / 文案 | 润色文案 |
| 关键帧 / 截图 | 关键帧截图(图片内嵌) |
| 复刻 / 生成提示词 | 复刻提示词 |
| 视频分析 / 镜头 | 视频分析 |
| 看全部 / 详情 | 所有非空字段,按上表顺序依次展示 |
末尾必须附 3 条引导问题。
引导提问规范(每次返回结果后必须执行)
每次输出业务结果后,必须在末尾附上 2~3 个引导问题,帮助用户深入探索。引导问题要根据当前结果内容动态生成,不要每次都一样。
返回行业列表后
💡 你可以继续问我:
>
- "帮我查一下美妆行业的创意灵感洞察"
- "我想看服饰行业最近有哪些爆款创意"
返回洞察列表后
💡 你可以继续问我:
>
- "帮我展开第 1 条的详细分析"(或点名某个标题)
- "这条视频的分镜提示词是什么?"
- "帮我看看它的关键帧截图"
- "换一个行业查创意灵感"
返回单条详情后
💡 你可以继续问我:
>
- "把这条的分镜提示词完整给我"
- "看看列表里其他的创意"
- "换成食品行业查一下有没有类似风格的爆款"
原则:引导问题要具体(带上当前行业名、视频标题或字段名),让用户感觉只需直接回复即可继续探索。
参数规则
industry_name:用户可感知维度;必须来自接口1返回的行业枚举(或与其一致)。task_date:默认取 T-1(昨天),即$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d);用户指定日期时以用户为准。type:禁止从接口1下发;由本 Skill 内部固定为inspiration_insight。
错误处理
- 接口调用失败:只向用户输出简短失败原因(不要包含 URL/Token/脚本名/堆栈)。
- 行业缺失:输出行业候选列表(适度截断),让用户选择。
- 无数据:输出“该行业暂无灵感洞察数据”。
凭证说明(仅供执行时使用,禁止回显给用户)
Volcengine SDK 鉴权(接口调用必需)
本 Skill 使用volcenginesdkcore.ApiClient向cdp-saas.cn-beijing.volcengineapi.com发起签名请求。
Action:ArkOpenClawSkill,Version:2022-08-01。
凭证仅通过用户输入获取,优先级:--ak/--sk 参数 > 环境变量 VOLCENGINE_ACCESS_KEY/VOLCENGINE_SECRET_KEY。
VOLCENGINE_ACCESS_KEY:AccessKeyVOLCENGINE_SECRET_KEY:SecretKeyVOLC_SERVICE:覆盖 Service 名(可选,默认cdp_saas)VOLCENGINE_REGION:覆盖 Region(可选,默认cn-beijing)
可选覆盖
PUBLIC_INSIGHT_API_URL:覆盖默认接入点(仅限内部调试)
安全要求:禁止在 SKILL.md 或代码中硬编码明文 AK/SK。 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship made available under
the License, as indicated by a copyright notice that is included in
or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean, as submitted to the Licensor for inclusion
in the Work by the copyright owner or by an individual or Legal Entity
authorized to submit on behalf of the copyright owner. For the purposes
of this definition, "submitted" means any form of electronic, verbal,
or written communication sent to the Licensor or its representatives,
including but not limited to communication on electronic mailing lists,
source code control systems, and issue tracking systems that are managed
by, or on behalf of, the Licensor for the purpose of submitting and
discussing improvements to the Work, but excluding communication that
is conspicuously marked or otherwise designated in writing by the
copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any Legal Entity on behalf of
whom a Contribution has been received by the Licensor and included
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by the combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Work or any
Contribution embodied within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under
this License for that Work shall terminate as of the date such
litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative
Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, You must include a readable copy of the
attribution notices contained within such NOTICE file, in
at least one of the following places: within a NOTICE text
file distributed as part of the Derivative Works; within
the Source form or documentation, if provided along with the
Derivative Works; or, within a display generated by the
Derivative Works, if and wherever such third-party notices
normally appear. The contents of the NOTICE file are for
informational purposes only and do not modify the License.
You may add Your own attribution notices within Derivative
Works that You distribute, alongside or in addition to the
NOTICE text from the Work, provided that such additional
attribution notices cannot be construed as modifying the License.
You may add Your own license statement for Your modifications and
may provide additional grant of rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the
Contribution, either before or after.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or reproducing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or exemplary damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or all other
commercial damages or losses), even if such Contributor has been
advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may offer only
conditions that are (a) consistent with the terms of this License, and
(b) include a complete copy of this License. Upon Your request, the
Licensor may provide such Contributor access to the License terms.
END OF TERMS AND CONDITIONS
Copyright 2024 ByteDance, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#!/usr/bin/env python3
# Copyright 2024 ByteDance, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import logging
import json
import os
import re
import sys
import time
from typing import Any, Dict, Optional
DEFAULT_API_HOST = "cdp-saas.cn-beijing.volcengineapi.com"
DEFAULT_API_PATH = "/"
DEFAULT_SERVICE = "cdp_saas"
DEFAULT_REGION = "cn-beijing"
DEFAULT_ACTION = "ArkOpenClawSkill"
DEFAULT_VERSION = "2022-08-01"
MIN_VOLC_SDK_VERSION = "4.0.43"
_SENSITIVE_KEY_RE = re.compile(r"(token|secret|password|passwd|api[_-]?key|task[_-]?id)", re.IGNORECASE)
# ── 字段别名映射:中文名 → 英文字段名 ──────────────────────────────────────────
# 用于:① --order-by / --filter 参数的中文→英文翻译(输入)
# ② JSON 输出时的英文 key→中文 key 反向翻译(输出)
_FIELD_ALIAS_HOT_VIDEO: Dict[str, str] = {
"视频ID": "item_id",
"标题": "item_title",
"创建日期": "item_create_day",
"链接": "url",
"播放量": "vv_all",
"点赞数": "like_cnt_all",
"评论数": "comment_cnt_all",
"分享数": "share_cnt_all",
"关注数": "follow_cnt_all",
"收藏数": "favourite_cnt_all",
"自然流播放量": "natural_vv_all",
"软广播放量": "soft_ad_vv_all",
"硬广播放量": "hard_ad_vv_all",
"5秒完看率": "gte5s_rate",
"完播次数": "finish_vv_all",
"跳过次数": "skip_play_all",
"商业类型": "commerce_type",
"语音文本": "item_asr",
"画面文字": "item_ocr",
"润色文案": "polished_asr",
"视频分析": "vlm_analysis",
"分析提示词": "vlm_prompt",
"分镜脚本": "storyboard_prompt",
"关键帧截图": "frame_extraction",
"复刻提示词": "prompt_replication",
"当日播放": "vv",
"当日点赞": "like_cnt",
"当日评论": "comment_cnt",
"当日分享": "share_cnt",
"当日关注": "follow_cnt",
"产出日期": "p_date",
"任务名": "task_name",
"行业": "industry",
"任务日期": "task_date",
}
_FIELD_ALIAS_HOT_VIDEO_REVERSE: Dict[str, str] = {v: k for k, v in _FIELD_ALIAS_HOT_VIDEO.items()}
def _translate_field_expr(expr: Optional[str], alias: Dict[str, str]) -> Optional[str]:
"""将表达式中的中文字段名替换为英文字段名(order_by / filter 通用)。
按中文名长度降序替换,避免短名称干扰长名称(如"播放量"误替换"自然流播放量")。
若表达式中无中文字段名则原样返回(兼容直接传英文名的情况)。
"""
if not expr:
return expr
result = expr
for cn, en in sorted(alias.items(), key=lambda x: -len(x[0])):
result = result.replace(cn, en)
return result
def _rename_keys(obj: Any, reverse: Dict[str, str]) -> Any:
"""递归将响应数据中的英文 key 替换为中文 key。"""
if isinstance(obj, dict):
return {reverse.get(k, k): _rename_keys(v, reverse) for k, v in obj.items()}
if isinstance(obj, list):
return [_rename_keys(i, reverse) for i in obj]
return obj
def _debug_enabled() -> bool:
v = _env("OPENCLAW_DEBUG")
return str(v).lower() in ("1", "true", "yes", "on") if v is not None else False
def _env(name: str) -> Optional[str]:
v = os.environ.get(name)
if v is None:
return None
v = v.strip()
return v or None
def _mask_secret(s: Optional[str]) -> Optional[str]:
"""对敏感字符串做脱敏展示:仅保留前后少量字符。"""
if not s:
return None
ss = str(s)
if len(ss) <= 8:
return "*" * len(ss)
return ss[:4] + "*" * (len(ss) - 8) + ss[-4:]
def _parse_version(v: str) -> tuple[int, int, int]:
parts = (v or "").strip().split(".")
nums: list[int] = []
for p in parts[:3]:
try:
nums.append(int(re.sub(r"\D.*$", "", p)))
except Exception:
nums.append(0)
while len(nums) < 3:
nums.append(0)
return nums[0], nums[1], nums[2]
def _get_volc_sdk_version() -> Optional[str]:
try:
from importlib.metadata import version # py3.8+
except Exception:
try:
from importlib_metadata import version # type: ignore
except Exception:
return None
try:
return version("volcengine-python-sdk")
except Exception:
return None
def _ensure_volc_sdk_min_version(min_version: str = MIN_VOLC_SDK_VERSION) -> Optional[str]:
cur = _get_volc_sdk_version()
if not cur:
return "未安装 volcengine-python-sdk。请先安装 volcengine-python-sdk>=4.0.43。"
if _parse_version(cur) < _parse_version(min_version):
return f"volcengine-python-sdk 版本过低(当前 {cur},要求 >= {min_version})。请升级以避免历史版本重试缺陷。"
return None
def _build_api_client(ak_override: Optional[str] = None, sk_override: Optional[str] = None) -> tuple[Any, str]:
"""构建已配置签名的 volcenginesdkcore.ApiClient,返回 (client, api_path)。"""
ver_err = _ensure_volc_sdk_min_version()
if ver_err:
raise RuntimeError(ver_err)
try:
import volcenginesdkcore # type: ignore
except ImportError:
raise RuntimeError(
"未安装 volcengine-python-sdk(缺少 volcenginesdkcore)。请先安装 volcengine-python-sdk>=4.0.43。"
)
ak = ak_override or _env("VOLCENGINE_ACCESS_KEY")
sk = sk_override or _env("VOLCENGINE_SECRET_KEY")
if not (ak and sk):
raise RuntimeError("未配置 Volcengine 凭证(需要同时设置 VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY)。")
# service / region / host 均有内置默认值,环境变量可覆盖(用于调试)
service = _env("VOLC_SERVICE") or DEFAULT_SERVICE
region = _env("VOLCENGINE_REGION") or DEFAULT_REGION
custom_url = _env("PUBLIC_INSIGHT_API_URL")
if custom_url:
from urllib.parse import urlsplit
p = urlsplit(custom_url)
host = p.netloc or DEFAULT_API_HOST
api_path = p.path or DEFAULT_API_PATH
scheme = p.scheme or "https"
else:
host = DEFAULT_API_HOST
api_path = DEFAULT_API_PATH
scheme = "https"
configuration = volcenginesdkcore.Configuration()
# 默认关闭 SDK 的日志输出(避免干扰用户输出)。
# 调试时可通过 OPENCLAW_DEBUG=1 打开。
if not _debug_enabled():
try:
configuration.logger["package_logger"].setLevel(logging.ERROR)
configuration.logger["urllib3_logger"].setLevel(logging.ERROR)
except Exception:
pass
configuration.ak = ak
configuration.sk = sk
configuration.region = region
configuration.host = host
if scheme != "https":
configuration.scheme = scheme
if hasattr(configuration, "service"):
configuration.service = service
return volcenginesdkcore.ApiClient(configuration), api_path
def _do_call(api_client: Any, api_path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
"""通过 SDK ApiClient 向火山 OpenAPI 发起签名 POST 请求。"""
# SDK call_api 拦截器把 202 当异常处理(期望 200),改用 rest_client 直接发,
# 仍通过 SDK SignerV4 签名,保持 SDK 依赖。
try:
from volcenginesdkcore.signv4 import SignerV4 # type: ignore
except ImportError:
return {"status": "error", "message": "未安装 volcengine-python-sdk。"}
try:
cfg = api_client.configuration
body_str = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
scheme = getattr(cfg, "scheme", "https") or "https"
service = getattr(cfg, "service", DEFAULT_SERVICE) or DEFAULT_SERVICE
url = f"{scheme}://{cfg.host}{api_path}"
query = [("Action", DEFAULT_ACTION), ("Version", DEFAULT_VERSION)]
headers: Dict[str, str] = {
"Content-Type": "application/json",
"Accept": "application/json",
"Host": cfg.host,
}
SignerV4.sign(api_path, "POST", headers, body_str, None, query,
cfg.ak, cfg.sk, cfg.region, service, None)
# 拼装完整 URL(含 query string)
from urllib.parse import urlencode
full_url = f"{url}?{urlencode(query)}"
import urllib.request as _urllib
import urllib.error as _urlerr
req = _urllib.Request(full_url, data=body_str.encode(), headers=headers, method="POST")
try:
http_resp = _urllib.urlopen(req, timeout=60)
status = http_resp.status
raw = http_resp.read()
except _urlerr.HTTPError as http_err:
status = http_err.code
raw = http_err.read()
try:
data = json.loads(raw.decode())
except Exception:
return {"status": "error", "message": f"服务返回非 JSON(HTTP {status})。"}
if status >= 400:
msg = f"服务请求失败(HTTP {status})。"
meta = data.get("ResponseMetadata") if isinstance(data, dict) else None
if isinstance(meta, dict) and isinstance(meta.get("Error"), dict):
err = meta["Error"]
code = str(err.get("Code") or err.get("CodeN") or "").lower()
if code in ("invalidcredential", "100025"):
msg = "鉴权失败:请检查 AK/SK 凭证配置是否正确。"
return {"status": "error", "message": msg, "data": data}
return {"status": "success", "data": data}
except Exception as e:
return {"status": "error", "message": f"请求失败:{str(e)[:200]}"}
def _is_retryable(resp: Dict[str, Any]) -> bool:
"""判断响应是否可重试(502/503/504 或 InternalServiceError)。"""
if resp.get("status") == "success":
return False
msg = resp.get("message", "")
if "HTTP 502" in msg or "HTTP 503" in msg or "HTTP 504" in msg:
return True
data = resp.get("data")
if isinstance(data, dict):
meta = data.get("ResponseMetadata")
if isinstance(meta, dict):
err = meta.get("Error", {})
code = str(err.get("Code") or err.get("CodeN") or "")
if code in ("InternalServiceError", "100023"):
return True
return False
def api_call(tool_name: str, arguments: Dict[str, Any], ak: Optional[str] = None, sk: Optional[str] = None) -> Dict[str, Any]:
"""封装单次工具调用:MCP JSON-RPC → SDK 签名发送 → 解析响应(含重试)。"""
try:
api_client, api_path = _build_api_client(ak_override=ak, sk_override=sk)
except RuntimeError as e:
return {"status": "error", "message": str(e)}
# 后端是 MCP 服务,body 保持 JSON-RPC 2.0 格式
payload = {
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {"name": tool_name, "arguments": arguments},
}
max_retries = 3
for attempt in range(max_retries + 1):
resp = _do_call(api_client, api_path, payload)
if not _is_retryable(resp) or attempt == max_retries:
break
wait = 2 ** attempt # 1s, 2s, 4s
time.sleep(wait)
if resp.get("status") != "success":
return resp
data = resp.get("data")
if not isinstance(data, dict):
return {"status": "error", "message": "响应格式异常。"}
# JSON-RPC 错误结构
if data.get("error"):
return {"status": "error", "message": str(data["error"].get("message") or data["error"])}
result = data.get("result")
if not isinstance(result, dict):
return {"status": "error", "message": "响应缺少 result。"}
if result.get("isError") is True:
return {"status": "error", "message": "服务返回错误。", "data": result}
content = result.get("content")
if isinstance(content, list) and content:
first = content[0]
if isinstance(first, dict) and first.get("type") == "text":
txt = first.get("text")
if isinstance(txt, str):
try:
return {"status": "success", "data": json.loads(txt)}
except Exception:
return {"status": "success", "data": {"raw": txt}}
return {"status": "success", "data": result}
def list_categories(ak: Optional[str] = None, sk: Optional[str] = None) -> Dict[str, Any]:
resp = api_call("list_categories", {}, ak=ak, sk=sk)
if resp.get("status") != "success":
return resp
data = resp.get("data")
if isinstance(data, dict) and data.get("ok") is True:
return {"status": "success", "categories": data.get("categories")}
return {"status": "success", "categories": (data.get("categories") if isinstance(data, dict) else data)}
def query_clickhouse_http(
*,
category: str,
query_type: str,
task_date: Optional[str],
filter_str: Optional[str],
order_by: Optional[str],
page: int,
page_size: int,
ak: Optional[str] = None,
sk: Optional[str] = None,
) -> Dict[str, Any]:
if query_type != "hot_video":
return {"status": "error", "message": "本接口仅支持 query_type=hot_video。"}
alias = _FIELD_ALIAS_HOT_VIDEO
en_order_by = _translate_field_expr(order_by, alias)
en_filter = _translate_field_expr(filter_str, alias)
args: Dict[str, Any] = {
"category": category,
"query_type": query_type,
"page": page,
"page_size": page_size,
}
if task_date:
args["task_date"] = task_date
if en_filter:
args["filter"] = en_filter
if en_order_by:
args["order_by"] = en_order_by
resp = api_call("query_clickhouse_http", args, ak=ak, sk=sk)
if resp.get("status") == "success":
resp["data"] = _rename_keys(resp.get("data"), _FIELD_ALIAS_HOT_VIDEO_REVERSE)
return resp
def _is_safe_kv(k: str, v: Any) -> bool:
if _SENSITIVE_KEY_RE.search(k or ""):
return False
if isinstance(v, str) and len(v) > 2000:
return False
return True
def _pick_first(d: Dict[str, Any], keys: list[str]) -> Optional[Any]:
for k in keys:
if k in d and d.get(k) not in (None, ""):
return d.get(k)
return None
def _extract_items(data: Any) -> list[Any]:
if isinstance(data, list):
return data
if isinstance(data, dict):
if isinstance(data.get("data"), list):
return data.get("data")
for k in ("items", "list", "rows", "result"):
v = data.get(k)
if isinstance(v, list):
return v
return []
def _format_categories_text(categories: Any, max_items: int) -> str:
items: list[Any]
if isinstance(categories, list):
items = categories
elif categories is None:
items = []
else:
items = [categories]
lines = [f"可选行业({len(items)}):"]
for i, it in enumerate(items[:max_items], start=1):
lines.append(f"{i}. {it}")
if len(items) > max_items:
lines.append(f"… 另有 {len(items) - max_items} 个行业未展示")
return "\n".join(lines) if items else "暂无可用行业。"
def _format_list_text(data: Any, max_items: int) -> str:
items = _extract_items(data)
if not items:
return "暂无数据。"
lines = [f"列表结果({len(items)} 条,展示前 {min(len(items), max_items)} 条):"]
for idx, it in enumerate(items[:max_items], start=1):
if isinstance(it, dict):
title = _pick_first(it, ["item_title", "itemTitle", "title", "name", "event_name", "eventName", "brief"])
url = _pick_first(it, ["url", "link"])
rank = _pick_first(it, ["rank", "ranking"])
heat = _pick_first(it, ["heat_value", "heatValue", "heat", "hot", "score"])
vv = _pick_first(it, ["vv", "vv_all", "play", "views", "play_cnt", "playCount"])
parts = []
if title:
parts.append(str(title))
if rank not in (None, ""):
parts.append(f"rank={rank}")
if heat not in (None, ""):
parts.append(f"heat={heat}")
if vv not in (None, ""):
parts.append(f"vv={vv}")
if url:
parts.append(str(url))
if not parts:
safe_pairs = [(k, v) for k, v in it.items() if isinstance(k, str) and _is_safe_kv(k, v)]
preview = ", ".join([f"{k}={v}" for k, v in safe_pairs[:4]])
parts = [preview or "(无法展示字段)"]
lines.append(f"{idx}. " + " | ".join(parts))
else:
lines.append(f"{idx}. {it}")
if len(items) > max_items:
lines.append(f"… 其余 {len(items) - max_items} 条未展示")
return "\n".join(lines)
def main() -> int:
ap = argparse.ArgumentParser(description="MarketingAgent OpenAPI Client (volcengine-sdk)")
ap.add_argument("--format", default="text", choices=["text", "json"], help="输出格式")
ap.add_argument("--max-items", type=int, default=20, help="text 输出时最多展示条数")
ap.add_argument("--debug", action="store_true", help="输出完整错误信息(也可用 OPENCLAW_DEBUG=1)")
ap.add_argument("--ak", default=None, help="Volcengine AccessKey(优先级高于环境变量和 .env 文件)")
ap.add_argument("--sk", default=None, help="Volcengine SecretKey(优先级高于环境变量和 .env 文件)")
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("list-industries", help="获取可用行业枚举")
q = sub.add_parser("query", help="按行业查询数据")
q.add_argument("--category", required=True, help="行业")
q.add_argument("--query-type", required=True, choices=["hot_video"], help="查询类型")
q.add_argument("--task-date", default=None, help="任务日期 YYYY-MM-DD(可选)")
q.add_argument("--filter", default=None, help="可选:过滤表达式字符串(例:vv > 1000)")
q.add_argument("--order-by", default=None, help="排序字段(例:vv_all DESC 或 like_cnt_all ASC);默认 vv_all DESC")
q.add_argument("--page", type=int, default=1)
q.add_argument("--page-size", type=int, default=20)
args = ap.parse_args()
if args.debug and not _debug_enabled():
os.environ["OPENCLAW_DEBUG"] = "1"
if args.cmd == "list-industries":
out = list_categories(ak=args.ak, sk=args.sk)
if args.format == "json":
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
return 0
if out.get("status") != "success":
sys.stdout.write(str(out.get("message") or "请求失败"))
if args.debug and out.get("data") is not None:
sys.stdout.write("\n")
sys.stdout.write(json.dumps(out.get("data"), ensure_ascii=False, indent=2))
return 1
sys.stdout.write(_format_categories_text(out.get("categories"), args.max_items))
return 0
if args.cmd == "query":
out = query_clickhouse_http(
category=args.category,
query_type=args.query_type,
task_date=args.task_date,
filter_str=args.filter,
order_by=args.order_by,
page=args.page,
page_size=args.page_size,
ak=args.ak,
sk=args.sk,
)
if args.format == "json":
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
return 0 if out.get("status") == "success" else 1
if out.get("status") != "success":
sys.stdout.write(str(out.get("message") or "请求失败"))
if args.debug and out.get("data") is not None:
sys.stdout.write("\n")
sys.stdout.write(json.dumps(out.get("data"), ensure_ascii=False, indent=2))
return 1
sys.stdout.write(_format_list_text(out.get("data"), args.max_items))
return 0
sys.stdout.write("unknown command")
return 1
if __name__ == "__main__":
raise SystemExit(main())