Compare commits

..

15 Commits

Author SHA1 Message Date
Junyan Qin
e775499080 chore: release v3.4.5 2025-02-02 17:10:12 +08:00
Junyan Qin
735aad5a91 doc: README 2025-02-02 16:32:40 +08:00
Junyan Qin
fb4e106f69 doc: update README 2025-02-02 16:31:32 +08:00
Junyan Qin (Chin)
e5659db535 Merge pull request #1002 from RockChinQ/feat/discord
feat: add `discord` adapter
2025-02-02 16:30:40 +08:00
Junyan Qin
5381e09a6c chore: config for discord 2025-02-02 16:28:21 +08:00
Junyan Qin
21f16ecd68 feat: discord adapter 2025-02-02 12:18:18 +08:00
Junyan Qin (Chin)
12fc76b326 Update README.md 2025-02-02 11:11:49 +08:00
Junyan Qin (Chin)
d7f87dd269 更新 README.md 2025-02-02 00:10:41 +08:00
Junyan Qin (Chin)
56227f3713 更新 README.md 2025-02-02 00:10:04 +08:00
Junyan Qin (Chin)
f492fee486 Merge pull request #1000 from RockChinQ/feat/siliconflow
feat: siliconflow provider
2025-02-01 14:19:43 +08:00
Junyan Qin
41a7814615 feat: siliconflow provider 2025-02-01 14:19:21 +08:00
Junyan Qin
8644f2c166 chore: update 2025-02-01 13:53:20 +08:00
Junyan Qin
e4a9365caf chore: update issue template 2025-02-01 12:13:20 +08:00
Junyan Qin (Chin)
9fc7af1295 Merge pull request #999 from RockChinQ/feat/lm-studio
feat: add supports for LM Studio
2025-02-01 12:01:45 +08:00
Junyan Qin
d0eeb2b304 feat: add supports for LM Studio 2025-02-01 12:01:07 +08:00
19 changed files with 508 additions and 12 deletions

View File

@@ -12,6 +12,8 @@ body:
- Nakurugo-cqhttp - Nakurugo-cqhttp
- aiocqhttp使用 OneBot 协议接入的) - aiocqhttp使用 OneBot 协议接入的)
- qq-botpyQQ官方API - qq-botpyQQ官方API
- lark飞书
- wecom企业微信
validations: validations:
required: true required: true
- type: input - type: input
@@ -30,9 +32,9 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: 复现步骤 label: 复现步骤
description: 如何重现这个问题,越详细越好 description: 如何重现这个问题,越详细越好;请贴上所有相关的配置文件和元数据文件(注意隐去敏感信息)
validations: validations:
required: false required: true
- type: textarea - type: textarea
attributes: attributes:
label: 启用的插件 label: 启用的插件

View File

@@ -15,7 +15,6 @@
<a href="https://docs.langbot.app/plugin/plugin-intro.html">插件介绍</a> <a href="https://docs.langbot.app/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a> <a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
<div align="center"> <div align="center">
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖 😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
</div> </div>
@@ -81,9 +80,12 @@
| 平台 | 状态 | 备注 | | 平台 | 状态 | 备注 |
| --- | --- | --- | | --- | --- | --- |
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 | | QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 频道机器人,支持频道、私聊、群聊 | | QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | | | 企业微信 | ✅ | |
| 飞书 | ✅ | | | 飞书 | ✅ | |
| Discord | ✅ | |
| 个人微信 | 🚧 | |
| WhatsApp | 🚧 | |
| 钉钉 | 🚧 | | | 钉钉 | 🚧 | |
🚧: 正在开发中 🚧: 正在开发中
@@ -99,8 +101,11 @@
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | | | [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 | | [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型管理平台 | | [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 | | [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台 |
## 😘 社区贡献 ## 😘 社区贡献
@@ -109,4 +114,4 @@ LangBot 离不开以下贡献者和社区内所有人的贡献,我们欢迎任
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>

View File

@@ -26,7 +26,8 @@ required_deps = {
"argon2": "argon2-cffi", "argon2": "argon2-cffi",
"jwt": "pyjwt", "jwt": "pyjwt",
"Crypto": "pycryptodome", "Crypto": "pycryptodome",
"lark_oapi": "lark-oapi" "lark_oapi": "lark-oapi",
"discord": "discord.py"
} }

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("lmstudio-config", 22)
class LmStudioConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'lmstudio-chat-completions' not in self.ap.provider_cfg.data['requester']
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['requester']['lmstudio-chat-completions'] = {
"base-url": "http://127.0.0.1:1234/v1",
"args": {},
"timeout": 120
}
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,27 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("siliconflow-config", 23)
class SiliconFlowConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'siliconflow-chat-completions' not in self.ap.provider_cfg.data['requester']
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['keys']['siliconflow'] = [
"xxxxxxx"
]
self.ap.provider_cfg.data['requester']['siliconflow-chat-completions'] = {
"base-url": "https://api.siliconflow.cn/v1",
"args": {},
"timeout": 120
}
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("discord-config", 24)
class DiscordConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'discord':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "discord",
"enable": False,
"client_id": "1234567890",
"token": "XXXXXXXXXX"
})
await self.ap.platform_cfg.dump_config()

View File

@@ -8,7 +8,7 @@ from ..migrations import m001_sensitive_word_migration, m002_openai_config_migra
from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config
from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_api_timeout_params, m018_xai_config, m019_zhipuai_config from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_api_timeout_params, m018_xai_config, m019_zhipuai_config
from ..migrations import m020_wecom_config, m021_lark_config from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config
@stage.stage_class("MigrationStage") @stage.stage_class("MigrationStage")

View File

@@ -37,7 +37,7 @@ class PlatformManager:
async def initialize(self): async def initialize(self):
from .sources import nakuru, aiocqhttp, qqbotpy, wecom, lark from .sources import nakuru, aiocqhttp, qqbotpy, wecom, lark, discord
async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter): async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter):

