Compare commits

...

2 Commits

Author SHA1 Message Date
taylorxie
9f25fdab12 add 2026-03-09 22:43:02 +08:00
taylorxie
b53a918aaa add 2026-03-09 22:34:58 +08:00
4 changed files with 201 additions and 21 deletions

View File

@@ -31,12 +31,33 @@ platforms:
max_tokens: 2000 max_tokens: 2000
max_words: 1000 max_words: 1000
max_context_messages: 20 max_context_messages: 20
qwen:
# 国内: https://dashscope.aliyuncs.com
# 海外: https://dashscope-intl.aliyuncs.com
url: https://dashscope.aliyuncs.com
api_key:
model: qwen-plus
temperature: 0.7
top_p: 0.8
max_tokens: 2000
max_words: 1000
max_context_messages: 20
# 是否开启深度思考模式(仅 qwq 系列支持)
enable_thinking: false
deepseek: deepseek:
url: https://api.deepseek.com url: https://api.deepseek.com
api_key: api_key:
model: model:
max_words: 1000 max_words: 1000
max_context_messages: 20 max_context_messages: 20
gemini:
url: https://generativelanguage.googleapis.com
api_key:
model: gemini-2.0-flash
temperature: 1
max_tokens: 2000
max_words: 1000
max_context_messages: 20
openai: openai:
url: https://api.openai.com url: https://api.openai.com
api_key: api_key:
@@ -52,6 +73,8 @@ platforms:
max_words: 1000 max_words: 1000
max_tokens: 2000 max_tokens: 2000
max_context_messages: 20 max_context_messages: 20
# 是否开启流式输出(开启后 Element 中消息会逐步更新)
streaming: false
xai: xai:
url: https://api.x.ai url: https://api.x.ai
api_key: api_key:
@@ -60,14 +83,7 @@ platforms:
max_tokens: 1000 max_tokens: 1000
max_words: 2000 max_words: 2000
max_context_messages: 20 max_context_messages: 20
gemini:
url: https://generativelanguage.googleapis.com
api_key:
model: gemini-2.0-flash
temperature: 1
max_tokens: 2000
max_words: 1000
max_context_messages: 20
# additional prompt # additional prompt
additional_prompt: additional_prompt:

View File

