Compare commits

...

22 Commits

Author SHA1 Message Date
Junyan Qin
98a9fed726 chore: release v3.4.10 2025-03-02 18:08:59 +08:00
Junyan Qin
720a218259 perf: simplify platform entities 2025-03-02 17:59:13 +08:00
Junyan Qin (Chin)
60c0adc6f9 Merge pull request #1152 from RockChinQ/feat/dingtalk-audio
feat(dingtalk): add supports for audio receiving
2025-03-02 17:38:19 +08:00
Junyan Qin
bc8c346e68 fix(dingtalk): group and person id not distinguished 2025-03-02 17:35:06 +08:00
Junyan Qin
a198b6da0b feat(dingtalk): add supports for audio receiving 2025-03-02 17:03:19 +08:00
Junyan Qin (Chin)
0f3dc35df4 Merge pull request #1150 from Tigrex-Dai/master
添加针对Anthropic新模型的thinking开关
2025-03-02 15:39:58 +08:00
Junyan Qin
7b6e6b046a perf(claude): simplify the thinking resp processing 2025-03-02 15:24:08 +08:00
Tigrex Dai
9e503191d6 Update anthropicmsgs.py 2025-03-01 17:27:01 +01:00
Tigrex Dai
1fd23a0d8d Merge branch 'RockChinQ:master' into master 2025-03-01 16:53:23 +01:00
Junyan Qin
3811700a78 chore: update llm-models.json template 2025-03-01 21:33:47 +08:00
Junyan Qin
8762ba3d9c feat(anthropic): add supports for tool use #763 2025-03-01 20:34:22 +08:00
Junyan Qin
c42b5aab5a feat: update components.yaml 2025-03-01 11:45:15 +08:00
Junyan Qin (Chin)
d724899ec0 Merge pull request #1148 from RockChinQ/feat/requester-manifests
feat: add manifests for all requesters
2025-03-01 00:12:55 +08:00
Junyan Qin
81aacdd76e refactor: lookup requester from discover engine 2025-03-01 00:12:23 +08:00
Junyan Qin
0aa072b4e8 feat: add manifests for all requesters 2025-02-28 22:47:34 +08:00
Tigrex Dai
6335e9dd8b Update anthropicmsgs.py 2025-02-28 13:02:06 +01:00
Tigrex Dai
a785289ac9 Update entities.py 2025-02-28 13:00:44 +01:00
Junyan Qin (Chin)
f8bace040c Merge pull request #1142 from fdc310/master
个人微信中主动发送图片的修改,但是只能发送链接
2025-02-28 11:33:43 +08:00
Dong_master
d62d597695 '个人微信中主动发送图片的修改,但是只能发送链接' 2025-02-28 01:31:59 +08:00
Dong_master
d938129884 '删除先' 2025-02-28 01:30:55 +08:00
Dong_master
327f448321 Resolved merge conflict in gewechat.py 2025-02-28 01:22:15 +08:00
Dong_master
19af3740c1 '个人微信中主动发送图片的修改,但是只能发送链接' 2025-02-28 01:17:25 +08:00
38 changed files with 610 additions and 199 deletions

View File

@@ -10,6 +10,10 @@ spec:
ComponentTemplate: ComponentTemplate:
fromFiles: fromFiles:
- pkg/platform/adapter.yaml - pkg/platform/adapter.yaml
- pkg/provider/modelmgr/requester.yaml
MessagePlatformAdapter: MessagePlatformAdapter:
fromDirs: fromDirs:
- path: pkg/platform/sources/ - path: pkg/platform/sources/
LLMAPIRequester:
fromDirs:
- path: pkg/provider/modelmgr/requesters/

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import json
import dingtalk_stream import dingtalk_stream
from dingtalk_stream import AckMessage from dingtalk_stream import AckMessage

View File

@@ -1,4 +1,5 @@
import base64 import base64
import json
import time import time
from typing import Callable from typing import Callable
import dingtalk_stream import dingtalk_stream
@@ -92,7 +93,30 @@ class DingTalkClient:
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式 base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str return base64_str
else: 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): async def update_incoming_message(self, message):
"""异步更新 DingTalkClient 中的 incoming_message""" """异步更新 DingTalkClient 中的 incoming_message"""
@@ -133,6 +157,7 @@ class DingTalkClient:
async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage): async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage):
try: try:
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
message_data = { message_data = {
"IncomingMessage":incoming_message, "IncomingMessage":incoming_message,
} }
@@ -160,10 +185,14 @@ class DingTalkClient:
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0]) message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
message_data['Type'] = 'image' message_data['Type'] = 'image'
elif incoming_message.message_type == 'audio':
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
# 删掉开头的@消息 message_data['Type'] = 'audio'
if message_data["Content"].startswith("@"+self.robot_name):
message_data["Content"][len("@"+self.robot_name):] 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: except Exception:
traceback.print_exc() traceback.print_exc()

