CapyPlayer 组件开发指南


1. 说明

CapyPlayer 组件是一个公开可访问的 .js 脚本,运行在沙箱环境中。通过编写组件,你可以接入一些第三方媒体站点或 API,自定义的媒体内容来源,丰富展示方式。

关键规则:

  1. WidgetMetadata 必须是全局 var,不能用 const/let
  2. modules[].functionName 必须和全局 async function 名字完全一致。
  3. 数据源函数必须返回数组(MediaItem[]),不能返回对象/null
  4. 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:发布并安装

  1. 将脚本托管到 GitHub Raw/Gist 或任意可公开访问 URL。
  2. App 内进入「组件」页,点击 +,粘贴 URL,预览后安装。

3. 契约规范

3.0 字段支持级别

级别 定义 示例
推荐 官方规范字段,优先使用 titlemodulesfunctionNametimeoutSeconds
兼容 为兼容历史脚本保留,建议迁移到推荐字段 nameiconUrltimeoutMstimeoutretry

3.1 WidgetMetadata

字段 必填 说明
titlename 组件显示名(二选一)
modules 至少一个模块
id 建议 组件唯一 ID,建议使用 [a-zA-Z0-9._-]
description 组件描述
author 作者名
version 版本号,默认 1.0.0
site 组件主页/站点 URL
icon / iconUrl 组件图标 URL(两者均可)
globalParams 自动合并到所有模块参数

兼容与解析说明:

  1. title 缺失,系统会回退读取 name
  2. icon 缺失,系统会回退读取 iconUrl
  3. id 中非 [a-zA-Z0-9._-] 字符会被替换为 _
  4. 若未提供 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 请求重试次数

兼容字段说明:

  1. 超时兼容:timeoutMstimeout 会折算为秒级超时,优先级低于 timeoutSeconds
  2. 重试兼容:retry 会回退映射到 retryCount

3.3 模块类型(modules[].type

type 说明
media_list 媒体列表模块(默认,可不填)
category 分类列表模块,返回 WidgetSystemCategory[]

兼容说明:

  1. 不填 type 时行为等同于 media_listtype 字段不会被自动补填)。
  2. list(兼容值)行为等同于 media_listtype 值原样保留不做转换。
  3. 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: string
  • titlename

常用字段:

  • mediaType: movie / tv / series / show / collection
  • posterUrlbackdropUrl
  • descriptionratingyear
  • tmdbIdimdbIddoubanId
  • link(触发 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 建议每集包含 episodeNumbertitlevideoUrl

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://..." }
      ]
    }
  ]
}

补充说明:

  1. 若返回季列表,建议总是显式提供 mediaType: "tv"
  2. 顶层 episodeItems 主要用于 TV 条目;电影条目不建议复用该结构表示播放线路。
  3. 单集仅有播放地址时,优先放在 videoUrl;仅有 link 时由 loadDetail 继续解析。

返回约束:

  1. 函数必须返回数组,空结果返回 []
  2. id 必须转字符串并在同一列表内唯一。
  3. 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. 支持返回对象或数组。
  2. 返回数组时,会优先选择”包含直链字段”的第一项作为主播放对象;若没有,则使用第一项。
  3. 返回数组且长度大于 1 时,系统会自动补充 playSources(若对象已包含 playSources 则不覆盖)。
  4. 返回对象且缺少 videoUrl 时,系统会尝试从 episodeItems / episode_items / episodes 中提取首个可用播放地址。
  5. 直链字段识别顺序:videoUrl -> video_url -> url -> link

3.8 参数合并优先级

参数最终值按以下顺序合并,后者覆盖前者:

  1. globalParams 默认值。
  2. modules[].params(同名参数覆盖全局同名参数配置)。
  3. 运行时参数(页面传入或测试面板输入)。

3.9 分类数据源协议(Categories)

除媒体列表外,系统也支持分类列表返回(用于分类卡片等场景)。

返回结构:

[
  {
    id: "action",
    name: "动作",
    icon: "https://example.com/icon.png",
    params: { genre: "action", page: 1 }
  }
]

