- 新增 mcp_server.py: 标准 MCP stdio 传输层入口 - TUI: 回车发送消息,Shift+回车换行 (SendTextArea子类) - 子Agent: 移除 max_steps=20 限制 - 安全引擎: 移除 ALWAYS_CONFIRM 关键词强制拦截 - code_editor: diff弹窗绿底(+)/红底(-)背景色
1389 lines
75 KiB
Python
1389 lines
75 KiB
Python
import os
|
||
import sys
|
||
import json
|
||
import re
|
||
import time
|
||
import shutil
|
||
import threading
|
||
import random
|
||
from typing import Optional
|
||
from colorama import Fore, Style
|
||
|
||
from .constants import TEXTUAL_AVAILABLE, LOVE_FILE_PATH, DEFAULT_MAX_HISTORY
|
||
from .config import get_system_config, logger
|
||
from .cli_base import BaseAICLI
|
||
from .safety import get_safety, MODE_UNRESTRICTED, MODE_NORMAL, MODE_MANUAL
|
||
from .cli_clawli import ClawliMixin
|
||
from .cli_tools import ToolMixin
|
||
from .cli_code_exec import CodeExecMixin
|
||
from .cli_display import DisplayMixin
|
||
from .cli_history import HistoryMixin
|
||
from .plugin_market import PluginMarketMixin
|
||
from .notification import notify_task_complete, get_notification_manager
|
||
from .process_protection import get_protection_status
|
||
|
||
|
||
class AICLI(BaseAICLI, ClawliMixin, ToolMixin, CodeExecMixin, DisplayMixin, HistoryMixin, PluginMarketMixin):
|
||
"""AI CLI主程序 - 组合所有 Mixin"""
|
||
|
||
# ── 深度思考处理 ──
|
||
|
||
def process_thinking_response(self, response):
|
||
"""处理深度思考AI返回的内容, 分离思考和回复内容"""
|
||
try:
|
||
response_data = json.loads(response)
|
||
if isinstance(response_data, dict) and response_data.get('action') == 'use_tool':
|
||
return response
|
||
if isinstance(response_data, dict):
|
||
if 'answer' in response_data:
|
||
extracted_content = response_data['answer']
|
||
elif 'response' in response_data:
|
||
extracted_content = response_data['response']
|
||
elif 'content' in response_data:
|
||
extracted_content = response_data['content']
|
||
elif 'text' in response_data:
|
||
extracted_content = response_data['text']
|
||
else:
|
||
return json.dumps(response_data, ensure_ascii=False, indent=2)
|
||
return self._process_content_with_thinking(extracted_content)
|
||
except json.JSONDecodeError:
|
||
return self._process_content_with_thinking(response)
|
||
|
||
def _process_content_with_thinking(self, content):
|
||
"""处理包含思考标记的内容"""
|
||
thinking_start_marker = getattr(self.current_engine, 'thinking_start_marker', None) if self.current_engine else None
|
||
thinking_end_marker = getattr(self.current_engine, 'thinking_end_marker', None) if self.current_engine else None
|
||
|
||
if '<think/>' in content:
|
||
parts = content.split('<think/>')
|
||
if len(parts) >= 2:
|
||
thinking_content = parts[0].strip()
|
||
reply_content = parts[1].strip()
|
||
if thinking_content:
|
||
print(f"{Fore.LIGHTBLACK_EX}{Style.DIM}[思考: {thinking_content}]{Style.RESET_ALL}")
|
||
return reply_content if reply_content else ""
|
||
|
||
if thinking_start_marker and thinking_end_marker and thinking_start_marker in content and thinking_end_marker in content:
|
||
parts = content.split(thinking_start_marker)
|
||
if len(parts) > 1:
|
||
remaining = parts[1]
|
||
if thinking_end_marker in remaining:
|
||
thinking_part, reply_part = remaining.split(thinking_end_marker, 1)
|
||
thinking_content = thinking_part.strip()
|
||
reply_content = reply_part.strip()
|
||
if thinking_content:
|
||
print(f"{Fore.LIGHTBLACK_EX}{Style.DIM}[思考: {thinking_content}]{Style.RESET_ALL}")
|
||
return reply_content if reply_content else ""
|
||
|
||
return content
|
||
|
||
# ── 系统提示构建 ──
|
||
|
||
def _build_system_prompt(self, liugin_prompts=None):
|
||
"""构建系统提示词 - 增强版,支持编程任务"""
|
||
base_prompt = """你叫小狸,是一个强大的智能编程助手。你具备以下核心能力:
|
||
|
||
1. **精准代码编辑** - 使用 code_editor 工具进行搜索替换、多文件批量编辑(修改后自动检查语法)
|
||
2. **代码搜索与理解** - 使用 code_search 工具跨文件搜索、提取符号、分析依赖
|
||
3. **Git 版本控制** - 使用 git_tools 工具管理代码版本
|
||
4. **Shell 命令执行** - 使用 cmd_executor 执行系统命令
|
||
5. **文件管理** - 使用 file_manager 管理文件和目录
|
||
6. **浏览器自动化** - 使用 browser_auto 工具操控浏览器
|
||
7. **子 Agent 协作** - 使用 sub_agent 创建子 Agent 并行处理任务
|
||
8. **工程化自动化** - 使用 auto_engineer 进行 lint/test/build
|
||
9. **网络工具** - 使用 network_tools 进行 HTTP 请求和网络诊断
|
||
10. **AI 联网搜索** - 使用 ai_search 搜索实时信息
|
||
|
||
你在说话时可以适当加上'nyan'来表现可爱性格,但不要过度使用。"""
|
||
|
||
has_tool_search = any(t.get('name') == 'tool_search' for t in self.liugin_manager.tools)
|
||
|
||
tool_search_rule = ""
|
||
if has_tool_search:
|
||
tool_search_rule = """【⚠️ 强制工具查询规则 — 必须遵守】
|
||
在调用任何工具之前,你必须先使用 tool_search 查询该工具的详细用法、参数格式和示例。
|
||
这是硬性要求,没有例外。即使你认为自己知道工具的用法,也必须先查询确认。
|
||
|
||
【⚠️ 工具调用格式 — 必须使用 JSON】
|
||
所有工具都必须通过 JSON 指令调用,格式如下:
|
||
{{"action": "use_tool", "tool": "工具名", "args": "操作 参数"}}
|
||
|
||
禁止直接写 "工具名 操作 参数",必须包在 JSON 里。
|
||
例如:
|
||
✅ 正确: {{"action": "use_tool", "tool": "code_editor", "args": "find . keyword *.py"}}
|
||
❌ 错误: code_editor find . keyword *.py
|
||
❌ 错误: code_editor("find . keyword *.py")
|
||
|
||
执行流程:
|
||
第一步:用 tool_search 查询工具用法
|
||
{{"action": "use_tool", "tool": "tool_search", "args": "你要用的工具名"}}
|
||
第二步:阅读 tool_search 返回的详细说明(包括参数格式、操作列表、示例)
|
||
第三步:根据返回的说明,用 JSON 格式正确调用工具
|
||
|
||
为什么要这样做:
|
||
- 工具的参数格式可能已更新,你的记忆可能过时
|
||
- 先查询可以避免参数格式错误导致的调用失败
|
||
- 确保你使用的是最优的操作和参数组合
|
||
- 减少因格式不对而反复重试的浪费
|
||
|
||
示例流程(修改代码):
|
||
用户: "帮我修复 src/app.py 第 10 行的 bug"
|
||
你的思考过程:
|
||
1. 需要先查看代码 → 先查 code_editor 怎么读取文件
|
||
2. {{"action": "use_tool", "tool": "tool_search", "args": "code_editor"}} → 发现 read_range 操作
|
||
3. {{"action": "use_tool", "tool": "code_editor", "args": "read_range src/app.py 5 15"}} → 拿到代码
|
||
4. 需要修改代码 → 已经在第一步查过 code_editor,知道 edit 格式
|
||
5. {{"action": "use_tool", "tool": "code_editor", "args": "edit src/app.py
|
||
旧代码
|
||
<<<>>>
|
||
新代码"}}
|
||
|
||
示例流程(Git 操作):
|
||
用户: "帮我提交代码"
|
||
你的思考过程:
|
||
1. 需要 git 操作 → 先查 git_tools 怎么用
|
||
2. {{"action": "use_tool", "tool": "tool_search", "args": "git_tools"}} → 发现 smart-commit 操作
|
||
3. {{"action": "use_tool", "tool": "git_tools", "args": "smart-commit"}} → 自动提交"""
|
||
|
||
if liugin_prompts:
|
||
prompt = f"""{base_prompt}
|
||
|
||
{liugin_prompts}
|
||
|
||
{tool_search_rule}
|
||
|
||
【工具调用格式】
|
||
在回复中包含 JSON 指令来执行操作:
|
||
|
||
1. 纯 JSON: {{"action": "use_tool", "tool": "code_editor", "args": "edit file.py old <<<>>> new"}}
|
||
2. 文本+JSON: 好的,我来修改。{{"action": "use_tool", "tool": "code_editor", "args": "edit file.py old <<<>>> new"}}
|
||
|
||
【编程工作流 — 每一步都必须先查询工具】
|
||
1. 理解需求 → 先 tool_search "code_editor",再用 structure 查看项目结构
|
||
2. 定位代码 → 已查询过,直接用 find 或 regex 搜索
|
||
3. 查看上下文 → 用 read_range 查看相关代码
|
||
4. 精准修改 → 用 edit 替换代码(old <<<>>> new 分隔)
|
||
5. 语法自动检查 → 编辑操作后自动执行,通过显示 ✓,失败显示详细错误
|
||
6. 验证变更 → 先 tool_search "git_tools",再用 diff 查看变更
|
||
7. 提交代码 → 已查询过,用 commit 提交
|
||
|
||
【语法检查 — 自动 + 主动】
|
||
- 编辑操作(edit/multi/insert/delete_lines/create/write/append)完成后自动检查语法
|
||
- 通过: 显示 " 语法检查通过 ✓"
|
||
- 失败: 显示具体错误(行号、列号、错误描述)
|
||
- 主动检查: syntax_check <文件路径> 或 check <文件路径>
|
||
- 支持语言: Python, JavaScript, TypeScript, JSON, YAML, TOML, HTML, XML, CSS, Shell, SQL
|
||
- 如果语法检查失败,根据错误信息修复代码后再次编辑
|
||
|
||
【Diff 弹窗 — 自动弹出修改对比】
|
||
- 编辑操作完成后自动显示修改前后的完整内容和行号对比
|
||
- 新增行用绿色标记,删除行用红色标记
|
||
- 支持两种模式: 弹窗模式(新终端窗口)/ 主终端内显示(SSH 兼容)
|
||
- 开关: diff_popup on / diff_popup off / diff_popup popup / diff_popup inline
|
||
- 默认开启,无需手动调用
|
||
|
||
【code_editor 编辑格式】
|
||
edit 操作使用 <<<>>> 分隔旧代码和新代码:
|
||
{{"action": "use_tool", "tool": "code_editor", "args": "edit path/to/file.py
|
||
def old_function():
|
||
pass
|
||
<<<>>>
|
||
def new_function():
|
||
return True"}}
|
||
|
||
【批量编辑】
|
||
multi 操作支持一次修改多处:
|
||
{{"action": "use_tool", "tool": "code_editor", "args": "multi path/to/file.py [{{\\"old\\": \\"old1\\", \\"new\\": \\"new1\\"}}, {{\\"old\\": \\"old2\\", \\"new\\": \\"new2\\"}}]"}}
|
||
|
||
【搜索代码】
|
||
- 关键词搜索: {{"action": "use_tool", "tool": "code_search", "args": "find . keyword *.py"}}
|
||
- 正则搜索: {{"action": "use_tool", "tool": "code_search", "args": "regex . def\\\\s+\\\\w+ *.py"}}
|
||
- 查看结构: {{"action": "use_tool", "tool": "code_search", "args": "structure ."}}
|
||
- 查找调用: {{"action": "use_tool", "tool": "code_search", "args": "callers . function_name"}}
|
||
|
||
【Git 操作】
|
||
- 查看状态: {{"action": "use_tool", "tool": "git_tools", "args": "status"}}
|
||
- 查看变更: {{"action": "use_tool", "tool": "git_tools", "args": "diff"}}
|
||
- 提交代码: {{"action": "use_tool", "tool": "git_tools", "args": "commit 描述信息"}}
|
||
- 查看历史: {{"action": "use_tool", "tool": "git_tools", "args": "log --oneline 10"}}
|
||
|
||
【重要提醒】
|
||
- 调用任何不熟悉的工具前,必须先 tool_search 查询
|
||
- code_editor.edit 的 old_text 必须与文件中的内容完全匹配(包括缩进)
|
||
- 如果 edit 失败,先用 read_range 查看实际内容再重试
|
||
- args 必须是字符串
|
||
- 可以一次返回多个 JSON 指令
|
||
- 使用 continue 补充回答: {{"action": "continue", "content": "补充内容"}}"""
|
||
else:
|
||
prompt = f"""{base_prompt}
|
||
|
||
{tool_search_rule}
|
||
|
||
【工具调用格式】
|
||
{{"action": "use_tool", "tool": "工具名", "args": "参数"}}
|
||
|
||
【⚠️ 核心规则:调用任何工具前必须先查询】
|
||
1. 先调用 tool_search 查询工具用法
|
||
2. 阅读返回的参数说明和示例
|
||
3. 再根据说明正确调用工具
|
||
|
||
【核心工具】
|
||
- tool_search: 查询任何工具的用法(必须先调用这个)
|
||
- code_editor: edit/search/diff/insert/create/write/read_range
|
||
- code_search: find/regex/symbols/imports/structure/stats
|
||
- git_tools: status/diff/log/add/commit/branch
|
||
- cmd_executor: 执行 shell 命令
|
||
- file_manager: 文件管理
|
||
|
||
【code_editor.edit 格式】
|
||
{{"action": "use_tool", "tool": "code_editor", "args": "edit 文件路径
|
||
旧代码内容
|
||
<<<>>>
|
||
新代码内容"}}
|
||
|
||
【continue 格式】
|
||
{{"action": "continue", "content": "要补充的内容"}}"""
|
||
|
||
# ── 注入持久化记忆上下文 ──
|
||
if hasattr(self, 'memory_manager') and self.memory_manager:
|
||
try:
|
||
memory_ctx = self.memory_manager.build_memory_context()
|
||
if memory_ctx:
|
||
prompt += memory_ctx
|
||
except Exception:
|
||
pass
|
||
|
||
return prompt
|
||
|
||
# ── JSON 解析 ──
|
||
|
||
def _clean_json_response(self, response):
|
||
"""清理AI响应中的Markdown代码块标记"""
|
||
if response.startswith("```json"):
|
||
response = response[7:]
|
||
elif response.startswith("```"):
|
||
response = response[3:]
|
||
if response.endswith("```"):
|
||
response = response[:-3]
|
||
return response.strip()
|
||
|
||
def get_liugin_usage_prompts(self):
|
||
"""获取可用工具提示词"""
|
||
if not self.liugin_manager.tools:
|
||
return "当前没有可用的工具."
|
||
|
||
prompts = []
|
||
prompts.append("可用的工具列表:\n")
|
||
|
||
plugins = []
|
||
skills = []
|
||
for tool in self.liugin_manager.tools:
|
||
protocol = tool.get('protocol', 'liugin')
|
||
if protocol == 'skill':
|
||
skills.append(tool)
|
||
else:
|
||
plugins.append(tool)
|
||
|
||
if plugins:
|
||
prompts.append("## 内置工具(使用 tool_search 查询详细用法):")
|
||
for tool in plugins:
|
||
tool_name = tool.get('name', '')
|
||
tool_desc = tool.get('description', '')
|
||
prompts.append(f"- {tool_name}: {tool_desc}")
|
||
|
||
if skills:
|
||
prompts.append("\n## 扩展技能(已加载详细指令):")
|
||
for tool in skills:
|
||
tool_name = tool.get('name', '')
|
||
tool_desc = tool.get('description', '')
|
||
prompts.append(f"\n### {tool_name}")
|
||
prompts.append(f"描述: {tool_desc}")
|
||
instance = tool.get('instance')
|
||
if instance:
|
||
instructions = getattr(instance, 'instructions', '')
|
||
if instructions:
|
||
if len(instructions) > 2000:
|
||
instructions = instructions[:2000] + "\n...(内容已截断)"
|
||
prompts.append(f"\n{instructions}")
|
||
|
||
if self.is_clawli_mode:
|
||
prompts.append("\n## Clawli 远程模式专属工具:")
|
||
prompts.append("""
|
||
### send_image - 发送图片到手机端
|
||
当用户请求发送图片到手机时,使用此工具:
|
||
|
||
```json
|
||
{
|
||
"action": "use_tool",
|
||
"tool": "send_image",
|
||
"args": {
|
||
"image_path": "图片文件的绝对路径",
|
||
"caption": "图片说明文字(可选)"
|
||
}
|
||
}
|
||
```
|
||
|
||
支持的图片格式:JPG、PNG、GIF、BMP、WEBP
|
||
""")
|
||
|
||
return "\n".join(prompts)
|
||
|
||
def _parse_mixed_response(self, response):
|
||
"""
|
||
解析混合响应,处理同时包含文本和JSON指令的内容
|
||
返回格式: (text_content, json_data)
|
||
"""
|
||
response = response.strip()
|
||
|
||
# 方法0: 处理 <tool_call>... 格式
|
||
if response.startswith("<tool_call>"):
|
||
json_str = response[11:]
|
||
json_str = "{" + json_str
|
||
try:
|
||
parsed = json.loads(json_str)
|
||
if isinstance(parsed, dict):
|
||
return "", parsed
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# 方法1: 直接解析为纯JSON
|
||
try:
|
||
parsed = json.loads(response)
|
||
if isinstance(parsed, (list, dict)):
|
||
return "", parsed
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# 方法2: 修复JSON中的控制字符问题
|
||
try:
|
||
def fix_json_string(match):
|
||
content = match.group(1)
|
||
content = content.replace('\\', '\\\\')
|
||
content = content.replace('\n', '\\n')
|
||
content = content.replace('\r', '\\r')
|
||
content = content.replace('\t', '\\t')
|
||
content = content.replace('"', '\\"')
|
||
return f'"{content}"'
|
||
fixed_response = re.sub(r'"([^"]*(?:\n[^"]*)*)"', fix_json_string, response, flags=re.DOTALL)
|
||
parsed = json.loads(fixed_response)
|
||
if isinstance(parsed, (list, dict)):
|
||
return "", parsed
|
||
except:
|
||
pass
|
||
|
||
# 方法3: 查找代码块中的JSON
|
||
json_pattern = r"```(?:json)?\s*({.*?})\s*```"
|
||
matches = re.findall(json_pattern, response, re.DOTALL)
|
||
if matches:
|
||
try:
|
||
json_data = json.loads(matches[-1])
|
||
if isinstance(json_data, (dict, list)):
|
||
text_content = re.sub(json_pattern, "", response, flags=re.DOTALL).strip()
|
||
text_content = re.sub(r"\n\s*\n", "\n\n", text_content).strip()
|
||
return text_content, json_data
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# 方法4: 查找每行独立的JSON对象
|
||
lines_resp = response.split("\n")
|
||
json_objects = []
|
||
text_lines = []
|
||
for line in lines_resp:
|
||
line = line.strip()
|
||
if line.startswith("{") and line.endswith("}"):
|
||
try:
|
||
parsed = json.loads(line)
|
||
if isinstance(parsed, dict):
|
||
json_objects.append(parsed)
|
||
continue
|
||
except json.JSONDecodeError:
|
||
pass
|
||
text_lines.append(line)
|
||
|
||
if json_objects:
|
||
text_content = "\n".join(text_lines).strip()
|
||
if len(json_objects) == 1:
|
||
return text_content, json_objects[0]
|
||
else:
|
||
return text_content, json_objects
|
||
|
||
# 方法5: 查找内联的JSON对象
|
||
json_start_markers = [
|
||
'{"action": "use_tool"', '{"action": "continue"',
|
||
'{"action":"use_tool"', '{"action":"continue"',
|
||
'{ "action": "use_tool"', '{ "action": "continue"',
|
||
'{ "action":"use_tool"', '{ "action":"continue"'
|
||
]
|
||
for marker in json_start_markers:
|
||
json_start = response.find(marker)
|
||
if json_start != -1:
|
||
brace_count = 0
|
||
in_json = False
|
||
for i in range(json_start, len(response)):
|
||
char = response[i]
|
||
if char == '{':
|
||
if not in_json:
|
||
in_json = True
|
||
brace_count += 1
|
||
elif char == '}':
|
||
brace_count -= 1
|
||
if in_json and brace_count == 0:
|
||
json_str = response[json_start:i+1]
|
||
try:
|
||
json_data = json.loads(json_str)
|
||
if isinstance(json_data, dict) and json_data.get('action') in ['use_tool', 'continue']:
|
||
text_content = response[:json_start].strip()
|
||
return text_content, json_data
|
||
except json.JSONDecodeError:
|
||
pass
|
||
break
|
||
|
||
# 方法6: 通用JSON对象查找
|
||
potential_jsons = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', response, re.DOTALL)
|
||
for json_str in potential_jsons:
|
||
try:
|
||
json_data = json.loads(json_str)
|
||
if isinstance(json_data, dict) and json_data.get('action') in ['use_tool', 'continue']:
|
||
json_start = response.find(json_str)
|
||
text_content = response[:json_start].strip()
|
||
return text_content, json_data
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
# 方法7: 批量JSON
|
||
try:
|
||
cleaned = response.strip()
|
||
for action in ['use_tool', 'continue']:
|
||
last_brace_pos = cleaned.rfind(f'{{"action": "{action}"')
|
||
if last_brace_pos == -1:
|
||
last_brace_pos = cleaned.rfind(f'{{"action":"{action}"')
|
||
if last_brace_pos != -1:
|
||
json_part = cleaned[last_brace_pos:]
|
||
if json_part.startswith('['):
|
||
parsed = json.loads(json_part)
|
||
if isinstance(parsed, list):
|
||
text_content = cleaned[:last_brace_pos].strip()
|
||
return text_content, parsed
|
||
else:
|
||
parsed = json.loads(json_part)
|
||
if isinstance(parsed, dict):
|
||
text_content = cleaned[:last_brace_pos].strip()
|
||
return text_content, parsed
|
||
except:
|
||
pass
|
||
|
||
return response, None
|
||
|
||
# ── 对话处理 ──
|
||
|
||
def process_conversation(self, user_input):
|
||
"""处理完整的对话循环, 支持工具调用循环和AI继续操作循环"""
|
||
self.shared_conversation_history.append({
|
||
"role": "user",
|
||
"content": user_input
|
||
})
|
||
liugin_prompts = self.get_liugin_usage_prompts()
|
||
loop_count = 0
|
||
current_input = user_input
|
||
max_loops = 20
|
||
|
||
while loop_count < max_loops:
|
||
response = self._generate_response_with_animation(current_input, liugin_prompts=liugin_prompts)
|
||
processed_response = self.process_thinking_response(response)
|
||
processed_response = self._process_code_blocks(processed_response)
|
||
|
||
# ── Function Calling 响应检测 ──
|
||
fc_handled = self._handle_fc_response(processed_response, user_input)
|
||
if fc_handled:
|
||
# FC 工具已执行,用结果继续循环
|
||
current_input = fc_handled
|
||
loop_count += 1
|
||
continue
|
||
|
||
try:
|
||
text_content, json_data = self._parse_mixed_response(processed_response)
|
||
|
||
if json_data:
|
||
if text_content:
|
||
text_content = self._process_code_blocks(text_content)
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content)
|
||
|
||
if isinstance(json_data, dict):
|
||
response_data = json_data
|
||
|
||
if response_data.get('action') == 'use_tool':
|
||
canceled = self._execute_single_tool(response_data)
|
||
if canceled:
|
||
break
|
||
break
|
||
|
||
if (response_data.get('continue') is True or
|
||
response_data.get('need_continue') is True or
|
||
response_data.get('think_more') is True):
|
||
continue_message = response_data.get('message', '')
|
||
if continue_message:
|
||
processed_response = continue_message
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": processed_response
|
||
})
|
||
self._display_response(processed_response, is_continue=True)
|
||
loop_count += 1
|
||
elif response_data.get('message'):
|
||
message = response_data.get('message')
|
||
if text_content:
|
||
text_content = self._process_code_blocks(text_content)
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content)
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": message
|
||
})
|
||
self._display_response(message)
|
||
break
|
||
else:
|
||
if text_content:
|
||
text_content = self._process_code_blocks(text_content)
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content)
|
||
else:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": processed_response
|
||
})
|
||
self._display_response(processed_response)
|
||
break
|
||
|
||
elif isinstance(json_data, list):
|
||
tool_calls = [item for item in json_data if isinstance(item, dict) and item.get('action') == 'use_tool']
|
||
if tool_calls:
|
||
self._execute_concurrent_tools(tool_calls)
|
||
break
|
||
continue_items = [item for item in json_data if isinstance(item, dict) and
|
||
(item.get('continue') is True or
|
||
item.get('need_continue') is True or
|
||
item.get('think_more') is True)]
|
||
if continue_items:
|
||
continue_message = continue_items[0].get('message', '')
|
||
if continue_message:
|
||
processed_response = continue_message
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": processed_response
|
||
})
|
||
self._display_response(processed_response, is_continue=True)
|
||
loop_count += 1
|
||
else:
|
||
if text_content:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content or processed_response)
|
||
break
|
||
else:
|
||
display_content = text_content if text_content else processed_response
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": display_content
|
||
})
|
||
self._display_response(display_content)
|
||
break
|
||
except json.JSONDecodeError:
|
||
text_content, json_data = self._parse_mixed_response(processed_response)
|
||
if json_data:
|
||
if text_content:
|
||
text_content = self._process_code_blocks(text_content)
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content)
|
||
|
||
if isinstance(json_data, dict):
|
||
response_data = json_data
|
||
if response_data.get('action') == 'continue':
|
||
continue_content = response_data.get('content', '')
|
||
if continue_content:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": continue_content
|
||
})
|
||
self._display_response(continue_content)
|
||
continue
|
||
|
||
if response_data.get('action') == 'use_tool':
|
||
canceled = self._execute_single_tool(response_data)
|
||
if canceled:
|
||
break
|
||
break
|
||
|
||
if (response_data.get('continue') is True or
|
||
response_data.get('need_continue') is True or
|
||
response_data.get('think_more') is True):
|
||
continue_message = response_data.get('message', '')
|
||
if continue_message:
|
||
processed_response = continue_message
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": processed_response
|
||
})
|
||
self._display_response(processed_response, is_continue=True)
|
||
loop_count += 1
|
||
else:
|
||
message = response_data.get('message', '')
|
||
if message:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": message
|
||
})
|
||
self._display_response(message)
|
||
loop_count += 1
|
||
else:
|
||
if text_content:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content or processed_response)
|
||
break
|
||
|
||
elif isinstance(json_data, list):
|
||
tool_calls = [item for item in json_data if isinstance(item, dict) and item.get('action') == 'use_tool']
|
||
if tool_calls:
|
||
self._execute_concurrent_tools(tool_calls)
|
||
break
|
||
continue_items = [item for item in json_data if isinstance(item, dict) and
|
||
(item.get('continue') is True or
|
||
item.get('need_continue') is True or
|
||
item.get('think_more') is True)]
|
||
if continue_items:
|
||
continue_message = continue_items[0].get('message', '')
|
||
if continue_message:
|
||
processed_response = continue_message
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": processed_response
|
||
})
|
||
self._display_response(processed_response, is_continue=True)
|
||
loop_count += 1
|
||
else:
|
||
if text_content:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": text_content
|
||
})
|
||
self._display_response(text_content or processed_response)
|
||
break
|
||
else:
|
||
display_content = text_content if text_content else processed_response
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": display_content
|
||
})
|
||
self._display_response(display_content)
|
||
break
|
||
else:
|
||
self._output(f"AI 已达到最大循环次数 ({max_loops}),已停止自动处理")
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": f"已达到最大处理次数 ({max_loops}),已自动停止。如需继续处理,请重新输入。"
|
||
})
|
||
|
||
# ── 任务完成通知 ──
|
||
task_summary = self._extract_task_summary(user_input)
|
||
notify_task_complete(task_summary)
|
||
|
||
# ── 自动保存聊天记录 ──
|
||
if hasattr(self, 'memory_manager') and self.memory_manager:
|
||
try:
|
||
self.memory_manager.auto_save_chat(
|
||
self.shared_conversation_history,
|
||
label=user_input[:30]
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# ── 自动压缩上下文 ──
|
||
try:
|
||
if self.memory_manager.should_compress(self.shared_conversation_history):
|
||
self.shared_conversation_history = self.memory_manager.compress_context(
|
||
self.shared_conversation_history,
|
||
engine=self.current_engine
|
||
)
|
||
self._set_shared_conversation_history()
|
||
print(f"{Fore.DIM}[上下文已压缩,保留最近 10 条]{Style.RESET_ALL}")
|
||
except Exception:
|
||
pass
|
||
|
||
def _extract_task_summary(self, user_input: str) -> str:
|
||
"""提取任务摘要用于通知显示"""
|
||
# 取用户输入的前 80 字符作为摘要
|
||
summary = user_input.strip().replace('\n', ' ')
|
||
if len(summary) > 80:
|
||
summary = summary[:77] + "..."
|
||
return summary
|
||
|
||
def _handle_fc_response(self, response: str, original_input: str) -> Optional[str]:
|
||
"""
|
||
处理 Function Calling 响应
|
||
如果响应是 FC 格式,执行工具并返回结果(供下一轮循环使用)
|
||
如果不是 FC 响应,返回 None
|
||
"""
|
||
if not response or '_fc' not in response:
|
||
return None
|
||
|
||
try:
|
||
data = json.loads(response)
|
||
if not data.get("_fc"):
|
||
return None
|
||
|
||
tool_calls = data.get("tool_calls", [])
|
||
if not tool_calls:
|
||
return None
|
||
|
||
# 记录 assistant 的 FC 消息到历史
|
||
fc_history_entry = {
|
||
"role": "assistant",
|
||
"content": None,
|
||
"tool_calls": [{
|
||
"id": tc.get("id", ""),
|
||
"type": "function",
|
||
"function": {
|
||
"name": tc.get("name", ""),
|
||
"arguments": json.dumps(tc.get("arguments", {}), ensure_ascii=False)
|
||
}
|
||
} for tc in tool_calls]
|
||
}
|
||
self.shared_conversation_history.append(fc_history_entry)
|
||
|
||
# 执行每个工具调用
|
||
results = []
|
||
for tc in tool_calls:
|
||
tool_name = tc.get("tool", tc.get("name", ""))
|
||
tool_args = tc.get("args", "")
|
||
call_id = tc.get("id", "")
|
||
|
||
self._output(f"[green] FC 调用: {tool_name}({tool_args[:60]})[/]")
|
||
|
||
# 通过插件管理器执行
|
||
result = self._execute_tool_by_name(tool_name, tool_args)
|
||
|
||
# 记录工具结果到历史
|
||
tool_result_msg = {
|
||
"role": "tool",
|
||
"tool_call_id": call_id,
|
||
"name": tool_name,
|
||
"content": result[:5000] if result else "执行完成"
|
||
}
|
||
self.shared_conversation_history.append(tool_result_msg)
|
||
|
||
results.append(f"[{tool_name}]: {result[:200] if result else '完成'}")
|
||
|
||
# 返回工具结果,供下一轮 AI 处理
|
||
return "工具执行结果:\n" + "\n".join(results)
|
||
|
||
except (ValueError, KeyError, TypeError):
|
||
return None
|
||
|
||
def _execute_tool_by_name(self, tool_name: str, tool_args: str) -> str:
|
||
"""根据名称执行工具(FC 调用路径)"""
|
||
# 安全检查
|
||
safety = get_safety()
|
||
allowed, msg = safety.check(tool_name, tool_args)
|
||
if not allowed:
|
||
return f"操作被拒绝: {msg}"
|
||
|
||
# 查找工具
|
||
tool = self.liugin_manager.get_tool_by_name(tool_name)
|
||
if tool and 'handler' in tool:
|
||
try:
|
||
return tool['handler'](tool_args)
|
||
except Exception as e:
|
||
return f"工具执行失败: {e}"
|
||
|
||
# memory 内置工具
|
||
if tool_name == 'memory' and hasattr(self, 'memory_manager'):
|
||
return self.memory_manager.handle_tool(tool_args)
|
||
|
||
# scheduler 内置工具
|
||
if tool_name == 'scheduler':
|
||
for t in self.liugin_manager.tools:
|
||
if t.get('name') == 'scheduler' and 'handler' in t:
|
||
try:
|
||
return t['handler'](tool_args)
|
||
except Exception as e:
|
||
return f"工具执行失败: {e}"
|
||
|
||
return f"未找到工具: {tool_name}"
|
||
|
||
def _generate_response_with_animation(self, current_input, liugin_prompts=None):
|
||
"""生成AI响应并显示等待动画 — 支持 ESC 跨平台取消"""
|
||
# TUI 模式:不做动画
|
||
if self.tui_output_callback:
|
||
system_prompt = self._build_system_prompt(liugin_prompts)
|
||
if not self.current_engine:
|
||
return "错误: 当前没有可用的AI引擎,请检查ai_engines目录中的引擎插件"
|
||
# TUI 模式也支持 FC
|
||
fc_tools = None
|
||
try:
|
||
from .fc_tools import mcp_to_openai_tools
|
||
fc_tools = mcp_to_openai_tools(self.liugin_manager)
|
||
except Exception:
|
||
pass
|
||
response = self.current_engine.generate_response(current_input, system_prompt=system_prompt, tools=fc_tools)
|
||
return response
|
||
|
||
# CLI 模式:带动画和跨平台 ESC 取消
|
||
animation_running = threading.Event()
|
||
animation_running.set()
|
||
esc_pressed = threading.Event()
|
||
response_ready = threading.Event()
|
||
response_result = [None] # 用列表存储,方便线程内修改
|
||
|
||
love_sentences = []
|
||
love_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), LOVE_FILE_PATH)
|
||
if os.path.exists(love_file_path):
|
||
try:
|
||
with open(love_file_path, 'r', encoding='utf-8') as f:
|
||
love_sentences = [line.strip() for line in f.readlines() if line.strip()]
|
||
except Exception:
|
||
pass
|
||
if not love_sentences:
|
||
love_sentences = ["AI正在思考中...", "请稍等片刻...", "正在处理您的请求..."]
|
||
|
||
# ── 跨平台 ESC 检测 ──
|
||
def check_for_esc():
|
||
"""跨平台 ESC 键检测"""
|
||
import platform
|
||
if platform.system() == 'Windows':
|
||
try:
|
||
import msvcrt
|
||
while animation_running.is_set():
|
||
if msvcrt.kbhit():
|
||
key = msvcrt.getch()
|
||
if ord(key) == 27: # ESC
|
||
esc_pressed.set()
|
||
break
|
||
time.sleep(0.05)
|
||
except ImportError:
|
||
pass
|
||
else:
|
||
# Linux / macOS — 用 select 做非阻塞读取
|
||
import select
|
||
import sys
|
||
try:
|
||
# 保存原始终端设置
|
||
import tty, termios
|
||
fd = sys.stdin.fileno()
|
||
old_settings = termios.tcgetattr(fd)
|
||
try:
|
||
tty.setcbreak(fd) # cbreak 模式:无需回车即可读取
|
||
while animation_running.is_set():
|
||
if select.select([sys.stdin], [], [], 0.05)[0]:
|
||
ch = sys.stdin.read(1)
|
||
if ord(ch) == 27: # ESC
|
||
esc_pressed.set()
|
||
break
|
||
finally:
|
||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||
except Exception:
|
||
# 降级:无法检测 ESC,只能等 AI 完成
|
||
pass
|
||
|
||
# ── 动画线程 ──
|
||
def show_animation():
|
||
last_change_time = time.time()
|
||
current_sentence = random.choice(love_sentences)
|
||
while animation_running.is_set():
|
||
current_time = time.time()
|
||
if current_time - last_change_time >= 5:
|
||
current_sentence = random.choice(love_sentences)
|
||
last_change_time = current_time
|
||
animation_chars = "|/-\\"
|
||
char_idx = int((current_time * 10) % len(animation_chars))
|
||
display_sentence = current_sentence[:47] + "..." if len(current_sentence) > 50 else current_sentence
|
||
print(f"\r{Fore.MAGENTA}{display_sentence} {animation_chars[char_idx]}{Style.RESET_ALL}", end="", flush=True)
|
||
time.sleep(0.1)
|
||
|
||
# ── AI 响应线程 ──
|
||
def generate_in_thread():
|
||
try:
|
||
system_prompt = self._build_system_prompt(liugin_prompts)
|
||
if not self.current_engine:
|
||
response_result[0] = "错误: 当前没有可用的AI引擎,请检查ai_engines目录中的引擎插件"
|
||
else:
|
||
# ── 收集 FC 工具定义 ──
|
||
fc_tools = None
|
||
try:
|
||
from .fc_tools import mcp_to_openai_tools
|
||
fc_tools = mcp_to_openai_tools(self.liugin_manager)
|
||
if fc_tools:
|
||
fc_tools = fc_tools # 已经是列表
|
||
except Exception:
|
||
fc_tools = None
|
||
|
||
response_result[0] = self.current_engine.generate_response(
|
||
current_input, system_prompt=system_prompt, tools=fc_tools
|
||
)
|
||
except Exception as e:
|
||
response_result[0] = f"AI 响应生成失败: {e}"
|
||
finally:
|
||
response_ready.set()
|
||
|
||
# 启动三个线程
|
||
threads = []
|
||
for target in (show_animation, check_for_esc, generate_in_thread):
|
||
t = threading.Thread(target=target, daemon=True)
|
||
t.start()
|
||
threads.append(t)
|
||
|
||
try:
|
||
# 主线程等待:ESC 或 响应完成
|
||
while not response_ready.is_set() and not esc_pressed.is_set():
|
||
time.sleep(0.1)
|
||
|
||
# ESC 被按下 → 取消
|
||
if esc_pressed.is_set():
|
||
# 清除动画行
|
||
print("\r" + " " * 80 + "\r", end="", flush=True)
|
||
print(f"\n{Fore.YELLOW}⚡ AI 请求已取消 (ESC){Style.RESET_ALL}")
|
||
return "[AI请求已取消]"
|
||
|
||
# 响应正常返回
|
||
response = response_result[0]
|
||
if response:
|
||
# 清除动画行后再输出
|
||
print("\r" + " " * 80 + "\r", end="", flush=True)
|
||
self._typeprint(response, Fore.CYAN)
|
||
|
||
if response and self.shared_conversation_history is not None:
|
||
self.shared_conversation_history.append({
|
||
"role": "assistant",
|
||
"content": response
|
||
})
|
||
|
||
return response
|
||
|
||
finally:
|
||
animation_running.clear()
|
||
for t in threads:
|
||
t.join(timeout=1)
|
||
|
||
# ── CLI 主循环 ──
|
||
|
||
def run_tui(self):
|
||
"""运行 TUI 模式"""
|
||
if not TEXTUAL_AVAILABLE:
|
||
print(f"{Fore.RED}TUI 模式不可用:Textual 库未安装{Style.RESET_ALL}")
|
||
print(f"{Fore.YELLOW}请运行: pip install textual rich{Style.RESET_ALL}")
|
||
return
|
||
from .tui import XiaoliTUI
|
||
app = XiaoliTUI(self)
|
||
app.run()
|
||
|
||
def run(self):
|
||
"""运行 CLI"""
|
||
import uuid
|
||
self.user_id = str(uuid.getnode())
|
||
print(f"{Fore.GREEN}小狸 Pro-CLI 已启动!{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}用户ID: {self.user_id}{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/help' 查看帮助信息{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/quit' 退出程序{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/tui' 切换到 TUI 模式{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/engine list' 查看可用AI引擎{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/engine switch <引擎名>' 切换AI引擎{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/safe' 切换安全模式 (普通→人工→无限制){Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/notify' 切换任务完成通知 (开/关){Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/scheduler' 或 '/remind' 管理定时任务{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/memory' 管理记忆系统 (日记/搜索/聊天记录){Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/file.read <文件名> [行数]' 直接读取文件内容{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '@文件路径' 自动读取文件内容并发送给AI{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '@图片路径' 自动分析图片并发送描述给AI{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/image engines' 查看图像识别引擎{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/chat save <名称>' 保存当前聊天记录{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/chat list' 查看所有已保存的聊天记录{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/chat open <名称>' 加载聊天记录{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/remote' 查看远程连接帮助{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/plugin' 管理插件市场 (安装/卸载/搜索){Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}输入 '/protect' 查看进程保护状态{Style.RESET_ALL}")
|
||
current_engine_name = getattr(self.current_engine, 'name', '未设置') if self.current_engine else '未设置'
|
||
print(f"{Fore.GREEN}当前使用的AI引擎: {current_engine_name}{Style.RESET_ALL}")
|
||
loaded_engines = list(self.engines.keys())
|
||
if len(loaded_engines) > 1:
|
||
print(f"{Fore.GREEN}已加载的AI引擎: {', '.join(loaded_engines)}{Style.RESET_ALL}")
|
||
print(f"{Fore.GREEN}{'-' * 50}{Style.RESET_ALL}")
|
||
|
||
# 显示进程保护状态
|
||
if hasattr(self, 'protection_results') and self.protection_results:
|
||
r = self.protection_results
|
||
si_icon = '' if r.get('single_instance') else ''
|
||
prio_icon = '' if r.get('priority') else ''
|
||
ppl_icon = '' if r.get('ppl_protection') else ''
|
||
wd_icon = '' if r.get('watchdog') else ''
|
||
print(f"{Fore.CYAN} 进程保护: {si_icon}单实例 {prio_icon}优先级 {ppl_icon}PPL保护 {wd_icon}看门狗{Style.RESET_ALL}")
|
||
|
||
# 询问 diff 弹窗模式
|
||
self._ask_diff_popup_mode()
|
||
|
||
self._run_cli_loop()
|
||
|
||
def _ask_diff_popup_mode(self):
|
||
"""启动时询问 diff 显示模式"""
|
||
# 找到 code_editor 插件
|
||
code_editor = None
|
||
for tool in self.liugin_manager.tools:
|
||
if tool.get('name') == 'code_editor':
|
||
code_editor = tool.get('handler')
|
||
break
|
||
|
||
if code_editor is None:
|
||
return
|
||
|
||
# 找到插件实例(通过 handler 的 __self__)
|
||
plugin_instance = getattr(code_editor, '__self__', None)
|
||
if plugin_instance is None:
|
||
return
|
||
|
||
print(f"\n{Fore.CYAN} Diff 显示模式(AI 修改文件后的对比方式):{Style.RESET_ALL}")
|
||
print(f" {Fore.WHITE}1{Style.RESET_ALL} - 弹窗模式(新终端窗口显示)")
|
||
print(f" {Fore.WHITE}2{Style.RESET_ALL} - 主终端内显示(SSH 兼容,推荐)")
|
||
|
||
try:
|
||
choice = input(f"\n{Fore.WHITE} 请选择 [1/2](默认 2): {Style.RESET_ALL}").strip()
|
||
except (EOFError, KeyboardInterrupt):
|
||
choice = '2'
|
||
|
||
if choice == '1':
|
||
plugin_instance.diff_popup_mode = True
|
||
print(f"{Fore.GREEN} ✓ 已选择: 弹窗模式{Style.RESET_ALL}")
|
||
else:
|
||
plugin_instance.diff_popup_mode = False
|
||
print(f"{Fore.GREEN} ✓ 已选择: 主终端内显示{Style.RESET_ALL}")
|
||
|
||
print()
|
||
|
||
def _run_cli_loop(self):
|
||
"""运行 CLI 主循环"""
|
||
while True:
|
||
try:
|
||
user_input = input(f"\n{Fore.WHITE}> {Style.RESET_ALL}").strip()
|
||
|
||
if user_input == '/quit':
|
||
print(f"{Fore.GREEN}再见!{Style.RESET_ALL}")
|
||
break
|
||
|
||
if user_input == '/help':
|
||
self.show_help()
|
||
continue
|
||
|
||
if user_input.startswith('/help '):
|
||
liugin_name = user_input[6:].strip()
|
||
self.show_liugin_help(liugin_name)
|
||
continue
|
||
|
||
if user_input.startswith('/engine '):
|
||
engine_command = user_input[8:].strip()
|
||
parts = engine_command.split(' ', 1)
|
||
cmd = parts[0]
|
||
args = parts[1] if len(parts) > 1 else ''
|
||
if cmd in self.engine_commands:
|
||
self.engine_commands[cmd](args)
|
||
else:
|
||
available_commands = ', '.join(self.engine_commands.keys())
|
||
print(f"{Fore.RED}未知的引擎命令.可用命令: {available_commands}{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/engine.'):
|
||
engine_command = user_input[8:].strip()
|
||
if not self.handle_engine_command(engine_command):
|
||
print(f"{Fore.RED}当前引擎不支持该命令或命令执行失败{Style.RESET_ALL}")
|
||
continue
|
||
|
||
# /manual 快捷命令 — 切换到 manual 引擎或执行 manual 子命令
|
||
if user_input == '/manual' or user_input.startswith('/manual '):
|
||
args = user_input[8:].strip() if user_input.startswith('/manual ') else ''
|
||
if 'manual' not in self.engines:
|
||
print(f"{Fore.RED}manual 引擎未加载{Style.RESET_ALL}")
|
||
elif not args:
|
||
# 无参数:切换到 manual 引擎
|
||
self.switch_engine('manual')
|
||
print(f"{Fore.GREEN}已切换到 manual 引擎{Style.RESET_ALL}")
|
||
else:
|
||
# 有参数:执行 manual 引擎命令(如 status, restart)
|
||
result = self.engines['manual'].handle_command(args)
|
||
if result:
|
||
print(f"{Fore.GREEN}{result}{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/file.read '):
|
||
args = user_input[11:].strip()
|
||
parts = args.split(' ', 2)
|
||
if len(parts) < 1:
|
||
print(f"{Fore.RED}请提供文件名.用法: /file.read <文件名> [行数]{Style.RESET_ALL}")
|
||
continue
|
||
filename = parts[0]
|
||
max_lines = None
|
||
if len(parts) >= 2:
|
||
try:
|
||
max_lines = int(parts[1])
|
||
if max_lines <= 0:
|
||
print(f"{Fore.RED}行数必须是正整数{Style.RESET_ALL}")
|
||
continue
|
||
except ValueError:
|
||
print(f"{Fore.RED}行数必须是正整数{Style.RESET_ALL}")
|
||
continue
|
||
result = self._read_file_direct(filename, max_lines)
|
||
print(f"{Fore.GREEN}{result}{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/chat '):
|
||
chat_command = user_input[6:].strip()
|
||
if chat_command.startswith('save '):
|
||
name = chat_command[5:].strip()
|
||
if name:
|
||
self.save_chat_history(name)
|
||
else:
|
||
print(f"{Fore.RED}请提供聊天记录名称.用法: /chat save <名称>{Style.RESET_ALL}")
|
||
continue
|
||
elif chat_command == 'list':
|
||
self.list_chat_history()
|
||
continue
|
||
elif chat_command.startswith('open '):
|
||
name = chat_command[5:].strip()
|
||
if name:
|
||
self.load_chat_history(name)
|
||
else:
|
||
print(f"{Fore.RED}请提供聊天记录名称.用法: /chat open <名称>{Style.RESET_ALL}")
|
||
continue
|
||
else:
|
||
print(f"{Fore.RED}未知的聊天命令.可用命令: save, list, open{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/plugin'):
|
||
plugin_args = user_input[7:].strip()
|
||
result = self.handle_plugin_command(plugin_args)
|
||
if result:
|
||
print(result)
|
||
continue
|
||
|
||
if user_input.startswith('/protect'):
|
||
parts = user_input.split()
|
||
if len(parts) > 1 and parts[1] == 'test':
|
||
# 重新测试所有保护
|
||
from .process_protection import enable_process_protection
|
||
results = enable_process_protection()
|
||
print(f"\n{Fore.CYAN} 进程保护测试结果:{Style.RESET_ALL}")
|
||
for key, val in results.items():
|
||
icon = '' if val else ''
|
||
print(f" {icon} {key}: {val}")
|
||
else:
|
||
status = get_protection_status()
|
||
print(f"\n{Fore.CYAN} 进程保护状态:{Style.RESET_ALL}")
|
||
print(f" 平台: {status.get('platform', '未知')}")
|
||
icon = '' if status.get('is_protected') else ''
|
||
print(f" {icon} 保护已启用: {status.get('is_protected', False)}")
|
||
icon = '' if status.get('single_instance_held') else ''
|
||
print(f" {icon} 单实例锁: {status.get('single_instance_held', False)}")
|
||
icon = '' if status.get('watchdog_running') else ''
|
||
print(f" {icon} 看门狗: {status.get('watchdog_running', False)}")
|
||
print(f" 关闭钩子: {status.get('shutdown_hooks_count', 0)} 个")
|
||
continue
|
||
|
||
if user_input.startswith('/remote'):
|
||
remote_command = user_input[7:].strip()
|
||
self._handle_remote_command(remote_command)
|
||
continue
|
||
|
||
if user_input == '/about':
|
||
self._read_about_file()
|
||
continue
|
||
|
||
if user_input == '/tui':
|
||
print(f"{Fore.CYAN}正在切换到 TUI 模式...{Style.RESET_ALL}")
|
||
self.run_tui()
|
||
print(f"{Fore.CYAN}已从 TUI 模式返回 CLI 模式{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/safe'):
|
||
parts = user_input.split()
|
||
safety = get_safety()
|
||
if len(parts) > 1:
|
||
mode_arg = parts[1].lower()
|
||
if mode_arg in ('off', 'unrestricted', '无限制'):
|
||
safety.set_mode(MODE_UNRESTRICTED)
|
||
elif mode_arg in ('on', 'normal', '普通'):
|
||
safety.set_mode(MODE_NORMAL)
|
||
elif mode_arg in ('manual', '人工', 'all'):
|
||
safety.set_mode(MODE_MANUAL)
|
||
else:
|
||
print(f"{Fore.RED}用法: /safe [off|on|manual]{Style.RESET_ALL}")
|
||
continue
|
||
else:
|
||
# 无参数:循环切换
|
||
safety.cycle_mode()
|
||
mode_name = safety.get_mode_name()
|
||
icons = {"无限制": "", "普通": "", "人工确认": ""}
|
||
icon = icons.get(mode_name, "")
|
||
print(f"{icon} 安全模式: {mode_name}")
|
||
continue
|
||
|
||
if user_input.startswith('/notify'):
|
||
parts = user_input.split()
|
||
nm = get_notification_manager()
|
||
if len(parts) > 1:
|
||
arg = parts[1].lower()
|
||
if arg in ('on', '开启', '启用'):
|
||
nm.set_enabled(True)
|
||
print(f"{Fore.GREEN}🔔 系统通知已开启{Style.RESET_ALL}")
|
||
elif arg in ('off', '关闭', '禁用'):
|
||
nm.set_enabled(False)
|
||
print(f"{Fore.YELLOW}🔕 系统通知已关闭{Style.RESET_ALL}")
|
||
else:
|
||
print(f"{Fore.RED}用法: /notify [on|off]{Style.RESET_ALL}")
|
||
else:
|
||
# 无参数:切换状态
|
||
nm.set_enabled(not nm.enabled)
|
||
if nm.enabled:
|
||
print(f"{Fore.GREEN}🔔 系统通知已开启{Style.RESET_ALL}")
|
||
else:
|
||
print(f"{Fore.YELLOW}🔕 系统通知已关闭{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/'):
|
||
command_parts = user_input[1:].split(' ', 1)
|
||
command = command_parts[0]
|
||
args = command_parts[1] if len(command_parts) > 1 else ""
|
||
if command in self.liugin_commands:
|
||
try:
|
||
result = self.liugin_commands[command](args)
|
||
if result:
|
||
result = self._limit_output_lines(result, max_lines=5)
|
||
print(f"{Fore.GREEN}{result}{Style.RESET_ALL}")
|
||
except Exception as e:
|
||
print(f"{Fore.RED}插件命令执行失败: {e}{Style.RESET_ALL}")
|
||
continue
|
||
|
||
if user_input.startswith('/thinking '):
|
||
thinking_input = user_input[10:].strip()
|
||
processed_response = self.process_thinking_response(thinking_input)
|
||
processed_response = self._process_code_blocks(processed_response)
|
||
user_id_display = f"[用户ID: {self.user_id}]"
|
||
response_lines = processed_response.split('\n')
|
||
for i, line in enumerate(response_lines):
|
||
if i == 0:
|
||
line = " " + line
|
||
if i == len(response_lines) - 1:
|
||
print(f"{line} {user_id_display}")
|
||
else:
|
||
print(f"{line}")
|
||
continue
|
||
|
||
if '@' in user_input:
|
||
processed_input = self._process_file_paths(user_input)
|
||
if processed_input != user_input:
|
||
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||
user_input = processed_input
|
||
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||
|
||
self.process_conversation(user_input)
|
||
except KeyboardInterrupt:
|
||
user_id_display = f"[用户ID: {self.user_id}]"
|
||
print(f"\n{Fore.GREEN}再见! {user_id_display}{Style.RESET_ALL}")
|
||
break
|
||
except Exception as e:
|
||
user_id_display = f"[用户ID: {self.user_id}]"
|
||
print(f"{Fore.RED}发生错误: {e} {user_id_display}{Style.RESET_ALL}")
|
||
|
||
|
||
# ============================================================================
|
||
# Main Entry Point
|
||
# ============================================================================
|
||
def main():
|
||
"""主入口函数"""
|
||
import time
|
||
import sys
|
||
|
||
art_lines = [
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"███████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████████████",
|
||
"██████████ ▓████████████████",
|
||
"██████████ ▒███████████████",
|
||
"██████████ ░██████████████",
|
||
"██████████ ░█████████████",
|
||
"██████████ ████████████",
|
||
"██████████ ███████████",
|
||
"██████████ ▒████████████████████████████████████████████████████",
|
||
"██████████ ░██████████████████████████████████████████████████████",
|
||
"██████████ ████████████████████████████████████████████████████████",
|
||
"██████████ ▓█████████████████████████████████████████████████████████",
|
||
"██████████ ▒███████████████████████████████████████████████████████████",
|
||
"██████████ ███████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓██████████",
|
||
"██████████ ███████████▓▒▓▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒██████████",
|
||
"██████████ ▓██████████▓▒▒▓▒▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▒▓██████████",
|
||
"██████████ ░███████████▒▒▓▒▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▓▒▒▓▒▒▒██████████",
|
||
"██████████ ███████████▒▒▓▒▒▓▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▒▓▓▒██████████",
|
||
"██████████ ███████████▓▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▒▓▒▓▒▒▓▒▒▒▓▒▓▒▒▓▒▒▒▓▒▓▒▒▓▒▒▒▓▒▓▒▓██████████",
|
||
"██████████ ▓██████████▓▒▒▒▒▓▒▒▓▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▓██████████",
|
||
"██████████ ░██████████▓▒▒▓▒▓▒▒▓▒▒▒▒▓▒▒▓▒▓▒▓▒▒▒▓▒▒▓▒▓▒▒▒▓▒▒▓▒▓▒▒▒▓▒▒▓▒▓▒▒▒▓▓██████████",
|
||
"██████████ ███████████▒▓▒▓▒▒▒▓▒▒▓▒▓▒▒▓▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▓▒▒▓▒▒▒▓▒▓▒▒▒▒▒▒▓▒▓▒▓██████████",
|
||
"██████████ ███████████▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▓▒▓▒▓▒▒▒▒▓▒▒▒▓▒▒▒▒▒▓▒▒▓▒▒▒▒▒▓▒▓▒▓▒▒▒▓▓██████████",
|
||
"██████████ ░██████████▒▓▒▓▒▓▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▓▒▒▓▒▓▒▒▓▒▒▓▒▓▒▓▒▒▒▒▓▒▓▒▓▒▒▓▒▒▒▓▒▒▒██████████",
|
||
"██████████ █████████▓▒▒▒▓▒▒▓▒▒▒▓▒▒▓▒▓▒▒▓▒▒▓▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▓▒▒██████████",
|
||
"██████████ █████████▒▒▓▒▒▓▒▒▓▒▓▒▒▓▒▒▒▓▒▒▓▒▒▓▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▒▓██████████",
|
||
"██████████ ▓████████▒▓▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▒▒▓▒▒▓▒▒▓▒▓▒▒▒▓▒▒▓▒▒▓▓▒▒▒██████████",
|
||
"██████████ █████████▒▒▒▒▓▒▒▒▓▒▓▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▓▒▒▓▒▒▒▒▒▓▒██████████",
|
||
"██████████ █████████▒▓▒▓▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▓▒▓▒▒▒▓▒▓▒▒▓▒▒▒▒▒▓▒▒▒██████████",
|
||
"██████████ ▓████████▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▓▒▓▒▒▒▒▒▒▒▓▒▒▓▒▒▓▒▓▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒██████████",
|
||
"██████████ █████████▒▓▒▒▒▒▓▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▓▒▒▒▒▒▒▒██████████",
|
||
"██████████ █████████▒▒▓▒▓▒▒▓▒▓▒▒▒▒▓▒▒▒▓▒▒▓▒▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▓▒▒▒▒▓▒▒▒▒▓▒▒▒▒▒▓▒██████████",
|
||
"██████████ ▓████████▒▓▒▒▒▓▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▓▒▒▒▒▒▓██████████",
|
||
"██████████ █████████▒▒▓▒▓▒▒▓▒▓▒▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ █████████▒▓▒▒▒▓▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ ▓████████▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▓▒▓▒▒▒▒▒▒▒▓▒▒▓▒▒▓▒▓▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▒██████████",
|
||
"██████████ █████████▒▓▒▒▒▒▓▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ █████████▒▒▓▒▓▒▒▓▒▓▒▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ ▓████████▒▓▒▒▒▓▒▒▒▒▒▒▓▒▓▒▒▓▒▒▒▓▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ █████████▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▓▒▓▒▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ █████████▒▓▒▒▒▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"██████████ ▓████████▒▒▓▒▒▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▒▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"███████████ █████████▒▓▒▒▒▒▒▒▓▒▒▒▒▓▒▒▓▒▒▒▒▒▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"█████████████▓ █████████▒▒▓▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"█████████████████ ▓████████▒▓▒▒▒▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▒▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"███████████████████▒ █████████▒▒▓▒▒▒▓▒▒▓▒▒▒▒▓▒▒▒▓▒▒▒▒▒▒▒▒▓▒▒▓▒▒▒▓▒▒▒▓▒▒▒▓▒▒▓▒▒▒▒▒▓▒▒▒▒▒▒██████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
"█████████████████████████████████████████████████████████████████████████████████████████████████████",
|
||
]
|
||
|
||
if sys.platform == 'win32':
|
||
import codecs
|
||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
|
||
|
||
for line in art_lines:
|
||
try:
|
||
print(line, flush=True)
|
||
except UnicodeEncodeError:
|
||
print(line.encode('gbk', errors='ignore').decode('gbk'), flush=True)
|
||
time.sleep(0.05)
|
||
|
||
cli = AICLI()
|
||
cli.run()
|