Files
xiaoli-cli-operit-plugins/operit_bridge.py
T

923 lines
35 KiB
Python

"""
Operit Bridge — xiaoli-cli 原生 Liugin 插件
===========================================
让 xiaoli-cli 兼容 Operit AI 的第三方工具格式。
自动扫描 plugins/operit/ 子目录下所有 .js/.ts 脚本,
解析 METADATA 块,注册为 xiaoli-cli 可调用的工具。
"""
import os
import re
import json
import glob
import importlib.util
from typing import Dict, List, Optional
# ═══════════════════════════════════════════════════════════════
# Operit METADATA 解析器
# ═══════════════════════════════════════════════════════════════
def _hjson_to_json(raw: str) -> str:
"""
将 Operit 的 HJSON 风格元数据转为标准 JSON。
逐行解析,处理: 无引号键名、单引号/三引号字符串、嵌套双引号、// 注释、尾逗号。
"""
SQ = chr(39) # '
DQ = chr(34) # "
result = []
i = 0
n = len(raw)
def skip_ws():
nonlocal i
while i < n and raw[i] in ' \t\r\n':
i += 1
def parse_string_val(quote):
"""解析引号字符串,正确处理转义"""
nonlocal i
i += 1 # skip opening quote
chars = []
while i < n:
ch = raw[i]
if ch == '\\':
chars.append(ch)
i += 1
if i < n:
chars.append(raw[i])
i += 1
elif ch == quote:
i += 1 # skip closing quote
return ''.join(chars)
else:
chars.append(ch)
i += 1
return ''.join(chars) # unclosed
def parse_triple_string():
"""解析 '''...''' 三引号字符串"""
nonlocal i
i += 3 # skip '''
chars = []
while i < n - 2:
if raw[i:i+3] == SQ * 3:
i += 3
return ''.join(chars)
chars.append(raw[i])
i += 1
return ''.join(chars)
def parse_unquoted_val():
"""解析无引号值,到行尾或逗号/}等"""
nonlocal i
chars = []
depth = 0
while i < n:
ch = raw[i]
if ch == '{' or ch == '[':
depth += 1
chars.append(ch)
i += 1
elif ch == '}' or ch == ']':
if depth <= 0:
break
depth -= 1
chars.append(ch)
i += 1
elif ch == ',' and depth == 0:
break
elif ch == '\n' and depth == 0:
break
else:
chars.append(ch)
i += 1
return ''.join(chars).strip()
def escape_for_json(s):
"""转义字符串为 JSON 安全格式"""
return json.dumps(s)
# ── 主解析循环 ──
skip_ws()
if i >= n or raw[i] != '{':
return raw # 不是对象
result.append('{')
i += 1
expect_comma = False
while i < n:
skip_ws()
if i >= n:
break
ch = raw[i]
# 对象结束
if ch == '}':
result.append('}')
i += 1
break
# 数组结束
if ch == ']':
result.append(']')
i += 1
break
# 逗号
if ch == ',':
result.append(',')
i += 1
expect_comma = False
continue
# 注释
if ch == '/' and i + 1 < n and raw[i+1] == '/':
while i < n and raw[i] != '\n':
i += 1
continue
# 如果需要逗号但遇到新键/值,自动补逗号
if expect_comma and (ch == '"' or ch == SQ or ch.isalpha() or ch == '_' or ch == '{' or ch == '['):
result.append(',')
expect_comma = False
# 解析 key
key = None
if ch == '"':
key = parse_string_val('"')
elif ch == SQ:
if i + 2 < n and raw[i+1] == SQ and raw[i+2] == SQ:
key = parse_triple_string()
else:
key = parse_string_val(SQ)
elif ch.isalpha() or ch == '_':
# 无引号键名
start = i
while i < n and (raw[i].isalnum() or raw[i] == '_'):
i += 1
key = raw[start:i]
if key is None:
i += 1
continue
skip_ws()
if i >= n or raw[i] != ':':
continue
i += 1 # skip ':'
skip_ws()
# 输出 key
result.append(escape_for_json(key))
result.append(':')
# 解析 value
if i >= n:
break
vch = raw[i]
if vch == '"':
# 双引号字符串
val = parse_string_val('"')
result.append(escape_for_json(val))
elif vch == SQ:
if i + 2 < n and raw[i+1] == SQ and raw[i+2] == SQ:
val = parse_triple_string()
else:
val = parse_string_val(SQ)
result.append(escape_for_json(val))
elif vch == '{' or vch == '[':
# 嵌套对象/数组 — 递归处理
if vch == '{':
depth = 1
j = i + 1
while j < n and depth > 0:
if raw[j] == '{': depth += 1
elif raw[j] == '}': depth -= 1
j += 1
inner = raw[i:j]
result.append(_hjson_to_json(inner))
i = j
else:
# 数组 — 找匹配的 ],对内部每个元素递归
depth = 1
j = i + 1
while j < n and depth > 0:
if raw[j] == '[': depth += 1
elif raw[j] == ']': depth -= 1
j += 1
arr_inner = raw[i+1:j-1].strip()
result.append('[')
# 按顶层逗号分割数组元素
elems = []
elem_start = 0
d = 0
for k, c in enumerate(arr_inner):
if c in '{[': d += 1
elif c in '}]': d -= 1
elif c == ',' and d == 0:
elems.append(arr_inner[elem_start:k])
elem_start = k + 1
elems.append(arr_inner[elem_start:])
for ei, elem in enumerate(elems):
elem = elem.strip()
if not elem:
continue
if ei > 0:
result.append(',')
if elem.startswith('{'):
result.append(_hjson_to_json(elem))
elif elem.startswith('['):
# 嵌套数组
result.append(_hjson_to_json('{"_":' + elem + '}')[6:-1])
else:
# 普通值
if elem.startswith('"') or elem.startswith(SQ):
q = elem[0]
if len(elem) >= 3 and elem[1] == q and elem[2] == q:
val = elem[3:-3]
else:
val = elem[1:-1]
result.append(json.dumps(val))
elif elem in ('true', 'false', 'null'):
result.append(elem)
else:
try:
float(elem)
result.append(elem)
except ValueError:
result.append(json.dumps(elem))
result.append(']')
i = j
elif vch in ('t', 'f', 'n') and raw[i:i+4] in ('true', 'null'):
result.append(raw[i:i+4])
i += 4
elif vch == 'f' and raw[i:i+5] == 'false':
result.append('false')
i += 5
elif vch.isdigit() or vch == '-':
start = i
while i < n and (raw[i].isdigit() or raw[i] in '.-eE+'):
i += 1
result.append(raw[start:i])
else:
# 无引号字符串值
val = parse_unquoted_val()
if val:
result.append(escape_for_json(val))
expect_comma = True
return ''.join(result)
def parse_operit_metadata(file_path: str) -> Optional[Dict]:
"""
从 JS/TS 文件中提取 /* METADATA ... */ 块并解析为 dict。
支持 Operit 完整 schema: name, display_name, description, author, category, env, tools, states
同时兼容标准 JSON 和 HJSON 风格(无引号键名、单引号、三引号、注释)。
"""
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
except (FileNotFoundError, UnicodeDecodeError):
return None
# 匹配 /* METADATA ... */ 块
pattern = r'/\*\s*METADATA\s*\n?(.*?)\*/'
match = re.search(pattern, content, re.DOTALL)
if not match:
return None
raw = match.group(1).strip()
# 先尝试标准 JSON
try:
return json.loads(raw)
except json.JSONDecodeError:
pass
# 尝试 HJSON 转换
try:
cleaned = _hjson_to_json(raw)
return json.loads(cleaned)
except json.JSONDecodeError:
# 最后兜底: 移除注释和尾逗号再试
fallback = re.sub(r'//.*$', '', raw, flags=re.MULTILINE)
fallback = re.sub(r',\s*([}\]])', r'\1', fallback)
try:
return json.loads(fallback)
except json.JSONDecodeError:
return None
def _localized(field, lang="zh") -> str:
"""解析 Operit 多语言字段: 字符串 或 {zh, en, default} 对象"""
if isinstance(field, str):
return field
if isinstance(field, dict):
for key in [lang, lang.split("-")[0], "default"]:
if key in field:
return field[key]
return next(iter(field.values()), "")
return str(field) if field else ""
# ═══════════════════════════════════════════════════════════════
# 格式转换: Operit ↔ MCP
# ═══════════════════════════════════════════════════════════════
_TYPE_MAP = {
"string": "string", "number": "number", "integer": "integer",
"boolean": "boolean", "object": "object", "array": "array",
}
def operit_tool_to_mcp(tool_def: Dict, script_name: str) -> Dict:
"""Operit tool 定义 → MCP inputSchema 格式"""
properties, required = {}, []
for p in tool_def.get("parameters", []):
pname = p.get("name", "")
properties[pname] = {
"type": _TYPE_MAP.get(p.get("type", "string"), "string"),
"description": p.get("description", ""),
}
if p.get("required", False):
required.append(pname)
full_name = f"{script_name}__{tool_def.get('name', 'unknown')}"
return {
"name": full_name,
"description": _localized(tool_def.get("description", "")),
"inputSchema": {
"type": "object",
"properties": properties,
"required": required,
},
}
def meta_to_tools(meta: Dict) -> List[Dict]:
"""一个 Operit METADATA → xiaoli-cli 工具列表"""
script_name = meta.get("name", "unknown")
category = meta.get("category", "")
tools = []
for td in meta.get("tools", []):
mcp = operit_tool_to_mcp(td, script_name)
tools.append({
"name": mcp["name"],
"description": mcp["description"],
"keywords": _build_keywords(meta, td),
"usage": _build_usage(mcp),
"mcp_definition": mcp,
"source": "operit",
"script_name": script_name,
"original_tool_name": td.get("name", ""),
})
return tools
def _build_keywords(meta: Dict, tool_def: Dict) -> List[str]:
kw = ["operit"]
for s in [meta.get("name", ""), tool_def.get("name", ""), meta.get("category", "")]:
if s and s not in kw:
kw.append(s)
for text in [_localized(meta.get("description", "")), _localized(tool_def.get("description", ""))]:
for w in re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z]{3,}', text):
if w not in kw:
kw.append(w)
return kw
def _build_usage(mcp: Dict) -> str:
props = mcp.get("inputSchema", {}).get("properties", {})
req = mcp.get("inputSchema", {}).get("required", [])
lines = [f"Operit 工具: {mcp['name']}", mcp.get("description", ""), ""]
if props:
lines.append("参数:")
for pn, pd in props.items():
r = " (必填)" if pn in req else ""
lines.append(f" {pn}: {pd.get('description', pd.get('type', ''))}{r}")
lines.append(f'\n调用: {{"action":"use_tool","tool":"{mcp["name"]}","args":"..."}}')
return "\n".join(lines)
def xiaoli_to_operit_js(plugin_instance) -> str:
"""将 xiaoli-cli 插件导出为 Operit METADATA JS 文件内容"""
info = plugin_instance.get_tool_info()
mcp = plugin_instance.get_mcp_definition()
name = info.get("name", "Unknown")
desc = info.get("description", "")
schema = mcp.get("inputSchema", {})
props = schema.get("properties", {})
required = schema.get("required", [])
parameters = []
for pn, pd in props.items():
parameters.append({
"name": pn,
"description": pd.get("description", ""),
"type": pd.get("type", "string"),
"required": pn in required,
})
metadata = {
"name": name,
"display_name": {"zh": desc[:50], "en": name},
"description": {"zh": desc, "en": desc},
"author": ["xiaoli-cli"],
"category": "Utility",
"tools": [{
"name": "execute",
"description": {"zh": f"执行 {name}", "en": f"Execute {name}"},
"parameters": parameters or [
{"name": "args", "description": "操作参数", "type": "string", "required": True}
],
}],
}
meta_json = json.dumps(metadata, indent=4, ensure_ascii=False)
lines = [
"/*", "METADATA", meta_json, "*/", "",
f"// Auto-generated from xiaoli-cli plugin: {name}",
f"// Compatible with Operit AI tool system", "",
f"const {name}Bridge = (function () {{",
" async function wrap(func, params) {",
" try { return await func(params); }",
' catch (e) { return { success: false, message: "执行失败: " + e.message }; }',
" }",
" async function execute(params) {",
f" // 由 xiaoli-cli {name} 插件提供",
' const args = params.args || JSON.stringify(params);',
f' const cmd = "cd /path/to/xiaoli-cli && python3 -c \\"'
f"import sys; sys.path.insert(0, '.'); "
f"from plugins.{name} import Liugin; p = Liugin(); "
f"""print(p.handle('\\" + args + \\"'))\\\\"";""",
" try { return { success: true, data: await Tools.System.execute(cmd) }; }",
" catch (e) { return { success: false, message: e.message }; }",
" }",
" return { execute: function(p) { return wrap(execute, p); } };",
"})();",
f"exports.execute = {name}Bridge.execute;",
]
return "\n".join(lines)
# ═══════════════════════════════════════════════════════════════
# Liugin 插件主体
# ═══════════════════════════════════════════════════════════════
class Liugin:
"""
Operit Bridge — 让 xiaoli-cli 兼容 Operit AI 第三方工具格式。
自动扫描 plugins/operit/ 子目录,解析所有 Operit METADATA 脚本,
注册为 xiaoli-cli 可调用的工具。支持双向格式转换和导出。
"""
def __init__(self):
self.cli = None
# 已加载的 Operit 工具: name -> tool_info
self._operit_tools: Dict[str, Dict] = {}
# 脚本源文件映射
self._script_files: Dict[str, str] = {}
def set_cli(self, cli):
self.cli = cli
# 挂载后自动扫描 operit 子目录
self._auto_scan()
# ── 自动扫描 plugins/operit/ ──
def _get_operit_dir(self) -> str:
"""获取 plugins/operit/ 绝对路径"""
plugin_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(plugin_dir, "operit")
def _auto_scan(self):
"""启动时自动扫描 operit 子目录"""
operit_dir = self._get_operit_dir()
if not os.path.isdir(operit_dir):
return
count = 0
for ext in ("*.js", "*.ts"):
for fpath in glob.glob(os.path.join(operit_dir, "**", ext), recursive=True):
meta = parse_operit_metadata(fpath)
if meta:
for t in meta_to_tools(meta):
t["_source_file"] = fpath
self._operit_tools[t["name"]] = t
self._script_files[meta.get("name", "")] = fpath
count += 1
if count:
print(f"[operit_bridge] 自动加载了 {count} 个 Operit 脚本, "
f"{len(self._operit_tools)} 个工具")
# ── Liugin 协议接口 ──
def get_tool_info(self):
return {
"name": "operit_bridge",
"description": "Operit 双向工具桥接 — 自动加载 plugins/operit/ 下的 Operit 脚本, "
"支持解析/扫描/导出/格式转换",
"keywords": [
"operit", "bridge", "桥接", "兼容", "格式转换", "METADATA",
"插件互通", "第三方工具", "脚本", "导入", "导出",
],
"usage": (
"operit_bridge <操作> [参数]\n"
"操作:\n"
" scan [目录] - 扫描 Operit 脚本目录 (默认 plugins/operit/)\n"
" list - 列出已加载的 Operit 工具\n"
" parse <文件> - 解析单个 Operit METADATA 文件\n"
" info <工具名> - 查看工具详情\n"
" export <插件名> - 将 xiaoli-cli 插件导出为 Operit 格式\n"
" export_all [目录] - 导出所有插件\n"
" convert_mcp <JSON> - MCP → Operit\n"
" convert_operit <JSON> - Operit → MCP"
),
}
def get_mcp_definition(self):
return {
"name": "operit_bridge",
"description": "Operit 双向工具桥接,自动加载 plugins/operit/ 目录下的 Operit 格式脚本",
"inputSchema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["scan", "list", "parse", "info",
"export", "export_all", "convert_mcp", "convert_operit"],
"description": "操作类型",
},
"target": {
"type": "string",
"description": "目标路径/插件名/JSON",
},
},
"required": ["operation"],
},
}
def convert_mcp_args(self, arguments):
op = arguments.get("operation", "")
target = arguments.get("target", "")
return f"{op} {target}".strip()
def handle(self, args: str) -> str:
parts = args.strip().split(maxsplit=1)
if not parts:
return self._help()
op = parts[0].lower()
target = parts[1].strip() if len(parts) > 1 else ""
handler = {
"scan": self._h_scan, "list": self._h_list,
"parse": self._h_parse, "info": self._h_info,
"export": self._h_export, "export_all": self._h_export_all,
"convert_mcp": self._h_convert_mcp, "convert_operit": self._h_convert_operit,
}.get(op)
if not handler:
return f"错误: 不支持的操作 '{op}'\n{self._help()}"
try:
return handler(target)
except Exception as e:
return f"Operit Bridge 错误: {e}"
def _help(self):
return (
"Operit Bridge — 可用操作:\n"
" scan [目录] - 扫描 Operit 脚本\n"
" list - 列出已加载工具\n"
" parse <文件> - 解析单个 METADATA 文件\n"
" info <工具名> - 查看工具详情\n"
" export <插件名> - 导出 xiaoli 插件为 Operit 格式\n"
" export_all [目录] - 批量导出\n"
" convert_mcp <JSON> - MCP → Operit\n"
" convert_operit <JSON> - Operit → MCP"
)
# ═══════════════════════════════════════════════════════════
# 操作实现
# ═══════════════════════════════════════════════════════════
def _h_scan(self, target: str) -> str:
"""扫描 Operit 脚本目录"""
operit_dir = target or self._get_operit_dir()
if not os.path.isdir(operit_dir):
return f"错误: 目录不存在 — {operit_dir}\n请将 Operit 脚本放入 plugins/operit/ 目录"
found, tool_count = 0, 0
for ext in ("*.js", "*.ts"):
for fpath in glob.glob(os.path.join(operit_dir, "**", ext), recursive=True):
meta = parse_operit_metadata(fpath)
if meta:
tools = meta_to_tools(meta)
for t in tools:
t["_source_file"] = fpath
self._operit_tools[t["name"]] = t
self._script_files[meta.get("name", "")] = fpath
found += 1
tool_count += len(tools)
lines = [f"✓ 扫描完成: {operit_dir}", f" {found} 个脚本, {tool_count} 个工具", ""]
for name, fpath in self._script_files.items():
lines.append(f" 📦 {name} — {os.path.basename(fpath)}")
return "\n".join(lines)
def _h_list(self, _: str) -> str:
"""列出已加载的 Operit 工具"""
if not self._operit_tools:
return ("暂无已加载的 Operit 工具。\n"
"请将 .js/.ts 脚本放入 plugins/operit/ 目录,或使用 scan <目录> 手动加载。")
lines = [f"已加载 {len(self._operit_tools)} 个 Operit 工具:", ""]
for name, t in self._operit_tools.items():
src = os.path.basename(t.get("_source_file", "?"))
lines.append(f" 🔧 {name} [{src}]")
desc = t.get("description", "")
if desc:
lines.append(f" {desc[:80]}")
return "\n".join(lines)
def _h_parse(self, target: str) -> str:
"""解析单个 Operit METADATA 文件"""
if not target:
return "错误: 请提供文件路径"
if not os.path.isfile(target):
return f"错误: 文件不存在 — {target}"
meta = parse_operit_metadata(target)
if not meta:
return f"错误: 未找到有效 METADATA 块 — {target}"
tools = meta_to_tools(meta)
for t in tools:
t["_source_file"] = target
self._operit_tools[t["name"]] = t
lines = [
f"✓ 解析成功: {target}",
f" 脚本: {meta.get('name', 'N/A')}",
f" 描述: {_localized(meta.get('description', ''))}",
f" 分类: {meta.get('category', 'N/A')}",
]
author = meta.get("author", [])
if author:
lines.append(f" 作者: {', '.join(author) if isinstance(author, list) else author}")
lines.append(f" 工具数: {len(tools)}")
lines.append("")
for t in tools:
mcp = t["mcp_definition"]
props = mcp["inputSchema"]["properties"]
req = mcp["inputSchema"]["required"]
lines.append(f" 🔧 {t['name']}")
lines.append(f" {t['description']}")
if props:
ps = []
for pn, pd in props.items():
r = "*" if pn in req else ""
ps.append(f"{pn}({pd.get('type', '?')}){r}")
lines.append(f" 参数: {', '.join(ps)}")
return "\n".join(lines)
def _h_info(self, target: str) -> str:
"""查看工具详情"""
if not target:
return "错误: 请提供工具名"
t = self._operit_tools.get(target)
if not t:
return f"错误: 未找到工具 '{target}'\n可用: {', '.join(self._operit_tools.keys()) or '无'}"
lines = [
f"工具: {t['name']}",
f"描述: {t.get('description', '')}",
f"来源: {t.get('_source_file', 'unknown')}",
f"脚本: {t.get('script_name', 'N/A')}",
f"原始名: {t.get('original_tool_name', 'N/A')}",
f"关键词: {', '.join(t.get('keywords', []))}",
"", "MCP 定义:",
json.dumps(t.get("mcp_definition", {}), indent=2, ensure_ascii=False),
]
return "\n".join(lines)
def _h_export(self, target: str) -> str:
"""将 xiaoli-cli 插件导出为 Operit 格式"""
if not target:
return "错误: 请提供插件名"
instance = self._load_xiaoli_plugin(target)
if isinstance(instance, str):
return instance # error msg
js = xiaoli_to_operit_js(instance)
out_dir = os.path.join(self._get_operit_dir(), "exported")
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, f"{target}.js")
with open(out_path, "w", encoding="utf-8") as f:
f.write(js)
return f"✓ 导出成功!\n 输出: {out_path}\n 可直接放入 Operit AI 使用"
def _h_export_all(self, target: str) -> str:
"""批量导出所有 xiaoli 插件"""
plugins_dir = os.path.dirname(os.path.abspath(__file__))
out_dir = target or os.path.join(self._get_operit_dir(), "exported")
os.makedirs(out_dir, exist_ok=True)
exported, errors = [], []
for fname in sorted(os.listdir(plugins_dir)):
if not fname.endswith(".py") or fname == "__init__.py":
continue
name = fname[:-3]
try:
spec = importlib.util.spec_from_file_location(name, os.path.join(plugins_dir, fname))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
cls = getattr(mod, 'Liugin', None) or getattr(mod, 'Plugin', None)
if cls:
inst = cls()
with open(os.path.join(out_dir, f"{name}.js"), "w", encoding="utf-8") as f:
f.write(xiaoli_to_operit_js(inst))
exported.append(name)
except Exception as e:
errors.append(f"{name}: {e}")
lines = [f"✓ 批量导出完成", f" 目录: {out_dir}", f" 成功: {len(exported)}"]
if errors:
lines.append(f" 失败: {len(errors)}")
for err in errors[:5]:
lines.append(f" ⚠ {err}")
return "\n".join(lines)
def _h_convert_mcp(self, target: str) -> str:
"""MCP → Operit METADATA"""
if not target:
return "错误: 请提供 MCP JSON"
mcp = json.loads(target)
name = mcp.get("name", "unknown")
desc = mcp.get("description", "")
props = mcp.get("inputSchema", {}).get("properties", {})
required = mcp.get("inputSchema", {}).get("required", [])
params = []
for pn, pd in props.items():
params.append({"name": pn, "description": pd.get("description", ""),
"type": pd.get("type", "string"), "required": pn in required})
operit = {
"name": name,
"display_name": {"zh": desc[:50] or name, "en": name},
"description": {"zh": desc, "en": desc},
"author": ["xiaoli-cli"],
"category": "Utility",
"tools": [{"name": "execute", "description": {"zh": f"执行 {name}", "en": f"Execute {name}"},
"parameters": params}],
}
return json.dumps(operit, indent=2, ensure_ascii=False)
def _h_convert_operit(self, target: str) -> str:
"""Operit → MCP"""
if not target:
return "错误: 请提供 Operit METADATA JSON"
meta = json.loads(target)
results = [operit_tool_to_mcp(t, meta.get("name", "unknown")) for t in meta.get("tools", [])]
return json.dumps(results, indent=2, ensure_ascii=False)
# ── 辅助 ──
def _load_xiaoli_plugin(self, name: str):
"""加载一个 xiaoli-cli 插件实例,返回实例或错误字符串"""
plugins_dir = os.path.dirname(os.path.abspath(__file__))
fpath = os.path.join(plugins_dir, f"{name}.py")
if not os.path.isfile(fpath):
# 按 name 搜索
for fname in os.listdir(plugins_dir):
if fname.endswith(".py") and fname != "__init__.py":
try:
spec = importlib.util.spec_from_file_location(fname[:-3],
os.path.join(plugins_dir, fname))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
cls = getattr(mod, 'Liugin', None) or getattr(mod, 'Plugin', None)
if cls:
inst = cls()
if inst.get_tool_info().get("name", "").lower() == name.lower():
return inst
except Exception:
continue
return f"错误: 未找到插件 '{name}'"
try:
spec = importlib.util.spec_from_file_location(name, fpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
cls = getattr(mod, 'Liugin', None) or getattr(mod, 'Plugin', None)
if not cls:
return f"错误: 插件文件中未找到 Liugin/Plugin 类"
return cls()
except Exception as e:
return f"错误: 加载失败 — {e}"
# ── 给 plugin_manager 用的工具注册接口 ──
def get_operit_tools_for_registration(self) -> List[Dict]:
"""
返回所有已加载的 Operit 工具,格式兼容 plugin_manager.tools 列表。
可被 cli_base 在初始化时调用,将 Operit 工具注入工具列表。
"""
result = []
for name, t in self._operit_tools.items():
result.append({
"name": name,
"description": t.get("description", ""),
"keywords": t.get("keywords", []),
"usage": t.get("usage", ""),
"handler": self._make_handler(name),
"source": "operit",
})
return result
def _make_handler(self, tool_name: str):
"""为 Operit 工具创建 handler 闭包"""
tool_info = self._operit_tools.get(tool_name)
if not tool_info:
return lambda args: f"错误: 工具 {tool_name} 未加载"
def handler(args: str) -> str:
# 解析参数: 支持 JSON 和 "key=value" 两种格式
params = {}
args = args.strip()
if args.startswith("{"):
try:
params = json.loads(args)
except json.JSONDecodeError:
params = {"args": args}
else:
# "city=北京 days=3" 格式
for part in re.split(r'[;\s]+', args):
if '=' in part:
k, v = part.split('=', 1)
params[k.strip()] = v.strip()
elif part:
# 单个值填入第一个参数
schema_props = tool_info["mcp_definition"]["inputSchema"]["properties"]
for pname in schema_props:
if pname not in params:
params[pname] = part
break
# 如果有 _source_file,尝试用 Node.js 执行
source_file = tool_info.get("_source_file", "")
original_name = tool_info.get("original_tool_name", "execute")
if source_file and os.path.isfile(source_file):
return self._exec_operit_script(source_file, original_name, params)
# 否则返回参数解析结果(供 AI 理解)
return json.dumps({
"status": "parsed",
"tool": tool_name,
"original_tool": original_name,
"source": source_file,
"params": params,
"note": "如需执行,请确保设备上有 Node.js 运行时",
}, ensure_ascii=False)
return handler
def _exec_operit_script(self, source_file: str, func_name: str, params: Dict) -> str:
"""通过 Node.js 执行 Operit 脚本中的指定函数"""
import subprocess
import tempfile
# 生成一个临时执行器脚本
runner_code = f"""
const path = require('path');
const mod = require({json.dumps(source_file)});
const func = mod[{json.dumps(func_name)}];
if (!func) {{
console.log(JSON.stringify({{error: "函数 '{func_name}' 不存在"}}));
process.exit(1);
}}
const params = {json.dumps(params)};
func(params).then(result => {{
console.log(JSON.stringify(result, null, 2));
}}).catch(err => {{
console.log(JSON.stringify({{error: err.message}}));
process.exit(1);
}});
"""
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False,
encoding='utf-8') as f:
f.write(runner_code)
runner_path = f.name
result = subprocess.run(
["node", runner_path],
capture_output=True, text=True, timeout=30,
)
os.unlink(runner_path)
if result.returncode == 0:
return result.stdout.strip()
else:
return f"执行失败: {result.stderr.strip() or result.stdout.strip()}"
except FileNotFoundError:
return ("错误: 未找到 Node.js 运行时。\n"
"请安装 Node.js 以执行 Operit 脚本,或将脚本移植为 Python 版本。")
except subprocess.TimeoutExpired:
return "错误: 脚本执行超时 (30s)"
except Exception as e:
return f"执行错误: {e}"