View File

@@ -1,4 +1,5 @@
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import dingtalk_stream
class DingTalkEvent(dict): class DingTalkEvent(dict):
@staticmethod @staticmethod
@@ -15,7 +16,7 @@ class DingTalkEvent(dict):
return self.get("Content","") return self.get("Content","")
@property @property
def incoming_message(self): def incoming_message(self) -> Optional["dingtalk_stream.chatbot.ChatbotMessage"]:
return self.get("IncomingMessage") return self.get("IncomingMessage")
@property @property
@@ -26,6 +27,10 @@ class DingTalkEvent(dict):
def picture(self): def picture(self):
return self.get("Picture","") return self.get("Picture","")
@property
def audio(self):
return self.get("Audio","")
@property @property
def conversation(self): def conversation(self):
return self.get("conversation_type","") return self.get("conversation_type","")
@@ -61,4 +66,4 @@ class DingTalkEvent(dict):
Returns: Returns:
str: 字符串表示。 str: 字符串表示。
""" """
return f"<WecomEvent {super().__repr__()}>" return f"<DingTalkEvent {super().__repr__()}>"

View File

@@ -187,6 +187,9 @@ class ComponentDiscoveryEngine:
if name == 'ComponentTemplate': if name == 'ComponentTemplate':
continue continue
components[name] = self.load_blueprint_comp_group(component, owner) components[name] = self.load_blueprint_comp_group(component, owner)
self.ap.logger.debug(f'Components: {components}')
return blueprint_manifest, components return blueprint_manifest, components

View File

