分享链接先归一化

短视频平台的链接很杂。用户可能给短链、口令文本、移动端链接、桌面端链接,也可能把一整段分享文案贴进来。工具的第一步不是解析视频,而是从输入里找 URL,跟随安全的跳转,并把最终地址归一化。

这一步要小心 SSRF 风险。工具不应该随便请求内网地址、本机地址或奇怪协议。只处理明确支持的平台域名,能少很多麻烦。

Shark-Tools 的 KuKu 解析界面支持「每行一个链接」的批量模式。我把它抽象成更通用的输入清洗逻辑:

import re
from urllib.parse import urlparse

SUPPORTED_HOST_KEYWORDS = ("douyin", "kuaishou", "xiaohongshu", "bilibili")

def extract_urls(text: str) -> list[str]:
    candidates = re.findall(r"https?://[^\s,,,。]+", text)
    urls = []
    for raw in candidates:
        parsed = urlparse(raw.strip())
        host = parsed.netloc.lower()
        if parsed.scheme in {"http", "https"} and any(key in host for key in SUPPORTED_HOST_KEYWORDS):
            urls.append(raw.strip())
    return list(dict.fromkeys(urls))

这里的重点是先做「提取 + 去重 + 平台白名单」,而不是拿整段分享文案直接请求。

多层抽取

页面里可能有结构化状态、内联 JSON、meta 标签,也可能只有服务端渲染后的少量信息。解析器应该按稳定程度排序,优先读取结构化数据,再用页面状态和 meta 作为兜底。

不同平台的字段不一样,但输出最好统一。比如都整理成 platformtitleauthorcoverdurationvideo_urlssource_urlerror。这样 Agent 不需要知道平台细节,也能继续做摘要或素材入库。

MCP 封装的好处

把短视频解析放进 Python MCP 服务里,Java 后端就不用直接依赖每个平台的解析细节。MCP 工具只暴露清楚的函数,比如解析抖音、解析快手、解析小红书。后端注册工具后,Agent 可以按意图调用。

这种拆法还有一个实际好处:平台解析经常需要小修小补,Python 工具服务独立部署会更轻。只要输出 Schema 稳定,上层工作台就不会被每次小改动牵着走。

桌面端工具也有类似分层。GUI 只负责收集输入、展示状态、接收结果;真正的解析放到后台线程里:

import threading

def start_parse(urls: list[str], parser, signals):
    def run_batch():
        total = len(urls)
        for index, url in enumerate(urls, start=1):
            try:
                result = parser.parse(url)
                signals.item_success.emit(index, total, result)
            except Exception as exc:
                signals.item_error.emit(index, total, str(exc))
        signals.finished.emit()

    threading.Thread(target=run_batch, daemon=True).start()

这个模式迁到 Web 后端就是队列、任务状态和 SSE;迁到 MCP 工具就是一次工具调用返回结构化结果。

失败要说人话

短视频解析失败很常见。链接过期、内容删除、平台改版、网络超时、访问需要登录,这些都应该是不同错误。工具返回「解析失败」四个字,对用户和 Agent 都没什么帮助。

我更喜欢返回可解释的错误:链接无效就提示换原始链接,需要登录就停止,不做绕过;平台改版就标成 platform_changed,方便后续维护。

我现在会把解析结果固定成下面这种格式,成功和失败都能被上层统一处理:

def ok(platform: str, source_url: str, data: dict) -> dict:
    return {
        "ok": True,
        "platform": platform,
        "source_url": source_url,
        "title": data.get("title", ""),
        "cover": data.get("cover", ""),
        "videos": data.get("videos", []),
    }

def fail(platform: str, source_url: str, code: str, message: str) -> dict:
    return {
        "ok": False,
        "platform": platform,
        "source_url": source_url,
        "error": {"code": code, "message": message},
    }

有了这个结构,Agent 不需要猜“空列表”是什么意思,前端也能把失败原因显示得更自然。

合规是底线

短视频解析最容易滑向灰色地带,所以边界要写在工具设计里。不要绕过登录,不批量抓取,不突破访问权限,不把别人的内容当作可以无限复用的资源。

对我来说,这类工具的价值是把自己有权查看的公开内容变成可整理的素材,而不是挑战平台的限制。技术要锋利,也要有鞘。