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
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 '' in content:
parts = content.split('')
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: 处理 ... 格式
if response.startswith(""):
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", "")
print(f"\n{Fore.GREEN} FC 调用: {tool_name}({tool_args[:60]}){Style.RESET_ALL}")
# 通过插件管理器执行
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)
if response and self.shared_conversation_history is not None:
self.shared_conversation_history.append({
"role": "assistant",
"content": response
})
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}")
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}")
# 询问 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('/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()