字段说明:

  1. id:分类 ID(建议稳定唯一)。
  2. name:分类名称。
  3. icon:分类图标(可选)。
  4. params:点击分类后透传给内容数据源的参数对象。

3.10 安全约束(functionName)

functionName 仅允许“标识符或点路径”格式,例如:

  1. 合法:getPopularapi.list
  2. 非法: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 日志脱敏与隐私

日志系统会默认进行基础脱敏:

  1. 参数名命中敏感关键词(如 tokenauthorizationcookiepassword)时,值会被替换为 ***
  2. URL 在日志中默认裁剪查询串和片段(只保留主路径),避免泄漏签名参数。
  3. 调试时建议输出”状态码、数量、字段存在性”,避免直接打印完整鉴权头。

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 执行失败与重试策略

默认执行策略:

  1. 超时:timeoutSeconds 默认 30 秒。
  2. 重试:最大尝试次数 = retryCount + 1(即 retryCount 为额外重试次数)。
  3. 退避:每次重试前按 250ms * 当前尝试序号 延迟。
  4. 回退:请求失败时,若存在未过期旧缓存(stale)则优先返回旧缓存;否则返回空数组。
  5. 失败缓存:当无可用旧缓存时,会缓存空结果约 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. 发布检查清单

  1. WidgetMetadata 是否为顶层 var
  2. 所有 functionName 是否可定位到同名全局函数。
  3. 所有数据源函数是否稳定返回数组。
  4. 每个条目是否有字符串 idtitle/name
  5. link 的条目是否都可由 loadDetail 解析出 videoUrl
  6. 分页是否使用 params.page(或 offset/count)。
  7. 错误是否有日志,便于 App 里查看。
  8. 脚本 URL 是否可公开访问。
  9. functionName 是否只使用标识符路径(无表达式)。
  10. 是否避免依赖未文档化字段作为功能开关。
  11. 若未显式设置 id,是否确认”来源 URL 变化会导致组件 ID 变化”。

8. 自动化生成输入清单

建议一次性提供以下信息,以减少反复补充:

  1. 目标站点/API 列表(首页列表、详情)。
  2. 认证规则(Header、Cookie、签名算法)。
  3. 字段映射(接口字段 -> MediaItem 字段)。
  4. 分页规则(page/offset/cursor)。
  5. 是否需要 loadDetail 抓取播放地址。
  6. 希望保留的模块 ID 与函数名。
  7. 兼容字段使用策略(是否允许 timeoutMs/timeout/retry)。
  8. 分类场景是否需要 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. 输出验收清单(人工 + 自动)

  1. 是否只输出 JS 代码(无 Markdown 包裹、无解释文本)。
  2. 顶层是否是 var WidgetMetadata
  3. functionName 是否全部可调用。
  4. loadDetail 是否存在且返回结构合法。
  5. 是否含字符串 JSON 解析逻辑。
  6. 是否含分页参数传递。
  7. 是否有关键日志(至少错误日志)。
  8. 是否可直接粘贴安装,不依赖外部库。

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. 安装与回退

安装:

  1. 上传 .js 到公开 URL。
  2. App「组件」页点击 + 添加 URL。
  3. 预览元信息无误后安装。

版本回退建议:

  1. URL 保持不变,发布新版本前保留旧代码 tag。
  2. WidgetMetadata.version 每次递增(如 1.0.1)。
  3. 遇到线上故障时回退到上一版本脚本内容。

13. 使用须知

组件系统是一个开放的扩展机制,允许任何人为 CapyPlayer 编写自定义内容来源。在使用前,请了解以下事项:

  1. 内容来源由开发者自行维护,CapyPlayer 不对第三方组件的内容、可用性或持续更新负责。
  2. 请仅安装来源可信的组件。组件脚本运行在沙箱中,但仍可发起网络请求,安装不明来源的脚本存在隐私风险。
  3. 内容合规由使用者自行负责。组件所访问的资源须符合所在地区的法律法规,CapyPlayer 不对因此产生的任何法律问题承担责任。
  4. 禁止恶意用途。组件开发者不得利用组件收集用户数据、实施欺骗或任何有害行为。