From 7a72a5c69e482249c287455bed420b9e16618296 Mon Sep 17 00:00:00 2001 From: xiaoli-cli-bot Date: Wed, 29 Apr 2026 19:51:42 +0800 Subject: [PATCH] fix: Ollama model validation, TUI CSS, WebUI overhaul 1. Ollama: validate model is configured before API call, show helpful error 2. TUI: remove unsupported scroll-behavior CSS property 3. WebUI server: add full tool calling loop (FC + JSON action) 4. WebUI HTML: complete redesign - Modern dark theme with better typography - Markdown rendering via marked.js - Tool call/result visualization with colored cards - Dynamic sidebar (engines + tools from server) - Better input area with gradient send button - Responsive layout --- ai_engines/ollama_engine.py | 7 + webui.html | 1232 +++++++++-------------------------- webui_server.py | 138 +++- xcli_core/tui.py | 3 +- 4 files changed, 415 insertions(+), 965 deletions(-) diff --git a/ai_engines/ollama_engine.py b/ai_engines/ollama_engine.py index 2535d02..1cfe2bb 100644 --- a/ai_engines/ollama_engine.py +++ b/ai_engines/ollama_engine.py @@ -122,6 +122,13 @@ Ollama AI引擎插件帮助信息 if not self.is_service_running: return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎." + # 检查模型是否已配置 + if not self.model or not self.model.strip(): + return ("错误:Ollama模型未配置。请执行以下操作:\n" + " 1. /engine.ollama models → 查看本地可用模型\n" + " 2. /engine.ollama set <模型名> → 设置要使用的模型\n" + " 或编辑 config.json 中 api.engines.ollama.model 字段") + # 如果有工具结果,将其作为上下文返回给AI处理 if tool_results: tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}" diff --git a/webui.html b/webui.html index 655df29..6dde764 100644 --- a/webui.html +++ b/webui.html @@ -5,752 +5,195 @@ 小狸 Pro-CLI +
- +
-
- - - +
+
小狸
- -
- - +
+ +
-
-
- - 连接中... -
-
- - 普通 -
+
连接中...
+
普通
- + - +
-
+
- +

小狸 Pro-CLI

-

智能编程助手 — 代码编辑 · Git · 浏览器自动化 · 多 Agent 协作

-
- - - - +

智能编程助手 — 代码编辑 · Git · 浏览器自动化 · 多引擎支持