View File

@@ -0,0 +1,264 @@
from __future__ import annotations
import discord
import typing
import asyncio
import traceback
import time
import re
import base64
import uuid
import json
import os
import datetime
import aiohttp
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...utils import image
class DiscordMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain
) -> typing.Tuple[str, typing.List[discord.File]]:
for ele in message_chain:
if isinstance(ele, platform_message.At):
message_chain.remove(ele)
break
text_string = ""
image_files = []
for ele in message_chain:
if isinstance(ele, platform_message.Image):
image_bytes = None
if ele.base64:
image_bytes = base64.b64decode(ele.base64)
elif ele.url:
async with aiohttp.ClientSession() as session:
async with session.get(ele.url) as response:
image_bytes = await response.read()
elif ele.path:
with open(ele.path, "rb") as f:
image_bytes = f.read()
image_files.append(discord.File(fp=image_bytes, filename=f"{uuid.uuid4()}.png"))
elif isinstance(ele, platform_message.Plain):
text_string += ele.text
return text_string, image_files
@staticmethod
async def target2yiri(
message: discord.Message
) -> platform_message.MessageChain:
lb_msg_list = []
msg_create_time = datetime.datetime.fromtimestamp(
int(message.created_at.timestamp())
)
lb_msg_list.append(
platform_message.Source(id=message.id, time=msg_create_time)
)
element_list = []
def text_element_recur(text_ele: str) -> list[platform_message.MessageComponent]:
if text_ele == "":
return []
# <@1234567890>
# @everyone
# @here
at_pattern = re.compile(r"(@everyone|@here|<@[\d]+>)")
at_matches = at_pattern.findall(text_ele)
if len(at_matches) > 0:
mid_at = at_matches[0]
text_split = text_ele.split(mid_at)
mid_at_component = []
if mid_at == "@everyone" or mid_at == "@here":
mid_at_component.append(platform_message.AtAll())
else:
mid_at_component.append(platform_message.At(target=mid_at[2:-1]))
return text_element_recur(text_split[0]) + \
mid_at_component + \
text_element_recur(text_split[1])
else:
return [platform_message.Plain(text=text_ele)]
element_list.extend(text_element_recur(message.content))
# attachments
for attachment in message.attachments:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(attachment.url) as response:
image_data = await response.read()
image_base64 = base64.b64encode(image_data).decode("utf-8")
image_format = response.headers["Content-Type"]
element_list.append(platform_message.Image(base64=f"data:{image_format};base64,{image_base64}"))
return platform_message.MessageChain(element_list)
class DiscordEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(
event: platform_events.Event
) -> discord.Message:
pass
@staticmethod
async def target2yiri(
event: discord.Message
) -> platform_events.Event:
message_chain = await DiscordMessageConverter.target2yiri(event)
if type(event.channel) == discord.DMChannel:
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event.author.id,
nickname=event.author.name,
remark=event.channel.id,
),
message_chain=message_chain,
time=event.created_at.timestamp(),
source_platform_object=event,
)
elif type(event.channel) == discord.TextChannel:
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=event.author.id,
member_name=event.author.name,
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=event.channel.id,
name=event.channel.name,
permission=platform_entities.Permission.Member,
),
special_title="",
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=message_chain,
time=event.created_at.timestamp(),
source_platform_object=event,
)
@adapter.adapter_class("discord")
class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter):
bot: discord.Client
bot_account_id: str # 用于在流水线中识别at是否是本bot直接以bot_name作为标识
config: dict
ap: app.Application
message_converter: DiscordMessageConverter = DiscordMessageConverter()
event_converter: DiscordEventConverter = DiscordEventConverter()
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
self.bot_account_id = self.config["client_id"]
adapter_self = self
class MyClient(discord.Client):
async def on_message(self: discord.Client, message: discord.Message):
if message.author.id == self.user.id or message.author.bot:
return
lb_event = await adapter_self.event_converter.target2yiri(message)
await adapter_self.listeners[type(lb_event)](lb_event, adapter_self)
intents = discord.Intents.default()
intents.message_content = True
args = {}
if os.getenv("http_proxy"):
args["proxy"] = os.getenv("http_proxy")
self.bot = MyClient(intents=intents, **args)
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
msg_to_send, image_files = await self.message_converter.yiri2target(message)
assert isinstance(message_source.source_platform_object, discord.Message)
args = {
"content": msg_to_send,
}
if len(image_files) > 0:
args["files"] = image_files
if quote_origin:
args["reference"] = message_source.source_platform_object
if message.has(platform_message.At):
args["mention_author"] = True
await message_source.source_platform_object.channel.send(**args)
async def is_muted(self, group_id: int) -> bool:
return False
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
):
self.listeners.pop(event_type)
async def run_async(self):
async with self.bot:
await self.bot.start(self.config["token"], reconnect=True)
async def kill(self) -> bool:
await self.bot.close()
return True

