mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 19:37:36 +08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a9fed726 | ||
|
|
720a218259 | ||
|
|
60c0adc6f9 | ||
|
|
bc8c346e68 | ||
|
|
a198b6da0b | ||
|
|
0f3dc35df4 | ||
|
|
7b6e6b046a | ||
|
|
9e503191d6 | ||
|
|
1fd23a0d8d | ||
|
|
3811700a78 | ||
|
|
8762ba3d9c | ||
|
|
c42b5aab5a | ||
|
|
d724899ec0 | ||
|
|
81aacdd76e | ||
|
|
0aa072b4e8 | ||
|
|
6335e9dd8b | ||
|
|
a785289ac9 | ||
|
|
f8bace040c | ||
|
|
d62d597695 | ||
|
|
d938129884 | ||
|
|
327f448321 | ||
|
|
19af3740c1 | ||
|
|
11b1110eed | ||
|
|
682b897e21 | ||
|
|
998ad7623c | ||
|
|
4f1db33abc | ||
|
|
ca6cb60bdd | ||
|
|
133e48a5a9 | ||
|
|
d659d01b1e | ||
|
|
34f73fd84b | ||
|
|
54b87ff79d | ||
|
|
6c2843e7c1 | ||
|
|
6761a31982 | ||
|
|
9401a79b2b | ||
|
|
7a4905d943 | ||
|
|
4db1d2b3a3 | ||
|
|
2ffe2967d6 | ||
|
|
0875c0f266 | ||
|
|
68c7de5199 | ||
|
|
4dfb8597ae | ||
|
|
e21a27ff23 | ||
|
|
91ad7944de | ||
|
|
c86602ebaf | ||
|
|
f75ac292db | ||
|
|
2742c249bf | ||
|
|
36f04849ab | ||
|
|
a60c896e89 | ||
|
|
c442320c7f | ||
|
|
6aeae7e9f5 | ||
|
|
cae79aac48 |
@@ -10,6 +10,10 @@ spec:
|
||||
ComponentTemplate:
|
||||
fromFiles:
|
||||
- pkg/platform/adapter.yaml
|
||||
- pkg/provider/modelmgr/requester.yaml
|
||||
MessagePlatformAdapter:
|
||||
fromDirs:
|
||||
- path: pkg/platform/sources/
|
||||
LLMAPIRequester:
|
||||
fromDirs:
|
||||
- path: pkg/provider/modelmgr/requesters/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import dingtalk_stream
|
||||
from dingtalk_stream import AckMessage
|
||||
|
||||
@@ -17,7 +18,7 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler):
|
||||
await self.client.update_incoming_message(incoming_message)
|
||||
|
||||
return AckMessage.STATUS_OK, 'OK'
|
||||
|
||||
|
||||
async def get_incoming_message(self):
|
||||
"""异步等待消息的到来"""
|
||||
while self.incoming_message is None:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
from typing import Callable
|
||||
import dingtalk_stream
|
||||
@@ -92,8 +93,31 @@ class DingTalkClient:
|
||||
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
||||
return base64_str
|
||||
else:
|
||||
raise Exception("获取图片失败")
|
||||
|
||||
raise Exception("获取文件失败")
|
||||
|
||||
async def get_audio_url(self,download_code:str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
||||
params = {
|
||||
"downloadCode":download_code,
|
||||
"robotCode":self.robot_code
|
||||
}
|
||||
headers ={
|
||||
"x-acs-dingtalk-access-token": self.access_token
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
download_url = result.get("downloadUrl")
|
||||
if download_url:
|
||||
return await self.download_url_to_base64(download_url)
|
||||
else:
|
||||
raise Exception("获取音频失败")
|
||||
else:
|
||||
raise Exception(f"Error: {response.status_code}, {response.text}")
|
||||
|
||||
async def update_incoming_message(self, message):
|
||||
"""异步更新 DingTalkClient 中的 incoming_message"""
|
||||
message_data = await self.get_message(message)
|
||||
@@ -133,6 +157,7 @@ class DingTalkClient:
|
||||
|
||||
async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage):
|
||||
try:
|
||||
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
|
||||
message_data = {
|
||||
"IncomingMessage":incoming_message,
|
||||
}
|
||||
@@ -160,10 +185,14 @@ class DingTalkClient:
|
||||
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
|
||||
|
||||
message_data['Type'] = 'image'
|
||||
elif incoming_message.message_type == 'audio':
|
||||
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
|
||||
|
||||
# 删掉开头的@消息
|
||||
if message_data["Content"].startswith("@"+self.robot_name):
|
||||
message_data["Content"][len("@"+self.robot_name):]
|
||||
message_data['Type'] = 'audio'
|
||||
|
||||
copy_message_data = message_data.copy()
|
||||
del copy_message_data['IncomingMessage']
|
||||
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import dingtalk_stream
|
||||
|
||||
class DingTalkEvent(dict):
|
||||
@staticmethod
|
||||
@@ -15,7 +16,7 @@ class DingTalkEvent(dict):
|
||||
return self.get("Content","")
|
||||
|
||||
@property
|
||||
def incoming_message(self):
|
||||
def incoming_message(self) -> Optional["dingtalk_stream.chatbot.ChatbotMessage"]:
|
||||
return self.get("IncomingMessage")
|
||||
|
||||
@property
|
||||
@@ -25,6 +26,10 @@ class DingTalkEvent(dict):
|
||||
@property
|
||||
def picture(self):
|
||||
return self.get("Picture","")
|
||||
|
||||
@property
|
||||
def audio(self):
|
||||
return self.get("Audio","")
|
||||
|
||||
@property
|
||||
def conversation(self):
|
||||
@@ -61,4 +66,4 @@ class DingTalkEvent(dict):
|
||||
Returns:
|
||||
str: 字符串表示。
|
||||
"""
|
||||
return f"<WecomEvent {super().__repr__()}>"
|
||||
return f"<DingTalkEvent {super().__repr__()}>"
|
||||
|
||||
@@ -29,6 +29,7 @@ from ..discover import engine as discover_engine
|
||||
from ..utils import logcache, ip
|
||||
from . import taskmgr
|
||||
from . import entities as core_entities
|
||||
from .bootutils import config
|
||||
|
||||
|
||||
class Application:
|
||||
@@ -203,6 +204,8 @@ class Application:
|
||||
case core_entities.LifecycleControlScope.PROVIDER.value:
|
||||
self.logger.info("执行热重载 scope="+scope)
|
||||
|
||||
latest_llm_model_config = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json")
|
||||
self.llm_models_meta = latest_llm_model_config
|
||||
llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
|
||||
await llm_model_mgr_inst.initialize()
|
||||
self.model_mgr = llm_model_mgr_inst
|
||||
|
||||
@@ -23,6 +23,7 @@ class GewechatConfigMigration(migration.Migration):
|
||||
"adapter": "gewechat",
|
||||
"enable": False,
|
||||
"gewechat_url": "http://your-gewechat-server:2531",
|
||||
"gewechat_file_url": "http://your-gewechat-server:2532",
|
||||
"port": 2286,
|
||||
"callback_url": "http://your-callback-url:2286/gewechat/callback",
|
||||
"app_id": "",
|
||||
|
||||
29
pkg/core/migrations/m034_gewechat_file_url_config.py
Normal file
29
pkg/core/migrations/m034_gewechat_file_url_config.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class("gewechat-file-url-config", 34)
|
||||
class GewechatFileUrlConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'gewechat':
|
||||
if 'gewechat_file_url' not in adapter:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'gewechat':
|
||||
if 'gewechat_file_url' not in adapter:
|
||||
parsed_url = urlparse(adapter['gewechat_url'])
|
||||
adapter['gewechat_file_url'] = f"{parsed_url.scheme}://{parsed_url.hostname}:2532"
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
@@ -11,7 +11,7 @@ from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_
|
||||
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config
|
||||
from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
|
||||
from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config
|
||||
from ..migrations import m033_dify_thinking_config
|
||||
from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config
|
||||
|
||||
@stage.stage_class("MigrationStage")
|
||||
class MigrationStage(stage.BootingStage):
|
||||
|
||||
@@ -187,6 +187,9 @@ class ComponentDiscoveryEngine:
|
||||
if name == 'ComponentTemplate':
|
||||
continue
|
||||
components[name] = self.load_blueprint_comp_group(component, owner)
|
||||
|
||||
self.ap.logger.debug(f'Components: {components}')
|
||||
|
||||
return blueprint_manifest, components
|
||||
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class PlatformManager:
|
||||
if len(self.adapters) == 0:
|
||||
self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。')
|
||||
|
||||
async def write_back_config(self, adapter_inst: msadapter.MessagePlatformAdapter, config: dict):
|
||||
async def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict):
|
||||
index = -2
|
||||
|
||||
for i, adapter in enumerate(self.adapters):
|
||||
@@ -132,7 +132,7 @@ class PlatformManager:
|
||||
break
|
||||
|
||||
new_cfg = {
|
||||
'adapter': adapter_inst.name,
|
||||
'adapter': adapter_name,
|
||||
'enable': True,
|
||||
**config
|
||||
}
|
||||
|
||||
@@ -28,16 +28,23 @@ class DingTalkMessageConverter(adapter.MessageConverter):
|
||||
return msg.text
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(event:DingTalkEvent):
|
||||
async def target2yiri(event:DingTalkEvent, bot_name:str):
|
||||
yiri_msg_list = []
|
||||
yiri_msg_list.append(
|
||||
platform_message.Source(id = '0',time=datetime.datetime.now())
|
||||
platform_message.Source(id = event.incoming_message.message_id,time=datetime.datetime.now())
|
||||
)
|
||||
|
||||
for atUser in event.incoming_message.at_users:
|
||||
if atUser.dingtalk_id == event.incoming_message.chatbot_user_id:
|
||||
yiri_msg_list.append(platform_message.At(target=bot_name))
|
||||
|
||||
if event.content:
|
||||
yiri_msg_list.append(platform_message.Plain(text=event.content))
|
||||
text_content = event.content.replace("@"+bot_name, '')
|
||||
yiri_msg_list.append(platform_message.Plain(text=text_content))
|
||||
if event.picture:
|
||||
yiri_msg_list.append(platform_message.Image(base64=event.picture))
|
||||
if event.audio:
|
||||
yiri_msg_list.append(platform_message.Voice(base64=event.audio))
|
||||
|
||||
chain = platform_message.MessageChain(yiri_msg_list)
|
||||
|
||||
@@ -54,18 +61,19 @@ class DingTalkEventConverter(adapter.EventConverter):
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(
|
||||
event:DingTalkEvent
|
||||
event:DingTalkEvent,
|
||||
bot_name:str
|
||||
):
|
||||
|
||||
message_chain = await DingTalkMessageConverter.target2yiri(event)
|
||||
message_chain = await DingTalkMessageConverter.target2yiri(event, bot_name)
|
||||
|
||||
|
||||
if event.conversation == 'FriendMessage':
|
||||
|
||||
return platform_events.FriendMessage(
|
||||
sender=platform_entities.Friend(
|
||||
id= 0,
|
||||
nickname ='nickname',
|
||||
id=event.incoming_message.sender_id,
|
||||
nickname = event.incoming_message.sender_nick,
|
||||
remark=""
|
||||
),
|
||||
message_chain = message_chain,
|
||||
@@ -73,14 +81,13 @@ class DingTalkEventConverter(adapter.EventConverter):
|
||||
source_platform_object=event,
|
||||
)
|
||||
elif event.conversation == 'GroupMessage':
|
||||
message_chain.insert(0, platform_message.At(target="justbot"))
|
||||
sender = platform_entities.GroupMember(
|
||||
id = 111,
|
||||
member_name="name",
|
||||
id = event.incoming_message.sender_id,
|
||||
member_name=event.incoming_message.sender_nick,
|
||||
permission= 'MEMBER',
|
||||
group = platform_entities.Group(
|
||||
id = 111,
|
||||
name = 'MEMBER',
|
||||
id = event.incoming_message.conversation_id,
|
||||
name = event.incoming_message.conversation_title,
|
||||
permission=platform_entities.Permission.Member
|
||||
),
|
||||
special_title='',
|
||||
@@ -117,6 +124,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
||||
missing_keys = [key for key in required_keys if key not in config]
|
||||
if missing_keys:
|
||||
raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员")
|
||||
|
||||
self.bot_account_id = self.config["robot_name"]
|
||||
|
||||
self.bot = DingTalkClient(
|
||||
client_id=config["client_id"],
|
||||
@@ -153,10 +162,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
||||
],
|
||||
):
|
||||
async def on_message(event: DingTalkEvent):
|
||||
self.bot_account_id = 'justbot'
|
||||
try:
|
||||
return await callback(
|
||||
await self.event_converter.target2yiri(event), self
|
||||
await self.event_converter.target2yiri(event, self.config["robot_name"]), self
|
||||
)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
@@ -167,7 +175,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
||||
self.bot.on_message("GroupMessage")(on_message)
|
||||
|
||||
async def run_async(self):
|
||||
|
||||
await self.bot.start()
|
||||
|
||||
async def kill(self) -> bool:
|
||||
|
||||
@@ -28,7 +28,10 @@ from ...utils import image
|
||||
|
||||
|
||||
class GewechatMessageConverter(adapter.MessageConverter):
|
||||
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
async def yiri2target(
|
||||
message_chain: platform_message.MessageChain
|
||||
@@ -40,20 +43,25 @@ class GewechatMessageConverter(adapter.MessageConverter):
|
||||
elif isinstance(component, platform_message.Plain):
|
||||
content_list.append({"type": "text", "content": component.text})
|
||||
elif isinstance(component, platform_message.Image):
|
||||
# content_list.append({"type": "image", "image_id": component.image_id})
|
||||
pass
|
||||
if not component.url:
|
||||
pass
|
||||
content_list.append({"type": "image", "image": component.url})
|
||||
|
||||
|
||||
elif isinstance(component, platform_message.Voice):
|
||||
content_list.append({"type": "voice", "url": component.url, "length": component.length})
|
||||
elif isinstance(component, platform_message.Forward):
|
||||
for node in component.node_list:
|
||||
content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain))
|
||||
|
||||
return content_list
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(
|
||||
self,
|
||||
message: dict,
|
||||
bot_account_id: str
|
||||
) -> platform_message.MessageChain:
|
||||
|
||||
|
||||
if message["Data"]["MsgType"] == 1:
|
||||
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
|
||||
regex = re.compile(r"^wxid_.*:")
|
||||
@@ -74,25 +82,77 @@ class GewechatMessageConverter(adapter.MessageConverter):
|
||||
return platform_message.MessageChain(content_list)
|
||||
|
||||
elif message["Data"]["MsgType"] == 3:
|
||||
image_base64 = message["Data"]["ImgBuf"]["buffer"]
|
||||
image_xml = message["Data"]["Content"]["string"]
|
||||
if not image_xml:
|
||||
return platform_message.MessageChain([
|
||||
platform_message.Plain(text="[图片内容为空]")
|
||||
])
|
||||
|
||||
|
||||
try:
|
||||
base64_str, image_format = await image.get_gewechat_image_base64(
|
||||
gewechat_url=self.config["gewechat_url"],
|
||||
gewechat_file_url=self.config["gewechat_file_url"],
|
||||
app_id=self.config["app_id"],
|
||||
xml_content=image_xml,
|
||||
token=self.config["token"],
|
||||
image_type=2,
|
||||
)
|
||||
|
||||
return platform_message.MessageChain([
|
||||
platform_message.Image(
|
||||
base64=f"data:image/{image_format};base64,{base64_str}"
|
||||
)
|
||||
])
|
||||
except Exception as e:
|
||||
print(f"处理图片消息失败: {str(e)}")
|
||||
return platform_message.MessageChain([
|
||||
platform_message.Plain(text=f"[图片处理失败]")
|
||||
])
|
||||
elif message["Data"]["MsgType"] == 34:
|
||||
audio_base64 = message["Data"]["ImgBuf"]["buffer"]
|
||||
return platform_message.MessageChain(
|
||||
[platform_message.Image(base64=f"data:image/jpeg;base64,{image_base64}")]
|
||||
[platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")]
|
||||
)
|
||||
elif message["Data"]["MsgType"] == 49:
|
||||
# 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递
|
||||
try:
|
||||
content = message["Data"]["Content"]["string"]
|
||||
|
||||
try:
|
||||
content_bytes = content.encode('utf-8')
|
||||
decoded_content = base64.b64decode(content_bytes)
|
||||
return platform_message.MessageChain(
|
||||
[platform_message.Unknown(content=decoded_content)]
|
||||
)
|
||||
except Exception as e:
|
||||
return platform_message.MessageChain(
|
||||
[platform_message.Plain(text=content)]
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error processing type 49 message: {str(e)}")
|
||||
return platform_message.MessageChain(
|
||||
[platform_message.Plain(text="[无法解析的消息]")]
|
||||
)
|
||||
|
||||
class GewechatEventConverter(adapter.EventConverter):
|
||||
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.message_converter = GewechatMessageConverter(config)
|
||||
|
||||
@staticmethod
|
||||
async def yiri2target(
|
||||
event: platform_events.MessageEvent
|
||||
) -> dict:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(
|
||||
self,
|
||||
event: dict,
|
||||
bot_account_id: str
|
||||
) -> platform_events.MessageEvent:
|
||||
message_chain = await GewechatMessageConverter.target2yiri(copy.deepcopy(event), bot_account_id)
|
||||
message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id)
|
||||
|
||||
if not message_chain:
|
||||
return None
|
||||
@@ -120,7 +180,7 @@ class GewechatEventConverter(adapter.EventConverter):
|
||||
time=event["Data"]["CreateTime"],
|
||||
source_platform_object=event,
|
||||
)
|
||||
elif 'wxid_' in event["Data"]["FromUserName"]["string"]:
|
||||
else:
|
||||
return platform_events.FriendMessage(
|
||||
sender=platform_entities.Friend(
|
||||
id=event["Data"]["FromUserName"]["string"],
|
||||
@@ -134,7 +194,9 @@ class GewechatEventConverter(adapter.EventConverter):
|
||||
|
||||
|
||||
class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
|
||||
|
||||
name: str = "gewechat" # 定义适配器名称
|
||||
|
||||
bot: gewechat_client.GewechatClient
|
||||
quart_app: quart.Quart
|
||||
|
||||
@@ -144,8 +206,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
|
||||
ap: app.Application
|
||||
|
||||
message_converter: GewechatMessageConverter = GewechatMessageConverter()
|
||||
event_converter: GewechatEventConverter = GewechatEventConverter()
|
||||
message_converter: GewechatMessageConverter
|
||||
event_converter: GewechatEventConverter
|
||||
|
||||
listeners: typing.Dict[
|
||||
typing.Type[platform_events.Event],
|
||||
@@ -158,6 +220,9 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
|
||||
self.quart_app = quart.Quart(__name__)
|
||||
|
||||
self.message_converter = GewechatMessageConverter(config)
|
||||
self.event_converter = GewechatEventConverter(config)
|
||||
|
||||
@self.quart_app.route('/gewechat/callback', methods=['POST'])
|
||||
async def gewechat_callback():
|
||||
data = await quart.request.json
|
||||
@@ -183,7 +248,19 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
target_id: str,
|
||||
message: platform_message.MessageChain
|
||||
):
|
||||
pass
|
||||
geweap_msg = await GewechatMessageConverter.yiri2target(message)
|
||||
# 此处加上群消息at处理
|
||||
# ats = [item["target"] for item in geweap_msg if item["type"] == "at"]
|
||||
|
||||
for msg in geweap_msg:
|
||||
if msg['type'] == 'text':
|
||||
await self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'])
|
||||
|
||||
elif msg['type'] == 'image':
|
||||
|
||||
await self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
|
||||
|
||||
|
||||
|
||||
async def reply_message(
|
||||
self,
|
||||
@@ -257,7 +334,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
|
||||
self.ap.logger.info(f"Gewechat 登录成功,app_id: {app_id}")
|
||||
|
||||
await self.ap.platform_mgr.write_back_config(self, self.config)
|
||||
await self.ap.platform_mgr.write_back_config('gewechat', self, self.config)
|
||||
|
||||
# 获取 nickname
|
||||
profile = self.bot.get_profile(self.config["app_id"])
|
||||
@@ -281,4 +358,4 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
)
|
||||
|
||||
async def kill(self) -> bool:
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -17,6 +17,13 @@ spec:
|
||||
type: string
|
||||
required: true
|
||||
default: ""
|
||||
- name: gewechat_file_url
|
||||
label:
|
||||
en_US: GeWeChat file download URL
|
||||
zh_CN: GeWeChat 文件下载URL
|
||||
type: string
|
||||
required: true
|
||||
default: ""
|
||||
- name: port
|
||||
label:
|
||||
en_US: Port
|
||||
|
||||
@@ -13,27 +13,20 @@ import pydantic.v1 as pydantic
|
||||
class Entity(pydantic.BaseModel):
|
||||
"""实体,表示一个用户或群。"""
|
||||
id: int
|
||||
"""QQ 号或群号。"""
|
||||
@abc.abstractmethod
|
||||
def get_avatar_url(self) -> str:
|
||||
"""头像图片链接。"""
|
||||
|
||||
"""ID。"""
|
||||
@abc.abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""名称。"""
|
||||
|
||||
|
||||
class Friend(Entity):
|
||||
"""好友。"""
|
||||
"""私聊对象。"""
|
||||
id: typing.Union[int, str]
|
||||
"""QQ 号。"""
|
||||
"""ID。"""
|
||||
nickname: typing.Optional[str]
|
||||
"""昵称。"""
|
||||
remark: typing.Optional[str]
|
||||
"""备注。"""
|
||||
def get_avatar_url(self) -> str:
|
||||
return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140'
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.nickname or self.remark or ''
|
||||
|
||||
@@ -59,8 +52,6 @@ class Group(Entity):
|
||||
"""群名称。"""
|
||||
permission: Permission
|
||||
"""Bot 在群中的权限。"""
|
||||
def get_avatar_url(self) -> str:
|
||||
return f'https://p.qlogo.cn/gh/{self.id}/{self.id}/'
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.name
|
||||
@@ -69,11 +60,11 @@ class Group(Entity):
|
||||
class GroupMember(Entity):
|
||||
"""群成员。"""
|
||||
id: typing.Union[int, str]
|
||||
"""QQ 号。"""
|
||||
"""群员 ID。"""
|
||||
member_name: str
|
||||
"""群成员名称。"""
|
||||
"""群员名称。"""
|
||||
permission: Permission
|
||||
"""Bot 在群中的权限。"""
|
||||
"""在群中的权限。"""
|
||||
group: Group
|
||||
"""群。"""
|
||||
special_title: str = ''
|
||||
@@ -84,61 +75,6 @@ class GroupMember(Entity):
|
||||
"""最后一次发言的时间。"""
|
||||
mute_time_remaining: int = 0
|
||||
"""禁言剩余时间。"""
|
||||
def get_avatar_url(self) -> str:
|
||||
return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140'
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.member_name
|
||||
|
||||
|
||||
class Client(Entity):
|
||||
"""来自其他客户端的用户。"""
|
||||
id: typing.Union[int, str]
|
||||
"""识别 id。"""
|
||||
platform: str
|
||||
"""来源平台。"""
|
||||
def get_avatar_url(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.platform
|
||||
|
||||
|
||||
class Subject(pydantic.BaseModel):
|
||||
"""另一种实体类型表示。"""
|
||||
id: typing.Union[int, str]
|
||||
"""QQ 号或群号。"""
|
||||
kind: typing.Literal['Friend', 'Group', 'Stranger']
|
||||
"""类型。"""
|
||||
|
||||
|
||||
class Config(pydantic.BaseModel):
|
||||
"""配置项类型。"""
|
||||
def modify(self, **kwargs) -> 'Config':
|
||||
"""修改部分设置。"""
|
||||
for k, v in kwargs.items():
|
||||
if k in self.__fields__:
|
||||
setattr(self, k, v)
|
||||
else:
|
||||
raise ValueError(f'未知配置项: {k}')
|
||||
return self
|
||||
|
||||
|
||||
class GroupConfigModel(Config):
|
||||
"""群配置。"""
|
||||
name: str
|
||||
"""群名称。"""
|
||||
confess_talk: bool
|
||||
"""是否允许坦白说。"""
|
||||
allow_member_invite: bool
|
||||
"""是否允许成员邀请好友入群。"""
|
||||
auto_approve: bool
|
||||
"""是否开启自动审批入群。"""
|
||||
anonymous_chat: bool
|
||||
"""是否开启匿名聊天。"""
|
||||
announcement: str = ''
|
||||
"""群公告。"""
|
||||
|
||||
|
||||
class MemberInfoModel(Config, GroupMember):
|
||||
"""群成员信息。"""
|
||||
|
||||
@@ -43,21 +43,6 @@ class Event(pydantic.BaseModel):
|
||||
return Event
|
||||
|
||||
|
||||
###############################
|
||||
# Bot Event
|
||||
class BotEvent(Event):
|
||||
"""Bot 自身事件。
|
||||
|
||||
Args:
|
||||
type: 事件名。
|
||||
qq: Bot 的 QQ 号。
|
||||
"""
|
||||
type: str
|
||||
"""事件名。"""
|
||||
qq: int
|
||||
"""Bot 的 QQ 号。"""
|
||||
|
||||
|
||||
###############################
|
||||
# Message Event
|
||||
class MessageEvent(Event):
|
||||
@@ -79,7 +64,7 @@ class MessageEvent(Event):
|
||||
|
||||
|
||||
class FriendMessage(MessageEvent):
|
||||
"""好友消息。
|
||||
"""私聊消息。
|
||||
|
||||
Args:
|
||||
type: 事件名。
|
||||
@@ -111,19 +96,3 @@ class GroupMessage(MessageEvent):
|
||||
@property
|
||||
def group(self) -> platform_entities.Group:
|
||||
return self.sender.group
|
||||
|
||||
|
||||
class StrangerMessage(MessageEvent):
|
||||
"""陌生人消息。
|
||||
|
||||
Args:
|
||||
type: 事件名。
|
||||
sender: 发送消息的人。
|
||||
message_chain: 消息内容。
|
||||
"""
|
||||
type: str = 'StrangerMessage'
|
||||
"""事件名。"""
|
||||
sender: platform_entities.Friend
|
||||
"""发送消息的人。"""
|
||||
message_chain: platform_message.MessageChain
|
||||
"""消息内容。"""
|
||||
|
||||
@@ -116,18 +116,6 @@ class MessageChain(PlatformBaseModel):
|
||||
print('At Me')
|
||||
```
|
||||
|
||||
消息链对索引操作进行了增强。以消息组件类型为索引,获取消息链中的全部该类型的消息组件。
|
||||
```py
|
||||
plain_list = message_chain[Plain]
|
||||
'[Plain("Hello World!")]'
|
||||
```
|
||||
|
||||
可以用加号连接两个消息链。
|
||||
```py
|
||||
MessageChain(['Hello World!']) + MessageChain(['Goodbye World!'])
|
||||
# 返回 MessageChain([Plain("Hello World!"), Plain("Goodbye World!")])
|
||||
```
|
||||
|
||||
"""
|
||||
__root__: typing.List[MessageComponent]
|
||||
|
||||
@@ -488,9 +476,9 @@ class Quote(MessageComponent):
|
||||
group_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""被引用回复的原消息所接收的群号,当为好友消息时为0。"""
|
||||
sender_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""被引用回复的原消息的发送者的QQ号。"""
|
||||
"""被引用回复的原消息的发送者的ID。"""
|
||||
target_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""被引用回复的原消息的接收者者的QQ号(或群号)。"""
|
||||
"""被引用回复的原消息的接收者者的ID或群ID。"""
|
||||
origin: MessageChain
|
||||
"""被引用回复的原消息的消息链对象。"""
|
||||
|
||||
@@ -504,7 +492,7 @@ class At(MessageComponent):
|
||||
type: str = "At"
|
||||
"""消息组件类型。"""
|
||||
target: typing.Union[int, str]
|
||||
"""群员 QQ 号。"""
|
||||
"""群员 ID。"""
|
||||
display: typing.Optional[str] = None
|
||||
"""At时显示的文字,发送消息时无效,自动使用群名片。"""
|
||||
def __eq__(self, other):
|
||||
@@ -527,9 +515,9 @@ class Image(MessageComponent):
|
||||
type: str = "Image"
|
||||
"""消息组件类型。"""
|
||||
image_id: typing.Optional[str] = None
|
||||
"""图片的 image_id,群图片与好友图片格式不同。不为空时将忽略 url 属性。"""
|
||||
"""图片的 image_id,不为空时将忽略 url 属性。"""
|
||||
url: typing.Optional[pydantic.HttpUrl] = None
|
||||
"""图片的 URL,发送时可作网络图片的链接;接收时为腾讯图片服务器的链接,可用于图片下载。"""
|
||||
"""图片的 URL,发送时可作网络图片的链接;接收时为图片的链接,可用于图片下载。"""
|
||||
path: typing.Union[str, Path, None] = None
|
||||
"""图片的路径,发送本地图片。"""
|
||||
base64: typing.Optional[str] = None
|
||||
@@ -663,7 +651,7 @@ class Voice(MessageComponent):
|
||||
voice_id: typing.Optional[str] = None
|
||||
"""语音的 voice_id,不为空时将忽略 url 属性。"""
|
||||
url: typing.Optional[str] = None
|
||||
"""语音的 URL,发送时可作网络语音的链接;接收时为腾讯语音服务器的链接,可用于语音下载。"""
|
||||
"""语音的 URL,发送时可作网络语音的链接;接收时为语音文件的链接,可用于语音下载。"""
|
||||
path: typing.Optional[str] = None
|
||||
"""语音的路径,发送本地语音。"""
|
||||
base64: typing.Optional[str] = None
|
||||
@@ -691,8 +679,6 @@ class Voice(MessageComponent):
|
||||
):
|
||||
"""下载语音到本地。
|
||||
|
||||
语音采用 silk v3 格式,silk 格式的编码解码请使用 [graiax-silkcoder](https://pypi.org/project/graiax-silkcoder/)。
|
||||
|
||||
Args:
|
||||
filename: 下载到本地的文件路径。与 `directory` 二选一。
|
||||
directory: 下载到本地的文件夹路径。与 `filename` 二选一。
|
||||
@@ -750,13 +736,13 @@ class Voice(MessageComponent):
|
||||
class ForwardMessageNode(pydantic.BaseModel):
|
||||
"""合并转发中的一条消息。"""
|
||||
sender_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""发送人QQ号。"""
|
||||
"""发送人ID。"""
|
||||
sender_name: typing.Optional[str] = None
|
||||
"""显示名称。"""
|
||||
message_chain: typing.Optional[MessageChain] = None
|
||||
"""消息内容。"""
|
||||
message_id: typing.Optional[int] = None
|
||||
"""消息的 message_id,可以只使用此属性,从缓存中读取消息内容。"""
|
||||
"""消息的 message_id。"""
|
||||
time: typing.Optional[datetime] = None
|
||||
"""发送时间。"""
|
||||
@pydantic.validator('message_chain', check_fields=False)
|
||||
|
||||
@@ -4,7 +4,7 @@ import aiohttp
|
||||
|
||||
from . import entities, requester
|
||||
from ...core import app
|
||||
|
||||
from ...discover import engine
|
||||
from . import token
|
||||
from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl
|
||||
|
||||
@@ -16,6 +16,8 @@ class ModelManager:
|
||||
|
||||
ap: app.Application
|
||||
|
||||
requester_components: list[engine.Component]
|
||||
|
||||
model_list: list[entities.LLMModelInfo]
|
||||
|
||||
requesters: dict[str, requester.LLMAPIRequester]
|
||||
@@ -38,14 +40,21 @@ class ModelManager:
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester')
|
||||
|
||||
# 初始化token_mgr, requester
|
||||
for k, v in self.ap.provider_cfg.data['keys'].items():
|
||||
self.token_mgrs[k] = token.TokenManager(k, v)
|
||||
|
||||
for api_cls in requester.preregistered_requesters:
|
||||
# for api_cls in requester.preregistered_requesters:
|
||||
# api_inst = api_cls(self.ap)
|
||||
# await api_inst.initialize()
|
||||
# self.requesters[api_inst.name] = api_inst
|
||||
for component in self.requester_components:
|
||||
api_cls = component.get_python_component_class()
|
||||
api_inst = api_cls(self.ap)
|
||||
await api_inst.initialize()
|
||||
self.requesters[api_inst.name] = api_inst
|
||||
self.requesters[component.metadata.name] = api_inst
|
||||
|
||||
# 尝试从api获取最新的模型信息
|
||||
try:
|
||||
|
||||
@@ -10,18 +10,6 @@ from . import entities as modelmgr_entities
|
||||
from ..tools import entities as tools_entities
|
||||
|
||||
|
||||
preregistered_requesters: list[typing.Type[LLMAPIRequester]] = []
|
||||
|
||||
def requester_class(name: str):
|
||||
|
||||
def decorator(cls: typing.Type[LLMAPIRequester]) -> typing.Type[LLMAPIRequester]:
|
||||
cls.name = name
|
||||
preregistered_requesters.append(cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class LLMAPIRequester(metaclass=abc.ABCMeta):
|
||||
"""LLM API请求器
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import json
|
||||
import traceback
|
||||
import base64
|
||||
|
||||
@@ -16,7 +17,6 @@ from ...tools import entities as tools_entities
|
||||
from ....utils import image
|
||||
|
||||
|
||||
@requester.requester_class("anthropic-messages")
|
||||
class AnthropicMessages(requester.LLMAPIRequester):
|
||||
"""Anthropic Messages API 请求器"""
|
||||
|
||||
@@ -69,11 +69,32 @@ class AnthropicMessages(requester.LLMAPIRequester):
|
||||
req_messages = []
|
||||
|
||||
for m in messages:
|
||||
if isinstance(m.content, str) and m.content.strip() != "":
|
||||
req_messages.append(m.dict(exclude_none=True))
|
||||
elif isinstance(m.content, list):
|
||||
if m.role == 'tool':
|
||||
tool_call_id = m.tool_call_id
|
||||
|
||||
msg_dict = m.dict(exclude_none=True)
|
||||
req_messages.append({
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_call_id,
|
||||
"content": m.content
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
continue
|
||||
|
||||
msg_dict = m.dict(exclude_none=True)
|
||||
|
||||
if isinstance(m.content, str) and m.content.strip() != "":
|
||||
msg_dict["content"] = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": m.content
|
||||
}
|
||||
]
|
||||
elif isinstance(m.content, list):
|
||||
|
||||
for i, ce in enumerate(m.content):
|
||||
|
||||
@@ -90,25 +111,60 @@ class AnthropicMessages(requester.LLMAPIRequester):
|
||||
}
|
||||
msg_dict["content"][i] = alter_image_ele
|
||||
|
||||
req_messages.append(msg_dict)
|
||||
if m.tool_calls:
|
||||
|
||||
for tool_call in m.tool_calls:
|
||||
msg_dict["content"].append({
|
||||
"type": "tool_use",
|
||||
"id": tool_call.id,
|
||||
"name": tool_call.function.name,
|
||||
"input": json.loads(tool_call.function.arguments)
|
||||
})
|
||||
|
||||
del msg_dict["tool_calls"]
|
||||
|
||||
req_messages.append(msg_dict)
|
||||
|
||||
|
||||
args["messages"] = req_messages
|
||||
|
||||
if funcs:
|
||||
tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs)
|
||||
|
||||
# anthropic的tools处在beta阶段,sdk不稳定,故暂时不支持
|
||||
#
|
||||
# if funcs:
|
||||
# tools = await self.ap.tool_mgr.generate_tools_for_openai(funcs)
|
||||
|
||||
# if tools:
|
||||
# args["tools"] = tools
|
||||
if tools:
|
||||
args["tools"] = tools
|
||||
|
||||
try:
|
||||
# print(json.dumps(args, indent=4, ensure_ascii=False))
|
||||
resp = await self.client.messages.create(**args)
|
||||
|
||||
return llm_entities.Message(
|
||||
content=resp.content[0].text,
|
||||
role=resp.role
|
||||
)
|
||||
args = {
|
||||
'content': '',
|
||||
'role': resp.role,
|
||||
}
|
||||
|
||||
assert type(resp) is anthropic.types.message.Message
|
||||
|
||||
for block in resp.content:
|
||||
if block.type == 'thinking':
|
||||
args['content'] = '<think>' + block.thinking + '</think>\n' + args['content']
|
||||
elif block.type == 'text':
|
||||
args['content'] += block.text
|
||||
elif block.type == 'tool_use':
|
||||
assert type(block) is anthropic.types.tool_use_block.ToolUseBlock
|
||||
tool_call = llm_entities.ToolCall(
|
||||
id=block.id,
|
||||
type="function",
|
||||
function=llm_entities.FunctionCall(
|
||||
name=block.name,
|
||||
arguments=json.dumps(block.input)
|
||||
)
|
||||
)
|
||||
if 'tool_calls' not in args:
|
||||
args['tool_calls'] = []
|
||||
args['tool_calls'].append(tool_call)
|
||||
|
||||
return llm_entities.Message(**args)
|
||||
except anthropic.AuthenticationError as e:
|
||||
raise errors.RequesterError(f'api-key 无效: {e.message}')
|
||||
except anthropic.BadRequestError as e:
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/anthropicmsgs.yaml
Normal file
34
pkg/provider/modelmgr/requesters/anthropicmsgs.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: anthropic-messages
|
||||
label:
|
||||
en_US: Anthropic
|
||||
zh_CN: Anthropic
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://api.anthropic.com/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./anthropicmsgs.py
|
||||
attr: AnthropicMessages
|
||||
@@ -7,7 +7,6 @@ from .. import requester
|
||||
from ....core import app
|
||||
|
||||
|
||||
@requester.requester_class("bailian-chat-completions")
|
||||
class BailianChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""阿里云百炼大模型平台 ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: bailian-chat-completions
|
||||
label:
|
||||
en_US: Aliyun Bailian
|
||||
zh_CN: 阿里云百炼
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./bailianchatcmpl.py
|
||||
attr: BailianChatCompletions
|
||||
@@ -20,7 +20,6 @@ from ...tools import entities as tools_entities
|
||||
from ....utils import image
|
||||
|
||||
|
||||
@requester.requester_class("openai-chat-completions")
|
||||
class OpenAIChatCompletions(requester.LLMAPIRequester):
|
||||
"""OpenAI ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/chatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/chatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: openai-chat-completions
|
||||
label:
|
||||
en_US: OpenAI
|
||||
zh_CN: OpenAI
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://api.openai.com/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./chatcmpl.py
|
||||
attr: OpenAIChatCompletions
|
||||
@@ -7,7 +7,6 @@ from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
|
||||
|
||||
@requester.requester_class("deepseek-chat-completions")
|
||||
class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""Deepseek ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: deepseek-chat-completions
|
||||
label:
|
||||
en_US: DeepSeek
|
||||
zh_CN: 深度求索
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://api.deepseek.com"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./deepseekchatcmpl.py
|
||||
attr: DeepseekChatCompletions
|
||||
@@ -14,7 +14,6 @@ from ...tools import entities as tools_entities
|
||||
from .. import entities as modelmgr_entities
|
||||
|
||||
|
||||
@requester.requester_class("gitee-ai-chat-completions")
|
||||
class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""Gitee AI ChatCompletions API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: gitee-ai-chat-completions
|
||||
label:
|
||||
en_US: Gitee AI
|
||||
zh_CN: Gitee AI
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://ai.gitee.com/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./giteeaichatcmpl.py
|
||||
attr: GiteeAIChatCompletions
|
||||
@@ -7,7 +7,6 @@ from .. import requester
|
||||
from ....core import app
|
||||
|
||||
|
||||
@requester.requester_class("lmstudio-chat-completions")
|
||||
class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""LMStudio ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: lmstudio-chat-completions
|
||||
label:
|
||||
en_US: LM Studio
|
||||
zh_CN: LM Studio
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "http://127.0.0.1:1234/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./lmstudiochatcmpl.py
|
||||
attr: LmStudioChatCompletions
|
||||
@@ -9,7 +9,6 @@ from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
|
||||
|
||||
@requester.requester_class("moonshot-chat-completions")
|
||||
class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""Moonshot ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: moonshot-chat-completions
|
||||
label:
|
||||
en_US: Moonshot
|
||||
zh_CN: 月之暗面
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://api.moonshot.com/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./moonshotchatcmpl.py
|
||||
attr: MoonshotChatCompletions
|
||||
@@ -20,7 +20,6 @@ from ....utils import image
|
||||
REQUESTER_NAME: str = "ollama-chat"
|
||||
|
||||
|
||||
@requester.requester_class(REQUESTER_NAME)
|
||||
class OllamaChatCompletions(requester.LLMAPIRequester):
|
||||
"""Ollama平台 ChatCompletion API请求器"""
|
||||
client: ollama.AsyncClient
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/ollamachat.yaml
Normal file
34
pkg/provider/modelmgr/requesters/ollamachat.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: ollama-chat
|
||||
label:
|
||||
en_US: Ollama
|
||||
zh_CN: Ollama
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "http://127.0.0.1:11434"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./ollamachat.py
|
||||
attr: OllamaChatCompletions
|
||||
@@ -7,7 +7,6 @@ from .. import requester
|
||||
from ....core import app
|
||||
|
||||
|
||||
@requester.requester_class("siliconflow-chat-completions")
|
||||
class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""SiliconFlow ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: siliconflow-chat-completions
|
||||
label:
|
||||
en_US: SiliconFlow
|
||||
zh_CN: 硅基流动
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://api.siliconflow.cn/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./siliconflowchatcmpl.py
|
||||
attr: SiliconFlowChatCompletions
|
||||
@@ -7,7 +7,6 @@ from .. import requester
|
||||
from ....core import app
|
||||
|
||||
|
||||
@requester.requester_class("volcark-chat-completions")
|
||||
class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""火山方舟大模型平台 ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: volcark-chat-completions
|
||||
label:
|
||||
en_US: Volc Engine Ark
|
||||
zh_CN: 火山方舟
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://ark.cn-beijing.volces.com/api/v3"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./volcarkchatcmpl.py
|
||||
attr: VolcArkChatCompletions
|
||||
@@ -7,7 +7,6 @@ from .. import requester
|
||||
from ....core import app
|
||||
|
||||
|
||||
@requester.requester_class("xai-chat-completions")
|
||||
class XaiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""xAI ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/xaichatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/xaichatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: xai-chat-completions
|
||||
label:
|
||||
en_US: xAI
|
||||
zh_CN: xAI
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://api.x.ai/v1"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./xaichatcmpl.py
|
||||
attr: XaiChatCompletions
|
||||
@@ -7,7 +7,6 @@ from . import chatcmpl
|
||||
from .. import requester
|
||||
|
||||
|
||||
@requester.requester_class("zhipuai-chat-completions")
|
||||
class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""智谱AI ChatCompletion API 请求器"""
|
||||
|
||||
|
||||
34
pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml
Normal file
34
pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: LLMAPIRequester
|
||||
metadata:
|
||||
name: zhipuai-chat-completions
|
||||
label:
|
||||
en_US: ZhipuAI
|
||||
zh_CN: 智谱 AI
|
||||
spec:
|
||||
config:
|
||||
- name: base-url
|
||||
label:
|
||||
en_US: Base URL
|
||||
zh_CN: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://open.bigmodel.cn/api/paas/v4"
|
||||
- name: args
|
||||
label:
|
||||
en_US: Args
|
||||
zh_CN: 附加参数
|
||||
type: object
|
||||
required: true
|
||||
default: {}
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_CN: 超时时间
|
||||
type: int
|
||||
required: true
|
||||
default: 120
|
||||
execution:
|
||||
python:
|
||||
path: ./zhipuaichatcmpl.py
|
||||
attr: ZhipuAIChatCompletions
|
||||
@@ -1,4 +1,4 @@
|
||||
semantic_version = "v3.4.9.2"
|
||||
semantic_version = "v3.4.10"
|
||||
|
||||
debug_mode = False
|
||||
|
||||
|
||||
@@ -8,6 +8,106 @@ import aiohttp
|
||||
import PIL.Image
|
||||
import httpx
|
||||
|
||||
import os
|
||||
import aiofiles
|
||||
import pathlib
|
||||
import asyncio
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
async def get_gewechat_image_base64(
|
||||
gewechat_url: str,
|
||||
gewechat_file_url: str,
|
||||
app_id: str,
|
||||
xml_content: str,
|
||||
token: str,
|
||||
image_type: int = 2,
|
||||
) -> typing.Tuple[str, str]:
|
||||
"""从gewechat服务器获取图片并转换为base64格式
|
||||
|
||||
Args:
|
||||
gewechat_url (str): gewechat服务器地址(用于获取图片URL)
|
||||
gewechat_file_url (str): gewechat文件下载服务地址
|
||||
app_id (str): gewechat应用ID
|
||||
xml_content (str): 图片的XML内容
|
||||
token (str): Gewechat API Token
|
||||
image_type (int, optional): 图片类型. Defaults to 2.
|
||||
|
||||
Returns:
|
||||
typing.Tuple[str, str]: (base64编码, 图片格式)
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientTimeout: 请求超时(15秒)或连接超时(2秒)
|
||||
Exception: 其他错误
|
||||
"""
|
||||
headers = {
|
||||
'X-GEWE-TOKEN': token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# 设置超时
|
||||
timeout = aiohttp.ClientTimeout(
|
||||
total=15.0, # 总超时时间15秒
|
||||
connect=2.0, # 连接超时2秒
|
||||
sock_connect=2.0, # socket连接超时2秒
|
||||
sock_read=15.0 # socket读取超时15秒
|
||||
)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
# 获取图片下载链接
|
||||
try:
|
||||
async with session.post(
|
||||
f"{gewechat_url}/v2/api/message/downloadImage",
|
||||
headers=headers,
|
||||
json={
|
||||
"appId": app_id,
|
||||
"type": image_type,
|
||||
"xml": xml_content
|
||||
}
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
raise Exception(f"获取gewechat图片下载失败: {await response.text()}")
|
||||
|
||||
resp_data = await response.json()
|
||||
if resp_data.get("ret") != 200:
|
||||
raise Exception(f"获取gewechat图片下载链接失败: {resp_data}")
|
||||
|
||||
file_url = resp_data['data']['fileUrl']
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception("获取图片下载链接超时")
|
||||
except aiohttp.ClientError as e:
|
||||
raise Exception(f"获取图片下载链接网络错误: {str(e)}")
|
||||
|
||||
# 解析原始URL并替换端口
|
||||
base_url = gewechat_file_url
|
||||
download_url = f"{base_url}/download/{file_url}"
|
||||
|
||||
# 下载图片
|
||||
try:
|
||||
async with session.get(download_url) as img_response:
|
||||
if img_response.status != 200:
|
||||
raise Exception(f"下载图片失败: {await img_response.text()}, URL: {download_url}")
|
||||
|
||||
image_data = await img_response.read()
|
||||
|
||||
content_type = img_response.headers.get('Content-Type', '')
|
||||
if content_type:
|
||||
image_format = content_type.split('/')[-1]
|
||||
else:
|
||||
image_format = file_url.split('.')[-1]
|
||||
|
||||
base64_str = base64.b64encode(image_data).decode('utf-8')
|
||||
|
||||
return base64_str, image_format
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception(f"下载图片超时, URL: {download_url}")
|
||||
except aiohttp.ClientError as e:
|
||||
raise Exception(f"下载图片网络错误: {str(e)}, URL: {download_url}")
|
||||
except Exception as e:
|
||||
raise Exception(f"获取图片失败: {str(e)}") from e
|
||||
|
||||
|
||||
async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]:
|
||||
"""
|
||||
下载企业微信图片并转换为 base64
|
||||
|
||||
@@ -74,19 +74,29 @@
|
||||
"name": "claude-3-opus-latest",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true
|
||||
"vision_supported": true,
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "claude-3-5-sonnet-latest",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true
|
||||
"vision_supported": true,
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "claude-3-5-haiku-latest",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true
|
||||
"vision_supported": true,
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "claude-3-7-sonnet-latest",
|
||||
"requester": "anthropic-messages",
|
||||
"token_mgr": "anthropic",
|
||||
"vision_supported": true,
|
||||
"tool_call_supported": true
|
||||
},
|
||||
{
|
||||
"name": "moonshot-v1-8k",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"adapter": "gewechat",
|
||||
"enable": false,
|
||||
"gewechat_url": "http://your-gewechat-server:2531",
|
||||
"gewechat_file_url": "http://your-gewechat-server:2532",
|
||||
"port": 2286,
|
||||
"callback_url": "http://your-callback-url:2286/gewechat/callback",
|
||||
"app_id": "",
|
||||
|
||||
@@ -325,6 +325,11 @@
|
||||
"default": "",
|
||||
"description": "gewechat 的 url"
|
||||
},
|
||||
"gewechat_file_url": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "gewechat 文件下载URL"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"default": 2286,
|
||||
|
||||
Reference in New Issue
Block a user