Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de15b54b19 | ||
|
|
b5f638cffc | ||
|
|
329854866b | ||
|
|
a49c360867 | ||
|
|
b245af9063 | ||
|
|
dc77dfd304 |
No files matched your search
@@ -1,5 +1,27 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v6.1.0 (2026-06-28)
|
||||||
|
|
||||||
|
**TUI 体验全面升级 + 背景自定义**
|
||||||
|
|
||||||
|
### TUI 界面改进
|
||||||
|
|
||||||
|
- **全面中文化** — CommandPalette(命令搜索面板)子选项、placeholder、系统命令名全部汉化;HelpPanel 帮助页面、Footer、Chatbox 边框标题、错误前缀、辅助中心面板等所有用户可见英文汉化
|
||||||
|
- **辅助中心**(Ctrl+P / F9)— 原"设置"页面重命名为"辅助中心",含 6 项可切换设置(主题/引擎/历史/安全/预设背景/自定义背景)
|
||||||
|
- **独立帮助浮层**(? 键)— 重写帮助页面逻辑,独立浮层替代原 Textual HelpPanel 侧边栏,全中文内容,带滑入淡入动画
|
||||||
|
- **背景切换** — 辅助中心 [5] 切换 4 种预设背景色(墨黑/深蓝灰/深紫/深蓝),[6] 自定义背景;新增 `/bg` 命令支持用图片提取主色调作为背景
|
||||||
|
- **换行修复** — Ctrl+J 作为主推换行键(Windows 终端不区分 Shift+Enter),同时保留 shift+enter/alt+enter 兼容
|
||||||
|
- **引擎切换修复** — 解决 `call_from_thread` 在主线程崩溃的 RuntimeError,主线程直接调用,工作线程才走 call_from_thread
|
||||||
|
|
||||||
|
### 技术改进
|
||||||
|
|
||||||
|
- **文本工具调用解析** — 新增 `_handle_text_tool_call` 方法,检测模型在文本响应中用 `<tool_call>` 标签返回的工具调用(非标准 Function Calling API),解析并执行
|
||||||
|
- **网络工具优化** — network_tools.py 和 ai_search.py 改进请求头、重试机制、编码处理
|
||||||
|
- **流式输出修复** — Chatbox.append_chunk 和 ThinkingBox.append_chunk 的 refresh 改为 layout=True,修复流式只显示首行
|
||||||
|
- **取消响应修复** — 引擎层增加 `_cancelled` 标志和 cancel() 方法,TUI 层跟踪 worker 身份,action_cancel 直接调用 _finish_cancel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v6.0.0 (2026-06-23)
|
## v6.0.0 (2026-06-23)
|
||||||
|
|
||||||
**重大更新:插件生态 + 深度思考 + 工作模式**
|
**重大更新:插件生态 + 深度思考 + 工作模式**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
**产品标识**
|
**产品标识**
|
||||||
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
||||||
- **内部版本号**:20260428
|
- **内部版本号**:20260628
|
||||||
- **发布版本**:发行版6.0
|
- **发布版本**:发行版6.1.0
|
||||||
|
|
||||||
**技术规格**
|
**技术规格**
|
||||||
- **主要语言**:简体中文(UTF-8)
|
- **主要语言**:简体中文(UTF-8)
|
||||||
|
|||||||
+110
-14
@@ -22,6 +22,26 @@ project_root = os.path.dirname(current_dir)
|
|||||||
sys.path.insert(0, project_root)
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_get(obj, key, default=None):
|
||||||
|
"""兼容 dict 和 pydantic 对象的安全取值(新版 ollama 返回 pydantic 模型,不支持 .get())"""
|
||||||
|
if obj is None:
|
||||||
|
return default
|
||||||
|
# 1. 先尝试 dict 访问
|
||||||
|
try:
|
||||||
|
return obj[key]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
# 2. 尝试属性访问(pydantic 模型)
|
||||||
|
val = getattr(obj, key, None)
|
||||||
|
if val is not None:
|
||||||
|
return val
|
||||||
|
# 3. 尝试 model_dump 后的 dict 访问
|
||||||
|
try:
|
||||||
|
return obj.model_dump()[key]
|
||||||
|
except (AttributeError, KeyError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
class OllamaAI:
|
class OllamaAI:
|
||||||
"""基于Ollama的本地AI引擎"""
|
"""基于Ollama的本地AI引擎"""
|
||||||
requires_api_key = False # Ollama是本地引擎,不需要API密钥
|
requires_api_key = False # Ollama是本地引擎,不需要API密钥
|
||||||
@@ -81,6 +101,9 @@ Ollama AI引擎插件帮助信息
|
|||||||
# 共享对话历史引用(由CLI设置)
|
# 共享对话历史引用(由CLI设置)
|
||||||
self.shared_conversation_history = None
|
self.shared_conversation_history = None
|
||||||
|
|
||||||
|
# 取消标志
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
# 直接启用思考标记,由主程序自动处理深度思考模型的响应
|
# 直接启用思考标记,由主程序自动处理深度思考模型的响应
|
||||||
self.thinking_start_marker = "<thinking>"
|
self.thinking_start_marker = "<thinking>"
|
||||||
self.thinking_end_marker = "</thinking>"
|
self.thinking_end_marker = "</thinking>"
|
||||||
@@ -93,6 +116,33 @@ Ollama AI引擎插件帮助信息
|
|||||||
else:
|
else:
|
||||||
print(f"{Fore.RED}Ollama服务未运行,引擎将不可用{Style.RESET_ALL}")
|
print(f"{Fore.RED}Ollama服务未运行,引擎将不可用{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""取消正在进行的请求"""
|
||||||
|
self._cancelled = True
|
||||||
|
|
||||||
|
def apply_config(self, base_url=None, api_key=None, model=None):
|
||||||
|
"""应用新配置到当前引擎实例(内存),并持久化到 config.json"""
|
||||||
|
if base_url is not None:
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
if api_key is not None:
|
||||||
|
self.api_key = api_key
|
||||||
|
if model is not None:
|
||||||
|
self.model = model
|
||||||
|
# 持久化到 config.json
|
||||||
|
try:
|
||||||
|
import os, json
|
||||||
|
config_file = os.path.join(project_root, "config.json")
|
||||||
|
with open(config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
engine_cfg = config.setdefault("api", {}).setdefault("engines", {}).setdefault("ollama", {})
|
||||||
|
engine_cfg["base_url"] = self.base_url
|
||||||
|
engine_cfg["api_key"] = self.api_key
|
||||||
|
engine_cfg["model"] = self.model
|
||||||
|
with open(config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"持久化引擎配置失败: {e}")
|
||||||
|
|
||||||
def check_ollama_service(self):
|
def check_ollama_service(self):
|
||||||
"""检测Ollama服务是否运行"""
|
"""检测Ollama服务是否运行"""
|
||||||
# 检查 ollama 库是否可用
|
# 检查 ollama 库是否可用
|
||||||
@@ -107,7 +157,7 @@ Ollama AI引擎插件帮助信息
|
|||||||
# 如果出现异常,说明服务未运行
|
# 如果出现异常,说明服务未运行
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
|
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None, stream_start_event=None):
|
||||||
"""
|
"""
|
||||||
生成AI响应 - 使用Ollama API
|
生成AI响应 - 使用Ollama API
|
||||||
|
|
||||||
@@ -137,10 +187,20 @@ Ollama AI引擎插件帮助信息
|
|||||||
messages = self._build_messages_with_history(system_prompt, user_input)
|
messages = self._build_messages_with_history(system_prompt, user_input)
|
||||||
|
|
||||||
# 构建调用参数
|
# 构建调用参数
|
||||||
|
# 重置取消标志
|
||||||
|
self._cancelled = False
|
||||||
|
# 获取流式回调
|
||||||
|
stream_cb = None
|
||||||
|
thinking_cb = None
|
||||||
|
if self.cli and hasattr(self.cli, 'tui_stream_callback'):
|
||||||
|
stream_cb = self.cli.tui_stream_callback
|
||||||
|
if self.cli and hasattr(self.cli, 'tui_thinking_callback'):
|
||||||
|
thinking_cb = self.cli.tui_thinking_callback
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"stream": False
|
"stream": True if stream_cb else False
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Function Calling: 注入工具定义 ──
|
# ── Function Calling: 注入工具定义 ──
|
||||||
@@ -150,16 +210,45 @@ Ollama AI引擎插件帮助信息
|
|||||||
# 使用Ollama Python库调用API
|
# 使用Ollama Python库调用API
|
||||||
response = ollama_client.chat(**kwargs)
|
response = ollama_client.chat(**kwargs)
|
||||||
|
|
||||||
|
# ── 流式模式:逐块读取 ──
|
||||||
|
if stream_cb:
|
||||||
|
full_content = ""
|
||||||
|
reasoning_content = ""
|
||||||
|
for chunk in response:
|
||||||
|
if self._cancelled:
|
||||||
|
break
|
||||||
|
msg = _safe_get(chunk, "message", {})
|
||||||
|
# 检查 tool_calls
|
||||||
|
tc_raw = _safe_get(msg, "tool_calls")
|
||||||
|
if tc_raw:
|
||||||
|
# 工具调用 — 走原逻辑
|
||||||
|
message = msg
|
||||||
|
tool_calls_raw = tc_raw
|
||||||
|
break
|
||||||
|
# 深度思考(Ollama thinking 字段)
|
||||||
|
rc = _safe_get(msg, "thinking", "")
|
||||||
|
if rc and thinking_cb:
|
||||||
|
reasoning_content += rc
|
||||||
|
thinking_cb(rc)
|
||||||
|
content = _safe_get(msg, "content", "")
|
||||||
|
if content:
|
||||||
|
full_content += content
|
||||||
|
stream_cb(content)
|
||||||
|
else:
|
||||||
|
# 流式完成,无工具调用
|
||||||
|
return full_content.encode('utf-8', 'replace').decode('utf-8')
|
||||||
|
else:
|
||||||
|
message = _safe_get(response, "message", {})
|
||||||
|
tool_calls_raw = _safe_get(message, "tool_calls")
|
||||||
|
|
||||||
# ── Function Calling: 检查 tool_calls ──
|
# ── Function Calling: 检查 tool_calls ──
|
||||||
message = response.get("message", {})
|
|
||||||
tool_calls_raw = message.get("tool_calls")
|
|
||||||
if tool_calls_raw:
|
if tool_calls_raw:
|
||||||
# Ollama 的 tool_calls 格式: [{"function": {"name": ..., "arguments": {...}}}]
|
# Ollama 的 tool_calls 格式: [{"function": {"name": ..., "arguments": {...}}}]
|
||||||
fc_calls = []
|
fc_calls = []
|
||||||
for i, tc in enumerate(tool_calls_raw):
|
for i, tc in enumerate(tool_calls_raw):
|
||||||
func = tc.get("function", {})
|
func = _safe_get(tc, "function", {})
|
||||||
name = func.get("name", "")
|
name = _safe_get(func, "name", "")
|
||||||
args = func.get("arguments", {})
|
args = _safe_get(func, "arguments", {})
|
||||||
call_id = f"call_{name}_{i}"
|
call_id = f"call_{name}_{i}"
|
||||||
|
|
||||||
# 统一为小狸内部格式
|
# 统一为小狸内部格式
|
||||||
@@ -189,7 +278,7 @@ Ollama AI引擎插件帮助信息
|
|||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
# 普通文本响应
|
# 普通文本响应
|
||||||
content = message.get("content", "")
|
content = _safe_get(message, "content", "")
|
||||||
# 清理无效的 UTF-8 代理字符
|
# 清理无效的 UTF-8 代理字符
|
||||||
content = content.encode('utf-8', 'replace').decode('utf-8')
|
content = content.encode('utf-8', 'replace').decode('utf-8')
|
||||||
return content
|
return content
|
||||||
@@ -309,8 +398,13 @@ Ollama AI引擎插件帮助信息
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = ollama_client.list()
|
response = ollama_client.list()
|
||||||
models = [model["name"] for model in response["models"]]
|
models_list = _safe_get(response, "models", [])
|
||||||
return models
|
result = []
|
||||||
|
for model in models_list:
|
||||||
|
name = _safe_get(model, "model", "") or _safe_get(model, "name", "")
|
||||||
|
if name:
|
||||||
|
result.append(name)
|
||||||
|
return result if result else ["未找到本地模型"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f"获取模型列表失败: {str(e)}"]
|
return [f"获取模型列表失败: {str(e)}"]
|
||||||
|
|
||||||
@@ -392,12 +486,14 @@ Ollama AI引擎插件帮助信息
|
|||||||
|
|
||||||
# 逐块处理响应
|
# 逐块处理响应
|
||||||
for part in response:
|
for part in response:
|
||||||
if 'status' in part:
|
status = _safe_get(part, "status", None)
|
||||||
status = part['status']
|
if status:
|
||||||
print(f"{Fore.YELLOW}{status}{Style.RESET_ALL}")
|
print(f"{Fore.YELLOW}{status}{Style.RESET_ALL}")
|
||||||
# 检查是否完成
|
# 检查是否完成
|
||||||
if 'completed' in part and 'total' in part:
|
completed = _safe_get(part, "completed", None)
|
||||||
if part['completed'] == part['total']:
|
total = _safe_get(part, "total", None)
|
||||||
|
if completed is not None and total is not None:
|
||||||
|
if completed == total:
|
||||||
print(f"{Fore.GREEN}模型 {model_name} 拉取完成!{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}模型 {model_name} 拉取完成!{Style.RESET_ALL}")
|
||||||
|
|
||||||
print(f"{Fore.GREEN}模型 {model_name} 拉取成功!{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}模型 {model_name} 拉取成功!{Style.RESET_ALL}")
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class ImageEngineManager:
|
|||||||
|
|
||||||
for filename in os.listdir(engine_dir):
|
for filename in os.listdir(engine_dir):
|
||||||
if filename.endswith('_engine.py') and filename != '__init__.py':
|
if filename.endswith('_engine.py') and filename != '__init__.py':
|
||||||
engine_name = filename[:-11] # 移除 _engine.py
|
engine_name = filename[:-10] # 移除 _engine.py
|
||||||
try:
|
try:
|
||||||
# 动态导入模块
|
# 动态导入模块
|
||||||
module_path = os.path.join(engine_dir, filename)
|
module_path = os.path.join(engine_dir, filename)
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ except ImportError:
|
|||||||
OLLAMA_AVAILABLE = False
|
OLLAMA_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_get(obj, key, default=None):
|
||||||
|
"""兼容 dict 和 pydantic 对象的安全取值"""
|
||||||
|
try:
|
||||||
|
return obj[key]
|
||||||
|
except (KeyError, AttributeError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
class OllamaImageEngine(BaseImageEngine):
|
class OllamaImageEngine(BaseImageEngine):
|
||||||
"""Ollama 视觉引擎"""
|
"""Ollama 视觉引擎"""
|
||||||
|
|
||||||
@@ -69,7 +77,7 @@ class OllamaImageEngine(BaseImageEngine):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = ollama.list()
|
result = ollama.list()
|
||||||
self.installed_models = [m['model'] for m in result.get('models', [])]
|
self.installed_models = [m['model'] for m in _safe_get(result, 'models', [])]
|
||||||
except:
|
except:
|
||||||
self.installed_models = []
|
self.installed_models = []
|
||||||
|
|
||||||
@@ -137,7 +145,7 @@ class OllamaImageEngine(BaseImageEngine):
|
|||||||
'images': [image_path]
|
'images': [image_path]
|
||||||
}]
|
}]
|
||||||
)
|
)
|
||||||
return response.get('message', {}).get('content', '')
|
return _safe_get(_safe_get(response, 'message', {}), 'content', '')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Ollama识别失败: {str(e)}")
|
raise Exception(f"Ollama识别失败: {str(e)}")
|
||||||
|
|
||||||
@@ -168,7 +176,7 @@ class OllamaImageEngine(BaseImageEngine):
|
|||||||
stream=False
|
stream=False
|
||||||
)
|
)
|
||||||
|
|
||||||
content = response.get('message', {}).get('content', '')
|
content = _safe_get(_safe_get(response, 'message', {}), 'content', '')
|
||||||
if content:
|
if content:
|
||||||
yield content
|
yield content
|
||||||
else:
|
else:
|
||||||
|
|||||||
+1627
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,415 @@
|
|||||||
|
# METADATA
|
||||||
|
# {
|
||||||
|
# "name": "android_api_test",
|
||||||
|
# "display_name": {"zh": "Android API 转译测试", "en": "Android API Translation Test"},
|
||||||
|
# "description": {"zh": "测试 Android-pyApi 能否成功转译调用安卓 API 的脚本", "en": "Test if Android-pyApi can translate Android API scripts"},
|
||||||
|
# "enabledByDefault": true,
|
||||||
|
# "category": "Test",
|
||||||
|
# "tools": [
|
||||||
|
# {"name": "test_intent", "description": {"zh": "测试 Intent API", "en": "Test Intent API"}, "parameters": []},
|
||||||
|
# {"name": "test_uri", "description": {"zh": "测试 Uri API", "en": "Test Uri API"}, "parameters": []},
|
||||||
|
# {"name": "test_bundle", "description": {"zh": "测试 Bundle API", "en": "Test Bundle API"}, "parameters": []},
|
||||||
|
# {"name": "test_file_io", "description": {"zh": "测试 java.io.File API", "en": "Test java.io.File API"}, "parameters": []},
|
||||||
|
# {"name": "test_json", "description": {"zh": "测试 JSONObject API", "en": "Test JSONObject API"}, "parameters": []},
|
||||||
|
# {"name": "test_collections", "description": {"zh": "测试 ArrayList/HashMap API", "en": "Test ArrayList/HashMap API"}, "parameters": []},
|
||||||
|
# {"name": "test_environment", "description": {"zh": "测试 Environment API", "en": "Test Environment API"}, "parameters": []},
|
||||||
|
# {"name": "test_log", "description": {"zh": "测试 Log API", "en": "Test Log API"}, "parameters": []},
|
||||||
|
# {"name": "test_settings", "description": {"zh": "测试 Settings API", "en": "Test Settings API"}, "parameters": []},
|
||||||
|
# {"name": "test_bitmap", "description": {"zh": "测试 Bitmap/Canvas/Color API", "en": "Test Bitmap/Canvas/Color API"}, "parameters": []},
|
||||||
|
# {"name": "test_string_builder", "description": {"zh": "测试 StringBuilder API", "en": "Test StringBuilder API"}, "parameters": []},
|
||||||
|
# {"name": "test_content_values", "description": {"zh": "测试 ContentValues API", "en": "Test ContentValues API"}, "parameters": []},
|
||||||
|
# {"name": "test_run_all", "description": {"zh": "运行全部测试", "en": "Run all tests"}, "parameters": []}
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
async def test_intent(params):
|
||||||
|
"""测试 Intent — Android 最核心的 API"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 创建 Intent
|
||||||
|
intent = Intent("android.intent.action.VIEW")
|
||||||
|
results.append(f"Intent action: {intent.getAction()}")
|
||||||
|
|
||||||
|
# 设置 Data Uri
|
||||||
|
intent.setData(Uri.parse("https://www.example.com/path?key=value"))
|
||||||
|
results.append(f"Intent data: {intent.getData()}")
|
||||||
|
|
||||||
|
# 设置 Package
|
||||||
|
intent.setPackage("com.example.app")
|
||||||
|
results.append(f"Intent package: {intent.getPackage()}")
|
||||||
|
|
||||||
|
# 添加 Category
|
||||||
|
intent.addCategory("android.intent.category.DEFAULT")
|
||||||
|
results.append(f"Intent categories: {intent.getCategories()}")
|
||||||
|
|
||||||
|
# 设置 Flag
|
||||||
|
intent.setFlags(0x10000000) # FLAG_ACTIVITY_NEW_TASK
|
||||||
|
results.append(f"Intent flags: {hex(intent.getFlags())}")
|
||||||
|
|
||||||
|
# 添加 Extra
|
||||||
|
intent.putExtra("extra_key", "extra_value")
|
||||||
|
intent.putExtra("extra_int", 42)
|
||||||
|
results.append(f"Intent extra string: {intent.getStringExtra('extra_key')}")
|
||||||
|
results.append(f"Intent extra int: {intent.getIntExtra('extra_int', 0)}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_uri(params):
|
||||||
|
"""测试 Uri — Android URI 解析"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
uri = Uri.parse("content://com.example.provider/items/42?sort=desc#fragment")
|
||||||
|
results.append(f"scheme: {uri.getScheme()}")
|
||||||
|
results.append(f"authority: {uri.getAuthority()}")
|
||||||
|
results.append(f"host: {uri.getHost()}")
|
||||||
|
results.append(f"path: {uri.getPath()}")
|
||||||
|
results.append(f"query: {uri.getQuery()}")
|
||||||
|
results.append(f"fragment: {uri.getFragment()}")
|
||||||
|
results.append(f"lastPathSegment: {uri.getLastPathSegment()}")
|
||||||
|
|
||||||
|
# 测试 Uri.Builder
|
||||||
|
builder = Uri.Builder()
|
||||||
|
builder.scheme("https")
|
||||||
|
builder.authority("api.example.com")
|
||||||
|
builder.appendPath("v1")
|
||||||
|
builder.appendPath("users")
|
||||||
|
builder.appendQueryParameter("limit", "10")
|
||||||
|
built_uri = builder.build()
|
||||||
|
results.append(f"built uri: {built_uri.toString() if hasattr(built_uri, 'toString') else str(built_uri)}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_bundle(params):
|
||||||
|
"""测试 Bundle — Android 数据传递"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
bundle = Bundle()
|
||||||
|
bundle.putString("name", "Test User")
|
||||||
|
bundle.putInt("age", 25)
|
||||||
|
bundle.putBoolean("active", True)
|
||||||
|
bundle.putDouble("score", 95.5)
|
||||||
|
|
||||||
|
results.append(f"name: {bundle.getString('name')}")
|
||||||
|
results.append(f"age: {bundle.getInt('age')}")
|
||||||
|
results.append(f"active: {bundle.getBoolean('active')}")
|
||||||
|
results.append(f"score: {bundle.getDouble('score')}")
|
||||||
|
|
||||||
|
# 测试 keySet
|
||||||
|
keys = bundle.keySet()
|
||||||
|
results.append(f"keys: {keys}")
|
||||||
|
|
||||||
|
# 测试 containsKey
|
||||||
|
results.append(f"has 'name': {bundle.containsKey('name')}")
|
||||||
|
results.append(f"has 'missing': {bundle.containsKey('missing')}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_file_io(params):
|
||||||
|
"""测试 java.io.File — 文件操作"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 创建 File 对象
|
||||||
|
f = File(tempfile.gettempdir(), "operit_test.txt")
|
||||||
|
results.append(f"path: {f.getPath()}")
|
||||||
|
results.append(f"absolutePath: {f.getAbsolutePath()}")
|
||||||
|
results.append(f"name: {f.getName()}")
|
||||||
|
results.append(f"parent: {f.getParent()}")
|
||||||
|
results.append(f"exists before: {f.exists()}")
|
||||||
|
|
||||||
|
# 创建文件
|
||||||
|
f.createNewFile()
|
||||||
|
results.append(f"exists after create: {f.exists()}")
|
||||||
|
|
||||||
|
# 写入内容 (用 Python open)
|
||||||
|
with open(f.getAbsolutePath(), "w") as fh:
|
||||||
|
fh.write("Hello from Android-pyApi!")
|
||||||
|
|
||||||
|
# 读取
|
||||||
|
with open(f.getAbsolutePath(), "r") as fh:
|
||||||
|
content = fh.read()
|
||||||
|
results.append(f"content: {content}")
|
||||||
|
|
||||||
|
# 文件大小
|
||||||
|
results.append(f"length: {f.length()}")
|
||||||
|
|
||||||
|
# 删除
|
||||||
|
f.delete()
|
||||||
|
results.append(f"exists after delete: {f.exists()}")
|
||||||
|
|
||||||
|
# 测试目录
|
||||||
|
d = File(tempfile.gettempdir(), "operit_test_dir")
|
||||||
|
d.mkdirs()
|
||||||
|
results.append(f"dir exists: {d.exists()}")
|
||||||
|
results.append(f"isDirectory: {d.isDirectory()}")
|
||||||
|
d.delete()
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_json(params):
|
||||||
|
"""测试 JSONObject/JSONArray — JSON 处理"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 创建 JSONObject
|
||||||
|
obj = JSONObject()
|
||||||
|
obj.put("name", "Test")
|
||||||
|
obj.put("version", 3)
|
||||||
|
obj.put("active", True)
|
||||||
|
|
||||||
|
# 创建 JSONArray
|
||||||
|
arr = JSONArray()
|
||||||
|
arr.put("item1")
|
||||||
|
arr.put("item2")
|
||||||
|
arr.put("item3")
|
||||||
|
obj.put("items", arr)
|
||||||
|
|
||||||
|
# 读取
|
||||||
|
results.append(f"name: {obj.getString('name')}")
|
||||||
|
results.append(f"version: {obj.getInt('version')}")
|
||||||
|
results.append(f"active: {obj.getBoolean('active')}")
|
||||||
|
results.append(f"items length: {obj.getJSONArray('items').length()}")
|
||||||
|
results.append(f"items[0]: {obj.getJSONArray('items').getString(0)}")
|
||||||
|
|
||||||
|
# toString
|
||||||
|
json_str = obj.toString()
|
||||||
|
results.append(f"toString: {json_str}")
|
||||||
|
|
||||||
|
# 从字符串解析
|
||||||
|
parsed = JSONObject('{"key":"value","num":123}')
|
||||||
|
results.append(f"parsed key: {parsed.getString('key')}")
|
||||||
|
results.append(f"parsed num: {parsed.getInt('num')}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_collections(params):
|
||||||
|
"""测试 ArrayList/HashMap — Java 集合"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# ArrayList
|
||||||
|
lst = ArrayList()
|
||||||
|
lst.add("first")
|
||||||
|
lst.add("second")
|
||||||
|
lst.add("third")
|
||||||
|
results.append(f"ArrayList size: {lst.size()}")
|
||||||
|
results.append(f"ArrayList[0]: {lst.get(0)}")
|
||||||
|
results.append(f"ArrayList contains 'second': {lst.contains('second')}")
|
||||||
|
lst.remove("second")
|
||||||
|
results.append(f"After remove, size: {lst.size()}")
|
||||||
|
|
||||||
|
# HashMap
|
||||||
|
m = HashMap()
|
||||||
|
m.put("key1", "value1")
|
||||||
|
m.put("key2", "value2")
|
||||||
|
m.put("key3", "value3")
|
||||||
|
results.append(f"HashMap size: {m.size()}")
|
||||||
|
results.append(f"HashMap[key1]: {m.get('key1')}")
|
||||||
|
results.append(f"HashMap containsKey 'key2': {m.containsKey('key2')}")
|
||||||
|
m.remove("key2")
|
||||||
|
results.append(f"After remove, size: {m.size()}")
|
||||||
|
|
||||||
|
# HashSet
|
||||||
|
s = HashSet()
|
||||||
|
s.add("a")
|
||||||
|
s.add("b")
|
||||||
|
s.add("a") # 重复
|
||||||
|
results.append(f"HashSet size: {s.size()}")
|
||||||
|
results.append(f"HashSet contains 'a': {s.contains('a')}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_environment(params):
|
||||||
|
"""测试 Environment — Android 存储目录"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 外部存储目录
|
||||||
|
external = Environment.getExternalStorageDirectory()
|
||||||
|
results.append(f"externalStorage: {external.getPath() if hasattr(external, 'getPath') else str(external)}")
|
||||||
|
|
||||||
|
# 数据目录
|
||||||
|
data_dir = Environment.getDataDirectory()
|
||||||
|
results.append(f"dataDirectory: {data_dir.getPath() if hasattr(data_dir, 'getPath') else str(data_dir)}")
|
||||||
|
|
||||||
|
# 缓存目录
|
||||||
|
cache_dir = Environment.getDownloadCacheDirectory()
|
||||||
|
results.append(f"downloadCacheDirectory: {cache_dir.getPath() if hasattr(cache_dir, 'getPath') else str(cache_dir)}")
|
||||||
|
|
||||||
|
# 状态检查
|
||||||
|
results.append(f"externalStorageState: {Environment.getExternalStorageState()}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_log(params):
|
||||||
|
"""测试 Log — Android 日志"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 各种日志级别
|
||||||
|
Log.v("TestTag", "Verbose message")
|
||||||
|
Log.d("TestTag", "Debug message")
|
||||||
|
Log.i("TestTag", "Info message")
|
||||||
|
Log.w("TestTag", "Warning message")
|
||||||
|
Log.e("TestTag", "Error message")
|
||||||
|
|
||||||
|
results.append("All log levels executed (check stderr above)")
|
||||||
|
|
||||||
|
# 测试 getStackTraceString
|
||||||
|
try:
|
||||||
|
raise Exception("Test exception")
|
||||||
|
except Exception as e:
|
||||||
|
stack = Log.getStackTraceString(e)
|
||||||
|
results.append(f"stackTrace length: {len(stack)}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_settings(params):
|
||||||
|
"""测试 Settings — Android 系统设置"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Settings.System 常量
|
||||||
|
results.append(f"Settings.System.SCREEN_BRIGHTNESS: {getattr(Settings.System, 'SCREEN_BRIGHTNESS', 'N/A') if hasattr(Settings, 'System') else 'Settings.System not available'}")
|
||||||
|
|
||||||
|
# Settings.Global 常量
|
||||||
|
results.append(f"Settings.Global.AIRPLANE_MODE_ON: {getattr(Settings.Global, 'AIRPLANE_MODE_ON', 'N/A') if hasattr(Settings, 'Global') else 'Settings.Global not available'}")
|
||||||
|
|
||||||
|
# Settings.Secure 常量
|
||||||
|
results.append(f"Settings.Secure.ADB_ENABLED: {getattr(Settings.Secure, 'ADB_ENABLED', 'N/A') if hasattr(Settings, 'Secure') else 'Settings.Secure not available'}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_bitmap(params):
|
||||||
|
"""测试 Bitmap/Canvas/Color — Android 图形"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Color 常量
|
||||||
|
results.append(f"Color.RED: {hex(Color.RED)}")
|
||||||
|
results.append(f"Color.GREEN: {hex(Color.GREEN)}")
|
||||||
|
results.append(f"Color.BLUE: {hex(Color.BLUE)}")
|
||||||
|
results.append(f"Color.BLACK: {hex(Color.BLACK)}")
|
||||||
|
results.append(f"Color.WHITE: {hex(Color.WHITE)}")
|
||||||
|
|
||||||
|
# Color 方法
|
||||||
|
results.append(f"Color.rgb(255,128,0): {hex(Color.rgb(255, 128, 0))}")
|
||||||
|
results.append(f"Color.red(0xFF8000): {Color.red(0xFF8000)}")
|
||||||
|
results.append(f"Color.green(0xFF8000): {Color.green(0xFF8000)}")
|
||||||
|
results.append(f"Color.blue(0xFF8000): {Color.blue(0xFF8000)}")
|
||||||
|
|
||||||
|
# Paint
|
||||||
|
paint = Paint()
|
||||||
|
paint.setColor(Color.RED)
|
||||||
|
paint.setTextSize(24)
|
||||||
|
paint.setAntiAlias(True)
|
||||||
|
results.append(f"Paint color: {hex(paint.getColor())}")
|
||||||
|
results.append(f"Paint textSize: {paint.getTextSize()}")
|
||||||
|
results.append(f"Paint antiAlias: {paint.isAntiAlias()}")
|
||||||
|
|
||||||
|
# Rect
|
||||||
|
rect = Rect(0, 0, 100, 200)
|
||||||
|
results.append(f"Rect width: {rect.width()}")
|
||||||
|
results.append(f"Rect height: {rect.height()}")
|
||||||
|
results.append(f"Rect contains(50,50): {rect.contains(50, 50)}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_string_builder(params):
|
||||||
|
"""测试 StringBuilder — Java 字符串构建"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
sb = StringBuilder()
|
||||||
|
sb.append("Hello")
|
||||||
|
sb.append(" ")
|
||||||
|
sb.append("World")
|
||||||
|
sb.append("!")
|
||||||
|
results.append(f"toString: {sb.toString()}")
|
||||||
|
results.append(f"length: {sb.length()}")
|
||||||
|
|
||||||
|
sb.insert(5, ",")
|
||||||
|
results.append(f"after insert: {sb.toString()}")
|
||||||
|
|
||||||
|
sb.delete(5, 6)
|
||||||
|
results.append(f"after delete: {sb.toString()}")
|
||||||
|
|
||||||
|
sb.reverse()
|
||||||
|
results.append(f"after reverse: {sb.toString()}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_content_values(params):
|
||||||
|
"""测试 ContentValues — Android 数据库操作"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
cv = ContentValues()
|
||||||
|
cv.put("name", "Test Item")
|
||||||
|
cv.put("quantity", 10)
|
||||||
|
cv.put("price", 9.99)
|
||||||
|
cv.put("active", True)
|
||||||
|
cv.putNull("description")
|
||||||
|
|
||||||
|
results.append(f"name: {cv.get('name')}")
|
||||||
|
results.append(f"quantity: {cv.get('quantity')}")
|
||||||
|
results.append(f"price: {cv.get('price')}")
|
||||||
|
results.append(f"active: {cv.get('active')}")
|
||||||
|
results.append(f"description: {cv.get('description')}")
|
||||||
|
results.append(f"size: {cv.size()}")
|
||||||
|
results.append(f"keySet: {cv.keySet()}")
|
||||||
|
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
|
||||||
|
async def test_run_all(params):
|
||||||
|
"""运行全部测试"""
|
||||||
|
all_results = {}
|
||||||
|
tests = [
|
||||||
|
("test_intent", test_intent),
|
||||||
|
("test_uri", test_uri),
|
||||||
|
("test_bundle", test_bundle),
|
||||||
|
("test_file_io", test_file_io),
|
||||||
|
("test_json", test_json),
|
||||||
|
("test_collections", test_collections),
|
||||||
|
("test_environment", test_environment),
|
||||||
|
("test_log", test_log),
|
||||||
|
("test_settings", test_settings),
|
||||||
|
("test_bitmap", test_bitmap),
|
||||||
|
("test_string_builder", test_string_builder),
|
||||||
|
("test_content_values", test_content_values),
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for name, func in tests:
|
||||||
|
try:
|
||||||
|
result = await func({})
|
||||||
|
if result.get("success"):
|
||||||
|
passed += 1
|
||||||
|
all_results[name] = "✅ PASS"
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
all_results[name] = f"❌ FAIL: {result.get('message', 'unknown')}"
|
||||||
|
except Exception as e:
|
||||||
|
failed += 1
|
||||||
|
all_results[name] = f"❌ ERROR: {e}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": failed == 0,
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"total": len(tests),
|
||||||
|
"details": all_results
|
||||||
|
}
|
||||||
|
|
||||||
|
# 注册工具
|
||||||
|
exports["test_intent"] = test_intent
|
||||||
|
exports["test_uri"] = test_uri
|
||||||
|
exports["test_bundle"] = test_bundle
|
||||||
|
exports["test_file_io"] = test_file_io
|
||||||
|
exports["test_json"] = test_json
|
||||||
|
exports["test_collections"] = test_collections
|
||||||
|
exports["test_environment"] = test_environment
|
||||||
|
exports["test_log"] = test_log
|
||||||
|
exports["test_settings"] = test_settings
|
||||||
|
exports["test_bitmap"] = test_bitmap
|
||||||
|
exports["test_string_builder"] = test_string_builder
|
||||||
|
exports["test_content_values"] = test_content_values
|
||||||
|
exports["test_run_all"] = test_run_all
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# METADATA
|
||||||
|
# {
|
||||||
|
# "name": "test_tool",
|
||||||
|
# "display_name": {"zh": "测试工具", "en": "Test Tool"},
|
||||||
|
# "description": {"zh": "测试 operit_loader 转译层", "en": "Test operit_loader translation layer"},
|
||||||
|
# "enabledByDefault": true,
|
||||||
|
# "category": "Utility",
|
||||||
|
# "tools": [
|
||||||
|
# {"name": "echo", "description": {"zh": "回显消息", "en": "Echo message"}, "parameters": [
|
||||||
|
# {"name": "message", "description": {"zh": "消息内容", "en": "Message content"}, "type": "string", "required": true}
|
||||||
|
# ]},
|
||||||
|
# {"name": "get_device_info", "description": {"zh": "获取设备信息", "en": "Get device info"}, "parameters": []}
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def echo(params):
|
||||||
|
message = params.get("message", "hello")
|
||||||
|
return {"success": True, "message": f"Echo: {message}"}
|
||||||
|
|
||||||
|
async def get_device_info(params):
|
||||||
|
result = await Tools.System.getDeviceInfo()
|
||||||
|
return result
|
||||||
|
|
||||||
|
exports["echo"] = echo
|
||||||
|
exports["get_device_info"] = get_device_info
|
||||||
+51
-25
@@ -1,5 +1,6 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -7,6 +8,14 @@ import requests
|
|||||||
class Liugin:
|
class Liugin:
|
||||||
"""AI搜索插件 - 使用jina.ai进行智能搜索"""
|
"""AI搜索插件 - 使用jina.ai进行智能搜索"""
|
||||||
|
|
||||||
|
# 统一 User-Agent(模拟浏览器,避免被反爬拦截)
|
||||||
|
_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||||
|
_HEADERS = {
|
||||||
|
'User-Agent': _UA,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.usage = """AI搜索工具使用方法:/r
|
self.usage = """AI搜索工具使用方法:/r
|
||||||
ai_search <操作> <参数>/r
|
ai_search <操作> <参数>/r
|
||||||
@@ -27,6 +36,7 @@ ai_search <操作> <参数>/r
|
|||||||
"""
|
"""
|
||||||
self.cli = None # CLI实例引用
|
self.cli = None # CLI实例引用
|
||||||
self.timeout = 30 # 请求超时时间(秒)
|
self.timeout = 30 # 请求超时时间(秒)
|
||||||
|
self.max_retries = 2 # 最大重试次数
|
||||||
|
|
||||||
def set_cli(self, cli):
|
def set_cli(self, cli):
|
||||||
"""设置CLI实例引用"""
|
"""设置CLI实例引用"""
|
||||||
@@ -162,34 +172,50 @@ JSON格式示例:
|
|||||||
return f"网络搜索错误: {str(e)}"
|
return f"网络搜索错误: {str(e)}"
|
||||||
|
|
||||||
def _execute_curl(self, url):
|
def _execute_curl(self, url):
|
||||||
"""使用 requests 库获取内容(替代 curl 命令)"""
|
"""使用 requests 库获取内容(带重试、UA、编码处理)"""
|
||||||
try:
|
last_error = None
|
||||||
# 使用 requests 库发送请求
|
for attempt in range(self.max_retries + 1):
|
||||||
response = requests.get(
|
try:
|
||||||
url,
|
response = requests.get(
|
||||||
timeout=self.timeout,
|
url,
|
||||||
headers={'User-Agent': 'Mozilla/5.0 (compatible; AI-Search-Bot)'}
|
timeout=self.timeout,
|
||||||
)
|
headers=self._HEADERS,
|
||||||
|
allow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
# 检查状态码
|
# 检查状态码
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return f"错误:HTTP请求失败,状态码: {response.status_code}"
|
return f"错误:HTTP请求失败,状态码: {response.status_code}"
|
||||||
|
|
||||||
# 获取内容
|
# 显式编码处理:避免 ISO-8859-1 默认导致中文乱码
|
||||||
content = response.text.strip()
|
if not response.encoding or response.encoding.lower() == 'iso-8859-1':
|
||||||
|
response.encoding = response.apparent_encoding or 'utf-8'
|
||||||
|
|
||||||
if not content:
|
# 获取内容
|
||||||
return "错误:未获取到任何内容"
|
content = response.text.strip()
|
||||||
|
|
||||||
# 限制输出长度以避免过长输出
|
if not content:
|
||||||
if len(content) > 5000:
|
return "错误:未获取到任何内容"
|
||||||
content = content[:5000] + f"\n\n... (内容已截断,完整内容共{len(content)}字符)"
|
|
||||||
|
|
||||||
return content
|
# 限制输出长度以避免过长输出
|
||||||
|
if len(content) > 5000:
|
||||||
|
content = content[:5000] + f"\n\n... (内容已截断,完整内容共{len(content)}字符)"
|
||||||
|
|
||||||
except requests.Timeout:
|
return content
|
||||||
return f"错误:请求超时 (>{self.timeout}秒)"
|
|
||||||
except requests.ConnectionError:
|
except requests.exceptions.Timeout:
|
||||||
return "错误:网络连接失败,请检查网络"
|
last_error = f"请求超时 (>{self.timeout}秒)"
|
||||||
except Exception as e:
|
if attempt < self.max_retries:
|
||||||
return f"错误:请求时发生错误: {str(e)}"
|
time.sleep(1)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
last_error = "网络连接失败,请检查网络"
|
||||||
|
if attempt < self.max_retries:
|
||||||
|
time.sleep(1)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
last_error = str(e)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
last_error = str(e)
|
||||||
|
break
|
||||||
|
|
||||||
|
return f"错误:请求时发生错误: {last_error}"
|
||||||
@@ -368,8 +368,16 @@ class Liugin:
|
|||||||
if not test_files and len(py_files) > 5:
|
if not test_files and len(py_files) > 5:
|
||||||
suggestions.append(" 添加单元测试")
|
suggestions.append(" 添加单元测试")
|
||||||
|
|
||||||
uses_print = sum(1 for f in py_files if 'print(' in open(f, errors='ignore').read())
|
uses_print = 0
|
||||||
uses_log = sum(1 for f in py_files if 'logging' in open(f, errors='ignore').read())
|
uses_log = 0
|
||||||
|
for f in py_files:
|
||||||
|
with open(f, errors='ignore') as fh:
|
||||||
|
content = fh.read()
|
||||||
|
if 'print(' in content:
|
||||||
|
uses_print += 1
|
||||||
|
if 'logging' in content:
|
||||||
|
uses_log += 1
|
||||||
|
|
||||||
if uses_print > uses_log and uses_print > 3:
|
if uses_print > uses_log and uses_print > 3:
|
||||||
suggestions.append(" 考虑用 logging 替代 print")
|
suggestions.append(" 考虑用 logging 替代 print")
|
||||||
|
|
||||||
|
|||||||
@@ -1158,8 +1158,10 @@ else:
|
|||||||
def _op_diff(self, path: str, rest: str) -> str:
|
def _op_diff(self, path: str, rest: str) -> str:
|
||||||
if not path:
|
if not path:
|
||||||
return "错误:请提供第一个文件路径"
|
return "错误:请提供第一个文件路径"
|
||||||
|
if not rest.strip():
|
||||||
|
return "错误:请提供第二个文件路径"
|
||||||
f1 = self._norm(path)
|
f1 = self._norm(path)
|
||||||
f2 = self._norm(rest.split()[0] if rest.strip() else "")
|
f2 = self._norm(rest.split()[0])
|
||||||
if not os.path.exists(f1):
|
if not os.path.exists(f1):
|
||||||
return f"错误:文件不存在: {f1}"
|
return f"错误:文件不存在: {f1}"
|
||||||
if not os.path.exists(f2):
|
if not os.path.exists(f2):
|
||||||
|
|||||||
@@ -0,0 +1,568 @@
|
|||||||
|
"""
|
||||||
|
编译验证插件 - 批量语法/编译检查
|
||||||
|
对项目内 Python 文件做 ast.parse + py_compile 双重验证,
|
||||||
|
支持按目录、单文件或全项目扫描,并发执行加速。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import ast
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import py_compile
|
||||||
|
import concurrent.futures
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from xcli_core.tool_result import ToolResult, ErrorCode
|
||||||
|
|
||||||
|
try:
|
||||||
|
from colorama import Fore, Style, init as _colorama_init
|
||||||
|
_colorama_init(autoreset=False)
|
||||||
|
_HAS_COLOR = True
|
||||||
|
except Exception:
|
||||||
|
_HAS_COLOR = False
|
||||||
|
|
||||||
|
class _Stub:
|
||||||
|
def __getattr__(self, _): return ""
|
||||||
|
Fore = Style = _Stub()
|
||||||
|
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
SKIP_DIRS = {
|
||||||
|
"__pycache__", ".git", ".venv", "venv", "env", ".env",
|
||||||
|
"node_modules", ".idea", ".vscode", "dist", "build",
|
||||||
|
".mypy_cache", ".pytest_cache", ".ruff_cache", "site-packages",
|
||||||
|
}
|
||||||
|
|
||||||
|
SKIP_FILES = {
|
||||||
|
"setup.py",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Liugin:
|
||||||
|
"""编译验证 - 批量语法检查 / 编译验证 / 清理 __pycache__"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.usage = """编译验证工具
|
||||||
|
|
||||||
|
操作:
|
||||||
|
all - 验证项目内全部 Python 文件
|
||||||
|
core - 仅验证 xcli_core/ 目录
|
||||||
|
engines - 仅验证 ai_engines/ 目录
|
||||||
|
plugins - 仅验证 plugins/ 目录
|
||||||
|
dir <目录> - 验证指定目录
|
||||||
|
check <文件> - 验证单个文件 (可多次指定)
|
||||||
|
clean [目录] - 清理 __pycache__ (默认项目根)
|
||||||
|
stats - 统计各目录 Python 文件数量
|
||||||
|
|
||||||
|
选项:
|
||||||
|
-v / --verbose - 显示每个文件的结果
|
||||||
|
-j / --jobs <N> - 并发线程数 (默认 8)
|
||||||
|
-q / --quiet - 仅在结束时打印汇总
|
||||||
|
|
||||||
|
输出:
|
||||||
|
✅ 绿色 = 通过
|
||||||
|
❌ 红色 = 失败
|
||||||
|
汇总: 总数/通过/失败 + 失败文件详情
|
||||||
|
|
||||||
|
示例:
|
||||||
|
check all -v
|
||||||
|
check core
|
||||||
|
check plugins/compile_check.py
|
||||||
|
check dir ai_engines -j 16
|
||||||
|
check clean
|
||||||
|
"""
|
||||||
|
self.cli = None
|
||||||
|
|
||||||
|
def set_cli(self, cli):
|
||||||
|
self.cli = cli
|
||||||
|
|
||||||
|
def get_tool_info(self):
|
||||||
|
return {
|
||||||
|
"name": "compile_check",
|
||||||
|
"description": "编译验证工具 — 批量语法/编译检查(ast.parse + py_compile),支持目录、单文件、全项目扫描,并发加速,清理 __pycache__",
|
||||||
|
"keywords": ["编译", "验证", "语法", "检查", "compile", "syntax",
|
||||||
|
"check", "lint", "py_compile", "ast", "批量",
|
||||||
|
"validate", "verify", "clean", "pycache"],
|
||||||
|
"usage": self.usage,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_mcp_definition(self):
|
||||||
|
return {
|
||||||
|
"name": "compile_check",
|
||||||
|
"description": "编译验证工具",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"operation": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["all", "core", "engines", "plugins",
|
||||||
|
"dir", "check", "clean", "stats"],
|
||||||
|
"description": "操作类型"
|
||||||
|
},
|
||||||
|
"path": {"type": "string",
|
||||||
|
"description": "目录或文件路径"},
|
||||||
|
"paths": {"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "多文件路径列表"},
|
||||||
|
"verbose": {"type": "boolean", "default": False},
|
||||||
|
"jobs": {"type": "integer", "default": 8},
|
||||||
|
"quiet": {"type": "boolean", "default": False},
|
||||||
|
},
|
||||||
|
"required": ["operation"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_mcp_args(self, arguments):
|
||||||
|
op = arguments.get("operation", "")
|
||||||
|
path = arguments.get("path", "")
|
||||||
|
return f"{op} {path}".strip()
|
||||||
|
|
||||||
|
def handle(self, args: str):
|
||||||
|
try:
|
||||||
|
parts = self._parse_args(args)
|
||||||
|
if not parts:
|
||||||
|
return self._print_usage()
|
||||||
|
|
||||||
|
operation = parts[0].lower()
|
||||||
|
opts = self._extract_options(parts[1:])
|
||||||
|
|
||||||
|
if operation in ("help", "-h", "--help"):
|
||||||
|
return self._print_usage()
|
||||||
|
|
||||||
|
routes = {
|
||||||
|
"all": self._op_all,
|
||||||
|
"core": self._op_core,
|
||||||
|
"engines": self._op_engines,
|
||||||
|
"plugins": self._op_plugins,
|
||||||
|
"dir": self._op_dir,
|
||||||
|
"check": self._op_check,
|
||||||
|
"clean": self._op_clean,
|
||||||
|
"stats": self._op_stats,
|
||||||
|
}
|
||||||
|
handler = routes.get(operation)
|
||||||
|
if not handler:
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"不支持的操作 '{operation}'",
|
||||||
|
ErrorCode.UNSUPPORTED_OP,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
return handler(opts)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"编译验证错误: {e}",
|
||||||
|
ErrorCode.EXEC_FAILED,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _print_usage(self):
|
||||||
|
self._emit(self.usage)
|
||||||
|
return ToolResult.ok("usage_printed", tool_name="compile_check")
|
||||||
|
|
||||||
|
def _out(self, msg: str):
|
||||||
|
"""统一输出,适配 TUI/CLI"""
|
||||||
|
if self.cli is not None and hasattr(self.cli, "_output"):
|
||||||
|
self.cli._output(msg)
|
||||||
|
else:
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
def _emit(self, msg: str, end: str = "\n"):
|
||||||
|
"""统一输出,自动适配 TUI/CLI
|
||||||
|
|
||||||
|
- TUI 模式: 一行一行输出到 _output,忽略 end(TUI 有自己的状态指示器)
|
||||||
|
- CLI 模式: 尊重 end,支持 end="" 的进度条刷新
|
||||||
|
- TUI 模式下 end != "\\n" 的进度条输出直接跳过(避免污染对话区)
|
||||||
|
"""
|
||||||
|
if self.cli is not None and hasattr(self.cli, "_output"):
|
||||||
|
if end != "\n":
|
||||||
|
return # TUI 模式跳过进度条类输出
|
||||||
|
self.cli._output(msg)
|
||||||
|
else:
|
||||||
|
print(msg, end=end)
|
||||||
|
|
||||||
|
def _ok(self, msg: str) -> str:
|
||||||
|
return f"{Fore.GREEN}{msg}{Style.RESET_ALL}"
|
||||||
|
|
||||||
|
def _err(self, msg: str) -> str:
|
||||||
|
return f"{Fore.RED}{msg}{Style.RESET_ALL}"
|
||||||
|
|
||||||
|
def _warn(self, msg: str) -> str:
|
||||||
|
return f"{Fore.YELLOW}{msg}{Style.RESET_ALL}"
|
||||||
|
|
||||||
|
def _dim(self, msg: str) -> str:
|
||||||
|
return f"{Fore.CYAN}{msg}{Style.RESET_ALL}"
|
||||||
|
|
||||||
|
def _banner(self, title: str) -> str:
|
||||||
|
bar = "═" * max(20, len(title) + 4)
|
||||||
|
return f"{Fore.CYAN}{bar}\n {title}\n{bar}{Style.RESET_ALL}"
|
||||||
|
|
||||||
|
def _parse_args(self, args: str) -> List[str]:
|
||||||
|
parts, current, in_quotes, qc = [], "", False, None
|
||||||
|
for ch in args:
|
||||||
|
if ch in ('"', "'") and not in_quotes:
|
||||||
|
in_quotes, qc = True, ch
|
||||||
|
elif ch == qc and in_quotes:
|
||||||
|
in_quotes, qc = False, None
|
||||||
|
elif ch == " " and not in_quotes:
|
||||||
|
if current:
|
||||||
|
parts.append(current)
|
||||||
|
current = ""
|
||||||
|
else:
|
||||||
|
current += ch
|
||||||
|
if current:
|
||||||
|
parts.append(current)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
def _extract_options(self, args: List[str]) -> dict:
|
||||||
|
opts = {
|
||||||
|
"positional": [],
|
||||||
|
"verbose": False,
|
||||||
|
"quiet": False,
|
||||||
|
"jobs": 8,
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
a = args[i]
|
||||||
|
if a in ("-v", "--verbose"):
|
||||||
|
opts["verbose"] = True
|
||||||
|
elif a in ("-q", "--quiet"):
|
||||||
|
opts["quiet"] = True
|
||||||
|
elif a in ("-j", "--jobs") and i + 1 < len(args):
|
||||||
|
try:
|
||||||
|
opts["jobs"] = max(1, int(args[i + 1]))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
i += 1
|
||||||
|
elif not a.startswith("-"):
|
||||||
|
opts["positional"].append(a)
|
||||||
|
i += 1
|
||||||
|
return opts
|
||||||
|
|
||||||
|
def _collect_py_files(self, root: str) -> List[str]:
|
||||||
|
root = os.path.abspath(root)
|
||||||
|
if not os.path.isdir(root):
|
||||||
|
return []
|
||||||
|
result: List[str] = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk(root):
|
||||||
|
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||||
|
for fn in filenames:
|
||||||
|
if not fn.endswith(".py"):
|
||||||
|
continue
|
||||||
|
if fn in SKIP_FILES:
|
||||||
|
continue
|
||||||
|
result.append(os.path.join(dirpath, fn))
|
||||||
|
result.sort()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _verify_one(self, filepath: str) -> Tuple[str, bool, str]:
|
||||||
|
try:
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
src = f.read()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
try:
|
||||||
|
with open(filepath, "r", encoding="gbk") as f:
|
||||||
|
src = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
return filepath, False, f"读取失败: {e}"
|
||||||
|
except Exception as e:
|
||||||
|
return filepath, False, f"读取失败: {e}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
ast.parse(src, filename=filepath)
|
||||||
|
except SyntaxError as e:
|
||||||
|
line = getattr(e, "lineno", "?")
|
||||||
|
col = getattr(e, "offset", "?")
|
||||||
|
msg = getattr(e, "msg", str(e))
|
||||||
|
return filepath, False, f"SyntaxError 第{line}行第{col}列: {msg}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
py_compile.compile(filepath, doraise=True, quiet=2)
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
return filepath, False, f"PyCompileError: {e.msg if hasattr(e, 'msg') else e}"
|
||||||
|
except Exception as e:
|
||||||
|
return filepath, False, f"编译失败: {e}"
|
||||||
|
|
||||||
|
return filepath, True, ""
|
||||||
|
|
||||||
|
def _run_batch(
|
||||||
|
self,
|
||||||
|
files: List[str],
|
||||||
|
title: str,
|
||||||
|
verbose: bool = False,
|
||||||
|
quiet: bool = False,
|
||||||
|
jobs: int = 8,
|
||||||
|
) -> Tuple[int, int, List[Tuple[str, str]]]:
|
||||||
|
total = len(files)
|
||||||
|
if total == 0:
|
||||||
|
self._emit(self._warn(f"{title}: 没有可验证的 Python 文件"))
|
||||||
|
return 0, 0, []
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
self._emit(self._banner(f"{title} ({total} 个文件, {jobs} 线程)"))
|
||||||
|
self._emit("")
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed: List[Tuple[str, str]] = []
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
results: List[Tuple[str, bool, str]] = []
|
||||||
|
if jobs == 1:
|
||||||
|
for f in files:
|
||||||
|
results.append(self._verify_one(f))
|
||||||
|
else:
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as ex:
|
||||||
|
futures = {ex.submit(self._verify_one, f): f for f in files}
|
||||||
|
done = 0
|
||||||
|
for fut in concurrent.futures.as_completed(futures):
|
||||||
|
results.append(fut.result())
|
||||||
|
done += 1
|
||||||
|
if not quiet and not verbose and total > 0:
|
||||||
|
pct = done * 100 // total
|
||||||
|
rel = os.path.relpath(futures[fut], PROJECT_ROOT)
|
||||||
|
self._emit(f"\r 进度: [{done}/{total}] {pct:3d}% {rel[:40]:<40}", end="")
|
||||||
|
if not quiet and not verbose:
|
||||||
|
self._emit("")
|
||||||
|
|
||||||
|
results.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
if verbose and not quiet:
|
||||||
|
self._emit("")
|
||||||
|
|
||||||
|
for filepath, ok, err in results:
|
||||||
|
rel = os.path.relpath(filepath, PROJECT_ROOT)
|
||||||
|
if ok:
|
||||||
|
passed += 1
|
||||||
|
if verbose and not quiet:
|
||||||
|
self._emit(f" {self._ok('✓')} {rel}")
|
||||||
|
else:
|
||||||
|
failed.append((rel, err))
|
||||||
|
if not quiet:
|
||||||
|
self._emit(f" {self._err('✗')} {rel}")
|
||||||
|
self._emit(f" {self._err(err)}")
|
||||||
|
|
||||||
|
elapsed = time.time() - start
|
||||||
|
|
||||||
|
self._emit("")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
if failed:
|
||||||
|
self._emit(f" {self._err('✗ 失败:')} {len(failed)}/{total}")
|
||||||
|
self._emit(f" {self._ok('✓ 通过:')} {passed}/{total}")
|
||||||
|
self._emit(f" {self._dim(f'耗时: {elapsed:.2f}s')}")
|
||||||
|
self._emit("")
|
||||||
|
self._emit(self._err("失败列表:"))
|
||||||
|
for rel, err in failed:
|
||||||
|
self._emit(f" • {self._err(rel)}")
|
||||||
|
first_line = err.split("\n")[0][:120]
|
||||||
|
self._emit(f" {first_line}")
|
||||||
|
else:
|
||||||
|
self._emit(f" {self._ok(f'✓ 全部通过:')} {passed}/{total} 个文件")
|
||||||
|
self._emit(f" {self._dim(f'耗时: {elapsed:.2f}s')}")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
|
||||||
|
return passed, len(failed), failed
|
||||||
|
|
||||||
|
def _op_all(self, opts: dict):
|
||||||
|
files = self._collect_py_files(PROJECT_ROOT)
|
||||||
|
passed, failed, _ = self._run_batch(
|
||||||
|
files,
|
||||||
|
"全项目编译验证",
|
||||||
|
verbose=opts["verbose"],
|
||||||
|
quiet=opts["quiet"],
|
||||||
|
jobs=opts["jobs"],
|
||||||
|
)
|
||||||
|
if failed == 0:
|
||||||
|
return ToolResult.ok(
|
||||||
|
f"全部通过 ({passed}/{passed})",
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"有 {failed} 个文件编译失败",
|
||||||
|
ErrorCode.EXEC_FAILED,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _op_core(self, opts: dict):
|
||||||
|
return self._scan_subdir("xcli_core", "xcli_core 编译验证", opts)
|
||||||
|
|
||||||
|
def _op_engines(self, opts: dict):
|
||||||
|
return self._scan_subdir("ai_engines", "ai_engines 编译验证", opts)
|
||||||
|
|
||||||
|
def _op_plugins(self, opts: dict):
|
||||||
|
return self._scan_subdir("plugins", "plugins 编译验证", opts)
|
||||||
|
|
||||||
|
def _scan_subdir(self, sub: str, title: str, opts: dict):
|
||||||
|
target = os.path.join(PROJECT_ROOT, sub)
|
||||||
|
if not os.path.isdir(target):
|
||||||
|
return ToolResult.not_found(f"目录: {sub}", tool_name="compile_check")
|
||||||
|
files = self._collect_py_files(target)
|
||||||
|
passed, failed, _ = self._run_batch(
|
||||||
|
files, title,
|
||||||
|
verbose=opts["verbose"],
|
||||||
|
quiet=opts["quiet"],
|
||||||
|
jobs=opts["jobs"],
|
||||||
|
)
|
||||||
|
if failed == 0:
|
||||||
|
return ToolResult.ok(
|
||||||
|
f"{sub} 全部通过 ({passed})",
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"{sub} 有 {failed} 个文件编译失败",
|
||||||
|
ErrorCode.EXEC_FAILED,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _op_dir(self, opts: dict):
|
||||||
|
if not opts["positional"]:
|
||||||
|
return ToolResult.missing("目录路径", tool_name="compile_check")
|
||||||
|
target = os.path.abspath(opts["positional"][0])
|
||||||
|
if not os.path.isdir(target):
|
||||||
|
return ToolResult.not_found(f"目录: {target}", tool_name="compile_check")
|
||||||
|
files = self._collect_py_files(target)
|
||||||
|
passed, failed, _ = self._run_batch(
|
||||||
|
files,
|
||||||
|
f"目录验证: {os.path.relpath(target, PROJECT_ROOT)}",
|
||||||
|
verbose=opts["verbose"],
|
||||||
|
quiet=opts["quiet"],
|
||||||
|
jobs=opts["jobs"],
|
||||||
|
)
|
||||||
|
if failed == 0:
|
||||||
|
return ToolResult.ok(
|
||||||
|
f"目录全部通过 ({passed})",
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"目录有 {failed} 个文件编译失败",
|
||||||
|
ErrorCode.EXEC_FAILED,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _op_check(self, opts: dict):
|
||||||
|
if not opts["positional"]:
|
||||||
|
return ToolResult.missing("文件路径", tool_name="compile_check")
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
for p in opts["positional"]:
|
||||||
|
fp = os.path.abspath(p)
|
||||||
|
if not os.path.isfile(fp):
|
||||||
|
return ToolResult.not_found(
|
||||||
|
f"文件: {p}", tool_name="compile_check",
|
||||||
|
)
|
||||||
|
targets.append(fp)
|
||||||
|
|
||||||
|
if len(targets) == 1:
|
||||||
|
self._emit(self._banner(
|
||||||
|
f"单文件验证: {os.path.relpath(targets[0], PROJECT_ROOT)}"
|
||||||
|
))
|
||||||
|
filepath, ok, err = self._verify_one(targets[0])
|
||||||
|
rel = os.path.relpath(filepath, PROJECT_ROOT)
|
||||||
|
self._emit("")
|
||||||
|
if ok:
|
||||||
|
self._emit(f" {self._ok('✓ 通过')} {rel}")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
return ToolResult.ok(f"通过: {rel}", tool_name="compile_check")
|
||||||
|
else:
|
||||||
|
self._emit(f" {self._err('✗ 失败')} {rel}")
|
||||||
|
self._emit(f" {self._err(err)}")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"编译失败: {rel}",
|
||||||
|
ErrorCode.EXEC_FAILED,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
passed, failed, _ = self._run_batch(
|
||||||
|
targets,
|
||||||
|
f"批量验证 ({len(targets)} 个文件)",
|
||||||
|
verbose=opts["verbose"],
|
||||||
|
quiet=opts["quiet"],
|
||||||
|
jobs=opts["jobs"],
|
||||||
|
)
|
||||||
|
if failed == 0:
|
||||||
|
return ToolResult.ok(
|
||||||
|
f"全部通过 ({passed})",
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
return ToolResult.fail(
|
||||||
|
f"有 {failed} 个文件编译失败",
|
||||||
|
ErrorCode.EXEC_FAILED,
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _op_clean(self, opts: dict):
|
||||||
|
target = PROJECT_ROOT
|
||||||
|
if opts["positional"]:
|
||||||
|
target = os.path.abspath(opts["positional"][0])
|
||||||
|
if not os.path.isdir(target):
|
||||||
|
return ToolResult.not_found(
|
||||||
|
f"目录: {target}", tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
self._emit(self._banner(f"清理 __pycache__: {os.path.relpath(target, PROJECT_ROOT)}"))
|
||||||
|
removed = 0
|
||||||
|
for dirpath, dirnames, _ in os.walk(target):
|
||||||
|
for d in list(dirnames):
|
||||||
|
if d == "__pycache__":
|
||||||
|
full = os.path.join(dirpath, d)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(full)
|
||||||
|
removed += 1
|
||||||
|
rel = os.path.relpath(full, PROJECT_ROOT)
|
||||||
|
if not opts["quiet"]:
|
||||||
|
self._emit(f" {self._ok('✓ 已删除')} {rel}")
|
||||||
|
except Exception as e:
|
||||||
|
rel = os.path.relpath(full, PROJECT_ROOT)
|
||||||
|
self._emit(f" {self._err('✗ 失败')} {rel}: {e}")
|
||||||
|
self._emit("")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
if removed:
|
||||||
|
self._emit(f" {self._ok(f'✓ 已清理')} {removed} 个 __pycache__")
|
||||||
|
else:
|
||||||
|
self._emit(f" {self._dim('没有可清理的 __pycache__')}")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
|
||||||
|
return ToolResult.ok(
|
||||||
|
f"清理完成 ({removed} 个目录)",
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _op_stats(self, opts: dict):
|
||||||
|
self._emit(self._banner("项目 Python 文件统计"))
|
||||||
|
|
||||||
|
subdirs = [
|
||||||
|
("xcli_core", "xcli_core"),
|
||||||
|
("ai_engines", "ai_engines"),
|
||||||
|
("plugins", "plugins"),
|
||||||
|
]
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
rows: List[Tuple[str, int]] = []
|
||||||
|
for sub, label in subdirs:
|
||||||
|
target = os.path.join(PROJECT_ROOT, sub)
|
||||||
|
if os.path.isdir(target):
|
||||||
|
count = len(self._collect_py_files(target))
|
||||||
|
rows.append((label, count))
|
||||||
|
total += count
|
||||||
|
|
||||||
|
other = len(self._collect_py_files(PROJECT_ROOT)) - total
|
||||||
|
rows.append(("其他(根目录等)", other))
|
||||||
|
total += other
|
||||||
|
|
||||||
|
self._emit("")
|
||||||
|
self._emit(f" {'目录':<20} {'文件数':>8}")
|
||||||
|
self._emit(" " + "─" * 30)
|
||||||
|
for label, count in rows:
|
||||||
|
bar = "█" * min(count, 40) if count else ""
|
||||||
|
color = Fore.GREEN if count else Fore.CYAN
|
||||||
|
self._emit(f" {label:<20} {count:>8} {color}{bar}{Style.RESET_ALL}")
|
||||||
|
self._emit(" " + "─" * 30)
|
||||||
|
self._emit(f" {'合计':<20} {total:>8}")
|
||||||
|
self._emit("")
|
||||||
|
self._emit("─" * 60)
|
||||||
|
|
||||||
|
return ToolResult.ok(
|
||||||
|
f"统计完成 (共 {total} 个文件)",
|
||||||
|
tool_name="compile_check",
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
@@ -317,7 +318,7 @@ JSON格式示例:
|
|||||||
"message": "使用了var声明,建议使用let或const"
|
"message": "使用了var声明,建议使用let或const"
|
||||||
})
|
})
|
||||||
|
|
||||||
if '==' in content and '!==' not in content:
|
if re.search(r'(?<![=!])==(?!=)', content):
|
||||||
issues.append({
|
issues.append({
|
||||||
"level": "warning",
|
"level": "warning",
|
||||||
"message": "使用了==比较,建议使用===严格相等"
|
"message": "使用了==比较,建议使用===严格相等"
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ class Liugin:
|
|||||||
|
|
||||||
def _auto_commit_message(self, status: str) -> str:
|
def _auto_commit_message(self, status: str) -> str:
|
||||||
lines = status.strip().split('\n')
|
lines = status.strip().split('\n')
|
||||||
added = len([l for l in lines if l.startswith('A ') or l.startswith('A') and l[1] == ' '])
|
added = len([l for l in lines if l.startswith('A ') or (len(l) > 1 and l.startswith('A') and l[1] == ' ')])
|
||||||
modified = len([l for l in lines if l.startswith('M ') or (len(l) > 1 and l[1] == 'M')])
|
modified = len([l for l in lines if l.startswith('M ') or (len(l) > 1 and l[1] == 'M')])
|
||||||
deleted = len([l for l in lines if l.startswith('D ') or (len(l) > 1 and l[1] == 'D')])
|
deleted = len([l for l in lines if l.startswith('D ') or (len(l) > 1 and l[1] == 'D')])
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -324,7 +324,7 @@ class Liugin:
|
|||||||
lines.append(f"{indent}{prefix}{type_part} {name_part}{id_part}{bounds_part}{state_str}")
|
lines.append(f"{indent}{prefix}{type_part} {name_part}{id_part}{bounds_part}{state_str}")
|
||||||
|
|
||||||
children = element.FindAll(
|
children = element.FindAll(
|
||||||
self._uia.CreatePropertyCondition(30000, 0), # TreeScope_Children
|
2, # TreeScope_Children
|
||||||
self._uia.CreateTrueCondition()
|
self._uia.CreateTrueCondition()
|
||||||
)
|
)
|
||||||
if children:
|
if children:
|
||||||
@@ -570,7 +570,7 @@ class Liugin:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
children = self._root.FindAll(
|
children = self._root.FindAll(
|
||||||
self._uia.CreatePropertyCondition(30000, 2),
|
2, # TreeScope_Children
|
||||||
self._uia.CreateTrueCondition()
|
self._uia.CreateTrueCondition()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ class Liugin:
|
|||||||
if output:
|
if output:
|
||||||
# 提取纯ID保存到文件
|
# 提取纯ID保存到文件
|
||||||
ids = [line.strip().lstrip(' ') for line in result.split('\n')
|
ids = [line.strip().lstrip(' ') for line in result.split('\n')
|
||||||
if line.strip() and not any(c in line for c in [' ', '️', '', '❄️', ''])]
|
if line.strip() and not any(c in line for c in [' ', '️', '❄️'])]
|
||||||
try:
|
try:
|
||||||
with open(output, 'w', encoding='utf-8') as f:
|
with open(output, 'w', encoding='utf-8') as f:
|
||||||
f.write('\n'.join(ids))
|
f.write('\n'.join(ids))
|
||||||
|
|||||||
+96
-78
@@ -9,6 +9,14 @@ from colorama import Fore, Style
|
|||||||
class Liugin:
|
class Liugin:
|
||||||
"""网络工具插件"""
|
"""网络工具插件"""
|
||||||
|
|
||||||
|
# 统一 User-Agent(模拟浏览器,避免被反爬拦截)
|
||||||
|
_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||||
|
_HEADERS = {
|
||||||
|
'User-Agent': _UA,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.usage = """网络工具使用方法:/r
|
self.usage = """网络工具使用方法:/r
|
||||||
network_tools <操作> <参数>/r
|
network_tools <操作> <参数>/r
|
||||||
@@ -35,6 +43,7 @@ network_tools <操作> <参数>/r
|
|||||||
"""
|
"""
|
||||||
self.cli = None # CLI实例引用
|
self.cli = None # CLI实例引用
|
||||||
self.timeout = 10 # 请求超时时间(秒)
|
self.timeout = 10 # 请求超时时间(秒)
|
||||||
|
self.max_retries = 2 # 最大重试次数
|
||||||
|
|
||||||
def set_cli(self, cli):
|
def set_cli(self, cli):
|
||||||
"""设置CLI实例引用"""
|
"""设置CLI实例引用"""
|
||||||
@@ -161,8 +170,35 @@ JSON格式示例:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"网络工具操作错误: {str(e)}"
|
return f"网络工具操作错误: {str(e)}"
|
||||||
|
|
||||||
|
def _request(self, url, method='GET', timeout=None):
|
||||||
|
"""统一请求方法 — 带重试、UA、编码处理"""
|
||||||
|
timeout = timeout or self.timeout
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(self.max_retries + 1):
|
||||||
|
try:
|
||||||
|
response = requests.request(
|
||||||
|
method, url, timeout=timeout, headers=self._HEADERS,
|
||||||
|
allow_redirects=True
|
||||||
|
)
|
||||||
|
# 显式编码处理:优先用响应头声明的编码,兜底 UTF-8
|
||||||
|
if not response.encoding or response.encoding.lower() == 'iso-8859-1':
|
||||||
|
response.encoding = response.apparent_encoding or 'utf-8'
|
||||||
|
return response
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
last_error = f"请求超时 (>{timeout}秒)"
|
||||||
|
if attempt < self.max_retries:
|
||||||
|
time.sleep(1)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
last_error = "连接失败 (无法连接到服务器)"
|
||||||
|
if attempt < self.max_retries:
|
||||||
|
time.sleep(1)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
last_error = str(e)
|
||||||
|
break
|
||||||
|
return last_error
|
||||||
|
|
||||||
def _ping(self, target):
|
def _ping(self, target):
|
||||||
"""测试网站连通性和延迟"""
|
"""测试网站连通性和延迟(单次请求,不重试,保证延迟测量准确)"""
|
||||||
try:
|
try:
|
||||||
# 确保URL格式正确
|
# 确保URL格式正确
|
||||||
if not target.startswith(('http://', 'https://')):
|
if not target.startswith(('http://', 'https://')):
|
||||||
@@ -170,12 +206,19 @@ JSON格式示例:
|
|||||||
else:
|
else:
|
||||||
test_url = target
|
test_url = target
|
||||||
|
|
||||||
# 添加协议如果缺失
|
# 单次请求测量延迟(不使用重试,避免重试干扰延迟数值)
|
||||||
if not test_url.startswith(('http://', 'https://')):
|
|
||||||
test_url = 'http://' + test_url
|
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = requests.get(test_url, timeout=self.timeout)
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
test_url, timeout=self.timeout,
|
||||||
|
headers=self._HEADERS, allow_redirects=True
|
||||||
|
)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return f".ping 测试结果:\n目标: {target}\n错误: 请求超时 (>{self.timeout}秒)\n连接状态: 失败"
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return f".ping 测试结果:\n目标: {target}\n错误: 连接失败 (无法连接到服务器)\n连接状态: 失败"
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return f".ping 测试结果:\n目标: {target}\n错误: {str(e)}\n连接状态: 失败"
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
|
|
||||||
response_time = (end_time - start_time) * 1000 # 转换为毫秒
|
response_time = (end_time - start_time) * 1000 # 转换为毫秒
|
||||||
@@ -191,98 +234,73 @@ JSON格式示例:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return f".ping 测试结果:\n目标: {target}\n错误: 请求超时 (>{self.timeout}秒)\n连接状态: 失败"
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
return f".ping 测试结果:\n目标: {target}\n错误: 连接失败 (无法连接到服务器)\n连接状态: 失败"
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f".ping 测试结果:\n目标: {target}\n错误: {str(e)}\n连接状态: 失败"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f".ping 测试结果:\n目标: {target}\n错误: {str(e)}\n连接状态: 失败"
|
return f".ping 测试结果:\n目标: {target}\n错误: {str(e)}\n连接状态: 失败"
|
||||||
|
|
||||||
def _get_content(self, url):
|
def _get_content(self, url):
|
||||||
"""获取网页内容"""
|
"""获取网页内容"""
|
||||||
try:
|
# 确保URL格式正确
|
||||||
# 确保URL格式正确
|
if not url.startswith(('http://', 'https://')):
|
||||||
if not url.startswith(('http://', 'https://')):
|
url = 'https://' + url
|
||||||
url = 'https://' + url
|
|
||||||
|
|
||||||
response = requests.get(url, timeout=self.timeout)
|
response = self._request(url)
|
||||||
|
# _request 返回字符串表示错误
|
||||||
|
if isinstance(response, str):
|
||||||
|
return f"获取网页内容失败: {response}"
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
content = response.text
|
content = response.text
|
||||||
# 限制内容长度以避免过长输出
|
# 限制内容长度以避免过长输出
|
||||||
if len(content) > 2000:
|
if len(content) > 2000:
|
||||||
content = content[:2000] + f"\n... (内容已截断,完整内容共{len(content)}字符)"
|
content = content[:2000] + f"\n... (内容已截断,完整内容共{len(content)}字符)"
|
||||||
|
|
||||||
result = f"网页内容 (来自 {url}):\n"
|
result = f"网页内容 (来自 {url}):\n"
|
||||||
result += content
|
result += content
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
return f"获取网页内容失败,状态码: {response.status_code}, 原因: {response.reason if hasattr(response, 'reason') else ''}"
|
return f"获取网页内容失败,状态码: {response.status_code}, 原因: {response.reason if hasattr(response, 'reason') else ''}"
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return f"获取网页内容超时 (>{self.timeout}秒)"
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
return f"无法连接到 {url},请检查URL是否正确"
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"请求失败: {str(e)}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"获取网页内容时发生错误: {str(e)}"
|
|
||||||
|
|
||||||
def _check_status(self, url):
|
def _check_status(self, url):
|
||||||
"""检查网站状态码"""
|
"""检查网站状态码"""
|
||||||
try:
|
# 确保URL格式正确
|
||||||
# 确保URL格式正确
|
if not url.startswith(('http://', 'https://')):
|
||||||
if not url.startswith(('http://', 'https://')):
|
url = 'https://' + url
|
||||||
url = 'https://' + url
|
|
||||||
|
|
||||||
response = requests.get(url, timeout=self.timeout)
|
response = self._request(url)
|
||||||
|
if isinstance(response, str):
|
||||||
|
return f"检查网站状态失败: {response}"
|
||||||
|
|
||||||
result = f"网站状态 (来自 {url}):\n"
|
result = f"网站状态 (来自 {url}):\n"
|
||||||
result += f"HTTP状态码: {response.status_code}\n"
|
result += f"HTTP状态码: {response.status_code}\n"
|
||||||
result += f"状态描述: {response.reason if hasattr(response, 'reason') else 'Unknown'}\n"
|
result += f"状态描述: {response.reason if hasattr(response, 'reason') else 'Unknown'}\n"
|
||||||
result += f"响应时间: {response.elapsed.total_seconds():.2f} 秒\n"
|
result += f"响应时间: {response.elapsed.total_seconds():.2f} 秒\n"
|
||||||
result += f"内容长度: {len(response.content)} 字节\n"
|
result += f"内容长度: {len(response.content)} 字节\n"
|
||||||
result += f"服务器: {response.headers.get('Server', '未知')}\n"
|
result += f"服务器: {response.headers.get('Server', '未知')}\n"
|
||||||
result += f"内容类型: {response.headers.get('Content-Type', '未知')}\n"
|
result += f"内容类型: {response.headers.get('Content-Type', '未知')}\n"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return f"检查网站状态超时 (>{self.timeout}秒)"
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
return f"无法连接到 {url},请检查URL是否正确"
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"请求失败: {str(e)}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"检查网站状态时发生错误: {str(e)}"
|
|
||||||
|
|
||||||
def _get_headers(self, url):
|
def _get_headers(self, url):
|
||||||
"""获取网站响应头"""
|
"""获取网站响应头(优先 HEAD 请求,失败则回退 GET)"""
|
||||||
try:
|
# 确保URL格式正确
|
||||||
# 确保URL格式正确
|
if not url.startswith(('http://', 'https://')):
|
||||||
if not url.startswith(('http://', 'https://')):
|
url = 'https://' + url
|
||||||
url = 'https://' + url
|
|
||||||
|
|
||||||
response = requests.get(url, timeout=self.timeout)
|
# 优先用 HEAD(更高效),失败则回退到 GET
|
||||||
|
response = self._request(url, method='HEAD')
|
||||||
|
if isinstance(response, str):
|
||||||
|
# HEAD 失败(可能服务器不支持),回退到 GET
|
||||||
|
response = self._request(url, method='GET')
|
||||||
|
if isinstance(response, str):
|
||||||
|
return f"获取响应头失败: {response}"
|
||||||
|
|
||||||
headers = dict(response.headers)
|
headers = dict(response.headers)
|
||||||
|
|
||||||
result = f"响应头 (来自 {url}):\n"
|
result = f"响应头 (来自 {url}):\n"
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
result += f"{key}: {value}\n"
|
result += f"{key}: {value}\n"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return f"获取响应头超时 (>{self.timeout}秒)"
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
return f"无法连接到 {url},请检查URL是否正确"
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"请求失败: {str(e)}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"获取响应头时发生错误: {str(e)}"
|
|
||||||
|
|
||||||
def _get_ip(self, domain):
|
def _get_ip(self, domain):
|
||||||
"""获取域名对应的IP地址"""
|
"""获取域名对应的IP地址"""
|
||||||
|
|||||||
+31
-11
@@ -295,17 +295,37 @@ JSON格式示例:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 打印醒目提醒
|
# 打印醒目提醒(TUI 兼容)
|
||||||
print()
|
is_tui = self.cli and getattr(self.cli, 'tui_output_callback', None)
|
||||||
print(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
|
is_generating = self.cli and getattr(self.cli, 'tui_stream_callback', None)
|
||||||
print(f"{Fore.YELLOW} ⏰ 定时任务提醒 #{task_id}{Style.RESET_ALL}")
|
|
||||||
print(f"{Fore.YELLOW}{'─' * 50}{Style.RESET_ALL}")
|
if is_tui and not is_generating:
|
||||||
print(f" {Fore.CYAN}任务:{Style.RESET_ALL} {message}")
|
# TUI 模式且未在生成中:通过回调输出
|
||||||
print(f" {Fore.CYAN}时间:{Style.RESET_ALL} {now.strftime('%H:%M:%S')}")
|
lines = [
|
||||||
if is_repeat:
|
"",
|
||||||
next_str = (now + timedelta(seconds=repeat_seconds)).strftime("%Y-%m-%d %H:%M:%S")
|
f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}",
|
||||||
print(f" {Fore.CYAN}下次:{Style.RESET_ALL} {next_str}")
|
f"{Fore.YELLOW} ⏰ 定时任务提醒 #{task_id}{Style.RESET_ALL}",
|
||||||
print(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
|
f"{Fore.YELLOW}{'─' * 50}{Style.RESET_ALL}",
|
||||||
|
f" {Fore.CYAN}任务:{Style.RESET_ALL} {message}",
|
||||||
|
f" {Fore.CYAN}时间:{Style.RESET_ALL} {now.strftime('%H:%M:%S')}",
|
||||||
|
]
|
||||||
|
if is_repeat:
|
||||||
|
next_str = (now + timedelta(seconds=repeat_seconds)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
lines.append(f" {Fore.CYAN}下次:{Style.RESET_ALL} {next_str}")
|
||||||
|
lines.append(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
|
||||||
|
self.cli.tui_output_callback("\n".join(lines))
|
||||||
|
elif not is_generating:
|
||||||
|
# CLI 模式
|
||||||
|
print()
|
||||||
|
print(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
|
||||||
|
print(f"{Fore.YELLOW} ⏰ 定时任务提醒 #{task_id}{Style.RESET_ALL}")
|
||||||
|
print(f"{Fore.YELLOW}{'─' * 50}{Style.RESET_ALL}")
|
||||||
|
print(f" {Fore.CYAN}任务:{Style.RESET_ALL} {message}")
|
||||||
|
print(f" {Fore.CYAN}时间:{Style.RESET_ALL} {now.strftime('%H:%M:%S')}")
|
||||||
|
if is_repeat:
|
||||||
|
next_str = (now + timedelta(seconds=repeat_seconds)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
print(f" {Fore.CYAN}下次:{Style.RESET_ALL} {next_str}")
|
||||||
|
print(f"{Fore.YELLOW}{'═' * 50}{Style.RESET_ALL}")
|
||||||
|
|
||||||
# 自动激活 AI — 将提醒注入对话
|
# 自动激活 AI — 将提醒注入对话
|
||||||
self._activate_ai(task_id, message)
|
self._activate_ai(task_id, message)
|
||||||
|
|||||||
@@ -83,7 +83,24 @@ class Liugin:
|
|||||||
def convert_mcp_args(self, arguments):
|
def convert_mcp_args(self, arguments):
|
||||||
op = arguments.get("operation", "")
|
op = arguments.get("operation", "")
|
||||||
name = arguments.get("name", "")
|
name = arguments.get("name", "")
|
||||||
return f"{op} {name}".strip()
|
parts = [op]
|
||||||
|
if name:
|
||||||
|
parts.append(name)
|
||||||
|
if arguments.get("keyword"):
|
||||||
|
parts.append(arguments["keyword"])
|
||||||
|
if arguments.get("file"):
|
||||||
|
parts.append(arguments["file"])
|
||||||
|
if arguments.get("count"):
|
||||||
|
parts.append(str(arguments["count"]))
|
||||||
|
if arguments.get("language"):
|
||||||
|
parts.extend(["-l", arguments["language"]])
|
||||||
|
if arguments.get("code"):
|
||||||
|
parts.extend(["-c", f'"{arguments["code"]}"'])
|
||||||
|
if arguments.get("tags"):
|
||||||
|
parts.extend(["-t", arguments["tags"]])
|
||||||
|
if arguments.get("description"):
|
||||||
|
parts.extend(["-d", f'"{arguments["description"]}"'])
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
def _load_snippets(self):
|
def _load_snippets(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
+25
-29
@@ -319,18 +319,29 @@ class Liugin:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _find_agent(self, agent_id: str):
|
||||||
|
"""线程安全地查找 Agent(精确匹配 + 模糊匹配)"""
|
||||||
|
with self._lock:
|
||||||
|
agent = self.agents.get(agent_id)
|
||||||
|
if not agent:
|
||||||
|
matches = [a for a in self.agents.values() if agent_id in a.id or agent_id == a.name]
|
||||||
|
if matches:
|
||||||
|
agent = matches[0]
|
||||||
|
return agent
|
||||||
|
|
||||||
def _list(self, args: str) -> str:
|
def _list(self, args: str) -> str:
|
||||||
"""列出所有子 Agent"""
|
"""列出所有子 Agent"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if not self.agents:
|
if not self.agents:
|
||||||
return "没有子 Agent"
|
return "没有子 Agent"
|
||||||
|
agents_list = list(self.agents.values())
|
||||||
|
|
||||||
result = [f"子 Agent 列表 ({len(self.agents)} 个):\n"]
|
result = [f"子 Agent 列表 ({len(agents_list)} 个):\n"]
|
||||||
|
|
||||||
# 按状态分组
|
# 按状态分组
|
||||||
running = [a for a in self.agents.values() if a.status == "running"]
|
running = [a for a in agents_list if a.status == "running"]
|
||||||
done = [a for a in self.agents.values() if a.status == "done"]
|
done = [a for a in agents_list if a.status == "done"]
|
||||||
failed = [a for a in self.agents.values() if a.status == "failed"]
|
failed = [a for a in agents_list if a.status == "failed"]
|
||||||
|
|
||||||
if running:
|
if running:
|
||||||
result.append(f" 运行中 ({len(running)}):")
|
result.append(f" 运行中 ({len(running)}):")
|
||||||
@@ -355,14 +366,9 @@ class Liugin:
|
|||||||
if not agent_id:
|
if not agent_id:
|
||||||
return "错误:请提供 Agent ID"
|
return "错误:请提供 Agent ID"
|
||||||
|
|
||||||
agent = self.agents.get(agent_id)
|
agent = self._find_agent(agent_id)
|
||||||
if not agent:
|
if not agent:
|
||||||
# 模糊匹配
|
return f"错误:未找到 Agent '{agent_id}'"
|
||||||
matches = [a for a in self.agents.values() if agent_id in a.id or agent_id == a.name]
|
|
||||||
if matches:
|
|
||||||
agent = matches[0]
|
|
||||||
else:
|
|
||||||
return f"错误:未找到 Agent '{agent_id}'"
|
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"Agent: {agent.id}",
|
f"Agent: {agent.id}",
|
||||||
@@ -386,13 +392,9 @@ class Liugin:
|
|||||||
if not agent_id:
|
if not agent_id:
|
||||||
return "错误:请提供 Agent ID"
|
return "错误:请提供 Agent ID"
|
||||||
|
|
||||||
agent = self.agents.get(agent_id)
|
agent = self._find_agent(agent_id)
|
||||||
if not agent:
|
if not agent:
|
||||||
matches = [a for a in self.agents.values() if agent_id in a.id or agent_id == a.name]
|
return f"错误:未找到 Agent '{agent_id}'"
|
||||||
if matches:
|
|
||||||
agent = matches[0]
|
|
||||||
else:
|
|
||||||
return f"错误:未找到 Agent '{agent_id}'"
|
|
||||||
|
|
||||||
if agent.status == "running":
|
if agent.status == "running":
|
||||||
return f"Agent {agent.id} 仍在运行中 (步骤 {agent.steps})"
|
return f"Agent {agent.id} 仍在运行中 (步骤 {agent.steps})"
|
||||||
@@ -417,13 +419,9 @@ class Liugin:
|
|||||||
agent_id = parts[0]
|
agent_id = parts[0]
|
||||||
message = parts[1]
|
message = parts[1]
|
||||||
|
|
||||||
agent = self.agents.get(agent_id)
|
agent = self._find_agent(agent_id)
|
||||||
if not agent:
|
if not agent:
|
||||||
matches = [a for a in self.agents.values() if agent_id in a.id or agent_id == a.name]
|
return f"错误:未找到 Agent '{agent_id}'"
|
||||||
if matches:
|
|
||||||
agent = matches[0]
|
|
||||||
else:
|
|
||||||
return f"错误:未找到 Agent '{agent_id}'"
|
|
||||||
|
|
||||||
if agent.status != "running":
|
if agent.status != "running":
|
||||||
return f"Agent {agent.id} 未在运行 (状态: {agent.status})"
|
return f"Agent {agent.id} 未在运行 (状态: {agent.status})"
|
||||||
@@ -467,17 +465,15 @@ class Liugin:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
agent.error = str(e)
|
agent.error = str(e)
|
||||||
|
agent.status = "failed"
|
||||||
|
agent.finished_at = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
def _kill(self, args: str) -> str:
|
def _kill(self, args: str) -> str:
|
||||||
"""终止子 Agent"""
|
"""终止子 Agent"""
|
||||||
agent_id = args.strip()
|
agent_id = args.strip()
|
||||||
agent = self.agents.get(agent_id)
|
agent = self._find_agent(agent_id)
|
||||||
if not agent:
|
if not agent:
|
||||||
matches = [a for a in self.agents.values() if agent_id in a.id or agent_id == a.name]
|
return f"错误:未找到 Agent '{agent_id}'"
|
||||||
if matches:
|
|
||||||
agent = matches[0]
|
|
||||||
else:
|
|
||||||
return f"错误:未找到 Agent '{agent_id}'"
|
|
||||||
|
|
||||||
agent.status = "failed"
|
agent.status = "failed"
|
||||||
agent.error = "被主 Agent 终止"
|
agent.error = "被主 Agent 终止"
|
||||||
|
|||||||
+26
-19
@@ -189,6 +189,13 @@ class WorkflowEngine:
|
|||||||
self.state_dir = os.path.join(self.workflows_dir, ".state")
|
self.state_dir = os.path.join(self.workflows_dir, ".state")
|
||||||
os.makedirs(self.state_dir, exist_ok=True)
|
os.makedirs(self.state_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def _out(self, msg):
|
||||||
|
"""TUI 兼容输出"""
|
||||||
|
if self.cli and getattr(self.cli, 'tui_output_callback', None):
|
||||||
|
self.cli.tui_output_callback(msg)
|
||||||
|
else:
|
||||||
|
print(msg)
|
||||||
|
|
||||||
def run(self, path: str, args: dict = None) -> str:
|
def run(self, path: str, args: dict = None) -> str:
|
||||||
"""执行工作流"""
|
"""执行工作流"""
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
@@ -226,8 +233,8 @@ class WorkflowEngine:
|
|||||||
'started_at': context['workflow']['started_at'],
|
'started_at': context['workflow']['started_at'],
|
||||||
})
|
})
|
||||||
|
|
||||||
print(f"\n{Fore.CYAN}🔄 工作流 '{wf_name}' 开始 (ID: {wf_id}){Style.RESET_ALL}")
|
self._out(f"\n{Fore.CYAN}🔄 工作流 '{wf_name}' 开始 (ID: {wf_id}){Style.RESET_ALL}")
|
||||||
print(f" 共 {len(steps)} 个步骤\n")
|
self._out(f" 共 {len(steps)} 个步骤\n")
|
||||||
|
|
||||||
failed = False
|
failed = False
|
||||||
for i, step in enumerate(steps):
|
for i, step in enumerate(steps):
|
||||||
@@ -240,7 +247,7 @@ class WorkflowEngine:
|
|||||||
if condition:
|
if condition:
|
||||||
resolved_condition = _resolve_vars(condition, context)
|
resolved_condition = _resolve_vars(condition, context)
|
||||||
if not self._eval_condition(resolved_condition):
|
if not self._eval_condition(resolved_condition):
|
||||||
print(f" {Fore.YELLOW}⏭ [{step_name}] 条件不满足,跳过{Style.RESET_ALL}")
|
self._out(f" {Fore.YELLOW}⏭ [{step_name}] 条件不满足,跳过{Style.RESET_ALL}")
|
||||||
context['steps'][step_name] = {
|
context['steps'][step_name] = {
|
||||||
'output': '', 'exit_code': 0, 'skipped': True
|
'output': '', 'exit_code': 0, 'skipped': True
|
||||||
}
|
}
|
||||||
@@ -249,7 +256,7 @@ class WorkflowEngine:
|
|||||||
# 解析步骤参数
|
# 解析步骤参数
|
||||||
resolved_step = _resolve_value(step, context)
|
resolved_step = _resolve_value(step, context)
|
||||||
|
|
||||||
print(f" {Fore.CYAN}▶ [{step_name}] {step_type}{Style.RESET_ALL}")
|
self._out(f" {Fore.CYAN}▶ [{step_name}] {step_type}{Style.RESET_ALL}")
|
||||||
|
|
||||||
# 更新状态
|
# 更新状态
|
||||||
self._save_state(wf_id, {
|
self._save_state(wf_id, {
|
||||||
@@ -268,29 +275,29 @@ class WorkflowEngine:
|
|||||||
|
|
||||||
if result.get('exit_code', 0) == 0:
|
if result.get('exit_code', 0) == 0:
|
||||||
output_preview = str(result.get('output', ''))[:100]
|
output_preview = str(result.get('output', ''))[:100]
|
||||||
print(f" {Fore.GREEN}✓ [{step_name}] 完成{Style.RESET_ALL}"
|
self._out(f" {Fore.GREEN}✓ [{step_name}] 完成{Style.RESET_ALL}"
|
||||||
f"{' → ' + output_preview if output_preview else ''}")
|
f"{' → ' + output_preview if output_preview else ''}")
|
||||||
else:
|
else:
|
||||||
failed = True
|
failed = True
|
||||||
print(f" {Fore.RED}✗ [{step_name}] 失败 (exit={result.get('exit_code')}){Style.RESET_ALL}")
|
self._out(f" {Fore.RED}✗ [{step_name}] 失败 (exit={result.get('exit_code')}){Style.RESET_ALL}")
|
||||||
print(f" {result.get('error', result.get('output', ''))[:200]}")
|
self._out(f" {result.get('error', result.get('output', ''))[:200]}")
|
||||||
|
|
||||||
if not continue_on_error:
|
if not continue_on_error:
|
||||||
# 检查 on_fail 分支
|
# 检查 on_fail 分支
|
||||||
on_fail = resolved_step.get('on_fail')
|
on_fail = resolved_step.get('on_fail')
|
||||||
if on_fail:
|
if on_fail:
|
||||||
print(f" {Fore.YELLOW}→ 执行 on_fail 分支{Style.RESET_ALL}")
|
self._out(f" {Fore.YELLOW}→ 执行 on_fail 分支{Style.RESET_ALL}")
|
||||||
self._execute_on_fail(on_fail, context)
|
self._execute_on_fail(on_fail, context)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(f" {Fore.YELLOW}⚠ continue_on_error=true,继续{Style.RESET_ALL}")
|
self._out(f" {Fore.YELLOW}⚠ continue_on_error=true,继续{Style.RESET_ALL}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed = True
|
failed = True
|
||||||
context['steps'][step_name] = {
|
context['steps'][step_name] = {
|
||||||
'output': '', 'exit_code': 1, 'error': str(e)
|
'output': '', 'exit_code': 1, 'error': str(e)
|
||||||
}
|
}
|
||||||
print(f" {Fore.RED}✗ [{step_name}] 异常: {e}{Style.RESET_ALL}")
|
self._out(f" {Fore.RED}✗ [{step_name}] 异常: {e}{Style.RESET_ALL}")
|
||||||
if not continue_on_error:
|
if not continue_on_error:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -310,7 +317,7 @@ class WorkflowEngine:
|
|||||||
})
|
})
|
||||||
|
|
||||||
icon = '✅' if not failed else '❌'
|
icon = '✅' if not failed else '❌'
|
||||||
print(f"\n{icon} 工作流 '{wf_name}' {status} (ID: {wf_id})")
|
self._out(f"\n{icon} 工作流 '{wf_name}' {status} (ID: {wf_id})")
|
||||||
return f"{icon} 工作流 '{wf_name}' {status}"
|
return f"{icon} 工作流 '{wf_name}' {status}"
|
||||||
|
|
||||||
def resume(self, wf_id: str) -> str:
|
def resume(self, wf_id: str) -> str:
|
||||||
@@ -335,7 +342,7 @@ class WorkflowEngine:
|
|||||||
start_from = state.get('current_step', 0)
|
start_from = state.get('current_step', 0)
|
||||||
context = state.get('context', {})
|
context = state.get('context', {})
|
||||||
|
|
||||||
print(f"\n{Fore.CYAN}🔄 恢复工作流 '{wf.get('name', wf_id)}' 从步骤 {start_from + 1}{Style.RESET_ALL}")
|
self._out(f"\n{Fore.CYAN}🔄 恢复工作流 '{wf.get('name', wf_id)}' 从步骤 {start_from + 1}{Style.RESET_ALL}")
|
||||||
|
|
||||||
failed = False
|
failed = False
|
||||||
for i in range(start_from, len(steps)):
|
for i in range(start_from, len(steps)):
|
||||||
@@ -345,23 +352,23 @@ class WorkflowEngine:
|
|||||||
continue_on_error = step.get('continue_on_error', False)
|
continue_on_error = step.get('continue_on_error', False)
|
||||||
|
|
||||||
resolved_step = _resolve_value(step, context)
|
resolved_step = _resolve_value(step, context)
|
||||||
print(f" {Fore.CYAN}▶ [{step_name}] {step_type}{Style.RESET_ALL}")
|
self._out(f" {Fore.CYAN}▶ [{step_name}] {step_type}{Style.RESET_ALL}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._execute_step(resolved_step, context)
|
result = self._execute_step(resolved_step, context)
|
||||||
context['steps'][step_name] = result
|
context['steps'][step_name] = result
|
||||||
|
|
||||||
if result.get('exit_code', 0) == 0:
|
if result.get('exit_code', 0) == 0:
|
||||||
print(f" {Fore.GREEN}✓ [{step_name}] 完成{Style.RESET_ALL}")
|
self._out(f" {Fore.GREEN}✓ [{step_name}] 完成{Style.RESET_ALL}")
|
||||||
else:
|
else:
|
||||||
failed = True
|
failed = True
|
||||||
print(f" {Fore.RED}✗ [{step_name}] 失败{Style.RESET_ALL}")
|
self._out(f" {Fore.RED}✗ [{step_name}] 失败{Style.RESET_ALL}")
|
||||||
if not continue_on_error:
|
if not continue_on_error:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed = True
|
failed = True
|
||||||
context['steps'][step_name] = {'output': '', 'exit_code': 1, 'error': str(e)}
|
context['steps'][step_name] = {'output': '', 'exit_code': 1, 'error': str(e)}
|
||||||
print(f" {Fore.RED}✗ [{step_name}] 异常: {e}{Style.RESET_ALL}")
|
self._out(f" {Fore.RED}✗ [{step_name}] 异常: {e}{Style.RESET_ALL}")
|
||||||
if not continue_on_error:
|
if not continue_on_error:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -553,7 +560,7 @@ class WorkflowEngine:
|
|||||||
"""打印步骤"""
|
"""打印步骤"""
|
||||||
message = step.get('message', step.get('text', ''))
|
message = step.get('message', step.get('text', ''))
|
||||||
resolved = _resolve_vars(str(message), context)
|
resolved = _resolve_vars(str(message), context)
|
||||||
print(f" 💬 {resolved}")
|
self._out(f" 💬 {resolved}")
|
||||||
return {'output': resolved, 'exit_code': 0}
|
return {'output': resolved, 'exit_code': 0}
|
||||||
|
|
||||||
def _exec_wait_step(self, step: dict, context: dict) -> dict:
|
def _exec_wait_step(self, step: dict, context: dict) -> dict:
|
||||||
@@ -622,7 +629,7 @@ class WorkflowEngine:
|
|||||||
def _execute_on_fail(self, on_fail, context: dict):
|
def _execute_on_fail(self, on_fail, context: dict):
|
||||||
"""执行 on_fail 处理"""
|
"""执行 on_fail 处理"""
|
||||||
if isinstance(on_fail, str):
|
if isinstance(on_fail, str):
|
||||||
print(f" 💬 {on_fail}")
|
self._out(f" 💬 {on_fail}")
|
||||||
elif isinstance(on_fail, list):
|
elif isinstance(on_fail, list):
|
||||||
for step in on_fail:
|
for step in on_fail:
|
||||||
self._execute_step(step, context)
|
self._execute_step(step, context)
|
||||||
@@ -636,7 +643,7 @@ class WorkflowEngine:
|
|||||||
with open(path, 'w', encoding='utf-8') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(state, f, ensure_ascii=False, indent=2, default=str)
|
json.dump(state, f, ensure_ascii=False, indent=2, default=str)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{Fore.YELLOW}⚠ 保存状态失败: {e}{Style.RESET_ALL}")
|
self._out(f"{Fore.YELLOW}⚠ 保存状态失败: {e}{Style.RESET_ALL}")
|
||||||
|
|
||||||
def _load_state(self, wf_id: str) -> dict:
|
def _load_state(self, wf_id: str) -> dict:
|
||||||
"""加载工作流状态"""
|
"""加载工作流状态"""
|
||||||
|
|||||||
+19
-21
@@ -1,27 +1,25 @@
|
|||||||
# 小狸插件配置
|
# 小狸插件配置 - 由启动器自动生成
|
||||||
|
|
||||||
# ── 必备生产力 ──
|
|
||||||
ENABLE_AI_SEARCH = True
|
ENABLE_AI_SEARCH = True
|
||||||
ENABLE_CMD_EXECUTOR = True
|
|
||||||
ENABLE_FILE_MANAGER = True
|
|
||||||
ENABLE_CODE_EDITOR = True
|
|
||||||
ENABLE_GIT_TOOLS = True
|
|
||||||
ENABLE_FRONTEND_TESTER = True
|
|
||||||
ENABLE_NETWORK_TOOLS = True
|
|
||||||
ENABLE_SCHEDULER = True
|
|
||||||
ENABLE_SPEECH_RECOGNITION = True
|
|
||||||
ENABLE_TASK_MANAGER = True
|
|
||||||
ENABLE_TOOL_SEARCH = True
|
|
||||||
ENABLE_AUTO_ENGINEER = True
|
ENABLE_AUTO_ENGINEER = True
|
||||||
ENABLE_TERMINAL = True
|
|
||||||
ENABLE_BROWSER_AUTO = True
|
ENABLE_BROWSER_AUTO = True
|
||||||
|
ENABLE_CMD_EXECUTOR = True
|
||||||
|
ENABLE_CODE_EDITOR = True
|
||||||
|
ENABLE_FILE_MANAGER = True
|
||||||
|
ENABLE_FRONTEND_TESTER = True
|
||||||
|
ENABLE_GIT_TOOLS = True
|
||||||
ENABLE_GUI_AUTO = True
|
ENABLE_GUI_AUTO = True
|
||||||
ENABLE_SUB_AGENT = True
|
|
||||||
ENABLE_WORKFLOW = True
|
|
||||||
ENABLE_MEMORY_PLUGIN = True
|
|
||||||
ENABLE_JSON_TOOLKIT = True
|
|
||||||
ENABLE_REGEX_TESTER = True
|
|
||||||
ENABLE_SNIPPET_MANAGER = True
|
|
||||||
ENABLE_MARKDOWN_TOOL = True
|
|
||||||
ENABLE_ID_GENERATOR = True
|
ENABLE_ID_GENERATOR = True
|
||||||
ENABLE_IMAGE_READER = True
|
ENABLE_IMAGE_READER = True
|
||||||
|
ENABLE_JSON_TOOLKIT = True
|
||||||
|
ENABLE_MARKDOWN_TOOL = True
|
||||||
|
ENABLE_MEMORY_PLUGIN = True
|
||||||
|
ENABLE_NETWORK_TOOLS = True
|
||||||
|
ENABLE_REGEX_TESTER = True
|
||||||
|
ENABLE_SCHEDULER = True
|
||||||
|
ENABLE_SNIPPET_MANAGER = True
|
||||||
|
ENABLE_SPEECH_RECOGNITION = True
|
||||||
|
ENABLE_SUB_AGENT = True
|
||||||
|
ENABLE_TASK_MANAGER = True
|
||||||
|
ENABLE_TERMINAL = True
|
||||||
|
ENABLE_TOOL_SEARCH = True
|
||||||
|
ENABLE_WORKFLOW = True
|
||||||
+11
-29
@@ -1,30 +1,12 @@
|
|||||||
# 小狸 Pro-CLI 依赖库
|
# 小狸 Pro-CLI 依赖
|
||||||
# 修复后的包名(使用 pip 安装名)
|
# 生成于 2026-06-26 20:58
|
||||||
|
|
||||||
# 核心依赖
|
PIL
|
||||||
colorama>=0.4.6
|
ascii_magic
|
||||||
openai>=1.0.0
|
colorama
|
||||||
requests>=2.28.0
|
cv2
|
||||||
ollama>=0.1.0
|
numpy
|
||||||
|
ollama
|
||||||
# 图像处理
|
openai
|
||||||
Pillow>=9.0.0
|
requests
|
||||||
opencv-python>=4.5.0
|
tkinter
|
||||||
numpy>=1.21.0
|
|
||||||
ascii-magic>=2.0.0
|
|
||||||
|
|
||||||
# 数据处理
|
|
||||||
pandas>=1.3.0
|
|
||||||
scipy>=1.7.0
|
|
||||||
|
|
||||||
# 音频
|
|
||||||
pygame>=2.0.0
|
|
||||||
|
|
||||||
# TUI 模式
|
|
||||||
textual>=0.40.0
|
|
||||||
|
|
||||||
# WebSocket 远程
|
|
||||||
websockets>=11.0
|
|
||||||
|
|
||||||
# 类型提示
|
|
||||||
typing-extensions>=4.0.0
|
|
||||||
@@ -380,17 +380,3 @@ class SkillLoader:
|
|||||||
else:
|
else:
|
||||||
definitions.append(skill.get_tool_definition())
|
definitions.append(skill.get_tool_definition())
|
||||||
return definitions
|
return definitions
|
||||||
|
|
||||||
def get_skill(self, name: str) -> Optional[Union[Skill, MarkdownSkill]]:
|
|
||||||
"""获取已加载的技能"""
|
|
||||||
return self._skills.get(name)
|
|
||||||
|
|
||||||
def get_all_tool_definitions(self, format: str = "openai") -> List[Dict]:
|
|
||||||
"""获取所有技能的工具定义"""
|
|
||||||
definitions = []
|
|
||||||
for skill in self._skills.values():
|
|
||||||
if format == "anthropic":
|
|
||||||
definitions.append(skill.get_anthropic_tool_definition())
|
|
||||||
else:
|
|
||||||
definitions.append(skill.get_tool_definition())
|
|
||||||
return definitions
|
|
||||||
@@ -90,7 +90,8 @@ def read_file(file_path: str, encoding: str = "utf-8", lines: int = None) -> str
|
|||||||
break
|
break
|
||||||
content_lines.append(line.rstrip('\n\r'))
|
content_lines.append(line.rstrip('\n\r'))
|
||||||
content = '\n'.join(content_lines)
|
content = '\n'.join(content_lines)
|
||||||
total_lines = sum(1 for _ in open(file_path, 'r', encoding=encoding))
|
with open(file_path, 'r', encoding=encoding) as f2:
|
||||||
|
total_lines = sum(1 for _ in f2)
|
||||||
else:
|
else:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
total_lines = content.count('\n') + 1
|
total_lines = content.count('\n') + 1
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""测试 TUI 回车发送消息功能(改进版)"""
|
|
||||||
import pexpect
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def test_tui_send():
|
|
||||||
child = pexpect.spawn(
|
|
||||||
'python3 ai_cli.py',
|
|
||||||
encoding='utf-8',
|
|
||||||
timeout=30,
|
|
||||||
dimensions=(30, 100),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. 等待 diff 选择提示
|
|
||||||
child.expect('请选择')
|
|
||||||
print('[OK] 出现 diff 选择提示')
|
|
||||||
child.sendline('2')
|
|
||||||
|
|
||||||
# 2. 等待 CLI 提示符
|
|
||||||
child.expect('>', timeout=10)
|
|
||||||
print('[OK] CLI 就绪')
|
|
||||||
|
|
||||||
# 3. 发送 /tui
|
|
||||||
child.sendline('/tui')
|
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
# 4. 检查 TUI 启动
|
|
||||||
try:
|
|
||||||
child.expect('回车发送', timeout=5)
|
|
||||||
print('[OK] TUI 启动成功')
|
|
||||||
except pexpect.TIMEOUT:
|
|
||||||
print('[FAIL] TUI 未启动')
|
|
||||||
child.close()
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 5. 输入 hello 并回车
|
|
||||||
time.sleep(1)
|
|
||||||
child.send('hello')
|
|
||||||
time.sleep(0.5)
|
|
||||||
child.send('\r') # Enter
|
|
||||||
time.sleep(5) # 等待处理(ollama 可能超时)
|
|
||||||
|
|
||||||
# 6. 读取所有可用输出
|
|
||||||
full_output = ''
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
chunk = child.read_nonblocking(size=4096, timeout=1)
|
|
||||||
full_output += chunk
|
|
||||||
except (pexpect.TIMEOUT, pexpect.EOF):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 7. 检测结果
|
|
||||||
sent = False
|
|
||||||
|
|
||||||
# 检查是否出现用户消息 "hello"
|
|
||||||
if 'hello' in full_output:
|
|
||||||
print('[OK] 检测到 "hello" — 用户消息已显示')
|
|
||||||
sent = True
|
|
||||||
|
|
||||||
# 检查是否出现错误(ollama 未运行)
|
|
||||||
if 'Ollama' in full_output or '服务未运行' in full_output:
|
|
||||||
print('[OK] 收到 Ollama 错误 — 消息已发送并处理')
|
|
||||||
sent = True
|
|
||||||
|
|
||||||
# 检查是否出现 "思考中"
|
|
||||||
if '思考中' in full_output:
|
|
||||||
print('[OK] 看到 "思考中" — AI 正在处理')
|
|
||||||
sent = True
|
|
||||||
|
|
||||||
# 检查是否出现消息气泡标记
|
|
||||||
if ' ' in full_output: # 用户消息前缀
|
|
||||||
# 检查是否有新内容出现
|
|
||||||
lines = [l for l in full_output.split('\n') if l.strip()]
|
|
||||||
if len(lines) > 0:
|
|
||||||
print(f'[INFO] 捕获到 {len(lines)} 行输出')
|
|
||||||
|
|
||||||
if not sent:
|
|
||||||
# 最后检查:如果 TUI 仍在运行且输入框为空,说明消息被发送了
|
|
||||||
# (因为发送后会清空输入框)
|
|
||||||
print('[INFO] 未检测到明确结果,但 TUI 仍在运行')
|
|
||||||
# 认为通过:TUI 没有崩溃,说明逻辑正常
|
|
||||||
sent = True
|
|
||||||
|
|
||||||
# 退出
|
|
||||||
try:
|
|
||||||
child.send('\x03') # Ctrl+C
|
|
||||||
time.sleep(0.5)
|
|
||||||
child.send('\x03')
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
child.close()
|
|
||||||
return sent
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
result = test_tui_send()
|
|
||||||
print(f'\n结果: {"✅ 测试通过" if result else "❌ 测试失败"}')
|
|
||||||
sys.exit(0 if result else 1)
|
|
||||||
+20
-21
@@ -417,21 +417,17 @@ class LocalServer:
|
|||||||
image_engine = getattr(self.cli, 'image_engine', None)
|
image_engine = getattr(self.cli, 'image_engine', None)
|
||||||
if not image_engine:
|
if not image_engine:
|
||||||
try:
|
try:
|
||||||
from image_engine import get_image_engine
|
from image_engine import get_engine_manager
|
||||||
image_engine = get_image_engine()
|
image_engine = get_engine_manager()
|
||||||
except:
|
except:
|
||||||
return f"@{file_path}" # 没有图像引擎,直接返回路径
|
return f"@{file_path}" # 没有图像引擎,直接返回路径
|
||||||
|
|
||||||
if websocket:
|
if websocket:
|
||||||
await self._send_tool_status(websocket, "图像识别", text or "分析图片", "calling")
|
await self._send_tool_status(websocket, "图像识别", text or "分析图片", "calling")
|
||||||
|
|
||||||
# 读取图片
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
image_data = f.read()
|
|
||||||
|
|
||||||
# 调用图像识别
|
# 调用图像识别
|
||||||
try:
|
try:
|
||||||
result = image_engine.analyze(image_data, text or "请描述这张图片")
|
result = image_engine.analyze(file_path, text or "请描述这张图片")
|
||||||
|
|
||||||
if websocket:
|
if websocket:
|
||||||
await self._send_tool_status(websocket, "图像识别", text or "分析图片", "success", result[:200] if result else "无结果")
|
await self._send_tool_status(websocket, "图像识别", text or "分析图片", "success", result[:200] if result else "无结果")
|
||||||
@@ -638,9 +634,13 @@ class LocalServer:
|
|||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
s.connect(("8.8.8.8", 80))
|
try:
|
||||||
local_ip = s.getsockname()[0]
|
s.connect(("8.8.8.8", 80))
|
||||||
s.close()
|
local_ip = s.getsockname()[0]
|
||||||
|
except:
|
||||||
|
local_ip = "未知"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
except:
|
except:
|
||||||
local_ip = "未知"
|
local_ip = "未知"
|
||||||
|
|
||||||
@@ -657,9 +657,13 @@ class LocalServer:
|
|||||||
def get_status(self) -> dict:
|
def get_status(self) -> dict:
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
s.connect(("8.8.8.8", 80))
|
try:
|
||||||
local_ip = s.getsockname()[0]
|
s.connect(("8.8.8.8", 80))
|
||||||
s.close()
|
local_ip = s.getsockname()[0]
|
||||||
|
except:
|
||||||
|
local_ip = "未知"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
except:
|
except:
|
||||||
local_ip = "未知"
|
local_ip = "未知"
|
||||||
return {
|
return {
|
||||||
@@ -817,15 +821,12 @@ class ProxyClient:
|
|||||||
image_engine = getattr(self.cli, 'image_engine', None)
|
image_engine = getattr(self.cli, 'image_engine', None)
|
||||||
if not image_engine:
|
if not image_engine:
|
||||||
try:
|
try:
|
||||||
from image_engine import get_image_engine
|
from image_engine import get_engine_manager
|
||||||
image_engine = get_image_engine()
|
image_engine = get_engine_manager()
|
||||||
except:
|
except:
|
||||||
return f"@{file_path}"
|
return f"@{file_path}"
|
||||||
|
|
||||||
with open(file_path, 'rb') as f:
|
result = image_engine.analyze(file_path, text or "请描述这张图片")
|
||||||
image_data = f.read()
|
|
||||||
|
|
||||||
result = image_engine.analyze(image_data, text or "请描述这张图片")
|
|
||||||
return await self._process_message(f"@{file_path}", [], f"图片内容: {result}\n\n{text}" if text else f"图片内容: {result}")
|
return await self._process_message(f"@{file_path}", [], f"图片内容: {result}\n\n{text}" if text else f"图片内容: {result}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1134,8 +1135,6 @@ def start_proxy_client(cli_instance, server_host: str, server_port: int,
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return _proxy_client.start(server_host, server_port, server_password, pc_port, pc_password)
|
|
||||||
|
|
||||||
|
|
||||||
def stop_all():
|
def stop_all():
|
||||||
global _local_server, _proxy_client, websocket_server
|
global _local_server, _proxy_client, websocket_server
|
||||||
|
|||||||
+23
-19
@@ -95,8 +95,8 @@ class BaseAICLI:
|
|||||||
self.load_liugins(liugins_dir, skills_dir)
|
self.load_liugins(liugins_dir, skills_dir)
|
||||||
# 初始化共享对话历史
|
# 初始化共享对话历史
|
||||||
self.shared_conversation_history = []
|
self.shared_conversation_history = []
|
||||||
# 设置最大历史记录数
|
# 设置最大历史记录数 — 从 config 读取,未配置则用默认值
|
||||||
self.max_history = DEFAULT_MAX_HISTORY
|
self.max_history = get_system_config('max_history', DEFAULT_MAX_HISTORY)
|
||||||
# 为所有已加载的引擎设置共享对话历史
|
# 为所有已加载的引擎设置共享对话历史
|
||||||
self._set_shared_conversation_history()
|
self._set_shared_conversation_history()
|
||||||
# 初始化代码执行相关功能
|
# 初始化代码执行相关功能
|
||||||
@@ -115,6 +115,14 @@ class BaseAICLI:
|
|||||||
self._clawli_monitor_running = False
|
self._clawli_monitor_running = False
|
||||||
# TUI 输出回调(用于 TUI 模式下的实时输出)
|
# TUI 输出回调(用于 TUI 模式下的实时输出)
|
||||||
self.tui_output_callback = None
|
self.tui_output_callback = None
|
||||||
|
# TUI 流式回调(用于 TUI 模式下逐字流式输出)
|
||||||
|
self.tui_stream_callback = None
|
||||||
|
# TUI 思考回调(用于 TUI 模式下深度思考内容单独显示)
|
||||||
|
self.tui_thinking_callback = None
|
||||||
|
# TUI 钩子:每次调用 AI 前触发(重新显示思考指示器等)
|
||||||
|
self.tui_before_generate_callback = None
|
||||||
|
# TUI 确认回调(用于 TUI 模式下工具调用审查,返回 True/False)
|
||||||
|
self.tui_confirm_callback = None
|
||||||
# 初始化用户ID(基于机器硬件动态生成)
|
# 初始化用户ID(基于机器硬件动态生成)
|
||||||
self.user_id = str(uuid.getnode())
|
self.user_id = str(uuid.getnode())
|
||||||
|
|
||||||
@@ -319,6 +327,13 @@ class BaseAICLI:
|
|||||||
"""获取插件管理器"""
|
"""获取插件管理器"""
|
||||||
return self.liugin_manager
|
return self.liugin_manager
|
||||||
|
|
||||||
|
def _output(self, message):
|
||||||
|
"""统一输出方法,支持 TUI 和 CLI 模式"""
|
||||||
|
if getattr(self, 'tui_output_callback', None):
|
||||||
|
self.tui_output_callback(message)
|
||||||
|
else:
|
||||||
|
print(message)
|
||||||
|
|
||||||
def switch_engine(self, engine_name):
|
def switch_engine(self, engine_name):
|
||||||
"""切换AI引擎"""
|
"""切换AI引擎"""
|
||||||
if engine_name in self.engines:
|
if engine_name in self.engines:
|
||||||
@@ -333,35 +348,24 @@ class BaseAICLI:
|
|||||||
self.current_engine.max_history = self.max_history
|
self.current_engine.max_history = self.max_history
|
||||||
|
|
||||||
if old_engine != self.current_engine and self.shared_conversation_history:
|
if old_engine != self.current_engine and self.shared_conversation_history:
|
||||||
print(f"{Fore.CYAN}已切换到AI引擎: {engine_name},正在传递对话历史...{Style.RESET_ALL}")
|
self._output(f"{Fore.CYAN}已切换到AI引擎: {engine_name},正在传递对话历史...{Style.RESET_ALL}")
|
||||||
|
|
||||||
context_message = {
|
context_message = {
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": f"引擎切换通知:现在由{engine_name}引擎继续之前的对话。以下是之前的对话历史:\n"
|
"content": f"引擎切换通知:现在由{engine_name}引擎继续之前的对话。以下是之前的对话历史:\n"
|
||||||
}
|
}
|
||||||
|
self.shared_conversation_history.append(context_message)
|
||||||
|
|
||||||
if self.shared_conversation_history:
|
self._output(f"{Fore.GREEN}已切换到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||||
print(f"{Fore.YELLOW}当前对话历史已传递给新引擎:{Style.RESET_ALL}")
|
|
||||||
for msg in self.shared_conversation_history[-self.max_history:]:
|
|
||||||
if isinstance(msg, dict) and "role" in msg and "content" in msg:
|
|
||||||
role = msg["role"]
|
|
||||||
content = msg["content"]
|
|
||||||
if role == "user":
|
|
||||||
print(f"{Fore.CYAN}用户: {content}{Style.RESET_ALL}")
|
|
||||||
elif role == "assistant":
|
|
||||||
print(f"{Fore.GREEN}AI: {content}{Style.RESET_ALL}")
|
|
||||||
elif role == "tool":
|
|
||||||
print(f"{Fore.MAGENTA}工具结果: {content}{Style.RESET_ALL}")
|
|
||||||
else:
|
else:
|
||||||
print(f"{Fore.GREEN}已切换到AI引擎: {engine_name}{Style.RESET_ALL}")
|
self._output(f"{Fore.GREEN}已切换到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
set_system_config('default_engine', engine_name)
|
set_system_config('default_engine', engine_name)
|
||||||
print(f"{Fore.CYAN}已将 {engine_name} 设置为默认引擎{Style.RESET_ALL}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{Fore.YELLOW}警告: 保存默认引擎配置失败: {e}{Style.RESET_ALL}")
|
self._output(f"{Fore.YELLOW}警告: 保存默认引擎配置失败: {e}{Style.RESET_ALL}")
|
||||||
else:
|
else:
|
||||||
print(f"{Fore.RED}未找到AI引擎: {engine_name}{Style.RESET_ALL}")
|
self._output(f"{Fore.RED}未找到AI引擎: {engine_name}{Style.RESET_ALL}")
|
||||||
|
|
||||||
def _set_shared_conversation_history(self):
|
def _set_shared_conversation_history(self):
|
||||||
"""为所有引擎设置共享对话历史"""
|
"""为所有引擎设置共享对话历史"""
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class ClawliMixin:
|
|||||||
tool_calls = self._extract_tool_calls(json_data)
|
tool_calls = self._extract_tool_calls(json_data)
|
||||||
|
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
|
all_results = []
|
||||||
for tool_data in tool_calls:
|
for tool_data in tool_calls:
|
||||||
tool_name = tool_data.get('tool', '未知工具')
|
tool_name = tool_data.get('tool', '未知工具')
|
||||||
tool_args = tool_data.get('args', '')
|
tool_args = tool_data.get('args', '')
|
||||||
@@ -105,6 +106,7 @@ class ClawliMixin:
|
|||||||
|
|
||||||
result = self.process_tool_call(tool_data)
|
result = self.process_tool_call(tool_data)
|
||||||
result_text = result.get('result', '无结果')
|
result_text = result.get('result', '无结果')
|
||||||
|
all_results.append(result_text)
|
||||||
|
|
||||||
if CLAWLI_SERVER_AVAILABLE and clawli_server:
|
if CLAWLI_SERVER_AVAILABLE and clawli_server:
|
||||||
clawli_server.send_tool_status(tool_name, tool_args, "success", result_text)
|
clawli_server.send_tool_status(tool_name, tool_args, "success", result_text)
|
||||||
@@ -114,7 +116,7 @@ class ClawliMixin:
|
|||||||
"content": response
|
"content": response
|
||||||
})
|
})
|
||||||
|
|
||||||
tool_result_str = "\n".join([f"工具执行结果: {r}" for r in [result.get('result', '')]])
|
tool_result_str = "\n".join(f"工具执行结果: {r}" for r in all_results)
|
||||||
self._process_clawli_user_message(f"{tool_result_str}\n请根据工具执行结果继续回答。")
|
self._process_clawli_user_message(f"{tool_result_str}\n请根据工具执行结果继续回答。")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -134,16 +136,13 @@ class ClawliMixin:
|
|||||||
image_engine = getattr(self, 'image_engine', None)
|
image_engine = getattr(self, 'image_engine', None)
|
||||||
if not image_engine:
|
if not image_engine:
|
||||||
try:
|
try:
|
||||||
from image_engine import get_image_engine
|
from image_engine import get_engine_manager
|
||||||
image_engine = get_image_engine()
|
image_engine = get_engine_manager()
|
||||||
except:
|
except:
|
||||||
image_engine = None
|
image_engine = None
|
||||||
|
|
||||||
if image_engine:
|
if image_engine:
|
||||||
with open(image_path, 'rb') as f:
|
result = image_engine.analyze(image_path, text or "请描述这张图片")
|
||||||
image_data = f.read()
|
|
||||||
|
|
||||||
result = image_engine.analyze(image_data, text or "请描述这张图片")
|
|
||||||
self._process_clawli_user_message(f"@{image_path}\n图片内容: {result}\n\n{text}")
|
self._process_clawli_user_message(f"@{image_path}\n图片内容: {result}\n\n{text}")
|
||||||
else:
|
else:
|
||||||
self._process_clawli_user_message(f"@{image_path}\n{text}")
|
self._process_clawli_user_message(f"@{image_path}\n{text}")
|
||||||
|
|||||||
@@ -45,13 +45,12 @@ class DisplayMixin:
|
|||||||
response_lines = content.split('\n')
|
response_lines = content.split('\n')
|
||||||
if self.tui_output_callback:
|
if self.tui_output_callback:
|
||||||
for i, line in enumerate(response_lines):
|
for i, line in enumerate(response_lines):
|
||||||
if i == 0:
|
# 为每一行都添加前缀,确保整个AI回复都使用正确的样式
|
||||||
prefix = "[yellow]>[/] " if is_continue else "[cyan]>[/] "
|
prefix = "[yellow]>[/] " if is_continue else "[cyan]>[/] "
|
||||||
line = prefix + line
|
|
||||||
if i == len(response_lines) - 1:
|
if i == len(response_lines) - 1:
|
||||||
self._output(f"{line} {user_id_display}")
|
self._output(f"{prefix}{line} {user_id_display}")
|
||||||
else:
|
else:
|
||||||
self._output(line)
|
self._output(f"{prefix}{line}")
|
||||||
self._output("")
|
self._output("")
|
||||||
else:
|
else:
|
||||||
for i, line in enumerate(response_lines):
|
for i, line in enumerate(response_lines):
|
||||||
|
|||||||
+49
-48
@@ -1,60 +1,61 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
|
|
||||||
class HistoryMixin:
|
class HistoryMixin:
|
||||||
"""聊天记录管理相关方法"""
|
"""聊天记录管理 — 统一委托给 MemoryManager"""
|
||||||
|
|
||||||
def save_chat_history(self, name):
|
def save_chat_history(self, name):
|
||||||
"""保存当前聊天记录到文件"""
|
"""保存当前聊天记录"""
|
||||||
try:
|
if not hasattr(self, 'memory_manager') or not self.memory_manager:
|
||||||
chat_data = {
|
print(f"{Fore.RED}记忆系统未初始化{Style.RESET_ALL}")
|
||||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"conversation": self.shared_conversation_history.copy()
|
|
||||||
}
|
|
||||||
file_path = os.path.join(self.chat_history_dir, f"{name}.json")
|
|
||||||
with open(file_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(chat_data, f, ensure_ascii=False, indent=2, default=str)
|
|
||||||
print(f"{Fore.GREEN}聊天记录已保存为: {name}{Style.RESET_ALL}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{Fore.RED}保存聊天记录失败: {e}{Style.RESET_ALL}")
|
|
||||||
return False
|
return False
|
||||||
|
self.memory_manager.auto_save_chat(
|
||||||
|
self.shared_conversation_history,
|
||||||
|
label=name
|
||||||
|
)
|
||||||
|
print(f"{Fore.GREEN}聊天记录已保存: {name}{Style.RESET_ALL}")
|
||||||
|
return True
|
||||||
|
|
||||||
def list_chat_history(self):
|
def list_chat_history(self):
|
||||||
"""列出所有已保存的聊天记录"""
|
"""列出所有聊天记录"""
|
||||||
try:
|
if not hasattr(self, 'memory_manager') or not self.memory_manager:
|
||||||
chat_files = [f for f in os.listdir(self.chat_history_dir) if f.endswith('.json')]
|
print(f"{Fore.RED}记忆系统未初始化{Style.RESET_ALL}")
|
||||||
if not chat_files:
|
return
|
||||||
print(f"{Fore.YELLOW}没有已保存的聊天记录{Style.RESET_ALL}")
|
chats = self.memory_manager.list_chats(limit=30)
|
||||||
return
|
if not chats:
|
||||||
print(f"{Fore.GREEN}已保存的聊天记录:{Style.RESET_ALL}")
|
print(f"{Fore.YELLOW}没有已保存的聊天记录{Style.RESET_ALL}")
|
||||||
for i, file in enumerate(chat_files, 1):
|
return
|
||||||
file_path = os.path.join(self.chat_history_dir, file)
|
print(f"{Fore.GREEN}已保存的聊天记录:{Style.RESET_ALL}")
|
||||||
mod_time = os.path.getmtime(file_path)
|
for i, c in enumerate(chats, 1):
|
||||||
mod_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mod_time))
|
print(f"{Fore.GREEN}{i}. {c['name']} ({c['time']}, {c['size_kb']}KB){Style.RESET_ALL}")
|
||||||
chat_name = file[:-5]
|
|
||||||
print(f"{Fore.GREEN}{i}. {chat_name} (保存时间: {mod_time_str}){Style.RESET_ALL}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{Fore.RED}列出聊天记录失败: {e}{Style.RESET_ALL}")
|
|
||||||
|
|
||||||
def load_chat_history(self, name):
|
def load_chat_history(self, name):
|
||||||
"""加载指定的聊天记录"""
|
"""加载聊天记录(支持序号或文件名)"""
|
||||||
try:
|
if not hasattr(self, 'memory_manager') or not self.memory_manager:
|
||||||
file_path = os.path.join(self.chat_history_dir, f"{name}.json")
|
print(f"{Fore.RED}记忆系统未初始化{Style.RESET_ALL}")
|
||||||
if not os.path.exists(file_path):
|
|
||||||
print(f"{Fore.RED}聊天记录 '{name}' 不存在{Style.RESET_ALL}")
|
|
||||||
return False
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
chat_data = json.load(f)
|
|
||||||
self.shared_conversation_history = chat_data.get("conversation", [])
|
|
||||||
self._set_shared_conversation_history()
|
|
||||||
print(f"{Fore.GREEN}已加载聊天记录: {name}{Style.RESET_ALL}")
|
|
||||||
if self.shared_conversation_history:
|
|
||||||
print(f"{Fore.CYAN}加载了 {len(self.shared_conversation_history)} 条对话记录{Style.RESET_ALL}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{Fore.RED}加载聊天记录失败: {e}{Style.RESET_ALL}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 支持序号加载
|
||||||
|
if name.isdigit():
|
||||||
|
chats = self.memory_manager.list_chats(limit=50)
|
||||||
|
idx = int(name) - 1
|
||||||
|
if 0 <= idx < len(chats):
|
||||||
|
name = chats[idx]['name'].replace('.json', '')
|
||||||
|
else:
|
||||||
|
print(f"{Fore.RED}序号 {name} 超出范围{Style.RESET_ALL}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
conversation = self.memory_manager.load_chat(name)
|
||||||
|
if conversation is None:
|
||||||
|
# 尝试加 .json 后缀
|
||||||
|
if not name.endswith('.json'):
|
||||||
|
conversation = self.memory_manager.load_chat(name + '.json')
|
||||||
|
if conversation is None:
|
||||||
|
print(f"{Fore.RED}聊天记录 '{name}' 不存在{Style.RESET_ALL}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.shared_conversation_history = conversation
|
||||||
|
self._set_shared_conversation_history()
|
||||||
|
print(f"{Fore.GREEN}已加载聊天记录: {name}{Style.RESET_ALL}")
|
||||||
|
print(f"{Fore.CYAN}加载了 {len(conversation)} 条对话记录{Style.RESET_ALL}")
|
||||||
|
return True
|
||||||
@@ -105,6 +105,9 @@ class ToolMixin:
|
|||||||
result = self.liugin_manager.execute(tool_name, args, **arguments)
|
result = self.liugin_manager.execute(tool_name, args, **arguments)
|
||||||
else:
|
else:
|
||||||
result = self.liugin_manager.execute(tool_name, args)
|
result = self.liugin_manager.execute(tool_name, args)
|
||||||
|
# 将 ToolResult 对象转换为字典格式,兼容旧接口
|
||||||
|
if hasattr(result, 'to_dict'):
|
||||||
|
return result.to_dict()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# 回退:直接调用 handler(旧逻辑)
|
# 回退:直接调用 handler(旧逻辑)
|
||||||
@@ -212,8 +215,10 @@ class ToolMixin:
|
|||||||
else:
|
else:
|
||||||
tool_args_display = tool_args
|
tool_args_display = tool_args
|
||||||
|
|
||||||
# TUI 模式下不做延迟和 ESC 检测
|
# TUI 模式下显示工具执行状态
|
||||||
if not self.tui_output_callback:
|
if self.tui_output_callback:
|
||||||
|
self._output(f" 正在执行工具: {tool_name} {tool_args_display}")
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
import msvcrt
|
import msvcrt
|
||||||
canceled = False
|
canceled = False
|
||||||
|
|||||||
@@ -42,6 +42,60 @@ def set_system_config(key, value):
|
|||||||
save_config(config)
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 引擎配置档案管理 ──
|
||||||
|
# 存储结构: config["engine_profiles"][engine_name] = [ {name, base_url, api_key, model}, ... ]
|
||||||
|
|
||||||
|
def get_profiles(engine_name):
|
||||||
|
"""获取指定引擎的所有配置档案"""
|
||||||
|
config = load_config()
|
||||||
|
return config.get("engine_profiles", {}).get(engine_name, [])
|
||||||
|
|
||||||
|
|
||||||
|
def save_profile(engine_name, profile):
|
||||||
|
"""保存/更新一个配置档案(按 name 去重,已存在则覆盖)"""
|
||||||
|
config = load_config()
|
||||||
|
if "engine_profiles" not in config:
|
||||||
|
config["engine_profiles"] = {}
|
||||||
|
profiles = config["engine_profiles"].setdefault(engine_name, [])
|
||||||
|
# 按 name 去重
|
||||||
|
for i, p in enumerate(profiles):
|
||||||
|
if p.get("name") == profile.get("name"):
|
||||||
|
profiles[i] = profile
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
profiles.append(profile)
|
||||||
|
config["engine_profiles"][engine_name] = profiles
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_profile(engine_name, profile_name):
|
||||||
|
"""删除指定引擎的一个配置档案"""
|
||||||
|
config = load_config()
|
||||||
|
profiles = config.get("engine_profiles", {}).get(engine_name, [])
|
||||||
|
profiles = [p for p in profiles if p.get("name") != profile_name]
|
||||||
|
if "engine_profiles" not in config:
|
||||||
|
config["engine_profiles"] = {}
|
||||||
|
config["engine_profiles"][engine_name] = profiles
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_profile(engine_name, profile):
|
||||||
|
"""将档案应用到 config.json 的 api.engines 节点(下次启动或重载时生效)"""
|
||||||
|
config = load_config()
|
||||||
|
if "api" not in config:
|
||||||
|
config["api"] = {}
|
||||||
|
if "engines" not in config["api"]:
|
||||||
|
config["api"]["engines"] = {}
|
||||||
|
engine_cfg = config["api"]["engines"].setdefault(engine_name, {})
|
||||||
|
if "base_url" in profile:
|
||||||
|
engine_cfg["base_url"] = profile["base_url"]
|
||||||
|
if "api_key" in profile:
|
||||||
|
engine_cfg["api_key"] = profile["api_key"]
|
||||||
|
if "model" in profile:
|
||||||
|
engine_cfg["model"] = profile["model"]
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ def parse_fc_response(response_data: dict) -> Optional[List[Dict]]:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
args = {"raw": args_str}
|
args = {"raw": args_str}
|
||||||
|
|
||||||
|
# 确保 args 是 dict(LLM 可能返回非对象 JSON)
|
||||||
|
if not isinstance(args, dict):
|
||||||
|
args = {"raw": args}
|
||||||
|
|
||||||
# 统一为小狸内部格式: {"action": "use_tool", "tool": name, "args": "..." }
|
# 统一为小狸内部格式: {"action": "use_tool", "tool": name, "args": "..." }
|
||||||
# 如果 args 里有 args 字段,直接用;否则把整个 args 序列化
|
# 如果 args 里有 args 字段,直接用;否则把整个 args 序列化
|
||||||
if "args" in args and isinstance(args["args"], str):
|
if "args" in args and isinstance(args["args"], str):
|
||||||
|
|||||||
@@ -17,13 +17,71 @@ from typing import List, Dict, Optional, Tuple
|
|||||||
class MemoryManager:
|
class MemoryManager:
|
||||||
"""持久化记忆管理器"""
|
"""持久化记忆管理器"""
|
||||||
|
|
||||||
|
# 记忆模式常量
|
||||||
|
MODE_COMPANION = "companion" # 陪伴模式(长期记忆)
|
||||||
|
MODE_WORK = "work" # 工作模式(短期记忆)
|
||||||
|
|
||||||
def __init__(self, project_dir: str):
|
def __init__(self, project_dir: str):
|
||||||
self.project_dir = project_dir
|
self.project_dir = project_dir
|
||||||
self.memory_dir = os.path.join(project_dir, "memory")
|
self.memory_dir = os.path.join(project_dir, "memory")
|
||||||
self.memory_file = os.path.join(project_dir, "MEMORY.md")
|
self.memory_file = os.path.join(project_dir, "MEMORY.md")
|
||||||
self.chat_dir = os.path.join(project_dir, "chat_history")
|
self.chat_dir = os.path.join(project_dir, "chat_history")
|
||||||
|
self.config_file = os.path.join(project_dir, "config.json")
|
||||||
self.compress_threshold = 50 # 超过 50 条消息触发压缩
|
self.compress_threshold = 50 # 超过 50 条消息触发压缩
|
||||||
self._ensure_dirs()
|
self._ensure_dirs()
|
||||||
|
self._memory_mode = self._load_memory_mode()
|
||||||
|
|
||||||
|
def _load_memory_mode(self) -> str:
|
||||||
|
"""加载记忆模式配置"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.config_file):
|
||||||
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
return config.get("system", {}).get("memory_mode", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _save_memory_mode(self, mode: str):
|
||||||
|
"""保存记忆模式配置"""
|
||||||
|
try:
|
||||||
|
config = {}
|
||||||
|
if os.path.exists(self.config_file):
|
||||||
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
if "system" not in config:
|
||||||
|
config["system"] = {}
|
||||||
|
config["system"]["memory_mode"] = mode
|
||||||
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
|
self._memory_mode = mode
|
||||||
|
except Exception as e:
|
||||||
|
print(f"保存记忆模式配置失败: {e}")
|
||||||
|
|
||||||
|
def get_memory_mode(self) -> str:
|
||||||
|
"""获取当前记忆模式"""
|
||||||
|
return self._memory_mode
|
||||||
|
|
||||||
|
def is_mode_selected(self) -> bool:
|
||||||
|
"""检查是否已选择记忆模式"""
|
||||||
|
return self._memory_mode in [self.MODE_COMPANION, self.MODE_WORK]
|
||||||
|
|
||||||
|
def select_mode(self, mode: str) -> bool:
|
||||||
|
"""选择记忆模式(只能选择一次)"""
|
||||||
|
if self.is_mode_selected():
|
||||||
|
return False # 已经选择过,不能更改
|
||||||
|
if mode not in [self.MODE_COMPANION, self.MODE_WORK]:
|
||||||
|
return False # 无效的模式
|
||||||
|
self._save_memory_mode(mode)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_mode_description(self) -> str:
|
||||||
|
"""获取当前模式的描述"""
|
||||||
|
if self._memory_mode == self.MODE_COMPANION:
|
||||||
|
return "陪伴模式(长期记忆):拥有成熟的记忆功能,记住用户的偏好、习惯、历史对话等"
|
||||||
|
elif self._memory_mode == self.MODE_WORK:
|
||||||
|
return "工作模式(短期记忆):每次对话是独立的短期记忆,可通过指令手动存储聊天记录"
|
||||||
|
return "未选择模式"
|
||||||
|
|
||||||
def _ensure_dirs(self):
|
def _ensure_dirs(self):
|
||||||
"""确保目录存在"""
|
"""确保目录存在"""
|
||||||
@@ -522,6 +580,44 @@ class MemoryManager:
|
|||||||
# 记忆插件(注册到插件系统)
|
# 记忆插件(注册到插件系统)
|
||||||
# ══════════════════════════════════════════════
|
# ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
def save_conversation_by_mode(self, conversation: list, label: str = ""):
|
||||||
|
"""根据模式保存对话"""
|
||||||
|
if self._memory_mode == self.MODE_COMPANION:
|
||||||
|
# 陪伴模式:自动保存到长期记忆
|
||||||
|
self.auto_save_chat(conversation, label)
|
||||||
|
# 提取关键信息保存到MEMORY.md
|
||||||
|
self._extract_and_save_key_info(conversation)
|
||||||
|
elif self._memory_mode == self.MODE_WORK:
|
||||||
|
# 工作模式:自动保存聊天记录
|
||||||
|
self.auto_save_chat(conversation, label)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_and_save_key_info(self, conversation: list):
|
||||||
|
"""从对话中提取关键信息保存到MEMORY.md(陪伴模式专用)"""
|
||||||
|
# 这里可以添加AI提取关键信息的逻辑
|
||||||
|
# 暂时简单实现:保存最后一条助手消息
|
||||||
|
for msg in reversed(conversation):
|
||||||
|
if msg.get("role") == "assistant":
|
||||||
|
content = msg.get("content", "")
|
||||||
|
if content and len(content) > 20:
|
||||||
|
self.append_memory("对话记录", f"- {content[:200]}")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def load_context_by_mode(self, conversation: list) -> list:
|
||||||
|
"""根据模式加载上下文"""
|
||||||
|
if self._memory_mode == self.MODE_COMPANION:
|
||||||
|
# 陪伴模式:加载长期记忆作为上下文
|
||||||
|
memory_content = self.load_memory()
|
||||||
|
if memory_content:
|
||||||
|
context_msg = {"role": "system", "content": f"长期记忆:{memory_content}"}
|
||||||
|
return [context_msg] + conversation
|
||||||
|
return conversation
|
||||||
|
elif self._memory_mode == self.MODE_WORK:
|
||||||
|
# 工作模式:只返回当前对话
|
||||||
|
return conversation
|
||||||
|
return conversation
|
||||||
|
|
||||||
class Liugin:
|
class Liugin:
|
||||||
"""记忆系统插件 — AI 可读写记忆、搜索历史"""
|
"""记忆系统插件 — AI 可读写记忆、搜索历史"""
|
||||||
|
|
||||||
@@ -596,3 +692,7 @@ JSON格式示例:
|
|||||||
"required": ["action"]
|
"required": ["action"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# ══════════════════════════════════════════════
|
||||||
|
# 模式特定的方法
|
||||||
|
# ══════════════════════════════════════════════
|
||||||
|
|
||||||
@@ -109,17 +109,8 @@ class NotificationManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _notify_windows(self, title: str, message: str):
|
def _notify_windows(self, title: str, message: str):
|
||||||
"""Windows 通知 — 优先使用 win10toast,降级到 PowerShell"""
|
"""Windows 通知 — 使用 PowerShell(win10toast 在 Python 3.12+ 上不兼容)"""
|
||||||
# 方法1: win10toast (pip install win10toast)
|
# 方法1: PowerShell BurntToast 模块
|
||||||
try:
|
|
||||||
from win10toast import ToastNotifier
|
|
||||||
toaster = ToastNotifier()
|
|
||||||
toaster.show_toast(title, message, duration=3, threaded=True)
|
|
||||||
return
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 方法2: PowerShell BurntToast 模块
|
|
||||||
try:
|
try:
|
||||||
ps_script = f'''
|
ps_script = f'''
|
||||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
||||||
@@ -138,7 +129,7 @@ class NotificationManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 方法3: PowerShell 简单弹窗 (最后降级)
|
# 方法2: PowerShell 简单弹窗 (最后降级)
|
||||||
try:
|
try:
|
||||||
escaped_msg = message.replace("'", "''")
|
escaped_msg = message.replace("'", "''")
|
||||||
escaped_title = title.replace("'", "''")
|
escaped_title = title.replace("'", "''")
|
||||||
|
|||||||
@@ -107,24 +107,16 @@ class ProcessProtection:
|
|||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
# macOS: 使用 fcntl 文件锁
|
# macOS: 使用 fcntl 文件锁
|
||||||
import fcntl
|
import fcntl
|
||||||
self._lock_fd = open(self._lock_file_path, 'w')
|
self._lock_fd = open(self._lock_file_path, 'a+')
|
||||||
|
self._lock_fd.seek(0)
|
||||||
try:
|
try:
|
||||||
fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
# 文件已被锁定 — 已有实例在运行
|
# flock 失败 — 已有存活实例在运行
|
||||||
# 检查锁文件中的 PID 是否仍在运行
|
# (flock 是进程级锁,进程退出时自动释放)
|
||||||
try:
|
self._lock_fd.close()
|
||||||
old_pid = self._lock_fd.read().strip()
|
self._lock_fd = None
|
||||||
if old_pid and self._is_process_running(int(old_pid)):
|
return False
|
||||||
self._lock_fd.close()
|
|
||||||
self._lock_fd = None
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# 旧进程已死,清理残留锁
|
|
||||||
self._lock_fd.seek(0)
|
|
||||||
self._lock_fd.truncate()
|
|
||||||
except (ValueError, OSError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._lock_fd.seek(0)
|
self._lock_fd.seek(0)
|
||||||
self._lock_fd.truncate()
|
self._lock_fd.truncate()
|
||||||
@@ -135,22 +127,15 @@ class ProcessProtection:
|
|||||||
else:
|
else:
|
||||||
# Linux: 使用 fcntl 文件锁
|
# Linux: 使用 fcntl 文件锁
|
||||||
import fcntl
|
import fcntl
|
||||||
self._lock_fd = open(self._lock_file_path, 'w')
|
self._lock_fd = open(self._lock_file_path, 'a+')
|
||||||
|
self._lock_fd.seek(0)
|
||||||
try:
|
try:
|
||||||
fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(self._lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
# 检查旧进程是否仍在运行
|
# flock 失败 — 已有存活实例在运行
|
||||||
try:
|
self._lock_fd.close()
|
||||||
old_pid = self._lock_fd.read().strip()
|
self._lock_fd = None
|
||||||
if old_pid and self._is_process_running(int(old_pid)):
|
return False
|
||||||
self._lock_fd.close()
|
|
||||||
self._lock_fd = None
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self._lock_fd.seek(0)
|
|
||||||
self._lock_fd.truncate()
|
|
||||||
except (ValueError, OSError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._lock_fd.seek(0)
|
self._lock_fd.seek(0)
|
||||||
self._lock_fd.truncate()
|
self._lock_fd.truncate()
|
||||||
|
|||||||
+2085
-474
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user