- 新增 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 专属功能已降级处理(保留接口,返回不支持提示)
338 lines
14 KiB
Python
338 lines
14 KiB
Python
# METADATA
|
||
# {
|
||
# "name": "Experimental_qq_intelligent",
|
||
# "display_name": {
|
||
# "zh": "实验性 QQ 智能助手",
|
||
# "en": "Experimental QQ Intelligent Assistant"
|
||
# },
|
||
# "description": { "zh": "高级QQ智能助手,通过UI自动化技术实现QQ应用交互,支持消息自动回复、历史记录读取、联系人搜索与通讯等功能,为AI赋予QQ社交能力。适用于智能客服、自动回复、社交辅助等场景。", "en": "Advanced QQ assistant powered by UI automation. Supports sending messages, reading chat history, searching contacts/groups, and basic communication workflows, enabling AI-driven social messaging scenarios." },
|
||
# "category": "Media",
|
||
# "tools": [
|
||
# {
|
||
# "name": "reply",
|
||
# "description": { "zh": "在当前聊天窗口输入消息并发送。一般情况下,用户想要ai帮忙发送消息时,需要ai自己去生成回复的消息,如果不确定发送的内容,请不要调用工具。只要是停留在聊天界面,就可以直接调用这个。", "en": "Type and send a message in the current chat window. Usually the AI should generate the message content by itself; if you are unsure what to send, do not call this tool. As long as you are staying on a chat page, you can call it directly." },
|
||
# "parameters": [
|
||
# { "name": "message", "description": { "zh": "要发送的消息", "en": "Message text to send." }, "type": "string", "required": true },
|
||
# { "name": "click_send", "description": { "zh": "是否点击发送按钮", "en": "Whether to tap the Send button." }, "type": "boolean", "required": false }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "find_user",
|
||
# "description": { "zh": "在QQ联系人或群成员中查找用户", "en": "Find a user in QQ contacts or group chats." },
|
||
# "parameters": [
|
||
# { "name": "user_name", "description": { "zh": "搜索用户名称", "en": "User name keyword to search." }, "type": "string", "required": true },
|
||
# { "name": "user_type", "description": { "zh": "搜索类型(contacts/groups)", "en": "Search type: contacts or groups." }, "type": "string", "required": true }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "find_and_reply",
|
||
# "description": { "zh": "查找用户并发送消息", "en": "Find a user and send a message." },
|
||
# "parameters": [
|
||
# { "name": "message", "description": { "zh": "要发送的消息", "en": "Message text to send." }, "type": "string", "required": true },
|
||
# { "name": "user_name", "description": { "zh": "发送目标用户名称", "en": "Target user name keyword." }, "type": "string", "required": true },
|
||
# { "name": "user_type", "description": { "zh": "用户类型(contacts/groups)", "en": "Target type: contacts or groups." }, "type": "string", "required": true },
|
||
# { "name": "click_send", "description": { "zh": "是否点击发送按钮", "en": "Whether to tap the Send button." }, "type": "boolean", "required": false }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "get_history",
|
||
# "description": { "zh": "获取当前聊天窗口的历史消息。如果用户要求读取聊天记录,那么可以用这个工具或者find_and_get_history。", "en": "Get chat history from the current chat window. If the user asks to read chat history, you can use this tool or `find_and_get_history`." },
|
||
# "parameters": [
|
||
# { "name": "message_num", "description": { "zh": "获取的消息数量", "en": "Number of messages to retrieve." }, "type": "number", "required": false }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "find_and_get_history",
|
||
# "description": { "zh": "查找用户并获取聊天历史记录。一般情况下,用户想要让ai帮忙回消息的时候,需要先调用这个,得到历史记录后直接调用reply。", "en": "Find a user and retrieve chat history. Typically, when the user wants the AI to reply, call this first to get context, then call `reply`." },
|
||
# "parameters": [
|
||
# { "name": "user_name", "description": { "zh": "搜索用户名称", "en": "User name keyword to search." }, "type": "string", "required": true },
|
||
# { "name": "user_type", "description": { "zh": "搜索类型(contacts/groups)", "en": "Search type: contacts or groups." }, "type": "string", "required": true },
|
||
# { "name": "message_num", "description": { "zh": "获取的消息数量", "en": "Number of messages to retrieve." }, "type": "number", "required": false }
|
||
# ]
|
||
# }
|
||
# ]
|
||
# }
|
||
|
||
|
||
def _at(arr, index):
|
||
if index < 0:
|
||
index = len(arr) + index
|
||
if 0 <= index < len(arr):
|
||
return arr[index]
|
||
return None
|
||
|
||
|
||
async def close_keyboard():
|
||
await Tools.UI.pressKey("KEYCODE_BACK")
|
||
|
||
|
||
async def is_in_group():
|
||
page = await UINode.getCurrentPage()
|
||
elements = page.findAllByContentDesc('语音')
|
||
idx = -1
|
||
for i, e in enumerate(elements):
|
||
className = e.className or ""
|
||
center = e.centerPoint or {}
|
||
if 'ImageButton' in className and (center.get('y') or 0) > 1000:
|
||
idx = i
|
||
break
|
||
return idx == -1
|
||
|
||
|
||
async def get_history(params):
|
||
message_num = int(params.get("message_num")) if params.get("message_num") else 10
|
||
if not message_num:
|
||
message_num = 10
|
||
page = await UINode.getCurrentPage()
|
||
|
||
# 获取群名称
|
||
title_node = page.findById('com.tencent.mobileqq:id/ivTitleBtnLeft')
|
||
chat_title = ""
|
||
if title_node is not None:
|
||
parent = getattr(title_node, "parent", None)
|
||
if parent is not None:
|
||
texts = parent.allTexts()
|
||
chat_title = texts[0] if texts else ""
|
||
console.log("chat_title", chat_title)
|
||
|
||
# 先滑动到最底部
|
||
tryMax = 0
|
||
while tryMax < 1:
|
||
tryMax += 1
|
||
await Tools.UI.swipe(100, 1000, 100, 200)
|
||
await Tools.System.sleep(500)
|
||
|
||
messageList = []
|
||
allMessages = []
|
||
|
||
# 获取历史消息
|
||
tryMax = 0
|
||
while tryMax < 5:
|
||
tryMax += 1
|
||
|
||
page = await UINode.getCurrentPage()
|
||
console.log("page", page.toFormattedString())
|
||
list_view = page.findByClass('RecyclerView')
|
||
if not list_view:
|
||
return None
|
||
|
||
# 清空临时消息列表
|
||
messageList = []
|
||
|
||
# 获取当前可见的消息列表
|
||
for message in list_view.children:
|
||
message_text = '/'.join(message.allTexts())
|
||
|
||
sender = "other"
|
||
# 判断头像在左边还是右边
|
||
avatar = message.findByClass('ImageView')
|
||
if avatar:
|
||
center = avatar.centerPoint or {}
|
||
if (center.get('x') or 0) < 300:
|
||
sender = "other"
|
||
console.log("other", center.get('x'))
|
||
else:
|
||
sender = "self"
|
||
console.log("self", center.get('x'))
|
||
else:
|
||
sender = "self"
|
||
console.log("self")
|
||
messageList.append({"message": message_text, "sender": sender})
|
||
|
||
# 将当前视图中的消息添加到总集合中,保持顺序
|
||
# 从下往上滑动时,将新获取的消息放在前面(保持旧消息在前,新消息在后的顺序)
|
||
allMessages = messageList + allMessages
|
||
|
||
# 如果已经获取了足够多的消息,停止滚动
|
||
if len(allMessages) >= message_num:
|
||
break
|
||
await Tools.UI.swipe(100, 300, 100, 1000)
|
||
await Tools.System.sleep(500)
|
||
|
||
# 删除重复的消息,但保持原始顺序
|
||
uniqueMessages = []
|
||
seen = set()
|
||
|
||
for msg in allMessages:
|
||
key = f"{msg['message']}-{msg['sender']}"
|
||
if key not in seen:
|
||
seen.add(key)
|
||
uniqueMessages.append(msg)
|
||
|
||
# 返回符合数量要求的消息列表(保持原始顺序)
|
||
return {"messages": uniqueMessages[:message_num], "chat_title": chat_title}
|
||
|
||
|
||
async def reply(params):
|
||
# 提取参数
|
||
message = params.get("message") or ""
|
||
click_send = params.get("click_send") or False
|
||
await Tools.UI.setText(message)
|
||
await Tools.System.sleep(500)
|
||
|
||
if click_send:
|
||
await Tools.UI.clickElement({
|
||
"resourceId": "com.tencent.mobileqq:id/send_btn",
|
||
"index": "0"
|
||
})
|
||
return True
|
||
|
||
|
||
async def find_and_reply(params):
|
||
# 提取参数
|
||
message = params.get("message") or ""
|
||
user_name = params.get("user_name") or ""
|
||
user_type = params.get("user_type") or "contacts"
|
||
click_send = params.get("click_send") or False
|
||
result = await find_user({"user_name": user_name, "user_type": user_type})
|
||
if not result:
|
||
return False
|
||
await Tools.System.sleep(1000)
|
||
return await reply({"message": message, "click_send": click_send})
|
||
|
||
|
||
async def ensureActivity(activityName="", packageName="com.tencent.mobileqq", enterActivity=None, tryMax=1):
|
||
if enterActivity is None:
|
||
async def enterActivity():
|
||
return True
|
||
android = Android()
|
||
activity = await Tools.UI.getPageInfo()
|
||
if (activity.get("activityName") or "").find(activityName) != -1 if activityName else False:
|
||
return True
|
||
|
||
while tryMax > 0:
|
||
tryMax -= 1
|
||
await Tools.System.stopApp(packageName)
|
||
await Tools.System.sleep(2000)
|
||
await Tools.System.startApp(packageName)
|
||
await Tools.System.sleep(3000) # Give some time for app to launch
|
||
if await enterActivity():
|
||
activity = await Tools.UI.getPageInfo()
|
||
if (activity.get("activityName") or "").find(activityName) != -1 if activityName else False:
|
||
return True
|
||
return False
|
||
|
||
|
||
async def find_user(params):
|
||
# 提取参数
|
||
user_name = params.get("user_name") or ""
|
||
user_type = params.get("user_type") or "contacts"
|
||
|
||
async def _enter_activity():
|
||
search_btn = (await UINode.getCurrentPage()).findByText("搜索")
|
||
if search_btn:
|
||
await search_btn.click()
|
||
return True
|
||
return False
|
||
|
||
if not await ensureActivity("com.tencent.mobileqq.search.activity.UniteSearchActivity", "com.tencent.mobileqq", _enter_activity):
|
||
return False
|
||
|
||
firstTarget = None
|
||
tryMax = 0
|
||
while tryMax < 2:
|
||
tryMax += 1
|
||
search_btn = (await UINode.getCurrentPage()).findByText("搜索")
|
||
if search_btn:
|
||
await search_btn.click()
|
||
await Tools.System.sleep(500)
|
||
# 输入搜索内容
|
||
await Tools.UI.setText(user_name)
|
||
await Tools.System.sleep(3000 * tryMax)
|
||
await close_keyboard()
|
||
|
||
currentPage = await UINode.getCurrentPage()
|
||
searchResult = currentPage.findAllById('com.tencent.mobileqq:id/title')
|
||
|
||
isNeedToCatch = False
|
||
for child in (searchResult or []):
|
||
if isNeedToCatch:
|
||
firstTarget = child
|
||
break
|
||
title = child
|
||
if title:
|
||
if user_type == "contacts" and title.text == "联系人":
|
||
isNeedToCatch = True
|
||
elif user_type == "groups" and title.text == "群聊":
|
||
isNeedToCatch = True
|
||
|
||
if firstTarget:
|
||
await firstTarget.click()
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
async def find_and_get_history(params):
|
||
user_name = params.get("user_name") or ""
|
||
user_type = params.get("user_type") or "contacts"
|
||
message_num = params.get("message_num") or 10
|
||
result = await find_user({"user_name": user_name, "user_type": user_type})
|
||
if not result:
|
||
return None
|
||
await Tools.System.sleep(1000)
|
||
return await get_history({"message_num": message_num})
|
||
|
||
|
||
async def main():
|
||
result = await find_and_get_history({"user_name": "Dec", "user_type": "groups", "message_num": 20})
|
||
console.log(result)
|
||
complete({
|
||
"success": result
|
||
})
|
||
|
||
|
||
async def wrap_bool(func, params, successMessage, failMessage, additionalMessage=""):
|
||
if await func(params):
|
||
complete({
|
||
"success": True,
|
||
"message": successMessage,
|
||
"additionalMessage": additionalMessage
|
||
})
|
||
else:
|
||
complete({
|
||
"success": False,
|
||
"message": failMessage,
|
||
"additionalMessage": additionalMessage
|
||
})
|
||
|
||
|
||
async def wrap_data(func, params, successMessage, failMessage, additionalMessage=""):
|
||
result = await func(params)
|
||
complete({
|
||
"success": True,
|
||
"message": successMessage,
|
||
"additionalMessage": additionalMessage,
|
||
"data": result
|
||
})
|
||
|
||
|
||
async def _exported_reply(params):
|
||
await wrap_bool(reply, params, "发送成功", "发送失败")
|
||
|
||
|
||
async def _exported_find_user(params):
|
||
additionalMessage = (await UINode.getCurrentPage()).toFormattedString()
|
||
await wrap_bool(find_user, params, "查找成功", "查找失败,停留在界面", additionalMessage)
|
||
|
||
|
||
async def _exported_find_and_reply(params):
|
||
additionalMessage = (await UINode.getCurrentPage()).toFormattedString()
|
||
await wrap_bool(find_and_reply, params, "发送成功", "发送失败,停留在界面", additionalMessage)
|
||
|
||
|
||
async def _exported_get_history(params):
|
||
await wrap_data(get_history, params, "获取历史消息成功", "获取历史消息失败")
|
||
|
||
|
||
async def _exported_find_and_get_history(params):
|
||
await wrap_data(find_and_get_history, params, "获取历史消息成功", "获取历史消息失败")
|
||
|
||
|
||
exports.reply = _exported_reply
|
||
exports.find_user = _exported_find_user
|
||
exports.find_and_reply = _exported_find_and_reply
|
||
exports.get_history = _exported_get_history
|
||
exports.find_and_get_history = _exported_find_and_get_history
|
||
exports.main = main
|