.agents/skills/datasource-test/SKILL.md
给定一份数据源配置 JSON,按本 skill 的流程判定它:
失败时要定位到具体引擎步骤和配置字段,给出修复,并复测。
失败还有一种正当结局: 网站形态太多,现有流程必然覆盖不了所有站点——站点可能超出 selector 引擎或 WebVideoExtractor 等组件的现有能力。此时不要硬调配置,走第 9 节的 「能力进化」流程: 取证 → 写缺口文档 → 交给隔离 subagent 实现新能力(输出 git patch) → 复测。
所有测试通过 ani-datasource-test MCP(由 tools/datasource-test-mcp 提供的 HTTP MCP server)完成。
它直接调用 App 的 SelectorMediaSourceEngine 真实代码,行为与 App 一致。两份参考文档:
tools/datasource-test-mcp/README.md — server 能力总览、多线路语义、运行方式;get_selector_engine_docs — 每个引擎步骤的输入/输出/常见问题/最小配置示例
(源文件 tools/datasource-test-mcp/src/main/resources/selector-engine-docs.md)。
开始调试 selector 源前先调用一次它,里面的步骤语义本 skill 不再重复。本 skill 只通过一个名为 ani-datasource-test 的 HTTP MCP server 工作,其工具形如
mcp__ani-datasource-test__validate_selector_config、mcp__ani-datasource-test__selector_resolve_episode 等。
开始任何测试前先确认该 MCP 已就绪: 在当前可用工具里查找 mcp__ani-datasource-test__*
(如 validate_selector_config);无法确认时,试调用一次
mcp__ani-datasource-test__validate_selector_config,返回「未知工具 / 未连接 / 连接错误」即视为不可用。
installDist 或后台启动 server,
不要用 curl 直连 HTTP 端口绕过。向用户输出下面的「添加方法」,请他们配置好
ani-datasource-test MCP 后重新发起测试。第一步——用户在 animeko 仓库根目录启动 HTTP server,并保持它运行:
./gradlew :tools:datasource-test-mcp:installDist
tools/datasource-test-mcp/launcher # unix / macOS
tools\datasource-test-mcp\build\install\datasource-test-mcp\bin\datasource-test-mcp.bat # Windows
默认监听 http://127.0.0.1:8264/mcp(可加 --host / --port 覆盖)。
第二步——在 agent 客户端里把它注册为名为 ani-datasource-test 的 HTTP MCP:
claude mcp add --transport http ani-datasource-test http://127.0.0.1:8264/mcp
.mcp.json:
{ "mcpServers": { "ani-datasource-test": { "type": "http", "url": "http://127.0.0.1:8264/mcp" } } }
~/.codex/config.toml 增加(以所用 Codex 版本的文档为准):
[mcp_servers.ani-datasource-test]
url = "http://127.0.0.1:8264/mcp"
配置后重启 / 重新连接客户端使 mcp__ani-datasource-test__* 工具生效,再重新发起测试。
url 让工具自行抓取,只在离线迭代必须传 html 时才内联,并留意上下文占用。如果当前还能用一个 chrome-devtools MCP(工具形如 mcp__chrome-devtools__*,如
navigate_page / take_snapshot / list_network_requests / get_network_request / click /
evaluate_script / list_console_messages;具体名称以所连接的 server 为准),把它与
ani-datasource-test 结合使用。它不是必需的——没有也能跑本 skill,只是遇到"工具解析结果与
站点实际不符"时少了直接对照手段。没有就别自己启动浏览器,按现有证据推进或转第 9 节取证。
为什么有用: ani-datasource-test 引擎是无 JS 的静态 HTTP 抓取 + 解析,chrome-devtools
驱动的是真实浏览器(执行 JS、能交互)。当某一步在 MCP 工具里跑不通、站点在浏览器里却正常时,
浏览器看到的才是"真相",两者的差异本身就是诊断信号。
怎么用(按需, 不是每步都跑;详细触发点见第 4、6、9 节):
navigate_page 打开对应页面(搜索页代入真实关键词 / 条目详情页 / playUrl)→
take_snapshot 看渲染后 DOM,与 MCP 工具返回的静态 HTML 对比;list_network_requests
看真实请求(搜索是否 POST/带 token、是否有返回数据的 JSON 接口、有没有 m3u8/mp4 媒体请求)。json-path-indexed,
否则是缺口);媒体请求要 click 播放器后才出现 → click-gate,WebVideoExtractor 缺口。关键纪律: chrome-devtools 只用来取真相、定位问题。selector/正则的修正最终必须回到
ani-datasource-test 的离线步骤、用工具实际抓到的静态 HTML(而非浏览器渲染后的 DOM)验证通过——
因为 App 里跑的就是那个无 JS 引擎。别把只在浏览器渲染后才成立的 selector 当作修好了。
先解析用户给的 JSON,按字段判定:
| 特征 | 类型 | 测试路径 |
|---|---|---|
factoryId == "web-selector"(或含 searchConfig/searchUrl) | Selector (CSS) 源 | 本 skill 第 2–6 步全套 |
factoryId == "rss" / "dmhy" / "mikan" / "jellyfin" / "emby" / "ikaros" 等 | 其他工厂 | 只走第 7 步 test_subject_episode_source |
{"mediaSources": [...]} | 订阅列表 | 逐个取出按上两行处理(selector 工具默认只取第一个 web-selector 源) |
selector 工具的 config 参数接受四种形态(App 导出 / 裸 arguments / 裸 searchConfig / 订阅列表),
原样传入即可,无需手动拆包。
validate_selector_config(config)
issues[] 按 severity 分 error / warning / info,每条带精确的字段路径;(?<ep>) 分组」会导致集数解析失败;⚠️ 命名区分:
search_subjects(下面这个,Ani API 元数据)与引擎步骤searchSubjects(selector_run_step的 step 值,请求目标站)只差一个下划线,是两回事。
search_subjects(query=番名) 拿 subjectId;
用户没指定 → get_trends() 挑一部热门当季番(目标站大概率收录;老站可再补一部完结老番);get_subject_episodes(subjectId) 拿剧集列表,通常选第 1 集的 episodeId(整数);keyword / query.subjectName 用。Ani API 冷启动可能要 40s+(server 已设 90s 超时),首次调用慢是正常的,不要过早判死。
selector_resolve_episode(subjectId, episodeId, config, extractVideo=false)
extractVideo=false 只测解析层: 快、不启动 CEF/VLC、不弹窗。返回:
ok + summary;steps[]: 每步 {name, status: success|failed, summary, details, errors, durationMillis};medias[]: 找到的候选播放页(含线路、集数、playUrl)。从第一个 status=failed 的步骤开始排查,对照下表行动:
| 最先失败的步骤 | 常见症状 (看 summary/details) | 下一步动作 |
|---|---|---|
aniMetadata | Ani API 拿不到条目/剧集 | 检查 subjectId/episodeId 是否为有效整数;网络问题则重试一次 |
searchSubjects | 404 | searchUrl 模板错误或站点换了搜索路径;有 chrome-devtools 时 navigate_page+list_network_requests 看站点真实搜索请求的 URL/method,否则 curl 人工确认 |
searchSubjects | captchaKind 非空 | 停。本工具无法过验证码,报告用户改用 App 内设置页的数据源测试器 |
selectSubjects | 解析出 0 个条目 | 转第 5 步: 拿搜索页 HTML 离线迭代 subjectFormat 的 selector;若 HTML 其实是 JSON,改用 json-path-indexed。有 chrome-devtools 时先 take_snapshot 对比:浏览器有条目而工具 HTML 没有 → JS 渲染,查 JSON 接口或转第 9 节 |
searchEpisodes | 条目详情页 404 | 条目链接拼接错误,检查 rawBaseUrl(留空时从 searchUrl 推断,details 里能看到实际 URL) |
selectEpisodes | 0 个剧集 / 0 条线路 | 转第 5 步: 拿详情页 HTML 离线迭代 channelFormat 的 selector;同上,可用 chrome-devtools 的渲染后 DOM 判断是 selector 问题还是 JS 渲染 |
selectEpisodes | 剧集有了但 episodeSort 全空 | matchEpisodeSortFromName 正则没匹配上剧集名(需要 (?<ep>) 分组);对照 details 里的剧集名调正则 |
selectMedia | filteredCount=0 但 original 非空 | 看 details 里的 filteredOut: 集数对不上(sort vs ep、电影/OVA 特判)或该条目根本不含目标集。也可临时把 config 的 filterByEpisodeSort 设 false 验证是过滤问题还是解析问题 |
全部成功但 medias 为空 | 目标条目排名靠后被截断 | 调大 maxSubjectsPerName(默认 3,App 内无此限制;条目按名称长度升序排) |
selectSubjects/selectEpisodes解析不出来时,「配置没写对」和「页面靠 JS 渲染(引擎能力缺口)」 表现相同(都是 0 条)。有 chrome-devtools MCP 就用它一步分辨:navigate_page后take_snapshot的渲染 DOM 若与工具返回的静态 HTML 结构一致,就是 selector 问题(转第 5 步);若渲染后才有数据, 就是 JS 渲染。但修好的 selector 必须在工具的静态 HTML 上验证通过(见第 0 节纪律)。
selector_run_step 参数速查(完整 schema 见 tools/list):
| step | 必需参数 | 说明 |
|---|---|---|
searchSubjects | config, keyword | 真实请求搜索页,返回 HTML(默认 100k 截断,maxHtmlLength 可调) |
selectSubjects | config, url 或 html | 解析条目列表;传 html 即离线 |
searchEpisodes | url | 抓条目详情页,返回 HTML |
selectEpisodes | config, url 或 html+subjectUrl | 解析线路/剧集;离线传 html 必须带真实 subjectUrl,相对链接以条目页所在目录为基,否则 playUrl 拼错 |
selectMedia | config, episodes, query | episodes 直接用 selectEpisodes 输出的数组({channel?, name, episodeSort?, playUrl});query 为 {subjectName, episodeSort, allSubjectNames?, episodeEp?, episodeName?} |
matchWebVideo | config, url | 离线测一个 URL 判为 视频/嵌套页/忽略 |
extractVideo | url(可选 config) | 真实 CEF WebView 加载播放页拦截视频 URL |
离线迭代 selector 的标准循环(改选择器时用,避免反复打站点):
searchSubjects / searchEpisodes 抓一次 HTML(页面超过 100k 时调大 maxHtmlLength,截断的 HTML 解析会失真);selectSubjects(html=...) / selectEpisodes(html=..., subjectUrl=...) 离线验证,重复 2–3 直到解析正确;url 让它现抓——但每次都是真实请求,修改配置期间优先离线;解析层通过后:
navigate_page 打开 playUrl、
list_network_requests 过滤 m3u8/mp4/媒体请求拿到真实 URL 与请求头(最可靠);否则从
playUrl 结构猜。用 matchWebVideo(config, url) 确认判定(matched / loadPage / continue)。
注意嵌套页判定优先于视频判定,matchNestedUrl 写太宽会把视频 URL 当页面加载导致永远超时;selector_run_step(step=extractVideo, url=playUrl, config, probeResolvedVideo=true),
或直接 selector_resolve_episode(..., extractVideo=true, probeVideo=true)(默认值)跑完整链路。
会启动 CEF(首次初始化下载/解压较慢)与 VLC(可能弹播放窗口);extractMode=all_channels 可逐线路全测;probe_video(videoUrl, headers) —— HTTP 探测 + VLC 真实播放几秒,报告分辨率/时长/编码。
headers 用 extractVideo 返回的(含 Referer/User-Agent,防盗链站点必需);showWindow=false 可不弹窗。
系统未装 VLC 时自动降级为仅 HTTP 探测,结论要相应弱化为「HTTP 可达」而非「可播放」。WebView 拦不到视频 URL 时: 看 extractVideo 返回的 diagnostics(拦截到的请求样本),
从中找出真实视频 URL 的模式,回去改 matchVideoUrl / matchNestedUrl,先用 matchWebVideo 离线验证再重跑。
若 diagnostics 显示页面已正常加载却完全没有媒体类请求,用 chrome-devtools 复现对照:
navigate_page 打开 playUrl → list_network_requests 看有无媒体请求 → 若没有,click 播放器按钮后
再 list_network_requests;若点击后才出现视频请求(而 App 的 WebViewExtractor 不会点击),
这不是配置问题,是 WebVideoExtractor 的能力缺口,把这段 devtools 证据带上转第 9 节。
(没有 chrome-devtools 时,凭 diagnostics 无媒体请求这一点也可推断,但证据较弱。)
test_subject_episode_source(subjectId, episodeId,
mediaSource={factoryId: "...", serializedArguments: {...arguments 对象...}})
SelectorMediaSource 跑原生 fetch()(含限流、冷却重试、无条目数截断),
是与 App 行为严格一致的最终复核——selector_resolve_episode 全绿后建议跑一次;serializedArguments 传配置里的 arguments 对象;candidateTestMode=all_channels 逐个候选测试;handshakeFailureDomainHint(可能的换域名提示),报告给用户但不要自动改配置。结论必须分层陈述,不要混为一个"能用/不能用":
| 层 | 通过标准 |
|---|---|
| L0 配置 | validate_selector_config 无 error |
| L1 解析 | selector_resolve_episode(extractVideo=false) 的 medias 非空(找到目标集数的候选) |
| L2 视频 | extractResults 中至少一个候选 resolvedVideo 非空 |
| L3 播放 | 顶层 ok=true(至少一条线路通过播放探测;first_success 模式下即首条通过的线路) |
报告内容: 每层结果与证据(候选数、可用线路名、视频 URL、探测到的分辨率/时长);失败层的定位 (哪一步、哪个字段、site 端还是配置端);对配置做过的每一处修改;以及修正后的完整配置 JSON。 测试用的条目/集数也要写明(结论仅对被测集数成立)。
若判定为能力缺口(第 9 节),报告还需附: 进化需求文档路径、subagent 产出的 patch 路径、 应用 patch 后的复测结果。
现有 selector 流程与 WebView 拦截覆盖不了所有网站。测试的正当结局除了「通过」与「配置修好了」, 还有第三种: 发现组件缺少某种能力。此时按本节把缺口整理成文档,交给独立 subagent 实现新能力 (输出 git patch),主会话应用后复测——这就是「进化」: 在检测数据源的过程中发现自身能力的不足并改进。 可进化的不只是 selector 引擎,WebVideoExtractor、matchVideo 等组件同样适用。
宣布「做不到」之前,必须同时满足:
validate_selector_config 无 error;典型缺口与确证方法(非穷尽,遇到新形态照同样思路取证):
| 组件 | 缺口示例 | 如何确证(chrome-devtools MCP) |
|---|---|---|
| 引擎·搜索 | 搜索要用 POST / 需要动态 token / 需要登录态 | 工具 fetch 搜索 URL 拿到的不是结果页;navigate_page 搜索页 + list_network_requests 看真实搜索请求的 method 与参数 |
| 引擎·解析 | 条目/剧集列表由 JS 动态渲染,静态 HTML 无数据 | 工具 searchSubjects/searchEpisodes 的 HTML 里搜不到条目文本,但 take_snapshot 的渲染 DOM 里有(引擎无 JS) |
| 引擎·解析 | 搜索结果/剧集列表分页,目标在后面几页 | 首页 HTML 只含前 N 项加分页控件;take_snapshot 确认目标在翻页/加载更多之后 |
| 引擎·selectMedia | playUrl 需要由 JS/接口计算,<a> 里没有 | HTML 里的 href 是 javascript:... 或占位符;list_network_requests 看点击后计算 playUrl 的接口 |
| WebVideoExtractor | 需要用户手势(点一下播放器)才触发视频加载 | extractVideo 的 diagnostics 无媒体请求;navigate_page playUrl 后 list_network_requests 也无,click 播放器后再看才出现 m3u8/mp4 |
| WebVideoExtractor | 视频走 blob:/MSE/WebSocket,没有可拦截的 HTTP 媒体 URL | list_network_requests 里只有 blob/WS,无媒体 HTTP 请求 |
| matchVideo | 判定需要看响应体/响应头,URL 层面区分不了视频与广告 | 两类 URL 结构相同;get_network_request 看响应体/头才能区分 |
路径: docs/evolution/<yyyyMMdd>-<组件>-<短slug>.md(目录不存在就创建)。固定结构:
# 需要的新能力: <一句话>
- **组件**: SelectorMediaSourceEngine / WebViewVideoExtractor(注明平台) / matchVideo / ...
- **触发站点**: <URL>(最小复现配置与测试条目/集数见「复现材料」)
- **现象与证据**: <ani-datasource-test 的工具调用序列与关键输出摘录;
chrome-devtools 复现的观察——真实网络请求 / 渲染后 DOM / 点击后才出现的请求等,尽量贴具体条目>
- **为什么现有能力做不到**: <落到具体代码与机制,例如「WebView 拦截器
(AndroidWebMediaResolver.shouldInterceptRequest / DesktopWebMediaResolver 的
onBeforeResourceLoad)只被动监听页面发出的请求,从不与页面交互,而该站点的
播放器要在用户点击后才创建 video 元素并发起请求」>
- **新能力的行为定义**: <精确描述期望行为;建议的配置字段名、类型、默认值与 JSON 示例;
默认值必须等价于旧行为>
- **兼容性约束**: 不改变现有源的行为;涉及 WebView 的能力需说明
commonMain 接口与 androidMain/desktopMain/appleMain 三端实现
- **验收标准**: 用触发站点跑通第 4–6 步;`:app:shared:app-data:desktopTest`
与 `:tools:datasource-test-mcp:test` 全绿
- **相关代码入口**: <文件清单>
## 复现材料
<最小配置 JSON、subjectId/episodeId、失败的工具调用参数与输出>
「为什么做不到」必须先读过相关源码、落到代码事实上,不要停留在现象描述——这一节的质量 直接决定 subagent 能否正确实现。
进化实现不在验证会话里做——验证会话不修改 App 源码。启动一个独立 subagent
(Claude Code 用 Agent 工具、general-purpose 类型,建议 isolation: worktree 隔离工作区;
其他环境开新会话),prompt 必须自包含,要点:
读 docs/evolution/<文档名>.md,按其中「新能力的行为定义」在 animeko 仓库实现该能力。
1. 只依据该文档与仓库源码工作;不运行 MCP server,不做数据源验证(那是主会话的事);
2. 新配置字段的默认值必须保持旧行为;涉及 WebView 的能力要覆盖 commonMain 接口
与 androidMain/desktopMain/appleMain 各端实现;
3. 为新逻辑补单元测试;跑 :app:shared:app-data:desktopTest 与
:tools:datasource-test-mcp:test,确认全绿;
4. 不 commit、不 push;最后用 git diff 把全部改动输出为 patch,
保存到 docs/evolution/<同名>.patch,报告 patch 路径与改动文件清单。
subagent 返回后由主会话闭环:
ani-datasource-test server 仍是旧代码——
请用户重新 installDist 并重启该 server(同第 0 节),使新构建生效;agent 不自行启动 server;requestInterval 语义);