Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
26 KiB
JSON
1 line
26 KiB
JSON
{"repo": "aim467/mini-code-assistant", "files": {"mini_code_assistant/cli.py": {"e0947de8c580e4c2df044bf8db00b9a6e309f822": {"t": 1786068188.6040792, "n": 21223, "h": "81108dcd5b7ddcba", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\ncli.py - Mini Code Assistant 命令行入口\n\n核心原理:\n 这是整个编程助手的入口,实现了 **REPL**(Read-Eval-Print Loop)交互循环\n 和 **Agent Loop**(智能体循环)。\n\n REPL 循环(外层):\n 用户输入 → 处理 → 等待下一次输入\n\n Agent Loop(内层):\n 用户消息 → 发给 LLM(流式)→ LLM 逐块返回文本/工具调用\n → 如果有工具调用 → 执行工具 → 结果喂回 LLM → 继续推理\n → 如果只有文本 → 显示最终回复,结束循环\n\n 这两个嵌套循环就是 Claude Code、Codex 等工具的基本架构。\n 流式输出(SSE)让用户实时看到 LLM 的生成过程,无需等待完整响应。\n\n使用方式:\n # 全局安装后(推荐)\n mca # 在当前目录运行\n mca /path/to/project # 在指定目录运行\n mca --model deepseek-chat # 覆盖模型\n mca --api-key sk-xxx # 覆盖 API 密钥\n\n # 未安装,直接运行模块\n python -m mini_code_assistant [目录] [选项]\n\n配置优先级(从高到低):\n 1. 命令行参数 (--model, --api-key, --base-url)\n 2. Shell 环境变量 (export API_KEY=xxx)\n 3. 本地 ./.env 文件(项目级配置)\n 4. 全局 ~/.mca/.env 文件(用户级配置)\n 5. 内置默认值\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport argparse\nfrom pathlib import Path\n\nfrom .llm import LLMClient\nfrom .tools import ToolSystem\nfrom .context import Context\nfrom .token_counter import format_token_count, HAS_TIKTOKEN\nfrom . import __version__\n\n# ── Rich 可选依赖 ────────────────────────────────────────────\n# 如果安装了 rich,用 Markdown 渲染 + 面板展示;否则降级为纯文本\ntry:\n from rich.console import Console\n from rich.markdown import Markdown\n from rich.panel import Panel\n\n _console = Console()\n HAS_RICH = True\nexcept ImportError:\n _console = None\n HAS_RICH = False\n\n# ── prompt_toolkit 可选依赖 ──────────────────────────────────\n# 如果安装了 prompt_toolkit,用增强 REPL(命令历史、自动补全、多行编辑);\n# 否则降级为 input() 原始 REPL\ntry:\n from prompt_toolkit import PromptSession\n from prompt_toolkit.completion import Completer, Completion\n from prompt_toolkit.history import FileHistory\n\n HAS_PROMPT_TOOLKIT = True\nexcept ImportError:\n HAS_PROMPT_TOOLKIT = False\n\n\n# ── 多路径 .env 加载器 ───────────────────────────────────────────\n# 不依赖 python-dotenv,手动解析 .env 文件\n# 支持两级配置:全局 ~/.mca/.env + 本地 ./.env(本地覆盖全局)\ndef load_env():\n \"\"\"\n 加载配置文件,优先级(从高到低):\n 1. Shell 环境变量(已存在的不会被覆盖)\n 2. 本地 ./.env(项目级配置,覆盖全局)\n 3. 全局 ~/.mca/.env(用户级配置)\n\n 实现方式:先加载全局配置到字典,再用本地配置覆盖,\n 最后用 setdefault 写入环境变量(不覆盖已有的 shell 变量)。\n \"\"\"\n config = {}\n\n # 1. 先加载全局配置(~/.mca/.env)\n global_path = Path.home() / \".mca\" / \".env\"\n _parse_env_file(global_path, config)\n\n # 2. 再加载本地配置(./.env),覆盖全局\n local_path = Path.cwd() / \".env\"\n _parse_env_file(local_path, config)\n\n # 3. 应用到环境变量(不覆盖已有的 shell 变量)\n for key, value in config.items():\n os.environ.setdefault(key, value)\n\n\ndef _parse_env_file(path: Path, config: dict):\n \"\"\"解析 .env 文件,将键值对写入 config 字典。\"\"\"\n if not path.exists():\n return\n for line in path.read_text(encoding=\"utf-8\").splitlines():\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, value = line.split(\"=\", 1)\n config[key.strip()] = value.strip().strip('\"').strip(\"'\")\n\n\n# ── 终端颜色 ───────────────────────────────────────────────────\nclass C:\n \"\"\"ANSI 颜色码简写。\"\"\"\n CYAN = \"\\033[96m\"\n GREEN = \"\\033[92m\"\n YELLOW = \"\\033[93m\"\n RED = \"\\033[91m\"\n DIM = \"\\033[2m\"\n BOLD = \"\\033[1m\"\n RESET = \"\\033[0m\"\n\n\ndef print_banner(workdir: Path, model: str, context_limit: int):\n \"\"\"打印启动横幅。\"\"\"\n token_mode = \"tiktoken\" if HAS_TIKTOKEN else \"estimated\"\n repl_mode = \"prompt_toolkit\" if HAS_PROMPT_TOOLKIT else \"basic\"\n stream_mode = \"streaming\" # 流式输出始终启用\n if HAS_RICH:\n _console.print(Panel(\n f\"[bold cyan]Mini Code Assistant[/] v{__version__}\\n\"\n f\"[dim]workdir:[/] {workdir}\\n\"\n f\"[dim]model: [/] {model}\\n\"\n f\"[dim]context:[/] {context_limit:,} tokens [dim]token count: {token_mode}[/]\\n\"\n f\"[dim]repl: [/] {repl_mode} [dim]output:[/] {stream_mode}\",\n border_style=\"cyan\",\n padding=(1, 2),\n ))\n if HAS_PROMPT_TOOLKIT:\n _console.print(\" type [yellow]/help[/] for help, [yellow]/exit[/] to quit [dim](↑↓ history, Tab complete, Ctrl+R search)[/]\\n\")\n else:\n _console.print(\" type [yellow]/help[/] for help, [yellow]/exit[/] to quit\\n\")\n else:\n print(f\"{C.CYAN}{C.BOLD}\")\n print(\" +======================================+\")\n print(\" | Mini Code Assistant |\")\n print(\" | v\" + __version__ + \" - \" + \" \" * (22 - len(__version__)) + \" |\")\n print(\" +======================================+\")\n print(f\"{C.RESET}\")\n print(f\" {C.DIM}workdir:{C.RESET} {workdir}\")\n print(f\" {C.DIM}model: {C.RESET} {model}\")\n print(f\" {C.DIM}context:{C.RESET} {context_limit:,} tokens {C.DIM}(token count: {token_mode}){C.RESET}\")\n print(f\" {C.DIM}repl: {C.RESET} {repl_mode} {C.DIM}output:{C.RESET} {stream_mode}\")\n print(f\" {C.DIM}--------------------------------------{C.RESET}\")\n if HAS_PROMPT_TOOLKIT:\n print(f\" type {C.YELLOW}/help{C.RESET} for help, {C.YELLOW}/exit{C.RESET} to quit {C.DIM}(↑↓ history, Tab complete, Ctrl+R search){C.RESET}\\n\")\n else:\n print(f\" type {C.YELLOW}/help{C.RESET} for help, {C.YELLOW}/exit{C.RESET} to quit\\n\")\n\n\ndef print_help():\n \"\"\"打印帮助信息。\"\"\"\n if HAS_RICH:\n repl_hint = \"\"\n if HAS_PROMPT_TOOLKIT:\n repl_hint = (\n \"\\n[bold]REPL Shortcuts:[/]\\n\"\n \" [yellow]↑/↓[/] navigate command history\\n\"\n \" [yellow]Tab[/] auto-complete /commands\\n\"\n \" [yellow]Ctrl+R[/] search command history\\n\"\n )\n _console.print(Panel(\n \"[bold]Commands:[/]\\n\"\n \" [yellow]/help[/] show this help\\n\"\n \" [yellow]/exit[/] quit\\n\"\n \" [yellow]/clear[/] clear conversation history\\n\"\n \" [yellow]/files[/] list files in workdir\\n\"\n \" [yellow]/tokens[/] show token usage\\n\"\n f\"{repl_hint}\\n\"\n \"[bold]Usage:[/]\\n\"\n \" Just type your question or task, e.g.:\\n\"\n ' [dim]\"read main.py and find issues\"[/]\\n'\n ' [dim]\"add a logging function to utils.py\"[/]\\n'\n ' [dim]\"search for all uses of requests\"[/]',\n title=\"Help\",\n border_style=\"yellow\",\n ))\n else:\n print(f\"\\n {C.BOLD}Commands:{C.RESET}\")\n print(f\" {C.YELLOW}/help{C.RESET} show this help\")\n print(f\" {C.YELLOW}/exit{C.RESET} quit\")\n print(f\" {C.YELLOW}/clear{C.RESET} clear conversation history\")\n print(f\" {C.YELLOW}/files{C.RESET} list files in workdir\")\n print(f\" {C.YELLOW}/tokens{C.RESET} show token usage\")\n if HAS_PROMPT_TOOLKIT:\n print(f\" {C.DIM}--------------------------------------{C.RESET}\")\n print(f\" {C.BOLD}REPL Shortcuts:{C.RESET}\")\n print(f\" {C.YELLOW}↑/↓{C.RESET} navigate command history\")\n print(f\" {C.YELLOW}Tab{C.RESET} auto-complete /commands\")\n print(f\" {C.YELLOW}Ctrl+R{C.RESET} search command history\")\n print(f\" {C.DIM}--------------------------------------{C.RESET}\")\n print(f\" {C.BOLD}Usage:{C.RESET}\")\n print(f\" Just type your question or task, e.g.:\")\n print(f' {C.DIM}\"read main.py and find issues\"{C.RESET}')\n print(f' {C.DIM}\"add a logging function to utils.py\"{C.RESET}')\n print(f' {C.DIM}\"search for all uses of requests\"{C.RESET}\\n')\n\n\n# ── prompt_toolkit 增强交互 ──────────────────────────────────\n# 命令补全器:为 /help, /exit 等斜杠命令提供 Tab 补全\nclass SlashCommandCompleter(Completer):\n \"\"\"斜杠命令补全器,支持 Tab 自动补全。\"\"\"\n\n COMMANDS = {\n \"/help\": \"show this help\",\n \"/exit\": \"quit\",\n \"/clear\": \"clear conversation history\",\n \"/files\": \"list files in workdir\",\n \"/tokens\": \"show token usage\",\n }\n\n def get_completions(self, document, complete_event):\n text = document.text_before_cursor\n # 只在输入以 / 开头时触发补全\n if text.startswith(\"/\"):\n for cmd, desc in self.COMMANDS.items():\n if cmd.startswith(text):\n yield Completion(\n cmd,\n start_position=-len(text),\n display=f\"{cmd} ({desc})\",\n )\n\n\ndef create_prompt_session() -> \"PromptSession | None\":\n \"\"\"\n 创建 prompt_toolkit 的 PromptSession。\n\n 功能:\n - 命令历史:持久化到 ~/.mca/history,跨会话保留\n - Tab 补全:输入 / 时自动补全斜杠命令\n - 多行编辑:Shift+Enter 换行,Enter 提交\n - Emacs/Vi 模式:自动检测用户偏好\n\n Returns:\n PromptSession 实例,或 None(如果 prompt_toolkit 未安装)\n \"\"\"\n if not HAS_PROMPT_TOOLKIT:\n return None\n\n # 历史文件路径:~/.mca/history\n history_dir = Path.home() / \".mca\"\n history_dir.mkdir(parents=True, exist_ok=True)\n history_file = history_dir / \"history\"\n\n return PromptSession(\n history=FileHistory(str(history_file)),\n completer=SlashCommandCompleter(),\n multiline=False, # 单行模式,与原始 REPL 行为一致\n enable_history_search=True, # Ctrl+R 搜索历史\n )\n\n\ndef _prompt_style():\n \"\"\"返回 prompt_toolkit 的样式配置。\"\"\"\n if not HAS_PROMPT_TOOLKIT:\n return None\n from prompt_toolkit.styles import Style\n\n return Style.from_dict({\n \"prompt\": \"bold ansigreen\", # >>> 提示符:绿色加粗,与原始 REPL 一致\n })\n\n\n# ── 核心:Agent Loop ───────────────────────────────────────────\ndef run_agent_loop(llm: LLMClient, tools: ToolSystem, context: Context):\n \"\"\"\n Agent 循环——编程助手的核心引擎。\n\n 流程:\n 1. 把对话历史(含用户最新消息)发给 LLM(流式)\n 2. LLM 返回回复:\n a. 如果包含 tool_calls → 执行工具,把结果加入历史,回到步骤 1\n b. 如果只有文本 → 这是最终回复,显示给用户,结束循环\n\n 流式输出原理:\n 使用 chat_stream() 逐块接收 LLM 响应,文本增量实时打印,\n 工具调用增量逐步拼接。用户无需等待完整响应即可看到输出。\n\n 这个循环会重复执行,直到:\n - LLM 不再请求工具调用(给出最终回复)\n - 达到最大迭代次数(防止无限循环)\n - 发生错误\n \"\"\"\n MAX_ITERATIONS = 20 # 安全限制:防止 LLM 陷入无限工具调用循环\n\n # 记录 Agent Loop 开始前的消息数,用于检测是否发生了摘要\n msg_count_before = len(context.messages)\n\n for iteration in range(MAX_ITERATIONS):\n # ── 流式调用 LLM ────────────────────────────────────\n # 逐块接收响应,实时显示文本,拼接工具调用\n content = \"\" # 累积完整文本\n tool_calls_map = {} # 累积工具调用:{index: {id, function: {name, arguments}}}\n finish_reason = None\n has_error = False\n\n # ── 实时显示文本流 ──────────────────────────────────\n # 流式输出时,文本片段逐个到达,立即打印让用户看到\n if HAS_RICH:\n _console.print() # 输出前空一行\n else:\n print() # 输出前空一行\n\n for chunk in llm.chat_stream(context.messages, tools.definitions):\n chunk_type = chunk[\"type\"]\n\n # ── 文本增量:实时打印 ──────────────────────────\n if chunk_type == \"text_delta\":\n text_delta = chunk[\"content\"]\n content += text_delta\n # 逐块打印,无需等待完整响应\n if HAS_RICH:\n _console.print(text_delta, end=\"\", highlight=False)\n else:\n print(f\"{C.CYAN}{text_delta}{C.RESET}\", end=\"\", flush=True)\n\n # ── 工具调用增量:逐步拼接 ──────────────────────\n elif chunk_type == \"tool_call_delta\":\n idx = chunk[\"index\"]\n if idx not in tool_calls_map:\n # 第一个 chunk:包含 id 和 name\n tool_calls_map[idx] = {\n \"id\": chunk.get(\"id\") or \"\",\n \"type\": \"function\",\n \"function\": {\"name\": \"\", \"arguments\": \"\"},\n }\n tc = tool_calls_map[idx]\n # 拼接 id\n if chunk.get(\"id\"):\n tc[\"id\"] = chunk[\"id\"]\n # 拼接 name\n if chunk.get(\"name\"):\n tc[\"function\"][\"name\"] = chunk[\"name\"]\n # 拼接 arguments 片段\n if chunk.get(\"arguments_delta\"):\n tc[\"function\"][\"arguments\"] += chunk[\"arguments_delta\"]\n\n # ── 流结束 ──────────────────────────────────────\n elif chunk_type == \"done\":\n finish_reason = chunk[\"finish_reason\"]\n\n # ── 错误 ────────────────────────────────────────\n elif chunk_type == \"error\":\n has_error = True\n if HAS_RICH:\n _console.print(f\"\\n [yellow]{chunk['content']}[/]\")\n else:\n print(f\"\\n {C.YELLOW}{chunk['content']}{C.RESET}\")\n return\n\n # 流式输出结束,换行\n if HAS_RICH:\n _console.print()\n else:\n print()\n\n # ── 将工具调用 map 转为列表格式 ─────────────────────\n # 与非流式 chat() 返回的 tool_calls 格式保持一致\n tool_calls = []\n for idx in sorted(tool_calls_map.keys()):\n tool_calls.append(tool_calls_map[idx])\n\n # ── 没有工具调用 → LLM 给出了最终回复,结束循环 ──\n if not tool_calls:\n context.add_assistant_message(content)\n _print_token_usage(context, msg_count_before)\n return\n\n # ── 有工具调用 → 记录助手消息(含 tool_calls)─────\n context.add_assistant_message(content, tool_calls)\n\n # ── 执行每个工具调用 ──────────────────────────────\n for tc in tool_calls:\n func = tc[\"function\"]\n name = func[\"name\"]\n\n # 解析参数(LLM 返回的是 JSON 字符串)\n try:\n args = json.loads(func[\"arguments\"])\n except json.JSONDecodeError:\n args = {}\n\n # 展示工具调用信息\n args_str = \", \".join(f\"{k}={repr(v)[:60]}\" for k, v in args.items())\n if HAS_RICH:\n _console.print(f\" [dim]tool:[/] [bold]{name}[/]({args_str})\")\n else:\n print(f\"\\n {C.DIM}tool: {C.RESET}{name}({args_str})\")\n\n # 执行工具\n result = tools.execute(name, args)\n\n # 展示结果(截断过长的输出)\n preview = result\n if len(preview) > 600:\n preview = preview[:600] + f\"\\n ...(total {len(result)} chars)\"\n if HAS_RICH:\n _console.print(Panel(\n preview,\n title=f\"[dim]result:[/] {name}\",\n border_style=\"dim\",\n padding=(0, 1),\n ))\n else:\n print(f\" {C.DIM}result:{C.RESET}\\n{preview}\")\n\n # 把工具结果加入对话历史(LLM 下一轮会看到)\n context.add_tool_result(tc[\"id\"], result)\n\n # 达到最大迭代次数\n if HAS_RICH:\n _console.print(f\"\\n [yellow]Max iterations ({MAX_ITERATIONS}) reached.[/]\")\n else:\n print(f\"\\n {C.YELLOW}Max iterations ({MAX_ITERATIONS}) reached.{C.RESET}\")\n _print_token_usage(context, msg_count_before)\n\n\ndef _print_token_usage(context: Context, msg_count_before: int):\n \"\"\"打印 token 用量和摘要通知。\"\"\"\n token_count = context.token_count()\n token_str = format_token_count(token_count, context.context_limit)\n\n # 检查是否发生了摘要(消息数减少了)\n summarized = len(context.messages) < msg_count_before\n\n if HAS_RICH:\n parts = [f\"[dim]tokens:[/] {token_str}\"]\n if summarized:\n parts.append(\"[yellow](auto-summarized)[/]\")\n if context._summary:\n parts.append(f\"[dim]({len(context.messages)} msgs)[/]\")\n _console.print(\" \" + \" \".join(parts))\n else:\n parts = [f\"{C.DIM}tokens:{C.RESET} {token_str}\"]\n if summarized:\n parts.append(f\"{C.YELLOW}(auto-summarized){C.RESET}\")\n if context._summary:\n parts.append(f\"{C.DIM}({len(context.messages)} msgs){C.RESET}\")\n print(\" \" + \" \".join(parts))\n\n\n# ── 主函数 ─────────────────────────────────────────────────────\ndef main():\n # ── 解析命令行参数 ──────────────────────────────────────\n parser = argparse.ArgumentParser(\n prog=\"mca\",\n description=\"Mini Code Assistant - a mini AI coding assistant for learning\",\n epilog=(\n \"Config via env vars or .env file: API_KEY, BASE_URL, MODEL\\n\"\n \" Local config: ./.env\\n\"\n \" Global config: ~/.mca/.env\"\n ),\n )\n parser.add_argument(\n \"directory\",\n nargs=\"?\",\n default=\".\",\n help=\"working directory (default: current directory)\",\n )\n parser.add_argument(\n \"-m\", \"--model\",\n help=\"override model name (e.g. gpt-4o, deepseek-chat)\",\n )\n parser.add_argument(\n \"-k\", \"--api-key\",\n help=\"override API key\",\n )\n parser.add_argument(\n \"-u\", \"--base-url\",\n help=\"override API base URL\",\n )\n parser.add_argument(\n \"-t\", \"--temperature\",\n type=float,\n help=\"override sampling temperature (default: 0.3)\",\n )\n parser.add_argument(\n \"-V\", \"--version\",\n action=\"version\",\n version=f\"Mini Code Assistant {__version__}\",\n )\n args = parser.parse_args()\n\n # ── 加载配置 ────────────────────────────────────────────\n load_env()\n\n # 配置优先级:命令行参数 > 环境变量 > .env 文件 > 默认值\n api_key = args.api_key or os.getenv(\"API_KEY\", \"\")\n base_url = args.base_url or os.getenv(\"BASE_URL\", \"https://api.openai.com/v1\")\n model = args.model or os.getenv(\"MODEL\", \"gpt-4o\")\n temperature = args.temperature if args.temperature is not None else float(os.getenv(\"TEMPERATURE\", \"0.3\"))\n\n # 工作目录\n working_dir = Path(args.directory).resolve()\n if not working_dir.exists():\n print(f\"Error: working directory does not exist: {working_dir}\")\n sys.exit(1)\n\n if not api_key:\n print(\"Error: API_KEY is required.\")\n print(\"\\nYou can configure it via:\")\n print(\" 1. CLI flag: mca --api-key sk-xxx\")\n print(\" 2. Local env: create .env in current directory\")\n print(\" 3. Global env: create ~/.mca/.env\")\n print(\" 4. Shell env: export API_KEY=sk-xxx\")\n sys.exit(1)\n\n # 初始化核心组件\n llm = LLMClient(api_key, base_url, model, temperature=temperature)\n tools = ToolSystem(working_dir)\n context = Context(working_dir, llm=llm, model=model)\n\n print_banner(working_dir, model, context.context_limit)\n\n # ── 创建增强 REPL 会话(如果 prompt_toolkit 可用)────\n session = create_prompt_session()\n\n # ── REPL 循环 ──────────────────────────────────────\n while True:\n try:\n if session is not None:\n # prompt_toolkit 增强 REPL:支持历史、补全、搜索\n user_input = session.prompt(\n [(\"class:prompt\", \">>> \")],\n style=_prompt_style() if HAS_PROMPT_TOOLKIT else None,\n ).strip()\n else:\n # 降级为原始 input()\n user_input = input(f\"{C.GREEN}{C.BOLD}>>> {C.RESET}\").strip()\n except (EOFError, KeyboardInterrupt):\n if HAS_RICH:\n _console.print(\"[dim]Bye![/]\\n\")\n else:\n print(f\"\\n{C.DIM}Bye!{C.RESET}\\n\")\n break\n\n if not user_input:\n continue\n\n # ── 处理特殊命令 ─────────────────────────────────\n if user_input.startswith(\"/\"):\n cmd = user_input.lower()\n if cmd == \"/exit\":\n if HAS_RICH:\n _console.print(\"[dim]Bye![/]\\n\")\n else:\n print(f\"{C.DIM}Bye!{C.RESET}\\n\")\n break\n elif cmd == \"/help\":\n print_help()\n continue\n elif cmd == \"/clear\":\n context.clear()\n if HAS_RICH:\n _console.print(\"[dim]Conversation history cleared.[/]\\n\")\n else:\n print(f\"{C.DIM}Conversation history cleared.{C.RESET}\\n\")\n continue\n elif cmd == \"/files\":\n result = tools.execute(\"list_files\", {})\n print(result)\n continue\n elif cmd == \"/tokens\":\n token_str = format_token_count(context.token_count(), context.context_limit)\n msg_count = len(context.messages)\n if HAS_RICH:\n _console.print(f\"[dim]messages:[/] {msg_count} [dim]tokens:[/] {token_str}\", style=\"white\")\n if context._summary:\n _console.print(\"[dim]context has been auto-summarized[/]\")\n else:\n print(f\"\\n {C.DIM}messages:{C.RESET} {msg_count} {C.DIM}tokens:{C.RESET} {token_str}\")\n if context._summary:\n print(f\" {C.DIM}(context has been auto-summarized){C.RESET}\")\n print()\n continue\n else:\n if HAS_RICH:\n _console.print(f\"[yellow]Unknown command: {user_input}[/]\")\n else:\n print(f\"{C.YELLOW}Unknown command: {user_input}{C.RESET}\")\n continue\n\n # ── 正常对话:启动 Agent Loop ────────────────────\n context.add_user_message(user_input)\n try:\n run_agent_loop(llm, tools, context)\n except KeyboardInterrupt:\n if HAS_RICH:\n _console.print(\"[yellow]Task interrupted.[/]\")\n else:\n print(f\"\\n{C.YELLOW}Task interrupted.{C.RESET}\")\n except Exception as e:\n if HAS_RICH:\n _console.print(f\"[yellow]Error: {e}[/]\")\n else:\n print(f\"\\n{C.YELLOW}Error: {e}{C.RESET}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}}}} |