mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
80eef46b4e
* refactor: move adapters from src/clis/ to root clis/ for monorepo separation Separates CLI adapters from the core runtime to prepare for independent adapter distribution via postinstall fetch. Key changes: - Move src/clis/ → clis/ (adapters at repo root) - Change tsconfig rootDir from "src" to "." so tsc compiles both - Create root-level shim files (registry.ts, errors.ts, etc.) so adapter relative imports (../../registry.js) resolve correctly - Update build-manifest.ts, main.ts paths for new dist/src/ structure - Expand ensureUserCliCompatShims() to cover all adapter import targets (types, utils, logger, launcher, browser/*, download/*, pipeline/*) - Add scripts/fetch-adapters.js postinstall for ~/.opencli/clis/ sync - Update vitest.config.ts adapter test paths - Add package.json files field to exclude adapters from npm package Official adapter files are unconditionally overwritten on update; user-created files not in the manifest are preserved. * fix: add dist/clis/ and cli-manifest.json to npm files, harden fetch-adapters - Add dist/clis/ and dist/cli-manifest.json to package.json files field so built-in adapters and manifest ship with the npm package - Replace execSync with execFileSync to prevent command injection - Add version check to skip redundant adapter fetches - Track tmpRoot explicitly for reliable cleanup * fix: address review blockers — manifest-based updates, global-only fetch, first-run fallback 1. Manifest-based update strategy: - Read old manifest to identify previously-official files - Clean up files removed upstream (in old manifest but not new) - User-created files (never in any manifest) remain untouched 2. Only run fetch-adapters on global install (npm_config_global=true) or explicit OPENCLI_FETCH=1, preventing heavy side effects for local/dev installs 3. First-run fallback in discovery.ts: - ensureUserAdapters() checks for adapter-manifest.json - If missing and ~/.opencli/clis/ is empty, spawns fetch-adapters.js - Guarantees adapters are available even with --ignore-scripts * fix: remove OPENCLI_FETCH env var, use internal _OPENCLI_FIRST_RUN instead * feat: also support OPENCLI_FETCH=1 for explicit adapter fetch trigger * simplify: replace git clone with local copy from dist/clis/ Adapters already ship in the npm package (dist/clis/), so there's no need to clone from GitHub. Copy directly from the installed package: - Eliminates git, curl, tar dependencies - No network calls in postinstall - No timeout/offline issues - Version always matches the installed CLI - ~65 lines of clone/download code replaced by one cpSync loop
125 lines
4.9 KiB
YAML
125 lines
4.9 KiB
YAML
site: xiaoe
|
|
name: play-url
|
|
description: 小鹅通视频/音频/直播回放 M3U8 播放地址
|
|
domain: h5.xet.citv.cn
|
|
strategy: cookie
|
|
|
|
args:
|
|
url:
|
|
type: str
|
|
required: true
|
|
positional: true
|
|
description: 小节页面 URL
|
|
|
|
pipeline:
|
|
- navigate: ${{ args.url }}
|
|
|
|
- wait: 2
|
|
|
|
- evaluate: |
|
|
(async () => {
|
|
var pageUrl = window.location.href;
|
|
var origin = window.location.origin;
|
|
var resourceId = (pageUrl.match(/[val]_[a-f0-9]+/) || [])[0] || '';
|
|
var productId = (pageUrl.match(/product_id=([^&]+)/) || [])[1] || '';
|
|
var appId = (origin.match(/(app[a-z0-9]+)\./) || [])[1] || '';
|
|
var isLive = resourceId.startsWith('l_') || pageUrl.includes('/alive/');
|
|
var m3u8Url = '', method = '', title = document.title, duration = 0;
|
|
|
|
// 深度搜索 Vue 组件树找 M3U8
|
|
function searchVueM3u8() {
|
|
var el = document.querySelector('#app');
|
|
if (!el || !el.__vue__) return '';
|
|
var walk = function(vm, d) {
|
|
if (!vm || d > 10) return '';
|
|
var data = vm.$data || {};
|
|
for (var k in data) {
|
|
if (k[0] === '_' || k[0] === '$') continue;
|
|
var v = data[k];
|
|
if (typeof v === 'string' && v.includes('.m3u8')) return v;
|
|
if (typeof v === 'object' && v) {
|
|
try {
|
|
var s = JSON.stringify(v);
|
|
var m = s.match(/https?:[^"]*\.m3u8[^"]*/);
|
|
if (m) return m[0].replace(/\\\//g, '/');
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
if (vm.$children) {
|
|
for (var c = 0; c < vm.$children.length; c++) {
|
|
var f = walk(vm.$children[c], d + 1);
|
|
if (f) return f;
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
return walk(el.__vue__, 0);
|
|
}
|
|
|
|
// ===== 视频课: detail_info → getPlayUrl =====
|
|
if (!isLive && resourceId.startsWith('v_')) {
|
|
try {
|
|
var detailRes = await fetch(origin + '/xe.course.business.video.detail_info.get/2.0.0', {
|
|
method: 'POST', credentials: 'include',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'bizData[resource_id]': resourceId,
|
|
'bizData[product_id]': productId || resourceId,
|
|
'bizData[opr_sys]': 'MacIntel',
|
|
}),
|
|
});
|
|
var detail = await detailRes.json();
|
|
var vi = (detail.data || {}).video_info || {};
|
|
title = vi.file_name || title;
|
|
duration = vi.video_length || 0;
|
|
if (vi.play_sign) {
|
|
var userId = (document.cookie.match(/ctx_user_id=([^;]+)/) || [])[1] || window.__user_id || '';
|
|
var playRes = await fetch(origin + '/xe.material-center.play/getPlayUrl', {
|
|
method: 'POST', credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
org_app_id: appId, app_id: vi.material_app_id || appId,
|
|
user_id: userId, play_sign: [vi.play_sign],
|
|
play_line: 'A', opr_sys: 'MacIntel',
|
|
}),
|
|
});
|
|
var playData = await playRes.json();
|
|
if (playData.code === 0 && playData.data) {
|
|
var m = JSON.stringify(playData.data).match(/https?:[^"]*\.m3u8[^"]*/);
|
|
if (m) { m3u8Url = m[0].replace(/\\u0026/g, '&').replace(/\\\//g, '/'); method = 'api_direct'; }
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
// ===== 兜底: Performance API + Vue 搜索轮询 =====
|
|
if (!m3u8Url) {
|
|
for (var attempt = 0; attempt < 30; attempt++) {
|
|
var entries = performance.getEntriesByType('resource');
|
|
for (var i = 0; i < entries.length; i++) {
|
|
if (entries[i].name.includes('.m3u8')) { m3u8Url = entries[i].name; method = 'perf_api'; break; }
|
|
}
|
|
if (!m3u8Url) { m3u8Url = searchVueM3u8(); if (m3u8Url) method = 'vue_search'; }
|
|
if (m3u8Url) break;
|
|
await new Promise(function(r) { setTimeout(r, 500); });
|
|
}
|
|
}
|
|
|
|
if (!duration) {
|
|
var vid = document.querySelector('video'), aud = document.querySelector('audio');
|
|
if (vid && vid.duration && !isNaN(vid.duration)) duration = Math.round(vid.duration);
|
|
if (aud && aud.duration && !isNaN(aud.duration)) duration = Math.round(aud.duration);
|
|
}
|
|
|
|
return [{ title: title, resource_id: resourceId, m3u8_url: m3u8Url, duration_sec: duration, method: method }];
|
|
})()
|
|
|
|
- map:
|
|
title: ${{ item.title }}
|
|
resource_id: ${{ item.resource_id }}
|
|
m3u8_url: ${{ item.m3u8_url }}
|
|
duration_sec: ${{ item.duration_sec }}
|
|
method: ${{ item.method }}
|
|
|
|
columns: [title, resource_id, m3u8_url, duration_sec, method]
|