@@ -10,7 +10,7 @@ from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot_llmplus.local_paltform import Ollama, LmStudio from maubot_llmplus.local_paltform import Ollama, LmStudio
from maubot_llmplus.platforms import Platform from maubot_llmplus.platforms import Platform
from maubot_llmplus.plugin import AbsExtraConfigPlugin, Config from maubot_llmplus.plugin import AbsExtraConfigPlugin, Config
from maubot_llmplus.thrid_platform import OpenAi, Anthropic, XAi, Deepseek, Gemini from maubot_llmplus.thrid_platform import OpenAi, Anthropic, XAi, Deepseek, Gemini, Qwen
class AiBotPlugin(AbsExtraConfigPlugin): class AiBotPlugin(AbsExtraConfigPlugin):
@@ -124,15 +124,17 @@ class AiBotPlugin(AbsExtraConfigPlugin):
await event.mark_read() await event.mark_read()
await self.client.set_typing(event.room_id, timeout=99999) await self.client.set_typing(event.room_id, timeout=99999)
platform = self.get_ai_platform() platform = self.get_ai_platform()
if platform.is_streaming_enabled():
await self.client.set_typing(event.room_id, timeout=0)
await self._handle_streaming(event, platform)
return
chat_completion = await platform.create_chat_completion(self, event) chat_completion = await platform.create_chat_completion(self, event)
self.log.debug( self.log.debug(
f"发送结果 {chat_completion.message}, {chat_completion.model}, {chat_completion.finish_reason}") f"发送结果 {chat_completion.message}, {chat_completion.model}, {chat_completion.finish_reason}")
# ai gpt调用
# 关闭typing提示
await self.client.set_typing(event.room_id, timeout=0) await self.client.set_typing(event.room_id, timeout=0)
# 打开typing提示
if chat_completion.result: if chat_completion.result:
# if hasattr(chat_completion.message, 'content'):
resp_content = chat_completion.message['content'] resp_content = chat_completion.message['content']
response = TextMessageEventContent(msgtype=MessageType.TEXT, body=resp_content, format=Format.HTML, response = TextMessageEventContent(msgtype=MessageType.TEXT, body=resp_content, format=Format.HTML,
formatted_body=markdown.render(resp_content)) formatted_body=markdown.render(resp_content))
@@ -150,6 +152,48 @@ class AiBotPlugin(AbsExtraConfigPlugin):
return None return None
async def _handle_streaming(self, evt: MessageEvent, platform) -> None:
# 发送初始占位消息
placeholder = TextMessageEventContent(
msgtype=MessageType.TEXT, body="", format=Format.HTML, formatted_body=""
)
response_event_id = await evt.respond(placeholder, in_thread=self.config['reply_in_thread'])
accumulated = ""
last_edit_len = 0
EDIT_THRESHOLD = 50 # 每积累50个字符更新一次消息
try:
async for chunk in platform.create_chat_completion_stream(self, evt):
accumulated += chunk
if len(accumulated) - last_edit_len >= EDIT_THRESHOLD:
display = accumulated + ""
new_content = TextMessageEventContent(
msgtype=MessageType.TEXT,
body=display,
format=Format.HTML,
formatted_body=markdown.render(display)
)
new_content.set_edit(response_event_id)
await self.client.send_message(evt.room_id, new_content)
last_edit_len = len(accumulated)
except Exception as e:
self.log.exception(f"Streaming error: {e}")
if not accumulated:
accumulated = f"Streaming error: {e}"
# 输出最终完整内容
if not accumulated:
accumulated = "(无响应)"
final_content = TextMessageEventContent(
msgtype=MessageType.TEXT,
body=accumulated,
format=Format.HTML,
formatted_body=markdown.render(accumulated)
)
final_content.set_edit(response_event_id)
await self.client.send_message(evt.room_id, final_content)
def get_ai_platform(self) -> Platform: def get_ai_platform(self) -> Platform:
use_platform = self.config.cur_platform use_platform = self.config.cur_platform
if use_platform == 'openai': if use_platform == 'openai':
@@ -162,6 +206,8 @@ class AiBotPlugin(AbsExtraConfigPlugin):
return Deepseek(self.config, self.http) return Deepseek(self.config, self.http)
if use_platform == 'gemini': if use_platform == 'gemini':
return Gemini(self.config, self.http) return Gemini(self.config, self.http)
if use_platform == 'qwen':
return Qwen(self.config, self.http)
if use_platform == 'local_ai#ollama': if use_platform == 'local_ai#ollama':
return Ollama(self.config, self.http) return Ollama(self.config, self.http)
if use_platform == 'local_ai#lmstudio': if use_platform == 'local_ai#lmstudio':
@@ -300,7 +346,7 @@ class AiBotPlugin(AbsExtraConfigPlugin):
self.config.cur_model = self.config['platforms'][argus.split("#")[0]]['model'] self.config.cur_model = self.config['platforms'][argus.split("#")[0]]['model']
await event.react("") await event.react("")
# 如果是openai或者是claude # 如果是openai或者是claude
elif argus == 'openai' or argus == 'anthropic' or argus == 'xai' or argus == 'deepseek' or argus == 'gemini': elif argus == 'openai' or argus == 'anthropic' or argus == 'xai' or argus == 'deepseek' or argus == 'gemini' or argus == 'qwen':
if argus == self.config.cur_platform: if argus == self.config.cur_platform:
await event.reply(f"current ai platform has be {argus}") await event.reply(f"current ai platform has be {argus}")
pass pass

View File