View File

@@ -72,6 +72,11 @@ class MessageEvent(Event):
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
"""消息内容。""" """消息内容。"""
source_platform_object: typing.Optional[typing.Any] = None
"""原消息平台对象。
供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息,
那么可以将其存到这个字段以供之后取出使用。"""
class FriendMessage(MessageEvent): class FriendMessage(MessageEvent):
"""好友消息。 """好友消息。

View File

@@ -6,7 +6,7 @@ from . import entities, requester
from ...core import app from ...core import app
from . import token from . import token
from .requesters import chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, xaichatcmpl, zhipuaichatcmpl from .requesters import chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl
FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list" FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list"
@@ -109,4 +109,4 @@ class ModelManager:
self.model_list.append(model_info) self.model_list.append(model_info)
except Exception as e: except Exception as e:
self.ap.logger.error(f"初始化模型 {model['name']} 失败: {e} ,请检查配置文件") self.ap.logger.error(f"初始化模型 {model['name']} 失败: {type(e)} {e} ,请检查配置文件")

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("lmstudio-chat-completions")
class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions):
"""LMStudio ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['lmstudio-chat-completions']

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("siliconflow-chat-completions")
class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions):
"""SiliconFlow ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['siliconflow-chat-completions']

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.4.1" semantic_version = "v3.4.5"
debug_mode = False debug_mode = False

View File

@@ -26,6 +26,7 @@ argon2-cffi
pyjwt pyjwt
pycryptodome pycryptodome
lark-oapi lark-oapi
discord.py
# indirect # indirect
taskgroup==0.0.0a4 taskgroup==0.0.0a4

View File

@@ -42,6 +42,12 @@
"app_id": "cli_abcdefgh", "app_id": "cli_abcdefgh",
"app_secret": "XXXXXXXXXX", "app_secret": "XXXXXXXXXX",
"bot_name": "LangBot" "bot_name": "LangBot"
},
{
"adapter": "discord",
"enable": true,
"client_id": "1234567890",
"token": "XXXXXXXXXX"
} }
], ],
"track-function-calls": true, "track-function-calls": true,

View File

@@ -22,6 +22,9 @@
], ],
"zhipuai": [ "zhipuai": [
"xxxxxxx" "xxxxxxx"
],
"siliconflow": [
"xxxxxxx"
] ]
}, },
"requester": { "requester": {
@@ -66,6 +69,16 @@
"base-url": "https://open.bigmodel.cn/api/paas/v4", "base-url": "https://open.bigmodel.cn/api/paas/v4",
"args": {}, "args": {},
"timeout": 120 "timeout": 120
},
"lmstudio-chat-completions": {
"base-url": "http://127.0.0.1:1234/v1",
"args": {},
"timeout": 120
},
"siliconflow-chat-completions": {
"base-url": "https://api.siliconflow.cn/v1",
"args": {},
"timeout": 120
} }
}, },
"model": "gpt-4o", "model": "gpt-4o",

View File

@@ -213,6 +213,37 @@
"description": "飞书的bot_name" "description": "飞书的bot_name"
} }
} }
},
{
"title": "Discord 适配器",
"description": "用于接入 Discord",
"properties": {
"adapter": {
"type": "string",
"const": "discord"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"client_id": {
"type": "string",
"default": "",
"description": "Discord 的 client_id"
},
"token": {
"type": "string",
"default": "",
"description": "Discord 的 token"
}
}
} }
] ]
} }

View File

@@ -74,6 +74,14 @@
"type": "string" "type": "string"
}, },
"default": [] "default": []
},
"siliconflow": {
"type": "array",
"title": "SiliconFlow API 密钥",
"items": {
"type": "string"
},
"default": []
} }
} }
}, },
@@ -240,6 +248,46 @@
"default": 120 "default": 120
} }
} }
},
"lmstudio-chat-completions": {
"type": "object",
"title": "LMStudio API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
},
"siliconflow-chat-completions": {
"type": "object",
"title": "SiliconFlow API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
} }
} }
}, },