- 新增 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
17 KiB
Python
338 lines
17 KiB
Python
# METADATA
|
||
# {
|
||
# "name": "super_admin",
|
||
# "display_name": {
|
||
# "zh": "超级管理员",
|
||
# "en": "Super Admin"
|
||
# },
|
||
# "description": { "zh": "超级管理员工具集,提供终端命令和Shell操作的高级功能。terminal工具运行在Ubuntu环境中(已正确挂载sdcard和storage),shell工具通过Shizuku/Root直接执行Android系统命令。适合需要进行底层系统管理和命令行操作的场景。", "en": "Super admin toolkit providing advanced terminal and shell capabilities. The terminal tool runs in an Ubuntu environment (with sdcard/storage mounted). The shell tool executes Android system commands directly via Shizuku/Root. Useful for low-level system administration and CLI operations." },
|
||
# "enabledByDefault": true,
|
||
# "category": "System",
|
||
# "tools": [
|
||
# {
|
||
# "name": "terminal",
|
||
# "description": { "zh": "在Ubuntu环境中执行命令并收集输出结果。运行环境:完整的Ubuntu系统,已正确挂载sdcard和storage目录,可访问Android存储空间。所有命令将会在相同的会话执行且上下文连贯。强烈建议每次都显式传 timeoutMs,避免命令卡住。禁止使用 `set -e`、`set -o errexit` 等会改变 shell 退出行为的命令,这会导致终端会话直接退出并卡死。若未传,前台默认15秒超时;background=true 时不使用该默认超时。命令超时时不会被自动取消,不需要重新执行命令,请继续通过 terminal_getscreen 跟踪当前屏幕内容。", "en": "Execute commands in an Ubuntu environment and collect output. Environment: full Ubuntu system with sdcard/storage mounted, allowing access to Android storage. Automatically preserves working-directory context. Strongly recommend explicitly passing timeoutMs every time to avoid hangs. Do not use commands such as `set -e` or `set -o errexit` that change shell exit behavior, because they can cause the terminal session to exit and hang. If omitted, foreground mode defaults to 15s timeout; background=true does not use this default timeout. When a command times out, it is not automatically cancelled. Do not rerun the command; continue tracking the current screen via terminal_getscreen." },
|
||
# "parameters": [
|
||
# { "name": "command", "description": { "zh": "要执行的命令", "en": "Command to execute." }, "type": "string", "required": true },
|
||
# { "name": "background", "description": { "zh": "是否在后台运行命令,\"true\" 表示后台执行并立即返回,适合启动服务器等长时间运行的任务(AI 不会收到该命令的输出结果),\"false\" 或未提供则前台执行并等待并返回命令结果", "en": "Run command in background. 'true' runs in background and returns immediately (good for long-running tasks like servers; AI will not receive output). 'false' or omitted runs in foreground and returns the command result." }, "type": "string", "required": false },
|
||
# { "name": "timeoutMs", "description": { "zh": "可选超时(毫秒,最低3000ms)。强烈建议显式传入;未传时前台默认15000ms,background=true时不使用默认超时。", "en": "Optional timeout (ms, minimum 3000ms). Strongly recommended to pass explicitly; if omitted, foreground defaults to 15000ms, and background=true does not use the default timeout." }, "type": "string", "required": false }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "terminal_wait",
|
||
# "description": { "zh": "等待同一终端会话中的上一条命令执行完成。适用于安装/编译等长命令在 timeout 后继续后台执行的场景。与 sleep 不同,本工具会在命令实际完成时提前返回,而不是固定睡眠。", "en": "Wait until the previous command in the same terminal session finishes. Useful when long install/build commands continue running after a timeout. Unlike sleep, this tool can return early as soon as the command actually completes." },
|
||
# "parameters": [
|
||
# { "name": "sessionId", "description": { "zh": "可选目标会话ID。不传则使用当前对话的默认会话;无 chatId 时为 super_admin_default_session。", "en": "Optional target session ID. If omitted, uses the current chat's default session; without chatId it is super_admin_default_session." }, "type": "string", "required": false },
|
||
# { "name": "timeoutMs", "description": { "zh": "可选超时(毫秒,最低3000ms)。未传时默认300000ms(5分钟)。", "en": "Optional timeout (ms, minimum 3000ms). Defaults to 300000ms (5 minutes) if omitted." }, "type": "string", "required": false }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "terminal_getscreen",
|
||
# "description": { "zh": "获取当前终端会话可见屏幕内容(仅一屏,不包含历史滚动缓冲)。", "en": "Get the current visible screen content for the active terminal session (single screen only, no scrollback history)." },
|
||
# "parameters": []
|
||
# },
|
||
# {
|
||
# "name": "terminal_input",
|
||
# "description": { "zh": "向当前终端会话写入输入。input 与 control 至少传一个。常见用法:先写 input,再写 control=enter 提交;control=ctrl 且 input=c 可发送 Ctrl+C。", "en": "Write input to the active terminal session. Provide at least one of input or control. Typical usage: send input first, then control=enter to submit; use control=ctrl with input=c for Ctrl+C." },
|
||
# "parameters": [
|
||
# { "name": "input", "description": { "zh": "写入终端的文本", "en": "Text to write to terminal." }, "type": "string", "required": false },
|
||
# { "name": "control", "description": { "zh": "控制键,例如 enter / tab / esc / ctrl", "en": "Control key, e.g. enter / tab / esc / ctrl." }, "type": "string", "required": false }
|
||
# ]
|
||
# },
|
||
# {
|
||
# "name": "shell",
|
||
# "description": { "zh": "通过Shizuku/Root权限直接在Android系统中执行Shell命令。运行环境:直接访问Android系统,具有系统级权限,适用于需要操作Android系统底层的场景(如pm、am等系统命令)。", "en": "Execute shell commands directly on Android with Shizuku/Root. Environment: direct Android system access with system-level privileges, suitable for low-level commands such as pm/am." },
|
||
# "parameters": [
|
||
# { "name": "command", "description": { "zh": "要执行的Shell命令", "en": "Shell command to execute." }, "type": "string", "required": true }
|
||
# ]
|
||
# }
|
||
# ]
|
||
# }
|
||
|
||
import asyncio
|
||
import datetime
|
||
import math
|
||
import random
|
||
import re
|
||
import time
|
||
import traceback
|
||
|
||
MAX_INLINE_TERMINAL_OUTPUT_CHARS = 12000
|
||
DEFAULT_FOREGROUND_TIMEOUT_MS = 15000
|
||
DEFAULT_WAIT_TIMEOUT_MS = 300000
|
||
MIN_TIMEOUT_MS = 3000
|
||
DEFAULT_TERMINAL_SESSION_NAME = "super_admin_default_session"
|
||
BACKGROUND_TERMINAL_SESSION_PREFIX = "super_admin_background"
|
||
|
||
|
||
def _is_finite(n):
|
||
return isinstance(n, (int, float)) and not isinstance(n, bool) and math.isfinite(n)
|
||
|
||
|
||
def getCurrentChatSessionSuffix():
|
||
chatId = getChatId()
|
||
if chatId is None:
|
||
return ""
|
||
normalizedChatId = (chatId or "").strip()
|
||
if not normalizedChatId:
|
||
return ""
|
||
return re.sub(r"[^a-zA-Z0-9._-]+", "_", normalizedChatId)
|
||
|
||
|
||
def getDefaultTerminalSessionName():
|
||
chatSuffix = getCurrentChatSessionSuffix()
|
||
return f"{DEFAULT_TERMINAL_SESSION_NAME}_{chatSuffix}" if chatSuffix else DEFAULT_TERMINAL_SESSION_NAME
|
||
|
||
|
||
def getBackgroundTerminalSessionName():
|
||
chatSuffix = getCurrentChatSessionSuffix()
|
||
prefix = f"{BACKGROUND_TERMINAL_SESSION_PREFIX}_{chatSuffix}" if chatSuffix else BACKGROUND_TERMINAL_SESSION_PREFIX
|
||
return f"{prefix}_{int(time.time() * 1000)}"
|
||
|
||
|
||
async def persistTerminalOutputIfTooLong(command, result):
|
||
outputStr = result.get("output") if isinstance(result, dict) else None
|
||
if not isinstance(outputStr, str):
|
||
outputStr = str((result.get("output") if isinstance(result, dict) else "") or "")
|
||
|
||
if len(outputStr) <= MAX_INLINE_TERMINAL_OUTPUT_CHARS:
|
||
return None
|
||
|
||
await Tools.Files.mkdir(OPERIT_CLEAN_ON_EXIT_DIR, True)
|
||
|
||
timestamp = datetime.datetime.utcnow().isoformat().replace(":", "-").replace(".", "-")
|
||
rand = int(random.random() * 1000000)
|
||
filePath = f"{OPERIT_CLEAN_ON_EXIT_DIR}/terminal_output_{timestamp}_{rand}.log"
|
||
|
||
await Tools.Files.write(filePath, outputStr, False)
|
||
|
||
return {
|
||
"command": command,
|
||
"output": "(saved_to_file)",
|
||
"exitCode": result.get("exitCode") if isinstance(result, dict) else None,
|
||
"sessionId": result.get("sessionId") if isinstance(result, dict) else None,
|
||
"context_preserved": True,
|
||
"output_saved_to": filePath,
|
||
"output_chars": len(outputStr),
|
||
"operit_clean_on_exit_dir": OPERIT_CLEAN_ON_EXIT_DIR,
|
||
"hint": "Output is large and saved to file. Use read_file_part or grep_code to inspect it.",
|
||
}
|
||
|
||
|
||
async def terminal(params):
|
||
try:
|
||
if not params.get("command"):
|
||
raise Exception("命令不能为空")
|
||
|
||
command = params["command"]
|
||
background = params.get("background")
|
||
timeoutMs = params.get("timeoutMs")
|
||
|
||
console.log(f"执行终端命令: {command}")
|
||
|
||
isBackground = background == "true"
|
||
timeout = None
|
||
if not isBackground:
|
||
if timeoutMs is not None:
|
||
parsedTimeout = int(timeoutMs)
|
||
if not _is_finite(parsedTimeout) or parsedTimeout < MIN_TIMEOUT_MS:
|
||
raise Exception(f"timeoutMs必须是整数且不少于{MIN_TIMEOUT_MS}毫秒")
|
||
timeout = parsedTimeout
|
||
else:
|
||
timeout = DEFAULT_FOREGROUND_TIMEOUT_MS
|
||
|
||
if isBackground:
|
||
session = await Tools.System.terminal.create(getBackgroundTerminalSessionName())
|
||
sessionId = session["sessionId"]
|
||
|
||
# 调用系统工具执行终端命令(后台运行,不等待)
|
||
async def _bg_exec():
|
||
try:
|
||
await Tools.System.terminal.exec(sessionId, command)
|
||
except Exception as error:
|
||
console.error(f"[terminal/background] 错误: {str(error)}")
|
||
console.error(traceback.format_exc())
|
||
|
||
asyncio.ensure_future(_bg_exec())
|
||
|
||
return {
|
||
"command": command,
|
||
"background": True,
|
||
"sessionId": sessionId,
|
||
"started": True
|
||
}
|
||
|
||
# 创建或获取一个默认会话
|
||
session = await Tools.System.terminal.create(getDefaultTerminalSessionName())
|
||
sessionId = session["sessionId"]
|
||
|
||
# 调用系统工具执行终端命令
|
||
result = await Tools.System.terminal.exec(sessionId, command, timeout)
|
||
timedOut = result.get("timedOut") is True
|
||
|
||
timeoutScreen = None
|
||
if timedOut:
|
||
screenResult = await Tools.System.terminal.screen(sessionId)
|
||
timeoutScreen = {
|
||
"sessionId": screenResult.get("sessionId") or sessionId,
|
||
"rows": screenResult.get("rows"),
|
||
"cols": screenResult.get("cols"),
|
||
"content": screenResult.get("content")
|
||
}
|
||
|
||
persistedResult = await persistTerminalOutputIfTooLong(command, result)
|
||
if persistedResult:
|
||
persistedResult["timeoutMsUsed"] = timeout
|
||
if timeoutScreen:
|
||
persistedResult["timeoutScreen"] = timeoutScreen
|
||
return persistedResult
|
||
|
||
return {
|
||
"command": command,
|
||
"output": result.get("output"),
|
||
"exitCode": result.get("exitCode"),
|
||
"sessionId": result.get("sessionId"),
|
||
"timedOut": timedOut,
|
||
"timeoutMsUsed": timeout,
|
||
"timeoutScreen": timeoutScreen,
|
||
"context_preserved": True # 标记此命令保留了目录上下文
|
||
}
|
||
except Exception as error:
|
||
console.error(f"[terminal] 错误: {str(error)}")
|
||
console.error(traceback.format_exc())
|
||
|
||
raise error
|
||
|
||
|
||
async def terminal_wait(params=None):
|
||
if params is None:
|
||
params = {}
|
||
try:
|
||
timeoutMs = params.get("timeoutMs")
|
||
timeout = DEFAULT_WAIT_TIMEOUT_MS
|
||
if timeoutMs is not None:
|
||
parsedTimeout = int(timeoutMs)
|
||
if not _is_finite(parsedTimeout) or parsedTimeout < MIN_TIMEOUT_MS:
|
||
raise Exception(f"timeoutMs必须是整数且不少于{MIN_TIMEOUT_MS}毫秒")
|
||
timeout = parsedTimeout
|
||
|
||
if params.get("sessionId"):
|
||
session = {"sessionId": params["sessionId"]}
|
||
else:
|
||
session = await Tools.System.terminal.create(getDefaultTerminalSessionName())
|
||
sessionId = session["sessionId"]
|
||
|
||
marker = f"__OPERIT_TERMINAL_WAIT_DONE_{int(time.time() * 1000)}_{int(random.random() * 1000000)}__"
|
||
waitCommand = f"printf '{marker}\\n'"
|
||
startedAt = int(time.time() * 1000)
|
||
result = await Tools.System.terminal.exec(sessionId, waitCommand, timeout)
|
||
elapsedMs = int(time.time() * 1000) - startedAt
|
||
timedOut = (result or {}).get("timedOut") is True
|
||
|
||
timeoutScreen = None
|
||
if timedOut:
|
||
screenResult = await Tools.System.terminal.screen(sessionId)
|
||
timeoutScreen = {
|
||
"sessionId": screenResult.get("sessionId") or sessionId,
|
||
"rows": screenResult.get("rows"),
|
||
"cols": screenResult.get("cols"),
|
||
"content": screenResult.get("content")
|
||
}
|
||
|
||
outputStr = (result or {}).get("output")
|
||
if not isinstance(outputStr, str):
|
||
outputStr = str((result or {}).get("output") or "")
|
||
markerSeen = marker in outputStr
|
||
|
||
return {
|
||
"sessionId": sessionId,
|
||
"timedOut": timedOut,
|
||
"timeoutMsUsed": timeout,
|
||
"elapsedMs": elapsedMs,
|
||
"waitCompleted": (not timedOut) and markerSeen,
|
||
"markerSeen": markerSeen,
|
||
"exitCode": (result or {}).get("exitCode"),
|
||
"timeoutScreen": timeoutScreen,
|
||
"context_preserved": True
|
||
}
|
||
except Exception as error:
|
||
console.error(f"[terminal_wait] 错误: {str(error)}")
|
||
console.error(traceback.format_exc())
|
||
raise error
|
||
|
||
|
||
async def shell(params):
|
||
try:
|
||
if not params.get("command"):
|
||
raise Exception("命令不能为空")
|
||
command = params["command"]
|
||
|
||
console.log(f"执行Shell命令: {command}")
|
||
|
||
# 通过Shizuku/Root权限执行shell操作
|
||
result = await Tools.System.shell(f"{command}")
|
||
|
||
return {
|
||
"command": command,
|
||
"output": result.get("output") if isinstance(result, dict) else result,
|
||
"exitCode": result.get("exitCode") if isinstance(result, dict) else None
|
||
}
|
||
except Exception as error:
|
||
console.error(f"[shell] 错误: {str(error)}")
|
||
console.error(traceback.format_exc())
|
||
|
||
raise error
|
||
|
||
|
||
async def terminal_getscreen(params=None):
|
||
if params is None:
|
||
params = {}
|
||
try:
|
||
session = await Tools.System.terminal.create(getDefaultTerminalSessionName())
|
||
sessionId = session["sessionId"]
|
||
result = await Tools.System.terminal.screen(sessionId)
|
||
return {
|
||
"sessionId": result.get("sessionId") or sessionId,
|
||
"rows": result.get("rows"),
|
||
"cols": result.get("cols"),
|
||
"content": result.get("content"),
|
||
"commandRunning": result.get("commandRunning") is True
|
||
}
|
||
except Exception as error:
|
||
console.error(f"[terminal_getscreen] 错误: {str(error)}")
|
||
console.error(traceback.format_exc())
|
||
raise error
|
||
|
||
|
||
async def terminal_input(params=None):
|
||
if params is None:
|
||
params = {}
|
||
try:
|
||
if params.get("input") is None and params.get("control") is None:
|
||
raise Exception("input和control至少需要提供一个")
|
||
|
||
session = await Tools.System.terminal.create(getDefaultTerminalSessionName())
|
||
sessionId = session["sessionId"]
|
||
result = await Tools.System.terminal.input(sessionId, {
|
||
"input": params.get("input"),
|
||
"control": params.get("control")
|
||
})
|
||
|
||
return {
|
||
"sessionId": sessionId,
|
||
"input": params.get("input"),
|
||
"control": params.get("control"),
|
||
"result": (result.get("value") if isinstance(result, dict) and result.get("value") is not None else str(result if result is not None else ""))
|
||
}
|
||
except Exception as error:
|
||
console.error(f"[terminal_input] 错误: {str(error)}")
|
||
console.error(traceback.format_exc())
|
||
raise error
|
||
|
||
|
||
exports.terminal = terminal
|
||
exports.terminal_wait = terminal_wait
|
||
exports.terminal_getscreen = terminal_getscreen
|
||
exports.terminal_input = terminal_input
|
||
exports.shell = shell
|