@@ -28,16 +28,23 @@ class DingTalkMessageConverter(adapter.MessageConverter):
return msg.text return msg.text
@staticmethod @staticmethod
async def target2yiri(event:DingTalkEvent): async def target2yiri(event:DingTalkEvent, bot_name:str):
yiri_msg_list = [] yiri_msg_list = []
yiri_msg_list.append( 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: 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: if event.picture:
yiri_msg_list.append(platform_message.Image(base64=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) chain = platform_message.MessageChain(yiri_msg_list)
@@ -54,18 +61,19 @@ class DingTalkEventConverter(adapter.EventConverter):
@staticmethod @staticmethod
async def target2yiri( 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': if event.conversation == 'FriendMessage':
return platform_events.FriendMessage( return platform_events.FriendMessage(
sender=platform_entities.Friend( sender=platform_entities.Friend(
id= 0, id=event.incoming_message.sender_id,
nickname ='nickname', nickname = event.incoming_message.sender_nick,
remark="" remark=""
), ),
message_chain = message_chain, message_chain = message_chain,
@@ -73,14 +81,13 @@ class DingTalkEventConverter(adapter.EventConverter):
source_platform_object=event, source_platform_object=event,
) )
elif event.conversation == 'GroupMessage': elif event.conversation == 'GroupMessage':
message_chain.insert(0, platform_message.At(target="justbot"))
sender = platform_entities.GroupMember( sender = platform_entities.GroupMember(
id = 111, id = event.incoming_message.sender_id,
member_name="name", member_name=event.incoming_message.sender_nick,
permission= 'MEMBER', permission= 'MEMBER',
group = platform_entities.Group( group = platform_entities.Group(
id = 111, id = event.incoming_message.conversation_id,
name = 'MEMBER', name = event.incoming_message.conversation_title,
permission=platform_entities.Permission.Member permission=platform_entities.Permission.Member
), ),
special_title='', special_title='',
@@ -118,6 +125,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
if missing_keys: if missing_keys:
raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员") raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员")
self.bot_account_id = self.config["robot_name"]
self.bot = DingTalkClient( self.bot = DingTalkClient(
client_id=config["client_id"], client_id=config["client_id"],
client_secret=config["client_secret"], client_secret=config["client_secret"],
@@ -153,10 +162,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
], ],
): ):
async def on_message(event: DingTalkEvent): async def on_message(event: DingTalkEvent):
self.bot_account_id = 'justbot'
try: try:
return await callback( return await callback(
await self.event_converter.target2yiri(event), self await self.event_converter.target2yiri(event, self.config["robot_name"]), self
) )
except: except:
traceback.print_exc() traceback.print_exc()
@@ -167,7 +175,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
self.bot.on_message("GroupMessage")(on_message) self.bot.on_message("GroupMessage")(on_message)
async def run_async(self): async def run_async(self):
await self.bot.start() await self.bot.start()
async def kill(self) -> bool: async def kill(self) -> bool:

View File

@@ -43,9 +43,11 @@ class GewechatMessageConverter(adapter.MessageConverter):
elif isinstance(component, platform_message.Plain): elif isinstance(component, platform_message.Plain):
content_list.append({"type": "text", "content": component.text}) content_list.append({"type": "text", "content": component.text})
elif isinstance(component, platform_message.Image): elif isinstance(component, platform_message.Image):
content_list.append({"type": "image", "url": component.url}) if not component.url:
# content_list.append({"type": "image", "image_id": component.image_id}) pass
#pass content_list.append({"type": "image", "image": component.url})
elif isinstance(component, platform_message.Voice): elif isinstance(component, platform_message.Voice):
content_list.append({"type": "voice", "url": component.url, "length": component.length}) content_list.append({"type": "voice", "url": component.url, "length": component.length})
elif isinstance(component, platform_message.Forward): elif isinstance(component, platform_message.Forward):
@@ -86,6 +88,7 @@ class GewechatMessageConverter(adapter.MessageConverter):
platform_message.Plain(text="[图片内容为空]") platform_message.Plain(text="[图片内容为空]")
]) ])
try: try:
base64_str, image_format = await image.get_gewechat_image_base64( base64_str, image_format = await image.get_gewechat_image_base64(
gewechat_url=self.config["gewechat_url"], gewechat_url=self.config["gewechat_url"],
@@ -104,7 +107,7 @@ class GewechatMessageConverter(adapter.MessageConverter):
except Exception as e: except Exception as e:
print(f"处理图片消息失败: {str(e)}") print(f"处理图片消息失败: {str(e)}")
return platform_message.MessageChain([ return platform_message.MessageChain([
platform_message.Plain(text="[图片处理失败]") platform_message.Plain(text=f"[图片处理失败]")
]) ])
elif message["Data"]["MsgType"] == 34: elif message["Data"]["MsgType"] == 34:
audio_base64 = message["Data"]["ImgBuf"]["buffer"] audio_base64 = message["Data"]["ImgBuf"]["buffer"]
@@ -246,10 +249,18 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
message: platform_message.MessageChain message: platform_message.MessageChain
): ):
geweap_msg = await GewechatMessageConverter.yiri2target(message) geweap_msg = await GewechatMessageConverter.yiri2target(message)
# 此处加上群消息at处理
# ats = [item["target"] for item in geweap_msg if item["type"] == "at"]
for msg in geweap_msg: for msg in geweap_msg:
if msg['type'] == 'text': if msg['type'] == 'text':
await self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id,content=msg['content']) 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( async def reply_message(
self, self,

View File

@@ -13,27 +13,20 @@ import pydantic.v1 as pydantic
class Entity(pydantic.BaseModel): class Entity(pydantic.BaseModel):
"""实体,表示一个用户或群。""" """实体,表示一个用户或群。"""
id: int id: int
"""QQ 号或群号""" """ID"""
@abc.abstractmethod
def get_avatar_url(self) -> str:
"""头像图片链接。"""
@abc.abstractmethod @abc.abstractmethod
def get_name(self) -> str: def get_name(self) -> str:
"""名称。""" """名称。"""
class Friend(Entity): class Friend(Entity):
"""好友""" """私聊对象"""
id: typing.Union[int, str] id: typing.Union[int, str]
"""QQ 号""" """ID"""
nickname: typing.Optional[str] nickname: typing.Optional[str]
"""昵称。""" """昵称。"""
remark: 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: def get_name(self) -> str:
return self.nickname or self.remark or '' return self.nickname or self.remark or ''
@@ -59,8 +52,6 @@ class Group(Entity):
"""群名称。""" """群名称。"""
permission: Permission permission: Permission
"""Bot 在群中的权限。""" """Bot 在群中的权限。"""
def get_avatar_url(self) -> str:
return f'https://p.qlogo.cn/gh/{self.id}/{self.id}/'
def get_name(self) -> str: def get_name(self) -> str:
return self.name return self.name
@@ -69,11 +60,11 @@ class Group(Entity):
class GroupMember(Entity): class GroupMember(Entity):
"""群成员。""" """群成员。"""
id: typing.Union[int, str] id: typing.Union[int, str]
"""QQ 号""" """群员 ID"""
member_name: str member_name: str
"""员名称。""" """群员名称。"""
permission: Permission permission: Permission
"""Bot 在群中的权限。""" """在群中的权限。"""
group: Group group: Group
"""群。""" """群。"""
special_title: str = '' special_title: str = ''
@@ -84,61 +75,6 @@ class GroupMember(Entity):
"""最后一次发言的时间。""" """最后一次发言的时间。"""
mute_time_remaining: int = 0 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: def get_name(self) -> str:
return self.member_name 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):
"""群成员信息。"""

View File

@@ -43,21 +43,6 @@ class Event(pydantic.BaseModel):
return Event return Event
###############################
# Bot Event
class BotEvent(Event):
"""Bot 自身事件。
Args:
type: 事件名。
qq: Bot 的 QQ 号。
"""
type: str
"""事件名。"""
qq: int
"""Bot 的 QQ 号。"""
############################### ###############################
# Message Event # Message Event
class MessageEvent(Event): class MessageEvent(Event):
@@ -79,7 +64,7 @@ class MessageEvent(Event):
class FriendMessage(MessageEvent): class FriendMessage(MessageEvent):
"""好友消息。 """私聊消息。
Args: Args:
type: 事件名。 type: 事件名。
@@ -111,19 +96,3 @@ class GroupMessage(MessageEvent):
@property @property
def group(self) -> platform_entities.Group: def group(self) -> platform_entities.Group:
return self.sender.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
"""消息内容。"""

View File

@@ -116,18 +116,6 @@ class MessageChain(PlatformBaseModel):
print('At Me') 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] __root__: typing.List[MessageComponent]
@@ -488,9 +476,9 @@ class Quote(MessageComponent):
group_id: typing.Optional[typing.Union[int, str]] = None group_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息所接收的群号当为好友消息时为0。""" """被引用回复的原消息所接收的群号当为好友消息时为0。"""
sender_id: typing.Optional[typing.Union[int, str]] = None sender_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的发送者的QQ号""" """被引用回复的原消息的发送者的ID"""
target_id: typing.Optional[typing.Union[int, str]] = None target_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的接收者者的QQ号或群号""" """被引用回复的原消息的接收者者的ID或群ID"""
origin: MessageChain origin: MessageChain
"""被引用回复的原消息的消息链对象。""" """被引用回复的原消息的消息链对象。"""
@@ -504,7 +492,7 @@ class At(MessageComponent):
type: str = "At" type: str = "At"
"""消息组件类型。""" """消息组件类型。"""
target: typing.Union[int, str] target: typing.Union[int, str]
"""群员 QQ 号""" """群员 ID"""
display: typing.Optional[str] = None display: typing.Optional[str] = None
"""At时显示的文字发送消息时无效自动使用群名片。""" """At时显示的文字发送消息时无效自动使用群名片。"""
def __eq__(self, other): def __eq__(self, other):
@@ -527,9 +515,9 @@ class Image(MessageComponent):
type: str = "Image" type: str = "Image"
"""消息组件类型。""" """消息组件类型。"""
image_id: typing.Optional[str] = None image_id: typing.Optional[str] = None
"""图片的 image_id群图片与好友图片格式不同。不为空时将忽略 url 属性。""" """图片的 image_id不为空时将忽略 url 属性。"""
url: typing.Optional[pydantic.HttpUrl] = None url: typing.Optional[pydantic.HttpUrl] = None
"""图片的 URL发送时可作网络图片的链接接收时为腾讯图片服务器的链接,可用于图片下载。""" """图片的 URL发送时可作网络图片的链接接收时为图片的链接,可用于图片下载。"""
path: typing.Union[str, Path, None] = None path: typing.Union[str, Path, None] = None
"""图片的路径,发送本地图片。""" """图片的路径,发送本地图片。"""
base64: typing.Optional[str] = None base64: typing.Optional[str] = None
@@ -663,7 +651,7 @@ class Voice(MessageComponent):
voice_id: typing.Optional[str] = None voice_id: typing.Optional[str] = None
"""语音的 voice_id不为空时将忽略 url 属性。""" """语音的 voice_id不为空时将忽略 url 属性。"""
url: typing.Optional[str] = None url: typing.Optional[str] = None
"""语音的 URL发送时可作网络语音的链接接收时为腾讯语音服务器的链接,可用于语音下载。""" """语音的 URL发送时可作网络语音的链接接收时为语音文件的链接,可用于语音下载。"""
path: typing.Optional[str] = None path: typing.Optional[str] = None
"""语音的路径,发送本地语音。""" """语音的路径,发送本地语音。"""
base64: 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: Args:
filename: 下载到本地的文件路径。与 `directory` 二选一。 filename: 下载到本地的文件路径。与 `directory` 二选一。
directory: 下载到本地的文件夹路径。与 `filename` 二选一。 directory: 下载到本地的文件夹路径。与 `filename` 二选一。
@@ -750,13 +736,13 @@ class Voice(MessageComponent):
class ForwardMessageNode(pydantic.BaseModel): class ForwardMessageNode(pydantic.BaseModel):
"""合并转发中的一条消息。""" """合并转发中的一条消息。"""
sender_id: typing.Optional[typing.Union[int, str]] = None sender_id: typing.Optional[typing.Union[int, str]] = None
"""发送人QQ号""" """发送人ID"""
sender_name: typing.Optional[str] = None sender_name: typing.Optional[str] = None
"""显示名称。""" """显示名称。"""
message_chain: typing.Optional[MessageChain] = None message_chain: typing.Optional[MessageChain] = None
"""消息内容。""" """消息内容。"""
message_id: typing.Optional[int] = None message_id: typing.Optional[int] = None
"""消息的 message_id,可以只使用此属性,从缓存中读取消息内容""" """消息的 message_id。"""
time: typing.Optional[datetime] = None time: typing.Optional[datetime] = None
"""发送时间。""" """发送时间。"""
@pydantic.validator('message_chain', check_fields=False) @pydantic.validator('message_chain', check_fields=False)

View File

@@ -4,7 +4,7 @@ import aiohttp
from . import entities, requester from . import entities, requester
from ...core import app from ...core import app
from ...discover import engine
from . import token from . import token
from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl 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 ap: app.Application
requester_components: list[engine.Component]
model_list: list[entities.LLMModelInfo] model_list: list[entities.LLMModelInfo]
requesters: dict[str, requester.LLMAPIRequester] requesters: dict[str, requester.LLMAPIRequester]
@@ -38,14 +40,21 @@ class ModelManager:
async def initialize(self): async def initialize(self):
self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester')
# 初始化token_mgr, requester # 初始化token_mgr, requester
for k, v in self.ap.provider_cfg.data['keys'].items(): for k, v in self.ap.provider_cfg.data['keys'].items():
self.token_mgrs[k] = token.TokenManager(k, v) 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) api_inst = api_cls(self.ap)
await api_inst.initialize() await api_inst.initialize()
self.requesters[api_inst.name] = api_inst self.requesters[component.metadata.name] = api_inst
# 尝试从api获取最新的模型信息 # 尝试从api获取最新的模型信息
try: try:

View File

@@ -10,18 +10,6 @@ from . import entities as modelmgr_entities
from ..tools import entities as tools_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): class LLMAPIRequester(metaclass=abc.ABCMeta):
"""LLM API请求器 """LLM API请求器
""" """

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
import json
import traceback import traceback
import base64 import base64
@@ -16,7 +17,6 @@ from ...tools import entities as tools_entities
from ....utils import image from ....utils import image
@requester.requester_class("anthropic-messages")
class AnthropicMessages(requester.LLMAPIRequester): class AnthropicMessages(requester.LLMAPIRequester):
"""Anthropic Messages API 请求器""" """Anthropic Messages API 请求器"""
@@ -69,11 +69,32 @@ class AnthropicMessages(requester.LLMAPIRequester):
req_messages = [] req_messages = []
for m in messages: for m in messages:
if isinstance(m.content, str) and m.content.strip() != "": if m.role == 'tool':
req_messages.append(m.dict(exclude_none=True)) tool_call_id = m.tool_call_id
elif isinstance(m.content, list):
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): for i, ce in enumerate(m.content):
@@ -90,25 +111,60 @@ class AnthropicMessages(requester.LLMAPIRequester):
} }
msg_dict["content"][i] = alter_image_ele 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 args["messages"] = req_messages
# anthropic的tools处在beta阶段sdk不稳定故暂时不支持 if funcs:
# tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs)
# if funcs:
# tools = await self.ap.tool_mgr.generate_tools_for_openai(funcs)
# if tools: if tools:
# args["tools"] = tools args["tools"] = tools
try: try:
# print(json.dumps(args, indent=4, ensure_ascii=False))
resp = await self.client.messages.create(**args) resp = await self.client.messages.create(**args)
return llm_entities.Message( args = {
content=resp.content[0].text, 'content': '',
role=resp.role '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: except anthropic.AuthenticationError as e:
raise errors.RequesterError(f'api-key 无效: {e.message}') raise errors.RequesterError(f'api-key 无效: {e.message}')
except anthropic.BadRequestError as e: except anthropic.BadRequestError as e:

View 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

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("bailian-chat-completions")
class BailianChatCompletions(chatcmpl.OpenAIChatCompletions): class BailianChatCompletions(chatcmpl.OpenAIChatCompletions):
"""阿里云百炼大模型平台 ChatCompletion API 请求器""" """阿里云百炼大模型平台 ChatCompletion API 请求器"""

View 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

View File

@@ -20,7 +20,6 @@ from ...tools import entities as tools_entities
from ....utils import image from ....utils import image
@requester.requester_class("openai-chat-completions")
class OpenAIChatCompletions(requester.LLMAPIRequester): class OpenAIChatCompletions(requester.LLMAPIRequester):
"""OpenAI ChatCompletion API 请求器""" """OpenAI ChatCompletion API 请求器"""

View 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

View File

@@ -7,7 +7,6 @@ from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
@requester.requester_class("deepseek-chat-completions")
class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Deepseek ChatCompletion API 请求器""" """Deepseek ChatCompletion API 请求器"""

