新增插件: - terminal: 持久交互式终端,纯标准库,全平台,13个测试 - operit_bridge: Operit AI工具格式桥接,47个脚本288个工具,12个测试 其他: - plugins/operit/: 47个Operit官方脚本 - 更新plugin_registry.json/plugins_config.py - 更新about.txt/CHANGELOG.md至v5.4.2
424 lines
20 KiB
TypeScript
424 lines
20 KiB
TypeScript
/*
|
||
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
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}*/
|
||
|
||
const superAdmin = (function () {
|
||
const MAX_INLINE_TERMINAL_OUTPUT_CHARS = 12_000;
|
||
const DEFAULT_FOREGROUND_TIMEOUT_MS = 15_000;
|
||
const DEFAULT_WAIT_TIMEOUT_MS = 300_000;
|
||
const MIN_TIMEOUT_MS = 3_000;
|
||
const DEFAULT_TERMINAL_SESSION_NAME = "super_admin_default_session";
|
||
const BACKGROUND_TERMINAL_SESSION_PREFIX = "super_admin_background";
|
||
|
||
function getCurrentChatSessionSuffix(): string {
|
||
const chatId = getChatId();
|
||
if (chatId === undefined) {
|
||
return "";
|
||
}
|
||
const normalizedChatId = chatId.trim();
|
||
if (!normalizedChatId) {
|
||
return "";
|
||
}
|
||
return normalizedChatId.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
||
}
|
||
|
||
function getDefaultTerminalSessionName(): string {
|
||
const chatSuffix = getCurrentChatSessionSuffix();
|
||
return chatSuffix
|
||
? `${DEFAULT_TERMINAL_SESSION_NAME}_${chatSuffix}`
|
||
: DEFAULT_TERMINAL_SESSION_NAME;
|
||
}
|
||
|
||
function getBackgroundTerminalSessionName(): string {
|
||
const chatSuffix = getCurrentChatSessionSuffix();
|
||
const prefix = chatSuffix
|
||
? `${BACKGROUND_TERMINAL_SESSION_PREFIX}_${chatSuffix}`
|
||
: BACKGROUND_TERMINAL_SESSION_PREFIX;
|
||
return `${prefix}_${Date.now()}`;
|
||
}
|
||
|
||
async function persistTerminalOutputIfTooLong(command: string, result: any): Promise<any | null> {
|
||
const outputStr = typeof result?.output === "string"
|
||
? result.output
|
||
: String(result?.output ?? "");
|
||
|
||
if (outputStr.length <= MAX_INLINE_TERMINAL_OUTPUT_CHARS) {
|
||
return null;
|
||
}
|
||
|
||
await Tools.Files.mkdir(OPERIT_CLEAN_ON_EXIT_DIR, true);
|
||
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||
const rand = Math.floor(Math.random() * 1_000_000);
|
||
const filePath = `${OPERIT_CLEAN_ON_EXIT_DIR}/terminal_output_${timestamp}_${rand}.log`;
|
||
|
||
await Tools.Files.write(filePath, outputStr, false);
|
||
|
||
return {
|
||
command,
|
||
output: "(saved_to_file)",
|
||
exitCode: result?.exitCode,
|
||
sessionId: result?.sessionId,
|
||
context_preserved: true,
|
||
output_saved_to: filePath,
|
||
output_chars: outputStr.length,
|
||
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.",
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 在Ubuntu环境中执行终端命令并收集输出结果
|
||
* 运行环境:完整的Ubuntu系统,已正确挂载sdcard和storage目录
|
||
* 禁止使用 set -e / set -o errexit 等会改变 shell 退出行为的命令,否则可能导致终端会话退出并卡死
|
||
* @param command - 要执行的命令
|
||
* @param background - 是否后台运行("true" 为后台执行并立即返回,适合启动服务器等长时间运行任务,AI 不会收到该命令的输出结果)
|
||
* @param timeoutMs - 可选的超时时间(毫秒,最低 3000ms)。强烈建议显式传入;前台未传时默认 15000ms,后台模式不应用该默认值。
|
||
*/
|
||
async function terminal(params: { command: string, background?: string, timeoutMs?: string }): Promise<any> {
|
||
try {
|
||
if (!params.command) {
|
||
throw new Error("命令不能为空");
|
||
}
|
||
|
||
const command = params.command;
|
||
const background = params.background;
|
||
const timeoutMs = params.timeoutMs;
|
||
|
||
console.log(`执行终端命令: ${command}`);
|
||
|
||
const isBackground = background === "true";
|
||
let timeout: number | undefined;
|
||
if (!isBackground) {
|
||
if (timeoutMs !== undefined) {
|
||
const parsedTimeout = parseInt(timeoutMs, 10);
|
||
if (!Number.isFinite(parsedTimeout) || parsedTimeout < MIN_TIMEOUT_MS) {
|
||
throw new Error(`timeoutMs必须是整数且不少于${MIN_TIMEOUT_MS}毫秒`);
|
||
}
|
||
timeout = parsedTimeout;
|
||
} else {
|
||
timeout = DEFAULT_FOREGROUND_TIMEOUT_MS;
|
||
}
|
||
}
|
||
|
||
if (isBackground) {
|
||
const session = await Tools.System.terminal.create(getBackgroundTerminalSessionName());
|
||
const sessionId = session.sessionId;
|
||
|
||
// 调用系统工具执行终端命令
|
||
(async () => {
|
||
try {
|
||
await Tools.System.terminal.exec(sessionId, command);
|
||
} catch (error) {
|
||
console.error(`[terminal/background] 错误: ${error.message}`);
|
||
console.error(error.stack);
|
||
}
|
||
})();
|
||
|
||
return {
|
||
command: command,
|
||
background: true,
|
||
sessionId: sessionId,
|
||
started: true
|
||
};
|
||
}
|
||
|
||
// 创建或获取一个默认会话
|
||
const session = await Tools.System.terminal.create(getDefaultTerminalSessionName());
|
||
const sessionId = session.sessionId;
|
||
|
||
// 调用系统工具执行终端命令
|
||
const result = await Tools.System.terminal.exec(sessionId, command, timeout);
|
||
const timedOut = result.timedOut === true;
|
||
|
||
let timeoutScreen: { sessionId: string, rows: number, cols: number, content: string } | null = null;
|
||
if (timedOut) {
|
||
const screenResult = await Tools.System.terminal.screen(sessionId);
|
||
timeoutScreen = {
|
||
sessionId: screenResult.sessionId ?? sessionId,
|
||
rows: screenResult.rows,
|
||
cols: screenResult.cols,
|
||
content: screenResult.content
|
||
};
|
||
}
|
||
|
||
const persistedResult = await persistTerminalOutputIfTooLong(command, result);
|
||
if (persistedResult) {
|
||
persistedResult.timeoutMsUsed = timeout;
|
||
if (timeoutScreen) {
|
||
persistedResult.timeoutScreen = timeoutScreen;
|
||
}
|
||
return persistedResult;
|
||
}
|
||
|
||
return {
|
||
command: command,
|
||
output: result.output,
|
||
exitCode: result.exitCode,
|
||
sessionId: result.sessionId,
|
||
timedOut: timedOut,
|
||
timeoutMsUsed: timeout,
|
||
timeoutScreen: timeoutScreen,
|
||
context_preserved: true // 标记此命令保留了目录上下文
|
||
};
|
||
} catch (error) {
|
||
console.error(`[terminal] 错误: ${error.message}`);
|
||
console.error(error.stack);
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 等待同一终端会话中的上一条命令执行完成
|
||
* 原理:向同会话追加一个内部 marker 命令。由于会话按序执行,marker 开始执行即代表前序命令已完成。
|
||
* @param sessionId - 可选会话ID;不传时使用当前对话的默认会话,无 chatId 时为 super_admin_default_session
|
||
* @param timeoutMs - 可选超时(毫秒,最低 3000ms);未传默认 300000ms
|
||
*/
|
||
async function terminal_wait(params: { sessionId?: string, timeoutMs?: string } = {}): Promise<any> {
|
||
try {
|
||
const timeoutMs = params.timeoutMs;
|
||
let timeout = DEFAULT_WAIT_TIMEOUT_MS;
|
||
if (timeoutMs !== undefined) {
|
||
const parsedTimeout = parseInt(timeoutMs, 10);
|
||
if (!Number.isFinite(parsedTimeout) || parsedTimeout < MIN_TIMEOUT_MS) {
|
||
throw new Error(`timeoutMs必须是整数且不少于${MIN_TIMEOUT_MS}毫秒`);
|
||
}
|
||
timeout = parsedTimeout;
|
||
}
|
||
|
||
const session =
|
||
params.sessionId
|
||
? { sessionId: params.sessionId }
|
||
: await Tools.System.terminal.create(getDefaultTerminalSessionName());
|
||
const sessionId = session.sessionId;
|
||
|
||
const marker = `__OPERIT_TERMINAL_WAIT_DONE_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}__`;
|
||
const waitCommand = `printf '${marker}\\n'`;
|
||
const startedAt = Date.now();
|
||
const result = await Tools.System.terminal.exec(sessionId, waitCommand, timeout);
|
||
const elapsedMs = Date.now() - startedAt;
|
||
const timedOut = result?.timedOut === true;
|
||
|
||
let timeoutScreen: { sessionId: string, rows: number, cols: number, content: string } | null = null;
|
||
if (timedOut) {
|
||
const screenResult = await Tools.System.terminal.screen(sessionId);
|
||
timeoutScreen = {
|
||
sessionId: screenResult.sessionId ?? sessionId,
|
||
rows: screenResult.rows,
|
||
cols: screenResult.cols,
|
||
content: screenResult.content
|
||
};
|
||
}
|
||
|
||
const outputStr = typeof result?.output === "string"
|
||
? result.output
|
||
: String(result?.output ?? "");
|
||
const markerSeen = outputStr.includes(marker);
|
||
|
||
return {
|
||
sessionId,
|
||
timedOut,
|
||
timeoutMsUsed: timeout,
|
||
elapsedMs,
|
||
waitCompleted: !timedOut && markerSeen,
|
||
markerSeen,
|
||
exitCode: result?.exitCode,
|
||
timeoutScreen,
|
||
context_preserved: true
|
||
};
|
||
} catch (error) {
|
||
console.error(`[terminal_wait] 错误: ${error.message}`);
|
||
console.error(error.stack);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过Shizuku/Root权限在Android系统中执行Shell命令
|
||
* 运行环境:直接访问Android系统,具有系统级权限
|
||
* @param command - 要执行的Shell命令
|
||
*/
|
||
async function shell(params: { command: string }): Promise<any> {
|
||
try {
|
||
if (!params.command) {
|
||
throw new Error("命令不能为空");
|
||
}
|
||
const command = params.command;
|
||
|
||
console.log(`执行Shell命令: ${command}`);
|
||
|
||
// 通过Shizuku/Root权限执行shell操作
|
||
const result = await Tools.System.shell(`${command}`);
|
||
|
||
return {
|
||
command: command,
|
||
output: result.output,
|
||
exitCode: result.exitCode
|
||
};
|
||
} catch (error) {
|
||
console.error(`[shell] 错误: ${error.message}`);
|
||
console.error(error.stack);
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前终端会话可见屏幕内容(仅一屏,不包含历史)
|
||
*/
|
||
async function terminal_getscreen(_params: {} = {}): Promise<any> {
|
||
try {
|
||
const session = await Tools.System.terminal.create(getDefaultTerminalSessionName());
|
||
const sessionId = session.sessionId;
|
||
const result = await Tools.System.terminal.screen(sessionId);
|
||
return {
|
||
sessionId: result.sessionId ?? sessionId,
|
||
rows: result.rows,
|
||
cols: result.cols,
|
||
content: result.content,
|
||
commandRunning: result.commandRunning === true
|
||
};
|
||
} catch (error) {
|
||
console.error(`[terminal_getscreen] 错误: ${error.message}`);
|
||
console.error(error.stack);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 向当前终端会话写入输入
|
||
* @param input - 文本输入
|
||
* @param control - 控制键
|
||
*/
|
||
async function terminal_input(params: { input?: string, control?: string } = {}): Promise<any> {
|
||
try {
|
||
if (params.input === undefined && params.control === undefined) {
|
||
throw new Error("input和control至少需要提供一个");
|
||
}
|
||
|
||
const session = await Tools.System.terminal.create(getDefaultTerminalSessionName());
|
||
const sessionId = session.sessionId;
|
||
const result = await Tools.System.terminal.input(sessionId, {
|
||
input: params.input,
|
||
control: params.control
|
||
});
|
||
|
||
return {
|
||
sessionId: sessionId,
|
||
input: params.input,
|
||
control: params.control,
|
||
result: result?.value ?? String(result ?? "")
|
||
};
|
||
} catch (error) {
|
||
console.error(`[terminal_input] 错误: ${error.message}`);
|
||
console.error(error.stack);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
return {
|
||
terminal,
|
||
terminal_wait,
|
||
terminal_getscreen,
|
||
terminal_input,
|
||
shell
|
||
};
|
||
})();
|
||
|
||
// 逐个导出
|
||
exports.terminal = superAdmin.terminal;
|
||
exports.terminal_wait = superAdmin.terminal_wait;
|
||
exports.terminal_getscreen = superAdmin.terminal_getscreen;
|
||
exports.terminal_input = superAdmin.terminal_input;
|
||
exports.shell = superAdmin.shell;
|