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
This commit is contained in:
1 parent
baa2cbc49a
commit
7a72a5c69e
4 files changed
+406
-956
No files matched your search
@@ -122,6 +122,13 @@ Ollama AI引擎插件帮助信息
|
|||||||
if not self.is_service_running:
|
if not self.is_service_running:
|
||||||
return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎."
|
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处理
|
# 如果有工具结果,将其作为上下文返回给AI处理
|
||||||
if tool_results:
|
if tool_results:
|
||||||
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
|
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
|
||||||
|
|||||||
+293
-931
File diff suppressed because it is too large.
Load diff
+105
-23
@@ -97,36 +97,103 @@ class AIEngineBridge:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def chat(self, message: str, callback=None):
|
def chat(self, message: str, callback=None):
|
||||||
"""发送消息到 AI 并获取回复"""
|
"""发送消息到 AI 并获取回复(支持工具调用循环)"""
|
||||||
if not self.cli:
|
if not self.cli:
|
||||||
return "❌ 小狸核心未初始化,请检查配置"
|
return {"type": "error", "message": "小狸核心未初始化,请检查配置"}
|
||||||
|
|
||||||
|
results = [] # 收集所有事件
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用小狸的核心对话处理
|
# 添加用户消息到历史
|
||||||
import io
|
self.cli.shared_conversation_history.append({
|
||||||
import contextlib
|
"role": "user", "content": message
|
||||||
|
})
|
||||||
|
|
||||||
# 捕获输出
|
|
||||||
captured = io.StringIO()
|
|
||||||
with contextlib.redirect_stdout(captured):
|
|
||||||
# 构建系统提示词
|
|
||||||
liugin_prompts = self.cli.get_liugin_usage_prompts()
|
liugin_prompts = self.cli.get_liugin_usage_prompts()
|
||||||
system_prompt = self.cli._build_system_prompt(liugin_prompts)
|
system_prompt = self.cli._build_system_prompt(liugin_prompts)
|
||||||
|
|
||||||
# 生成响应
|
loop_count = 0
|
||||||
if self.cli.current_engine:
|
max_loops = 15
|
||||||
response = self.cli.current_engine.generate_response(
|
current_input = message
|
||||||
message,
|
|
||||||
system_prompt=system_prompt
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = "❌ 没有可用的 AI 引擎"
|
|
||||||
|
|
||||||
output = captured.getvalue()
|
while loop_count < max_loops:
|
||||||
return response or "(无响应)"
|
# 生成 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:
|
except Exception as e:
|
||||||
return f"❌ 错误: {e}"
|
return {"type": "error", "message": f"错误: {e}"}
|
||||||
|
|
||||||
def execute_tool(self, tool_name: str, tool_args: str):
|
def execute_tool(self, tool_name: str, tool_args: str):
|
||||||
"""直接执行工具"""
|
"""直接执行工具"""
|
||||||
@@ -195,14 +262,29 @@ async def ws_handler(websocket):
|
|||||||
|
|
||||||
# 在线程中执行 AI 调用(避免阻塞事件循环)
|
# 在线程中执行 AI 调用(避免阻塞事件循环)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
response = await loop.run_in_executor(
|
result = await loop.run_in_executor(
|
||||||
None, bridge.chat, user_msg
|
None, bridge.chat, user_msg
|
||||||
)
|
)
|
||||||
|
|
||||||
# 发送回复
|
# 处理返回结果
|
||||||
|
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({
|
await websocket.send(json.dumps({
|
||||||
"type": "reply",
|
"type": "reply",
|
||||||
"data": {"message": response},
|
"data": {"message": str(result)},
|
||||||
}, ensure_ascii=False))
|
}, ensure_ascii=False))
|
||||||
|
|
||||||
elif msg_type == "tool":
|
elif msg_type == "tool":
|
||||||
|
|||||||
+1
-2
@@ -62,8 +62,7 @@ if TEXTUAL_AVAILABLE:
|
|||||||
scrollbar-color: {_Theme.BORDER};
|
scrollbar-color: {_Theme.BORDER};
|
||||||
scrollbar-color-hover: {_Theme.TEXT_MUTED};
|
scrollbar-color-hover: {_Theme.TEXT_MUTED};
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
/* 平滑滚动 */
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/* ── 输入区域 ── */
|
/* ── 输入区域 ── */
|
||||||
|
|||||||
Reference in new issue
Block a user