View 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

View File

@@ -14,7 +14,6 @@ from ...tools import entities as tools_entities
from .. import entities as modelmgr_entities from .. import entities as modelmgr_entities
@requester.requester_class("gitee-ai-chat-completions")
class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Gitee AI ChatCompletions API 请求器""" """Gitee AI ChatCompletions API 请求器"""

View 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

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("lmstudio-chat-completions")
class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions): class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions):
"""LMStudio ChatCompletion API 请求器""" """LMStudio ChatCompletion API 请求器"""

View 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

View File

@@ -9,7 +9,6 @@ from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
@requester.requester_class("moonshot-chat-completions")
class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Moonshot ChatCompletion API 请求器""" """Moonshot ChatCompletion API 请求器"""

View 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

View File

@@ -20,7 +20,6 @@ from ....utils import image
REQUESTER_NAME: str = "ollama-chat" REQUESTER_NAME: str = "ollama-chat"
@requester.requester_class(REQUESTER_NAME)
class OllamaChatCompletions(requester.LLMAPIRequester): class OllamaChatCompletions(requester.LLMAPIRequester):
"""Ollama平台 ChatCompletion API请求器""" """Ollama平台 ChatCompletion API请求器"""
client: ollama.AsyncClient client: ollama.AsyncClient

