@streamapp/stream 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/stream.mjs +89 -0
- package/package.json +34 -0
- package/resources/app/dist-panel/panel-boards.js +6 -0
- package/resources/app/dist-panel/panel-detail.css +1 -0
- package/resources/app/dist-panel/panel-detail.js +2141 -0
- package/resources/app/dist-panel/panel-manage.css +1 -0
- package/resources/app/dist-panel/panel-manage.js +2246 -0
- package/resources/app/dist-panel/panel-movie.css +1 -0
- package/resources/app/dist-panel/panel-movie.js +2268 -0
- package/resources/app/dist-panel/panel-research.css +1 -0
- package/resources/app/dist-panel/panel-research.js +316 -0
- package/resources/app/dist-panel/panel.css +1 -0
- package/resources/app/dist-panel/panel.js +463 -0
- package/resources/dsh-engine-lock/package-lock.json +9032 -0
- package/resources/extension/.output/chrome-mv3/assets/popup-C2NUZZQW.css +2 -0
- package/resources/extension/.output/chrome-mv3/background.js +1 -0
- package/resources/extension/.output/chrome-mv3/chunks/popup-BeBts9v2.js +50 -0
- package/resources/extension/.output/chrome-mv3/icon/128.png +0 -0
- package/resources/extension/.output/chrome-mv3/icon/16.png +0 -0
- package/resources/extension/.output/chrome-mv3/icon/32.png +0 -0
- package/resources/extension/.output/chrome-mv3/icon/48.png +0 -0
- package/resources/extension/.output/chrome-mv3/icon/96.png +0 -0
- package/resources/extension/.output/chrome-mv3/manifest.json +1 -0
- package/resources/extension/.output/chrome-mv3/popup.html +21 -0
- package/resources/packages/1lou/1lou-search.recipe.json +102 -0
- package/resources/packages/1lou/README.md +7 -0
- package/resources/packages/1lou/package.json +15 -0
- package/resources/packages/Douyin_TikTok_Download_API/README.md +23 -0
- package/resources/packages/Douyin_TikTok_Download_API/activate.ts +14 -0
- package/resources/packages/Douyin_TikTok_Download_API/adapter/adapter.ts +321 -0
- package/resources/packages/Douyin_TikTok_Download_API/adapter/danmaku.ts +21 -0
- package/resources/packages/Douyin_TikTok_Download_API/adapter/executor.ts +75 -0
- package/resources/packages/Douyin_TikTok_Download_API/adapter/normalize.ts +20 -0
- package/resources/packages/Douyin_TikTok_Download_API/manifests.yaml +521 -0
- package/resources/packages/Douyin_TikTok_Download_API/package.json +32 -0
- package/resources/packages/alist/README.md +14 -0
- package/resources/packages/alist/activate.ts +12 -0
- package/resources/packages/alist/adapter.ts +74 -0
- package/resources/packages/alist/manifests.yaml +76 -0
- package/resources/packages/alist/mounts.ts +120 -0
- package/resources/packages/alist/package.json +24 -0
- package/resources/packages/alist/presets.ts +53 -0
- package/resources/packages/alist/provision.ts +84 -0
- package/resources/packages/baidu/baidu-share.recipe.json +93 -0
- package/resources/packages/baidu/package.json +16 -0
- package/resources/packages/baidu-search/baidu-search.recipe.json +129 -0
- package/resources/packages/baidu-search/package.json +25 -0
- package/resources/packages/brave/brave-search.recipe.json +129 -0
- package/resources/packages/brave/package.json +25 -0
- package/resources/packages/browser/README.md +3 -0
- package/resources/packages/browser/adapter.ts +44 -0
- package/resources/packages/browser/manifests.yaml +20 -0
- package/resources/packages/browser/package.json +13 -0
- package/resources/packages/bt0/README.md +7 -0
- package/resources/packages/bt0/bt0-search.recipe.json +69 -0
- package/resources/packages/bt0/bt0-tlist.recipe.json +74 -0
- package/resources/packages/bt0/package.json +15 -0
- package/resources/packages/btbtla/README.md +6 -0
- package/resources/packages/btbtla/btbtla-detail.recipe.json +76 -0
- package/resources/packages/btbtla/btbtla-search.recipe.json +74 -0
- package/resources/packages/btbtla/package.json +15 -0
- package/resources/packages/builtin/README.md +8 -0
- package/resources/packages/builtin/manifests.yaml +487 -0
- package/resources/packages/builtin/package.json +14 -0
- package/resources/packages/douyin/README.md +39 -0
- package/resources/packages/douyin/douyin-collection.recipe.json +127 -0
- package/resources/packages/douyin/douyin-search.recipe.json +163 -0
- package/resources/packages/douyin/package.json +16 -0
- package/resources/packages/firecrawl/firecrawl-create-key.recipe.json +81 -0
- package/resources/packages/firecrawl/firecrawl-read-key.recipe.json +56 -0
- package/resources/packages/firecrawl/package.json +16 -0
- package/resources/packages/google/google-search.recipe.json +127 -0
- package/resources/packages/google/package.json +24 -0
- package/resources/packages/groq/groq-create-key.recipe.json +67 -0
- package/resources/packages/groq/package.json +16 -0
- package/resources/packages/huggingface/README.md +7 -0
- package/resources/packages/huggingface/huggingface-spaces.recipe.json +73 -0
- package/resources/packages/huggingface/package.json +15 -0
- package/resources/packages/imdb/imdb-chart.recipe.json +75 -0
- package/resources/packages/imdb/package.json +15 -0
- package/resources/packages/index.ts +15 -0
- package/resources/packages/iqiyi/README.md +10 -0
- package/resources/packages/iqiyi/iqiyi-cn-album.recipe.json +80 -0
- package/resources/packages/iqiyi/iqiyi-cn-search.recipe.json +71 -0
- package/resources/packages/iqiyi/package.json +15 -0
- package/resources/packages/lizhi/README.md +35 -0
- package/resources/packages/lizhi/lizhi-user.recipe.json +80 -0
- package/resources/packages/lizhi/package.json +15 -0
- package/resources/packages/manmanbuy/README.md +16 -0
- package/resources/packages/manmanbuy/manmanbuy-search.recipe.json +115 -0
- package/resources/packages/manmanbuy/package.json +15 -0
- package/resources/packages/mineru/README.md +38 -0
- package/resources/packages/mineru/package.json +28 -0
- package/resources/packages/pansou/README.md +14 -0
- package/resources/packages/pansou/activate.ts +11 -0
- package/resources/packages/pansou/adapter.ts +94 -0
- package/resources/packages/pansou/manifests.yaml +50 -0
- package/resources/packages/pansou/package.json +27 -0
- package/resources/packages/qq/package.json +15 -0
- package/resources/packages/qq/qq-send.recipe.json +104 -0
- package/resources/packages/quark/package.json +16 -0
- package/resources/packages/quark/quark-share.recipe.json +40 -0
- package/resources/packages/reddit/package.json +15 -0
- package/resources/packages/reddit/reddit-sub.recipe.json +77 -0
- package/resources/packages/replay/README.md +7 -0
- package/resources/packages/replay/package.json +13 -0
- package/resources/packages/rsshub/README.md +16 -0
- package/resources/packages/rsshub/manifests.yaml +107 -0
- package/resources/packages/rsshub/package.json +21 -0
- package/resources/packages/telegram/package.json +15 -0
- package/resources/packages/telegram/telegram-search.recipe.json +125 -0
- package/resources/packages/tencent/README.md +11 -0
- package/resources/packages/tencent/package.json +15 -0
- package/resources/packages/tencent/tencent-cn-episode.recipe.json +98 -0
- package/resources/packages/toubiec/README.md +38 -0
- package/resources/packages/toubiec/package.json +15 -0
- package/resources/packages/toubiec/toubiec-download.recipe.json +58 -0
- package/resources/packages/toubiec/toubiec-playlist.recipe.json +49 -0
- package/resources/packages/toubiec/toubiec-search.recipe.json +80 -0
- package/resources/packages/voiceprint/README.md +31 -0
- package/resources/packages/voiceprint/package.json +36 -0
- package/resources/packages/wikipedia/package.json +15 -0
- package/resources/packages/wikipedia/wikipedia-award-list.recipe.json +102 -0
- package/resources/packages/xhs/README.md +32 -0
- package/resources/packages/xhs/package.json +21 -0
- package/resources/packages/xhs/xhs-detail.recipe.json +129 -0
- package/resources/packages/xhs/xhs-home.recipe.json +144 -0
- package/resources/packages/xhs/xhs-like.recipe.json +96 -0
- package/resources/packages/xhs/xhs-search.recipe.json +138 -0
- package/resources/packages/xueqiu/README.md +9 -0
- package/resources/packages/xueqiu/package.json +16 -0
- package/resources/packages/xueqiu/xueqiu-detail.recipe.json +92 -0
- package/resources/packages/xueqiu/xueqiu-user.recipe.json +75 -0
- package/resources/packages/zhipu/package.json +16 -0
- package/resources/packages/zhipu/zhipu-create-key.recipe.json +74 -0
- package/resources/packages/zuna/README.md +31 -0
- package/resources/packages/zuna/package.json +15 -0
- package/resources/packages/zuna/zuna-album.recipe.json +46 -0
- package/resources/packages/zuna/zuna-download.recipe.json +53 -0
- package/resources/packages/zuna/zuna-playlist.recipe.json +46 -0
- package/resources/packages/zuna/zuna-search.recipe.json +96 -0
- package/resources/server.mjs +238876 -0
- package/resources/sidequest.jobs.js +1 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_why_version_2": "提版本不是形式:`RepairLedger` 的检疫**只有更高的 recipe 版本才解除**。这条腿刚在活体连着失败过(被 Brave 自己的人机验证页拦下,而当时的 wall 认不出它),不提版本的话它可能已经被隔离、之后静默 DECLINE。",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"kind": "browser",
|
|
5
|
+
"sourceId": "brave-search",
|
|
6
|
+
"_why_allow_empty": "**「Brave 明确说匹配太少」是一个成功的 0 条,不是失败。** 这条腿的 0 条有两种来源,处置完全相反:Brave 自己给出那张「没有太多匹配的内容」的卡 → 成功空(allowEmpty 收下,梯子据此告诉模型「确实没搜到」);条目选择器漂了/撞上拦截 → 抛错(等待步自己判,见它的头注)。allowEmpty 在这里**不会**把「静默 0 条」放回来,因为漂移那条路根本走不到这里。",
|
|
7
|
+
"allowEmpty": true,
|
|
8
|
+
"_why_cookie_domain": "Brave 搜索不需要登录,这一格只是「这个包能走到哪些域」的申报(装载器拿它卡 goto 的落点,见 `src/replay/recipe-import.ts`)。填 brave.com 以覆盖 search.brave.com。",
|
|
9
|
+
"cookieDomain": "brave.com",
|
|
10
|
+
"_why_browser": "和 google 那条腿同一个理由:值钱的是**用户自己那个 Chrome**——同一条查询在他浏览器里打 Brave 回 12 条全切题、无验证码。Brave 另有一层价值:它是**自己的索引**(不是 Google/Bing 转发),实测和真 Google 的结果只重合 2/10,补的是小众博客和论坛。",
|
|
11
|
+
"_why_entry_is_homepage": "entryUrl 是**首页**、搜索 URL 在 step#0 的 goto 上,这不是绕路:expect 的判据必须有区分力——动作前为假、动作后为真(browser-harvest/references/authoring.md §3.5)。实测首页 `#mixed-main` 命中 0(首页的容器叫 `#home-wrapper`),任何一张结果页上都命中 1,于是这个 expect 是真监督。直接把搜索 URL 当 entryUrl 的话,唯一能写的 expect 在动作之前就已经成立,引擎当场判它恒真(StepExpectVacuousError)。",
|
|
12
|
+
"entryUrl": "https://search.brave.com/",
|
|
13
|
+
"entryWait": "load",
|
|
14
|
+
"_why_login_check": "这个源不需要登录,两个信号因此各自换了个更有用的意思:loggedIn 用搜索框 `#searchbox`(首页和结果页都在,命中 = 我们拿到的是 Brave 真页面)。**注意别照抄 google 那份的 `textarea[name=q]`**:实测 Brave 结果页上它命中 0(只有首页有),照抄会让 detectLoginState 在结果页恒判 UNKNOWN、白等一轮。wall 是一组**没有实测到的**候选:这台机器上一次都没撞见 Brave 的人机验证,所以只能按常见形状(Cloudflare 挑战 / turnstile / captcha 表单)先摆着。**它没命中不会造成静默错**——真撞上拦截时页面里既没有条目也没有那张「匹配太少」的卡,等待步会当成漂移抛错(响,且会被检疫),只是错误信息说得没那么准。真撞见一次就回来把选择器改准。",
|
|
15
|
+
"loginCheck": {
|
|
16
|
+
"loggedIn": "#searchbox",
|
|
17
|
+
"_why_captcha_wrapper": "`.captcha-wrapper` 是 Brave **自己那张**人机验证页(不是 Cloudflare 的):2026-08-13 活体撞到「正在验证您不是机器人」时,`#mixed-main` 不在、`#main` 里就是这个 wrapper。少了这一条,被拦会报成一句 `step#0 expect 未满足:#mixed-main 没有出现`——那读起来是「选择器漂了」,处置完全相反(该退让,不是该改选择器),而且 facility 的冷却也不会被触发。",
|
|
18
|
+
"wall": "#challenge-form, .cf-turnstile, iframe[src*=\"challenges.cloudflare.com\"], form[action*=\"captcha\"], .captcha-wrapper"
|
|
19
|
+
},
|
|
20
|
+
"_why_one_shot": "session.lifecycle = one-shot:查完就关。这条腿是对话里临时补一发搜索,不是持续采集——没有账本要留、没有 feed 要骑,留一个常驻标签只会白占用户浏览器里的一格。",
|
|
21
|
+
"session": {
|
|
22
|
+
"facility": "brave",
|
|
23
|
+
"lifecycle": "one-shot",
|
|
24
|
+
"visibility": "unattended"
|
|
25
|
+
},
|
|
26
|
+
"steps": [
|
|
27
|
+
{
|
|
28
|
+
"_why": "{query} 由调用方按 URL 组件编码好再传进来(substitute 是纯文本替换,不编码)——查询词里一个 # 就会把 querystring 截断,而截断之后 Brave 照样回一页结果,于是「搜错了」长得和「搜到了」一模一样。不加任何条数参数:实测一页固定 20 条自然结果,够 targetCount 取 10。",
|
|
29
|
+
"kind": "goto",
|
|
30
|
+
"url": "https://search.brave.com/search?q={query}",
|
|
31
|
+
"_why_expect_is_the_page_not_the_results": "这道闸门只判「我们拿到了一张真的 Brave 结果页」,**不判它有没有结果**——两件事必须分开,否则「Brave 确实没有」和「选择器漂了」会合并成同一个失败,而那正是这条链路上最贵的一种混淆。实测三态:首页 `#mixed-main` = 0(所以这个 expect 非恒真)、有结果页 = 1(内含 20 个 `div[data-type=web]`)、**没有结果的页 `#mixed-main` 照样 = 1**(里面换成一张 `.fallback-snippet`)。这一点和 Google 正好相反——Google 没结果时 `#rso` 整个消失,Brave 的容器一直在,所以判据只能往容器**里面**看。谁是谁由下一步分辨。timeout 8s:实测渲染 2 秒出头。",
|
|
32
|
+
"expect": {
|
|
33
|
+
"selector": "#mixed-main",
|
|
34
|
+
"timeout": 8000
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"_why": "**这一步只做一件事:等结果列表不再长了。** 结果是渐进出现的,observer 早读一步就少读几条,而少读不会报错。所以在读之前先在页面里数着 `#mixed-main div[data-type=\"web\"]`,连着 3 次 200ms 读数不变才算画完,封顶 3s。它不产 item(items 恒空),产出全在下面那个 dom observer 里。",
|
|
39
|
+
"_why_not_scroll_or_settle": "**这个等待刻意不用 settle,也不用 scroll。** settle 要「逼出一帧」(逐帧截图比字节),落到 `Page.captureScreenshot`,而对着后台标签这条 relay 命令在这台机器上稳定挂到 30s 超时(详见 browser-harvest/references/failure-atlas.md 4.3.6)。scroll 则是另一个理由:driver 今天一帧都不逼、滚动本身不贵了,但本步只是「等结果区 DOM 稳定」,滚动对它没有意义,白等一个 dwell。换成纯 `Runtime.evaluate` 的等待是几十毫秒。**判据:需要真帧的东西才配付这个价**,而「等 DOM 稳定」根本不需要帧——它是 Tier A。",
|
|
40
|
+
"_why_it_also_judges": "**这一步负责把「Brave 确实没有」和「我们读不到了」分开,而且必须分成「成功空」和「抛错」两种收场**——因为失败在这条链路上不只是一句话,**它带着检疫**:`RepairLedger` 见到一个源反复失败就把它隔离,之后 `ReplayAdapter.fetch` 直接 `return DECLINED`(`src/adapters/replay/adapter.ts`),**连浏览器都不开、也不报错**,表现就是这条腿静默死掉(2026-08-12 google 那条腿真撞过)。判据是对活体量出来的:**Brave 几乎从不给「零结果」,它给的是一张「找到的匹配项太少」的卡**——`#mixed-main > .fallback-snippet`,此时 `div[data-type=web]` 恰好 0(实测查询 `site:<不存在的域> foobar`;注意**加引号的精确匹配走不到这一态**,Brave 会丢掉运算符、照样回 20 条模糊结果,页顶只挂一句「未应用 搜索运算符」)。所以:0 条 + `.fallback-snippet` 在 = Brave 明确说没有 → **正常返回空**(配 `allowEmpty`,是一个成功的 0 条);0 条而那张卡也不在 = 真的漂了/被拦 → **抛错**(该响、也该被检疫)。**字段也要在这里验一次**(第一张卡里必须取得出 `.title` 和 `a:has(.title)`):开了 `allowEmpty` 之后,observer 的**字段**选择器漂了会表现成「条目在、link 取不到 → 被 dedupe 丢光 → 0 条」,而 0 条现在是合法的成功——不在这一步拦住,它就变成一个安静的空答案。这一步和 observer 用同一批选择器不是重复:这里是**判据**(漂了就响),那里是**取数**。",
|
|
41
|
+
"kind": "evaluate",
|
|
42
|
+
"call": "async () => { const sel = '#mixed-main div[data-type=\"web\"]'; let last = -1, stable = 0; for (let i = 0; i < 15 && stable < 3; i++) { const c = document.querySelectorAll(sel).length; stable = (c > 0 && c === last) ? stable + 1 : 0; last = c; await new Promise(r => setTimeout(r, 200)); } const cards = document.querySelectorAll(sel); if (cards.length === 0) { if (document.querySelector('#mixed-main .fallback-snippet')) return { items: [] }; throw new Error('[brave] 结果容器在,但既没有一条自然结果、也没有「匹配太少」那张卡 —— 条目选择器漂了或撞上了拦截页'); } const first = cards[0]; if (!first.querySelector('.title') || !first.querySelector('a:has(.title)')) { throw new Error('[brave] 读到了条目,但取不出标题或链接 —— 字段选择器漂了'); } return { items: [] }; }",
|
|
43
|
+
"itemsAt": "items",
|
|
44
|
+
"maxPages": 1
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"observers": [
|
|
48
|
+
{
|
|
49
|
+
"_why_selectors": "2026-08-13 对活体重定过:`#mixed-main div[data-type=\"web\"]` 恰好命中 20 条自然结果,20/20 同时含 `.title` 和 `.generic-snippet`,每条的 `a:has(.title)` 恰好 1 个且 href 全是绝对 URL。**刻意不用 svelte 的那些 class**(`.svelte-jmfu5f` / `.svelte-1rq4ngz` 之类):它们是编译期哈希,改一行样式就换一批。`#mixed-main` 是容器 id,`data-type=\"web\"` 是 Brave 给结果种类打的标,`.title` / `.generic-snippet` 是语义类名——三样都不带哈希。注意 `#mixed-main .snippet` 是 22(掺着 `#search-elsewhere` 和 `#pagination-snippet`),不能当条目单位。",
|
|
50
|
+
"_why_dom": "Brave 的结果是 SSR 进 HTML 的,页面不为它发 XHR——network observer 在这里没有东西可拦。",
|
|
51
|
+
"kind": "dom",
|
|
52
|
+
"itemSelector": "#mixed-main div[data-type=\"web\"]",
|
|
53
|
+
"fields": {
|
|
54
|
+
"title": {
|
|
55
|
+
"selector": ".title"
|
|
56
|
+
},
|
|
57
|
+
"link": {
|
|
58
|
+
"selector": "a:has(.title)",
|
|
59
|
+
"attr": "href"
|
|
60
|
+
},
|
|
61
|
+
"snippet": {
|
|
62
|
+
"selector": ".generic-snippet"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"trigger": "final"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"output": {
|
|
69
|
+
"itemsAt": "items",
|
|
70
|
+
"_why_dedupe_link": "dom observer 产出的「原始条目」就是上面那张扁平卡片,所以 dedupeBy 走 link(页内唯一、也是这条结果的身份)。**注意这条路自己不会喊**:选择器漂了 → link 取不到 → 每条都因缺身份被丢 → 0 条,而 `allowEmpty` 开着,0 条是合法的成功。所以「漂了要响」这件事**不由这里保证**,由上面那个等待步的判据保证(它验条目数、也验第一张卡里取不取得出 `.title` 和 `a:has(.title)`)。两处的分工写在那一步的 `_why_it_also_judges` 里——改任何一边之前先读它。",
|
|
71
|
+
"dedupeBy": "link",
|
|
72
|
+
"targetCount": 10,
|
|
73
|
+
"mapping": {
|
|
74
|
+
"title": "title",
|
|
75
|
+
"link": "link",
|
|
76
|
+
"snippet": "snippet",
|
|
77
|
+
"guid": "link"
|
|
78
|
+
},
|
|
79
|
+
"assert": [
|
|
80
|
+
{
|
|
81
|
+
"path": "items",
|
|
82
|
+
"desc": "brave #mixed-main 里一条自然结果都没读到 —— 选择器漂移或撞上了拦截页"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"policy": {
|
|
87
|
+
"minActionIntervalMs": 500,
|
|
88
|
+
"maxTaskMs": 60000
|
|
89
|
+
},
|
|
90
|
+
"meta": {
|
|
91
|
+
"description": "Brave 网页搜索 — 在用户自己的 Chrome 里打开结果页,读 SSR 出来的自然结果(title/link/snippet)。Brave 有自己的索引,是 web_search 里 Google 那一档的备胎",
|
|
92
|
+
"type": "post",
|
|
93
|
+
"title": "网页搜索",
|
|
94
|
+
"topics": [
|
|
95
|
+
"brave",
|
|
96
|
+
"搜索",
|
|
97
|
+
"search",
|
|
98
|
+
"web"
|
|
99
|
+
],
|
|
100
|
+
"categories": [
|
|
101
|
+
"news"
|
|
102
|
+
],
|
|
103
|
+
"facility": {
|
|
104
|
+
"key": "brave",
|
|
105
|
+
"label": "Brave Search"
|
|
106
|
+
},
|
|
107
|
+
"capabilities": [
|
|
108
|
+
"search"
|
|
109
|
+
],
|
|
110
|
+
"provides": [
|
|
111
|
+
"search-content"
|
|
112
|
+
],
|
|
113
|
+
"key_param": "query",
|
|
114
|
+
"priority": 50,
|
|
115
|
+
"_why_not_discoverable": "不进发现列表:它不是一个「可订阅的信息源」(没人要一条定时跑的搜索),只是 web_search 借的一条腿。",
|
|
116
|
+
"discoverable": false,
|
|
117
|
+
"_why_pick_in": "只在 Provider 成员面挑得到,不在频道订阅面——web_search 第二档的一条腿",
|
|
118
|
+
"pick_in": ["provider"],
|
|
119
|
+
"cadence_hint_seconds": 300,
|
|
120
|
+
"homepage": "search.brave.com",
|
|
121
|
+
"params_schema": {
|
|
122
|
+
"query": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"required": true,
|
|
125
|
+
"description": "搜索词,**已按 URL 组件编码**(调用方负责 encodeURIComponent)"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streamapp/brave",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "stream",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stream-recipe"
|
|
7
|
+
],
|
|
8
|
+
"stream": {
|
|
9
|
+
"type": "recipe",
|
|
10
|
+
"name": "Brave",
|
|
11
|
+
"tagline": "网页搜索",
|
|
12
|
+
"facility": "brave",
|
|
13
|
+
"schemaVersion": 1,
|
|
14
|
+
"_why_cookieDomain": "Brave 搜索**不需要登录**,这一格填的不是「去哪儿取登录态」,而是「这个包能走到哪些域」——装载器拿它卡 goto 的落点(`src/replay/recipe-import.ts`:goto 的 host 必须落在 cookieDomain 之内,否则拒绝装载)。填 brave.com 而不是 search.brave.com,是因为它要覆盖 www/search 两个子域。",
|
|
15
|
+
"cookieDomain": "brave.com",
|
|
16
|
+
"_why_rateLimit": "和 google 那份同一个节奏、同一个理由:连着敲三次是正常的(burst 3),持续每分钟十次以上就不像人了(perMinute 10)。maxWaitMs 3000 是关键那条——这条腿长在对话热路径上(web_search 主腿的备胎),桶空了默认行为是「等」,等在对话里就是用户干瞪着 spinner;3 秒排不上就抛 RateLimitedError,梯子当场如实回一句「这次没查成」,不编结果。",
|
|
17
|
+
"_why_perHour": "**这个 100 不是量出来的,是照 google 那份抄的上限**(说明见 packages/google 的 _why_perHour:那边的 100 是实测撞墙点)。抄过来的理由是同一个出口:Google 累计打爆之后,同出口的 Brave 也跟着弹人机验证——所以 Brave 自己的累计量没有理由假定能更高。实际上它是备胎、只在 Google 那条腿断了时才跑,正常一小时远够不到 100,这一格是天花板不是闸门。哪天真在 Brave 上撞到拦截,就在这里改一个数、并把实测写进这段。",
|
|
18
|
+
"rateLimit": {
|
|
19
|
+
"burst": 3,
|
|
20
|
+
"perMinute": 10,
|
|
21
|
+
"perHour": 100,
|
|
22
|
+
"maxWaitMs": 3000
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Adapter, AdapterFetchResult } from '../../src/adapters/types.ts'
|
|
2
|
+
import type { SourceManifest } from '../../src/manifest/types.ts'
|
|
3
|
+
|
|
4
|
+
/** A rendered web page, the raw shape the browser adapter emits (one item per page). */
|
|
5
|
+
export interface RenderedPage {
|
|
6
|
+
url: string
|
|
7
|
+
title: string
|
|
8
|
+
text: string
|
|
9
|
+
html: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Renders a URL to its loaded DOM. Injected so tests don't launch a real browser. */
|
|
13
|
+
export interface PageRenderer {
|
|
14
|
+
render(url: string): Promise<RenderedPage>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `browser` adapter — the universal failover last rung. A source with `adapter: browser`
|
|
19
|
+
* and a `url` param renders that page and returns it as a single raw item. Just another
|
|
20
|
+
* adapter, so creds/cache/dedup wiring is unchanged.
|
|
21
|
+
*
|
|
22
|
+
* The renderer is INJECTED and there is no default. Stream owns no browser of its own to fall
|
|
23
|
+
* back on: rendering happens in the user's Chrome over the extension relay, wired in bootstrap
|
|
24
|
+
* (which is also where "is that browser even up?" is answered). An adapter that quietly
|
|
25
|
+
* launched its own second browser is exactly what this rung must not do.
|
|
26
|
+
*/
|
|
27
|
+
export class BrowserAdapter implements Adapter {
|
|
28
|
+
readonly id = 'browser'
|
|
29
|
+
constructor(private readonly renderer: PageRenderer) {}
|
|
30
|
+
|
|
31
|
+
async init(_env: Record<string, string>): Promise<void> {}
|
|
32
|
+
|
|
33
|
+
async fetch(params: Record<string, unknown>, _manifest: SourceManifest): Promise<AdapterFetchResult> {
|
|
34
|
+
const url = String(params.url ?? '').trim()
|
|
35
|
+
if (!url) throw new Error('[browser] fetch needs a `url` param')
|
|
36
|
+
const page = await this.renderer.render(url)
|
|
37
|
+
return {
|
|
38
|
+
items: [{ guid: page.url, url: page.url, title: page.title, text: page.text }],
|
|
39
|
+
// 这条流 = 这一个页面,所以页面标题就是流的名字。报上去,订阅时名字自动填好
|
|
40
|
+
// (scheduler → backfillLabel);空标题不报,占位名比空名字好。
|
|
41
|
+
title: page.title.trim() || undefined,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
- id: browser-page
|
|
2
|
+
adapter: browser
|
|
3
|
+
title: 网页抓取
|
|
4
|
+
description: 使用无头浏览器直接加载并提取页面内容,作为 API 或 RSS 失效时的通用兜底获取方案。
|
|
5
|
+
topics: [browser, fallback, webpage, 兜底, 浏览器]
|
|
6
|
+
categories: [fallback]
|
|
7
|
+
example_queries: []
|
|
8
|
+
capabilities: [timeline]
|
|
9
|
+
provides: [generic-url]
|
|
10
|
+
key_param: url
|
|
11
|
+
priority: 99
|
|
12
|
+
auth:
|
|
13
|
+
type: none
|
|
14
|
+
discoverable: false
|
|
15
|
+
cadence_hint_seconds: 3600
|
|
16
|
+
params_schema:
|
|
17
|
+
url:
|
|
18
|
+
type: string
|
|
19
|
+
required: true
|
|
20
|
+
description: the page URL to render and extract as a single item
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streamapp/browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"stream": {
|
|
7
|
+
"id": "browser",
|
|
8
|
+
"name": "Headless Browser",
|
|
9
|
+
"tagline": "把页面渲染出来再抓",
|
|
10
|
+
"description": "Render a web page with a headless browser.",
|
|
11
|
+
"normalizer": "generic"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
## 说明(迁自 manifests.yaml 注释)
|
|
3
|
+
|
|
4
|
+
bt0(不太灵影视)— 纯 http recipe。站点从 (1-9)bt0.com 搬到 web{n}.mukaku.com(Vue SPA),
|
|
5
|
+
数据走 JSON API `/prod/api/v1/`;每请求必带站点 JS 写死的常量 app_id + identity(axios 拦截器
|
|
6
|
+
逐请求追加,非按访客生成,照抄即站点自身行为)。迁自本地 RSSHub 路由 lib/routes/bt0/{tlist,search}.ts。
|
|
7
|
+
domain 固定 web2(2/3/5 可达,默认 2);站方换掉那两个常量时需去 assets/index-*.js grep `app_id=` 重抠。
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kind": "http",
|
|
4
|
+
"sourceId": "bt0-search",
|
|
5
|
+
"request": {
|
|
6
|
+
"url": "https://web2.mukaku.com/prod/api/v1/getVideoList?sb={keyword}&page={page}&limit=24&app_id=83768d9ad4&identity=23734adac0301bccdcb107c4aa21f96c",
|
|
7
|
+
"method": "GET"
|
|
8
|
+
},
|
|
9
|
+
"pagination": {
|
|
10
|
+
"mode": "increment",
|
|
11
|
+
"param": "page",
|
|
12
|
+
"start": 1,
|
|
13
|
+
"step": 1,
|
|
14
|
+
"itemsAt": "data.data",
|
|
15
|
+
"maxPages": 2
|
|
16
|
+
},
|
|
17
|
+
"assert": [
|
|
18
|
+
{
|
|
19
|
+
"path": "data.data",
|
|
20
|
+
"desc": "mukaku getVideoList returned data.data array"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"mapping": {
|
|
24
|
+
"title": "title",
|
|
25
|
+
"guid": "mukaku-mv-{idcode}",
|
|
26
|
+
"link": "https://web2.mukaku.com/mv/{idcode}",
|
|
27
|
+
"description": "<img src=\"{image}\"><br>{production_area} / {class} / {language}<br>豆瓣评分:{doub_score}<br>{abstract}",
|
|
28
|
+
"category": "class"
|
|
29
|
+
},
|
|
30
|
+
"meta": {
|
|
31
|
+
"normalizer": "rsshub",
|
|
32
|
+
"type": "post",
|
|
33
|
+
"description": "不太灵影视 — 关键词搜索",
|
|
34
|
+
"topics": [
|
|
35
|
+
"不太灵",
|
|
36
|
+
"影视",
|
|
37
|
+
"磁力",
|
|
38
|
+
"搜索",
|
|
39
|
+
"movie",
|
|
40
|
+
"bt",
|
|
41
|
+
"search"
|
|
42
|
+
],
|
|
43
|
+
"categories": [
|
|
44
|
+
"multimedia"
|
|
45
|
+
],
|
|
46
|
+
"facility": {
|
|
47
|
+
"key": "bt0",
|
|
48
|
+
"label": "不太灵影视"
|
|
49
|
+
},
|
|
50
|
+
"capabilities": [
|
|
51
|
+
"search"
|
|
52
|
+
],
|
|
53
|
+
"key_param": "keyword",
|
|
54
|
+
"priority": 80,
|
|
55
|
+
"discoverable": true,
|
|
56
|
+
"cadence_hint_seconds": 3600,
|
|
57
|
+
"auth": {
|
|
58
|
+
"type": "none"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "mukaku.com",
|
|
61
|
+
"params_schema": {
|
|
62
|
+
"keyword": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"required": true,
|
|
65
|
+
"description": "搜索关键词(片名)"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kind": "http",
|
|
4
|
+
"sourceId": "bt0-tlist",
|
|
5
|
+
"request": {
|
|
6
|
+
"url": "https://web2.mukaku.com/prod/api/v1/getTList?sc={sc}&page={page}&app_id=83768d9ad4&identity=23734adac0301bccdcb107c4aa21f96c",
|
|
7
|
+
"method": "GET"
|
|
8
|
+
},
|
|
9
|
+
"pagination": {
|
|
10
|
+
"mode": "increment",
|
|
11
|
+
"param": "page",
|
|
12
|
+
"start": 1,
|
|
13
|
+
"step": 1,
|
|
14
|
+
"itemsAt": "data.list",
|
|
15
|
+
"maxPages": 1
|
|
16
|
+
},
|
|
17
|
+
"assert": [
|
|
18
|
+
{
|
|
19
|
+
"path": "data.list",
|
|
20
|
+
"desc": "mukaku getTList returned data.list array"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"mapping": {
|
|
24
|
+
"title": "zname",
|
|
25
|
+
"guid": "zname",
|
|
26
|
+
"link": "https://web2.mukaku.com{aurl}",
|
|
27
|
+
"description": "《{title}》 导演: {daoyan}<br>编剧: {bianji}<br>演员: {yanyuan}<br>简介: {conta}",
|
|
28
|
+
"pubDate": "eztime",
|
|
29
|
+
"enclosure_url": "zlink",
|
|
30
|
+
"itunes_item_image": "epic"
|
|
31
|
+
},
|
|
32
|
+
"meta": {
|
|
33
|
+
"normalizer": "rsshub",
|
|
34
|
+
"type": "post",
|
|
35
|
+
"description": "不太灵影视 — 最新资源列表",
|
|
36
|
+
"topics": [
|
|
37
|
+
"不太灵",
|
|
38
|
+
"影视",
|
|
39
|
+
"磁力",
|
|
40
|
+
"电影",
|
|
41
|
+
"电视剧",
|
|
42
|
+
"movie",
|
|
43
|
+
"bt"
|
|
44
|
+
],
|
|
45
|
+
"categories": [
|
|
46
|
+
"multimedia"
|
|
47
|
+
],
|
|
48
|
+
"facility": {
|
|
49
|
+
"key": "bt0",
|
|
50
|
+
"label": "不太灵影视"
|
|
51
|
+
},
|
|
52
|
+
"capabilities": [
|
|
53
|
+
"timeline"
|
|
54
|
+
],
|
|
55
|
+
"key_param": "sc",
|
|
56
|
+
"priority": 80,
|
|
57
|
+
"discoverable": true,
|
|
58
|
+
"cadence_hint_seconds": 3600,
|
|
59
|
+
"auth": {
|
|
60
|
+
"type": "none"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "mukaku.com",
|
|
63
|
+
"params_schema": {
|
|
64
|
+
"sc": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"required": true,
|
|
67
|
+
"description": "分类 1:电影 2:电视剧 3:近日热门 4:本周热门 5:本月热门"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"radar": [
|
|
71
|
+
"mukaku.com/tlist/"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kind": "html",
|
|
4
|
+
"sourceId": "btbtla-detail",
|
|
5
|
+
"request": {
|
|
6
|
+
"url": "{detailUrl}",
|
|
7
|
+
"method": "GET"
|
|
8
|
+
},
|
|
9
|
+
"list": {
|
|
10
|
+
"selector": "div[name=download-list] .module-downlist .module-row-info",
|
|
11
|
+
"limit": 100,
|
|
12
|
+
"fields": {
|
|
13
|
+
"title": {
|
|
14
|
+
"selector": ".module-row-title h4",
|
|
15
|
+
"text": true
|
|
16
|
+
},
|
|
17
|
+
"link": {
|
|
18
|
+
"selector": "a.module-row-text",
|
|
19
|
+
"attr": "href",
|
|
20
|
+
"resolve": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"pagination": {
|
|
25
|
+
"mode": "increment",
|
|
26
|
+
"param": "page",
|
|
27
|
+
"start": 1,
|
|
28
|
+
"step": 1,
|
|
29
|
+
"maxPages": 1
|
|
30
|
+
},
|
|
31
|
+
"assert": [
|
|
32
|
+
{
|
|
33
|
+
"selector": "div[name=download-list] .module-downlist .module-row-info",
|
|
34
|
+
"desc": "btbtla detail download rows present"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"meta": {
|
|
38
|
+
"normalizer": "rsshub",
|
|
39
|
+
"type": "post",
|
|
40
|
+
"description": "BT影视 — 详情页下载行",
|
|
41
|
+
"topics": [
|
|
42
|
+
"btbtla",
|
|
43
|
+
"BT影视",
|
|
44
|
+
"下载",
|
|
45
|
+
"磁力",
|
|
46
|
+
"网盘"
|
|
47
|
+
],
|
|
48
|
+
"categories": [
|
|
49
|
+
"multimedia"
|
|
50
|
+
],
|
|
51
|
+
"facility": {
|
|
52
|
+
"key": "btbtla",
|
|
53
|
+
"label": "BT影视"
|
|
54
|
+
},
|
|
55
|
+
"capabilities": [
|
|
56
|
+
"search"
|
|
57
|
+
],
|
|
58
|
+
"key_param": "detailUrl",
|
|
59
|
+
"priority": 80,
|
|
60
|
+
"discoverable": false,
|
|
61
|
+
"_why_pick_in": "只在 Provider 成员面挑得到,不在频道订阅面——btbtla 组合体里 expand 的那一档",
|
|
62
|
+
"pick_in": ["provider"],
|
|
63
|
+
"cadence_hint_seconds": 3600,
|
|
64
|
+
"auth": {
|
|
65
|
+
"type": "none"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "btbtla.com",
|
|
68
|
+
"params_schema": {
|
|
69
|
+
"detailUrl": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"required": true,
|
|
72
|
+
"description": "详情页绝对 URL"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kind": "html",
|
|
4
|
+
"sourceId": "btbtla-search",
|
|
5
|
+
"request": {
|
|
6
|
+
"url": "https://www.btbtla.com/search/{name}",
|
|
7
|
+
"method": "GET"
|
|
8
|
+
},
|
|
9
|
+
"list": {
|
|
10
|
+
"selector": ".module-item-titlebox a[href^=\"/detail/\"]",
|
|
11
|
+
"limit": 20,
|
|
12
|
+
"fields": {
|
|
13
|
+
"title": {
|
|
14
|
+
"text": true
|
|
15
|
+
},
|
|
16
|
+
"detailUrl": {
|
|
17
|
+
"attr": "href",
|
|
18
|
+
"resolve": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"pagination": {
|
|
23
|
+
"mode": "increment",
|
|
24
|
+
"param": "page",
|
|
25
|
+
"start": 1,
|
|
26
|
+
"step": 1,
|
|
27
|
+
"maxPages": 1
|
|
28
|
+
},
|
|
29
|
+
"assert": [
|
|
30
|
+
{
|
|
31
|
+
"selector": ".module-item-titlebox a[href^=\"/detail/\"]",
|
|
32
|
+
"desc": "btbtla search season cards present"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"meta": {
|
|
36
|
+
"normalizer": "rsshub",
|
|
37
|
+
"type": "post",
|
|
38
|
+
"description": "BT影视 — 关键词搜索(季卡片)",
|
|
39
|
+
"topics": [
|
|
40
|
+
"btbtla",
|
|
41
|
+
"BT影视",
|
|
42
|
+
"影视",
|
|
43
|
+
"磁力",
|
|
44
|
+
"网盘",
|
|
45
|
+
"搜索",
|
|
46
|
+
"movie"
|
|
47
|
+
],
|
|
48
|
+
"categories": [
|
|
49
|
+
"multimedia"
|
|
50
|
+
],
|
|
51
|
+
"facility": {
|
|
52
|
+
"key": "btbtla",
|
|
53
|
+
"label": "BT影视"
|
|
54
|
+
},
|
|
55
|
+
"capabilities": [
|
|
56
|
+
"search"
|
|
57
|
+
],
|
|
58
|
+
"key_param": "name",
|
|
59
|
+
"priority": 80,
|
|
60
|
+
"discoverable": true,
|
|
61
|
+
"cadence_hint_seconds": 3600,
|
|
62
|
+
"auth": {
|
|
63
|
+
"type": "none"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "btbtla.com",
|
|
66
|
+
"params_schema": {
|
|
67
|
+
"name": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"required": true,
|
|
70
|
+
"description": "搜索关键词(片名)"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streamapp/btbtla",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "stream",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stream-recipe"
|
|
7
|
+
],
|
|
8
|
+
"stream": {
|
|
9
|
+
"type": "recipe",
|
|
10
|
+
"name": "BT影视",
|
|
11
|
+
"tagline": "关键词搜索,取季卡片",
|
|
12
|
+
"facility": "btbtla",
|
|
13
|
+
"schemaVersion": 1
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Stream 内置
|
|
2
|
+
|
|
3
|
+
进程内实现的 Source 化声明(方案 A:Provider 成员一律是 Source)。
|
|
4
|
+
每个源的 `fixed_params.mode` 对应 bootstrap 在 builtin adapter 上注册的实现函数;
|
|
5
|
+
名称/描述/参数模式住在这里(manifest 是元数据唯一的家),不进订阅目录(`discoverable: false`)。
|
|
6
|
+
|
|
7
|
+
`required: true` —— 核心插件:磁力解析/播放解析/转写/LLM/抓取都是进程内能力,不能被用户关掉。
|
|
8
|
+
开关锁定为开。
|