release: v5.4.2 - terminal模拟器 + Operit桥接

新增插件:
- 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
This commit is contained in:
xiaoli-cli bot committed 2026-06-23 08:50:15 +00:00
1 parent b8faaae0c6
commit dfe21298b9
56 files changed
+32586 -1

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
"""Operit Bridge 插件测试"""
import os
import sys
import json
import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins.operit_bridge import (
parse_operit_metadata,
_localized,
operit_tool_to_mcp,
meta_to_tools,
xiaoli_to_operit_js,
Liugin,
)
SAMPLE_META = {
"name": "WeatherQuery",
"display_name": {"zh": "天气查询", "en": "Weather Query"},
"description": "查询指定城市的天气信息",
"author": ["TestAuthor"],
"category": "Life",
"env": ["WEATHER_API_KEY"],
"tools": [
{
"name": "get_weather",
"description": {"zh": "获取城市天气", "en": "Get city weather"},
"parameters": [
{"name": "city", "description": "城市名称", "type": "string", "required": True},
{"name": "days", "description": "预报天数", "type": "integer", "required": False},
],
},
{
"name": "get_alert",
"description": "获取天气预警",
"parameters": [
{"name": "city", "description": "城市名称", "type": "string", "required": True},
],
},
],
}
SAMPLE_JS = '''
/*
METADATA
''' + json.dumps(SAMPLE_META, ensure_ascii=False) + '''
*/
const WeatherQuery = (function () {
async function wrap(func, params) {
try { return await func(params); }
catch (e) { return { success: false, message: e.message }; }
}
async function get_weather(params) { return { success: true }; }
async function get_alert(params) { return { success: true }; }
return {
get_weather: function (p) { return wrap(get_weather, p); },
get_alert: function (p) { return wrap(get_alert, p); }
};
})();
exports.get_weather = WeatherQuery.get_weather;
exports.get_alert = WeatherQuery.get_alert;
'''
def test_parse_metadata():
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, encoding='utf-8') as f:
f.write(SAMPLE_JS)
fpath = f.name
try:
meta = parse_operit_metadata(fpath)
assert meta is not None
assert meta["name"] == "WeatherQuery"
assert len(meta["tools"]) == 2
assert meta["category"] == "Life"
assert "WEATHER_API_KEY" in meta["env"]
print("✓ test_parse_metadata")
finally:
os.unlink(fpath)
def test_parse_no_metadata():
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, encoding='utf-8') as f:
f.write("console.log('hi');")
fpath = f.name
try:
assert parse_operit_metadata(fpath) is None
print("✓ test_parse_no_metadata")
finally:
os.unlink(fpath)
def test_localized():
assert _localized("hello") == "hello"
assert _localized({"zh": "你好", "en": "hi"}, "zh") == "你好"
assert _localized({"zh": "你好", "en": "hi"}, "en") == "hi"
assert _localized({"zh": "你好"}, "en") == "你好" # fallback
assert _localized({"default": "默认"}, "ja") == "默认"
print("✓ test_localized")
def test_operit_to_mcp():
td = SAMPLE_META["tools"][0]
mcp = operit_tool_to_mcp(td, "WeatherQuery")
assert mcp["name"] == "WeatherQuery__get_weather"
assert mcp["inputSchema"]["properties"]["city"]["type"] == "string"
assert mcp["inputSchema"]["properties"]["days"]["type"] == "integer"
assert "city" in mcp["inputSchema"]["required"]
assert "days" not in mcp["inputSchema"]["required"]
print("✓ test_operit_to_mcp")
def test_meta_to_tools():
tools = meta_to_tools(SAMPLE_META)
assert len(tools) == 2
assert tools[0]["name"] == "WeatherQuery__get_weather"
assert tools[1]["name"] == "WeatherQuery__get_alert"
assert tools[0]["source"] == "operit"
assert "operit" in tools[0]["keywords"]
assert "WeatherQuery" in tools[0]["keywords"]
print("✓ test_meta_to_tools")
def test_plugin_auto_scan():
"""测试自动扫描 plugins/operit/ 目录"""
with tempfile.TemporaryDirectory() as tmpdir:
# 创建 operit 子目录
operit_dir = os.path.join(tmpdir, "operit")
os.makedirs(operit_dir)
# 写入一个有效脚本
with open(os.path.join(operit_dir, "weather.js"), 'w', encoding='utf-8') as f:
f.write(SAMPLE_JS)
# 写入一个无效文件
with open(os.path.join(operit_dir, "bad.js"), 'w', encoding='utf-8') as f:
f.write("no metadata here")
# 创建插件实例,手动设置 operit 目录
plugin = Liugin()
# monkey-patch 目录路径
plugin._get_operit_dir = lambda: operit_dir
plugin._auto_scan()
assert len(plugin._operit_tools) == 2, f"期望 2, 实际 {len(plugin._operit_tools)}"
assert "WeatherQuery__get_weather" in plugin._operit_tools
assert "WeatherQuery__get_alert" in plugin._operit_tools
print("✓ test_plugin_auto_scan")
def test_plugin_list():
plugin = Liugin()
plugin._operit_tools["test__foo"] = {
"name": "test__foo", "description": "test tool",
"keywords": [], "usage": "", "mcp_definition": {},
}
result = plugin._h_list("")
assert "test__foo" in result
assert "1 个" in result
print("✓ test_plugin_list")
def test_plugin_list_empty():
plugin = Liugin()
result = plugin._h_list("")
assert "暂无" in result
print("✓ test_plugin_list_empty")
def test_export_xiaoli_to_operit():
class MockPlugin:
def get_tool_info(self):
return {"name": "test_tool", "description": "测试工具", "keywords": ["test"]}
def get_mcp_definition(self):
return {
"name": "test_tool", "description": "测试工具",
"inputSchema": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "目标URL"},
"timeout": {"type": "integer", "description": "超时"},
},
"required": ["url"],
},
}
js = xiaoli_to_operit_js(MockPlugin())
assert "METADATA" in js
assert "test_tool" in js
assert "目标URL" in js
assert "exports.execute" in js
print("✓ test_export_xiaoli_to_operit")
def test_convert_mcp_roundtrip():
tool_def = SAMPLE_META["tools"][0]
mcp = operit_tool_to_mcp(tool_def, "WeatherQuery")
assert mcp["inputSchema"]["properties"]["city"]["type"] == "string"
assert "city" in mcp["inputSchema"]["required"]
print("✓ test_convert_mcp_roundtrip")
def test_handler_parse_json():
"""测试 handler 解析 JSON 参数"""
plugin = Liugin()
plugin._operit_tools["Test__fn"] = {
"name": "Test__fn", "description": "test",
"keywords": [], "usage": "",
"mcp_definition": {
"name": "Test__fn", "description": "",
"inputSchema": {
"type": "object",
"properties": {"x": {"type": "string"}},
"required": [],
},
},
"original_tool_name": "fn",
"_source_file": "/nonexist.js",
}
handler = plugin._make_handler("Test__fn")
result = handler('{"x": "hello"}')
parsed = json.loads(result)
assert parsed["params"]["x"] == "hello"
print("✓ test_handler_parse_json")
def test_handler_parse_kv():
"""测试 handler 解析 key=value 参数"""
plugin = Liugin()
plugin._operit_tools["Test__fn"] = {
"name": "Test__fn", "description": "test",
"keywords": [], "usage": "",
"mcp_definition": {
"name": "Test__fn", "description": "",
"inputSchema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"days": {"type": "integer"},
},
"required": [],
},
},
"original_tool_name": "fn",
"_source_file": "/nonexist.js",
}
handler = plugin._make_handler("Test__fn")
result = handler("city=北京 days=3")
parsed = json.loads(result)
assert parsed["params"]["city"] == "北京"
assert parsed["params"]["days"] == "3"
print("✓ test_handler_parse_kv")
if __name__ == "__main__":
test_parse_metadata()
test_parse_no_metadata()
test_localized()
test_operit_to_mcp()
test_meta_to_tools()
test_plugin_auto_scan()
test_plugin_list()
test_plugin_list_empty()
test_export_xiaoli_to_operit()
test_convert_mcp_roundtrip()
test_handler_parse_json()
test_handler_parse_kv()
print("\n🎉 全部 12 个测试通过!")
+229
View File
@@ -0,0 +1,229 @@
"""终端模拟器插件测试"""
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins.terminal import Liugin, TerminalSession
def test_basic_open_close():
"""打开和关闭终端"""
p = Liugin()
r = p.handle("open")
assert "✓" in r
assert "s1" in r or "main" in r
r = p.handle("list")
assert "终端" in r
r = p.handle("close")
assert "✓" in r
r = p.handle("list")
assert "暂无" in r
print("✓ test_basic_open_close")
def test_send_command():
"""发送命令并获取输出"""
p = Liugin()
p.handle("open")
r = p.handle("send echo hello_world")
assert "hello_world" in r, f"期望 hello_world,实际: {r}"
r = p.handle("send echo 42")
assert "42" in r
p.handle("close")
print("✓ test_send_command")
def test_cwd():
"""切换目录"""
p = Liugin()
p.handle("open")
r = p.handle("cwd /tmp")
assert "✓" in r
r = p.handle("send pwd")
assert "/tmp" in r
p.handle("close")
print("✓ test_cwd")
def test_env():
"""环境变量"""
p = Liugin()
p.handle("open")
r = p.handle("env MY_TEST_VAR hello123")
assert "✓" in r
r = p.handle("send echo $MY_TEST_VAR")
assert "hello123" in r
p.handle("close")
print("✓ test_env")
def test_multiple_sessions():
"""多会话管理"""
p = Liugin()
p.handle("open bash session1")
p.handle("open bash session2")
r = p.handle("list")
assert "2 个" in r
# 第一个会话是 main
p.handle("send echo AAA") # 当前是 s2,发给 s2
p.handle("switch main")
p.handle("send echo BBB") # 发给 main
r = p.handle("close main")
assert "✓" in r
r = p.handle("list")
assert "1 个" in r
p.handle("close s2")
print("✓ test_multiple_sessions")
def test_keys():
"""特殊按键"""
p = Liugin()
p.handle("open")
r = p.handle("keys")
assert "ctrl+c" in r
# 发送 Ctrl+C(不应该报错)
r = p.handle("keys ctrl+c")
assert "←" in r
p.handle("close")
print("✓ test_keys")
def test_input():
"""纯文本输入"""
p = Liugin()
p.handle("open")
p.handle("send cat << 'EOF'")
p.handle("input line1")
p.handle("input line2")
p.handle("input EOF")
p.handle("close")
print("✓ test_input")
def test_status():
"""状态查询"""
p = Liugin()
p.handle("open")
r = p.handle("status")
assert "终端状态" in r
assert "运行中" in r
p.handle("close")
print("✓ test_status")
def test_read_output():
"""读取输出"""
p = Liugin()
p.handle("open")
p.handle("send echo AAA")
p.handle("send echo BBB")
p.handle("send echo CCC")
r = p.handle("read")
# read 返回最近一次 send_cmd 的结果
assert "CCC" in r, f"read 结果: {r[:100]}"
p.handle("close")
print("✓ test_read_output")
def test_python_repl():
"""Python REPL 交互"""
p = Liugin()
p.handle("open python3 Python-REPL")
import time; time.sleep(1)
# REPL 用 input 发送(不触发哨兵),用 read 拿输出
p.handle('input print(2 + 3)')
time.sleep(0.8)
r = p.handle('read')
assert "5" in r, f"Python REPL 输出: {r[:200]}"
p.handle("close")
print("✓ test_python_repl")
def test_session_persistence():
"""会话持久性 — 环境变量在命令间保持"""
p = Liugin()
p.handle("open")
p.handle("send export FOO=bar123")
r = p.handle("send echo $FOO")
assert "bar123" in r, f"环境变量未保持: {r}"
p.handle("send cd /var")
r = p.handle("send pwd")
assert "/var" in r, f"目录未保持: {r}"
p.handle("close")
print("✓ test_session_persistence")
def test_auto_create_session():
"""无会话时自动创建"""
p = Liugin()
# 不 open,直接 send
r = p.handle("send echo auto_created")
assert "auto_created" in r
p.handle("close")
print("✓ test_auto_create_session")
def test_long_output():
"""长输出处理"""
p = Liugin()
p.handle("open")
# 生成长输出
r = p.handle("send seq 1 200")
assert "200" in r or "1" in r
p.handle("close")
print("✓ test_long_output")
if __name__ == "__main__":
test_basic_open_close()
test_send_command()
test_cwd()
test_env()
test_multiple_sessions()
test_keys()
test_input()
test_status()
test_read_output()
test_python_repl()
test_session_persistence()
test_auto_create_session()
test_long_output()
print("\n🎉 全部 13 个测试通过!")