-
Notifications
You must be signed in to change notification settings - Fork 4
feat(browser-act): add Kuaishou search pack #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
2233admin
merged 2 commits into
2233admin:main
from
chuanxu742-glitch:contrib/kuaishou-search
Sep 6, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
backend/browser_act_packs/video-platforms/kuaishou-search/SKILL.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| --- | ||
| name: kuaishou-search | ||
| description: "Extract structured public Kuaishou video search results from the current browser page." | ||
| --- | ||
|
|
||
| # Kuaishou — Video Search | ||
|
|
||
| > Search keyword → bounded structured video results | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Browser Act is available. | ||
| - The target browser can reach `kuaishou.com`. | ||
| - The current session may need human login or verification. | ||
|
|
||
| ## Execution | ||
|
|
||
| Navigate to: | ||
|
|
||
| ```text | ||
| https://www.kuaishou.com/search/video?searchKey={query} | ||
| ``` | ||
|
|
||
| Wait for the page to settle, then run: | ||
|
|
||
| ```bash | ||
| python scripts/extract-search.py --max-results 10 | ||
| ``` | ||
|
|
||
| The result contains the video URL, caption, author, cover, playable media | ||
| URL when exposed by the page, publication timestamp, tags, and bounded | ||
| engagement statistics. | ||
|
|
||
| ## Operational boundary | ||
|
|
||
| This pack reads the public search state already present in the browser. It | ||
| never automates login, captcha solving, or anti-bot bypass. Login, verification, | ||
| regional restrictions, and blocked responses are human-handled conditions. | ||
|
|
||
| ## Limitations | ||
|
|
||
| The manifest extracts the initial search result state only. Cursor-based | ||
| follow-up requests and comment collection are intentionally out of scope for | ||
| this pack; they require a separate pagination contract and should not be | ||
| silently represented as complete results. |
15 changes: 15 additions & 0 deletions
15
backend/browser_act_packs/video-platforms/kuaishou-search/channel.manifest.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "domain": "video-platforms", | ||
| "capability": "kuaishou-search", | ||
| "param_schema": [ | ||
| {"name": "query", "required": true}, | ||
| {"name": "max_results", "required": false, "default": "10"} | ||
| ], | ||
| "steps": [ | ||
| {"op": "navigate", "url_template": "https://www.kuaishou.com/search/video?searchKey={query}"}, | ||
| {"op": "wait", "wait_mode": "stable"}, | ||
| {"op": "eval_script", "script": "scripts/extract-search.py", "args": ["--max-results", "{max_results}"]} | ||
| ], | ||
| "pagination": {"mode": "none"}, | ||
| "success": {"min_count": 1, "required_field": "url"} | ||
| } | ||
88 changes: 88 additions & 0 deletions
88
backend/browser_act_packs/video-platforms/kuaishou-search/scripts/extract-search.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import argparse | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument("--max-results", type=int, default=10) | ||
| args = parser.parse_args() | ||
| max_results = max(1, min(args.max_results, 50)) | ||
|
|
||
| js = r"""(() => { | ||
| const clean = (value) => String(value || '').replace(/\s+/g, ' ').trim(); | ||
| const firstUrl = (value) => { | ||
| if (typeof value === 'string' && value) return value; | ||
| if (Array.isArray(value)) { | ||
| for (const item of value) { | ||
| const result = firstUrl(item); | ||
| if (result) return result; | ||
| } | ||
| } | ||
| if (value && typeof value === 'object') { | ||
| for (const key of ['url', 'src', 'srcNoWatermark', 'playUrl']) { | ||
| const result = firstUrl(value[key]); | ||
| if (result) return result; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| const isoTime = (value) => { | ||
| const timestamp = Number(value); | ||
| if (!Number.isFinite(timestamp) || timestamp <= 0) return null; | ||
| const date = new Date(timestamp < 100000000000 ? timestamp * 1000 : timestamp); | ||
| return Number.isNaN(date.getTime()) ? null : date.toISOString(); | ||
| }; | ||
| const stateValues = Object.values(window.INIT_STATE || {}); | ||
| const state = stateValues.find((value) => value && Array.isArray(value.feeds)) || {feeds: []}; | ||
| const items = state.feeds.map((feed) => { | ||
| if (!feed || typeof feed !== 'object') return null; | ||
| const photo = feed.photo && typeof feed.photo === 'object' ? feed.photo : null; | ||
| if (!photo || !clean(photo.id)) return null; | ||
| const author = feed.author && typeof feed.author === 'object' ? feed.author : {}; | ||
| const comment = feed.comment && typeof feed.comment === 'object' ? feed.comment : {}; | ||
| const photoId = clean(photo.id); | ||
| const caption = clean(photo.caption); | ||
| const coverUrl = firstUrl(photo.coverUrl); | ||
| const playUrl = firstUrl(photo.manifestH265) || firstUrl(photo.manifest); | ||
| const statistics = { | ||
| like_count: photo.likeCount, | ||
| comment_count: comment.us_c, | ||
| collect_count: photo.collectCount, | ||
| view_count: photo.viewCount, | ||
| share_count: photo.shareCount, | ||
| }; | ||
| Object.keys(statistics).forEach((key) => { | ||
| if (statistics[key] === null || statistics[key] === undefined) delete statistics[key]; | ||
| }); | ||
| return { | ||
| title: caption || `Kuaishou video ${photoId}`, | ||
| content: caption, | ||
| author: clean(author.name), | ||
| author_id: clean(author.id) || null, | ||
| author_avatar: firstUrl(author.headerUrl), | ||
| url: `https://www.kuaishou.com/short-video/${encodeURIComponent(photoId)}`, | ||
| photo_id: photoId, | ||
| create_time: photo.timestamp || null, | ||
| published_at: isoTime(photo.timestamp), | ||
| cover_url: coverUrl, | ||
| play_url: playUrl, | ||
| statistics, | ||
| media: { | ||
| type: 'video', | ||
| play_url: playUrl, | ||
| cover_url: coverUrl, | ||
| duration_ms: photo.duration || null, | ||
| width: photo.width || null, | ||
| height: photo.height || null, | ||
| }, | ||
| tags: Array.isArray(feed.tags) | ||
| ? feed.tags.map((tag) => clean(tag && tag.name)).filter(Boolean) | ||
| : [], | ||
| }; | ||
| }).filter(Boolean).slice(0, MAX_RESULTS); | ||
| return {count: items.length, items}; | ||
| })()""" | ||
| print(js.replace("MAX_RESULTS", str(max_results))) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| """Behavioral checks for the Kuaishou Browser Act pack.""" | ||
|
|
||
| import json | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from backend.browser_act_packs.catalog import PackCatalog | ||
|
|
||
| _PACK = Path(PackCatalog().root) / "video-platforms" / "kuaishou-search" | ||
|
|
||
|
|
||
| def _extract(state: dict, max_results: int) -> dict: | ||
| script = subprocess.run( | ||
| [ | ||
| sys.executable, | ||
| str(_PACK / "scripts" / "extract-search.py"), | ||
| "--max-results", | ||
| str(max_results), | ||
| ], | ||
| check=True, | ||
| capture_output=True, | ||
| encoding="utf-8", | ||
| timeout=30, | ||
| ).stdout | ||
| result = subprocess.run( | ||
| [ | ||
| "node", | ||
| "-e", | ||
| "const fs = require('node:fs');" | ||
| "const {state, script} = JSON.parse(fs.readFileSync(0, 'utf8'));" | ||
| "globalThis.window = {INIT_STATE: state};" | ||
| "console.log(JSON.stringify(eval(script)));", | ||
| ], | ||
| input=json.dumps({"state": state, "script": script}, ensure_ascii=False), | ||
| check=True, | ||
| capture_output=True, | ||
| encoding="utf-8", | ||
| timeout=30, | ||
| ) | ||
| return json.loads(result.stdout) | ||
|
|
||
|
|
||
| def test_search_returns_bounded_records_after_skipping_invalid_feeds() -> None: | ||
| result = _extract( | ||
| { | ||
| "search": { | ||
| "feeds": [ | ||
| None, | ||
| {"photo": {"caption": "Missing ID"}}, | ||
| { | ||
| "photo": { | ||
| "id": "a/b", | ||
| "caption": " 快手\n 视频 ", | ||
| "timestamp": 1700000000, | ||
| "likeCount": 0, | ||
| "coverUrl": [{"url": "https://media.example/cover.jpg"}], | ||
| "manifest": {"playUrl": "https://media.example/video.mp4"}, | ||
| }, | ||
| "author": {"name": " Alice "}, | ||
| }, | ||
| {"photo": {"id": "second"}}, | ||
| {"photo": {"id": "third"}}, | ||
| ] | ||
| } | ||
| }, | ||
| 2, | ||
| ) | ||
|
|
||
| assert result["count"] == 2 | ||
| assert [item["url"] for item in result["items"]] == [ | ||
| "https://www.kuaishou.com/short-video/a%2Fb", | ||
| "https://www.kuaishou.com/short-video/second", | ||
| ] | ||
| first = result["items"][0] | ||
| assert first["title"] == "快手 视频" | ||
| assert first["author"] == "Alice" | ||
| assert first["published_at"] == "2023-11-14T22:13:20.000Z" | ||
| assert first["statistics"] == {"like_count": 0} | ||
| assert first["cover_url"] == "https://media.example/cover.jpg" | ||
| assert first["play_url"] == "https://media.example/video.mp4" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize(("requested", "expected"), [(0, 1), (100, 50)]) | ||
| def test_search_clamps_result_limit(requested: int, expected: int) -> None: | ||
| feeds = [{"photo": {"id": str(index)}} for index in range(51)] | ||
|
|
||
| result = _extract({"search": {"feeds": feeds}}, requested) | ||
|
|
||
| assert result["count"] == expected | ||
| assert [item["photo_id"] for item in result["items"]] == [ | ||
| str(index) for index in range(expected) | ||
| ] | ||
|
|
||
|
|
||
| def test_search_without_initial_state_returns_no_records() -> None: | ||
| assert _extract({}, 10) == {"count": 0, "items": []} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 50384
🤖 get_repo_knowledge executed:
get_repo_knowledge 2233admin/opencli-Razormind /tmp/coderabbit-repo-knowledge/2233admin-opencli-razormind-cb538e01Length of output: 830
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 35722
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 50385
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 50385
URL-encode
querybefore navigation.BrowserActChannel._run_pageappliesurl_template.format(**ctx)and passes the result directly tosess.navigate; it does not encode template values. Query values containing&,?, or#can therefore change the search URL. Encodequeryexactly once before formatting the URL.🤖 Prompt for AI Agents