+
+ + + +
- -
-
- -
@@ -760,239 +203,205 @@ body {
- +
-
-

已加载工具

-
+
+

已加载工具

+
- diff --git a/webui_server.py b/webui_server.py index 3f5bf93..9d99ccb 100644 --- a/webui_server.py +++ b/webui_server.py @@ -97,36 +97,103 @@ class AIEngineBridge: } def chat(self, message: str, callback=None): - """发送消息到 AI 并获取回复""" + """发送消息到 AI 并获取回复(支持工具调用循环)""" if not self.cli: - return "❌ 小狸核心未初始化,请检查配置" + return {"type": "error", "message": "小狸核心未初始化,请检查配置"} + + results = [] # 收集所有事件 try: - # 使用小狸的核心对话处理 - import io - import contextlib + # 添加用户消息到历史 + self.cli.shared_conversation_history.append({ + "role": "user", "content": message + }) - # 捕获输出 - captured = io.StringIO() - with contextlib.redirect_stdout(captured): - # 构建系统提示词 - liugin_prompts = self.cli.get_liugin_usage_prompts() - system_prompt = self.cli._build_system_prompt(liugin_prompts) + liugin_prompts = self.cli.get_liugin_usage_prompts() + system_prompt = self.cli._build_system_prompt(liugin_prompts) - # 生成响应 - if self.cli.current_engine: - response = self.cli.current_engine.generate_response( - message, - system_prompt=system_prompt - ) - else: - response = "❌ 没有可用的 AI 引擎" + loop_count = 0 + max_loops = 15 + current_input = message - output = captured.getvalue() - return response or "(无响应)" + while loop_count < max_loops: + # 生成 AI 响应 + if not self.cli.current_engine: + return {"type": "error", "message": "没有可用的 AI 引擎"} + + response = self.cli.current_engine.generate_response( + current_input, system_prompt=system_prompt + ) + + if not response: + return {"type": "reply", "message": "(无响应)"} + + # 检查 FC 响应(Ollama Function Calling) + fc_result = self.cli._handle_fc_response(response, message) + if fc_result: + # FC 工具已执行,结果作为下一轮输入 + results.append({"type": "fc_tool_done", "message": fc_result[:200]}) + current_input = fc_result + loop_count += 1 + continue + + # 解析混合响应(文本 + JSON 工具调用) + try: + text_content, json_data = self.cli._parse_mixed_response(response) + except Exception: + text_content, json_data = response, None + + if json_data and isinstance(json_data, dict): + if json_data.get('action') == 'use_tool': + tool_name = json_data.get('tool', '') + tool_args = json_data.get('args', '') + results.append({"type": "tool_call", "name": tool_name, "args": tool_args[:200]}) + + # 执行工具 + try: + tool_result = self.cli._execute_tool_by_name(tool_name, tool_args) + except Exception as e: + tool_result = f"工具执行错误: {e}" + + results.append({"type": "tool_result", "name": tool_name, "result": str(tool_result)[:500]}) + + # 把工具结果加入历史并继续循环 + self.cli.shared_conversation_history.append({ + "role": "tool", "content": str(tool_result), + "name": tool_name + }) + current_input = f"工具 {tool_name} 的执行结果:\n{tool_result}" + loop_count += 1 + continue + + elif json_data.get('continue') or json_data.get('need_continue'): + msg = json_data.get('message', text_content or '') + if msg: + results.append({"type": "partial_reply", "message": msg}) + self.cli.shared_conversation_history.append({"role": "assistant", "content": msg}) + current_input = "继续" + loop_count += 1 + continue + + elif json_data.get('message'): + final = json_data.get('message') + self.cli.shared_conversation_history.append({"role": "assistant", "content": final}) + results.append({"type": "reply", "message": final}) + break + + # 纯文本响应 + display = text_content if text_content else response + self.cli.shared_conversation_history.append({"role": "assistant", "content": display}) + results.append({"type": "reply", "message": display}) + break + + if not results: + results.append({"type": "reply", "message": response or "(无响应)"}) + + return {"type": "multi", "events": results} except Exception as e: - return f"❌ 错误: {e}" + return {"type": "error", "message": f"错误: {e}"} def execute_tool(self, tool_name: str, tool_args: str): """直接执行工具""" @@ -195,15 +262,30 @@ async def ws_handler(websocket): # 在线程中执行 AI 调用(避免阻塞事件循环) loop = asyncio.get_event_loop() - response = await loop.run_in_executor( + result = await loop.run_in_executor( None, bridge.chat, user_msg ) - # 发送回复 - await websocket.send(json.dumps({ - "type": "reply", - "data": {"message": response}, - }, ensure_ascii=False)) + # 处理返回结果 + if isinstance(result, dict): + if result.get("type") == "multi": + # 多事件:逐个发送 + for event in result.get("events", []): + await websocket.send(json.dumps({ + "type": event.get("type", "reply"), + "data": event, + }, ensure_ascii=False)) + else: + await websocket.send(json.dumps({ + "type": result.get("type", "reply"), + "data": result, + }, ensure_ascii=False)) + else: + # 兼容旧格式 + await websocket.send(json.dumps({ + "type": "reply", + "data": {"message": str(result)}, + }, ensure_ascii=False)) elif msg_type == "tool": # 直接工具调用 diff --git a/xcli_core/tui.py b/xcli_core/tui.py index 8312132..6b2fb33 100644 --- a/xcli_core/tui.py +++ b/xcli_core/tui.py @@ -62,8 +62,7 @@ if TEXTUAL_AVAILABLE: scrollbar-color: {_Theme.BORDER}; scrollbar-color-hover: {_Theme.TEXT_MUTED}; padding: 0 1; - /* 平滑滚动 */ - scroll-behavior: smooth; + }} /* ── 输入区域 ── */