From 1e24ceac68917b76c81a97afe40412bc5a6443e1 Mon Sep 17 00:00:00 2001 From: riesa Date: Mon, 29 Jun 2026 14:38:53 +0800 Subject: [PATCH] feat: group-shared DeepSeek context for /ds - Replace nonebot-plugin-deepseek with local src/plugins/deepseek plugin - Share conversation history within the same group across all users - Isolate contexts between different groups/private chats - Trim history to DEEPSEEK__MAX_HISTORY (default 20) messages - Add /ds reset to clear current session --- .env | 5 +- pyproject.toml | 1 - src/plugins/deepseek/__init__.py | 138 +++++++++++++++++++++++++++++++ src/plugins/deepseek/config.py | 13 +++ uv.lock | 33 -------- 5 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 src/plugins/deepseek/__init__.py create mode 100644 src/plugins/deepseek/config.py diff --git a/.env b/.env index 34e2b1c..4d0b7b5 100644 --- a/.env +++ b/.env @@ -18,7 +18,6 @@ PLUGINS='[ "nonebot_plugin_shorturl", "nonebot_bison", "nonebot_plugin_treehelp", - "nonebot_plugin_deepseek", "nonebot_plugin_llmchat", "nonebot_plugin_zssm" ]' @@ -85,6 +84,10 @@ SHORTURL_HOST SHORTURL_ENDPOINT ## deepseek config +# DEEPSEEK__API_KEY=sk-xxx +DEEPSEEK__BASE_URL=https://api.deepseek.com +DEEPSEEK__MODEL=deepseek-chat +DEEPSEEK__MAX_HISTORY=20 DEEPSEEK__TIMEOUT=600 ## zssm config diff --git a/pyproject.toml b/pyproject.toml index a851f52..df808b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ dependencies = [ "nonebot-bison >=0.9.0, <1.0.0", "nonebot-plugin-alconna >=0.38.0, <1.0.0", "nonebot-plugin-apscheduler >=0.4.0, <1.0.0", - "nonebot-plugin-deepseek >=0.1.0, <1.0.0", "nonebot-plugin-filehost >=1.1.0, <2.0.0", "nonebot-plugin-llmchat >=0.1.0, <1.0.0", "nonebot-plugin-sentry >=2.0.0, <3.0.0", diff --git a/src/plugins/deepseek/__init__.py b/src/plugins/deepseek/__init__.py new file mode 100644 index 0000000..14a30d8 --- /dev/null +++ b/src/plugins/deepseek/__init__.py @@ -0,0 +1,138 @@ +from typing import Any + +import httpx +from nonebot import get_plugin_config, logger +from nonebot.adapters import Event +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Match, MultiVar, on_alconna + +from .config import Config + +config = get_plugin_config(Config) +ds_config = config.deepseek + +__plugin_meta__ = PluginMetadata( + name="DeepSeek 群聊版", + description="群内共享上下文的 DeepSeek 问答", + usage="/ds <内容> | /ds reset", + config=Config, +) + +# {session_id: [message, ...]} +_sessions: dict[str, list[dict[str, str]]] = {} + + +async def chat_with_deepseek(messages: list[dict[str, str]]) -> str: + if not ds_config.api_key: + raise RuntimeError("未配置 DeepSeek API Key") + + headers = { + "Authorization": f"Bearer {ds_config.api_key}", + "Content-Type": "application/json", + } + json_data = { + "model": ds_config.model, + "messages": messages, + } + + async with httpx.AsyncClient(timeout=ds_config.timeout) as client: + response = await client.post( + f"{ds_config.base_url}/chat/completions", + headers=headers, + json=json_data, + ) + response.raise_for_status() + data: dict[str, Any] = response.json() + + if error := data.get("error"): + raise RuntimeError(error.get("message", "DeepSeek API 返回错误")) + + try: + message = data["choices"][0]["message"] + except (KeyError, IndexError) as e: + raise RuntimeError("DeepSeek 返回格式异常") from e + + content = message.get("content", "") + return content.strip() + + +def _get_session_id(event: Event) -> str: + """按群(或频道)隔离会话;私聊按用户隔离。""" + message_type = getattr(event, "message_type", None) + if message_type == "group": + group_id = getattr(event, "group_id", None) + if group_id is not None: + return f"group_{group_id}" + if message_type == "private": + return f"private_{event.get_user_id()}" + guild_id = getattr(event, "guild_id", None) + channel_id = getattr(event, "channel_id", None) + if guild_id is not None and channel_id is not None: + return f"guild_{guild_id}_{channel_id}" + return event.get_session_id() + + +def _get_user_name(event: Event) -> str: + user_id = event.get_user_id() + try: + sender = getattr(event, "sender", None) + if sender: + nickname = getattr(sender, "nickname", None) + if nickname: + return str(nickname) + except Exception: + pass + return user_id + + +def _trim_history(messages: list[dict[str, str]]) -> list[dict[str, str]]: + limit = ds_config.max_history + if len(messages) > limit: + return messages[-limit:] + return messages + + +deepseek_cmd = on_alconna( + Alconna("deepseek", Args["content?", MultiVar("str")]), + aliases={"ds"}, + use_cmd_start=True, +) + + +@deepseek_cmd.handle() +async def handle_deepseek(event: Event, content: Match[tuple[str, ...]]) -> None: + if not content.available: + await deepseek_cmd.finish(__plugin_meta__.usage) + + text = " ".join(content.result).strip() + if not text: + await deepseek_cmd.finish(__plugin_meta__.usage) + + session_id = _get_session_id(event) + + if text.lower() == "reset": + _sessions.pop(session_id, None) + await deepseek_cmd.finish("已重置当前会话") + + user_name = _get_user_name(event) + messages = _sessions.setdefault(session_id, []) + messages.append({"role": "user", "content": f"[{user_name}] {text}"}) + messages = _trim_history(messages) + _sessions[session_id] = messages + + try: + reply = await chat_with_deepseek(messages) + except httpx.HTTPError as e: + logger.opt(colors=True, exception=e).error("DeepSeek API request failed") + await deepseek_cmd.finish(f"请求 DeepSeek 失败:\n{e!r}") + except RuntimeError as e: + await deepseek_cmd.finish(str(e)) + except Exception as e: + logger.opt(colors=True, exception=e).error("Unexpected DeepSeek error") + await deepseek_cmd.finish(f"DeepSeek 发生未知错误:\n{e!r}") + + messages.append({"role": "assistant", "content": reply}) + messages = _trim_history(messages) + _sessions[session_id] = messages + + await deepseek_cmd.finish(reply) diff --git a/src/plugins/deepseek/config.py b/src/plugins/deepseek/config.py new file mode 100644 index 0000000..5683b80 --- /dev/null +++ b/src/plugins/deepseek/config.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel, Field + + +class DeepSeekConfig(BaseModel): + api_key: str = "" + base_url: str = "https://api.deepseek.com" + model: str = "deepseek-chat" + max_history: int = Field(default=20, ge=1, le=100) + timeout: int = 100 + + +class Config(BaseModel): + deepseek: DeepSeekConfig = Field(default_factory=DeepSeekConfig) diff --git a/uv.lock b/uv.lock index 8c5d3f1..f3466ed 100644 --- a/uv.lock +++ b/uv.lock @@ -166,7 +166,6 @@ dependencies = [ { name = "nonebot-bison" }, { name = "nonebot-plugin-alconna" }, { name = "nonebot-plugin-apscheduler" }, - { name = "nonebot-plugin-deepseek" }, { name = "nonebot-plugin-filehost" }, { name = "nonebot-plugin-llmchat" }, { name = "nonebot-plugin-sentry" }, @@ -197,7 +196,6 @@ requires-dist = [ { name = "nonebot-bison", specifier = ">=0.9.0,<1.0.0" }, { name = "nonebot-plugin-alconna", specifier = ">=0.38.0,<1.0.0" }, { name = "nonebot-plugin-apscheduler", specifier = ">=0.4.0,<1.0.0" }, - { name = "nonebot-plugin-deepseek", specifier = ">=0.1.0,<1.0.0" }, { name = "nonebot-plugin-filehost", specifier = ">=1.1.0,<2.0.0" }, { name = "nonebot-plugin-llmchat", specifier = ">=0.1.0,<1.0.0" }, { name = "nonebot-plugin-sentry", specifier = ">=2.0.0,<3.0.0" }, @@ -302,20 +300,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] -[[package]] -name = "cli-lite" -version = "0.11.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "arclet-alconna" }, - { name = "arclet-alconna-tools" }, - { name = "importlib-metadata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/1e/ff103a40e897e3103151659b53151281ed0d261d4c8b3fbe6a9f6f7a61b9/cli_lite-0.11.3.tar.gz", hash = "sha256:7911c6a2603b2289e72defbd3185a0deb81f926f909cc8bd15299593166c0e95", size = 7055, upload-time = "2025-08-25T07:42:15.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/37/c9bd8b8fd6d0ad105b0c6060ca4181fda9e9192eaa3fbad421dc73d0b577/cli_lite-0.11.3-py3-none-any.whl", hash = "sha256:93d3e68bc532a2c39505295b52f3adc8f9bc950f1683256ff3004234d595231d", size = 8247, upload-time = "2025-08-25T07:42:13.745Z" }, -] - [[package]] name = "click" version = "8.4.1" @@ -1661,23 +1645,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/3a/3742894d5c774ec9fcc786ef629a058e21760d22c0c7acdc9a7820b57e57/nonebot_plugin_datastore-1.3.1-py3-none-any.whl", hash = "sha256:3f6fa91e9fac8edd143b7213371307c40598faa3c2a5f20fbda1f3b686a4fa95", size = 26791, upload-time = "2025-08-13T15:03:18.244Z" }, ] -[[package]] -name = "nonebot-plugin-deepseek" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "cli-lite" }, - { name = "httpx", extra = ["http2"] }, - { name = "nonebot-plugin-alconna" }, - { name = "nonebot-plugin-localstore" }, - { name = "nonebot2" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/14/5fd4d74053c9c858114a123bea2f325a8b6137210bc53ca3b756d73de18e/nonebot_plugin_deepseek-0.2.1.tar.gz", hash = "sha256:2f348276507174a7f4df108368b75f1286c61a6b8d44a342880fc343c3773a08", size = 27379, upload-time = "2026-05-04T08:45:13.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/a8/b07f673f8cbfc5571f69c6355644bd52e12de7675bcaa8e4c93310579fc0/nonebot_plugin_deepseek-0.2.1-py3-none-any.whl", hash = "sha256:3f184c0fe3cd8170d1ddbf04eecec7ca7e8f6b6df35f1788784b44edeed466bb", size = 37196, upload-time = "2026-05-04T08:45:12.228Z" }, -] - [[package]] name = "nonebot-plugin-filehost" version = "1.1.1"