MySQL Natural Language Query Assistant
将自然语言请求转换为安全的 MySQL 查询,支持实时数据库连接与结果验证。
从YouTube或Bilibili提取字幕,生成结构化摘要并保存原始文本。
openclaw skills install @xiapuyang/youtube-subtitle-summary命令、参数、文件名以原文为准
检测平台 → 下载字幕 → 清理 → 保存原始内容 → 生成摘要
if ! command -v yt-dlp &>/dev/null; then
echo "yt-dlp 未找到,正在安装..."
pip install -q yt-dlp || pip3 install -q yt-dlp
fi
yt-dlp -U --quiet 2>/dev/null || true如果安装失败,请停止并提示用户手动安装 yt-dlp(使用 pip install yt-dlp 或 brew install yt-dlp)。
根据 URL 判断是 Bilibili 还是 YouTube,并采用相应的策略。
URL="<用户提供的 URL>"
TMPDIR=$(mktemp -d)
SUB_FILE=""
SUBTITLE_LANG=""
# 检测平台
if echo "$URL" | grep -qE '(bilibili\.com|b23\.tv)'; then
PLATFORM="bilibili"
SITE_NAME="Bilibili"
SITE_DOMAIN="bilibili.com"
else
PLATFORM="youtube"
SITE_NAME="YouTube"
SITE_DOMAIN="youtube.com"
fiBilibili 字幕需要登录凭据。始终使用 cookies 文件——若文件缺失或已过期(超过 30 天),则从 Chrome 中刷新:
if [ "$PLATFORM" = "bilibili" ]; then
BILI_COOKIES="${BILIBILI_COOKIES_FILE:-$HOME/bilibili_cookies.txt}"
NEED_REFRESH=false
if [ ! -f "$BILI_COOKIES" ]; then
NEED_REFRESH=true
elif [ "$(find "$BILI_COOKIES" -mtime +30 2>/dev/null | wc -l | tr -d ' ')" -gt 0 ]; then
echo "Bilibili cookies 已超过 30 天,正在刷新..."
NEED_REFRESH=true
fi
if [ "$NEED_REFRESH" = true ]; then
echo "从 Chrome 读取 cookies(一次性的密钥链提示)..."
yt-dlp --cookies-from-browser chrome --cookies "$BILI_COOKIES" \
--skip-download -i "https://www.bilibili.com/" 2>/dev/null
fi
COOKIE_ARGS="--cookies $BILI_COOKIES"
# 获取可用字幕语言列表 —— 捕获 stderr 以检测登录失败
LIST_OUTPUT=$(yt-dlp --list-subs $COOKIE_ARGS "$URL" 2>&1)
if echo "$LIST_OUTPUT" | grep -qi "login\|not logged\|需要登录\|please log"; then
echo ""
echo "❌ Bilibili cookies 已过期或无效。"
echo " 修复方法:删除 cookies 文件后重试 —— 它将重新从 Chrome 读取。"
echo " rm \"$BILI_COOKIES\""
rm -rf "$TMPDIR"
exit 1
fi
AVAIL_LANGS=$(echo "$LIST_OUTPUT" | awk '/^[a-z]/{print $1}' | grep -v "^Language$")
# 优先尝试 ai-zh,然后是 zh 变体,最后是 en
for lang in ai-zh zh-Hans zh-CN zh en; do
if echo "$AVAIL_LANGS" | grep -q "^${lang}$"; then
yt-dlp \
--write-sub \
--sub-langs "$lang" \
--skip-download \
--retries 3 \
-o "$TMPDIR/bili_%(id)s" \
$COOKIE_ARGS \
"$URL" 2>/dev/null
SUB_FILE=$(ls "$TMPDIR"/*.${lang}.* 2>/dev/null | head -1)
if [ -n "$SUB_FILE" ]; then
SUBTITLE_LANG="$lang"
break
fi
fi
done
fiif [ "$PLATFORM" = "youtube" ]; then
for lang in zh-Hans zh-CN zh en; do
yt-dlp \
--write-subs \
--write-auto-subs \
--sub-langs "$lang" \
--skip-download \
--sub-format vtt \
--retries 3 \
--sleep-requests 1 \
-o "$TMPDIR/yt_%(id)s" \
"$URL" 2>/dev/null
SUB_FILE=$(ls "$TMPDIR"/*.${lang}.vtt 2>/dev/null | head -1)
if [ -n "$SUB_FILE" ]; then
SUBTITLE_LANG="$lang"
break
fi
sleep 1
done
fiif [ -z "$SUB_FILE" ]; then
echo "此视频未找到字幕。"
echo " - 无手动上传的字幕"
echo " - 无自动生成的字幕"
echo "缺少字幕无法继续处理。"
rm -rf "$TMPDIR"
exit 1
fi检测格式(SRT 或 VTT),并进行相应清理:
EXT="${SUB_FILE##*.}"
if [ "$EXT" = "srt" ]; then
# SRT:移除序号、时间戳、HTML 标签、去重
grep -v "^[0-9]*$" "$SUB_FILE" \
| grep -v "^[0-9][0-9]:[0-9][0-9]:[0-9][0-9],[0-9]* --> " \
| sed 's/<[^>]*>//g' \
| grep -v "^$" \
| python3 -c "
import sys, html
seen = set()
for line in sys.stdin:
line = html.unescape(line).strip()
if line and line not in seen:
seen.add(line)
print(line)
print()
" > "$TMPDIR/cleaned.txt"
else
# VTT
sed 's/<[^>]*>//g' "$SUB_FILE" \
| grep -v "^WEBVTT" \
| grep -v "^NOTE" \
| grep -v "^Kind:" \
| grep -v "^Language:" \
| grep -v "^[0-9][0-9]:[0-9][0-9]:[0-9][0-9]" \
| grep -v "^$" \
| python3 -c "
import sys, html
seen = set()
for line in sys.stdin:
line = html.unescape(line).strip()
if line and line not in seen:
seen.add(line)
print(line)
print()
" > "$TMPDIR/cleaned.txt"
fiOUTPUT_DIR="${YOUTUBE_SUBTITLES_DIR:-.}"
mkdir -p "$OUTPUT_DIR"使用原视频标题作为文件名。仅移除 macOS 不支持的字符(/ 和 ASCII 冒号 :);保留其他所有字符,包括全角标点符号(如 :、《》、、)。限制长度为 100 个字符:
SLUG=$(echo "<title>" | python3 -c "
import sys
title = sys.stdin.read().strip()
title = title.replace('/', '').replace(':', '')
print(title[:100])
")使用 $COOKIE_ARGS(在步骤 2 中设置;YouTube 为空,Bilibili 为 --cookies FILE)
yt-dlp --dump-json --no-playlist $COOKIE_ARGS "$URL" 2>/dev/null \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
desc = d.get('description','')
first_para = desc.split('\n\n')[0].replace('\n',' ')[:300]
chapters = d.get('chapters') or []
chapter_lines = '\n'.join(f' - \"{int(c[\"start_time\"]//60)}:{int(c[\"start_time\"]%60):02d} {c[\"title\"]}\"' for c in chapters)
cats = d.get('categories') or []
print('TITLE:', d.get('title',''))
print('CHANNEL:', d.get('uploader',''))
print('DURATION:', d.get('duration_string',''))
print('DATE:', d.get('upload_date',''))
print('DESCRIPTION:', first_para)
print('CATEGORY:', cats[0] if cats else '')
print('CHAPTERS:')
print(chapter_lines)
"写入文件 $OUTPUT_DIR/$SLUG.md:
NOW=$(date +"%Y-%m-%dT%H:%M")
WORDS=$(wc -w < "$TMPDIR/cleaned.txt" | tr -d ' ')---
title: "<title>"
source: "<URL>"
author:
- "[[<channel>]]"
published: "<YYYYMMDD>"
description: "<DESCRIPTION>"
tags:
- "<PLATFORM>"
ctime: "<NOW>"
mtime: "<NOW>"
words: "<WORDS>"
site: "<SITE_NAME>"
domain: "<SITE_DOMAIN>"
channel: "<channel>"
duration: "<duration>"
category: "<CATEGORY>"
subtitle_lang: "<SUBTITLE_LANG>"
chapters:
<CHAPTERS or empty>
type: "source"
---
<完整的清洗后字幕文本>CHARS=$(wc -c < "$TMPDIR/cleaned.txt" | tr -d ' ')≤ 120000:直接使用完整清洗后的文本作为摘要输入。> 120000:需先执行分治处理(步骤 8),再将合并后的要点作为摘要输入。将文本拆分为约每块 10,000 字符的片段:
python3 - <<'EOF'
text = open("$TMPDIR/cleaned.txt").read()
size = 10000
chunks = [text[i:i+size] for i in range(0, len(text), size)]
for i, chunk in enumerate(chunks):
print(f"=== CHUNK {i+1}/{len(chunks)} ===")
print(chunk)
EOF对每个片段,提取结构化笔记,格式如下:
TOPIC: [该片段的推断主题名称,例如“AI泡沫评估”]
- [关键观点或事实 — 2 句话:内容陈述 + 支持细节]
- [关键观点或事实 — 2 句话]
...
QUOTES: [1–3 条值得保留的原文引用]
DATA: [任何具体数字或指标]每块提取 8–12 个条目。收集所有结构化笔记,按主题归类,作为步骤 9 的摘要输入。在传递给步骤 9 前,需合并相同主题下的跨块笔记。
确定摘要语言:
if [ -n "$YOUTUBE_SUBTITLES_SUMMARY_LANG" ]; then
SUMMARY_LANG="$YOUTUBE_SUBTITLES_SUMMARY_LANG"
elif [[ "$SUBTITLE_LANG" == zh* ]]; then
SUMMARY_LANG="zh"
else
_SYS_LANG="${LANG:-${LANGUAGE:-}}"
case "$_SYS_LANG" in
zh*) SUMMARY_LANG="zh" ;;
*) SUMMARY_LANG="en" ;;
esac
fi使用步骤 7 或 8 提供的摘要输入,生成摘要正文(所有章节),并先写入临时文件:
cat > "$TMPDIR/summary_body.md" <<'SUMMARY_EOF'
<生成的摘要章节内容>
SUMMARY_EOF
SUMMARY_WORDS=$(wc -w < "$TMPDIR/summary_body.md" | tr -d ' ')然后将前文和正文合并,写入最终文件 $OUTPUT_DIR/$SLUG-summary.md,语言为 $SUMMARY_LANG:
cat > "$OUTPUT_DIR/$SLUG-summary.md" <<EOF
<前文信息>
EOF
cat "$TMPDIR/summary_body.md" >> "$OUTPUT_DIR/$SLUG-summary.md"前文模板:
---
title: "<title>"
source: "<URL>"
author:
published: "<YYYYMMDD>"
description: "<DESCRIPTION>"
tags:
- "<PLATFORM>"
ctime: "<NOW>"
mtime: "<NOW>"
words: "<SUMMARY_WORDS>"
site: "<SITE_NAME>"
domain: "<SITE_DOMAIN>"
channel: "<channel>"
duration: "<duration>"
category: "<CATEGORY>"
subtitle_lang: "<SUBTITLE_LANG>"
chapters:
<CHAPTERS or empty>
type: "source"
lang: "<SUMMARY_LANG>"
---章节标题必须与摘要语言一致。根据语言选择对应标题:
**英文 (en):**
## Overall Summary / ## Topic Chapters / ## Key Quotes / ## Novel Ideas / ## Counter-intuitive Views / ## Core Tensions / ## Methodology / ## Key Data
**中文 (zh):**
## 总体摘要 / ## 话题章节 / ## 关键引用 / ## 新颖观点 / ## 反直觉观点 / ## 核心张力 / ## 方法论 / ## 关键数据
直接陈述内容。禁止在每条要点前添加归属语句,如“X 认为”“X 指出”“X 表示”“据 X 所述”等,无论何种语言。说话人已在前文中明确标识,重复其姓名是冗余信息。应直接陈述观点本身。
错误示例:“演讲者认为 API 业务没有护城河,因为用户毫无忠诚度。”
正确示例:“API 业务没有护城河——用户会立即转向更便宜或更好的替代方案。”
每个要点必须包含 2–3 句话以上:
单句要点不可接受。若无法写出 2–3 句话来阐述某一点,说明该点过于薄弱,应直接省略,而非强行填充。
### [话题名称] 小节,包含3–5个要点,每条2–3句话。话题应反映实际对话流程(例如“OpenAI战略”、“AI泡沫评估”、“中美对比”、“投资哲学”)。涵盖所有重要话题——不要因某话题看似次要就忽略。即使是短暂或个人化的片段(如电视节目类比、提及的爱好),只要承载有意义的思想,也应单独设立小节。省略源内容无关的任何部分。不得编造信息。
rm -rf "$TMPDIR"原始文件: $OUTPUT_DIR/$SLUG.md
摘要文件: $OUTPUT_DIR/$SLUG-summary.md已收录 1 个 Skill