View 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

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("siliconflow-chat-completions")
class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions): class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions):
"""SiliconFlow ChatCompletion API 请求器""" """SiliconFlow ChatCompletion API 请求器"""

View 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

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("volcark-chat-completions")
class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions): class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions):
"""火山方舟大模型平台 ChatCompletion API 请求器""" """火山方舟大模型平台 ChatCompletion API 请求器"""

View 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

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("xai-chat-completions")
class XaiChatCompletions(chatcmpl.OpenAIChatCompletions): class XaiChatCompletions(chatcmpl.OpenAIChatCompletions):
"""xAI ChatCompletion API 请求器""" """xAI ChatCompletion API 请求器"""

View 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

View File

@@ -7,7 +7,6 @@ from . import chatcmpl
from .. import requester from .. import requester
@requester.requester_class("zhipuai-chat-completions")
class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions): class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""智谱AI ChatCompletion API 请求器""" """智谱AI ChatCompletion API 请求器"""

View 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

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.9.5" semantic_version = "v3.4.10"
debug_mode = False debug_mode = False

View File

@@ -74,19 +74,29 @@
"name": "claude-3-opus-latest", "name": "claude-3-opus-latest",
"requester": "anthropic-messages", "requester": "anthropic-messages",
"token_mgr": "anthropic", "token_mgr": "anthropic",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "claude-3-5-sonnet-latest", "name": "claude-3-5-sonnet-latest",
"requester": "anthropic-messages", "requester": "anthropic-messages",
"token_mgr": "anthropic", "token_mgr": "anthropic",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "claude-3-5-haiku-latest", "name": "claude-3-5-haiku-latest",
"requester": "anthropic-messages", "requester": "anthropic-messages",
"token_mgr": "anthropic", "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", "name": "moonshot-v1-8k",