Q机器人,接ollama 本地AI的Nonebot插件教程
·
1.环境配置
打开.env,输入ollama的API接口,DRIVER改为fastapi(如图)
2. 插件位置确定,例如我这里为src文件夹为插件文件夹|
3·进入命令行,创建插件



创建一个新插件(之后内容略)
4。进入创建的插件文件夹(推荐)
配置以下两个py文件
--init--.py和config.py(代码随后提供)
5. 重新进入命令行,启动
6.测试(注意,第一个命令来自其他博主的插件,请勿测试)

附录:代码
--int--.py
# ollamachat/__init__.py
import json
import re
import httpx
from datetime import datetime
from typing import Dict, Any
from pathlib import Path
from nonebot import on_command, on_message
from nonebot.plugin import PluginMetadata
from nonebot.matcher import Matcher
from nonebot.params import CommandArg, EventMessage
from nonebot.adapters.onebot.v11 import Message, MessageEvent, MessageSegment
from nonebot.log import logger
from nonebot.exception import FinishedException
from nonebot.rule import to_me
from .config import Config
from nonebot.plugin import get_plugin_config
__plugin_meta__ = PluginMetadata(
name="OllamaChat",
description="基于Ollama的AI聊天插件",
usage=(
"命令列表:\n"
"/model - 选择模型\n"
"/model.t <温度> - 设置温度(0~2)\n"
"/model.q - 退出AI模式\n"
"/model.prompt - 设置系统预设\n"
"/model.prompt.rm - 删除预设\n"
"/model.prompt.q - 退出预设编辑模式\n"
"/model.think.on - 显示<think>内容\n"
"/model.think.off - 隐藏<think>内容\n"
"/AIhelp - 显示帮助"
),
type="application",
config=Config,
homepage="https://github.com/your-repo/ollamachat",
supported_adapters={"~onebot.v11"},
)
config = get_plugin_config(Config)
# 状态管理
user_states: Dict[str, Dict[str, Any]] = {}
# 初始化路径
HISTORY_DIR = Path(config.history_dir).expanduser().resolve()
HISTORY_DIR.mkdir(parents=True, exist_ok=True, mode=0o755)
OLLAMA_API = config.ollama_api.rstrip("/")
# 命令处理器
model_cmd = on_command("model", aliases={"模型"}, priority=5, block=True)
model_temp_cmd = on_command("model.t", priority=5, block=True)
model_quit_cmd = on_command("model.q", priority=5, block=True)
prompt_cmd = on_command("model.prompt", priority=5, block=True)
prompt_rm_cmd = on_command("model.prompt.rm", priority=5, block=True)
model_think_on_cmd = on_command("model.think.on", priority=5, block=True)
model_think_off_cmd = on_command("model.think.off", priority=5, block=True)
help_cmd = on_command("AIhelp", priority=5, block=True)
ai_matcher = on_message(rule=to_me(), priority=10, block=False)
# --------------------------
# 增强型工具函数
# --------------------------
async def save_history(user_id: str, query: str, response: str):
"""安全保存聊天记录"""
date_str = datetime.now().strftime("%Y-%m-%d")
filename = HISTORY_DIR / f"{user_id}_{date_str}.jsonl"
log_entry = {
"timestamp": datetime.now().isoformat(),
"query": query,
"response": response
}
try:
with open(filename, "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
except (IOError, PermissionError) as e:
logger.error(f"历史记录保存失败: {repr(e)}")
except Exception as e:
logger.error(f"未知保存错误: {repr(e)}")
async def get_ollama_models() -> list[str]:
"""安全获取模型列表"""
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.get(f"{OLLAMA_API}/api/tags")
if resp.status_code != 200:
raise httpx.RequestError(f"API响应异常: {resp.status_code}")
data = resp.json()
return [model["name"] for model in data.get("models", [])]
except httpx.RequestError as e:
logger.error(f"网络请求失败: {repr(e)}")
return []
except json.JSONDecodeError as e:
logger.error(f"JSON解析失败: {repr(e)}")
return []
except KeyError as e:
logger.error(f"数据格式异常: {repr(e)}")
return []
async def generate_response(user_id: str, prompt: str) -> str:
"""增强型回复生成"""
state = user_states.get(user_id, {})
if not state.get("selected_model"):
return "❌ 请先使用/model选择模型"
data = {
"model": state["selected_model"],
"prompt": prompt,
"stream": False,
"options": {
"temperature": state.get("temperature", 0.7),
"num_predict": 512,
}
}
if system_prompt := state.get("system_prompt"):
data["system"] = system_prompt
try:
async with httpx.AsyncClient(timeout=300) as client:
resp = await client.post(f"{OLLAMA_API}/api/generate", json=data)
resp.raise_for_status()
response_text = resp.json().get("response", "❌ 收到空响应")
# 改进的<think>处理:删除标签及前后空白
if not state.get("show_think", False):
response_text = re.sub(
r'\s*<think>.*?</think>\s*',
'',
response_text,
flags=re.DOTALL
)
# 处理可能残留的多余换行
response_text = re.sub(r'\n{3,}', '\n\n', response_text)
# 去除首尾空白
response_text = response_text.strip()
return response_text
except httpx.RequestError as e:
logger.error(f"API请求失败: {repr(e)}")
return "🔧 服务暂时不可用,请稍后再试"
except KeyError as e:
logger.error(f"响应格式异常: {repr(e)}")
return "⚠️ 响应解析失败,请重试"
# --------------------------
# 以下命令处理部分保持不变(与之前相同)
# --------------------------
@model_cmd.handle()
async def handle_model_select(event: MessageEvent, matcher: Matcher):
"""模型选择入口"""
user_id = str(event.user_id)
try:
models = await get_ollama_models()
if not models:
await matcher.finish("❌ 没有可用模型,请检查Ollama服务")
user_states[user_id] = {
"mode": "model_selection",
"model_list": models,
"retry_count": 0
}
msg = "📜 可用模型列表:\n" + "\n".join(
f"{i + 1}. {model}" for i, model in enumerate(models)
)
await matcher.send(msg + "\n🔢 请回复数字选择模型")
except Exception as e:
logger.error(f"模型选择初始化失败: {repr(e)}")
await matcher.finish("❌ 服务初始化失败")
@model_cmd.got("model_choice")
async def handle_model_choice(event: MessageEvent, matcher: Matcher):
"""强化模型选择处理"""
user_id = str(event.user_id)
choice = event.get_plaintext().strip()
state = user_states.get(user_id, {})
# 状态验证
if not state.get("model_list"):
try:
models = await get_ollama_models()
if not models:
await matcher.finish("❌ 模型列表获取失败,请重试")
state["model_list"] = models
except Exception as e:
logger.error(f"模型列表恢复失败: {repr(e)}")
await matcher.finish("❌ 服务不可用,请稍后重试")
models = state["model_list"]
max_retry = 3
try:
index = int(choice) - 1
if index < 0 or index >= len(models):
state["retry_count"] += 1
if state["retry_count"] >= max_retry:
user_states.pop(user_id, None)
await matcher.finish("❌ 重试次数过多,已退出选择")
await matcher.reject(f"❌ 请输入1~{len(models)}之间的数字")
selected_model = models[index]
except ValueError:
state["retry_count"] += 1
if state["retry_count"] >= max_retry:
user_states.pop(user_id, None)
await matcher.finish("❌ 重试次数过多,已退出选择")
await matcher.reject("❌ 请输入有效数字")
# 更新状态
user_states[user_id] = {
"mode": "ai_chat",
"selected_model": selected_model,
"temperature": 0.7,
"model_list": models,
"show_think": False
}
try:
await matcher.finish(f"✅ 已选择模型: {selected_model}\n💬 进入AI模式,输入/model.q退出\n🔍 需要@我进行对话")
except FinishedException:
logger.debug("正常流程结束")
@model_temp_cmd.handle()
async def handle_temperature(event: MessageEvent, matcher: Matcher, args: Message = CommandArg()):
"""温度设置强化版"""
user_id = str(event.user_id)
temp_str = args.extract_plaintext().strip()
if not temp_str:
await matcher.reject("❌ 请输入温度值,例如:/model.t 0.8")
try:
temperature = round(float(temp_str), 1)
if not 0 <= temperature <= 2:
await matcher.finish("🌡️ 温度值需在0~2之间")
new_state = user_states.get(user_id, {})
new_state.update({
"temperature": temperature,
"mode": new_state.get("mode", "ai_chat")
})
user_states[user_id] = new_state
await matcher.finish(f"✅ 温度已设置为 {temperature}")
except ValueError:
await matcher.reject("❌ 请输入有效数字,例如:/model.t 0.8")
except Exception as e:
logger.error(f"温度设置异常: {repr(e)}")
await matcher.finish("❌ 设置失败,请重试")
@model_quit_cmd.handle()
async def handle_model_quit(event: MessageEvent, matcher: Matcher):
"""安全退出处理"""
user_id = str(event.user_id)
user_states.pop(user_id, None)
await matcher.finish("✅ 已退出AI模式")
@model_think_on_cmd.handle()
async def handle_think_on(event: MessageEvent, matcher: Matcher):
"""启用think显示"""
user_id = str(event.user_id)
if user_id not in user_states:
await matcher.finish("❌ 请先进入AI模式")
user_states[user_id]["show_think"] = True
await matcher.finish("✅ 已启用<think>内容显示")
@model_think_off_cmd.handle()
async def handle_think_off(event: MessageEvent, matcher: Matcher):
"""禁用think显示"""
user_id = str(event.user_id)
if user_id not in user_states:
await matcher.finish("❌ 请先进入AI模式")
user_states[user_id]["show_think"] = False
await matcher.finish("✅ 已禁用<think>内容显示")
@prompt_cmd.handle()
async def handle_prompt(event: MessageEvent, matcher: Matcher):
"""预设模式入口"""
user_id = str(event.user_id)
if user_id not in user_states or user_states[user_id].get("mode") != "ai_chat":
await matcher.finish("❌ 请先进入AI模式再设置预设")
user_states[user_id]["mode"] = "prompt_editing"
await matcher.send(
"📝 请输入系统预设(输入 /model.prompt.q 退出):\n"
"可用变量:{username}(用户名)"
)
@prompt_cmd.got("prompt_input")
async def handle_prompt_input(event: MessageEvent, matcher: Matcher):
"""预设输入处理"""
user_id = str(event.user_id)
prompt = event.get_plaintext().strip()
if prompt == "/model.prompt.q":
user_states[user_id]["mode"] = "ai_chat"
await matcher.finish("✅ 已退出预设编辑模式")
return
username = event.sender.nickname or "用户"
final_prompt = prompt.replace("{username}", username)
user_states.setdefault(user_id, {}).update({
"system_prompt": final_prompt,
"mode": "ai_chat"
})
await matcher.finish("✅ 预设已更新!")
@prompt_rm_cmd.handle()
async def handle_prompt_remove(event: MessageEvent, matcher: Matcher):
"""安全删除预设"""
user_id = str(event.user_id)
if user_states.get(user_id, {}).get("system_prompt"):
del user_states[user_id]["system_prompt"]
await matcher.finish("✅ 已删除系统预设")
else:
await matcher.finish("ℹ️ 当前没有预设配置")
@help_cmd.handle()
async def handle_help(matcher: Matcher):
"""帮助信息格式化"""
help_msg = MessageSegment.text(__plugin_meta__.usage)
await matcher.finish(help_msg)
# --------------------------
# AI消息处理强化版
# --------------------------
@ai_matcher.handle()
async def handle_ai_chat(event: MessageEvent, matcher: Matcher):
"""安全消息处理"""
user_id = str(event.user_id)
state = user_states.get(user_id, {})
if state.get("mode") != "ai_chat":
return
message = event.get_plaintext().strip()
if not message or message.startswith('/'):
return
try:
response = await generate_response(user_id, message)
await save_history(user_id, message, response)
await matcher.send(MessageSegment.text(response))
except Exception as e:
logger.error(f"AI处理异常: {repr(e)}")
await matcher.send("⚠️ 消息处理失败,请稍后再试")
config.py
from pydantic import BaseModel, Field
class Config(BaseModel):
ollama_api: str = Field(
"http://127.0.0.1:11434",
description="Ollama API地址,结尾不要带斜线"
)
history_dir: str = Field(
r"C:\Users\your_username\Qbot\history",
description="历史记录存储路径,使用原始字符串"
)
__all__ = ["Config"]
说明:ollama接口为11434(若不是请自行修改)
更多推荐



所有评论(0)