CapyPlayer 组件开发指南
1. 说明
CapyPlayer 组件是一个公开可访问的 .js 脚本,运行在沙箱环境中。通过编写组件,你可以接入一些第三方媒体站点或 API,自定义的媒体内容来源,丰富展示方式。
关键规则:
WidgetMetadata必须是全局var,不能用const/let。modules[].functionName必须和全局async function名字完全一致。- 数据源函数必须返回数组(
MediaItem[]),不能返回对象/null。 loadDetail用于“详情链接条目”的二次解析。推荐显式返回type: "link"与link。type和mediaType不是同一个字段:type决定 Widget 行为,mediaType决定内容类型(电影 / 剧集)。
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 {
id: String(data.id || link),
title: data.title || "",
mediaType: "tv",
episodeItems: ensureArray(data.episodes).map(function(ep, idx) {
return {
id: String(ep.id || ("s1e" + (idx + 1))),
seasonNumber: Number(ep.season_number || 1),
episodeNumber: Number(ep.episode_number || (idx + 1)),
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
常用字段:
type:link/url/tmdb/imdb/doubanmediaType:movie/tv/series/show/collectionposterUrl、backdropUrldescription、rating、yeartmdbId、imdbId、doubanIdlink(详情地址,通常与type: "link"搭配)videoUrl(直接播放,通常与type: "url"搭配)
重要说明:
type是 Widget 条目类型,不等于mediaType。mediaType: "movie"只表示“这是电影条目”,不会自动触发loadDetail。- 想让详情页进入
loadDetail(link),推荐返回:
{
id: "https://example.com/detail/1",
type: "link",
title: "示例影片",
mediaType: "movie",
link: "https://example.com/detail/1"
}
- 想让条目直接播放,推荐返回:
{
id: "play-1",
type: "url",
title: "示例影片",
mediaType: "movie",
videoUrl: "https://cdn.example.com/video.m3u8"
}
- 兼容说明:当前版本会对旧组件做兜底推断。
- 若未显式提供
type,但条目有link且无videoUrl,系统会按link条目处理。 - 即便如此,仍建议组件作者显式填写
type,避免旧版本或第三方实现行为不一致。
字段映射优先级(建议按此输出,减少歧义):
| 语义 | 优先读取顺序 |
|---|---|
| 标题 | 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 |
| 单集播放线路 | playSources |
playSources -> play_sources |
用于同一集的多线路切换;单线路继续使用 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",
playSources: [
{ id: "line-1", title: "线路一", videoUrl: "https://cdn1/...m3u8", isDefault: true },
{ id: "line-2", title: "线路二", videoUrl: "https://cdn2/...m3u8" }
]
}
]
}
]
}
补充说明:
- 若返回季列表,建议总是显式提供
mediaType: "tv"。 - 顶层
episodeItems/episodes会被识别为 TV,再按每集的seasonNumber分季;未提供季号时归入第 1 季。 - 默认选中顺序为:有效的当前季/集 ID -> 当前季/集编号 -> 第一季第一集。
- 单集只有一条线路时使用
videoUrl;多条线路时使用playSources。 - 电影条目不要复用
episodeItems表示播放线路。
返回约束:
- 函数必须返回数组,空结果返回
[]。 id必须转字符串并在同一列表内唯一。videoUrl优先级高于link。
3.7 loadDetail 返回结构
除播放信息外,loadDetail 也可以填充详情页现有的演员、推荐作品和相关视频数据:
{
id: "tv-001",
title: "示例剧集",
mediaType: "tv",
tags: ["悬疑", "犯罪", "高分"],
credits: {
directors: [
{ id: "director-1", name: "导演甲", profileUrl: "https://example.com/director.jpg" }
],
cast: [
{
id: "12345",
name: "演员甲",
character: "角色甲",
profileUrl: "https://example.com/actor.jpg",
order: 0,
source: "tmdb"
}
]
},
recommendations: [
{
id: "recommend-1",
title: "相关作品",
mediaType: "movie",
posterUrl: "https://example.com/poster.jpg",
type: "link",
link: "https://example.com/detail/1"
}
],
relatedVideos: [
{
id: "trailer-1",
title: "正式预告",
posterUrl: "https://example.com/trailer.jpg",
type: "url",
videoUrl: "https://example.com/trailer.m3u8",
customHeaders: { "Referer": "https://example.com/" }
}
]
}
字段规则:
credits.cast兼容顶层cast、actors;directors、writers、producers也可放在顶层。- 人物必须有非空
name。只有source: "tmdb"且id是有效正整数时,人物卡片才进入 TMDB 人物详情。 tags接受字符串或字符串数组,会去空、按原顺序去重,最多保留 20 条。recommendations兼容similarItems、relatedItems;relatedVideos兼容extras、videos。- 推荐作品和相关视频各最多保留 30 条,只解析一层;嵌套条目中的同类列表不会继续解析。
- 嵌套条目必须有标题,并且至少有
link、videoUrl或非空playSources之一。 - 组件演员、推荐或相关视频非空时优先使用;字段缺失或空数组时继续使用现有服务器/TMDB 兜底。
- 推荐项有
link时进入组件详情并再次执行loadDetail;相关视频有直链或playSources时直接播放,并透传customHeaders。 - 组件只能填充现有详情数据,不支持自定义详情区块、标题、顺序、显隐、样式或可执行 UI。
单集:
{ title: "标题", videoUrl: "https://...m3u8" }
多季多集(推荐):
{
title: "剧名",
mediaType: "tv",
seasons: [
{
id: "s1",
seasonNumber: 1,
title: "第一季",
episodes: [
{ id: "s1e1", episodeNumber: 1, title: "第1集", videoUrl: "https://...m3u8" }
]
}
]
}
单季兼容结构:
{
title: "剧名",
mediaType: "tv",
episodeItems: [
{ id: "s1e1", seasonNumber: 1, episodeNumber: 1, title: "第1集", videoUrl: "https://...m3u8" }
]
}
电影/单视频多播放线路:
[
{ id: "line-1", title: "线路1", videoUrl: "https://cdn1/...m3u8", isDefault: true },
{ id: "line-2", title: "线路2", videoUrl: "https://cdn2/...m3u8" }
]
单集多播放线路:
{
id: "s1e1",
seasonNumber: 1,
episodeNumber: 1,
title: "第1集",
playSources: [
{ id: "line-1", title: "线路1", videoUrl: "https://cdn1/...m3u8", isDefault: true },
{ id: "line-2", title: "线路2", videoUrl: "https://cdn2/...m3u8" }
]
}
播放线路字段:
| 语义 | 推荐字段 | 兼容字段 |
|---|---|---|
| 线路 ID | id |
sourceId -> source_id |
| 线路名称 | title |
name -> label |
| 播放地址 | videoUrl |
video_url -> url -> path -> link |
| 默认线路 | isDefault |
is_default |
线路初始选择会先尊重用户已记住的线路和画质优先级;都未命中时才选择 isDefault: true 的线路,否则回退到第一条。
解析规则说明:
- 支持返回对象或数组。
- 顶层返回数组表示“同一影片的多条播放线路”,不用于表示季或集列表。
- 返回数组时,系统会选择第一个包含直链字段的对象作为主对象,并将整个数组转为可切换的
playSources。 seasons/episodeItems/episodes会保持为季集数据,不会再把第一集的地址提升为顶层videoUrl。- 顶层
episodeItems/episodes会按seasonNumber分季,缺省季号时归入第 1 季。 - 顶层数组的直链识别顺序:
videoUrl->video_url->url->link。 playSources的播放地址识别顺序:videoUrl->video_url->url->path->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 |
| 组件演员被 TMDB 覆盖 | 演员未放入支持的 credits/cast 字段,或演员列表为空 | 检查 credits.cast / cast / actors,并确保人物 name 非空 |
| 推荐列表不显示 | 推荐项缺少标题,或同时缺少详情链接和播放字段 | 为每项提供 title,以及 link、videoUrl、playSources 中至少一项 |
| 相关视频点击无法播放 | 直链字段为空,或请求头只写在外层详情对象 | 在相关视频条目中提供 videoUrl/playSources,并把所需 customHeaders 放在该条目上 |
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、playSources或完整季集结构。 - TV 的每个季、集是否都有稳定且唯一的 ID 与正确编号。
- 每集是否至少有
videoUrl或一个可用的playSources线路。 - 分页是否使用
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}、{playSources:[...]}、{episodeItems:[...]} 或 {seasons:[...]}。
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 不对因此产生的任何法律问题承担责任。
- 禁止恶意用途。组件开发者不得利用组件收集用户数据、实施欺骗或任何有害行为。
14. 更新日志
2026-08-22(048ea32)
- 明确
type与mediaType的职责:type决定条目行为,mediaType描述电影、剧集等内容类型;推荐详情条目显式使用type: "link",直接播放条目使用type: "url"。 - 补充旧组件兼容规则:未提供
type、存在link且没有videoUrl时,仍按详情链接处理并调用loadDetail。 - 完善
loadDetail的 TV 返回结构,增加季、集稳定 ID、季号、集号、当前选中项及seasons、episodeItems、episodes的解析说明。 - 增加多线路播放协议:电影、单视频和单集均可返回
playSources,并明确线路字段兼容名与默认线路选择规则。 - 扩展详情页数据支持:
loadDetail可返回标签、演职员、推荐作品和相关视频,同时补充字段别名、数量限制、回退策略及customHeaders透传规则。 - 同步更新基础示例、常见错误、发布检查清单和 AI 组件生成模板,使其覆盖完整季集结构与多线路播放。
