Files
xiaoli-cli/operit_py/hex_editor.py
T
shuiyu1123 dc77dfd304 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 专属功能已降级处理(保留接口,返回不支持提示)
2026-06-23 12:27:18 +00:00

357 lines
14 KiB
Python

# METADATA
# {
# "name": "hex_editor",
# "display_name": {
# "zh": "十六进制编辑器",
# "en": "Hex Editor"
# },
# "description": {
# "zh": "一个面向二进制文件的轻量十六进制查看与修改工具包,支持查看指定偏移的 hex 窗口、按偏移写入字节序列,以及搜索指定 hex 模式。",
# "en": "A lightweight binary hex inspection and patching toolkit. It can read hex windows at offsets, write byte sequences at offsets, and search for hex patterns."
# },
# "enabledByDefault": false,
# "category": "File",
# "tools": [
# {
# "name": "read_hex_window",
# "description": {
# "zh": "读取二进制文件在指定偏移附近的十六进制窗口,并附带 ASCII 预览,便于快速定位和人工核对。",
# "en": "Read a hex window from a binary file at a specific offset, including an ASCII preview for quick inspection."
# },
# "parameters": [
# { "name": "file_path", "description": { "zh": "目标文件路径。", "en": "Target file path." }, "type": "string", "required": true },
# { "name": "offset", "description": { "zh": "起始偏移,可写十进制或 0x 开头的十六进制。", "en": "Start offset in decimal or 0x-prefixed hexadecimal." }, "type": "string", "required": true },
# { "name": "length", "description": { "zh": "读取字节数,默认 128,最大 4096。", "en": "Number of bytes to read. Default 128, max 4096." }, "type": "number", "required": false },
# { "name": "environment", "description": { "zh": "文件环境:android 或 linux,默认 android。", "en": "File environment: android or linux. Defaults to android." }, "type": "string", "required": false }
# ]
# },
# {
# "name": "write_hex_bytes",
# "description": {
# "zh": "把给定的 hex 字节序列直接写入文件指定偏移。该工具会原地修改文件,不会自动生成回退文件。",
# "en": "Write a hex byte sequence directly into a file at the specified offset. This patches the file in place and does not create rollback files."
# },
# "parameters": [
# { "name": "file_path", "description": { "zh": "目标文件路径。", "en": "Target file path." }, "type": "string", "required": true },
# { "name": "offset", "description": { "zh": "写入起始偏移,可写十进制或 0x 开头的十六进制。", "en": "Write offset in decimal or 0x-prefixed hexadecimal." }, "type": "string", "required": true },
# { "name": "hex_bytes", "description": { "zh": "要写入的 hex 字节序列,例如 'DE AD BE EF' 或 'deadbeef'。", "en": "Hex bytes to write, for example 'DE AD BE EF' or 'deadbeef'." }, "type": "string", "required": true },
# { "name": "environment", "description": { "zh": "文件环境:android 或 linux,默认 android。", "en": "File environment: android or linux. Defaults to android." }, "type": "string", "required": false }
# ]
# },
# {
# "name": "find_hex_pattern",
# "description": {
# "zh": "在二进制文件中搜索指定的 hex 模式,返回命中偏移以及每个命中点附近的十六进制窗口。",
# "en": "Search a binary file for a hex pattern and return matched offsets with nearby hex windows."
# },
# "parameters": [
# { "name": "file_path", "description": { "zh": "目标文件路径。", "en": "Target file path." }, "type": "string", "required": true },
# { "name": "hex_bytes", "description": { "zh": "要搜索的 hex 模式,例如 '50 4B 03 04'。", "en": "Hex pattern to search for, for example '50 4B 03 04'." }, "type": "string", "required": true },
# { "name": "max_results", "description": { "zh": "最多返回多少个命中,默认 20,最大 100。", "en": "Maximum number of matches to return. Default 20, max 100." }, "type": "number", "required": false },
# { "name": "environment", "description": { "zh": "文件环境:android 或 linux,默认 android。", "en": "File environment: android or linux. Defaults to android." }, "type": "string", "required": false }
# ]
# }
# ]
# }
import base64
import math
import re
import traceback
DEFAULT_WINDOW_LENGTH = 128
MAX_WINDOW_LENGTH = 4096
DEFAULT_MAX_RESULTS = 20
MAX_RESULTS_LIMIT = 100
CONTEXT_BEFORE = 16
CONTEXT_TOTAL = 64
BYTES_PER_LINE = 16
def _is_finite(n):
return isinstance(n, (int, float)) and not isinstance(n, bool) and math.isfinite(n)
def normalizeEnvironment(environment=None):
value = str(environment or "android").strip().lower()
if value != "android" and value != "linux":
raise Exception("environment 只能是 android 或 linux。")
return value
def normalizeBase64Input(value):
cleaned = re.sub(r"\s+", "", str(value or ""))
cleaned = cleaned.replace("-", "+").replace("_", "/")
if not cleaned:
return ""
remainder = len(cleaned) % 4
if remainder == 1:
raise Exception("Invalid base64 string")
if remainder == 0:
return cleaned
return cleaned + "=" * (4 - remainder)
def decodeBase64ToBytes(value):
normalized = normalizeBase64Input(value)
if not normalized:
return []
decoded = base64.b64decode(normalized)
return list(decoded)
def encodeBytesToBase64(byte_list):
raw = bytes((b & 0xFF) for b in byte_list)
return base64.b64encode(raw).decode("ascii")
def parseOffset(value):
trimmed = str(value or "").strip().lower()
if not trimmed:
raise Exception("offset 不能为空。")
if re.match(r"^0x[0-9a-f]+$", trimmed):
parsed = int(trimmed[2:], 16)
elif re.match(r"^\d+$", trimmed):
parsed = int(trimmed, 10)
else:
raise Exception("offset 必须是十进制数字或 0x 开头的十六进制。")
if not _is_finite(parsed) or parsed < 0:
raise Exception("offset 必须是大于等于 0 的整数。")
return parsed
def clampPositiveInteger(value, defaultValue, maxValue, fieldName):
actual = defaultValue if value is None else int(value)
if not _is_finite(actual) or actual <= 0:
raise Exception(f"{fieldName} 必须是大于 0 的整数。")
return min(actual, maxValue)
def normalizeHexBytesInput(value):
trimmed = str(value or "").strip()
if not trimmed:
raise Exception("hex_bytes 不能为空。")
withoutPrefix = re.sub(r"^0x", "", trimmed, flags=re.IGNORECASE)
normalized = re.sub(r"[^0-9a-fA-F]", "", withoutPrefix).lower()
if not normalized:
raise Exception("hex_bytes 中没有可用的十六进制字符。")
if len(normalized) % 2 != 0:
raise Exception("hex_bytes 必须包含偶数个十六进制字符。")
return normalized
def parseHexBytes(value):
normalized = normalizeHexBytesInput(value)
bytes_list = []
for i in range(0, len(normalized), 2):
bytes_list.append(int(normalized[i:i + 2], 16))
return bytes_list
def toHex(value, width):
hex_str = format(max(0, int(value or 0)), "x").upper()
while len(hex_str) < width:
hex_str = "0" + hex_str
return hex_str
def bytesToHexString(bytes_list):
return " ".join(toHex(b, 2) for b in bytes_list)
def bytesToAscii(bytes_list):
return "".join(chr(b) if 32 <= b <= 126 else "." for b in bytes_list)
def buildHexDump(bytes_list, startOffset):
if len(bytes_list) == 0:
return ""
lines = []
offsetWidth = max(8, len(format(startOffset + len(bytes_list), "x")))
for index in range(0, len(bytes_list), BYTES_PER_LINE):
slice_ = bytes_list[index:index + BYTES_PER_LINE]
hexCells = [toHex(b, 2) for b in slice_]
while len(hexCells) < BYTES_PER_LINE:
hexCells.append(" ")
left = " ".join(hexCells[:8])
right = " ".join(hexCells[8:])
ascii_str = bytesToAscii(slice_).ljust(BYTES_PER_LINE, " ")
lines.append(f"{toHex(startOffset + index, offsetWidth)} {left} {right} |{ascii_str}|")
return "\n".join(lines)
async def ensureFileExists(path, environment):
exists = await Tools.Files.exists(path, environment)
if not exists.get("exists"):
raise Exception(f"文件不存在: {path}")
if exists.get("isDirectory"):
raise Exception(f"目标路径是目录,不是文件: {path}")
async def readFileBytes(path, environment):
await ensureFileExists(path, environment)
result = await Tools.Files.readBinary(path, environment)
base64_str = str(result.get("contentBase64") or "")
return {
"base64": base64_str,
"bytes": decodeBase64ToBytes(base64_str),
"size": int(result.get("size") or 0)
}
async def writeFileBytes(path, bytes_list, environment):
return await Tools.Files.writeBinary(path, encodeBytesToBase64(bytes_list), environment)
def collectWindow(bytes_list, offset, length):
if offset > len(bytes_list):
raise Exception(f"offset 超出文件范围。文件大小为 {len(bytes_list)} 字节。")
end = min(len(bytes_list), offset + length)
window = bytes_list[offset:end]
return {
"window": window,
"actualLength": len(window)
}
async def read_hex_window(params):
environment = normalizeEnvironment(params.get("environment"))
offset = parseOffset(params.get("offset"))
length = clampPositiveInteger(params.get("length"), DEFAULT_WINDOW_LENGTH, MAX_WINDOW_LENGTH, "length")
file = await readFileBytes(params["file_path"], environment)
collected = collectWindow(file["bytes"], offset, length)
window = collected["window"]
actualLength = collected["actualLength"]
return {
"file_path": params["file_path"],
"environment": environment,
"file_size": file["size"],
"offset_decimal": offset,
"offset_hex": f"0x{toHex(offset, 8)}",
"requested_length": length,
"actual_length": actualLength,
"end_offset_exclusive_decimal": offset + actualLength,
"end_offset_exclusive_hex": f"0x{toHex(offset + actualLength, 8)}",
"hex": bytesToHexString(window),
"ascii_preview": bytesToAscii(window),
"hex_dump": buildHexDump(window, offset)
}
async def write_hex_bytes(params):
environment = normalizeEnvironment(params.get("environment"))
offset = parseOffset(params.get("offset"))
patchBytes = parseHexBytes(params.get("hex_bytes"))
file = await readFileBytes(params["file_path"], environment)
if offset + len(patchBytes) > len(file["bytes"]):
raise Exception(f"写入范围超出文件大小。文件大小 {len(file['bytes'])} 字节,尝试写入到 {offset + len(patchBytes)}。")
beforeBytes = file["bytes"][offset:offset + len(patchBytes)]
for i in range(len(patchBytes)):
file["bytes"][offset + i] = patchBytes[i]
writeResult = await writeFileBytes(params["file_path"], file["bytes"], environment)
afterBytes = file["bytes"][offset:offset + len(patchBytes)]
return {
"file_path": params["file_path"],
"environment": environment,
"offset_decimal": offset,
"offset_hex": f"0x{toHex(offset, 8)}",
"bytes_written": len(patchBytes),
"before_hex": bytesToHexString(beforeBytes),
"after_hex": bytesToHexString(afterBytes),
"write_result": writeResult
}
async def find_hex_pattern(params):
environment = normalizeEnvironment(params.get("environment"))
patternBytes = parseHexBytes(params.get("hex_bytes"))
maxResults = clampPositiveInteger(params.get("max_results"), DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT, "max_results")
file = await readFileBytes(params["file_path"], environment)
matches = []
if len(patternBytes) > len(file["bytes"]):
return {
"file_path": params["file_path"],
"environment": environment,
"file_size": file["size"],
"pattern_hex": bytesToHexString(patternBytes),
"match_count": 0,
"matches": matches
}
for offset in range(0, len(file["bytes"]) - len(patternBytes) + 1):
matched = True
for index in range(len(patternBytes)):
if file["bytes"][offset + index] != patternBytes[index]:
matched = False
break
if not matched:
continue
contextStart = max(0, offset - CONTEXT_BEFORE)
contextEnd = min(len(file["bytes"]), contextStart + CONTEXT_TOTAL)
contextBytes = file["bytes"][contextStart:contextEnd]
matches.append({
"offset_decimal": offset,
"offset_hex": f"0x{toHex(offset, 8)}",
"matched_hex": bytesToHexString(patternBytes),
"context_hex_dump": buildHexDump(contextBytes, contextStart)
})
if len(matches) >= maxResults:
break
return {
"file_path": params["file_path"],
"environment": environment,
"file_size": file["size"],
"pattern_hex": bytesToHexString(patternBytes),
"match_count": len(matches),
"matches": matches
}
async def wrap(fn, params, successMessage, failureMessage):
try:
data = await fn(params)
complete({
"success": True,
"message": successMessage,
"data": data
})
except Exception as error:
console.error(f"[hex_editor] {fn.__name__} failed:", error)
complete({
"success": False,
"message": f"{failureMessage}: {str(error)}",
"error_stack": traceback.format_exc()
})
async def _exported_read_hex_window(params):
await wrap(read_hex_window, params, "Hex 窗口读取成功。", "Hex 窗口读取失败")
async def _exported_write_hex_bytes(params):
await wrap(write_hex_bytes, params, "Hex 字节写入成功。", "Hex 字节写入失败")
async def _exported_find_hex_pattern(params):
await wrap(find_hex_pattern, params, "Hex 模式搜索成功。", "Hex 模式搜索失败")
exports.read_hex_window = _exported_read_hex_window
exports.write_hex_bytes = _exported_write_hex_bytes
exports.find_hex_pattern = _exported_find_hex_pattern