diff --git a/README.md b/README.md index dd518656..347db63f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@
+Featured|HelloGitHub + [English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language) [![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87) @@ -85,8 +87,9 @@ docker compose up -d | --- | --- | --- | | QQ 个人号 | ✅ | QQ 个人号私聊、群聊 | | QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 | -| 微信 | ✅ | | +| 企业微信 | ✅ | | | 企微对外客服 | ✅ | | +| 个人微信 | ✅ | | | 微信公众号 | ✅ | | | 飞书 | ✅ | | | 钉钉 | ✅ | | diff --git a/README_TW.md b/README_TW.md index 27bf5e14..7746031a 100644 --- a/README_TW.md +++ b/README_TW.md @@ -3,7 +3,7 @@ LangBot -
+
Featured|HelloGitHub [English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language) diff --git a/libs/qq_official_api/api.py b/libs/qq_official_api/api.py index fa38073d..cb5f658a 100644 --- a/libs/qq_official_api/api.py +++ b/libs/qq_official_api/api.py @@ -104,7 +104,7 @@ class QQOfficialClient: return {'code': 0, 'message': 'success'} except Exception as e: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') return {'error': str(e)}, 400 async def run_task(self, host: str, port: int, *args, **kwargs): @@ -168,7 +168,6 @@ class QQOfficialClient: if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/v2/users/' + user_openid + '/messages' async with httpx.AsyncClient() as client: headers = { @@ -193,7 +192,6 @@ class QQOfficialClient: if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/v2/groups/' + group_openid + '/messages' async with httpx.AsyncClient() as client: headers = { @@ -209,7 +207,7 @@ class QQOfficialClient: if response.status_code == 200: return else: - await self.logger.error(f"发送群聊消息失败:{response.json()}") + await self.logger.error(f'发送群聊消息失败:{response.json()}') raise Exception(response.read().decode()) async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str): @@ -217,7 +215,6 @@ class QQOfficialClient: if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/channels/' + channel_id + '/messages' async with httpx.AsyncClient() as client: headers = { @@ -240,7 +237,6 @@ class QQOfficialClient: """发送频道私聊消息""" if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/dms/' + guild_id + '/messages' async with httpx.AsyncClient() as client: diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index c291e92f..746d15da 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -34,7 +34,6 @@ class SlackClient: if self.bot_user_id and bot_user_id == self.bot_user_id: return jsonify({'status': 'ok'}) - # 处理私信 if data and data.get('event', {}).get('channel_type') in ['im']: @@ -52,7 +51,7 @@ class SlackClient: return jsonify({'status': 'ok'}) except Exception as e: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') raise (e) async def _handle_message(self, event: SlackEvent): @@ -82,7 +81,7 @@ class SlackClient: self.bot_user_id = response['message']['bot_id'] return except Exception as e: - await self.logger.error(f"Error in send_message: {e}") + await self.logger.error(f'Error in send_message: {e}') raise e async def send_message_to_one(self, text: str, user_id: str): @@ -93,7 +92,7 @@ class SlackClient: return except Exception as e: - await self.logger.error(f"Error in send_message: {traceback.format_exc()}") + await self.logger.error(f'Error in send_message: {traceback.format_exc()}') raise e async def run_task(self, host: str, port: int, *args, **kwargs): diff --git a/libs/wechatpad_api/api/user.py b/libs/wechatpad_api/api/user.py index 2dc73bd2..d2187c7c 100644 --- a/libs/wechatpad_api/api/user.py +++ b/libs/wechatpad_api/api/user.py @@ -12,12 +12,9 @@ class UserApi: return get_json(base_url=url, token=self.token) - def get_qr_code(self, recover:bool=True, style:int=8): + def get_qr_code(self, recover: bool = True, style: int = 8): """获取自己的二维码""" - param = { - "Recover": recover, - "Style": style - } + param = {'Recover': recover, 'Style': style} url = f'{self.base_url}/user/GetMyQRCode' return post_json(base_url=url, token=self.token, data=param) @@ -26,12 +23,8 @@ class UserApi: url = f'{self.base_url}/equipment/GetSafetyInfo' return post_json(base_url=url, token=self.token) - - - async def update_head_img(self, head_img_base64): + async def update_head_img(self, head_img_base64): """修改头像""" - param = { - "Base64": head_img_base64 - } + param = {'Base64': head_img_base64} url = f'{self.base_url}/user/UploadHeadImage' - return await async_request(base_url=url, token_key=self.token, json=param) \ No newline at end of file + return await async_request(base_url=url, token_key=self.token, json=param) diff --git a/libs/wechatpad_api/client.py b/libs/wechatpad_api/client.py index f5ded1cb..5e699d03 100644 --- a/libs/wechatpad_api/client.py +++ b/libs/wechatpad_api/client.py @@ -1,4 +1,3 @@ - from libs.wechatpad_api.api.login import LoginApi from libs.wechatpad_api.api.friend import FriendApi from libs.wechatpad_api.api.message import MessageApi @@ -7,9 +6,6 @@ from libs.wechatpad_api.api.downloadpai import DownloadApi from libs.wechatpad_api.api.chatroom import ChatRoomApi - - - class WeChatPadClient: def __init__(self, base_url, token, logger=None): self._login_api = LoginApi(base_url, token) @@ -20,16 +16,16 @@ class WeChatPadClient: self._chatroom_api = ChatRoomApi(base_url, token) self.logger = logger - def get_token(self,admin_key, day: int): - '''获取token''' + def get_token(self, admin_key, day: int): + """获取token""" return self._login_api.get_token(admin_key, day) - def get_login_qr(self, Proxy:str=""): + def get_login_qr(self, Proxy: str = ''): """登录二维码""" return self._login_api.get_login_qr(Proxy=Proxy) - def awaken_login(self, Proxy:str=""): - '''唤醒登录''' + def awaken_login(self, Proxy: str = ''): + """唤醒登录""" return self._login_api.wake_up_login(Proxy=Proxy) def log_out(self): @@ -40,59 +36,57 @@ class WeChatPadClient: """获取登录状态""" return self._login_api.get_login_status() - def send_text_message(self, to_wxid, message, ats: list=[]): + def send_text_message(self, to_wxid, message, ats: list = []): """发送文本消息""" - return self._message_api.post_text(to_wxid, message, ats) + return self._message_api.post_text(to_wxid, message, ats) - def send_image_message(self, to_wxid, img_url, ats: list=[]): + def send_image_message(self, to_wxid, img_url, ats: list = []): """发送图片消息""" - return self._message_api.post_image(to_wxid, img_url, ats) + return self._message_api.post_image(to_wxid, img_url, ats) def send_voice_message(self, to_wxid, voice_data, voice_forma, voice_duration): """发送音频消息""" - return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration) + return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration) def send_app_message(self, to_wxid, app_message, type): """发送app消息""" - return self._message_api.post_app_msg(to_wxid, app_message, type) + return self._message_api.post_app_msg(to_wxid, app_message, type) def send_emoji_message(self, to_wxid, emoji_md5, emoji_size): """发送emoji消息""" - return self._message_api.post_emoji(to_wxid,emoji_md5,emoji_size) + return self._message_api.post_emoji(to_wxid, emoji_md5, emoji_size) def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time): """撤回消息""" - return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time) + return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time) def get_profile(self): """获取用户信息""" return self._user_api.get_profile() - def get_qr_code(self, recover:bool=True, style:int=8): + def get_qr_code(self, recover: bool = True, style: int = 8): """获取用户二维码""" - return self._user_api.get_qr_code(recover=recover, style=style) + return self._user_api.get_qr_code(recover=recover, style=style) def get_safety_info(self): """获取设备信息""" - return self._user_api.get_safety_info() + return self._user_api.get_safety_info() - def update_head_img(self, head_img_base64): + def update_head_img(self, head_img_base64): """上传用户头像""" - return self._user_api.update_head_img(head_img_base64) + return self._user_api.update_head_img(head_img_base64) def cdn_download(self, aeskey, file_type, file_url): """cdn下载""" - return self._download_api.send_download( aeskey, file_type, file_url) + return self._download_api.send_download(aeskey, file_type, file_url) - def get_msg_voice(self,buf_id, length, msgid): + def get_msg_voice(self, buf_id, length, msgid): """下载语音""" return self._download_api.get_msg_voice(buf_id, length, msgid) - async def download_base64(self,url): + async def download_base64(self, url): return await self._download_api.download_url_to_base64(download_url=url) def get_chatroom_member_detail(self, chatroom_name): """查看群成员详情""" return self._chatroom_api.get_chatroom_member_detail(chatroom_name) - - diff --git a/libs/wechatpad_api/util/terminal_printer.py b/libs/wechatpad_api/util/terminal_printer.py index 48af021e..19a35ffa 100644 --- a/libs/wechatpad_api/util/terminal_printer.py +++ b/libs/wechatpad_api/util/terminal_printer.py @@ -1,31 +1,34 @@ import qrcode + def print_green(text): - print(f"\033[32m{text}\033[0m") + print(f'\033[32m{text}\033[0m') + def print_yellow(text): - print(f"\033[33m{text}\033[0m") + print(f'\033[33m{text}\033[0m') + def print_red(text): - print(f"\033[31m{text}\033[0m") + print(f'\033[31m{text}\033[0m') + def make_and_print_qr(url): """生成并打印二维码 - + Args: url: 需要生成二维码的URL字符串 - + Returns: None - + 功能: 1. 在终端打印二维码的ASCII图形 2. 同时提供在线二维码生成链接作为备选 """ - print_green("请扫描下方二维码登录") + print_green('请扫描下方二维码登录') qr = qrcode.QRCode() qr.add_data(url) qr.make() qr.print_ascii(invert=True) - print_green(f"也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}") - + print_green(f'也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}') diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index cbd1b73f..c1328b0d 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -57,7 +57,7 @@ class WecomClient: if 'access_token' in data: return data['access_token'] else: - await self.logger.error(f"获取accesstoken失败:{response.json()}") + await self.logger.error(f'获取accesstoken失败:{response.json()}') raise Exception(f'未获取access token: {data}') async def get_users(self): @@ -129,7 +129,7 @@ class WecomClient: response = await client.post(url, json=params) data = response.json() except Exception as e: - await self.logger.error(f"发送图片失败:{data}") + await self.logger.error(f'发送图片失败:{data}') raise Exception('Failed to send image: ' + str(e)) # 企业微信错误码40014和42001,代表accesstoken问题 @@ -164,7 +164,7 @@ class WecomClient: self.access_token = await self.get_access_token(self.secret) return await self.send_private_msg(user_id, agent_id, content) if data['errcode'] != 0: - await self.logger.error(f"发送消息失败:{data}") + await self.logger.error(f'发送消息失败:{data}') raise Exception('Failed to send message: ' + str(data)) async def handle_callback_request(self): @@ -181,7 +181,7 @@ class WecomClient: echostr = request.args.get('echostr') ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) if ret != 0: - await self.logger.error("验证失败") + await self.logger.error('验证失败') raise Exception(f'验证失败,错误码: {ret}') return reply_echo_str @@ -189,9 +189,8 @@ class WecomClient: encrypt_msg = await request.data ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce) if ret != 0: - await self.logger.error("消息解密失败") + await self.logger.error('消息解密失败') raise Exception(f'消息解密失败,错误码: {ret}') - # 解析消息并处理 message_data = await self.get_message(xml_msg) @@ -202,7 +201,7 @@ class WecomClient: return 'success' except Exception as e: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') return f'Error processing request: {str(e)}', 400 async def run_task(self, host: str, port: int, *args, **kwargs): @@ -301,7 +300,7 @@ class WecomClient: except binascii.Error as e: raise ValueError(f'Invalid base64 string: {str(e)}') else: - await self.logger.error("Image对象出错") + await self.logger.error('Image对象出错') raise ValueError('image对象出错') # 设置 multipart/form-data 格式的文件 @@ -325,7 +324,7 @@ class WecomClient: self.access_token = await self.get_access_token(self.secret) media_id = await self.upload_to_work(image) if data.get('errcode', 0) != 0: - await self.logger.error(f"上传图片失败:{data}") + await self.logger.error(f'上传图片失败:{data}') raise Exception('failed to upload file') media_id = data.get('media_id') diff --git a/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py index 09805aa9..32fab7f7 100644 --- a/libs/wecom_customer_service_api/api.py +++ b/libs/wecom_customer_service_api/api.py @@ -187,7 +187,7 @@ class WecomCSClient: self.access_token = await self.get_access_token(self.secret) return await self.send_text_msg(open_kfid, external_userid, msgid, content) if data['errcode'] != 0: - await self.logger.error(f"发送消息失败:{data}") + await self.logger.error(f'发送消息失败:{data}') raise Exception('Failed to send message') return data @@ -227,7 +227,7 @@ class WecomCSClient: return 'success' except Exception as e: if self.logger: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') else: traceback.print_exc() return f'Error processing request: {str(e)}', 400 diff --git a/pkg/api/http/controller/group.py b/pkg/api/http/controller/group.py index 16fa1df1..8ab4f4d9 100644 --- a/pkg/api/http/controller/group.py +++ b/pkg/api/http/controller/group.py @@ -123,4 +123,4 @@ class RouterGroup(abc.ABC): def http_status(self, status: int, code: int, msg: str) -> typing.Tuple[quart.Response, int]: """返回一个指定状态码的响应""" - return (self.fail(code, msg), status) \ No newline at end of file + return (self.fail(code, msg), status) diff --git a/pkg/api/http/controller/groups/pipelines/pipelines.py b/pkg/api/http/controller/groups/pipelines/pipelines.py index d056afb4..e3d08e28 100644 --- a/pkg/api/http/controller/groups/pipelines/pipelines.py +++ b/pkg/api/http/controller/groups/pipelines/pipelines.py @@ -13,7 +13,9 @@ class PipelinesRouterGroup(group.RouterGroup): if quart.request.method == 'GET': sort_by = quart.request.args.get('sort_by', 'created_at') sort_order = quart.request.args.get('sort_order', 'DESC') - return self.success(data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)}) + return self.success( + data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)} + ) elif quart.request.method == 'POST': json_data = await quart.request.json diff --git a/pkg/api/http/controller/groups/user.py b/pkg/api/http/controller/groups/user.py index d8024107..b84b2292 100644 --- a/pkg/api/http/controller/groups/user.py +++ b/pkg/api/http/controller/groups/user.py @@ -67,3 +67,19 @@ class UserRouterGroup(group.RouterGroup): await self.ap.user_service.reset_password(user_email, new_password) return self.success(data={'user': user_email}) + + @self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + async def _(user_email: str) -> str: + json_data = await quart.request.json + + current_password = json_data['current_password'] + new_password = json_data['new_password'] + + try: + await self.ap.user_service.change_password(user_email, current_password, new_password) + except argon2.exceptions.VerifyMismatchError: + return self.http_status(400, -1, 'Current password is incorrect') + except ValueError as e: + return self.http_status(400, -1, str(e)) + + return self.success(data={'user': user_email}) diff --git a/pkg/api/http/service/knowledge.py b/pkg/api/http/service/knowledge.py index 27506ec9..7b748bc6 100644 --- a/pkg/api/http/service/knowledge.py +++ b/pkg/api/http/service/knowledge.py @@ -78,7 +78,9 @@ class KnowledgeService: runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid) if runtime_kb is None: raise Exception('Knowledge base not found') - return [result.model_dump() for result in await runtime_kb.retrieve(query)] + return [ + result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k) + ] async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]: """获取知识库文件""" diff --git a/pkg/api/http/service/pipeline.py b/pkg/api/http/service/pipeline.py index 96504d61..d3d0bfa7 100644 --- a/pkg/api/http/service/pipeline.py +++ b/pkg/api/http/service/pipeline.py @@ -40,7 +40,7 @@ class PipelineService: async def get_pipelines(self, sort_by: str = 'created_at', sort_order: str = 'DESC') -> list[dict]: query = sqlalchemy.select(persistence_pipeline.LegacyPipeline) - + if sort_by == 'created_at': if sort_order == 'DESC': query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.desc()) @@ -51,7 +51,7 @@ class PipelineService: query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.desc()) else: query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.asc()) - + result = await self.ap.persistence_mgr.execute_async(query) pipelines = result.all() return [ diff --git a/pkg/api/http/service/user.py b/pkg/api/http/service/user.py index c724bfcf..7a1f0323 100644 --- a/pkg/api/http/service/user.py +++ b/pkg/api/http/service/user.py @@ -82,3 +82,18 @@ class UserService: await self.ap.persistence_mgr.execute_async( sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password) ) + + async def change_password(self, user_email: str, current_password: str, new_password: str) -> None: + ph = argon2.PasswordHasher() + + user_obj = await self.get_user_by_email(user_email) + if user_obj is None: + raise ValueError('User not found') + + ph.verify(user_obj.password, current_password) + + hashed_password = ph.hash(new_password) + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password) + ) diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 5f357d78..1efee3fc 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -19,7 +19,7 @@ class LifecycleControlScope(enum.Enum): APPLICATION = 'application' PLATFORM = 'platform' PLUGIN = 'plugin' - PROVIDER = 'provider' + PROVIDER = 'provider' class LauncherTypes(enum.Enum): diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index da32c7ac..9e26f239 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -20,9 +20,11 @@ from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities + # 语音功能相关异常定义 class VoiceConnectionError(Exception): """语音连接基础异常""" + def __init__(self, message: str, error_code: str = None, guild_id: int = None): super().__init__(message) self.error_code = error_code @@ -32,8 +34,9 @@ class VoiceConnectionError(Exception): class VoicePermissionError(VoiceConnectionError): """语音权限异常""" + def __init__(self, message: str, missing_permissions: list = None, user_id: int = None, channel_id: int = None): - super().__init__(message, "PERMISSION_ERROR") + super().__init__(message, 'PERMISSION_ERROR') self.missing_permissions = missing_permissions or [] self.user_id = user_id self.channel_id = channel_id @@ -41,40 +44,42 @@ class VoicePermissionError(VoiceConnectionError): class VoiceNetworkError(VoiceConnectionError): """语音网络异常""" + def __init__(self, message: str, retry_count: int = 0): - super().__init__(message, "NETWORK_ERROR") + super().__init__(message, 'NETWORK_ERROR') self.retry_count = retry_count self.last_attempt = datetime.datetime.now() class VoiceConnectionStatus(Enum): """语音连接状态枚举""" - IDLE = "idle" - CONNECTING = "connecting" - CONNECTED = "connected" - PLAYING = "playing" - RECONNECTING = "reconnecting" - FAILED = "failed" + + IDLE = 'idle' + CONNECTING = 'connecting' + CONNECTED = 'connected' + PLAYING = 'playing' + RECONNECTING = 'reconnecting' + FAILED = 'failed' class VoiceConnectionInfo: """ 语音连接信息类 - + 用于存储和管理单个语音连接的详细信息,包括连接状态、时间戳、 频道信息等。提供连接信息的标准化数据结构。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 """ - + def __init__(self, guild_id: int, channel_id: int, channel_name: str = None): """ 初始化语音连接信息 - + @author: @ydzat - + Args: guild_id (int): 服务器ID channel_id (int): 语音频道ID @@ -82,28 +87,28 @@ class VoiceConnectionInfo: """ self.guild_id = guild_id self.channel_id = channel_id - self.channel_name = channel_name or f"Channel-{channel_id}" + self.channel_name = channel_name or f'Channel-{channel_id}' self.connected = False self.connection_time: datetime.datetime = None self.last_activity = datetime.datetime.now() self.status = VoiceConnectionStatus.IDLE self.user_count = 0 self.latency = 0.0 - self.connection_health = "unknown" + self.connection_health = 'unknown' self.voice_client = None - + def update_status(self, status: VoiceConnectionStatus): """ 更新连接状态 - + @author: @ydzat - + Args: status (VoiceConnectionStatus): 新的连接状态 """ self.status = status self.last_activity = datetime.datetime.now() - + if status == VoiceConnectionStatus.CONNECTED: self.connected = True if self.connection_time is None: @@ -112,48 +117,48 @@ class VoiceConnectionInfo: self.connected = False self.connection_time = None self.voice_client = None - + def to_dict(self) -> dict: """ 转换为字典格式 - + @author: @ydzat - + Returns: dict: 连接信息的字典表示 """ return { - "guild_id": self.guild_id, - "channel_id": self.channel_id, - "channel_name": self.channel_name, - "connected": self.connected, - "connection_time": self.connection_time.isoformat() if self.connection_time else None, - "last_activity": self.last_activity.isoformat(), - "status": self.status.value, - "user_count": self.user_count, - "latency": self.latency, - "connection_health": self.connection_health + 'guild_id': self.guild_id, + 'channel_id': self.channel_id, + 'channel_name': self.channel_name, + 'connected': self.connected, + 'connection_time': self.connection_time.isoformat() if self.connection_time else None, + 'last_activity': self.last_activity.isoformat(), + 'status': self.status.value, + 'user_count': self.user_count, + 'latency': self.latency, + 'connection_health': self.connection_health, } class VoiceConnectionManager: """ 语音连接管理器 - + 负责管理多个服务器的语音连接,提供连接建立、断开、状态查询等功能。 采用单例模式确保全局只有一个连接管理器实例。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 """ - + def __init__(self, bot: discord.Client, logger: EventLogger): """ 初始化语音连接管理器 - + @author: @ydzat - + Args: bot (discord.Client): Discord 客户端实例 logger (EventLogger): 事件日志记录器 @@ -164,25 +169,24 @@ class VoiceConnectionManager: self._connection_lock = asyncio.Lock() self._cleanup_task = None self._monitoring_enabled = True - - async def join_voice_channel(self, guild_id: int, channel_id: int, - user_id: int = None) -> discord.VoiceClient: + + async def join_voice_channel(self, guild_id: int, channel_id: int, user_id: int = None) -> discord.VoiceClient: """ 加入语音频道 - + 验证用户权限和频道状态后,建立到指定语音频道的连接。 支持连接复用和自动重连机制。 - + @author: @ydzat - + Args: guild_id (int): 服务器ID channel_id (int): 语音频道ID user_id (int, optional): 请求用户ID,用于权限验证 - + Returns: discord.VoiceClient: 语音客户端实例 - + Raises: VoicePermissionError: 权限不足时抛出 VoiceNetworkError: 网络连接失败时抛出 @@ -193,370 +197,353 @@ class VoiceConnectionManager: # 获取服务器和频道对象 guild = self.bot.get_guild(guild_id) if not guild: - raise VoiceConnectionError( - f"无法找到服务器 {guild_id}", - "GUILD_NOT_FOUND", - guild_id - ) - + raise VoiceConnectionError(f'无法找到服务器 {guild_id}', 'GUILD_NOT_FOUND', guild_id) + channel = guild.get_channel(channel_id) if not channel or not isinstance(channel, discord.VoiceChannel): - raise VoiceConnectionError( - f"无法找到语音频道 {channel_id}", - "CHANNEL_NOT_FOUND", - guild_id - ) - + raise VoiceConnectionError(f'无法找到语音频道 {channel_id}', 'CHANNEL_NOT_FOUND', guild_id) + # 验证用户是否在语音频道中(如果提供了用户ID) if user_id: await self._validate_user_in_channel(guild, channel, user_id) - + # 验证机器人权限 await self._validate_bot_permissions(channel) - + # 检查是否已有连接 if guild_id in self.connections: existing_conn = self.connections[guild_id] if existing_conn.connected and existing_conn.voice_client: if existing_conn.channel_id == channel_id: # 已连接到相同频道,返回现有连接 - await self.logger.info(f"复用现有语音连接: {guild.name} -> {channel.name}") + await self.logger.info(f'复用现有语音连接: {guild.name} -> {channel.name}') return existing_conn.voice_client else: # 连接到不同频道,先断开旧连接 await self._disconnect_internal(guild_id) - + # 建立新连接 voice_client = await channel.connect() - + # 更新连接信息 conn_info = VoiceConnectionInfo(guild_id, channel_id, channel.name) conn_info.voice_client = voice_client conn_info.update_status(VoiceConnectionStatus.CONNECTED) conn_info.user_count = len(channel.members) self.connections[guild_id] = conn_info - - await self.logger.info(f"成功连接到语音频道: {guild.name} -> {channel.name}") + + await self.logger.info(f'成功连接到语音频道: {guild.name} -> {channel.name}') return voice_client - + except discord.ClientException as e: - raise VoiceNetworkError(f"Discord 客户端错误: {str(e)}") + raise VoiceNetworkError(f'Discord 客户端错误: {str(e)}') except discord.opus.OpusNotLoaded as e: - raise VoiceConnectionError(f"Opus 编码器未加载: {str(e)}", "OPUS_NOT_LOADED", guild_id) + raise VoiceConnectionError(f'Opus 编码器未加载: {str(e)}', 'OPUS_NOT_LOADED', guild_id) except Exception as e: - await self.logger.error(f"连接语音频道时发生未知错误: {str(e)}") - raise VoiceConnectionError(f"连接失败: {str(e)}", "UNKNOWN_ERROR", guild_id) - + await self.logger.error(f'连接语音频道时发生未知错误: {str(e)}') + raise VoiceConnectionError(f'连接失败: {str(e)}', 'UNKNOWN_ERROR', guild_id) + async def leave_voice_channel(self, guild_id: int) -> bool: """ 离开语音频道 - + 断开指定服务器的语音连接,清理相关资源和状态信息。 确保音频播放停止后再断开连接。 - + @author: @ydzat - + Args: guild_id (int): 服务器ID - + Returns: bool: 断开是否成功 """ async with self._connection_lock: return await self._disconnect_internal(guild_id) - + async def _disconnect_internal(self, guild_id: int) -> bool: """ 内部断开连接方法 - + @author: @ydzat - + Args: guild_id (int): 服务器ID - + Returns: bool: 断开是否成功 """ if guild_id not in self.connections: return True - + conn_info = self.connections[guild_id] - + try: if conn_info.voice_client and conn_info.voice_client.is_connected(): # 停止当前播放 if conn_info.voice_client.is_playing(): conn_info.voice_client.stop() - + # 等待播放完全停止 await asyncio.sleep(0.1) - + # 断开连接 await conn_info.voice_client.disconnect() - + conn_info.update_status(VoiceConnectionStatus.IDLE) del self.connections[guild_id] - - await self.logger.info(f"已断开语音连接: Guild {guild_id}") + + await self.logger.info(f'已断开语音连接: Guild {guild_id}') return True - + except Exception as e: - await self.logger.error(f"断开语音连接时发生错误: {str(e)}") + await self.logger.error(f'断开语音连接时发生错误: {str(e)}') # 即使出错也要清理连接记录 conn_info.update_status(VoiceConnectionStatus.FAILED) if guild_id in self.connections: del self.connections[guild_id] return False - + async def get_voice_client(self, guild_id: int) -> typing.Optional[discord.VoiceClient]: """ 获取语音客户端 - + 返回指定服务器的语音客户端实例,如果未连接则返回 None。 会验证连接的有效性,自动清理无效连接。 - + @author: @ydzat - + Args: guild_id (int): 服务器ID - + Returns: Optional[discord.VoiceClient]: 语音客户端实例或 None """ if guild_id not in self.connections: return None - + conn_info = self.connections[guild_id] - + # 验证连接是否仍然有效 if conn_info.voice_client and not conn_info.voice_client.is_connected(): # 连接已失效,清理状态 await self._disconnect_internal(guild_id) return None - + return conn_info.voice_client if conn_info.connected else None - + async def is_connected_to_voice(self, guild_id: int) -> bool: """ 检查是否连接到语音频道 - + @author: @ydzat - + Args: guild_id (int): 服务器ID - + Returns: bool: 是否已连接 """ if guild_id not in self.connections: return False - + conn_info = self.connections[guild_id] - + # 检查实际连接状态 if conn_info.voice_client and not conn_info.voice_client.is_connected(): # 连接已失效,清理状态 await self._disconnect_internal(guild_id) return False - + return conn_info.connected - + async def get_connection_status(self, guild_id: int) -> typing.Optional[dict]: """ 获取连接状态信息 - + @author: @ydzat - + Args: guild_id (int): 服务器ID - + Returns: Optional[dict]: 连接状态信息字典或 None """ if guild_id not in self.connections: return None - + conn_info = self.connections[guild_id] - + # 更新实时信息 if conn_info.voice_client and conn_info.voice_client.is_connected(): conn_info.latency = conn_info.voice_client.latency * 1000 # 转换为毫秒 - conn_info.connection_health = "good" if conn_info.latency < 100 else "poor" - + conn_info.connection_health = 'good' if conn_info.latency < 100 else 'poor' + # 更新频道用户数 guild = self.bot.get_guild(guild_id) if guild: channel = guild.get_channel(conn_info.channel_id) if channel and isinstance(channel, discord.VoiceChannel): conn_info.user_count = len(channel.members) - + return conn_info.to_dict() - + async def list_active_connections(self) -> typing.List[dict]: """ 列出所有活跃连接 - + @author: @ydzat - + Returns: List[dict]: 活跃连接列表 """ active_connections = [] - + for guild_id, conn_info in self.connections.items(): if conn_info.connected: status = await self.get_connection_status(guild_id) if status: active_connections.append(status) - + return active_connections - + async def get_voice_channel_info(self, guild_id: int, channel_id: int) -> typing.Optional[dict]: """ 获取语音频道信息 - + @author: @ydzat - + Args: guild_id (int): 服务器ID channel_id (int): 频道ID - + Returns: Optional[dict]: 频道信息字典或 None """ guild = self.bot.get_guild(guild_id) if not guild: return None - + channel = guild.get_channel(channel_id) if not channel or not isinstance(channel, discord.VoiceChannel): return None - + # 获取用户信息 users = [] for member in channel.members: - users.append({ - "id": member.id, - "name": member.display_name, - "status": str(member.status), - "is_bot": member.bot - }) - + users.append( + {'id': member.id, 'name': member.display_name, 'status': str(member.status), 'is_bot': member.bot} + ) + # 获取权限信息 bot_member = guild.me permissions = channel.permissions_for(bot_member) - + return { - "channel_id": channel_id, - "channel_name": channel.name, - "guild_id": guild_id, - "guild_name": guild.name, - "user_limit": channel.user_limit, - "current_users": users, - "user_count": len(users), - "bitrate": channel.bitrate, - "permissions": { - "connect": permissions.connect, - "speak": permissions.speak, - "use_voice_activation": permissions.use_voice_activation, - "priority_speaker": permissions.priority_speaker - } + 'channel_id': channel_id, + 'channel_name': channel.name, + 'guild_id': guild_id, + 'guild_name': guild.name, + 'user_limit': channel.user_limit, + 'current_users': users, + 'user_count': len(users), + 'bitrate': channel.bitrate, + 'permissions': { + 'connect': permissions.connect, + 'speak': permissions.speak, + 'use_voice_activation': permissions.use_voice_activation, + 'priority_speaker': permissions.priority_speaker, + }, } - - async def _validate_user_in_channel(self, guild: discord.Guild, - channel: discord.VoiceChannel, user_id: int): + + async def _validate_user_in_channel(self, guild: discord.Guild, channel: discord.VoiceChannel, user_id: int): """ 验证用户是否在语音频道中 - + @author: @ydzat - + Args: guild: Discord 服务器对象 channel: 语音频道对象 user_id: 用户ID - + Raises: VoicePermissionError: 用户不在频道中时抛出 """ member = guild.get_member(user_id) if not member: - raise VoicePermissionError( - f"无法找到用户 {user_id}", - ["member_not_found"], - user_id, - channel.id - ) - + raise VoicePermissionError(f'无法找到用户 {user_id}', ['member_not_found'], user_id, channel.id) + if not member.voice or member.voice.channel != channel: raise VoicePermissionError( - f"用户 {member.display_name} 不在语音频道 {channel.name} 中", - ["user_not_in_channel"], + f'用户 {member.display_name} 不在语音频道 {channel.name} 中', + ['user_not_in_channel'], user_id, - channel.id + channel.id, ) - + async def _validate_bot_permissions(self, channel: discord.VoiceChannel): """ 验证机器人权限 - + @author: @ydzat - + Args: channel: 语音频道对象 - + Raises: VoicePermissionError: 权限不足时抛出 """ bot_member = channel.guild.me permissions = channel.permissions_for(bot_member) - + missing_permissions = [] - + if not permissions.connect: - missing_permissions.append("connect") + missing_permissions.append('connect') if not permissions.speak: - missing_permissions.append("speak") - + missing_permissions.append('speak') + if missing_permissions: raise VoicePermissionError( - f"机器人在频道 {channel.name} 中缺少权限: {', '.join(missing_permissions)}", + f'机器人在频道 {channel.name} 中缺少权限: {", ".join(missing_permissions)}', missing_permissions, - channel_id=channel.id + channel_id=channel.id, ) - + async def cleanup_inactive_connections(self): """ 清理无效连接 - + 定期检查并清理已断开或无效的语音连接,释放资源。 - + @author: @ydzat """ cleanup_guilds = [] - + for guild_id, conn_info in self.connections.items(): if not conn_info.voice_client or not conn_info.voice_client.is_connected(): cleanup_guilds.append(guild_id) - + for guild_id in cleanup_guilds: await self._disconnect_internal(guild_id) - + if cleanup_guilds: - await self.logger.info(f"清理了 {len(cleanup_guilds)} 个无效的语音连接") - + await self.logger.info(f'清理了 {len(cleanup_guilds)} 个无效的语音连接') + async def start_monitoring(self): """ 开始连接监控 - + @author: @ydzat """ if self._cleanup_task is None and self._monitoring_enabled: self._cleanup_task = asyncio.create_task(self._monitoring_loop()) - + async def stop_monitoring(self): """ 停止连接监控 - + @author: @ydzat """ self._monitoring_enabled = False @@ -567,11 +554,11 @@ class VoiceConnectionManager: except asyncio.CancelledError: pass self._cleanup_task = None - + async def _monitoring_loop(self): """ 监控循环 - + @author: @ydzat """ try: @@ -580,18 +567,18 @@ class VoiceConnectionManager: await self.cleanup_inactive_connections() except asyncio.CancelledError: pass - + async def disconnect_all(self): """ 断开所有连接 - + @author: @ydzat """ async with self._connection_lock: guild_ids = list(self.connections.keys()) for guild_id in guild_ids: await self._disconnect_internal(guild_id) - + await self.stop_monitoring() @@ -814,7 +801,7 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): self.logger = logger self.bot_account_id = self.config['client_id'] - + # 初始化语音连接管理器 self.voice_manager: VoiceConnectionManager = None @@ -837,163 +824,162 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): args['proxy'] = os.getenv('http_proxy') self.bot = MyClient(intents=intents, **args) - + # Voice functionality methods - async def join_voice_channel(self, guild_id: int, channel_id: int, - user_id: int = None) -> discord.VoiceClient: + async def join_voice_channel(self, guild_id: int, channel_id: int, user_id: int = None) -> discord.VoiceClient: """ 加入语音频道 - + 为指定服务器的语音频道建立连接,支持用户权限验证和连接复用。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Args: guild_id (int): Discord 服务器ID channel_id (int): 语音频道ID user_id (int, optional): 请求用户ID,用于权限验证 - + Returns: discord.VoiceClient: 语音客户端实例 - + Raises: VoicePermissionError: 权限不足 VoiceNetworkError: 网络连接失败 VoiceConnectionError: 其他连接错误 """ if not self.voice_manager: - raise VoiceConnectionError("语音管理器未初始化", "MANAGER_NOT_READY") - + raise VoiceConnectionError('语音管理器未初始化', 'MANAGER_NOT_READY') + return await self.voice_manager.join_voice_channel(guild_id, channel_id, user_id) - + async def leave_voice_channel(self, guild_id: int) -> bool: """ 离开语音频道 - + 断开指定服务器的语音连接,清理相关资源。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Args: guild_id (int): Discord 服务器ID - + Returns: bool: 是否成功断开连接 """ if not self.voice_manager: return False - + return await self.voice_manager.leave_voice_channel(guild_id) - + async def get_voice_client(self, guild_id: int) -> typing.Optional[discord.VoiceClient]: """ 获取语音客户端 - + 返回指定服务器的语音客户端实例,用于音频播放控制。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Args: guild_id (int): Discord 服务器ID - + Returns: Optional[discord.VoiceClient]: 语音客户端实例或 None """ if not self.voice_manager: return None - + return await self.voice_manager.get_voice_client(guild_id) - + async def is_connected_to_voice(self, guild_id: int) -> bool: """ 检查语音连接状态 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Args: guild_id (int): Discord 服务器ID - + Returns: bool: 是否已连接到语音频道 """ if not self.voice_manager: return False - + return await self.voice_manager.is_connected_to_voice(guild_id) - + async def get_voice_connection_status(self, guild_id: int) -> typing.Optional[dict]: """ 获取语音连接详细状态 - + 返回包含连接时间、延迟、用户数等详细信息的状态字典。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Args: guild_id (int): Discord 服务器ID - + Returns: Optional[dict]: 连接状态信息或 None """ if not self.voice_manager: return None - + return await self.voice_manager.get_connection_status(guild_id) - + async def list_active_voice_connections(self) -> typing.List[dict]: """ 列出所有活跃的语音连接 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Returns: List[dict]: 活跃语音连接列表 """ if not self.voice_manager: return [] - + return await self.voice_manager.list_active_connections() - + async def get_voice_channel_info(self, guild_id: int, channel_id: int) -> typing.Optional[dict]: """ 获取语音频道详细信息 - + 包括频道名称、用户列表、权限信息等。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 - + Args: guild_id (int): Discord 服务器ID channel_id (int): 语音频道ID - + Returns: Optional[dict]: 频道信息字典或 None """ if not self.voice_manager: return None - + return await self.voice_manager.get_voice_channel_info(guild_id, channel_id) - + async def cleanup_voice_connections(self): """ 清理无效的语音连接 - + 手动触发语音连接清理,移除已断开或无效的连接。 - + @author: @ydzat @version: 1.0 @since: 2025-07-04 @@ -1068,30 +1054,29 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): async def run_async(self): """ 启动 Discord 适配器 - + 初始化语音管理器并启动 Discord 客户端连接。 - + @author: @ydzat (修改) """ async with self.bot: # 初始化语音管理器 self.voice_manager = VoiceConnectionManager(self.bot, self.logger) await self.voice_manager.start_monitoring() - - await self.logger.info("Discord 适配器语音功能已启用") + + await self.logger.info('Discord 适配器语音功能已启用') await self.bot.start(self.config['token'], reconnect=True) async def kill(self) -> bool: """ 关闭 Discord 适配器 - + 清理语音连接并关闭 Discord 客户端。 - + @author: @ydzat (修改) """ if self.voice_manager: await self.voice_manager.disconnect_all() - + await self.bot.close() return True - diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index 895e77fb..819ae400 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -29,10 +29,9 @@ import logging class WeChatPadMessageConverter(adapter.MessageConverter): - def __init__(self, config: dict, logger: logging.Logger): self.config = config - self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"]) + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) self.logger = logger @staticmethod @@ -41,9 +40,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter): for component in message_chain: if isinstance(component, platform_message.AtAll): - content_list.append({"type": "at", "target": "all"}) + content_list.append({'type': 'at', 'target': 'all'}) elif isinstance(component, platform_message.At): - content_list.append({"type": "at", "target": component.target}) + content_list.append({'type': 'at', 'target': component.target}) elif isinstance(component, platform_message.Plain): content_list.append({'type': 'text', 'content': component.text}) elif isinstance(component, platform_message.Image): @@ -77,9 +76,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return content_list async def target2yiri( - self, - message: dict, - bot_account_id: str, + self, + message: dict, + bot_account_id: str, ) -> platform_message.MessageChain: """外部消息转平台消息""" # 数据预处理 @@ -92,18 +91,18 @@ class WeChatPadMessageConverter(adapter.MessageConverter): if is_group_message: ats_bot = self._ats_bot(message, bot_account_id) - self.logger.info(f"ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}") - if "@所有人" in content: + self.logger.info(f'ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}') + if '@所有人' in content: message_list.append(platform_message.AtAll()) if ats_bot: message_list.append(platform_message.At(target=bot_account_id)) - + # 解析@信息并生成At组件 at_targets = self._extract_at_targets(message) for target_id in at_targets: if target_id != bot_wxid: # 避免重复添加机器人的At message_list.append(platform_message.At(target=target_id)) - + content_no_preifx, _ = self._extract_content_and_sender(content) msg_type = message['msg_type'] @@ -418,14 +417,14 @@ class WeChatPadMessageConverter(adapter.MessageConverter): msg_source = message.get('msg_source', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) - at_user_list = msg_source_data.findtext("atuserlist") or "" + at_user_list = msg_source_data.findtext('atuserlist') or '' if at_user_list: # atuserlist格式通常是逗号分隔的用户ID列表 at_targets = [user_id.strip() for user_id in at_user_list.split(',') if user_id.strip()] except Exception as e: - self.logger.error(f"_extract_at_targets got except: {e}") + self.logger.error(f'_extract_at_targets got except: {e}') return at_targets - + # 提取一下content前面的sender_id, 和去掉前缀的内容 def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: try: @@ -449,22 +448,20 @@ class WeChatPadMessageConverter(adapter.MessageConverter): class WeChatPadEventConverter(adapter.EventConverter): - def __init__(self, config: dict, logger: logging.Logger): self.config = config self.message_converter = WeChatPadMessageConverter(config, logger) self.logger = logger - + @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> dict: pass async def target2yiri( - self, - event: dict, - bot_account_id: str, + self, + event: dict, + bot_account_id: str, ) -> platform_events.MessageEvent: - # 排除公众号以及微信团队消息 if ( event['from_user_name']['str'].startswith('gh_') @@ -576,26 +573,22 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): for msg in content_list: # 文本消息处理@ if msg['type'] == 'text' and at_targets: - if "all" in at_targets: + if 'all' in at_targets: msg['content'] = f'@所有人 {msg["content"]}' else: at_nick_name_list = [] for member in member_info: - if member["user_name"] in at_targets: + if member['user_name'] in at_targets: at_nick_name_list.append(f'@{member["nick_name"]}') msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}' # 统一消息派发 handler_map = { 'text': lambda msg: self.bot.send_text_message( - to_wxid=target_id, - message=msg['content'], - ats= ["notify@all"] if "all" in at_targets else at_targets + to_wxid=target_id, message=msg['content'], ats=['notify@all'] if 'all' in at_targets else at_targets ), 'image': lambda msg: self.bot.send_image_message( - to_wxid=target_id, - img_url=msg["image"], - ats = ["notify@all"] if "all" in at_targets else at_targets + to_wxid=target_id, img_url=msg['image'], ats=['notify@all'] if 'all' in at_targets else at_targets ), 'WeChatEmoji': lambda msg: self.bot.send_emoji_message( to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'] diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml index 6f320e66..9a22c5d9 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml @@ -4,7 +4,7 @@ metadata: name: deepseek-chat-completions label: en_US: DeepSeek - zh_Hans: 深度求索 + zh_Hans: DeepSeek icon: deepseek.svg spec: config: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index b61b02e4..124d405f 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -65,7 +65,7 @@ class LocalAgentRunner(runner.RequestRunner): self.ap.logger.warning(f'Knowledge base {kb_uuid} not found') raise ValueError(f'Knowledge base {kb_uuid} not found') - result = await kb.retrieve(user_message_text) + result = await kb.retrieve(user_message_text, kb.knowledge_base_entity.top_k) final_user_message_text = '' diff --git a/pkg/rag/knowledge/kbmgr.py b/pkg/rag/knowledge/kbmgr.py index a9e7e57a..1cdef361 100644 --- a/pkg/rag/knowledge/kbmgr.py +++ b/pkg/rag/knowledge/kbmgr.py @@ -123,11 +123,11 @@ class RuntimeKnowledgeBase: ) return wrapper.id - async def retrieve(self, query: str) -> list[retriever_entities.RetrieveResultEntry]: + async def retrieve(self, query: str, top_k: int) -> list[retriever_entities.RetrieveResultEntry]: embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid( self.knowledge_base_entity.embedding_model_uuid ) - return await self.retriever.retrieve(self.knowledge_base_entity.uuid, query, embedding_model) + return await self.retriever.retrieve(self.knowledge_base_entity.uuid, query, embedding_model, top_k) async def delete_file(self, file_id: str): # delete vector diff --git a/web/package-lock.json b/web/package-lock.json index cb2b05d8..01a5a8b7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,8 +10,10 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.13", @@ -1090,6 +1092,57 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz", + "integrity": "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", diff --git a/web/package.json b/web/package.json index cd869438..4749f463 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.13", diff --git a/web/src/app/global.css b/web/src/app/global.css index 079437e8..310d509a 100644 --- a/web/src/app/global.css +++ b/web/src/app/global.css @@ -56,6 +56,15 @@ background: rgba(0, 0, 0, 0.35); /* 悬停加深 */ } +/* 暗黑模式下的滚动条 */ +.dark ::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); /* 半透明白色 */ +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.35); /* 悬停加深 */ +} + /* 兼容 Edge */ @supports (-ms-ime-align: auto) { body { @@ -108,36 +117,36 @@ } .dark { - --background: oklch(0.141 0.005 285.823); + --background: oklch(0.08 0.002 285.823); --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); + --card: oklch(0.12 0.004 285.885); --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); + --popover: oklch(0.12 0.004 285.885); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.92 0.004 286.32); - --primary-foreground: oklch(0.21 0.006 285.885); - --secondary: oklch(0.274 0.006 286.033); + --primary: oklch(0.62 0.2 255); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.18 0.004 286.033); --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); + --muted: oklch(0.18 0.004 286.033); --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); + --accent: oklch(0.18 0.004 286.033); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); + --border: oklch(1 0 0 / 8%); + --input: oklch(1 0 0 / 10%); --ring: oklch(0.552 0.016 285.938); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.006 285.885); + --sidebar: oklch(0.1 0.003 285.885); --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-primary: oklch(0.62 0.2 255); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.18 0.004 286.033); --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-border: oklch(1 0 0 / 8%); --sidebar-ring: oklch(0.552 0.016 285.938); } diff --git a/web/src/app/home/bots/BotDetailDialog.tsx b/web/src/app/home/bots/BotDetailDialog.tsx index db19e1d4..fbb8c359 100644 --- a/web/src/app/home/bots/BotDetailDialog.tsx +++ b/web/src/app/home/bots/BotDetailDialog.tsx @@ -159,7 +159,7 @@ export default function BotDetailDialog({ diff --git a/web/src/app/home/bots/components/bot-card/botCard.module.css b/web/src/app/home/bots/components/bot-card/botCard.module.css index 396b5831..d9b82b9b 100644 --- a/web/src/app/home/bots/components/bot-card/botCard.module.css +++ b/web/src/app/home/bots/components/bot-card/botCard.module.css @@ -6,12 +6,22 @@ box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2); padding: 1.2rem; cursor: pointer; + transition: all 0.2s ease; +} + +:global(.dark) .cardContainer { + background-color: #1f1f22; + box-shadow: 0; } .cardContainer:hover { box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); } +:global(.dark) .cardContainer:hover { + box-shadow: 0; +} + .iconBasicInfoContainer { width: 100%; height: 100%; @@ -47,6 +57,11 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: #1a1a1a; +} + +:global(.dark) .basicInfoName { + color: #f0f0f0; } .basicInfoDescription { @@ -58,6 +73,10 @@ text-overflow: ellipsis; } +:global(.dark) .basicInfoDescription { + color: #888888; +} + .basicInfoAdapterContainer { display: flex; flex-direction: row; @@ -71,12 +90,20 @@ color: #626262; } +:global(.dark) .basicInfoAdapterIcon { + color: #a0a0a0; +} + .basicInfoAdapterLabel { font-size: 1.2rem; font-weight: 500; color: #626262; } +:global(.dark) .basicInfoAdapterLabel { + color: #a0a0a0; +} + .basicInfoPipelineContainer { display: flex; flex-direction: row; @@ -90,12 +117,20 @@ margin-top: 0.2rem; } +:global(.dark) .basicInfoPipelineIcon { + color: #a0a0a0; +} + .basicInfoPipelineLabel { font-size: 1.2rem; font-weight: 500; color: #626262; } +:global(.dark) .basicInfoPipelineLabel { + color: #a0a0a0; +} + .bigText { white-space: nowrap; overflow: hidden; diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index bd757ae0..92811a65 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -394,7 +394,7 @@ export default function BotForm({ {t('bots.bindPipeline')} - + @@ -150,7 +151,7 @@ export default function DynamicFormItemComponent({ case DynamicFormItemType.LLM_MODEL_SELECTOR: return ( - + @@ -291,7 +292,7 @@ export default function DynamicFormItemComponent({
{/* 角色选择 */} {index === 0 ? ( -
+
system
) : ( @@ -303,7 +304,7 @@ export default function DynamicFormItemComponent({ field.onChange(newValue); }} > - + @@ -315,7 +316,7 @@ export default function DynamicFormItemComponent({ )} {/* 内容输入 */} - { diff --git a/web/src/app/home/components/empty-and-create-component/EmptyAndCreateComponent.tsx b/web/src/app/home/components/empty-and-create-component/EmptyAndCreateComponent.tsx deleted file mode 100644 index b55b4cee..00000000 --- a/web/src/app/home/components/empty-and-create-component/EmptyAndCreateComponent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import styles from './emptyAndCreate.module.css'; - -export default function EmptyAndCreateComponent({ - title, - subTitle, - buttonText, - onButtonClick, -}: { - title: string; - subTitle: string; - buttonText: string; - onButtonClick: () => void; -}) { - return ( -
-
-
-
{title}
-
{subTitle}
-
-
- {buttonText} -
-
-
- ); -} diff --git a/web/src/app/home/components/empty-and-create-component/emptyAndCreate.module.css b/web/src/app/home/components/empty-and-create-component/emptyAndCreate.module.css deleted file mode 100644 index 3504d7a3..00000000 --- a/web/src/app/home/components/empty-and-create-component/emptyAndCreate.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.emptyPageContainer { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: #fff; - border: 1px solid #c5c5c5; - border-radius: 10px; -} - -.emptyContainer { - width: 100%; - height: 50%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-evenly; -} - -.emptyCreateButton { - width: 200px; - height: 50px; - border-radius: 20px; - background-color: #2288ee; - color: #fff; - font-size: 20px; - font-weight: bold; - text-align: center; - line-height: 50px; - user-select: none; -} - -.emptyCreateButton:hover { - background-color: #1b77d2; -} - -.emptyInfoContainer { - width: 100%; - height: 60px; - display: flex; - flex-direction: column; - align-items: center; - color: #353535; -} - -.emptyInfoText { - font-size: 30px; -} - -.emptyInfoSubText { - font-size: 28px; -} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.module.css b/web/src/app/home/components/home-sidebar/HomeSidebar.module.css index d525d495..4aeb96e3 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.module.css +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.module.css @@ -13,6 +13,10 @@ /* box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); */ } +:global(.dark) .sidebarContainer { + background-color: #0a0a0b !important; +} + .langbotIconContainer { width: 200px; height: 70px; @@ -21,32 +25,49 @@ align-items: center; justify-content: center; gap: 0.8rem; +} - .langbotIcon { - width: 2.8rem; - height: 2.8rem; - box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); - border-radius: 8px; - } +.langbotIcon { + width: 2.8rem; + height: 2.8rem; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); + border-radius: 8px; +} - .langbotTextContainer { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - gap: 0.1rem; - } +:global(.dark) .langbotIcon { + box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.1); +} - .langbotText { - font-size: 1.4rem; - font-weight: 500; - } +.langbotTextContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: 0.1rem; +} - .langbotVersion { - font-size: 0.8rem; - font-weight: 700; - color: #6c6c6c; - } +.langbotText { + font-size: 1.4rem; + font-weight: 500; + color: #1a1a1a; +} + +:global(.dark) .langbotText { + font-size: 1.4rem; + font-weight: 500; + color: #f0f0f0 !important; +} + +.langbotVersion { + font-size: 0.8rem; + font-weight: 700; + color: #6c6c6c; +} + +:global(.dark) .langbotVersion { + font-size: 0.8rem; + font-weight: 700; + color: #a0a0a0 !important; } .sidebarTopContainer { @@ -76,6 +97,7 @@ justify-content: flex-start; cursor: pointer; gap: 0.5rem; + transition: all 0.2s ease; /* background-color: aqua; */ } @@ -85,16 +107,40 @@ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); } +:global(.dark) .sidebarSelected { + background-color: #2288ee; + color: white; + box-shadow: 0 0 10px 0 rgba(34, 136, 238, 0.3); +} + .sidebarUnselected { color: #6c6c6c; } +:global(.dark) .sidebarUnselected { + color: #a0a0a0 !important; +} + +.sidebarUnselected:hover { + background-color: rgba(34, 136, 238, 0.1); + color: #2288ee; +} + +:global(.dark) .sidebarUnselected:hover { + background-color: rgba(34, 136, 238, 0.2); + color: #66baff; +} + .sidebarChildIcon { width: 20px; height: 20px; background-color: rgba(96, 149, 209, 0); } +.sidebarChildName { + color: inherit; +} + .sidebarBottomContainer { width: 100%; display: flex; diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index fa435cf7..25b3b58f 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -11,6 +11,18 @@ import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConf import langbotIcon from '@/app/assets/langbot-logo.webp'; import { systemInfo } from '@/app/infra/http/HttpClient'; import { useTranslation } from 'react-i18next'; +import { Moon, Sun, Monitor } from 'lucide-react'; +import { useTheme } from 'next-themes'; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { LanguageSelector } from '@/components/ui/language-selector'; +import PasswordChangeDialog from '@/app/home/components/password-change-dialog/PasswordChangeDialog'; // TODO 侧边导航栏要加动画 export default function HomeSidebar({ @@ -27,8 +39,11 @@ export default function HomeSidebar({ }, [pathname]); const [selectedChild, setSelectedChild] = useState(); - + const { theme, setTheme } = useTheme(); const { t } = useTranslation(); + const [popoverOpen, setPopoverOpen] = useState(false); + const [passwordChangeOpen, setPasswordChangeOpen] = useState(false); + const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false); useEffect(() => { initSelect(); @@ -168,23 +183,113 @@ export default function HomeSidebar({ } name={t('common.helpDocs')} /> - { - handleLogout(); + + { + // 防止语言选择器打开时关闭popover + if (!open && languageSelectorOpen) return; + setPopoverOpen(open); }} - isSelected={false} - icon={ - - - - } - name={t('common.logout')} - /> + > + + {}} + isSelected={false} + icon={ + + + + } + name={t('common.accountOptions')} + /> + + +
+ {t('common.theme')} + { + if (value) setTheme(value); + }} + className="justify-start" + > + + + + + + + + + + +
+ +
+ + {t('common.language')} + + +
+ +
+ {t('common.account')} + + +
+
+
+
); } diff --git a/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css b/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css index bc740231..73bb2242 100644 --- a/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css +++ b/web/src/app/home/components/home-titlebar/HomeTittleBar.module.css @@ -17,6 +17,10 @@ color: #585858; } +:global(.dark) .titleText { + color: #e0e0e0; +} + .subtitleText { margin-left: 3.2rem; font-size: 0.8rem; @@ -25,8 +29,16 @@ align-items: center; } +:global(.dark) .subtitleText { + color: #b0b0b0; +} + .helpLink { margin-left: 0.2rem; font-size: 0.8rem; color: #8b8b8b; } + +:global(.dark) .helpLink { + color: #a0a0a0; +} diff --git a/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx b/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx new file mode 100644 index 00000000..03a302af --- /dev/null +++ b/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx @@ -0,0 +1,163 @@ +'use client'; + +import * as React from 'react'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { toast } from 'sonner'; +import { useTranslation } from 'react-i18next'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { httpClient } from '@/app/infra/http/HttpClient'; + +const getFormSchema = (t: (key: string) => string) => + z + .object({ + currentPassword: z + .string() + .min(1, { message: t('common.currentPasswordRequired') }), + newPassword: z + .string() + .min(1, { message: t('common.newPasswordRequired') }), + confirmNewPassword: z + .string() + .min(1, { message: t('common.confirmPasswordRequired') }), + }) + .refine((data) => data.newPassword === data.confirmNewPassword, { + message: t('common.passwordsDoNotMatch'), + path: ['confirmNewPassword'], + }); + +interface PasswordChangeDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function PasswordChangeDialog({ + open, + onOpenChange, +}: PasswordChangeDialogProps) { + const { t } = useTranslation(); + const [isSubmitting, setIsSubmitting] = useState(false); + const formSchema = getFormSchema(t); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + currentPassword: '', + newPassword: '', + confirmNewPassword: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsSubmitting(true); + try { + await httpClient.changePassword( + values.currentPassword, + values.newPassword, + ); + toast.success(t('common.changePasswordSuccess')); + form.reset(); + onOpenChange(false); + } catch { + toast.error(t('common.changePasswordFailed')); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {t('common.changePassword')} + +
+ + ( + + {t('common.currentPassword')} + + + + + + )} + /> + ( + + {t('common.newPassword')} + + + + + + )} + /> + ( + + {t('common.confirmNewPassword')} + + + + + + )} + /> + + + + + + +
+
+ ); +} diff --git a/web/src/app/home/knowledge/KBDetailDialog.tsx b/web/src/app/home/knowledge/KBDetailDialog.tsx index 7ad8d4a4..33a21267 100644 --- a/web/src/app/home/knowledge/KBDetailDialog.tsx +++ b/web/src/app/home/knowledge/KBDetailDialog.tsx @@ -152,7 +152,7 @@ export default function KBDetailDialog({ diff --git a/web/src/app/home/knowledge/components/kb-card/KBCard.module.css b/web/src/app/home/knowledge/components/kb-card/KBCard.module.css index 2ecbd44a..d2bfebca 100644 --- a/web/src/app/home/knowledge/components/kb-card/KBCard.module.css +++ b/web/src/app/home/knowledge/components/kb-card/KBCard.module.css @@ -10,12 +10,22 @@ flex-direction: row; justify-content: space-between; gap: 0.5rem; + transition: all 0.2s ease; +} + +:global(.dark) .cardContainer { + background-color: #1f1f22; + box-shadow: 0; } .cardContainer:hover { box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); } +:global(.dark) .cardContainer:hover { + box-shadow: 0; +} + .basicInfoContainer { width: 100%; height: 100%; @@ -35,6 +45,11 @@ .basicInfoNameText { font-size: 1.4rem; font-weight: 500; + color: #1a1a1a; +} + +:global(.dark) .basicInfoNameText { + color: #f0f0f0; } .basicInfoDescriptionText { @@ -48,6 +63,10 @@ color: #b1b1b1; } +:global(.dark) .basicInfoDescriptionText { + color: #888888; +} + .basicInfoLastUpdatedTimeContainer { display: flex; flex-direction: row; @@ -58,11 +77,21 @@ .basicInfoUpdateTimeIcon { width: 1.2rem; height: 1.2rem; + color: #626262; +} + +:global(.dark) .basicInfoUpdateTimeIcon { + color: #a0a0a0; } .basicInfoUpdateTimeText { font-size: 1rem; font-weight: 400; + color: #626262; +} + +:global(.dark) .basicInfoUpdateTimeText { + color: #a0a0a0; } .operationContainer { @@ -86,12 +115,20 @@ color: #ffcd27; } +:global(.dark) .operationDefaultBadgeIcon { + color: #fbbf24; +} + .operationDefaultBadgeText { font-size: 1rem; font-weight: 400; color: #ffcd27; } +:global(.dark) .operationDefaultBadgeText { + color: #fbbf24; +} + .bigText { white-space: nowrap; overflow: hidden; diff --git a/web/src/app/home/knowledge/components/kb-card/KBCardVO.ts b/web/src/app/home/knowledge/components/kb-card/KBCardVO.ts index bfbc2adb..b13d2c24 100644 --- a/web/src/app/home/knowledge/components/kb-card/KBCardVO.ts +++ b/web/src/app/home/knowledge/components/kb-card/KBCardVO.ts @@ -3,6 +3,7 @@ export interface IKnowledgeBaseVO { name: string; description: string; embeddingModelUUID: string; + top_k: number; lastUpdatedTimeAgo: string; } @@ -11,6 +12,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO { name: string; description: string; embeddingModelUUID: string; + top_k: number; lastUpdatedTimeAgo: string; constructor(props: IKnowledgeBaseVO) { @@ -18,6 +20,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO { this.name = props.name; this.description = props.description; this.embeddingModelUUID = props.embeddingModelUUID; + this.top_k = props.top_k; this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo; } } diff --git a/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx b/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx index 3b4123ec..512ea198 100644 --- a/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx +++ b/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx @@ -127,12 +127,12 @@ export default function FileUploadZone({
-

+

{isUploading ? t('knowledge.documentsTab.uploading') : t('knowledge.documentsTab.dragAndDrop')}

-

+

{t('knowledge.documentsTab.supportedFormats')}

diff --git a/web/src/app/home/knowledge/components/kb-docs/documents/columns.tsx b/web/src/app/home/knowledge/components/kb-docs/documents/columns.tsx index 6142cfc4..2880c1ba 100644 --- a/web/src/app/home/knowledge/components/kb-docs/documents/columns.tsx +++ b/web/src/app/home/knowledge/components/kb-docs/documents/columns.tsx @@ -77,7 +77,10 @@ export const columns = ( - + {t('knowledge.documentsTab.actions')} diff --git a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx index 54d5d6e4..0804ee12 100644 --- a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx +++ b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx @@ -35,6 +35,10 @@ const getFormSchema = (t: (key: string) => string) => embeddingModelUUID: z .string() .min(1, { message: t('knowledge.embeddingModelUUIDRequired') }), + top_k: z + .number() + .min(1, { message: t('knowledge.topKRequired') }) + .max(30, { message: t('knowledge.topKMax') }), }); export default function KBForm({ @@ -55,6 +59,7 @@ export default function KBForm({ name: '', description: t('knowledge.defaultDescription'), embeddingModelUUID: '', + top_k: 5, }, }); @@ -69,6 +74,7 @@ export default function KBForm({ form.setValue('name', val.name); form.setValue('description', val.description); form.setValue('embeddingModelUUID', val.embeddingModelUUID); + form.setValue('top_k', val.top_k || 5); }); } }); @@ -83,6 +89,7 @@ export default function KBForm({ name: res.base.name, description: res.base.description, embeddingModelUUID: res.base.embedding_model_uuid, + top_k: res.base.top_k || 5, }); }); }); @@ -109,6 +116,7 @@ export default function KBForm({ name: data.name, description: data.description, embedding_model_uuid: data.embeddingModelUUID, + top_k: data.top_k, }; httpClient .updateKnowledgeBase(initKbId, updateKb) @@ -127,6 +135,7 @@ export default function KBForm({ name: data.name, description: data.description, embedding_model_uuid: data.embeddingModelUUID, + top_k: data.top_k, }; httpClient .createKnowledgeBase(newKb) @@ -200,7 +209,7 @@ export default function KBForm({ }} value={field.value} > - + @@ -226,6 +235,30 @@ export default function KBForm({ )} /> + ( + + + {t('knowledge.topK')} + * + + + field.onChange(Number(e.target.value))} + className="w-[180px] h-10 text-base appearance-none" + /> + + + {t('knowledge.topKdescription')} + + + + )} + />
diff --git a/web/src/app/home/knowledge/page.tsx b/web/src/app/home/knowledge/page.tsx index 0a8cc2eb..85e99c56 100644 --- a/web/src/app/home/knowledge/page.tsx +++ b/web/src/app/home/knowledge/page.tsx @@ -46,6 +46,7 @@ export default function KnowledgePage() { name: kb.name, description: kb.description, embeddingModelUUID: kb.embedding_model_uuid, + top_k: kb.top_k ?? 5, lastUpdatedTimeAgo: lastUpdatedTimeAgoText, }); }), diff --git a/web/src/app/home/layout.module.css b/web/src/app/home/layout.module.css index 78a11beb..ad9725ca 100644 --- a/web/src/app/home/layout.module.css +++ b/web/src/app/home/layout.module.css @@ -7,6 +7,19 @@ background-color: #eee; } +:global(.dark) .homeLayoutContainer { + background-color: #0a0a0b; +} + +/* 侧边栏区域 */ +.sidebar { + background-color: #eee; +} + +:global(.dark) .sidebar { + background-color: #0a0a0b; +} + /* 主内容区域 */ .main { background-color: #fafafa; @@ -23,6 +36,11 @@ box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.05); } +:global(.dark) .main { + background-color: #151518; + box-shadow: 0 0 6px 0 rgba(255, 255, 255, 0.05); +} + .mainContent { padding: 1.5rem; padding-left: 2rem; @@ -30,3 +48,7 @@ overflow-y: auto; background-color: #fafafa; } + +:global(.dark) .mainContent { + background-color: #151518; +} diff --git a/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css b/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css index 9c6c54f7..9545619b 100644 --- a/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css +++ b/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css @@ -6,12 +6,22 @@ box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2); padding: 1.2rem; cursor: pointer; + transition: all 0.2s ease; +} + +:global(.dark) .cardContainer { + background-color: #1f1f22; + box-shadow: 0; } .cardContainer:hover { box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); } +:global(.dark) .cardContainer:hover { + box-shadow: 0; +} + .iconBasicInfoContainer { width: 100%; height: 100%; @@ -39,6 +49,11 @@ .basicInfoText { font-size: 1.4rem; font-weight: bold; + color: #1a1a1a; +} + +:global(.dark) .basicInfoText { + color: #f0f0f0; } .providerContainer { @@ -56,12 +71,20 @@ color: #626262; } +:global(.dark) .providerIcon { + color: #a0a0a0; +} + .providerLabel { font-size: 1.2rem; font-weight: 600; color: #626262; } +:global(.dark) .providerLabel { + color: #a0a0a0; +} + .baseURLContainer { display: flex; flex-direction: row; @@ -77,6 +100,10 @@ color: #626262; } +:global(.dark) .baseURLIcon { + color: #a0a0a0; +} + .baseURLText { font-size: 1rem; width: 100%; @@ -87,6 +114,10 @@ max-width: 100%; } +:global(.dark) .baseURLText { + color: #a0a0a0; +} + .bigText { white-space: nowrap; overflow: hidden; diff --git a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx index 18be1662..eee02b5a 100644 --- a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx +++ b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx @@ -417,7 +417,7 @@ export default function EmbeddingForm({ }} value={field.value} > - + @@ -492,7 +492,7 @@ export default function EmbeddingForm({ updateExtraArg(index, 'type', value) } > - + diff --git a/web/src/app/home/models/component/llm-card/LLMCard.module.css b/web/src/app/home/models/component/llm-card/LLMCard.module.css index b6d1ac6f..aedbebde 100644 --- a/web/src/app/home/models/component/llm-card/LLMCard.module.css +++ b/web/src/app/home/models/component/llm-card/LLMCard.module.css @@ -6,12 +6,22 @@ box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2); padding: 1.2rem; cursor: pointer; + transition: all 0.2s ease; +} + +:global(.dark) .cardContainer { + background-color: #1f1f22; + box-shadow: 0; } .cardContainer:hover { box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); } +:global(.dark) .cardContainer:hover { + box-shadow: 0; +} + .iconBasicInfoContainer { width: 100%; height: 100%; @@ -40,6 +50,11 @@ .basicInfoText { font-size: 1.4rem; font-weight: bold; + color: #1a1a1a; +} + +:global(.dark) .basicInfoText { + color: #f0f0f0; } .providerContainer { @@ -57,12 +72,20 @@ color: #626262; } +:global(.dark) .providerIcon { + color: #a0a0a0; +} + .providerLabel { font-size: 1.2rem; font-weight: 600; color: #626262; } +:global(.dark) .providerLabel { + color: #a0a0a0; +} + .baseURLContainer { display: flex; flex-direction: row; @@ -78,6 +101,10 @@ color: #626262; } +:global(.dark) .baseURLIcon { + color: #a0a0a0; +} + .baseURLText { font-size: 1rem; width: 100%; @@ -88,6 +115,10 @@ max-width: 100%; } +:global(.dark) .baseURLText { + color: #a0a0a0; +} + .abilitiesContainer { display: flex; flex-direction: row; @@ -108,18 +139,30 @@ background-color: #66baff80; } +:global(.dark) .abilityBadge { + background-color: rgba(34, 136, 238, 0.3); +} + .abilityIcon { width: 1rem; height: 1rem; color: #2288ee; } +:global(.dark) .abilityIcon { + color: #66baff; +} + .abilityLabel { font-size: 0.8rem; font-weight: 400; color: #2288ee; } +:global(.dark) .abilityLabel { + color: #66baff; +} + .bigText { white-space: nowrap; overflow: hidden; diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index f7c329e1..434e68a4 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -432,7 +432,7 @@ export default function LLMForm({ }} value={field.value} > - + @@ -565,7 +565,7 @@ export default function LLMForm({ updateExtraArg(index, 'type', value) } > - + diff --git a/web/src/app/home/models/page.tsx b/web/src/app/home/models/page.tsx index 2f936753..66099125 100644 --- a/web/src/app/home/models/page.tsx +++ b/web/src/app/home/models/page.tsx @@ -192,7 +192,7 @@ export default function LLMConfigPage() {
- + {t('llm.llmModels')} @@ -206,12 +206,14 @@ export default function LLMConfigPage() {
-

{t('llm.description')}

+

+ {t('llm.description')} +

-

+

{t('embedding.description')}

diff --git a/web/src/app/home/pipelines/PipelineDetailDialog.tsx b/web/src/app/home/pipelines/PipelineDetailDialog.tsx index 53cf9eaa..91b8b192 100644 --- a/web/src/app/home/pipelines/PipelineDetailDialog.tsx +++ b/web/src/app/home/pipelines/PipelineDetailDialog.tsx @@ -142,7 +142,7 @@ export default function PipelineDialog({ diff --git a/web/src/app/home/pipelines/components/debug-dialog/AtBadge.tsx b/web/src/app/home/pipelines/components/debug-dialog/AtBadge.tsx index 38a83d39..1ead0ab3 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/AtBadge.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/AtBadge.tsx @@ -15,13 +15,13 @@ export default function AtBadge({ return ( @{targetName} {!readonly && onRemove && ( diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index e29af1c3..71e5b748 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -302,14 +302,14 @@ export default function DebugDialog({ const renderContent = () => (
-
+
- +
{messages.length === 0 ? (
@@ -365,7 +365,7 @@ export default function DebugDialog({ 'max-w-md px-5 py-3 rounded-2xl', message.role === 'user' ? 'bg-[#2288ee] text-white rounded-br-none' - : 'bg-gray-100 text-gray-900 rounded-bl-none', + : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-none', )} > {renderMessageContent(message)} @@ -374,7 +374,7 @@ export default function DebugDialog({ 'text-xs mt-2', message.role === 'user' ? 'text-white/70' - : 'text-gray-500', + : 'text-gray-500 dark:text-gray-400', )} > {message.role === 'user' @@ -389,7 +389,7 @@ export default function DebugDialog({
-
+
{t('pipelines.debugDialog.streaming')} @@ -412,23 +412,25 @@ export default function DebugDialog({ ? t('pipelines.debugDialog.privateChat') : t('pipelines.debugDialog.groupChat'), })} - className="flex-1 rounded-md px-3 py-2 border border-gray-300 focus:border-[#2288ee] transition-none text-base" + className="flex-1 rounded-md px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 focus:border-[#2288ee] transition-none text-base" /> {showAtPopover && (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} > - + @webchatbot - {t('pipelines.debugDialog.atTips')}
@@ -459,7 +461,7 @@ export default function DebugDialog({ // 原有的Dialog包装 return ( - + {renderContent()} ); diff --git a/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css b/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css index 2ecbd44a..d2bfebca 100644 --- a/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css +++ b/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css @@ -10,12 +10,22 @@ flex-direction: row; justify-content: space-between; gap: 0.5rem; + transition: all 0.2s ease; +} + +:global(.dark) .cardContainer { + background-color: #1f1f22; + box-shadow: 0; } .cardContainer:hover { box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1); } +:global(.dark) .cardContainer:hover { + box-shadow: 0; +} + .basicInfoContainer { width: 100%; height: 100%; @@ -35,6 +45,11 @@ .basicInfoNameText { font-size: 1.4rem; font-weight: 500; + color: #1a1a1a; +} + +:global(.dark) .basicInfoNameText { + color: #f0f0f0; } .basicInfoDescriptionText { @@ -48,6 +63,10 @@ color: #b1b1b1; } +:global(.dark) .basicInfoDescriptionText { + color: #888888; +} + .basicInfoLastUpdatedTimeContainer { display: flex; flex-direction: row; @@ -58,11 +77,21 @@ .basicInfoUpdateTimeIcon { width: 1.2rem; height: 1.2rem; + color: #626262; +} + +:global(.dark) .basicInfoUpdateTimeIcon { + color: #a0a0a0; } .basicInfoUpdateTimeText { font-size: 1rem; font-weight: 400; + color: #626262; +} + +:global(.dark) .basicInfoUpdateTimeText { + color: #a0a0a0; } .operationContainer { @@ -86,12 +115,20 @@ color: #ffcd27; } +:global(.dark) .operationDefaultBadgeIcon { + color: #fbbf24; +} + .operationDefaultBadgeText { font-size: 1rem; font-weight: 400; color: #ffcd27; } +:global(.dark) .operationDefaultBadgeText { + color: #fbbf24; +} + .bigText { white-space: nowrap; overflow: hidden; diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 4d2dfbd5..7d26ad30 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -342,7 +342,7 @@ export default function PipelineFormComponent({ return ( <> -
+
{/* 按钮栏移到 Tabs 外部,始终固定底部 */} {showButtons && ( -
+
{isEditMode && !isDefaultPipeline && (