feat: 将 47 个 Operit TypeScript 脚本全面翻译为 Python

- 新增 operit_runtime.py: 跨平台 Python 运行时
  - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker
  - 实现 OkHttp 链式 HTTP 客户端兼容层
  - 实现 complete()/exports/console 全局对象
  - 实现 OperitScriptLoader 脚本加载器
  - 支持 Windows/macOS/Linux 三平台

- 新增 operit_py/ 目录: 47 个 Python 翻译脚本
  - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref
  - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw
  - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic
  - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader
  - 网络类: extended_http_tools, network_test
  - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant
  - 系统类: system_tools, super_admin, daily_life, operit_editor
  - 对话类: ai_chat, extended_chat, qq_intelligent
  - 工作流类: workflow, vflow_trigger, tasker
  - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser

- 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册
- Android 专属功能已降级处理(保留接口,返回不支持提示)
This commit is contained in:
shuiyu1123 committed 2026-06-23 12:27:18 +00:00
1 parent 6fa759665d
commit dc77dfd304
48 files changed
+22273

No files matched your search

+666
View File
@@ -0,0 +1,666 @@
# METADATA
# {
# "name": "Automatic_bilibili_assistant",
# "display_name": {"zh": "Bilibili自动化助手", "en": "Bilibili Automation Assistant"},
# "description": {"zh": "高级B站智能助手,通过UI自动化技术实现B站应用交互,支持视频搜索播放、评论互动、用户关注等功能,为AI赋予B站社交和内容消费能力。适用于自动追番、视频推荐、社交互动等场景。", "en": "Advanced Bilibili assistant powered by UI automation. Supports video search/playback, commenting, and following uploaders, enabling AI-driven content consumption and social interaction on Bilibili."},
# "category": "Automatic",
# "tools": [
# {"name": "workflow_guide", "description": {"zh": "B站助手工具使用流程指南。", "en": "Workflow guide for the Bilibili assistant."}, "parameters": [], "advice": True},
# {"name": "search_video", "description": {"zh": "在B站搜索视频内容", "en": "Search videos on Bilibili."}, "parameters": [
# {"name": "keyword", "description": {"zh": "搜索关键词", "en": "Search keyword."}, "type": "string", "required": True},
# {"name": "filter_type", "description": {"zh": "搜索结果过滤类型:comprehensive(综合)、new(最新)、hot(最多播放)、danmaku(最多弹幕)", "en": "Result filter: comprehensive, new, hot (most played), danmaku (most danmaku)."}, "type": "string", "required": False}
# ]},
# {"name": "play_video", "description": {"zh": "播放指定的视频,可以通过标题或位置选择", "en": "Play a specific video by title keyword or by index."}, "parameters": [
# {"name": "video_title", "description": {"zh": "要播放的视频标题关键词", "en": "Keyword to match the video title."}, "type": "string", "required": False},
# {"name": "video_index", "description": {"zh": "要播放的视频在搜索结果中的索引位置(从1开始)", "en": "Index of the video in the search results (1-based)."}, "type": "number", "required": False}
# ]},
# {"name": "return_to_video_list", "description": {"zh": "从视频播放界面返回到视频列表界面", "en": "Return from the video player page back to the video list."}, "parameters": []},
# {"name": "send_comment", "description": {"zh": "在当前视频下发送评论", "en": "Post a comment under the current video."}, "parameters": [
# {"name": "comment_text", "description": {"zh": "要发送的评论内容", "en": "Comment text to send."}, "type": "string", "required": True}
# ]},
# {"name": "like_video", "description": {"zh": "给当前视频点赞", "en": "Like the current video."}, "parameters": []},
# {"name": "collect_video", "description": {"zh": "收藏当前视频", "en": "Save (favorite) the current video."}, "parameters": [
# {"name": "folder_name", "description": {"zh": "收藏夹名称,留空则使用默认收藏夹", "en": "Favorite folder name (leave empty to use the default folder)."}, "type": "string", "required": False}
# ]},
# {"name": "follow_uploader", "description": {"zh": "关注当前视频的UP主", "en": "Follow the uploader (UP) of the current video."}, "parameters": []},
# {"name": "get_video_info", "description": {"zh": "获取当前视频的详细信息", "en": "Get detailed info of the current video."}, "parameters": []},
# {"name": "browse_comments", "description": {"zh": "浏览当前视频的评论", "en": "Browse comments of the current video."}, "parameters": [
# {"name": "comment_count", "description": {"zh": "获取的评论数量,默认为5条", "en": "Number of comments to fetch (default: 5)."}, "type": "number", "required": False}
# ]},
# {"name": "navigate_to_home", "description": {"zh": "导航到B站首页", "en": "Navigate to the Bilibili home page."}, "parameters": []},
# {"name": "navigate_to_following", "description": {"zh": "导航到关注页面", "en": "Navigate to the Following page."}, "parameters": []},
# {"name": "navigate_to_history", "description": {"zh": "导航到观看历史页面", "en": "Navigate to the watch history page."}, "parameters": []},
# {"name": "toggle_fullscreen", "description": {"zh": "切换视频全屏/非全屏状态", "en": "Toggle fullscreen / windowed mode."}, "parameters": []},
# {"name": "adjust_playback_speed", "description": {"zh": "调整视频播放速度", "en": "Adjust video playback speed."}, "parameters": [
# {"name": "speed", "description": {"zh": "播放速度:0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x", "en": "Playback speed: 0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x."}, "type": "string", "required": True}
# ]}
# ]
# }
# 注意:本脚本深度依赖 Android UI 自动化 API(UINode、Tools.UI),在非 Android 平台上运行时相关功能不可用。
import json
import re
import time
from urllib.parse import quote, unquote
Error = Exception
# B站应用包名和主要Activity
BILIBILI_PACKAGE = "tv.danmaku.bili"
MAIN_ACTIVITY = "tv.danmaku.bili.MainActivityV2"
VIDEO_ACTIVITY = "com.bilibili.ship.theseus.detail.UnitedBizDetailsActivity"
def createResponse(success, message, data=None):
if data is None:
data = {}
if isinstance(data, str):
return {"success": True, "message": message, "data": data}
result = {"success": success, "message": message}
if isinstance(data, dict):
result.update(data)
return result
async def findAndClick(finder):
element = await finder()
if element:
await element.click()
await Tools.System.sleep(1000)
return True
return False
async def findAndClickByText(text):
async def _finder():
page = await UINode.getCurrentPage()
return page.findByText(text)
return await findAndClick(_finder)
async def ensureMain(packageName=BILIBILI_PACKAGE):
pageInfo = await Tools.UI.getPageInfo()
if pageInfo.packageName == packageName and (getattr(pageInfo, "activityName", None) or "") and MAIN_ACTIVITY in (pageInfo.activityName or ""):
console.log("Already on the main activity.")
return True
if pageInfo.packageName != packageName:
console.log(f"Not in the correct app. Current: {pageInfo.packageName}. Starting {packageName}.")
await Tools.System.startApp(packageName)
await Tools.System.sleep(200)
console.log("Attempting to go back to main activity.")
for i in range(4):
pageInfo = await Tools.UI.getPageInfo()
if pageInfo.packageName == packageName and (getattr(pageInfo, "activityName", None) or "") and MAIN_ACTIVITY in (pageInfo.activityName or ""):
console.log(f"Successfully returned to main activity on attempt {i + 1}.")
return True
if pageInfo.packageName != packageName:
console.log("Exited the app while trying to go back.")
break
await Tools.UI.pressKey("KEYCODE_BACK")
await Tools.System.sleep(500)
console.log("Pressing back failed or exited app. Performing a full app restart.")
await Tools.System.stopApp(packageName)
await Tools.System.sleep(1000)
await Tools.System.startApp(packageName)
await Tools.System.sleep(4000)
pageInfo = await Tools.UI.getPageInfo()
if pageInfo.packageName == packageName and (getattr(pageInfo, "activityName", None) or "") and MAIN_ACTIVITY in (pageInfo.activityName or ""):
console.log("Successfully reached main activity after restart.")
return True
console.error(f"Failed to navigate to the main activity of {packageName}.")
return False
async def navigateToSearch():
console.log("导航到搜索页面")
if not await ensureMain():
return False
page = await UINode.getCurrentPage()
searchContainer = page.findById('expand_search')
if searchContainer:
await searchContainer.click()
await Tools.System.sleep(1000)
return True
return False
lastSearchResults = []
async def search_video(params):
keyword = params.get("keyword")
filter_type = params.get("filter_type")
count = params.get("count", 20)
global lastSearchResults
console.log(f"搜索视频: {keyword}, 过滤类型: {filter_type}")
if not await navigateToSearch():
return createResponse(False, "无法进入搜索页面", {"keyword": keyword})
await Tools.System.sleep(1000)
searchInput = (await UINode.getCurrentPage()).findById('search_plate')
if searchInput:
await Tools.UI.setText(keyword)
await Tools.System.sleep(500)
else:
await Tools.UI.setText(keyword)
await Tools.System.sleep(500)
searchButton = (await UINode.getCurrentPage()).findById('action_search')
if searchButton:
await searchButton.click()
else:
await Tools.UI.pressKey("KEYCODE_ENTER")
await Tools.System.sleep(4000)
if filter_type:
await applySearchFilter(filter_type)
results = await getSearchResults(count)
lastSearchResults = results
return createResponse(True, f"搜索到 {len(results)} 条有效视频结果", {
"keyword": keyword,
"filter_type": filter_type,
"results": [{k: v for k, v in r.items() if k != 'element'} for r in results],
"result_count": len(results),
})
async def applySearchFilter(filterType):
page = await UINode.getCurrentPage()
filterButton = page.findByText("综合")
if filterButton:
await filterButton.click()
await Tools.System.sleep(1000)
if filterType == "new":
await findAndClickByText("最新")
await Tools.System.sleep(1000)
elif filterType == "hot":
await findAndClickByText("最多播放")
await Tools.System.sleep(1000)
elif filterType == "danmaku":
await findAndClickByText("最多弹幕")
await Tools.System.sleep(1000)
async def getSearchResults(desiredCount=20):
global lastSearchResults
page = await UINode.getCurrentPage()
results = []
seenTitles = set()
resultsContainer = page.findById('tv.danmaku.bili:id/recycler_view')
if not resultsContainer:
console.log("Could not find search results container.")
return results
lastResultCount = -1
maxScrolls = 10
scrollCount = 0
for i in range(maxScrolls):
page = await UINode.getCurrentPage()
currentContainer = page.findById('tv.danmaku.bili:id/recycler_view')
if not currentContainer:
break
for item in (getattr(currentContainer, "children", None) or []):
titleNode = item.findById('tv.danmaku.bili:id/title')
title = titleNode.text if titleNode else None
if not title or title in seenTitles:
continue
isAd = item.findById('tv.danmaku.bili:id/ad_tint_frame') or item.findById('tv.danmaku.bili:id/ad_tag')
isUser = item.findById('tv.danmaku.bili:id/user_info')
isGame = item.findById('tv.danmaku.bili:id/iv_background')
isLive = item.findByText("直播")
if isAd or isUser or isGame or isLive:
continue
seenTitles.add(title)
uploaderNode = item.findById('tv.danmaku.bili:id/upper') or item.findById('tv.danmaku.bili:id/upuser')
playCountNode = item.findById('tv.danmaku.bili:id/play_num')
durationNode = item.findById('tv.danmaku.bili:id/duration')
postTimeNode = item.findById('tv.danmaku.bili:id/post_time') or item.findById('tv.danmaku.bili:id/danmakus_num')
results.append({
"index": len(results) + 1,
"title": title,
"uploader": uploaderNode.text if uploaderNode else "N/A",
"play_count": playCountNode.text if playCountNode else "N/A",
"duration": durationNode.text if durationNode else "N/A",
"post_time": postTimeNode.text if postTimeNode else "N/A",
"element": item,
})
if len(results) >= desiredCount:
break
if len(results) >= desiredCount:
console.log(f"Reached desired count of {desiredCount}.")
break
if len(results) == lastResultCount:
console.log("Scrolling is not yielding new results. Stopping.")
break
lastResultCount = len(results)
await Tools.UI.swipe(540, 1500, 540, 800)
await Tools.System.sleep(1500)
scrollCount += 1
console.log(f"Scrolling back up {scrollCount} times.")
for i in range(scrollCount):
await Tools.UI.swipe(540, 800, 540, 1550)
await Tools.System.sleep(1000)
return results[:desiredCount]
async def play_video(params):
video_title = params.get("video_title")
video_index = params.get("video_index")
if not video_title and not video_index:
return createResponse(False, "请提供视频标题或索引。")
console.log(f'尝试播放视频 - 标题: "{video_title}", 索引: {video_index}')
page = await UINode.getCurrentPage()
seenTitles = set()
currentVideoCount = 0
maxScrolls = 10
videoFoundAndClicked = False
foundVideoTitle = None
initialContainer = page.findById('tv.danmaku.bili:id/recycler_view')
if not initialContainer:
return createResponse(False, "似乎不在搜索结果页面,无法播放视频。")
lastSeenCount = -1
scrollCount = 0
for i in range(maxScrolls):
page = await UINode.getCurrentPage()
currentContainer = page.findById('tv.danmaku.bili:id/recycler_view')
if not currentContainer:
console.log("Could not find search results container during scroll.")
break
visibleItems = getattr(currentContainer, "children", None) or []
for item in visibleItems:
titleNode = item.findById('tv.danmaku.bili:id/title')
title = titleNode.text if titleNode else None
if not title or title in seenTitles:
continue
isAd = item.findById('tv.danmaku.bili:id/ad_tint_frame') or item.findById('tv.danmaku.bili:id/ad_tag')
isUser = item.findById('tv.danmaku.bili:id/user_info')
isGame = item.findById('tv.danmaku.bili:id/iv_background')
isLive = item.findByText("直播")
if isAd or isUser or isGame or isLive:
continue
seenTitles.add(title)
currentVideoCount += 1
shouldClick = False
if video_index:
if currentVideoCount == video_index:
shouldClick = True
elif video_title:
if video_title in title:
shouldClick = True
if shouldClick:
coverImage = item.findById('tv.danmaku.bili:id/cover')
if coverImage:
console.log("Clicking on video cover image.")
await coverImage.click()
else:
console.log("Cover image not found, clicking on the whole item.")
await item.click()
await Tools.System.sleep(4000)
videoFoundAndClicked = True
foundVideoTitle = title
break
if videoFoundAndClicked:
break
if len(seenTitles) == lastSeenCount:
console.log("Scrolling is not yielding new results. Stopping.")
break
lastSeenCount = len(seenTitles)
if video_index and currentVideoCount >= video_index:
break
await Tools.UI.swipe(540, 1500, 540, 800)
await Tools.System.sleep(1500)
scrollCount += 1
if not videoFoundAndClicked and scrollCount > 0:
console.log(f"未找到视频,滚动回去 {scrollCount} 次")
for i in range(scrollCount):
await Tools.UI.swipe(540, 800, 540, 1550)
await Tools.System.sleep(1000)
if videoFoundAndClicked:
return createResponse(True, f'成功开始播放视频: "{foundVideoTitle}"', {"video_title": video_title, "video_index": video_index})
else:
return createResponse(False, "未找到指定的视频", {"video_title": video_title, "video_index": video_index})
async def send_comment(params):
commentText = params.get("comment_text") or ""
if not await switch_to_comment_activity():
return createResponse(False, "不在视频页面")
console.log(f"发送评论: {commentText}")
page = await UINode.getCurrentPage()
commentArea = page.findById("input")
if commentArea:
await commentArea.click()
await Tools.System.sleep(1000)
await Tools.UI.setText(commentText)
await Tools.System.sleep(500)
sendButton = page.findByText("发送")
if sendButton:
await sendButton.click()
await Tools.System.sleep(1000)
return createResponse(True, "评论发送成功", {"comment_text": commentText})
else:
return createResponse(False, "未找到评论输入框", {"comment_text": commentText})
async def is_in_video_activity():
pageInfo = await Tools.UI.getPageInfo()
return VIDEO_ACTIVITY in (getattr(pageInfo, "activityName", "") or "")
async def is_on_search_results_page():
page = await UINode.getCurrentPage()
filterButton = page.findByText("综合") and page.findByText("搜索")
return bool(filterButton)
async def return_to_video_list(params):
console.log("Attempting to return to video list...")
if await is_in_video_activity():
await Tools.UI.pressKey("KEYCODE_BACK")
await Tools.System.sleep(2000)
if await is_on_search_results_page():
return createResponse(True, "Successfully returned to video list.")
else:
return createResponse(False, "Failed to return to video list, did not land on search results page.")
else:
if await is_on_search_results_page():
return createResponse(True, "Already on the video list page.")
return createResponse(False, "Not in a video activity, no action taken.")
async def switch_to_comment_activity():
if not await is_in_video_activity():
return False
console.log("切换到评论页面")
await findAndClickByText("评论")
await Tools.System.sleep(1000)
return True
async def like_video(params):
if not await is_in_video_activity():
return createResponse(False, "不在视频页面")
console.log("给视频点赞")
async def _finder():
return (await UINode.getCurrentPage()).findById("frame_like")
if await findAndClick(_finder):
return createResponse(True, "点赞成功")
else:
return createResponse(False, "未找到点赞按钮")
async def collect_video(params):
if not await is_in_video_activity():
return createResponse(False, "不在视频页面")
folderName = params.get("folder_name")
console.log(f"开始收藏视频, 文件夹: {folderName}")
async def _finder():
return (await UINode.getCurrentPage()).findById("frame_fav")
if await findAndClick(_finder):
return createResponse(True, "收藏成功", {"folder_name": folderName})
else:
return createResponse(False, "未找到收藏按钮", {"folder_name": folderName})
async def follow_uploader(params):
if not await is_in_video_activity():
return createResponse(False, "不在视频页面")
console.log("开始关注UP主")
async def _finder():
return (await UINode.getCurrentPage()).findAllById("follow")
if await findAndClick(_finder):
return createResponse(True, "关注成功")
else:
return createResponse(False, "未找到关注按钮或已经关注")
async def get_video_info(params):
if not await is_in_video_activity():
return createResponse(False, "不在视频页面")
console.log("获取视频信息")
page = await UINode.getCurrentPage()
videoInfo = {
"title": "",
"uploader": "",
"play_count": "",
"comment_count": "",
"like_count": "",
"coin_count": "",
"collect_count": "",
"forward_count": "",
}
titleElement = page.findById('title')
if titleElement and getattr(titleElement, "text", None):
videoInfo["title"] = titleElement.text
likeElement = page.findById('frame_like')
if likeElement and getattr(likeElement, "contentDesc", None):
videoInfo["like_count"] = likeElement.contentDesc
collectElement = page.findById('frame_fav')
if collectElement and getattr(collectElement, "contentDesc", None):
videoInfo["collect_count"] = collectElement.contentDesc
coinElement = page.findById('frame_coin')
if coinElement and getattr(coinElement, "contentDesc", None):
videoInfo["coin_count"] = coinElement.contentDesc
forwardElement = page.findById('frame_share')
if forwardElement and getattr(forwardElement, "contentDesc", None):
videoInfo["forward_count"] = forwardElement.contentDesc
uploaderElements = page.findAllById("author_name")
if uploaderElements and len(uploaderElements) > 0:
videoInfo["uploader"] = "/".join([u.text for u in uploaderElements])
return createResponse(True, "获取视频信息成功", {"video_info": videoInfo})
async def browse_comments(params):
commentCount = params.get("comment_count") or 5
if not await switch_to_comment_activity():
return createResponse(False, "不在视频页面")
console.log(f"浏览评论,获取{commentCount}条")
page = await UINode.getCurrentPage()
await Tools.UI.swipe(540, 1500, 540, 800)
await Tools.System.sleep(2000)
commentList = page.allTexts()
return createResponse(True, "获取评论成功", {"all_texts": commentList})
async def navigate_to_home(params):
console.log("导航到首页")
if await ensureMain():
return createResponse(True, "已导航到首页")
else:
return createResponse(False, "无法导航到首页")
async def navigate_to_following(params):
console.log("导航到关注页面")
page = await UINode.getCurrentPage()
followingTab = page.findByText("关注")
if followingTab:
await followingTab.click()
await Tools.System.sleep(2000)
return createResponse(True, "已导航到关注页面")
else:
return createResponse(False, "未找到关注标签")
async def navigate_to_history(params):
console.log("导航到历史页面")
page = await UINode.getCurrentPage()
historyButton = page.findByText("历史")
if not historyButton:
historyButton = page.findByText("观看历史")
if historyButton:
await historyButton.click()
await Tools.System.sleep(2000)
return createResponse(True, "已导航到历史页面")
else:
return createResponse(False, "未找到历史按钮")
async def toggle_fullscreen(params):
console.log("切换全屏状态")
await Tools.UI.tap(540, 960)
await Tools.System.sleep(1000)
page = await UINode.getCurrentPage()
fullscreenButton = page.findByContentDesc("全屏")
if not fullscreenButton:
fullscreenButton = page.findByText("全屏")
if not fullscreenButton:
fullscreenButton = page.findById('fullscreen_button')
if fullscreenButton:
await fullscreenButton.click()
await Tools.System.sleep(1000)
return createResponse(True, "全屏状态切换成功")
else:
return createResponse(False, "未找到全屏按钮")
async def adjust_playback_speed(params):
speed = params.get("speed") or "1.0x"
console.log(f"调整播放速度为: {speed}")
await Tools.UI.tap(540, 960)
await Tools.System.sleep(1000)
page = await UINode.getCurrentPage()
speedButton = page.findByText("倍速")
if not speedButton:
speedButton = page.findByText("1.0x")
if not speedButton:
speedButton = page.findById('speed_button')
if speedButton:
await speedButton.click()
await Tools.System.sleep(1000)
targetSpeedButton = page.findByText(speed)
if targetSpeedButton:
await targetSpeedButton.click()
await Tools.System.sleep(1000)
return createResponse(True, f"播放速度已调整为 {speed}", {"speed": speed})
else:
return createResponse(False, f"未找到 {speed} 速度选项", {"speed": speed})
else:
return createResponse(False, "未找到速度设置按钮", {"speed": speed})
async def main():
console.log("=== B站智能助手测试 ===")
console.log(await search_video({"keyword": "原神"}))
await play_video({"video_index": 4})
console.log(await get_video_info({}))
console.log(await browse_comments({}))
async def wrapToolExecution(func, params):
try:
result = await func(params)
complete(dict(result))
except Exception as error:
console.error(f"Tool {func.__name__} failed unexpectedly", error)
complete({
"success": False,
"message": f"工具执行时发生意外错误: {str(error)}",
})
def _make_wrapper(func):
async def wrapper(params):
await wrapToolExecution(func, params)
return wrapper
exports.search_video = _make_wrapper(search_video)
exports.play_video = _make_wrapper(play_video)
exports.return_to_video_list = _make_wrapper(return_to_video_list)
exports.send_comment = _make_wrapper(send_comment)
exports.like_video = _make_wrapper(like_video)
exports.collect_video = _make_wrapper(collect_video)
exports.follow_uploader = _make_wrapper(follow_uploader)
exports.get_video_info = _make_wrapper(get_video_info)
exports.browse_comments = _make_wrapper(browse_comments)
exports.navigate_to_home = _make_wrapper(navigate_to_home)
exports.navigate_to_following = _make_wrapper(navigate_to_following)
exports.navigate_to_history = _make_wrapper(navigate_to_history)
exports.toggle_fullscreen = _make_wrapper(toggle_fullscreen)
exports.adjust_playback_speed = _make_wrapper(adjust_playback_speed)
exports.main = main