@@ -1,7 +1,7 @@
import json import json
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
from typing import Optional, List, Generator from typing import Optional, List, Generator, AsyncIterator
from aiohttp import ClientSession from aiohttp import ClientSession
from maubot import Plugin from maubot import Plugin
@@ -55,6 +55,12 @@ class Platform:
async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion: async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion:
raise NotImplementedError() raise NotImplementedError()
async def create_chat_completion_stream(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> AsyncIterator[str]:
raise NotImplementedError()
def is_streaming_enabled(self) -> bool:
return False
async def list_models(self) -> List[str]: async def list_models(self) -> List[str]:
raise NotImplementedError() raise NotImplementedError()

View File

@@ -129,10 +129,22 @@ class OpenAi(Platform):
class Anthropic(Platform): class Anthropic(Platform):
max_tokens: int max_tokens: int
streaming: bool
def __init__(self, config: BaseProxyConfig, http: ClientSession) -> None: def __init__(self, config: BaseProxyConfig, http: ClientSession) -> None:
super().__init__(config, http) super().__init__(config, http)
self.max_tokens = self.config['max_tokens'] self.max_tokens = self.config['max_tokens']
self.streaming = self.config.get('streaming', False)
def is_streaming_enabled(self) -> bool:
return self.streaming
def _build_request(self, full_chat_context: list) -> tuple:
endpoint = f"{self.url}/v1/messages"
headers = {"x-api-key": self.api_key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
req_body = {"model": self.model, "max_tokens": self.max_tokens, "system": self.system_prompt,
"messages": full_chat_context}
return endpoint, headers, req_body
async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion: async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion:
full_chat_context = [] full_chat_context = []
@@ -140,10 +152,7 @@ class Anthropic(Platform):
chat_context = await maubot_llmplus.platforms.get_chat_context(system_context, plugin, self, evt) chat_context = await maubot_llmplus.platforms.get_chat_context(system_context, plugin, self, evt)
full_chat_context.extend(list(chat_context)) full_chat_context.extend(list(chat_context))
endpoint = f"{self.url}/v1/messages" endpoint, headers, req_body = self._build_request(full_chat_context)
headers = {"x-api-key": self.api_key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
req_body = {"model": self.model, "max_tokens": self.max_tokens, "system": self.system_prompt,
"messages": full_chat_context}
async with self.http.post(endpoint, headers=headers, data=json.dumps(req_body)) as response: async with self.http.post(endpoint, headers=headers, data=json.dumps(req_body)) as response:
# plugin.log.debug(f"响应内容:{response.status}, {await response.json()}") # plugin.log.debug(f"响应内容:{response.status}, {await response.json()}")
@@ -162,6 +171,33 @@ class Anthropic(Platform):
finish_reason=response_json['stop_reason'], finish_reason=response_json['stop_reason'],
model=response_json['model'] model=response_json['model']
) )
async def create_chat_completion_stream(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent):
full_chat_context = []
system_context = deque()
chat_context = await maubot_llmplus.platforms.get_chat_context(system_context, plugin, self, evt)
full_chat_context.extend(list(chat_context))
endpoint, headers, req_body = self._build_request(full_chat_context)
req_body["stream"] = True
async with self.http.post(endpoint, headers=headers, data=json.dumps(req_body)) as response:
if response.status != 200:
raise ValueError(f"Error: {await response.text()}")
async for line_bytes in response.content:
line = line_bytes.decode("utf-8").strip()
if not line.startswith("data: "):
continue
data_str = line[6:]
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
if data.get("type") == "content_block_delta":
delta = data.get("delta", {})
if delta.get("type") == "text_delta":
yield delta.get("text", "")
except json.JSONDecodeError:
pass pass
async def list_models(self) -> List[str]: async def list_models(self) -> List[str]:
@@ -324,3 +360,79 @@ class XAi(Platform):
def get_type(self) -> str: def get_type(self) -> str:
return "xai" return "xai"
class Qwen(Platform):
max_tokens: int
temperature: float
top_p: float
enable_thinking: bool
def __init__(self, config: BaseProxyConfig, http: ClientSession) -> None:
super().__init__(config, http)
self.max_tokens = self.config['max_tokens']
self.temperature = self.config['temperature']
self.top_p = self.config['top_p']
self.enable_thinking = self.config['enable_thinking']
async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion:
full_context = []
context = await maubot_llmplus.platforms.get_context(plugin, self, evt)
full_context.extend(list(context))
parameters = {
"result_format": "message"
}
if self.max_tokens:
parameters["max_tokens"] = self.max_tokens
if self.temperature is not None:
parameters["temperature"] = self.temperature
if self.top_p is not None:
parameters["top_p"] = self.top_p
if self.enable_thinking:
parameters["enable_thinking"] = True
request_body = {
"model": self.model,
"input": {
"messages": full_context
},
"parameters": parameters
}
endpoint = f"{self.url}/api/v1/services/aigc/text-generation/generation"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
async with self.http.post(endpoint, headers=headers, data=json.dumps(request_body)) as response:
if response.status != 200:
return ChatCompletion(
result=False,
message={},
finish_reason=f"Error: {await response.text()}",
model=None
)
response_json = await response.json()
choice = response_json["output"]["choices"][0]
return ChatCompletion(
result=True,
message=choice["message"],
finish_reason=choice.get("finish_reason", "stop"),
model=response_json.get("model", self.model)
)
async def list_models(self) -> List[str]:
models = [
"qwen-max", "qwen-max-latest",
"qwen-plus", "qwen-plus-latest",
"qwen-turbo", "qwen-turbo-latest",
"qwen-long",
"qwen3-235b-a22b", "qwen3-30b-a3b",
"qwq-plus", "qwq-plus-latest",
]
return [f"- {m}" for m in models]
def get_type(self) -> str:
return "qwen"