CapyPlayer 组件开发指南
1. 说明
CapyPlayer 组件是一个公开可访问的 .js 脚本,运行在沙箱环境中。通过编写组件,你可以接入一些第三方媒体站点或 API,自定义的媒体内容来源,丰富展示方式。
关键规则:
WidgetMetadata必须是全局var,不能用const/let。modules[].functionName必须和全局async function名字完全一致。- 数据源函数必须返回数组(
MediaItem[]),不能返回对象/null。 loadDetail只在条目没有videoUrl且有link时触发。
2. 基础示例
步骤 1:定义元数据
var WidgetMetadata = {
id: "my_source",
title: "我的片源",
description: "示例组件",
version: "1.0.0",
modules: [
{
id: "popular",
title: "热门",
type: "media_list",
functionName: "getPopular",
cacheDuration: 3600,
params: [{ name: "page", label: "页码", type: "page" }]
}
]
};
步骤 2:实现模块函数
async function getPopular(params) {
var data = await apiGet("/popular", { page: params.page || 1 });
return ensureArray(data.results).map(mapItem);
}
步骤 3:实现 loadDetail
async function loadDetail(link) {
var data = await apiGetAbsolute(link);
if (data.video_url) {
return {
title: data.title || "",
videoUrl: String(data.video_url)
};
}
return {
title: data.title || "",
episodeItems: ensureArray(data.episodes).map(function(ep, idx) {
return {
title: ep.name || ep.title || ("第" + (idx + 1) + "集"),
videoUrl: String(ep.url || "")
};
}).filter(function(ep) {
return !!ep.videoUrl;
})
};
}
步骤 4:发布并安装
- 将脚本托管到 GitHub Raw/Gist 或任意可公开访问 URL。
- App 内进入「组件」页,点击
+,粘贴 URL,预览后安装。
3. 契约规范
3.0 字段支持级别
| 级别 | 定义 | 示例 |
|---|---|---|
| 推荐 | 官方规范字段,优先使用 | title、modules、functionName、timeoutSeconds |
| 兼容 | 为兼容历史脚本保留,建议迁移到推荐字段 | name、iconUrl、timeoutMs、timeout、retry |
3.1 WidgetMetadata
| 字段 | 必填 | 说明 |
|---|---|---|
title 或 name |
✓ | 组件显示名(二选一) |
modules |
✓ | 至少一个模块 |
id |
建议 | 组件唯一 ID,建议使用 [a-zA-Z0-9._-] |
description |
组件描述 | |
author |
作者名 | |
version |
版本号,默认 1.0.0 |
|
site |
组件主页/站点 URL | |
icon / iconUrl |
组件图标 URL(两者均可) | |
globalParams |
自动合并到所有模块参数 |
兼容与解析说明:
- 若
title缺失,系统会回退读取name。 - 若
icon缺失,系统会回退读取iconUrl。 id中非[a-zA-Z0-9._-]字符会被替换为_。- 若未提供
id,系统会按脚本来源 URL 生成稳定 ID;来源 URL 改变会导致组件 ID 改变。
3.2 Module
| 字段 | 必填 | 默认 | 说明 |
|---|---|---|---|
id |
functionName |
模块 ID,缺省时使用 functionName |
|
title |
✓ | - | 显示名(也可用 name) |
functionName |
✓ | - | 全局函数名 |
type |
- | 模块类型,见 3.3,建议显式填写 | |
description |
- | 模块描述 | |
params |
[] |
参数配置 | |
cacheDuration |
3600 |
缓存秒数 | |
timeoutSeconds |
30 |
请求超时秒数 | |
retryCount |
1 |
请求重试次数 |
兼容字段说明:
- 超时兼容:
timeoutMs、timeout会折算为秒级超时,优先级低于timeoutSeconds。 - 重试兼容:
retry会回退映射到retryCount。
3.3 模块类型(modules[].type)
| type | 说明 |
|---|---|
media_list |
媒体列表模块(默认,可不填) |
category |
分类列表模块,返回 WidgetSystemCategory[] |
兼容说明:
- 不填
type时行为等同于media_list(type字段不会被自动补填)。 list(兼容值)行为等同于media_list,type值原样保留不做转换。type值会在 UI 中作为标签展示。
3.4 参数字段(params[])
| 字段 | 必填 | 说明 |
|---|---|---|
name |
✓ | 参数键名,传入函数时使用此名 |
type |
✓ | 参数类型,见下方类型表 |
label / title |
用户界面显示名(二选一) | |
description |
参数说明文字 | |
defaultValue / value |
默认值(两者均可) | |
required |
是否必填,默认 false |
|
enumOptions / enumValues |
enum 类型的选项列表,支持字符串数组或 { title, value } 对象数组 |
|
placeholders |
候选值列表,结构同 enumOptions,用于输入框的快捷选项 |
|
belongTo |
条件显示:{ paramName, value },value 支持字符串或数组,仅当指定参数命中其中一个值时展示 |
3.5 参数类型(params[].type)
| type | 用户可见 | 说明 |
|---|---|---|
string |
✓ | 文本输入 |
number |
✓ | 数字输入 |
enum / enumeration |
✓ | 下拉(需 enumOptions) |
boolean |
✓ | 开关 |
page |
系统注入页码(从 1 开始) | |
offset |
系统注入偏移量 | |
count |
系统注入每页数量 | |
language |
✓ | 设备语言,可修改 |
constant |
不展示,始终传 defaultValue |
3.6 MediaItem(返回项)
最小必填字段:
id: stringtitle或name
常用字段:
mediaType:movie/tv/series/show/collectionposterUrl、backdropUrldescription、rating、yeartmdbId、imdbId、doubanIdlink(触发loadDetail)videoUrl(直接播放)
字段映射优先级(建议按此输出,减少歧义):
| 语义 | 优先读取顺序 |
|---|---|
| 标题 | title -> name |
| 条目 ID | id -> mediaId |
| 海报图 | posterUrl -> poster_url -> poster -> posterPath |
| 背景图 | backdropUrl -> backdrop_url -> backdrop -> coverUrl -> backdropPath |
| 上映日期 | releaseDate -> release_date -> date |
| 评分 | rating |
| 直链播放 | videoUrl -> video_url |
| 详情链接 | link |
季集字段兼容表(TV):
| 语义 | 推荐字段 | 兼容字段(按优先级) | 说明 |
|---|---|---|---|
| 媒体类型 | mediaType |
mediaType -> media_type |
建议明确返回 tv / series / show,避免歧义 |
| 季总数 | seasonCount |
seasonCount -> season_count |
未提供时可由 seasons 长度推断 |
| 集总数 | episodeCount |
episodeCount -> episode_count -> episode |
未提供时可由季内集数汇总 |
| 当前季号 | currentSeason |
currentSeason -> current_season -> season -> seasonNumber -> season_number |
用于详情页默认选中 |
| 当前集号 | currentEpisode |
currentEpisode -> current_episode -> episodeNumber -> episode_number |
用于详情页默认选中 |
| 当前季 ID | currentSeasonId |
currentSeasonId -> current_season_id -> seasonId -> season_id -> selectedSeasonId -> selected_season_id |
可与季号同时返回 |
| 当前集 ID | currentEpisodeId |
currentEpisodeId -> current_episode_id -> episodeId -> episode_id -> selectedEpisodeId -> selected_episode_id |
可与集号同时返回 |
| 当前集标题 | currentEpisodeName |
currentEpisodeName -> current_episode_name -> episodeTitle -> episode_title -> currentEpisodeTitle -> current_episode_title |
可选 |
| 季列表 | seasons |
seasons -> seasonItems -> season_items |
每项建议包含季号和集列表 |
| 季内集列表 | episodes |
episodes -> episodeItems -> episode_items |
建议每集包含 episodeNumber、title、videoUrl |
TV 推荐返回结构:
{
id: "tv.1001",
title: "Demo Show",
mediaType: "tv",
seasonCount: 2,
episodeCount: 24,
currentSeason: 1,
currentEpisode: 3,
seasons: [
{
id: "s1",
seasonNumber: 1,
title: "Season 1",
episodeCount: 12,
episodes: [
{ id: "s1e1", episodeNumber: 1, title: "E01", videoUrl: "https://..." }
]
}
]
}
补充说明:
- 若返回季列表,建议总是显式提供
mediaType: "tv"。 - 顶层
episodeItems主要用于 TV 条目;电影条目不建议复用该结构表示播放线路。 - 单集仅有播放地址时,优先放在
videoUrl;仅有link时由loadDetail继续解析。
返回约束:
- 函数必须返回数组,空结果返回
[]。 id必须转字符串并在同一列表内唯一。videoUrl优先级高于link。
3.7 loadDetail 返回结构
单集:
{ title: "标题", videoUrl: "https://...m3u8" }
多集:
{
title: "剧名",
episodeItems: [{ title: "第1集", videoUrl: "https://...m3u8" }]
}
多播放线路(可选):
[
{ title: "线路1", videoUrl: "https://cdn1/...m3u8" },
{ title: "线路2", videoUrl: "https://cdn2/...m3u8" }
]
解析规则说明:
- 支持返回对象或数组。
- 返回数组时,会优先选择”包含直链字段”的第一项作为主播放对象;若没有,则使用第一项。
- 返回数组且长度大于 1 时,系统会自动补充
playSources(若对象已包含playSources则不覆盖)。 - 返回对象且缺少
videoUrl时,系统会尝试从episodeItems/episode_items/episodes中提取首个可用播放地址。 - 直链字段识别顺序:
videoUrl->video_url->url->link。
3.8 参数合并优先级
参数最终值按以下顺序合并,后者覆盖前者:
globalParams默认值。modules[].params(同名参数覆盖全局同名参数配置)。- 运行时参数(页面传入或测试面板输入)。
3.9 分类数据源协议(Categories)
除媒体列表外,系统也支持分类列表返回(用于分类卡片等场景)。
返回结构:
[
{
id: "action",
name: "动作",
icon: "https://example.com/icon.png",
params: { genre: "action", page: 1 }
}
]
字段说明:
id:分类 ID(建议稳定唯一)。name:分类名称。icon:分类图标(可选)。params:点击分类后透传给内容数据源的参数对象。
3.10 安全约束(functionName)
functionName 仅允许“标识符或点路径”格式,例如:
- 合法:
getPopular、api.list - 非法:
list()、api['list']、a;b
4. 参考实现结构
建议采用:
请求层 -> 解析层 -> 映射层,便于维护与接口调整。
var API_BASE = "https://api.example.com";
function safeJson(data) {
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (_) {
return {};
}
}
return data || {};
}
function ensureArray(v) {
return Array.isArray(v) ? v : [];
}
async function request(path, params, headers) {
var resp = await Widget.http.get(API_BASE + path, {
params: params || {},
headers: headers || {}
});
if (!resp.ok) {
throw new Error("HTTP " + resp.status + " - " + path);
}
return safeJson(resp.data);
}
async function apiGet(path, params) {
return request(path, params, {
Accept: "application/json"
});
}
async function apiGetAbsolute(url) {
var resp = await Widget.http.get(url);
if (!resp.ok) throw new Error("HTTP " + resp.status + " - detail");
return safeJson(resp.data);
}
function mapItem(item, index) {
var id = String(item.id || item.video_id || item.slug || ("item_" + index));
return {
id: id,
title: item.title || item.name || "",
posterUrl: item.poster_url || item.cover || "",
backdropUrl: item.backdrop_url || "",
description: item.overview || item.description || "",
rating: typeof item.rating === "number" ? item.rating : null,
year: item.year ? String(item.year) : null,
mediaType: item.type === "tv" ? "tv" : "movie",
link: item.detail_url || null,
videoUrl: item.video_url || null
};
}
5. 内置 API 参考
5.1 网络请求
await Widget.http.get(url, { headers, params, timeout });
await Widget.http.post(url, body, { headers, timeout });
await Widget.http.request({ method, url, headers, params, data, timeout });
响应结构:
{ ok: boolean, status: number, data: any, headers: Record<string, string[]> }
5.2 TMDB 请求
Widget.tmdb 是对 TMDB API 的封装,自动处理 base URL 和响应解析,返回值直接是数据对象(无需再取 .data)。
// GET /discover/tv?language=zh-CN&page=1
var data = await Widget.tmdb.get("/discover/tv", {
params: { language: "zh-CN", page: 1 }
});
// 可选参数
await Widget.tmdb.get(path, {
params: {}, // 查询参数
headers: {}, // 自定义请求头
baseUrl: "...", // 覆盖默认 base URL
apiKey: "...", // 注入 api_key 参数
timeout: 30000 // 超时毫秒数
});
5.3 存储
Widget.storage.set("token", "abc");
var token = Widget.storage.get("token", ""); // 同步读取
var token = await Widget.storage.getAsync("token", ""); // 异步读取
Widget.storage.remove("token");
5.4 HTML 解析
var docId = Widget.dom.parse(html);
var cards = Widget.dom.select(docId, ".card");
var text = Widget.dom.text(cards[0]); // 获取节点文本内容
var href = Widget.dom.attr(cards[0], "href"); // 获取节点属性值
Widget.dom.remove(docId);
select 返回节点对象数组,每个节点包含以下字段:
{
tag: "a", // 标签名
text: "标题", // 文本内容
html: "<b>标题</b>", // innerHTML
outerHtml: "<a>...</a>",
attributes: { href: "...", class: "card" }, // 也可用 attrs
id: "item1",
className: "card active",
classes: ["card", "active"]
}
5.5 日志
console.log(“popular params”, params);
console.warn(“empty results”);
console.error(“request failed”, err.message);
5.6 日志脱敏与隐私
日志系统会默认进行基础脱敏:
- 参数名命中敏感关键词(如
token、authorization、cookie、password)时,值会被替换为***。 - URL 在日志中默认裁剪查询串和片段(只保留主路径),避免泄漏签名参数。
- 调试时建议输出”状态码、数量、字段存在性”,避免直接打印完整鉴权头。
5.7 fetch(全局 polyfill)
沙箱内注入了标准 fetch polyfill,可直接使用:
var resp = await fetch(“https://api.example.com/data”);
var json = await resp.json();
// resp.ok, resp.status 可用
注意:fetch 底层走 Widget.http,受同样的沙箱限制。复杂场景(自定义 headers、超时)建议直接使用 Widget.http。
6. 调试与排查
6.1 执行失败与重试策略
默认执行策略:
- 超时:
timeoutSeconds默认30秒。 - 重试:最大尝试次数 =
retryCount + 1(即retryCount为额外重试次数)。 - 退避:每次重试前按
250ms * 当前尝试序号延迟。 - 回退:请求失败时,若存在未过期旧缓存(stale)则优先返回旧缓存;否则返回空数组。
- 失败缓存:当无可用旧缓存时,会缓存空结果约 2 分钟,避免短时间内重复击穿上游。
6.2 常见错误
| 错误 | 原因 | 修复 |
|---|---|---|
WidgetMetadata not found in script |
顶层不是 var WidgetMetadata |
改为全局 var |
No supported modules found |
modules 为空/结构错 |
保证至少 1 个合法模块 |
Data source function not found: xxx |
functionName 和函数名不一致 |
校对大小写与命名 |
Data source must return an array |
返回对象或未 return | 保证每条路径都返回数组 |
loadDetail function not found |
条目有 link 但无 loadDetail |
增加全局 async function loadDetail |
HTTP 401/403 |
鉴权头或 Cookie 缺失 | 补齐 Authorization/Referer/Cookie |
6.3 浏览器预检
var Widget = {
http: {
get: async function(url, opts) {
opts = opts || {};
var fullUrl = url;
if (opts.params) {
var qs = Object.keys(opts.params).map(function(k) {
return k + "=" + encodeURIComponent(opts.params[k]);
}).join("&");
fullUrl += (url.indexOf("?") >= 0 ? "&" : "?") + qs;
}
var r = await fetch(fullUrl, { headers: opts.headers || {} });
var data = await r.json().catch(function() { return r.text(); });
return { ok: r.ok, status: r.status, data: data, headers: {} };
}
},
storage: { get: function() {}, set: function() {}, remove: function() {} },
dom: { parse: function(v) { return v; }, select: function() { return []; }, remove: function() {} }
};
测试调用:
getPopular({ page: 1 }).then(console.log).catch(console.error);
7. 发布检查清单
WidgetMetadata是否为顶层var。- 所有
functionName是否可定位到同名全局函数。 - 所有数据源函数是否稳定返回数组。
- 每个条目是否有字符串
id和title/name。 - 有
link的条目是否都可由loadDetail解析出videoUrl。 - 分页是否使用
params.page(或offset/count)。 - 错误是否有日志,便于 App 里查看。
- 脚本 URL 是否可公开访问。
functionName是否只使用标识符路径(无表达式)。- 是否避免依赖未文档化字段作为功能开关。
- 若未显式设置
id,是否确认”来源 URL 变化会导致组件 ID 变化”。
8. 自动化生成输入清单
建议一次性提供以下信息,以减少反复补充:
- 目标站点/API 列表(首页列表、详情)。
- 认证规则(Header、Cookie、签名算法)。
- 字段映射(接口字段 -> MediaItem 字段)。
- 分页规则(page/offset/cursor)。
- 是否需要
loadDetail抓取播放地址。 - 希望保留的模块 ID 与函数名。
- 兼容字段使用策略(是否允许
timeoutMs/timeout/retry)。 - 分类场景是否需要
WidgetSystemCategory[]返回。
9. AI开发-组件生成模板
你是 CapyPlayer 组件工程师。请输出可直接安装的 JavaScript 脚本,仅输出代码,不要附加说明。
输入:
- API_BASE: {{API_BASE}}
- 热门接口: {{POPULAR_API}}
- 详情接口: {{DETAIL_API}}
- 鉴权规则: {{AUTH_RULE}}
- 字段映射: {{FIELD_MAPPING}}
约束条件:
1) 必须使用全局 var WidgetMetadata。
2) modules 至少包含一个可用模块(如 popular)。
3) 每个 functionName 必须有同名全局 async function。
4) 所有数据源函数必须返回 MediaItem[],空结果返回 []。
5) 条目 `id` 必须为字符串;每个条目至少包含 `id` 和 `title/name`。
6) response.data 需兼容 string/object 两种情况。
7) 若列表项没有 videoUrl,必须提供 link,并实现 loadDetail(link)。
8) loadDetail 必须返回 {videoUrl} 或 {episodeItems:[{title,videoUrl}]}。
9) 网络错误抛 Error,且加 console.error 日志。
10) 代码中仅允许使用 Widget.http / Widget.storage / Widget.dom / console。
11) functionName 仅允许标识符路径(如 getPopular 或 api.list)。
12) 不依赖未文档化字段驱动功能逻辑。
10. AI开发-组件修复模板
请修复以下 CapyPlayer 组件,在最小改动前提下输出完整 JS 文件。
报错日志:
{{ERROR_LOG}}
现有代码:
{{CURRENT_CODE}}
修复约束:
1) 不改变 WidgetMetadata.id、modules[].id、functionName。
2) 不删除已有模块。
3) 为所有函数路径补齐 return,数据源函数统一返回数组。
4) 补充 response.data 字符串 JSON 解析。
5) 仅修复相关问题,不做风格重写。
6) 对失败请求增加明确错误日志。
11. 输出验收清单(人工 + 自动)
- 是否只输出 JS 代码(无 Markdown 包裹、无解释文本)。
- 顶层是否是
var WidgetMetadata。 functionName是否全部可调用。loadDetail是否存在且返回结构合法。- 是否含字符串 JSON 解析逻辑。
- 是否含分页参数传递。
- 是否有关键日志(至少错误日志)。
- 是否可直接粘贴安装,不依赖外部库。
11.1 自动化预检脚本(最小版)
将组件代码加载到控制台后,可执行以下预检函数:
function validateCurrentWidget() {
var issues = [];
var warnings = [];
var safePath = /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
var md = globalThis.WidgetMetadata;
if (!md || typeof md !== "object") {
return { ok: false, issues: ["WidgetMetadata 缺失或不是对象"], warnings: warnings };
}
if (!Array.isArray(md.modules) || md.modules.length === 0) {
issues.push("modules 不能为空");
}
var modules = Array.isArray(md.modules) ? md.modules : [];
modules.forEach(function(m, i) {
var idx = "modules[" + i + "]";
var fn = m && m.functionName ? String(m.functionName) : "";
if (!fn) issues.push(idx + ".functionName 不能为空");
if (fn && !safePath.test(fn)) issues.push(idx + ".functionName 格式非法: " + fn);
if (fn) {
var cur = globalThis;
fn.split(".").forEach(function(k) {
cur = cur && cur[k];
});
if (typeof cur !== "function") issues.push(idx + ".functionName 未找到可调用函数: " + fn);
}
});
if (typeof globalThis.loadDetail !== "function") {
warnings.push("未定义 loadDetail(仅当条目返回 link 且无 videoUrl 时会影响播放)");
}
return { ok: issues.length === 0, issues: issues, warnings: warnings };
}
示例:
validateCurrentWidget();
12. 安装与回退
安装:
- 上传
.js到公开 URL。 - App「组件」页点击
+添加 URL。 - 预览元信息无误后安装。
版本回退建议:
- URL 保持不变,发布新版本前保留旧代码 tag。
WidgetMetadata.version每次递增(如1.0.1)。- 遇到线上故障时回退到上一版本脚本内容。
13. 使用须知
组件系统是一个开放的扩展机制,允许任何人为 CapyPlayer 编写自定义内容来源。在使用前,请了解以下事项:
- 内容来源由开发者自行维护,CapyPlayer 不对第三方组件的内容、可用性或持续更新负责。
- 请仅安装来源可信的组件。组件脚本运行在沙箱中,但仍可发起网络请求,安装不明来源的脚本存在隐私风险。
- 内容合规由使用者自行负责。组件所访问的资源须符合所在地区的法律法规,CapyPlayer 不对因此产生的任何法律问题承担责任。
- 禁止恶意用途。组件开发者不得利用组件收集用户数据、实施欺骗或任何有害行为。
